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,