Files
confidence-engine/docs/experiment-60b65.md
T

354 lines
17 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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:** 61117 (total), 6385 constants + 96117 function body
- Header comment: line 61 (1 line)
- `CONTRADICTION_PHRASES`: lines 6366 (4 lines)
- `CONFIRMATION_PHRASES`: lines 6980 (12 lines)
- `CONFIRMATION_PATTERNS`: lines 8285 (4 lines)
- JSDoc for `isUserConfirmationOfNoRemainingUncertainty`: lines 8795 (9 lines)
- Function `isUserConfirmationOfNoRemainingUncertainty`: lines 96117 (22 lines)
**Approx count:** ~47 production lines (constants + function body, excl. header comment)
### Remaining-factor helpers
**Lines:** 46154730 (total)
- Comment header: line 4615 (1 line)
- `TERMINAL_STATUSES`: line 4617 (1 line)
- `isUnresolvedUnknown`: lines 46194623 (5 lines)
- `hasRemainingMaterialFactors`: lines 46254627 (3 lines, thin wrapper)
- JSDoc + `countRemainingMaterialFactors`: lines 46294729 (101 lines incl. JSDoc)
**Approx count:** ~110 production lines
### Closure integration block (inside applyValidatedProposal)
**Lines:** 38353984 (within function)
- Comment header: line 3835 (1 line)
- `pendingResolvedIds` + virtual helper setup: lines 38433853 (~11 lines)
- `checkRemainingFactorsVirtual`: lines 38553942 (88 lines — **duplicates** graph traversal from countRemainingMaterialFactors)
- Parent-node iteration + closure predicate application: lines 39453983 (~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 39523955).
### 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 50125364, ~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 53675784)
- 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
```