# Experiment 60B.58 — Decision-Sufficiency Evidence Map **Date:** 2026-08-14 **Branch:** `feature/closure-selection-reconciliation-v0.41` **Head commit:** 2394ad4 experiment: confirm negative closure live ## Objective Answer exactly: what graph evidence already exists in the current architecture that can distinguish "this decision still has a material unresolved factor" from "all represented material uncertainty has been resolved", without relying on option status alone? No implementation. Read-only analysis of existing topology, code, and fixture. ## Method Analyzed: 1. Fixture `tests/fixtures/pre-anchored-product-launch-customer-signing.json` 2. `lib/graph/apply-proposal.js` — functions: `findDirectChildUnknowns`, `computeParentProgressState`, `propagateResolvedChildEvidence`, `listUnresolvedUnknownCandidates`, `selectActiveUnknownCandidate`, `scoreUnknownCandidate`, `evaluateBranchInteractions`, `syncParentChildReferences`, `buildAncestorChain` 3. `lib/graph/utils.js` — functions: `findAffectedNodes`, `findDependentNodes`, `scoreUnknownCandidate`, `countIncomingUnknownDependencies`, `selectActiveUnknownCandidate`, `explainUnknownSelection` --- ## Checkpoint 1 — Decision-Factor Linkage Map For `n_enterprise_customer_signing` in the fixture, here are every structural relationship linking it to the decision and its options: ### Relationship: `n_enterprise_customer_signing -> opt_launch_this_year (edge)` - **Edge ID:** `e-customer-signing-to-launch-option` - **From:** `n_enterprise_customer_signing` (unknown) - **To:** `opt_launch_this_year` (option) - **Relationship type:** `contained_in` - **Direction:** unknown → option (upstream dependency flow) - **Semantic role:** OPTION CONSEQUENCE — the unknown is a condition that affects/attaches to this specific option - **Currently used by closure propagation:** **NO** ### Relationship: `n_enterprise_customer_signing -> n_product_launch_decision` - **Direct edge exists?** NO - **Direct parentId relationship?** NO (both have `parentId: null`) - **Direct depends_on relationship?** NO - **Direct affects relationship?** NO - **Semantic role:** NONE — structurally disconnected at the decision level - **Currently used by closure propagation:** **NO** ### Relationship: `opt_launch_this_year -> n_product_launch_decision (edge)` - **Edge ID:** `e-opt-launch-to-dec` - **Relationship type:** `contained_in` - **Direction:** option → decision (candidate-for) - **Semantic role:** CONTAINMENT — option is a candidate for this decision ### Relationship: `opt_wait_twelve_months -> n_product_launch_decision (edge)` - **Edge ID:** `e-opt-wait-to-dec` - **Relationship type:** `contained_in` - **Direction:** option → decision (candidate-for) - **Semantic role:** CONTAINMENT — option is a candidate for this decision ### Summary of structural attribution: ``` n_product_launch_decision has no direct unknown child. childIds: [] dependsOn: [] parentId: null n_enterprise_customer_signing: parentId: null dependsOn: [] affects: [] opt_launch_this_year is contained_in n_product_launch_decision. n_enterprise_customer_signing has an edge to opt_launch_this_year labeled "contained_in". No propagation path exists from n_enterprise_customer_signing to n_product_launch_decision through any of: parentId, childIds, depends_on, affects, may_cause, causes, contained_in. ``` **Factor structurally attributable to the decision today:** PARTIAL --- ## Checkpoint 2 — Material Unresolved Factor Detection via Existing Topology ### parentId / childIds **Classification: INSUFFICIENT for the current case.** `findDirectChildUnknowns()` (apply-proposal.js:671) finds children where `node.parentId === parentNodeId OR edge.fromNodeId -> parentNodeId with relationship=depends_on`. In the fixture, no unknown has `parentId` set to the decision. The factor has `parentId: null`. Only decomposition-created unknowns get parentId populated (via `buildDecompositionContext` → `buildDecompositionTemplates`). For generic decision-factor relationships created outside decomposition, parentId is not set. The function also scans edges with `depends_on` from unknown-to-decision, which would catch factor→decision dependencies IF the LLM creates them — but the fixture has no such edge on the factor node. ### depends_on **Classification: CONTEXTUAL.** The decision node itself has `dependsOn: []`. The factor node has `dependsOn: []`. Neither unknown has a `depends_on` edge between them in the fixture. If an LLM-created edge connected `n_enterprise_customer_signing -> n_product_launch_decision` with relationship=`depends_on`, the existing `findDirectChildUnknowns` path would catch it (edge scanning at line 678). But this is not present in the fixture. ### affects **Classification: INSUFFICIENT.** The factor's `affects: []` is empty. No edge originates from the factor with relationship pointing to any decision option beyond the `contained_in` edge to `opt_launch_this_year`. The existing code does not use `affects` for closure propagation — it uses it only for `findAffectedNodes` (impact scanning, not dependency tracking). ### may_cause / causes **Classification: UNUSED BY CURRENT CLOSURE.** These appear in `STRUCTURAL_CONSEQUENCE_RELATIONSHIPS` at line 1911 of apply-proposal.js but only within the Route B structural context admission check for reasoning-pattern compatibility during new-unknown selection. They are never used in `propagateResolvedChildEvidence`, `computeParentProgressState`, or any closure-determining path. ### contained_in **Classification: INSUFFICIENT.** The factor has a `contained_in` edge to `opt_launch_this_year`. The options have `contained_in` edges to the decision. However, "contained_in" semantics mean "is-a-candidate-for" in this architecture — it flows from option→decision for containment of candidates. The reverse flow (unknown→option via contained_in) is not interpreted as a dependency. No code traverses `contained_in` edges in either direction for closure propagation. ### direct decision -> unknown edge **Classification: UNUSED BY CURRENT CLOSURE.** No such edges exist in the fixture and none are created by production code for generic factor relationships. Only decomposition children receive `depends_on` edges to their parent (see line 1637 in apply-proposal.js). --- ## Checkpoint 3 — Remaining-Factor Query ``` Possible with current schema: PARTIAL Requires new schema: NO Existing helper already does this: PARTIAL Closest existing helper: findDirectChildUnknowns (only catches parentId/depends_on children) + propagateResolvedChildEvidence (only propagates from known children) ``` **Narrowest deterministic predicate derivable from current code:** > An unresolved unknown counts against a decision's sufficiency when it either (a) has `parentId` set to the decision node, or (b) has a `depends_on` edge pointing to the decision node, or (c) is a new unknown admitted during the same turn through Route A/B structural context embedding. This predicate is **too narrow** for the customer-signing case: the factor is linked via option-attachment (`contained_in` → opt), not parentId or depends_on. No existing function traverses option→decision containment edges to find unknowns that attach to any contained option. --- ## Checkpoint 4 — Self-Counting Problem ### Parent decision appears in generic unresolved list: YES `selectActiveUnknownCandidate()` (utils.js:593) filters `graph.nodes` for `kind=unknown AND status not in [known, resolved, contradicted] AND id not in resolvedNodeIds`. `n_product_launch_decision` has `status=unknown`, is not in `resolvedNodeIds`, so it IS included. ### Parent can self-count as remaining unresolved: YES Because the decision node itself is an unknown with status=unknown, a generic unresolved list will always contain it unless explicitly filtered. After all subordinate factors resolve, the decision node remains in the list — creating exactly the self-counting problem. The system cannot distinguish "the decision itself hasn't been concluded" from "evidence for the decision is incomplete." ### Current distinction between decision and factor: PARTIAL `computeParentProgressState()` (apply-proposal.js:744) distinguishes parent from children by examining `findDirectChildUnknowns(graph, parentNode.id)`. But this only works when unknowns have parentId/depends_on links to the parent. When a factor is structurally disconnected (as in the fixture), no child-unknown path exists — so there is zero distinction between "parent awaiting conclusion" and "factor beneath parent unresolved." `propagateResolvedChildEvidence()` at line 894 filters for `node.parentId` on resolved children. If parentId is null, nothing propagates upward. The decision node never gets marked "resolved by propagation" when no direct child exists. --- ## Checkpoint 5 — Candidate Assessment ### Candidate A — CHILD UNKNOWN COMPLETION ONLY Close decision only when all direct `parentId` child unknowns resolve. - **Architecture fit:** HIGH — uses existing `propagateResolvedChildEvidence` and `computeParentProgressState` - **Fixes 60B.56:** NO — the factor has no parentId to the decision, so completion is never triggered - **Premature-closure risk:** LOW — requires explicit decomposition relationship - **Depends on model compliance:** HIGH — only works if LLM always creates parentId links - **Requires schema change:** YES (for non-decomposition factors) or NO (if we extend parentId semantics) - **Principal weakness:** Cannot capture generic decision-factor relationships created outside decomposition ### Candidate B — RELATIONSHIP-AWARE MATERIAL FACTORS Close decision when no unresolved decision-relevant unknown remains across approved structural relationships. - **Architecture fit:** MEDIUM — requires adding traversal of option-attachment edges - **Fixes 60B.56:** YES — would traverse factor→option(contained_in)→decision path - **Premature-closure risk:** LOW — only traverses known relationship types - **Depends on model compliance:** MEDIUM — depends on correct edge creation - **Requires schema change:** NO (uses existing edge fields) - **Principal weakness:** Must define which relationships count as "decision-relevant"; currently ambiguous what qualifies ### Candidate C — OPTION STATUS Close when all contained options have `status=known`. - **Architecture fit:** HIGH — options already track status - **Fixes 60B.56:** PARTIAL — addresses symptom but not the causal question - **Premature-closure risk:** HIGH — option `status=known` may only mean "the alternative itself is established, not that its comparative value is fully determined" - **Depends on model compliance:** LOW - **Requires schema change:** NO - **Principal weakness:** Premature closure. The fixed answer explicitly states no other uncertainties remain, but the option status alone doesn't prove material evidence is complete ### Candidate D — USER DECLARATION ONLY Close when user explicitly says no other material uncertainty remains. - **Architecture fit:** MEDIUM — requires capturing and evaluating user statement - **Fixes 60B.56:** YES — the 60B.56 answer includes "There are no other material uncertainties between launching this year and waiting twelve months." - **Premature-closure risk:** LOW (with graph guard) / HIGH (without it) - **Depends on model compliance:** HIGH - **Requires schema change:** NO - **Principal weakness:** Relies entirely on model extracting/propagating user statement; no independent graph verification ### Candidate E — RELATIONSHIP-AWARE FACTORS + USER DECLARATION Require both graph evidence of no represented unresolved factor AND explicit user confirmation. - **Architecture fit:** MEDIUM — combines B and D - **Fixes 60B.56:** YES — handles both the graph gap and the user statement - **Premature-closure risk:** LOW — dual-signal requirement reduces false closure - **Depends on model compliance:** MEDIUM - **Requires schema change:** NO - **Principal weakness:** Requires defining "approved structural relationships" for factor-to-decision linkage ### Candidate F — MODEL MUST CONTINUE TO OWN CLOSURE No deterministic propagation beyond existing child mechanism. - **Architecture fit:** HIGH — current state - **Fixes 60B.56:** NO — leaves the problem unresolved - **Premature-closure risk:** NONE (won't close at all) - **Depends on model compliance:** VERY HIGH - **Requires schema change:** NO - **Principal weakness:** The model will keep generating follow-up questions forever for non-decomposition decisions --- ## Checkpoint 6 — Exact 60B.56 Sufficiency Test Using Candidate E (relationship-aware + user declaration) as the winning model: ### Post-60B.56 graph state: ``` n_enterprise_customer_signing: status=resolved opt_launch_this_year: status=known, contained_in n_product_launch_decision opt_wait_twelve_months: status=known, contained_in n_product_launch_decision n_product_launch_decision: status=unknown, childIds=[], dependsOn=[] ``` ### Graph-side check: ``` Represented unresolved material factors remaining: 0 No unknown has parentId set to the decision. No unknown has depends_on pointing to the decision. The only structural path from the resolved factor to the decision goes through option-attachment (factor -> opt_launch_this_year via contained_in edge -> decision via contained_in), which isn't traversed by current propagation code. But no UNKNOWN node remains structurally linked to any option that belongs to this decision — both options are status=known and contain no unresolved unknown children. However: the factor IS still in the graph as a resolved unknown, not an unknown unknown. The real question is whether there's an unresolved unknown structurally attached via any approved relationship. The answer is NO — all such links would show through existing parentId/depends_on routes that are empty. ``` ### User statement: ``` User explicitly says no other material uncertainty remains: YES "There are no other material uncertainties between launching this year and waiting twelve months." ``` ### Would deterministic sufficiency close n_product_launch_decision: CONDITIONAL The winning rule (Candidate E) would close the decision because: 1. Graph check passes: no unresolved unknown linked via parentId/depends_on to the decision or its options 2. User statement provides explicit closure confirmation **Why:** The graph-side predicate evaluates empty for this case (no unresolved unknowns in the parentId/depends_on chain). The user statement is captured by the LLM's answer extraction as a "no more uncertainty" signal. Combined, both signals are present. ### Counterexample from existing fixture Testing `pre-anchored-decision-options.json` where an additional material unknown exists: If we modify the decision-options fixture to add: ```json { "id": "n_stickiness_uncertainty", "label": "Whether engineering retention is achievable", "description": "Uncertain whether two senior engineers will remain after relocation, because they account for key delivery capacity.", "kind": "unknown", "status": "unknown", "parentId": null, "dependsOn": [], "affects": ["opt_relocate"] } ``` This unknown has `affects` pointing to an option contained in the decision. No parentId link exists. The factor would: ``` Existing counterexample: synthetic extension of pre-anchored-decision-options fixture with n_stickiness_uncertainty having affects → opt_relocate Remaining material factor: n_stickiness_unclosure (status=unknown) Would winning rule keep decision open: UNPROVEN — the current graph-side predicate (parentId/depends_on only) would NOT detect this factor. The rule needs the relationship-aware traversal to catch affects→option links. However, if we extend Candidate E's graph check to include: - parentId → decision - depends_on → decision - affects → option contained_in decision Then it WOULD detect n_stickiness_uncertainty and keep the decision open. Without that extension, both 60B.56 (correct closure) AND this counterexample (incorrect closure) pass through the same predicate — which is exactly the defect we're diagnosing. ``` --- ## CRITICAL DISTINCTION **Choice:** E **Why:** The analysis identified six candidates for how to determine that a decision's material uncertainty is fully resolved. Candidate E — RELATIONSHIP-AWARE FACTORS + USER DECLARATION — was selected as the winning model because it alone satisfies both requirements simultaneously: (1) graph evidence that no unresolved unknown remains across all structural relationships linking factors to the decision or its options, and (2) explicit user confirmation that nothing else is uncertain. Single-signal approaches (parentId-only, option-status-only, user-declaration-only) each fail on at least one dimension. Candidate E's dual-signal requirement reduces premature-closure risk to LOW. The critical distinction is that closure requires TWO independent signals converging — not one strong signal and not two weak ones. The graph-side signal proves "nothing left unresolved in the model." The user signal proves "nothing left unresolved in reality." Only together do they establish sufficiency. --- ## MINIMUM CORRECTIVE BOUNDARY **Choice:** B **Why:** The smallest change that makes closure detection correct is extending the unresolved-unknown predicate to traverse option-attachment edges: `parentId → decision`, `depends_on → decision`, and `affects → option contained_in decision`. This is a traversal-extension, not a schema change. No new fields or node types are required. The edge semantics already exist in the graph. Only the propagation logic in `propagateResolvedChildEvidence` / `computeParentProgressState` needs to widen its scan to include option-contained unknowns reachable via the approved relationship set. This matches Candidate B from Checkpoint 5. --- ## CLOSURE VS DIRECTION **Can close without preferred option:** PARTIAL **Why:** Currently, the predicate only checks parentId/depends_on children of the decision node. It does not check unknowns attached to any of the decision's options via affected/contained relationships. Closing would require checking ALL options of the decision for unresolved unknowns, not just those directly under the decision as a child. The architecture supports option-attached factors (as shown by the customer-signing case), but the closure predicate doesn't traverse into them. This is PARTIAL because the infrastructure exists but the traversal gap means only decomposition-child closure works correctly today. --- ## IMPLEMENTATION READINESS **B** One unresolved question: Which exact relationship types qualify as "decision-relevant" for generic (non-decomposition) factors — `affects`, `may_cause`, `causes`, or all three? 60B.15 established these for context admission but didn't define their closure-weight semantics. Smallest implementation boundary: Extend `propagateResolvedChildEvidence` to also scan option-attached unknowns: for each option contained_in the decision, find all unresolved unknowns linked via `affects` or `contained_in` edges to that option. Combine with existing parentId/depends_on child scan. If combined result is empty AND user confirmation exists → close decision. Production code changed: NO Tests changed: NO Prompt changed: NO Schema changed: NO Ollama calls: 0 Live API calls: 0 Vitest run: NO Documentation updated: experiment-60b58.md + current-handoff.md Git status: (to be confirmed after commit)