fix(confidence-engine): scope episode closure authority

This commit is contained in:
2026-09-01 08:55:38 +01:00
parent 0752c53a25
commit ab655e2222
3 changed files with 506 additions and 13 deletions
+68 -13
View File
@@ -2,6 +2,7 @@ import { describeGraph } from "./builder.js";
import {
countRemainingMaterialFactors,
hasRemainingMaterialFactors,
isDecisionClosureAuthorityQuestion,
isUserConfirmationOfNoRemainingUncertainty,
shouldCloseDecision,
} from "./decision-sufficiency.js";
@@ -4032,15 +4033,39 @@ export async function applyValidatedProposal({
);
// ── 60B.80 — normalise terminal parent closure without explicit confirmation
// legacy only: episode evidence does not supply a single-answer confirmation string;
// Done and Finding agree are NOT closure authority (settled architecture rule).
const ownershipChanges = isLegacySingleTurn
? reconcileDecisionClosureOwnership(
situationGraph,
reconciledProposal.proposal,
answer,
)
: { strippedUpdates: [], strippedResolvedIds: [] };
// legacy: use raw answer for explicit confirmation detection.
// episode: scan preserved paired turns for a QUALIFIED authority pair;
// question must grant decision-level closure authority AND
// answer must confirm no remaining uncertainty, SAME TURN.
let ownershipAnswer = answer;
if (!isLegacySingleTurn) {
const epTurns = evidenceContext?.isCompletedEpisode
? evidenceContext.episodeEvidence?.turns
: [];
if (Array.isArray(epTurns) && epTurns.length > 0) {
// Scan all turns — paired check on SAME turn only.
let explicitConfirmationAnswer = null;
for (const turn of epTurns) {
const tQuestion = turn?.question;
const tAnswer = turn?.answer;
if (
typeof tQuestion === "string" &&
typeof tAnswer === "string" &&
isDecisionClosureAuthorityQuestion(tQuestion) &&
isUserConfirmationOfNoRemainingUncertainty(tAnswer)
) {
explicitConfirmationAnswer = tAnswer;
break;
}
}
ownershipAnswer = explicitConfirmationAnswer ?? null;
}
}
const ownershipChanges = reconcileDecisionClosureOwnership(
situationGraph,
reconciledProposal.proposal,
ownershipAnswer,
);
if (ownershipChanges.strippedResolvedIds.length > 0) {
// Reconciler may have added selectedQuestion = null because the parent
// appeared resolved during reconciliation. If we stripped it, restore
@@ -4175,10 +4200,40 @@ export async function applyValidatedProposal({
? findNodeById(graphSnapshot, previousActiveUnknownNodeId)
: null;
const affectedNodeIds = buildAffectedNodeIds(graphSnapshot, proposalSnapshot);
// For episode evidence: extract turn text from the ordered Q/A sequence.
const _epTurns = evidenceContext?.isCompletedEpisode ? evidenceContext.episodeEvidence.turns : null;
const _derivedQuestion = isLegacySingleTurn ? previousQuestion : (_epTurns?.[0]?.question ?? "");
const _derivedAnswer = isLegacySingleTurn ? answer : (_epTurns?.[0]?.answer ?? "");
// For episode evidence: derive comparability override from the relevant Q/A pair,
// not from turns[0]. Scan preserved paired turns for a matching comparability question
// whose answer confirms comparability.
let _derivedQuestion = isLegacySingleTurn ? previousQuestion : "";
let _derivedAnswer = isLegacySingleTurn ? answer : "";
if (!isLegacySingleTurn) {
const epTurns = evidenceContext?.isCompletedEpisode
? evidenceContext.episodeEvidence?.turns
: [];
if (Array.isArray(epTurns)) {
// Scan all turns for a matching comparability Q/A pair.
let found = false;
for (const turn of epTurns) {
const tq = turn?.question;
const ta = turn?.answer;
if (
typeof tq === "string" &&
typeof ta === "string" &&
isComparabilityQuestion(tq) &&
answerConfirmsComparability(ta)
) {
_derivedQuestion = tq;
_derivedAnswer = ta;
found = true;
break;
}
}
if (!found) {
// No qualifying comparability pair — do not fabricate confirmation.
_derivedQuestion = "";
_derivedAnswer = "";
}
}
}
const reasoningResolution = deriveReasoningStateOverride({
graph: graphSnapshot,
previousQuestion: _derivedQuestion,
+39
View File
@@ -65,6 +65,45 @@ export function isUserConfirmationOfNoRemainingUncertainty(answer) {
return false;
}
// ── Pure: decision-closure authority question predicate ───────────
/**
* Determines whether a question explicitly asks the user whether any
* material uncertainty remains at the parent-decision level — i.e. the
* question grants closure authority when answered with confirmation.
*
* Bounded to the decision_threshold sufficiency-confirmation semantic family.
* Focused questions (supplier, budget, etc.) do NOT match even if they
* receive a confirming answer.
*
* Returns true only for:
* - "Is there anything else material that could change which option is better?"
* (the canonical product template)
* - Questions using the closure-family pattern:
* <remaining-uncertainty-phrase> + <closure-action-phrase>
* (e.g. "remaining material uncertainty preventing this decision from being closed")
*/
export function isDecisionClosureAuthorityQuestion(question) {
const text = String(question || "");
const lower = text.toLowerCase();
// Direct match for the canonical decision_threshold sufficiency confirmation template
if (/change which option (is |was )?better/i.test(lower)) return true;
// Closure-family pattern: "remaining-uncertainty" + "closure-action" conjunction
const hasRemainingUncertainty = /any(?:thing)?\s+(?:else\s+)?(?:material\s+)?uncertain(?:ty|ces)/i.test(lower);
if (!hasRemainingUncertainty) return false;
const hasClosureAction = /\b(closure|close[sd]?|resolve[d]?)\b/i.test(lower);
if (hasClosureAction) return true;
// "remaining material uncertainty" + decision-referencing words
const hasDecisionRef = /(?:decision|deciding|disposition)\b/i.test(lower);
if (hasDecisionRef) return true;
return false;
}
// ── Pure: unresolved predicate ─────────────────────────────────
/**