/**
* insight-layer-b.js
*
* Layer B – Understanding + Decisions
* ———————————–
*
* Purpose:
* Convert a user reply that has passed Layer A into structured coaching intelligence.
*
* Layer A has already established that the reply:
* – contains new material;
* – is relevant to the current question and Insight;
* – is suitable for Layer B evaluation.
*
* Layer B may also receive important additional context identified by Layer A.
* That context must influence the coach’s sensitivity and wording where appropriate,
* but must not automatically be treated as Insight evidence.
*
* Layer B answers:
* – What did we learn?
* – Is there something worth reflecting?
* – Are there trait signals?
* – Is anything useful for future Chapters?
* – What should happen next?
*
* Layer B must NOT:
* – write the reflection
* – write the next question
* – write the end-of-Insight review
* – directly write to the database
*
* Layer B produces decisions and payloads.
* The application handles storage.
* Layer C handles user-facing writing.
*/

(function () {
‘use strict’;

/**
* Utility: clean()
*
* Purpose:
* Normalise values into trimmed strings before prompt use.
*/
function clean(value) {
return String(value == null ? ” : value).trim();
}

/**
* B1 – Evidence Extraction
*
* Purpose:
* Extract what the user actually said into stable evidence categories.
*
* This stage observes.
* It should not interpret, reflect, assess traits, or decide progression.
*/
async function extractEvidence(ctx) {
const config = ctx.config || {};
const userText = clean(ctx.userText);

if (!config.aiUrl || !userText) {
return { evidence: [] };
}

const payload = {
model: config.aiModel,
messages: [
{
role: ‘system’,

content:
`You are B1 Evidence Extraction in a structured AI Coach system.

Layer A has already determined that the user’s latest reply contains new material
that is relevant to the current question and Insight and is suitable for Layer B.

Your single responsibility:
Extract factual coaching evidence from the Insight-relevant part of the user’s
latest answer.

Return ONLY valid JSON:
{
“evidence”: [
{
“category”: “belief|value|concern|pattern|tension|experience|goal|intention|emotion|insight”,
“statement”: “…”,
“quote”: “…”
}
]
}

Definitions:
– belief: something the user considers true.
– value: something the user considers important.
– concern: a worry, obstacle, risk, or issue.
– pattern: a recurring behaviour or tendency.
– tension: a conflict between priorities, needs, beliefs, or values.
– experience: something that has happened to the user or that the user has done.
– goal: an outcome the user wants to achieve.
– intention: something the user plans or intends to do.
– emotion: a feeling the user explicitly expresses.
– insight: a genuine realisation, newly recognised connection, or meaningful shift
in understanding that the user explicitly communicates. An ordinary opinion,
feeling, fact, concern, or description of behaviour is not an insight merely
because it is self-reflective.

Layer A context:
– Layer A may provide an assessment explaining why the reply passed.
– Layer A may also identify additional context that is important but is not the
main Insight answer.
– Use the Layer A assessment to understand which part of the reply answers the
current question.
– Treat additional context as background that may affect how the coach responds.
– Do not automatically extract additional context as Insight evidence.
– Extract additional context only when the user has also made it substantively
relevant to their answer, and only to the extent that it contributes to the
current Insight.
– Do not ignore evidence merely because Layer A also identified it as additional
context. The distinction depends on how the user used the information.

Rules:
– Use the supplied framework step, step objective, last AI Coach question and
Layer A result only to interpret the user’s latest reply.
– Do not extract evidence from the framework step, coach question or Layer A
wording themselves.
– Extract evidence only from what the user communicated.
– Stay close to the user’s words.
– Extract observations, not interpretations.
– Choose the category that best matches the user’s actual statement.
– Do not force evidence into a category that is only approximately correct.
– Use “insight” only when the user explicitly communicates a realisation, newly
recognised connection, changed perspective, or shift in understanding.
– Do not classify a simple feeling, belief, concern, experience, goal, intention
or recurring behaviour as an insight.
– When the same statement could reasonably be classified as both “insight” and
another category, use “insight” only when the realisation itself is the
important evidence.
– If the user’s reply depends on the previous question for its meaning, such as
“yes”, “the first one” or “not anymore”, use that context to understand it.
– A single reply may contain multiple evidence categories.
– Use exact user wording for quote where possible.
– If there is no useful evidence, return {“evidence”:[]}.
– Do NOT reassess whether the reply should have passed Layer A.
– Do NOT assess novelty.
– Do NOT decide reflection.
– Do NOT assess traits.
– Do NOT decide next step.`
},
{
role: ‘user’,
content:
`CURRENT FRAMEWORK STEP:
${clean(ctx.currentFrameworkStep)}

CURRENT STEP OBJECTIVE:
${clean(ctx.currentStepObjective)}

LAST AI COACH QUESTION:
${clean(ctx.lastCoachQuestion)}

LAYER A ASSESSMENT:
${clean(ctx.gate?.assessment)}

LAYER A ADDITIONAL CONTEXT:
${JSON.stringify(
ctx.gate?.additional_context || {
present: false,
summary: ”,
coach_note: ”
}
)}

LATEST USER REPLY:
${userText}`
}
]
};

let parsed = { evidence: [] };

try {
const aiResult = await window.cpAiService.callJson({
task: ‘layer_b_evidence_extraction’,
responseId: ctx.responseId,
config,
payload
});

if (!aiResult.ok) {
throw new Error(
aiResult.error?.message ||
‘Layer B1 AI request failed.’
);
}

parsed = aiResult.data || { evidence: [] };
} catch (err) {
console.log(‘LAYER B1 ERROR:’, err);
}

const allowedCategories = [
‘belief’,
‘value’,
‘concern’,
‘pattern’,
‘tension’,
‘experience’,
‘goal’,
‘intention’,
’emotion’,
‘insight’
];

const evidence = Array.isArray(parsed.evidence)
? parsed.evidence.filter(item =>
item &&
allowedCategories.includes(item.category) &&
clean(item.statement) &&
clean(item.quote)
)
: [];

return {
evidence
};
}

/**
* B1p – Progressive Evidence Update
*
* Purpose:
* Update provisional B1 evidence while the user is still composing.
*
* This uses the same evidence categories and observational standard as B1,
* but works incrementally: earlier provisional evidence is the starting
* state and only new or changed material should alter it.
*
* The result is provisional. Authoritative Send-time Layer B work remains
* separate and may finish after the next question has already been written.
*/
async function updateProgressiveEvidence(
ctx,
previousEvidenceResult = null,
previousCheckpointText = ”
) {
const config = ctx.config || {};
const currentText = clean(ctx.userText);
const previousText = clean(previousCheckpointText);

const allowedCategories = [
‘belief’,
‘value’,
‘concern’,
‘pattern’,
‘tension’,
‘experience’,
‘goal’,
‘intention’,
’emotion’,
‘insight’
];

const existingEvidence =
Array.isArray(previousEvidenceResult?.evidence)
? previousEvidenceResult.evidence
.filter(item =>
item &&
allowedCategories.includes(item.category) &&
clean(item.statement) &&
clean(item.quote)
)
.map(item => ({
category: item.category,
statement: clean(item.statement),
quote: clean(item.quote)
}))
: [];

if (!config.aiUrl || !currentText) {
return { evidence: existingEvidence };
}

if (currentText === previousText) {
return { evidence: existingEvidence };
}

const newMaterial =
previousText && currentText.startsWith(previousText)
? clean(currentText.slice(previousText.length))
: currentText;

const payload = {
model: config.aiModel,
messages: [
{
role: ‘system’,
content:
`You are B1 Progressive Evidence Update in a structured AI Coach system.

The user is still composing their answer. Your result is provisional.

Your single responsibility:
Update the existing provisional factual coaching evidence only where the user’s
new or changed material adds, qualifies, corrects, contradicts or supersedes it.

Use the same evidence categories as authoritative B1:
belief, value, concern, pattern, tension, experience, goal, intention, emotion, insight.

Return ONLY valid JSON:
{
“remove_indexes”: [0],
“replace”: [
{
“evidence_index”: 0,
“category”: “belief|value|concern|pattern|tension|experience|goal|intention|emotion|insight”,
“statement”: “…”,
“quote”: “…”
}
],
“add”: [
{
“category”: “belief|value|concern|pattern|tension|experience|goal|intention|emotion|insight”,
“statement”: “…”,
“quote”: “…”
}
]
}

Rules:
– Treat EXISTING PROVISIONAL EVIDENCE as the starting state.
– Assess NEW OR CHANGED MATERIAL in the context of the full current response,
previous checkpoint text, framework step and previous Coach question.
– Do not add unchanged earlier evidence again.
– If later wording qualifies or overturns earlier evidence, replace or remove it.
– Use remove_indexes and evidence_index values from the supplied existing evidence array.
– Stay close to the user’s words and extract observations, not interpretations.
– Use “insight” only for an explicitly communicated realisation, newly recognised
connection, changed perspective or meaningful shift in understanding.
– If the new material changes nothing in the provisional evidence, return empty arrays.
– Do not decide whether the completed response passes Layer A.
– Do not assess novelty.
– Do not decide reflection.
– Do not assess traits.
– Do not decide next step.`
},
{
role: ‘user’,
content:
`CURRENT FRAMEWORK STEP:
${clean(ctx.currentFrameworkStep)}

CURRENT STEP OBJECTIVE:
${clean(ctx.currentStepObjective)}

LAST AI COACH QUESTION:
${clean(ctx.lastCoachQuestion)}

PREVIOUS CHECKPOINT TEXT:
${previousText}

NEW OR CHANGED MATERIAL:
${newMaterial}

FULL CURRENT RESPONSE:
${currentText}

EXISTING PROVISIONAL EVIDENCE:
${JSON.stringify(
existingEvidence.map((item, index) => ({
evidence_index: index,
…item
}))
)}`
}
]
};

let parsed = {
remove_indexes: [],
replace: [],
add: []
};

try {
const aiResult = await window.cpAiService.callJson({
task: ‘layer_b_progressive_evidence_update’,
responseId: ctx.responseId,
config,
payload
});

if (!aiResult.ok) {
throw new Error(
aiResult.error?.message ||
‘Layer B1 progressive evidence request failed.’
);
}

parsed = aiResult.data || parsed;
} catch (err) {
console.log(‘LAYER B1 PROGRESSIVE ERROR:’, err);
return { evidence: existingEvidence };
}

const evidence = existingEvidence.map(item => ({ …item }));

if (Array.isArray(parsed.replace)) {
parsed.replace.forEach(item => {
const index = Number(item?.evidence_index);

if (!Number.isInteger(index)) return;
if (index < 0 || index >= evidence.length) return;
if (!allowedCategories.includes(item?.category)) return;

const statement = clean(item?.statement);
const quote = clean(item?.quote);

if (!statement || !quote) return;

evidence[index] = {
category: item.category,
statement,
quote
};
});
}

const removeIndexes = Array.isArray(parsed.remove_indexes)
? [
…new Set(
parsed.remove_indexes
.map(Number)
.filter(index =>
Number.isInteger(index) &&
index >= 0 &&
index < evidence.length ) ) ].sort((a, b) => b – a)
: [];

removeIndexes.forEach(index => {
evidence.splice(index, 1);
});

if (Array.isArray(parsed.add)) {
parsed.add.forEach(item => {
if (!item || !allowedCategories.includes(item.category)) return;

const statement = clean(item.statement);
const quote = clean(item.quote);

if (!statement || !quote) return;

const duplicate = evidence.some(existing =>
existing.category === item.category &&
clean(existing.statement).toLowerCase() ===
statement.toLowerCase() &&
clean(existing.quote).toLowerCase() ===
quote.toLowerCase()
);

if (duplicate) return;

evidence.push({
category: item.category,
statement,
quote
});
});
}

return { evidence };
}

/**
* B1a – Novelty Assessment
*
* Purpose:
* Decide whether extracted evidence is new, repeated, or materially developed.
*
* Materially developed means the user has expanded, deepened, clarified,
* or re-contextualised something already known.
*/
async function assessNovelty(ctx, evidenceResult) {
const config = ctx.config || {};
const evidence = evidenceResult?.evidence || [];
const acceptedEvidence = Array.isArray(ctx.acceptedEvidence)
? ctx.acceptedEvidence
: [];

if (!evidence.length) {
return { novelty: [] };
}

if (!acceptedEvidence.length) {
return {
novelty: evidence.map((_, index) => ({
evidence_index: index,
status: ‘new’,
reason: ‘No previously accepted evidence exists for comparison.’
}))
};
}

if (!config.aiUrl) {
return {
novelty: evidence.map((_, index) => ({
evidence_index: index,
status: ‘unknown’,
reason: ‘Novelty assessment was unavailable.’
}))
};
}

const payload = {
model: config.aiModel,
messages: [
{
role: ‘system’,
content:
`You are B1a Novelty Assessment.

Your single responsibility:
Assess the novelty of every evidence item supplied.

Return exactly one novelty assessment for each evidence item, using its position in the supplied evidence array.

Return ONLY valid JSON:
{
“novelty”: [
{
“evidence_index”: 0,
“status”: “new|repeated|materially_developed”,
“reason”: “…”
}
]
}

Definitions:
– new: not already present in accepted evidence.
– repeated: mainly restates something already known.
– materially_developed: significantly expands, deepens, clarifies, or re-contextualises a previous observation.

Rules:
– Return one novelty object for every supplied evidence item.
– Use the evidence_index to identify which evidence item is being assessed.
– Do not omit evidence items.
– Do not create additional novelty items.
– Do NOT write reflections.
– Do NOT assess traits.
– Do NOT decide next step.
– Use “materially_developed” only when the new evidence adds a meaningful new layer to previously accepted evidence of the same category.
– A materially developed item should deepen understanding, reveal a significant change, introduce an important new cause, consequence, exception or connection, or substantially extend what was previously known.
– Additional examples, minor rewording, stronger emotion, repetition, or extra detail alone are not materially developed.
– If uncertain whether something is repeated or materially developed, choose repeated.`
},
{
role: ‘user’,
content:
`NEW EVIDENCE:
${JSON.stringify(
evidence.map((e, index) => ({
evidence_index: index,
category: e.category,
statement: e.statement,
quote: e.quote
}))
)}

RELEVANT ACCEPTED EVIDENCE:
${JSON.stringify(acceptedEvidence)}`
}
]
};

let parsed = { novelty: [] };

try {
const aiResult = await window.cpAiService.callJson({
task: ‘layer_b_novelty_assessment’,
responseId: ctx.responseId,
config,
payload
});

if (!aiResult.ok) {
throw new Error(
aiResult.error?.message ||
‘Layer B1a AI request failed.’
);
}

parsed = aiResult.data || { novelty: [] };
} catch (err) {
console.log(‘LAYER B1a ERROR:’, err);
}

const allowedStatuses = [
‘new’,
‘repeated’,
‘materially_developed’,
‘unknown’
];

const evidenceCount = evidence.length;
const noveltyByIndex = new Map();

if (Array.isArray(parsed.novelty)) {
parsed.novelty.forEach(item => {

if (!item) return;

const index = Number(item.evidence_index);

if (!Number.isInteger(index)) return;
if (index < 0 || index >= evidenceCount) return;
if (!allowedStatuses.includes(item.status)) return;
if (noveltyByIndex.has(index)) return;

noveltyByIndex.set(index, {
evidence_index: index,
status: item.status,
reason: clean(item.reason)
});

});
}

const novelty = evidence.map((_, index) =>
noveltyByIndex.get(index) || {
evidence_index: index,
status: ‘unknown’,
reason: ‘No valid novelty assessment returned.’
}
);

return {
novelty
};
}

/**
* B2 – Reflection Assessment
*
* Purpose:
* Decide whether a reflection is justified and what it should be about.
*
* This stage decides WHAT may be reflected.
* Layer C decides HOW to write it.
*/
async function assessReflection(ctx, evidenceResult, noveltyResult) {
const config = ctx.config || {};
const evidence = evidenceResult?.evidence || [];

if (!config.aiUrl || !evidence.length) {
return {
reflection_required: false,
reflection_basis: ”,
relevant_evidence: []
};
}

const payload = {
model: config.aiModel,
messages: [
{
role: ‘system’,
content:
`You are B2 Reflection Assessment.

Your single responsibility:
Decide whether the user’s latest reply contains sufficiently valuable new understanding to justify a reflection, and identify the evidence that forms the basis of that reflection.

Base your decision primarily on evidence whose novelty status is “new” or “materially_developed”.

Repeated evidence alone should never justify a reflection.

Return ONLY valid JSON:
{
“reflection_required”: true|false,
“reflection_basis”: “…”,
“relevant_evidence”: [“…”]
}

Rules:
– Not every response requires a reflection.
– Consider only evidence whose novelty status is “new” or “materially_developed”.
– Ignore evidence whose novelty status is “repeated”.
– If every evidence item is repeated, do not generate a reflection.
– New evidence alone is not sufficient to justify a reflection.
– Produce a reflection only when the new or materially developed evidence meaningfully deepens understanding of the user, reveals a significant pattern, important connection, change in perspective, or valuable self-awareness.
– Minor additions, tentative comments, isolated facts, or slight elaborations should not trigger a reflection.
– If uncertain whether the new evidence is substantial enough, do not generate a reflection.
– Reflection basis should be concise.
– A reflection should highlight genuine new understanding rather than summarise what the user has already said previously.
– Do NOT write the reflection.
– Do NOT ask a question.
– Do NOT assess traits.
– If no reflection is justified, return:
{“reflection_required”:false,”reflection_basis”:””,”relevant_evidence”:[]}`
},
{
role: ‘user’,
content:
`EVIDENCE:
${JSON.stringify(evidence)}

NOVELTY:
${JSON.stringify(noveltyResult?.novelty || [])}

LATEST USER REPLY:
${clean(ctx.userText)}`
}
]
};

let parsed = {
reflection_required: false,
reflection_basis: ”,
relevant_evidence: []
};

try {
const aiResult = await window.cpAiService.callJson({
task: ‘layer_b_reflection_assessment’,
responseId: ctx.responseId,
config,
payload
});

if (!aiResult.ok) {
throw new Error(
aiResult.error?.message ||
‘Layer B2 AI request failed.’
);
}

parsed = aiResult.data || parsed;
} catch (err) {
console.log(‘LAYER B2 ERROR:’, err);
}

const reflectionBasis = clean(parsed.reflection_basis);

const relevantEvidence = Array.isArray(parsed.relevant_evidence)
? parsed.relevant_evidence
.map(item => clean(item))
.filter(Boolean)
: [];

const reflectionRequired =
parsed.reflection_required === true &&
!!reflectionBasis &&
relevantEvidence.length > 0;

return {
reflection_required: reflectionRequired,
reflection_basis: reflectionRequired ? reflectionBasis : ”,
relevant_evidence: reflectionRequired ? relevantEvidence : []
};
}

/**
* B3 – Trait Assessment
*
* Purpose:
* Assess whether the user’s answer provides evidence of movement
* in one or more Change Pathway traits.
*
* This stage produces storage recommendations and payloads.
* It does NOT directly write to the database.
*/
async function assessTraits(ctx, evidenceResult, noveltyResult) {
const config = ctx.config || {};
const evidence = evidenceResult?.evidence || [];

if (!config.aiUrl || !evidence.length) {
return { trait_updates: [] };
}

const payload = {
model: config.aiModel,
messages: [
{
role: ‘system’,
content:
`You are B3 Trait Assessment.

Traits:

– Awareness: recognising internal states, patterns, assumptions, effects, or relevant aspects of a situation more clearly.
– Balance: maintaining proportion between competing needs, demands, priorities, perspectives, or areas of life.
– Commitment: sustaining intention, responsibility, follow-through, or alignment with a chosen direction.
– Drive: applying energy, initiative, momentum, persistence, or purposeful effort towards an outcome.
– Engagement: participating actively, communicating, contributing, connecting, or remaining meaningfully involved.
– Flexibility: adapting thinking, behaviour, expectations, or approach in response to changing circumstances or new information.
– Groundedness: responding with steadiness, realism, perspective, emotional regulation, or connection to what is actually happening.
– Heart: showing empathy, compassion, care, courage, openness, or consideration for oneself or others.

Your single responsibility:
Assess whether the supplied user evidence provides evidence of trait movement.

Return ONLY valid JSON:
{
“trait_updates”: [
{
“trait”: “…”,
“direction”: “increase|decrease|caution”,
“confidence”: “low|medium|high”,
“evidence”: “…”,
“quote”: “…”,
“store_recommendation”: true|false
}
]
}

Rules:
– Be conservative.
– Do not infer personality.
– Use the supplied trait definitions rather than relying on broad or generic interpretations of the trait names.
– Select a trait only when the evidence directly matches its definition.
– Do not assign multiple traits merely because they are loosely related.
– Where several traits are plausible, return only the trait or traits most directly supported by the evidence.
– Use only user evidence.
– Layer A additional context must not itself produce a trait update.
– Do not infer trait movement from an event or circumstance merely because it is
important, difficult or emotionally significant.
– Additional context may support a trait assessment only when the user explicitly
connects it to a meaningful behavioural, attitudinal or motivational shift.
– Base any trait update primarily on evidence whose novelty status is “new” or
“materially_developed”.
– Do not create a new trait update from evidence whose novelty status is “repeated” unless the materially developed evidence itself justifies it.
– Recommend a trait update only when the evidence indicates a meaningful behavioural, attitudinal, or motivational shift relevant to that trait.
– Do not treat a single isolated behaviour, emotion, opinion, or event as evidence of lasting trait movement unless the user explicitly describes an underlying change, sustained pattern, or meaningful shift.
– Distinguish between describing behaviour and demonstrating development.
– Do not recommend updates from isolated remarks, speculation, or weak evidence.
– Return [] if the evidence does not justify a confident recommendation.
– Do NOT write to the database.`
},
{
role: ‘user’,
content:
`EVIDENCE:
${JSON.stringify(evidence)}

NOVELTY:
${JSON.stringify(noveltyResult?.novelty || [])}

CURRENT TRAIT STATUS:
${JSON.stringify(ctx.currentTraitStatus || {})}`
}
]
};

let parsed = { trait_updates: [] };

try {
const aiResult = await window.cpAiService.callJson({
task: ‘layer_b_trait_assessment’,
responseId: ctx.responseId,
config,
payload
});

if (!aiResult.ok) {
throw new Error(
aiResult.error?.message ||
‘Layer B3 AI request failed.’
);
}

parsed = aiResult.data || { trait_updates: [] };
} catch (err) {
console.log(‘LAYER B3 ERROR:’, err);
}

const allowedTraits = [
‘Awareness’,
‘Balance’,
‘Commitment’,
‘Drive’,
‘Engagement’,
‘Flexibility’,
‘Groundedness’,
‘Heart’
];

const allowedDirections = [
‘increase’,
‘decrease’,
‘caution’
];

const allowedConfidence = [
‘low’,
‘medium’,
‘high’
];

const seenTraits = new Set();

const traitUpdates = Array.isArray(parsed.trait_updates)
? parsed.trait_updates.reduce((items, item) => {
if (!item) return items;

const trait = clean(item.trait);
const direction = clean(item.direction);
const confidence = clean(item.confidence);
const evidence = clean(item.evidence);
const quote = clean(item.quote);

if (!allowedTraits.includes(trait)) return items;
if (!allowedDirections.includes(direction)) return items;
if (!allowedConfidence.includes(confidence)) return items;
if (!evidence || !quote) return items;
if (seenTraits.has(trait)) return items;

seenTraits.add(trait);

items.push({
trait,
direction,
confidence,
evidence,
quote,
store_recommendation: item.store_recommendation === true
});

return items;
}, [])
: [];

return {
trait_updates: traitUpdates
};
}

/**
* B4 – Future Chapter Assessment
*
* Purpose:
* Identify information that may be useful later when introducing
* future Chapters.
*
* This produces future-chapter storage recommendations.
* It does NOT directly write to the database.
*/
async function assessFutureChapters(ctx, evidenceResult, noveltyResult) {
const config = ctx.config || {};
const evidence = evidenceResult?.evidence || [];

if (!config.aiUrl || !evidence.length) {
return { future_chapter_items: [] };
}

const payload = {
model: config.aiModel,
messages: [
{
role: ‘system’,
content:
`You are B4 Future Chapter Assessment.

Your single responsibility:
Identify whether anything in the supplied user evidence should be added to or meaningfully enrich the Future Chapter Map.
The purpose of the Future Chapter Map is to preserve specific user statements that can later be quoted back to the user when they begin a genuinely related Chapter. It is not the user’s general coaching memory.
Do not recommend storing information that is already adequately represented in the supplied Future Chapter Map unless the new evidence materially enriches it.

Return ONLY valid JSON:
{
“future_chapter_items”: [
{
“chapter_key”: “…”,
“evidence”: “…”,
“quote”: “…”,
“reason”: “…”,
“store_recommendation”: true|false
}
]
}

Rules:
– Recommend storage only when quoting the user’s own words in a future Chapter would make that Chapter meaningfully more personal or connected.
– Ask yourself: “Will knowing this several months from now help the coach understand, personalise, or guide this user more effectively?”
– If the answer is probably no, do not recommend storing it.
– Never recommend the CURRENT CHAPTER shown below as a Future Chapter.
– Base recommendations only on what the user explicitly communicated. Do not infer interests, values, motivations or beliefs.
– Usually return either no recommendation or the single future Chapter that provides the clearest and most meaningful place to reconnect with the user’s own words.
– If several future Chapters are relevant, recommend the one that would create the strongest future coaching connection, not simply the first plausible match.
– Return more than one recommendation only when clearly different parts of the user’s reply naturally belong to different future Chapters.
– Prefer durable information such as recurring patterns, enduring beliefs,
important values, meaningful concerns, significant experiences, goals, or
persistent tensions.
– Prefer information that explains why the user thinks, feels, or behaves as they
do, rather than simply describing what happened.
– Layer A additional context must not be stored merely because it is important
in the present conversation.
– A recent event or circumstance should be stored only when the supplied evidence
reveals durable coaching knowledge that is likely to remain useful in future
Chapters.
– Do not store temporary moods, one-off events, passing preferences, casual
remarks, or facts that are unlikely to remain useful.
– Base storage recommendations primarily on evidence whose novelty status is “new” or “materially_developed”.
– Before recommending storage, compare the evidence with the supplied Future Chapter Map.
– If the same underlying coaching knowledge is already represented, do not recommend another item merely because the wording differs.
– Recommend storage only when the new evidence materially enriches, clarifies, or extends what is already stored.
– If uncertain whether the information will remain useful in future Chapters, do not recommend storing it.
– Do NOT write to the database.`
},
{
role: ‘user’,
content:
`EVIDENCE:
${JSON.stringify(evidence)}

NOVELTY:
${JSON.stringify(noveltyResult?.novelty || [])}

CURRENT CHAPTER:
${clean(ctx.currentChapterKey || ”)}

FUTURE CHAPTER MAP:
${JSON.stringify(ctx.futureChapterMap || {})}`
}
]
};

let parsed = {
future_chapter_items: []
};

try {
const aiResult = await window.cpAiService.callJson({
task: ‘layer_b_future_chapter_assessment’,
responseId: ctx.responseId,
config,
payload
});

if (!aiResult.ok) {
throw new Error(
aiResult.error?.message ||
‘Layer B4 AI request failed.’
);
}

parsed = aiResult.data || {
future_chapter_items: []
};
} catch (err) {
console.log(‘LAYER B4 ERROR:’, err);
}

const validChapterKeys = new Set(
Object.keys(ctx.futureChapterMap || {})
);

const seenItems = new Set();

const futureChapterItems = Array.isArray(parsed.future_chapter_items)
? parsed.future_chapter_items.reduce((items, item) => {
if (!item) return items;

const chapterKey = clean(item.chapter_key);
const evidence = clean(item.evidence);
const quote = clean(item.quote);
const reason = clean(item.reason);

if (!chapterKey || !evidence || !quote || !reason) {
return items;
}

if (
validChapterKeys.size > 0 &&
!validChapterKeys.has(chapterKey)
) {
return items;
}

const currentChapterKey =
clean(ctx.currentChapterKey);

if (
currentChapterKey &&
chapterKey === currentChapterKey
) {
return items;
}

const duplicateKey = [
chapterKey.toLowerCase(),
evidence.toLowerCase(),
quote.toLowerCase()
].join(‘|’);

if (seenItems.has(duplicateKey)) {
return items;
}

seenItems.add(duplicateKey);

items.push({
chapter_key: chapterKey,
evidence,
quote,
reason,
store_recommendation: item.store_recommendation === true
});

return items;
}, [])
: [];

return {
future_chapter_items: futureChapterItems
};
}

/**
* B5 – Response Strategy Assessment
*
* Purpose:
* Decide what should happen next.
*
* Possible outcomes:
* – next_step
* – follow_up
* – non_resonance
*
* Closure is not a separate strategy.
* When the final step is complete, the orchestrator moves to Layer D.
*/
async function assessResponseStrategyCore(ctx, evidenceResult, noveltyResult) {
const config = ctx.config || {};

if (!config.aiUrl) {
return {
response_strategy: ‘follow_up’,
reason: ‘No AI endpoint available.’,
follow_up_gap: ”
};
}

const payload = {
model: config.aiModel,
messages: [
{
role: ‘system’,
content:
`You are B5 Response Strategy Assessment.

Your single responsibility:
Decide what should happen next in the coaching flow.

Return ONLY valid JSON:
{
“response_strategy”: “next_step|follow_up|non_resonance”,
“reason”: “…”,
“follow_up_gap”: “…”
}

Decision principles:

Layer A has already established that the user’s reply is relevant enough to reach Layer B. Do not reassess that routing decision.

Your job is to use coaching judgement to choose the most coherent next move within this Insight.

Treat the framework as the intended coaching journey, not as a script. Respond to the meaning of what the user has contributed rather than whether they answered the previous question literally.

Choose next_step when moving forward from the current step is the most coherent continuation.

Choose follow_up when the user’s latest contribution opens or continues a genuinely productive line of enquiry that is likely to add materially new understanding relevant to the purpose of the current Insight.

A productive line of enquiry may continue across framework-step boundaries. Do not move on merely because the current step has already been sufficiently addressed if the user’s latest contribution has opened a valuable thread that still belongs within the purpose of the current Insight.

Continue the thread while each further exchange is materially developing the user’s understanding. When further exploration would mainly repeat, elaborate or confirm what is already understood, stop following that thread and use the cumulative framework coverage to determine what useful territory remains.

Layer A has supplied cumulative coverage for the current framework step as context:

– not_addressed: meaningful coverage of this step’s purpose has not yet been established;
– partially_addressed: meaningful coverage exists, but useful territory may remain;
– sufficiently_addressed: enough meaningful material has accumulated to serve the purpose of the step.

Coverage does not make this decision for you.

In particular, partially_addressed does not automatically mean follow_up. Decide whether a genuinely useful further exploration exists. If not, move forward.

Balance depth with progression. A meaningful answer does not automatically justify another follow-up, and the theoretical possibility of going deeper does not mean that doing so would improve the coaching.

Use the step objective and completion criteria to understand what the step is trying to achieve, not as a test the user must pass. Missing information does not itself justify another attempt to obtain it.

Use the NOVELTY assessment when judging whether another follow-up is productive:
– “new” or “materially_developed” evidence may provide a productive basis for further exploration.
– If the latest evidence is entirely “repeated”, do not choose follow_up merely because some desired example, experience, quality, explanation or other completion-criteria material is still missing.
– Repeated evidence can justify follow_up only when it genuinely opens a different and useful line of enquiry that has not already been pursued.
– Rewording a request for the same missing information is not a different line of enquiry.
– Changing the requested form — for example from an “experience” to an “example”, “skill”, “area”, “occasion” or “situation” — does not make the underlying enquiry new.

When considering another follow-up, use the previous AI Coach question and the conversation context represented by the evidence and novelty assessment. Stop pursuing a particular answer when further attempts are becoming repetitive or unproductive.

If the user has repeatedly communicated the same substantive position, treat that position as meaningful evidence about where the conversation currently is. Do not keep probing simply in the hope that the user will eventually provide the particular form of answer the framework originally anticipated.

Coaching progression should be forward-moving. Do not return to an earlier framework step or exhausted line of enquiry merely because the current one has stalled. Instead, continue productively within the current step, move to the next appropriate step, or choose non_resonance when genuinely justified.

Never progress into a framework question that depends on information the user has not established. Do not manufacture a missing experience, example, belief or other premise.

Choose non_resonance when the conversation provides genuine evidence that the Insight’s framing or direction is not useful or does not fit for the user.

Clear explicit rejection can justify non_resonance immediately. Explicit rejection means the user clearly states that the Insight’s framing, subject or direction is not useful, meaningful, relevant or appropriate for them, or clearly states that they do not want to explore it. Do not treat tentative or uncertain language as clear explicit rejection.

When the user expresses uncertainty about whether the Insight’s framing fits them, or describes a different way of approaching the subject, do not choose non_resonance from that alone. Treat this as possible non-resonance, not established non-resonance. If there is a reasonable way to work once with the perspective the user has actually offered while remaining within the Insight’s purpose, prefer that before concluding that the Insight is not resonating.

Less explicit non-resonance should be sustained across the conversation before choosing non_resonance. If, after that reasonable attempt, the user again communicates that the Insight’s framing does not fit, is not meaningful, or is not useful to them, treat the repeated position as evidence of non-resonance rather than finding another way to pursue the same Insight territory.

Distinguish disagreement within the Insight from rejection of the Insight’s framing. Uncertainty, difficulty answering, not having a particular experience, rejecting an assumed conclusion, or seeing the subject differently can still represent useful engagement. These do not by themselves justify non_resonance.

Do not infer non_resonance merely because the user did not answer the previous question in the particular form requested. A missing example, experience, explanation, quality or other unmet completion criterion is not evidence of non-resonance.

New or materially developed evidence is usually evidence of useful engagement, but first consider what that evidence means. If the new evidence itself expresses or strengthens rejection of the Insight’s framing or direction, do not treat its novelty as a reason to continue probing. It may instead be the evidence that justifies non_resonance.

Otherwise, when new or materially developed evidence opens or continues a productive line of enquiry relevant to the purpose of the current Insight, prefer follow_up and explore what the user actually contributed rather than repeatedly pursuing the particular form of answer previously requested.

Never choose one response strategy simply because another is unavailable. Choose each strategy because the conversation provides a positive reason for it.

Use the supplied step objective, completion criteria, allowed moves, disallowed moves and fallback routes as guidance and boundaries. They should inform coaching judgement rather than replace it.

Important:
– Do NOT write the next question.
– Do NOT write a reflection.
– Do NOT decide review content.
– Closure is not a separate response_strategy. If the Insight has reached its natural conclusion, use the most appropriate permitted strategy and allow the orchestrator to handle completion.`
},
{
role: ‘user’,
content:
`CURRENT FRAMEWORK STEP:
${clean(ctx.currentFrameworkStep)}

CURRENT STEP OBJECTIVE:
${clean(ctx.currentStepObjective)}

CURRENT STEP COVERAGE:
${clean(
ctx.stepCoverageMap?.[
String(Number(ctx.currentStepIndex || 0) + 1)
] || ‘not_addressed’
)}

STEP COMPLETION CRITERIA:
${JSON.stringify(ctx.stepCompletionCriteria || [])}

ALLOWED MOVES:
${JSON.stringify(ctx.allowedMoves || [])}

DISALLOWED MOVES:
${JSON.stringify(ctx.disallowedMoves || [])}

FALLBACK ROUTES:
${JSON.stringify(ctx.fallbackRoutes || {})}

IS FINAL FRAMEWORK STEP:
${ctx.isFinalFrameworkStep ? ‘YES’ : ‘NO’}

LAYER A ELIGIBILITY ASSESSMENT:
${clean(ctx.gate?.assessment)}

LAYER A ADDITIONAL CONTEXT:
${JSON.stringify(
ctx.gate?.additional_context || {
present: false,
summary: ”,
coach_note: ”
}
)}

EVIDENCE:
${JSON.stringify(evidenceResult?.evidence || [])}

NOVELTY:
${JSON.stringify(noveltyResult?.novelty || [])}

PREVIOUS AI COACH QUESTION:
${clean(ctx.lastCoachQuestion)}

LATEST USER REPLY:
${clean(ctx.userText)}`
}
]
};

let parsed = {
response_strategy: ‘follow_up’,
reason: ”,
follow_up_gap: ”
};

try {
const aiResult = await window.cpAiService.callJson({
task: ‘layer_b_response_strategy’,
responseId: ctx.responseId,
config,
payload
});

if (!aiResult.ok) {
throw new Error(
aiResult.error?.message ||
‘Layer B5 AI request failed.’
);
}

parsed = aiResult.data || parsed;
} catch (err) {
console.log(‘LAYER B5 ERROR:’, err);
}

const validStrategies = [
‘next_step’,
‘follow_up’,
‘non_resonance’
];

const requestedStrategy =
clean(parsed.response_strategy);

const reason =
clean(parsed.reason);

const followUpGap =
clean(parsed.follow_up_gap);

console.log(‘B5 RAW STRATEGY DEBUG:’, {
parsed,
requestedStrategy,
followUpGap,
followUpUsed: ctx.followUpUsed,
currentFrameworkStep: ctx.currentFrameworkStep,
fallbackRoutes: ctx.fallbackRoutes
});

let responseStrategy =
validStrategies.includes(requestedStrategy)
? requestedStrategy
: ”;

/*
* Follow-ups are governed by coaching judgement in B5.
*
* Do not block another follow-up merely because one has already been used.
* B5 decides whether continuing on the current step is still productive.
*
* A separate technical ceiling may be added later purely to prevent
* pathological loops, but it should not determine normal coaching strategy.
*/

/*
* Technical fallback only.
*
* If B5 does not return a usable strategy, remain on the current step
* rather than manufacturing completion or non-resonance.
*/
if (!responseStrategy) {
return {
response_strategy: ‘follow_up’,
reason:
‘No valid response strategy was returned, so the coach will remain on the current framework step.’,
follow_up_gap:
followUpGap || ‘A coherent continuation is still needed.’
};
}

return {
response_strategy: responseStrategy,
reason,
follow_up_gap:
responseStrategy === ‘follow_up’
? followUpGap
: ”
};
}

/*
* Authoritative B5 entry point.
*
* Used after Send. The coaching judgement itself lives only in
* assessResponseStrategyCore().
*/
async function assessResponseStrategy(
ctx,
evidenceResult,
noveltyResult
) {
return assessResponseStrategyCore(
ctx,
evidenceResult,
noveltyResult
);
}

/*
* Progressive B5 entry point.
*
* Used while the user is still composing. It deliberately uses the
* same B5 coaching intelligence as the authoritative Send path.
*
* The caller remains responsible for treating the result as provisional.
*/
async function assessProgressiveResponseStrategy(
ctx,
evidenceResult,
noveltyResult
) {
return assessResponseStrategyCore(
ctx,
evidenceResult,
noveltyResult
);
}

/**
* runLayerB()
*
* Purpose:
* Main entry point for Layer B.
*
* Called only after Layer A returns:
* eligible_for_layer_b = true.
*
* Layer B may use:
* – ctx.gate.assessment
* – ctx.gate.additional_context
*
* Additional context may affect the eventual coach response, but must not
* automatically be treated as Insight evidence, step completion, trait
* movement or durable Future Chapter material.
*
* Runs:
* – B1 Evidence Extraction
* – B1a Novelty Assessment
* – B2 Reflection Assessment
* – B3 Trait Assessment
* – B4 Future Chapter Assessment
* – B5 Response Strategy Assessment
*/

/**
* runLayerBProgressive()
*
* Fast user-facing Layer B path.
*
* B1 and B1a remain sequential.
* B2 reflection remains Personal-Coach Send-time work because it can affect
* the visible response. B3 traits and B4 Future Chapters do NOT run here:
* they are assessed once from accumulated evidence at the end of a Chapter.
*
* B5 remains a lazy defensive fallback requested only when Layer A #1 is
* unusable.
*/
async function runLayerBProgressive(
ctx,
precomputed = null
) {
/*
* B1 and B1a have one implementation each.
*
* When the orchestrator supplies results from a completed progressive
* checkpoint, reuse those results rather than calling the same
* intelligence again on Send.
*
* Otherwise run the normal authoritative functions.
*/
const evidence =
precomputed?.evidence &&
typeof precomputed.evidence === ‘object’ &&
Array.isArray(precomputed.evidence.evidence)
? precomputed.evidence
: await extractEvidence(ctx);

const novelty =
precomputed?.novelty &&
typeof precomputed.novelty === ‘object’ &&
Array.isArray(precomputed.novelty.novelty)
? precomputed.novelty
: await assessNovelty(ctx, evidence);

/*
* Personal Coach processing is entitlement-dependent.
*
* B2 reflection remains authoritative Send-time work because it can affect
* the visible response.
*
* B3 traits and B4 Future Chapters are deliberately absent from this path.
* They run once at the end of a Chapter from accumulated Chapter evidence.
*/
const hasPersonalCoach =
clean(ctx?.config?.aiCoachLevel).toLowerCase() === ‘personal’;

const reflectionPromise = hasPersonalCoach
? assessReflection(ctx, evidence, novelty)
: Promise.resolve({
reflection_required: false,
reflection_basis: ”,
relevant_evidence: []
});

/*
* Keep the existing pending result shape for orchestrator compatibility,
* but B3/B4 no longer start on every Send.
*/
const traitsPromise =
Promise.resolve({
trait_updates: []
});

const futureChaptersPromise =
Promise.resolve({
future_chapter_items: []
});

/*
* B5 is now a defensive fallback only.
*
* Layer A #1 owns normal framework destination selection, so do not
* start or await B5 on the normal user-facing path.
*
* If #1 is unusable, the orchestrator can request the same existing
* B5 intelligence through getFallbackStrategy().
*/
let fallbackStrategyPromise = null;

function getFallbackStrategy() {
if (!fallbackStrategyPromise) {
fallbackStrategyPromise =
precomputed?.strategy &&
typeof precomputed.strategy === ‘object’ &&
[
‘next_step’,
‘follow_up’,
‘non_resonance’
].includes(precomputed.strategy.response_strategy)
? Promise.resolve(precomputed.strategy)
: assessResponseStrategy(
ctx,
evidence,
novelty
);
}

return fallbackStrategyPromise;
}

return {
evidence,
novelty,
strategy: null,
getFallbackStrategy,

pending: {
reflection: reflectionPromise,
traits: traitsPromise,
futureChapters: futureChaptersPromise
},

debug: {
layer: ‘B’,
decision: ‘B1/B1a’,
value: ‘understanding ready’,
reason:
‘Evidence and novelty are ready; normal progression is owned by Layer A #1.’,
details: [
{
route: ‘B1’,
value: `${(evidence.evidence || []).length} evidence item(s)`,
reason: ‘Evidence extracted from user reply.’
},

…(evidence.evidence || []).map((item, index) => ({
route: `B1.${index + 1}`,
value: `${item.category}: ${item.statement}`,
reason: `Quote: “${item.quote}”`
})),

{
route: ‘B1a’,
value: `${(novelty.novelty || []).length} novelty item(s)`,
reason: ‘Novelty compared with accepted evidence.’
},

…(novelty.novelty || []).map((item, index) => ({
route: `B1a.${index + 1}`,
value: item.status,
reason:
`Evidence item ${Number(item.evidence_index) + 1}: ` +
`${item.reason || ‘No reason supplied.’}`
}))
]
}
};
}
/**
* assessChapterPersonalCoach()
*
* Run the existing B3 and B4 intelligence once at the end of a Chapter.
*
* The caller supplies accumulated accepted evidence from the Chapter.
* Evidence retained by the live coach is already limited to material that
* was new or materially developed when first accepted, so no second
* evidence-extraction pass is required here.
*/
async function assessChapterPersonalCoach(
ctx,
chapterEvidence = []
) {
const hasPersonalCoach =
clean(ctx?.config?.aiCoachLevel).toLowerCase() === ‘personal’;

if (!hasPersonalCoach) {
return {
traits: {
trait_updates: []
},
futureChapters: {
future_chapter_items: []
}
};
}

const allowedCategories = new Set([
‘belief’,
‘value’,
‘concern’,
‘pattern’,
‘tension’,
‘experience’,
‘goal’,
‘intention’,
’emotion’,
‘insight’
]);

const retainedEvidence =
Array.isArray(chapterEvidence)
? chapterEvidence.reduce((items, item) => {
if (!item || typeof item !== ‘object’) {
return items;
}

const category = clean(item.category);
const statement = clean(item.statement);
const quote = clean(item.quote);

if (
!allowedCategories.has(category) ||
!statement ||
!quote
) {
return items;
}

items.push({
category,
statement,
quote,
novelty_status:
clean(item.novelty_status) ===
‘materially_developed’
? ‘materially_developed’
: ‘new’
});

return items;
}, [])
: [];

if (!retainedEvidence.length) {
return {
traits: {
trait_updates: []
},
futureChapters: {
future_chapter_items: []
}
};
}

const evidence = {
evidence:
retainedEvidence.map(item => ({
category: item.category,
statement: item.statement,
quote: item.quote
}))
};

const novelty = {
novelty:
retainedEvidence.map((item, index) => ({
evidence_index: index,
status: item.novelty_status,
reason:
‘Accepted during this Chapter as new or materially developed evidence.’
}))
};

const [
traits,
futureChapters
] = await Promise.all([
assessTraits(ctx, evidence, novelty),
assessFutureChapters(ctx, evidence, novelty)
]);

return {
traits,
futureChapters
};
}

async function runLayerB(ctx) {
/*
* B1 and B1a must remain sequential because novelty depends
* on the evidence extracted by B1.
*/
const evidence = await extractEvidence(ctx);
const novelty = await assessNovelty(ctx, evidence);

/*
* The legacy full entry point follows the same lifecycle rule as the
* progressive path: B3/B4 are Chapter-end work, never per-turn work.
*/
const [
reflection,
strategy
] = await Promise.all([
assessReflection(ctx, evidence, novelty),
assessResponseStrategy(ctx, evidence, novelty)
]);

const traits = {
trait_updates: []
};

const futureChapters = {
future_chapter_items: []
};
return {
evidence,
novelty,
reflection,
traits,
futureChapters,
strategy,

debug: {
layer: ‘B’,
decision: ‘B5’,
value: strategy.response_strategy,
reason: strategy.reason,
details: [
{
route: ‘B1’,
value: `${(evidence.evidence || []).length} evidence item(s)`,
reason: ‘Evidence extracted from user reply.’
},

…(evidence.evidence || []).map((item, index) => ({
route: `B1.${index + 1}`,
value: `${item.category}: ${item.statement}`,
reason: `Quote: “${item.quote}”`
})),

{
route: ‘B1a’,
value: `${(novelty.novelty || []).length} novelty item(s)`,
reason: ‘Novelty compared with accepted evidence.’
},

…(novelty.novelty || []).map((item, index) => ({
route: `B1a.${index + 1}`,
value: item.status,
reason:
`Evidence item ${Number(item.evidence_index) + 1}: ` +
`${item.reason || ‘No reason supplied.’}`
})),

{
route: ‘B2’,
value: `reflection_required: ${!!reflection.reflection_required}`,
reason:
reflection.reflection_basis ||
‘No reflection basis identified.’
},

{
route: ‘B3’,
value: `${(traits.trait_updates || []).length} trait update(s)`,
reason:
(traits.trait_updates || []).length
? ‘Trait recommendations are listed below.’
: ‘No trait update was recommended.’
},

…(traits.trait_updates || []).map((item, index) => ({
route: `B3.${index + 1}`,
value:
`${item.trait} — ${item.direction} — ` +
`${item.confidence} confidence — ` +
`store: ${item.store_recommendation ? ‘yes’ : ‘no’}`,
reason:
`${item.evidence} Quote: “${item.quote}”`
})),

{
route: ‘B4’,
value:
`${(futureChapters.future_chapter_items || []).length} ` +
‘future chapter item(s)’,
reason:
(futureChapters.future_chapter_items || []).length
? ‘Future Chapter recommendations are listed below.’
: ‘No Future Chapter recommendation was made.’
},

…(futureChapters.future_chapter_items || []).map(
(item, index) => ({
route: `B4.${index + 1}`,
value:
`${item.chapter_key} — ` +
`store: ${item.store_recommendation ? ‘yes’ : ‘no’}`,
reason:
`${item.reason || ‘No reason supplied.’} ` +
`Evidence: ${item.evidence}. ` +
`Quote: “${item.quote}”`
})
)
]
}
};
}
/**
* Public Layer B API
*/
window.cpInsightLayerB = {
runLayerB,
runLayerBProgressive,
extractEvidence,
updateProgressiveEvidence,
assessNovelty,
assessReflection,
assessChapterPersonalCoach,
assessTraits,
assessFutureChapters,
assessResponseStrategy,
assessProgressiveResponseStrategy
};
})();