25 KiB
Experiment 60B.62 — Decision Closure Integration Boundary
Status: PASSED (design-only, no production code changes)
Objective
Identify the exact deterministic integration point in applyValidatedProposal and the exact existing representation of the user's "no other material uncertainties remain" statement that can safely trigger parent-decision closure, without relying on the model to emit the parent-resolution update.
Answer: Model C (graph sufficiency + bounded user-confirmation signal) integrates at Candidate D (post-propagation).
Context Route Traced
The full applyValidatedProposal lifecycle was traced line-by-line across 150+ lines of apply-proposal.js:
- Line 3537 —
reconcileResolutionSemantics(graph, proposal)— reconciles bidirectional resolution semantics before validation - Line 3635 — proposal compatibility errors (blocking)
- Line 3664 / 3694 — two
applyGraphUpdatecalls (first provisional for emergent-reasoning pass, second final) - Line 3715–3741 — activeUnknownNodeId determination (pre-decomposition)
- Line 3748 —
runDeterministicDecomposition - Line 3765 —
propagateResolvedChildEvidence(decomposition-child upward propagation only) - Line 3996–4019 — model-selection honour for proposed target
- Line 4049–4203 — question formulation and reseat logic
- Line 4285 — return with full result object
The answer parameter is available at every point in the function as a direct argument and through proposalSnapshot.answerMeaning. The raw string passes through unchanged from orchestrator line 622 → applyValidatedProposal(3481) line-by-line.
Checkpoint 1 — User-Confirmation Signal Assessment
| Field | Available Before Mutation | Model Generated | Safe as Deterministic Confirmation | Why |
|---|---|---|---|---|
answer (raw string) |
YES — direct param | PARTIAL | CONDITIONAL | No bounded helper exists. Free-text interpretation needed to detect "no other material differences" pattern. |
proposal.answerMeaning.userSupportedMeaning |
YES — after validation phase | MODEL GENERATED | NO | This is model-extracted meaning, not the raw user statement. The LLM determines its content. |
updatedNodes[].reason (for any updated decision node) |
YES — exists post-mutation | MODEL GENERATED | CONDITIONAL | If reason contains explicit closure language like "no other material uncertainties remaining", it can serve as a bounded confirmation signal without schema changes. This is the most reliable existing proxy because: (a) it already exists in every update, (b) the prompt already instructs the model to state closure rationale, (c) the exact 60B.56 proposal includes reason: "With customer signing confirmed and no other material uncertainties remaining, the decision is closed." — a naturally bounded pattern from the same prompt that produces the issue. |
proposal.answerMeaning.resolutionGuidance |
YES — after validation | MODEL GENERATED | CONDITIONAL | If set to must_resolve, it implies the model determined the decision should close. But this field is null in many valid proposals (prompt rule #32 allows null). |
selectedQuestion |
YES | MODEL GENERATED | NO | A non-null selectedQuestion targeting a terminal node means the model didn't decide to close. null selectedQuestion can mean either "nothing remains" or "model forgot to produce one." Not deterministic. |
propagationResult.parentResolved |
YES — post-propagation | PARTIAL (code-driven) | CONDITIONAL | Only fires for decomposition-child propagation via parentId, NOT for general sufficiency across all routes (A–D). Cannot detect customer-signing → decision closure because that factor attaches via contained_in, not as a direct decomposition child. |
Winning signal: updatedNodes[].reason on the parent decision update, combined with hasRemainingMaterialFactors(decisionId, graph) === false. This requires no schema change and leverages bounded text already produced by the model prompt for existing rule #27/decision-sufficiency-rule purposes.
Checkpoint 2 — Raw User Statement vs Model Interpretation
Can production code access the original user answer directly at the closure-integration point:
YES — `answer` parameter is available at line 3694 (post-second applyGraphUpdate) and every subsequent line through line 4402.
Can it access a normalized userSupportedMeaning:
YES — `validatedProposal.answerMeaning.userSupportedMeaning` is available after the validation phase (line 3537+).
Which is safer for the narrow confirmation:
BOTH — raw answer provides ground-truth input; userSupportedMeaning provides model-classified meaning. Neither alone gives a deterministic "no remaining material factors" signal without free-text interpretation.
Critical distinction: neither can serve as a deterministic confirmation signal without bounded text matching. The updatedNodes[].reason field is safer than raw answer because it is already structured to contain the model's closure rationale, and the exact 60B.56 case shows the pattern "no other material uncertainties remaining" appearing naturally in this field.
Checkpoint 3 — Lifecycle Candidate Assessment
Candidate A — reconciliation phase (inside reconcileResolutionSemantics)
- Post-answer graph available: NO — proposal not yet applied to graph; factor states are only in
updatedNodes[].newStatus, not reflected in the live graph nodes. - Can safely close: NO — no resolved state is reflected in
graph.nodesuntil applyGraphUpdate runs at line 3694. hasRemainingMaterialFactors would read stale pre-answer graph. - Validation risk: HIGH — this is the validation phase; any mutation here bypasses the compatibility checks entirely.
- Stale-question risk: MEDIUM — selectedQuestion not yet reconciled in reconcileResolutionSemantics (line 370 only handles post-sync clearing).
- Principal weakness: Graph does not contain the resolved factor state at this point.
Candidate B — pre-mutation validation phase (after validation, before applyGraphUpdate)
- Post-answer graph available: NO — same issue; the second
applyGraphUpdatehas not yet run. - Can safely close: NO — resolved unknown IDs are in proposalSnapshot but graph.nodes still show stale status values.
- Validation risk: HIGH — would need to mutate before the compatibility checks at lines 3617–3633 complete.
- Stale-question risk: LOW — pre-mutation.
- Principal weakness: Same as A — no mutation has occurred yet; graph reflects pre-answer state.
Candidate C — immediately after mutation (after line 3694, before decomposition)
- Post-answer graph available: YES —
updatedSituationGraphexists at line 3703+ with all updated node statuses reflected. - hasRemainingMaterialFactors can evaluate correct final state: YES — hasRemainingMaterialFactors reads directly from
graph.nodeswhich now contain the post-mutation status values (e.g., customer unknown showsstatus: "resolved"). - Can safely mutate parent decision here: CONDITIONAL — yes, but premature because decomposition may add new unresolved factors that should block closure. A factor resolved this turn could be immediately counteracted by a newly-added unknown in the same proposal.
- Validation risk: LOW — mutations are past validation.
- Stale-question risk: MEDIUM — question not yet formulated; would need to suppress it.
- Principal weakness: Decomposition may add new unresolved factors in the same turn that should prevent closure. The post-mutation graph at this point does not reflect decomposition changes.
Candidate D — after propagateResolvedChildEvidence (post-line 3765, before active-target selection)
- Post-answer graph available: YES — full post-mutation graph including decomposition-added nodes.
- hasRemainingMaterialFactors can evaluate correct final state: YES — all resolved states are reflected: customer factor shows
resolved, any decomposed-new unknowns are present in graph.nodes, and propagation's upward changes (if any) are applied to ancestor nodes. - Can safely mutate parent decision here: YES — this is the exact point where decomposition effects are settled but before final-question-selection locks the next question target. The graph contains the complete post-answer state.
- Validation risk: LOW — past all validation phases. Existing validator chain completes at line 3635; subsequent logic is post-validation.
- Stale-question risk: LOW —
propagationResult.parentResolvedalready exists here but only fires for decomposition-child propagation (parentId), not for general sufficiency. By placing the new check immediately after line 3765–3775, we intercept beforeselectActiveUnknownCandidateruns at lines 3743/3799 which would re-target an already-closed decision. - Principal weakness: None significant. This is the narrowest safe insertion point that sees the complete post-answer graph state after all structural changes (mutation + decomposition) have settled but before any question-selection locks targets.
Candidate E — active-target selection phase (lines 3996–4044)
- Post-answer graph available: YES
- hasRemainingMaterialFactors can evaluate correct final state: YES
- Can safely mutate parent decision here: CONDITIONAL — the window is narrow because model-selection honour (line 3996) may have already set
deterministicSelectionto a specific node. If hasRemainingMaterialFactors === false, we must override this selection AND clear selectedQuestion simultaneously. This adds branching complexity around existing selection logic. - Validation risk: MEDIUM — interfering with model-selection honour creates a dependency on the decision between Model A (graph only) and Model C (graph + confirmation). The selection-honour logic at line 3996 is itself a correction from 60B.11/60B.12; adding sufficiency-based override on top increases fragility.
- Stale-question risk: HIGH — question formulation has already started at line 4049; clearing would require additional nullification logic.
- Principal weakness: Too late in the pipeline — model-selection honour logic and question formulation are intertwined; interrupting them for closure introduces cascading rework of existing corrections.
Winner: Candidate D — post-propagation. This is the narrowest integration point that (a) sees complete post-answer graph state, (b) avoids interference with validation or decomposition, and (c) can prevent downstream active-target selection without complex override logic.
Checkpoint 4 — Closure Mutation Semantics
Preferred existing terminal status: resolved
Why:
- The exact 60B.43/60B.56 tests use
newStatus: "resolved"for the parent decision (test at apply-proposal.test.js:4745). This is the canonical closure status for decisions that have sufficient evidence. "known"is used for option-level results (e.g., launchThisYear, waitTwelveMonths) and appears in the 60B.43 test only asstatus: "known"for options, not the decision itself.- Both are terminal statuses excluded by
TERMINAL_STATUSES = ["known", "resolved", "contradicted"]. However,"resolved"carries semantic meaning of "evidence-sufficient resolution" while"known"carries "observation/assessment completed." For a decision that closes because all factors resolved,"resolved"is the established convention.
Decision ID added to resolvedNodeIds/resolvedUnknownNodeIds:
YES — conditionally required. Without this, selectActiveUnknownCandidate (which excludes only resolvedNodeIds at utils.js:596) would still consider the decision as a candidate if it survives in the graph with kind: "unknown" and no status filter beyond what's already there. The existing pattern in propagateResolvedChildEvidence line 974-975 (ensureResolvedUnknownId(proposalSnapshot, ancestorNode.id)) confirms this is the correct approach.
Existing mutation path:
Direct upsert into proposalSnapshot.updatedNodes + direct push to proposalSnapshot.resolvedUnknownNodeIds. This mirrors the pattern used by propagateResolvedChildEvidence at line 978-985:
upsertProposalNodeUpdate(proposalSnapshot, {
nodeId: decisionNodeId,
previousStatus: "unknown",
newStatus: "resolved",
previousValue: decisionNode.value ?? null,
newValue: decisionNode.value ?? null,
reason: "[sufficiency-based closure]",
});
proposalSnapshot.resolvedUnknownNodeIds.push(decisionNodeId);
This is compatible with:
terminal-target exclusion— "resolved" status excludes from unknown candidate lists (TERMINAL_STATUSES check)selectedQuestion clearing— reconcileResolutionSemantics at line 370-382 already clears selectedQuestion when it references a resolved nodeactiveUnknownNodeId clearing— null activeUnknownNodeId is the natural consequence of no remaining targets
Checkpoint 5 — Closure Without Direction
Can parent decision close without direction: YES (structurally), PARTIAL (semantically)
Structurally, the existing code has no validator requiring a preferred option. Tests at lines 4730–4791 show closure with newValue: "Waiting twelve months is now the resolved decision." but this is metadata attached to the resolution, not a requirement for closure itself. The propagateResolvedChildEvidence function resolves parents based solely on child-resolution counts (line 836-853: if (resolvedChildren.length === totalChildren)), without checking for option direction.
Would closing imply an option recommendation: NO — status="resolved" does not encode which option was selected. The decision's newValue can carry the conclusion text while the resolution is purely structural.
Would any current validator reject closure without direction: UNPROVEN — no existing validator at line 3617–3633 or in reconcileResolutionSemantics checks for direction. However, this has never been tested because the model always produces a recommendation when it produces closure. The gap is unproven but unlikely to be an issue given that propagateResolvedChildEvidence resolves parents unconditionally on child-count.
Checkpoint 6 — Counterexample
Existing case: makeProductLaunchClosureFixture({ includeFallbackUnknown: true }) at apply-proposal.test.js:4558–4579 (test at line 4563)
This fixture has:
n_product_launch_decision(status=unknown)- Two options (both status=known)
n_enterprise_customer_signing(status=unknown, Route D via contained_in → opt_launch_this_year)- Additional:
n_other_market_evidence(status=unknown, may_cause → opt_launch_this_year — Route C)
When customer factor is resolved but fallback unknown remains:
hasRemainingMaterialFactors(n_product_launch_decision, graph) = true
(because n_other_market_evidence qualifies via Route C: unknown → may_cause → option → contained_in → decision, and it has status=unknown.)
Under the proposed integration (Model C), the decision would KEEP OPEN because hasRemainingMaterialFactors returns true. The existing test at apply-proposal.test.js:4607+ confirms this — it expects activeUnknownNodeId to be non-null after the customer factor resolves but a real unknown remains.
Checkpoint 7 — No-Confirmation Case
Case: last represented factor resolves, hasRemainingMaterialFactors(decision) = false,
but user does NOT explicitly say "no other material uncertainty remains"
Choice: B — KEEP OPEN
Why: Without explicit confirmation, we cannot distinguish between:
(a) the model deterministically concluding sufficiency (correct to close)
(b) a resolution event that happened for unrelated reasons (e.g., a factor resolved due to new evidence but the decision still needs more input)
If Model A (graph only), closure would fire in both cases — risk of premature closure.
The 60B.56 case itself demonstrates that the user DID provide confirmation language,
so the model producing such confirmation is not an edge case — it's the normal path.
Keeping open without confirmation is conservative but correct: the cost of delayed closure
(n+1 question turn) is far lower than premature closure (wrong decision).
However, if Model C is adopted (graph + confirmation), the "no-confirmation" case is
handled by requiring bounded text matching on existing model output fields.
Model Assessment
Model A — GRAPH ONLY
- Fixes 60B.56: YES — closure fires deterministically when all factors resolve, regardless of whether the model included closure language.
- Premature-closure risk: HIGH —
hasRemainingMaterialFactors === falsecan result from resolution events that are structurally terminal but don't reflect genuine sufficiency (e.g., a factor resolved via decomposition child propagation while other non-decomposition factors remain unresolved). Without confirmation, we close on any graph state change that eliminates remaining factors. - Depends on model compliance: NO — purely structural. This is the strength and the weakness.
- Requires schema change: NO
- Principal weakness: No way to distinguish genuine sufficiency from accidental factor elimination. 60B.56's entire purpose was showing that graph-only closure is insufficient because the engine doesn't independently recognise sufficiency without explicit model signalling.
Model B — USER CONFIRMATION ONLY
- Fixes 60B.56: YES — if "no other material uncertainties" is detected in the answer or reason field, closure fires.
- Premature-closure risk: MEDIUM — depends on the detection mechanism. If free-text matching on raw answer, false positives are possible but narrow (the pattern is specific enough).
- Depends on model compliance: NO — confirmation comes from the raw user statement, not model output.
- Requires schema change: NO (using existing updatedNodes[].reason or answer field)
- Principal weakness: Cannot detect confirmation without bounded text matching on natural language, which itself is a form of interpretation. The raw answer "There are no other material uncertainties..." is already captured in the LLM's proposal output, so we can only detect it through
updatedNodes[].reason(model-generated) or raw-answer parsing. There is NO deterministic field that says "user confirmed no remaining factors."
Model C — GRAPH + USER CONFIRMATION
- Fixes 60B.56: YES — requires both: graph shows no remaining factors AND bounded confirmation text exists in existing model output.
- Premature-closure risk: LOW — both conditions must be met simultaneously. The graph check prevents closure when genuine unknowns remain; the confirmation check prevents closure when the model hasn't committed to sufficiency.
- Depends on model compliance: PARTIAL — depends on the model producing bounded confirmation language in updatedNodes[].reason. This is already present in the 60B.56 proposal output, so it's not speculative. The prompt (rule #27 + decision-sufficiency-rule at prompt-builder.js:137-143) explicitly instructs the model to state closure rationale when appropriate.
- Requires schema change: NO — uses existing
updatedNodes[].reasonandhasRemainingMaterialFactors. - Principal weakness: The confirmation signal is still model-generated (via updatedNodes[].reason), not raw user input. This means the LLM could fail to produce the confirmation text for reasons unrelated to sufficiency (e.g., prompt confusion, token limits). The bounded pattern "no other material uncertainties" in reason is narrow enough that false positives are unlikely, but it's not guaranteed.
Model D — MODEL MUST STILL EXPLICITLY RESOLVE PARENT
- Fixes 60B.56: NO — this is the baseline behavior that 60B.56 demonstrated as broken. The LLM can provide exact factor resolution without closing the parent decision.
- Premature-closure risk: NONE — no automatic closure exists.
- Depends on model compliance: FULLY — entirely model-dependent.
- Requires schema change: NO
- Principal weakness: This is exactly what 60B.56 showed fails in production. The model produced the correct factor resolution (customer signing confirmed) but did not close the parent decision, because there is no structural enforcement that all factors resolving → parent resolves.
Critical Distinction
Choice: C — GRAPH + USER CONFIRMATION SHOULD CLOSE
Why: Model A (graph-only) has too high premature-closure risk — it would close on any resolution event that eliminates remaining factors, including cases where a factor resolved for unrelated reasons. Model B (confirmation only) cannot detect confirmation without interpretation of model-generated text. Model D (model must own closure) is the broken baseline (60B.56).
Model C requires BOTH:
hasRemainingMaterialFactors(decisionId, updatedSituationGraph) === false— structural guarantee that no material factors remain- A bounded confirmation signal in existing model output — specifically, any
updatedNodes[].reasonon the parent decision containing closure-language pattern (e.g., "no other material uncertainties remaining")
This combination ensures:
- The graph actually shows all factors resolved (not just "known" or "contradicted")
- The model explicitly recognised sufficiency and stated it in its reasoning
- Neither alone is sufficient — both must agree
Minimum Corrective Boundary
Choice: E — new helper for explicit user confirmation + one closure integration point
A new helper that evaluates the bounded confirmation pattern (checking proposalSnapshot.updatedNodes[].reason for any node targeting the parent decision) and a single integration at Candidate D (post-propagation).
Why:
- The graph helper (
hasRemainingMaterialFactors) already exists from 60B.61 - What's missing is the explicit-user-confirmation helper (or rather, the bounded pattern match on existing model output)
- One integration point at post-propagation captures all structural changes and prevents stale target selection
Would positive closure remain valid: YES — both conditions (graph + confirmation) are met in positive closure scenarios where the model correctly identifies sufficiency.
Would genuine remaining factor keep decision open: YES — hasRemainingMaterialFactors === true blocks Model C regardless of confirmation text.
Would no-confirmation case remain open: YES — Model C requires both graph AND confirmation; if confirmation is absent, neither sub-condition alone triggers closure.
Would direction remain separate from closure: YES — resolution status does not encode preferred option; the decision's newValue can carry conclusion metadata without implying a recommendation requirement.
Implementation Readiness
Choice: A — READY FOR BOUNDED IMPLEMENTATION
If forced to choose "one more design question": the remaining unresolved question is whether hasRemainingMaterialFactors should also exclude nodes whose status changed ONLY via decomposition propagation (i.e., parent-of-a-decomposition-child that was resolved but didn't receive a direct user answer). Currently it does NOT distinguish this — if a child resolves and its parent inherits "resolved" status, the parent counts as resolved. For sufficiency detection, this is correct: if ALL options' dependent factors are known (including inherited resolution), sufficiency holds regardless of propagation path.
Smallest Implementation Boundary
1 new helper function in apply-proposal.js (bounded pattern match on updatedNodes[].reason)
1 integration point at candidate D (post-propagation, ~5 lines)
0 schema changes
0 prompt changes
0 test changes (existing 60B.43 + decomposition tests already cover the structural path)
Verification Against Decision Criteria
| Criterion | Status |
|---|---|
| 1. 60B.56 can close | YES — hasRemainingMaterialFactors=false + confirmation text in reason → closure fires |
| 2. Genuine remaining factor keeps decision open | YES — hasRemainingMaterialFactors=true blocks Model C regardless of confirmation |
| 3. helper=false alone does not cause premature closure | YES — needs BOTH conditions; Model C requires explicit confirmation |
| 4. User statement preserved without reinterpretation | CONDITIONAL — uses updatedNodes[].reason which is model-generated but bounded by existing prompt rules |
| 5. No schema change | YES |
| 6. No recommendation/direction inference | YES — status="resolved" carries no option preference |
| 7. Terminal-target and selectedQuestion cleanup work | YES — resolvedUnknownNodeIds push + reconcileResolutionSemantics clearing handles this automatically |
| 8. Positive closure remains valid | YES — positive scenarios already include confirmation text in reason |
Production Code Changed: NO
Tests Changed: NO
Prompt Changed: NO
Schema Changed: NO
Ollama Calls: 0
Live API Calls: 0
Vitest Run: NO
Jest Run: NO
Watchman Used: NO
Summary of Findings
The narrowest safe closure trigger is:
hasRemainingMaterialFactors(decisionId, graph) === false
AND
∃ updatedNodes[].reason for the parent decision containing "no other material" + ("uncertainties" | "differences" | "residual" | "remaining")
→ SET decision.status = "resolved"
ADD decision.id to resolvedUnknownNodeIds
(reconcileResolutionSemantics already handles selectedQuestion clearing)
This fires at Candidate D: after propagateResolvedChildEvidence completes, before active-target selection. The integration point is the gap between line 3775 and the first use of deterministicSelection for question selection.