# Experiment 60B.65 — Decision-sufficiency module boundary audit **Branch:** `feature-decision-closure-integration-v0.43` **Status:** audit only, zero production changes **Date:** 2026-08-14 --- ## Pre-check ```text branch = feature-decision-closure-integration-v0.43 ✓ working tree = clean ✓ HEAD includes bce05f7 ✓ ``` --- ## LINE FOOTPRINT (lib/graph/apply-proposal.js) ### Confirmation helper **Lines:** 61–117 (total), 63–85 constants + 96–117 function body - Header comment: line 61 (1 line) - `CONTRADICTION_PHRASES`: lines 63–66 (4 lines) - `CONFIRMATION_PHRASES`: lines 69–80 (12 lines) - `CONFIRMATION_PATTERNS`: lines 82–85 (4 lines) - JSDoc for `isUserConfirmationOfNoRemainingUncertainty`: lines 87–95 (9 lines) - Function `isUserConfirmationOfNoRemainingUncertainty`: lines 96–117 (22 lines) **Approx count:** ~47 production lines (constants + function body, excl. header comment) ### Remaining-factor helpers **Lines:** 4615–4730 (total) - Comment header: line 4615 (1 line) - `TERMINAL_STATUSES`: line 4617 (1 line) - `isUnresolvedUnknown`: lines 4619–4623 (5 lines) - `hasRemainingMaterialFactors`: lines 4625–4627 (3 lines, thin wrapper) - JSDoc + `countRemainingMaterialFactors`: lines 4629–4729 (101 lines incl. JSDoc) **Approx count:** ~110 production lines ### Closure integration block (inside applyValidatedProposal) **Lines:** 3835–3984 (within function) - Comment header: line 3835 (1 line) - `pendingResolvedIds` + virtual helper setup: lines 3843–3853 (~11 lines) - `checkRemainingFactorsVirtual`: lines 3855–3942 (88 lines — **duplicates** graph traversal from countRemainingMaterialFactors) - Parent-node iteration + closure predicate application: lines 3945–3983 (~39 lines) **Approx count:** ~149 production lines ### Supporting additions (60B.64-specific) - `TERMINAL_STATUSES` at line 4617: 1 line (shared between remaining-factor detection and closure virtual helper) ### Total decision-sufficiency production lines in apply-proposal.js ```text Confirmation constants + function: ~57 Remaining-factor helpers: ~111 Closure integration block: ~150 ───────────────────────────────────────────── Total in apply-proposal.js: ~318 ``` Of these, **~149 lines are the closure integration block** (the bulk of the 210-line addition cited for 60B.64). The remaining ~70 lines are helper functions/constants that support it. --- ## RESPONSIBILITIES ### Confirmation helper (`isUserConfirmationOfNoRemainingUncertainty`) - **Classification:** TEXT CONFIRMATION - Pure text-predicate on raw user answer string - Zero graph access, zero side effects ### Remaining-factor helpers - `isUnresolvedUnknown`: **GRAPH QUERY** (simple status check) - `hasRemainingMaterialFactors`: **GRAPH QUERY** (thin boolean wrapper) - `countRemainingMaterialFactors`: **GRAPH QUERY** (complex traversal across 4 routes) ### Closure integration block responsibilities The block performs **three distinct** responsibilities: 1. **Virtual resolution set construction** — builds `pendingResolvedIds` from `proposalSnapshot.resolvedUnknownNodeIds` and `proposalSnapshot.updatedNodes` 2. **Decision sufficiency evaluation** — calls `checkRemainingFactorsVirtual` + `isUserConfirmationOfNoRemainingUncertainty` to produce a boolean predicate 3. **Graph mutation** — sets `parentNode.status = "resolved"`, calls `ensureResolvedUnknownId`, upserts `proposalSnapshot.updatedNodes` ### Mixed responsibilities present? **YES.** The closure integration block mixes: - Decision sufficiency *evaluation* (responsibility 2) with graph *mutation* (responsibility 3). - The virtual factor-counting function (`checkRemainingFactorsVirtual`) is also a duplicate of the pure `countRemainingMaterialFactors` from 60B.61, creating **intra-file duplication** of ~55 lines of traversal logic. --- ## DATA DEPENDENCIES ### Confirmation helper **Needs:** - `answer` (raw user answer string) — from applyValidatedProposal argument **Accidental coupling:** NONE - Pure function with single input, zero graph access ### Remaining-factor detection (`countRemainingMaterialFactors`) **Needs:** - `decisionNodeId` (string) - `graph.nodes`, `graph.edges` - `TERMINAL_STATUSES` constant (internal to same module) **Accidental coupling:** NONE - Pure function with two explicit parameters; all logic is internal ### Closure application (integration block) **Needs:** - `parentNode` — from iteration over `updatedSituationGraph.nodes` - `answer` — for confirmation check - `proposalSnapshot` — to read `resolvedUnknownNodeIds`, `updatedNodes`; to mutate status entries - `updatedSituationGraph.nodes/edges` — to build nodesById map (duplicates what countRemainingMaterialFactors already does) **Accidental coupling:** - **LOW.** Reads from `proposalSnapshot` and `updatedSituationGraph` which are natural outputs of the preceding decomposition → propagation stages. These are essential flow-throughs, not deep-local coupling. - The **virtual helper** duplicates the graph traversal from `countRemainingMaterialFactors`, reading nodes/edges that the pure function already accepts as parameters. This is *latent* duplication rather than accidental coupling per se — it exists because the block chooses to re-implement rather than reuse. --- ## HIDDEN COUPLING AUDIT | Local variable in applyValidatedProposal | Dependency type | |---|---| | `proposalSnapshot` | **PASSABLE ARGUMENT** — could be passed to a predicate | | `updatedSituationGraph` | **PASSABLE ARGUMENT** — same as graph parameter to pure function | | `reasoningState` | NOT used by closure block | | `deterministicSelection` | NOT used BY closure (but read AFTER if closureApplied=true) | | `resolvedUnknownNodeIds` | PART of `proposalSnapshot`; not accessed directly | | `validatedProposal` | NOT used by closure block | | `answer` | **PASSABLE ARGUMENT** — single string, already extracted in confirmation helper | No deep/local-variable coupling discovered. The closure block's dependencies are all at the function's parameter/early-boundary level. --- ## CANDIDATE ASSESSMENT ### Candidate A — NO EXTRACTION - **Semantic-change risk:** N/A (no change) - **Coupling reduction:** NONE - **Testability improvement:** NONE (tests already exist but in large file) - **Complexity reduction:** NONE (~318 lines of decision-sufficiency code still mixed in 4730-line file) - **Schema change:** NO - **Principal weakness:** The virtual helper duplicates `countRemainingMaterialFactors`. Two independent implementations of the same graph traversal logic create maintenance risk. ### Candidate B — EXTRACT GRAPH QUERY ONLY Extract `isUnresolvedUnknown`, `hasRemainingMaterialFactors`, `countRemainingMaterialFactors` → `decision-sufficiency.js` - **Semantic-change risk:** LOW (all three are pure functions already exported) - **Coupling reduction:** MEDIUM (removes ~111 lines from apply-proposal.js; eliminates one duplication source by enabling reuse) - **Testability improvement:** MEDIUM (pure graph queries become importable test fixtures) - **Complexity reduction:** MEDIUM (~111 fewer lines in apply-proposal.js) - **Schema change:** NO - **Principal weakness:** The virtual helper inside the closure block still duplicates traversal logic. It would need to be rewritten to call `countRemainingMaterialFactors` with a custom "unresolved predicate" parameter, or the extracted module would need to accept such a parameter — introducing a new signature variant that complicates the extraction. ### Candidate C — EXTRACT QUERY + CONFIRMATION Add `isUserConfirmationOfNoRemainingUncertainty`, `hasRemainingMaterialFactors`, `countRemainingMaterialFactors` → `decision-sufficiency.js` - **Semantic-change risk:** LOW (all pure, zero state dependency) - **Coupling reduction:** HIGH (removes all decision-sufficiency *evaluation* from apply-proposal.js; ~167 lines) - **Testability improvement:** HIGH (confirmation detection becomes independently testable) - **Complexity reduction:** MEDIUM (~167 fewer lines in apply-proposal.js; closure block reduced to orchestration/mutation only) - **Schema change:** NO - **Principal weakness:** The closure integration block's virtual helper still exists and duplicates graph traversal. It must be eliminated or rewritten. ### Candidate D — EXTRACT PURE DECISION-SUFFICIENCY UNIT ★ RECOMMENDED Extract all three functions + a combined predicate: ```js // decision-sufficiency.js exports: isUserConfirmationOfNoRemainingUncertainty(answer) -> boolean hasRemainingMaterialFactors(decisionNodeId, graph) -> boolean countRemainingMaterialFactors(decisionNodeId, graph) -> number shouldCloseDecision({ decisionNodeId, graph, answer }) -> boolean ``` Keep in apply-proposal.js only: - The confirmation constants (or move them to the new module too) - `TERMINAL_STATUSES` (or move it — see below) - The closure *mutation* block that applies parentNode.status = "resolved" - **Semantic-change risk:** LOW (pure functions extracted; apply-proposal.js becomes a thin consumer of a predicate result) - **Coupling reduction:** HIGH (all evaluation moves to dedicated module; only orchestration/mutation stays) - **Testability improvement:** HIGH (`shouldCloseDecision` is the clearest possible unit test target — 3 inputs, 1 boolean output, zero graph access needed in tests) - **Complexity reduction:** HIGH (~210 fewer lines in apply-proposal.js for evaluation; closure block reduced to ~40 mutation lines) - **Schema change:** NO (existing `hasRemainingMaterialFactors` and `isUserConfirmationOfNoRemainingUncertainty` already exported — no public API change) - **Principal weakness:** Requires adding a new `shouldCloseDecision` predicate that doesn't exist today. This is the only "new function" introduced, but it's derived directly from the existing inline code (lines 3952–3955). ### Candidate E — EXTRACT QUERY + MUTATION Move both evaluation AND graph mutation to a new module. - **Semantic-change risk:** HIGH (breaks apply-proposal.js's ownership of all graph mutations) - **Coupling reduction:** MEDIUM (evaluation isolated but now also outside apply-proposal.js) - **Testability improvement:** MEDIUM (mutation tests require graph state setup in every test) - **Complexity reduction:** LOW-MEDIUM (apply-proposal.js loses mutation code but also loses visibility into the full lifecycle) - **Schema change:** YES or NO depending on whether mutation is applied inside apply-proposal or returned as a diff — either way requires interface change - **Principal weakness:** Violates principle #4 ("graph mutation ownership stays in apply-proposal"). Introduces dual-mutation-source risk. The extracted module would need to be aware of `applyValidatedProposal`'s post-closure flow (`deterministicSelection`, selectedQuestion) to avoid orphaned state. --- ## PURE-FUNCTION BOUNDARY **Pure-function boundary possible:** YES **Recommended shape:** ```js shouldCloseDecision({ decisionNodeId, // string — the unknown node ID being evaluated for closure graph, // SituationGraph — post-propagation graph state answer // string — raw user answer (not processed/normalized) }) -> boolean ``` **Why:** - All three inputs are naturally available at the point where the closure block runs. - The existing `isUserConfirmationOfNoRemainingUncertainty` already accepts a single `answer` parameter and is pure. - The existing `countRemainingMaterialFactors` already accepts `(decisionNodeId, graph)` and is pure. - The predicate is simply: `countRemainingMaterialFactors(decisionNodeId, graph) === 0 && isUserConfirmationOfNoRemainingUncertainty(answer)`. - No mutation, no question selection, no state change — all within the strict purity constraints listed in the prompt. --- ## ORCHESTRATION BOUNDARY **Minimum code remaining in applyValidatedProposal after extraction:** ```js // Lines ~15-20 would remain: const sufficiency = shouldCloseDecision({ decisionNodeId: parentNode.id, graph: updatedSituationGraph, answer, }); if (sufficiency) { // mutation only: parentNode.status = "resolved"; ensureResolvedUnknownId(proposalSnapshot, parentNode.id); upsertUpdateInSnapshot(proposalSnapshot, parentNode.id, ...); closureApplied = true; } ``` **Approximate orchestration lines after extraction:** ~40 lines (reduced from ~150 lines currently) The remaining code is purely: 1. Iterate parent unknown nodes 2. Call external predicate 3. Apply mutation if predicate returns true 4. Mark `closureApplied = true` --- ## TEST MIGRATION **60B.61 tests movable:** YES - 9 test cases (lines 5012–5364, ~353 lines) - All test `hasRemainingMaterialFactors` which is a pure function - Could be extracted to `tests/graph/decision-sufficiency.test.js` without assertion changes **Confirmation tests movable:** PARTIAL - 8 tests in "60B.64 — explicit decision sufficiency closure" (lines 5367–5784) - These test the *full integration* of confirmation + remaining-factor evaluation + mutation - The confirmation helper's individual behaviour is tested indirectly through these integration tests - Could extract ~120 lines of confirmation-only subtests to a separate file, but the fixtures (makeClosureDecisionFixture) are shared **60B.64 full integration tests should remain in apply-proposal.test.js:** YES - These test the end-to-end flow: applyValidatedProposal → closure mutation → downstream state effects - Any extraction must preserve these assertions exactly as they stand --- ## RUNTIME / TOOLING **Would splitting this logic into modules materially improve runtime performance:** NEGLIGIBLE - No computational complexity change; same function calls, same object allocations - Possibly microscopically slower due to module import overhead (unobservable in practice) **Would it improve Claude/Codex edit reliability:** LIKELY YES - Decision-sufficiency logic would live in a ~150-line file instead of being scattered across a 4730-line file - Future edits to the confirmation phrases, factor routes, or closure predicate can be done with ~60 lines of context vs ~400+ lines today **Would it reduce context required for future reasoning changes:** LIKELY YES - Confirmation logic is conceptually independent from graph traversal - Factor-detection logic is independently auditable - Today all three are interleaved inside applyValidatedProposal, requiring the reader to mentally separate concerns while reading ~150 lines of inline code --- ## CRITICAL DISTINCTION **Choice: D — EXTRACT PURE DECISION-SUFFICIENCY UNIT** **Why:** The evaluation logic (confirmation detection + remaining-factor counting + closure predicate) is entirely pure and self-contained. It should own itself as a unit. Graph mutation stays in apply-proposal.js per principle #4. This is the narrowest boundary that achieves goals #1–#7. --- ## MINIMUM REFACTOR BOUNDARY **Choice: B — one new decision-sufficiency module** **Why:** A single `decision-sufficiency.js` module containing all five functions (`isUserConfirmationOfNoRemainingUncertainty`, `hasRemainingMaterialFactors`, `countRemainingMaterialFactors`, `shouldCloseDecision`, and `TERMINAL_STATUSES`) achieves: - Zero semantic change (all existing exports preserved) - 60B.64 behaviour identical (apply-proposal.js calls the same predicate, produces same result) - Apply-proposal orchestration fully visible (~40 lines) - Graph mutation ownership stays in apply-proposal - Pure logic independently testable (`shouldCloseDecision` is the ideal unit test target) - No schema change - No prompt change - Future edits require less context --- ## REFACTOR TIMING **Choice: B — RUN LIVE REGRESSION FIRST, THEN REFACTOR** **Why:** The current implementation passes its targeted behavioural tests. Introducing a refactor before verifying that live regression (60B.56) still passes would conflate two risk vectors: regression risk + extraction risk. Running regression first provides confidence that the existing code is correct, making any subsequent extraction's "zero semantic change" claim verifiable against a known-good baseline. --- ## IMPLEMENTATION READINESS **Choice: A — READY FOR ZERO-SEMANTIC-CHANGE REFACTOR** If B (one more design question required), the unresolved question would be: should `shouldCloseDecision` return just `boolean` or a richer shape `{ hasRemainingFactors, userConfirmedNoRemainingUncertainty, shouldClose }` for diagnostic logging? This does not affect correctness of extraction — only post-refactor API surface. **Smallest zero-semantic-change refactor:** Extract all decision-sufficiency evaluation logic to a single `decision-sufficiency.js` module with 5 exports, replace the inline closure evaluation in apply-proposal.js with a call to `shouldCloseDecision`, and keep mutation code in apply-proposal.js. --- ## CONSTRAINTS CHECK ```text 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 ```