fix(confidence-engine): scope episode closure authority
This commit is contained in:
+68
-13
@@ -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,
|
||||
|
||||
@@ -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 ─────────────────────────────────
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user