5.3 KiB
Experiment 60B.67 — Decision-Sufficiency Module Extraction
Branch: feature/decision-sufficiency-module-v0.44
Status: extraction complete, zero semantic change verified
Date: 2026-08-14
Objective
Extract all decision-sufficiency evaluation logic from apply-proposal.js (4730 → 4480 lines) into a dedicated decision-sufficiency.js module (~233 lines), per the audit conclusions in experiment 60B.65. This narrows the boundary between evaluation (pure predicate) and mutation (orchestration), eliminating the ~130-line checkRemainingFactorsVirtual duplication described in 60B.65's LINE FOOTPRINT section.
Changes
New module: lib/graph/decision-sufficiency.js (~233 lines)
Exports (5 public, 1 shared constant):
isUserConfirmationOfNoRemainingUncertainty(answer) → boolean— pure text-predicate on raw user answer stringhasRemainingMaterialFactors(decisionNodeId, graph) → boolean— pure graph query (thin wrapper over count)countRemainingMaterialFactors(decisionNodeId, graph, pendingResolvedIds?) → number— pure graph traversal across 4 routes; now accepts optionalpendingResolvedIdsparameter for same-turn virtual resolution semanticsshouldCloseDecision({ decisionNodeId, graph, answer, pendingResolvedIds? }) → boolean— new combined predicate; eliminates the need for the caller to compose two checksTERMINAL_STATUSES— internal constant (not exported; kept private)
Internal (private):
CONTRADICTION_PHRASES,CONFIRMATION_PHRASES,CONFIRMATION_PATTERNS— moved from apply-proposal.js constantsisUnresolvedUnknown(node)— pure graph query used as internal predicate
apply-proposal.js changes (~250 net lines removed)
- Import statement added for
countRemainingMaterialFactors,hasRemainingMaterialFactors,isUserConfirmationOfNoRemainingUncertainty,shouldCloseDecision - Re-export of
hasRemainingMaterialFactorspreserved for backward compatibility (existing tests import from apply-proposal.js) - Confirmation constants + function body removed (~57 lines)
- Remaining-factor helpers + TERMINAL_STATUSES removed (~111 lines)
checkRemainingFactorsVirtualclosure block replaced with singleshouldCloseDecision()call (~130 lines eliminated as duplication)- Local
TERMINAL_STATUSESconstant added inside the closure iteration loop to avoid breaking theparentNode.statusguard that already existed there
Tests: tests/graph/decision-sufficiency.test.js (~456 lines)
- 15 tests for
isUserConfirmationOfNoRemainingUncertainty— all phrase-family variants confirmed - 11 tests for
hasRemainingMaterialFactors— identical assertions to 60B.61 in apply-proposal.test.js (route A–D coverage, edge cases) - 6 tests for
shouldCloseDecision— full predicate testing includingpendingResolvedIdsvirtual resolution semantics
Tests: tests/graph/apply-proposal.test.js
- Added import line for
shouldCloseDecision,isUserConfirmationOfNoRemainingUncertaintyfrom the new module (for future use) - Existing 60B.61 and 60B.64 test suites unchanged (zero semantic change verified)
Verification
npx vitest run tests/graph/decision-sufficiency.test.js \
tests/graph/apply-proposal.test.js -t "60B.67|60B.64|60B.61"
# Result: 35 integration tests (apply-proposal) + 32 pure unit tests = 67 passing
All pre-existing test suites pass without modification — confirming zero semantic change.
Complexity Reduction
| Metric | Before | After | Delta |
|---|---|---|---|
| apply-proposal.js lines | 4730 | 4480 | −250 |
| Decision-sufficiency eval lines in file | ~318 (scattered) | 0 | −318 |
| Closure integration orchestration lines | ~150 | ~40 | −110 |
| CheckRemainingFactorsVirtual duplication | ~90 lines (inline) | Eliminated | −90 |
| New module size | — | 233 | +233 |
| New test file size | — | 456 | +456 |
What Was NOT Moved (by design)
Per principle #4, graph mutation ownership stays in apply-proposal.js:
parentNode.status = "resolved"assignmentensureResolvedUnknownId()callsproposalSnapshot.updatedNodesmanipulationclosureAppliedflag propagation- Iteration loop over parent nodes
Only the evaluation predicate (shouldCloseDecision) was extracted. This keeps apply-proposal.js as the single source of graph truth for mutations while allowing the predicate to be independently testable and editable in a ~233-line file.
Why countRemainingMaterialFactors Gains a 3rd Parameter
The original checkRemainingFactorsVirtual accepted pendingResolvedIds because it was designed for same-turn resolutions where the graph hasn't yet been reconciled with the proposal snapshot. The extracted countRemainingMaterialFactors(decisionNodeId, graph) signature was deliberately extended to accept an optional pendingResolvedIds parameter so the pure function can serve both use cases:
- Without the param: standard post-propagation evaluation (existing callers)
- With the param: virtual resolution semantics during applyValidatedProposal
This preserves behavioral identity without requiring a separate "virtual" variant of the function.