/**
* insight-layer-a.js
*
* Layer A β Gatekeeping
* ———————
* Responsibility:
* Decide whether the latest user reply is a genuine answer to the latest AI Coach question.
*
* Layer A answers only:
* βCan the coaching process continue from this user reply?β
*
* Layer A must NOT decide:
* – whether the framework step is complete
* – whether to move to the next framework step
* – whether to ask a follow-up question
* – whether to generate a reflection
* – whether to update traits
* – whether to store future chapter evidence
*
* Those decisions belong to Layer B.
*/
(function () {
‘use strict’;
/**
* Utility: clean()
*
* Purpose:
* Normalise any incoming value into a trimmed string.
*
* Why:
* Prevents null, undefined or whitespace-only values from causing inconsistent prompt behaviour.
*
* Input:
* Any value.
*
* Output:
* A trimmed string.
*/
function clean(value) {
return String(value == null ? ” : value).trim();
}
/**
* Utility: getLastCoachQuestion()
*
* Purpose:
* Find the most recent AI Coach question in the conversation thread.
*
* Why:
* A1 must judge the user’s reply against the actual last question asked, not against the wider Insight.
*
* Input:
* thread β the current conversation array.
*
* Output:
* The latest assistant question as a string, or an empty string if none is found.
*/
function getLastCoachQuestion(thread) {
for (let i = (thread || []).length – 1; i >= 0; i–) {
const msg = thread[i];
if (!msg || msg.role !== ‘assistant’) continue;
if (typeof msg.content === ‘object’) {
const q = clean(msg.content.question);
if (q) return q;
}
const q = clean(msg.content);
if (q) return q;
}
return ”;
}
/**
* A1 β Good-Faith Answer Gate
*
* Purpose:
* Decide whether the user’s latest reply is a genuine attempt to answer the previous AI Coach question.
*
* Called by:
* insight-orchestrator.js
*
* Inputs:
* ctx.config
* ctx.userText
* ctx.thread
* ctx.lastCoachQuestion
* ctx.insightQuestion
*
* Output:
* {
* status: ‘answered’ | ‘clarification_required’ | ‘redirect_required’,
* answered_question: boolean,
* within_insight: boolean,
* response_quality: string,
* reason: string
* }
*
* Allowed decisions:
* – Did the user genuinely answer?
* – Is clarification required?
* – Is redirection required?
*
* Forbidden decisions:
* – Step completion
* – Follow-up question
* – Reflection
* – Trait updates
* – Future chapter updates
* – Next-step progression
*
* Important:
* This replaces only the gatekeeping part of the old evaluateTurn().
* It deliberately does not return advance, fallback or close.
*/
async function evaluateGoodFaithAnswer(ctx) {
const config = ctx.config || {};
const userText = clean(ctx.userText);
const thread = ctx.thread || [];
if (!config.aiUrl) {
return {
status: ‘redirect_required’,
answered_question: false,
within_insight: true,
response_quality: ‘unknown’,
reason: ‘No AI endpoint available.’,
debug: {
layer: ‘A’,
route: ‘A1’,
value: ‘redirect_required’,
reason: ‘No AI endpoint available.’
}
};
}
const lastCoachQuestion = clean(ctx.lastCoachQuestion || getLastCoachQuestion(thread));
const insightQuestion = clean(ctx.insightQuestion);
const payload = {
model: ‘gpt-4o’,
temperature: 0,
messages: [
{
role: ‘system’,
content:
`You are A1 Good-Faith Answer Gate in a structured AI Coach dialogue.
Your single responsibility:
Decide whether the user’s latest reply is a genuine answer to the last AI Coach question.
Return ONLY valid JSON in this exact form:
{
“status”: “answered|clarification_required|redirect_required|boundary_set”,
“answered_question”: true|false,
“within_insight”: true|false,
“response_quality”: “good_faith|partial|unclear|symbolic|playful_evasive|non_answer|hostile|off_topic”,
“reason”: “short reason”
}
Definitions:
– answered: the user made a genuine attempt to answer the last AI Coach question.
– clarification_required: the user appears to be trying to answer, but the meaning is incomplete, unclear, ambiguous, or symbolic.
– redirect_required: the user did not genuinely engage with the question, was clearly evasive, or moved off topic..
– boundary_set: the user clearly and respectfully declines to answer, says the subject is too personal or uncomfortable, or asks to approach it differently.
Rules:
– Do NOT judge depth, insightfulness, emotional disclosure, or usefulness.
– Do NOT decide whether the framework step is complete.
– Do NOT decide whether to move to the next step.
– Do NOT decide whether to reflect.
– A brief answer can still be a genuine answer.
– A short answer should be classified as answered when it gives a clear, relevant response to the last question. Do not require explanation, detail, examples, or justification at Layer A.
– Humour, sarcasm or exaggeration should not automatically be treated as non-engagement.
– When humour, sarcasm or exaggeration contains a clear underlying answer, classify the reply as answered.
– Classify humour as playful_evasive only when it replaces the answer rather than expressing one.
– Hostile, angry, dismissive or confrontational wording should not automatically be treated as non-engagement.
– If a hostile or angry reply still contains a clear answer or position, classify it as answered and use response_quality “hostile”.
– Classify a hostile reply as redirect_required only when the hostility replaces any genuine answer, boundary or request for clarification.
– If a genuine answer is still present, classify as answered.
– If the reply contains both a genuine answer and additional unrelated or tangential material, classify it as answered. Ignore the tangent for the purposes of Layer A.
– If the user appears to be answering but the meaning is unclear, ambiguous or difficult to interpret, classify as clarification_required.
– If the user asks for the previous question to be explained, clarified or rephrased because they do not understand it, classify as clarification_required.
– A figurative, metaphorical or symbolic reply should be classified as answered if its meaning is reasonably clear in ordinary language. Only classify it as clarification_required when the intended meaning cannot be understood with reasonable confidence.
– If the user clearly and respectfully declines to answer, says the subject is too personal or uncomfortable, or asks to approach it differently, classify as boundary_set.
– Do not classify a clear personal boundary as evasion, hostility, ambiguity, or a non-answer.
– If no genuine attempt to answer exists and no personal boundary has been expressed, classify as redirect_required.
– A reply that challenges, questions or rejects the assumptions or framing of the AI Coach question should still be classified as answered if it clearly communicates the user’s position.
– If the reply relates to the wider Insight but neither answers the last AI Coach question, expresses a clear position about its assumptions or framing, nor expresses a personal boundary, classify as redirect_required.
– A single reply may contain elements of more than one status.
– Consider the reply as a whole rather than isolated words or phrases.
– Choose the single status that best represents the user’s primary communicative intent.
– Do not use a fixed precedence order or count keywords.
– If the reply clearly answers the question while also containing humour, frustration, hostility, disagreement or a minor tangent, classify it as answered.
– If the reply is primarily seeking clarification, classify it as clarification_required even if it contains a tentative or partial answer.
– If the reply is primarily expressing a personal boundary or declining to answer, classify it as boundary_set even if it also asks for clarification or comments on the question.
– Use redirect_required only when the dominant communicative intent is neither a genuine answer, a request for clarification, nor a personal boundary.
– If, after considering the whole reply, the dominant communicative intent still cannot be determined with reasonable confidence, choose clarification_required rather than guessing.`
},
{
role: ‘user’,
content:
`CURRENT INSIGHT QUESTION:
${insightQuestion}
LAST AI COACH QUESTION:
${lastCoachQuestion}
LATEST USER REPLY:
${userText}`
}
]
};
try {
const aiResult = await window.cpAiService.callJson({
task: ‘layer_a_good_faith_gate’,
responseId: ctx.responseId,
config,
payload
});
if (!aiResult.ok) {
throw new Error(
aiResult.error?.message ||
‘Layer A1 AI request failed.’
);
}
const parsed = aiResult.data;
const allowedStatus = [
‘answered’,
‘clarification_required’,
‘redirect_required’,
‘boundary_set’
];
const allowedQuality = [
‘good_faith’,
‘partial’,
‘unclear’,
‘symbolic’,
‘playful_evasive’,
‘non_answer’,
‘hostile’,
‘off_topic’
];
const status = allowedStatus.includes(parsed.status)
? parsed.status
: ‘redirect_required’;
return {
status,
answered_question: !!parsed.answered_question,
within_insight: parsed.within_insight !== false,
response_quality: allowedQuality.includes(parsed.response_quality)
? parsed.response_quality
: ‘unclear’,
reason: clean(parsed.reason),
debug: {
layer: ‘A’,
route: ‘A1’,
value: status,
reason: clean(parsed.reason)
}
};
} catch (err) {
console.log(‘LAYER A1 ERROR:’, err);
return {
status: ‘redirect_required’,
answered_question: false,
within_insight: true,
response_quality: ‘unknown’,
reason: ‘A1 failed; safely redirecting.’,
debug: {
layer: ‘A’,
route: ‘A1’,
value: ‘redirect_required’,
reason: ‘A1 failed’
}
};
}
}
/**
* A2 β Clarification Writer
*
* Purpose:
* Ask for clarification when the user appears to be answering but the meaning is unclear.
*
* Called when:
* A1 returns status = ‘clarification_required’.
*
* Inputs:
* ctx.userText
* ctx.lastCoachQuestion
* ctx.thread
* gate β the A1 result.
*
* Output:
* One short clarification question.
*
* Must not:
* – coach
* – reflect
* – analyse
* – progress the framework
* – introduce a new topic
*/
async function writeClarification(ctx, gate) {
const config = ctx.config || {};
if (!config.aiUrl) return ‘Could you say a little more clearly what you mean?’;
const payload = {
model: ‘gpt-4o’,
temperature: 0.2,
messages: [
{
role: ‘system’,
content:
`You are A2 Clarification Writer.
Write ONE short clarification question.
Rules:
– The user appears to be trying to answer, but the meaning is unclear, incomplete, ambiguous, or symbolic.
– Ask only for the missing clarity.
– Do not coach.
– Do not analyse.
– Do not reflect.
– Do not introduce a new topic.
– Use UK English.`
},
{
role: ‘user’,
content:
`LAST AI COACH QUESTION:
${clean(ctx.lastCoachQuestion || getLastCoachQuestion(ctx.thread || []))}
LATEST USER REPLY:
${clean(ctx.userText)}
A1 RESULT:
${JSON.stringify(gate || {})}`
}
]
};
try {
const aiResult = await window.cpAiService.callText({
task: ‘layer_a_clarification_writer’,
responseId: ctx.responseId,
config,
payload
});
if (!aiResult.ok) {
throw new Error(
aiResult.error?.message ||
‘Layer A2 AI request failed.’
);
}
return clean(aiResult.data) ||
‘Could you say a little more clearly what you mean?’;
} catch (err) {
console.log(‘LAYER A2 ERROR:’, err);
return ‘Could you say a little more clearly what you mean?’;
}
}
/**
* A3 β Redirect Writer
*
* Purpose:
* Redirect the user back to the current AI Coach question when they have not genuinely answered.
*
* Called when:
* A1 returns status = ‘redirect_required’.
*
* Inputs:
* ctx.userText
* ctx.lastCoachQuestion
* ctx.thread
* gate β the A1 result.
*
* Output:
* One redirect question.
*
* Must not:
* – criticise the user
* – analyse their behaviour
* – generate a reflection
* – follow the tangent
* – progress the framework
*/
async function writeRedirect(ctx, gate) {
const config = ctx.config || {};
const fallbackQuestion = clean(ctx.lastCoachQuestion || getLastCoachQuestion(ctx.thread || []));
if (!config.aiUrl) return fallbackQuestion;
const payload = {
model: ‘gpt-4o’,
temperature: 0.2,
messages: [
{
role: ‘system’,
content:
`You are A3 Redirect Writer.
Write ONE question that redirects the user back to the current AI Coach question.
Rules:
– Be calm, direct, and respectful.
– Do not criticise the user.
– Do not analyse their behaviour.
– Do not generate a reflection.
– Do not follow the user’s tangent.
– Re-ask the last question in simpler or clearer wording.
– Use UK English.`
},
{
role: ‘user’,
content:
`LAST AI COACH QUESTION:
${fallbackQuestion}
LATEST USER REPLY:
${clean(ctx.userText)}
A1 RESULT:
${JSON.stringify(gate || {})}`
}
]
};
try {
const aiResult = await window.cpAiService.callText({
task: ‘layer_a_redirect_writer’,
responseId: ctx.responseId,
config,
payload
});
if (!aiResult.ok) {
throw new Error(
aiResult.error?.message ||
‘Layer A3 AI request failed.’
);
}
return clean(aiResult.data) || fallbackQuestion;
} catch (err) {
console.log(‘LAYER A3 ERROR:’, err);
return fallbackQuestion;
}
}
/**
* A4 β Boundary Writer
*
* Purpose:
* Respect a user’s stated boundary without pressing them to disclose more.
*/
async function writeBoundaryResponse(ctx, gate) {
const config = ctx.config || {};
if (!config.aiUrl) {
return ‘That is completely fine. What aspect of this would feel useful to explore instead?’;
}
const payload = {
model: ‘gpt-4o’,
temperature: 0.2,
messages: [
{
role: ‘system’,
content:
`You are A4 Boundary Writer.
The user has clearly expressed a personal boundary or declined to answer the previous question.
Write one brief response that:
– acknowledges the boundary without judgement;
– does not ask the user to explain or justify it;
– does not repeat or rephrase the declined question;
– does not imply that disclosure is necessary;
– offers a safe, relevant alternative way to continue with the Insight;
– uses UK English.
Return only the response shown to the user.`
},
{
role: ‘user’,
content:
`LAST AI COACH QUESTION:
${clean(ctx.lastCoachQuestion || getLastCoachQuestion(ctx.thread || []))}
LATEST USER REPLY:
${clean(ctx.userText)}
A1 RESULT:
${JSON.stringify(gate || {})}`
}
]
};
try {
const aiResult = await window.cpAiService.callText({
task: ‘layer_a_boundary_writer’,
responseId: ctx.responseId,
config,
payload
});
if (!aiResult.ok) {
throw new Error(
aiResult.error?.message ||
‘Layer A4 AI request failed.’
);
}
return clean(aiResult.data) ||
‘That is completely fine. What aspect of this would feel useful to explore instead?’;
} catch (err) {
console.log(‘LAYER A4 ERROR:’, err);
return ‘That is completely fine. What aspect of this would feel useful to explore instead?’;
}
}
/**
* Public Layer A API
*
* Exposes the four Layer A operations:
* – A1 Good-Faith Answer Gate
* – A2 Clarification Writer
* – A3 Redirect Writer
* – A4 Boundary Writer
*/
window.cpInsightLayerA = {
evaluateGoodFaithAnswer,
writeClarification,
writeRedirect,
writeBoundaryResponse
};
})();