/**
* 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 ”;
}
/**
* Utility: buildHeaders()
*
* Purpose:
* Build the standard headers required by the WP/MU AI proxy.
*
* Why:
* Keeps token/nonce handling consistent across all Layer A AI calls.
*
* Input:
* config β usually cpVars.insightPage.
*
* Output:
* Headers object for fetch().
*/
function buildHeaders(config) {
const headers = { ‘Content-Type’: ‘application/json’ };
if (config.token) headers[‘X-CP-Token’] = config.token;
else if (config.restNonce) headers[‘X-WP-Nonce’] = config.restNonce;
return headers;
}
/**
* Utility: callAiJson()
*
* Purpose:
* Send a Layer A prompt to the AI proxy and parse the response as JSON.
*
* Why:
* A1 requires structured output. This helper keeps the JSON parsing behaviour in one place.
*
* Input:
* config β AI endpoint configuration.
* payload β OpenAI-style messages payload.
*
* Output:
* Parsed JSON object.
*
* Failure:
* Throws if the network request fails or the model returns invalid JSON.
*/
async function callAiJson(config, payload) {
const res = await fetch(config.aiUrl, {
method: ‘POST’,
headers: buildHeaders(config),
body: JSON.stringify(payload)
});
if (!res.ok) throw new Error(`Layer A AI error ${res.status}`);
const json = await res.json();
const raw = clean(json.text || json.reply || json.content);
return JSON.parse(raw.replace(/“`json/g, ”).replace(/“`/g, ”).trim());
}
/**
* 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) {
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”,
“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 hostile, was clearly evasive, or moved off topic.
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.
– Humour, sarcasm or exaggeration should not automatically be treated as non-engagement.
– If a genuine answer is still present, classify as answered.
– If the user appears to be answering but the meaning is unclear, ambiguous or difficult to interpret, classify as clarification_required.
– If no genuine attempt to answer exists, classify as redirect_required.
– If the reply relates to the wider Insight but does not answer the last AI Coach question, classify as redirect_required.
– If unsure between answered and clarification_required, choose clarification_required.
– If unsure between clarification_required and redirect_required, choose clarification_required unless the user is clearly avoiding the question or is off-topic.`
},
{
role: ‘user’,
content:
`CURRENT INSIGHT QUESTION:
${insightQuestion}
LAST AI COACH QUESTION:
${lastCoachQuestion}
LATEST USER REPLY:
${userText}`
}
]
};
try {
const parsed = await callAiJson(config, payload);
const allowedStatus = [‘answered’, ‘clarification_required’, ‘redirect_required’];
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 res = await fetch(config.aiUrl, {
method: ‘POST’,
headers: buildHeaders(config),
body: JSON.stringify(payload)
});
const json = await res.json();
return clean(json.text || json.reply || json.content);
} 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 res = await fetch(config.aiUrl, {
method: ‘POST’,
headers: buildHeaders(config),
body: JSON.stringify(payload)
});
const json = await res.json();
return clean(json.text || json.reply || json.content);
} catch (err) {
console.log(‘LAYER A3 ERROR:’, err);
return fallbackQuestion;
}
}
/**
* Public Layer A API
*
* Exposes only the three Layer A operations:
* – A1 Good-Faith Answer Gate
* – A2 Clarification Writer
* – A3 Redirect Writer
*/
window.cpInsightLayerA = {
evaluateGoodFaithAnswer,
writeClarification,
writeRedirect
};
})();