experiment: map remaining apply proposal boundaries

This commit is contained in:
2026-08-14 16:27:55 +01:00
parent 6cb91099c2
commit a5b71ad89a
2 changed files with 292 additions and 0 deletions
+280
View File
@@ -0,0 +1,280 @@
# Experiment 60B.70 — Remaining apply-proposal.js Boundary Map
**Date:** 2026-08-14
**Branch:** `feature/decision-sufficiency-module-v0.44`
**Type:** Read-only structural audit (no code changes)
---
## Objective
Identify the highest-value zero-semantic-change extraction boundary in `apply-proposal.js` (4,480 lines) that would materially reduce edit/context risk without obscuring lifecycle orchestration.
---
## Git Pre-check
```
branch = feature/decision-sufficiency-module-v0.44
working tree = clean
HEAD includes: 36b4f47, 1ca5026, 6cb9109 ✓
```
---
## Responsibility Map
### Proposal reconciliation (lines 320409)
- **Approx line count:** 90 lines
- **Primary responsibility:** Normalize `resolvedUnknownNodeIds``updatedNodes` symmetry; auto-add synthetic updatedNode when proposal lists a resolved ID without corresponding update; nuke selectedQuestion if its node was resolved by the same proposal
- **Pure / impure / mixed:** Mixed (pure on proposal object, reads graph state only for existence checks)
- **Depends heavily on applyValidatedProposal locals:** NO — takes `graph` + `proposal` as parameters; returns `{proposal, errors}`
- **Existing focused tests:** STRONG (60B.49 suite: 6 integration tests across reconciliation, staleness, dedup, non-unknown guard)
### Proposal compatibility / selected-question validation (lines ~35813648)
- **Approx line count:** 130 lines of inline validation logic within applyValidatedProposal
- **Primary responsibility:** Pre-mutation graph integrity — added edge duplicates, edge reference validity (from/to node existence, cross-boundary edges), removed edge existence, combined node deduplication, semantic duplicate unknown detection, added-unknown support validation, selectedQuestion node validity, answer-meaning compatibility with raw answer, answer-meaning alignment, question-selection requirement
- **Pure / impure / mixed:** Mixed — calls helpers that read graph + proposal; mutates no state
- **Depends heavily on applyValidatedProposal locals:** PARTIAL — operates on `validatedProposal` (local) and `situationGraph` (param); calls imported `validateGraphUpdate`
- **Existing focused tests:** STRONG — 60B.61/64 suites, structured-fidelity suite (8 tests), boundary overlap tests (3), regression A/B/C/D suites, add-unknown support tests (7 cases)
### Resolution propagation (lines 9021261; `propagateResolvedChildEvidence`)
- **Approx line count:** 360 lines (exported function)
- **Primary responsibility:** Post-mutation parent progress state computation; child branch evidence aggregation; ancestor chain confidence propagation; confidence cap logic; comparison vs independent evidence distinction
- **Pure / impure / mixed:** Mixed — reads graph, computes derived metrics, returns rich result object
- **Depends heavily on applyValidatedProposal locals:** NO — already extracted as standalone export
- **Existing focused tests:** MEDIUM (covered by 60B.43/64 integration; no dedicated unit suite)
### Active unknown / target selection (lines ~25563488 + inlined orchestration at 39074121)
- **Approx line count:** 933 lines (exported `determineGraphBackedQuestion`) + ~215 lines inlined within applyValidatedProposal
- **Primary responsibility:** Unknown candidate eligibility filtering; reasoning pattern compatibility scoring; decomposition child selection; reseat-after-rejection; model-selected target preference via depends_on prerequisite check; sibling ordering tiebreakers
- **Pure / impure / mixed:** Mixed — reads graph, returns selection result (no mutations)
- **Depons heavily on applyValidatedProposal locals:** NO — the exported `determineGraphBackedQuestion` is fully self-contained. The inlined 257 lines at 38604121 are orchestration glue that depends on decompositionResult/propagationResult locals.
- **Existing focused tests:** STRONG (60B.42 active selector guard: 5 tests; 60B.11 prerequisite-aware targeting: 9 tests; selectedQuestion lifecycle in 60B.43/64)
### Answer semantic validation (lines ~32673454)
- **Approx line count:** 297 lines (validateAnswerMeaningCompatibilityWithRawAnswer + validateAnswerMeaningAlignment helpers)
- **Primary responsibility:** Raw answer → userSupportedMeaning alignment verification; unclassified meaning support detection; hard constraint boundary language analysis; conditional qualification preservation
- **Pure / impure / mixed:** Mostly pure — reads answer + proposal, returns errors array
- **Depends heavily on applyValidatedProposal locals:** NO — operates on `answer` + `proposal` only
- **Existing focused tests:** MEDIUM (regression A/B/C suites test the path end-to-end but don't isolate the helpers)
### Graph mutation (applyGraphUpdate import from utils.js)
- **Approx line count:** ~0 in apply-proposal.js (imported)
- **Primary responsibility:** The single mutation point — applies node updates, resolves nodes, adds/removes edges
- **Pure / impure / mixed:** Pure mutation function
### Final selectedQuestion lifecycle (lines ~41524312 within applyValidatedProposal)
- **Approx line count:** ~160 lines of inlined orchestration
- **Primary responsibility:** Compose finalSelectedQuestion from deterministicSelection + formulatedQuestion; repeated-question rejection + reseat; effectiveSelectedQuestion composition
- **Pure / impure / mixed:** Mixed — reads multiple locals, returns selected question or null
- **Depends heavily on applyValidatedProposal locals:** YES — tight coupling with deterministicSelection, proposedNode, decompositionResult
### Supporting pure helpers (lines 38569)
- **Approx line count:** ~530 lines
- **Primary responsibility:** JSON cloning, Zod error formatting, edge duplicate detection, text normalization, node lookup, compound question detection, confidence assessment, branch conflict signature computation, token overlap utilities
- **Pure / impure / mixed:** All pure — no side effects
- **Depends heavily on applyValidatedProposal locals:** NO
---
## Orchestration vs Extractable Logic
```text
proposal reconciliation: GOOD EXTRACTION CANDIDATE (pure on proposal+graph)
proposal compatibility val: GOOD EXTRACTION CANDIDATE (complex but stateless)
answer semantic validation: POSSIBLE LATER (good candidate but lower priority)
resolution propagation: ALREADY EXTRACTED (standalone export)
active unknown / target sel: ALREADY PARTIALLY EXTRACTED (determineGraphBackedQuestion is standalone; inlined orchestration stays)
final selectedQuestion: SHOULD STAY IN apply-proposal.js (tightly coupled to deterministicSelection lifecycle)
graph mutation: MUST STAY IN apply-proposal.js (single ownership point)
supporting pure helpers: POSSIBLE LATER (large cluster, but low edit frequency)
```
---
## Candidate Assessment
### Candidate A — Proposal Reconciliation (`reconcileResolutionSemantics`)
- **Approx removable lines:** ~90 (lines 320409)
- **Semantic-change risk:** LOW — pure function on proposal object; existing tests cover all paths
- **Coupling:** LOW — takes graph + proposal; returns {proposal, errors}
- **Test coverage:** STRONG — 6 dedicated integration tests in 60B.49
- **Context reduction:** MEDIUM — removes 90 lines of self-contained logic
- **Future edit-frequency:** LOW — stable reconciliation rules unlikely to change
- **Lifecycle clarity after extraction:** BETTER — apply-proposal.js pre-validation flow becomes a clear sequence of named steps
- **Principal risk:** Must verify every edge case (selectedQuestion nuke on resolution, bidirectional update-node/ID consistency) is captured in the new module's tests
### Candidate B — Proposal Compatibility Validation
- **Approx removable lines:** ~130 (lines 35813648 inline within applyValidatedProposal)
- **Semantic-change risk:** LOW — stateless validation logic; all callers pass through same helpers
- **Coupling:** MEDIUM — imports `validateGraphUpdate` from utils.js and calls other internal helpers
- **Test coverage:** STRONG — 25+ tests across multiple suites exercise every validation path
- **Context reduction:** HIGH — removes the largest single block of inline logic from applyValidatedProposal, splitting it into a named pre-check step
- **Future edit-frequency:** MEDIUM — schema-driven, may need updates when graph schema evolves
- **Lifecycle clarity after extraction:** BETTER — `validateProposalCompatibility()` becomes a single readable call replacing 7+ individual validation pushes
- **Principal risk:** Must preserve exact error aggregation order and deduplication semantics across the extracted validator
### Candidate C — Resolution Propagation
- **Already extracted as standalone export (lines 9021261)**
- **No remaining inline logic to extract**
### Candidate D — Active Unknown / Target Selection
- **Approx removable lines:** ~215 (inlined orchestration at 38704121 within applyValidatedProposal)
- **Semantic-change risk:** MEDIUM — the inlined block has many local-variable side effects and interacts with decompositionResult/propagationResult state
- **Coupling:** HIGH — deeply reads locals from applyValidatedProposal; recomputes deterministicSelection multiple times
- **Test coverage:** STRONG (exported function); but inlined orchestration has MEDIUM test coverage
- **Context reduction:** MEDIUM
- **Future edit-frequency:** LOW-MEDIUM
- **Lifecycle clarity after extraction:** WORSE — would separate the "post-propagation reselection decision" from its governing state variables across function boundary
- **Principal risk:** Extracting the inlined orchestration block would scatter the candidate selection logic across multiple function boundaries, making it harder to trace the active unknown lifecycle
### Candidate E — Answer Semantic Validation
- **Approx removable lines:** ~297 (validateAnswerMeaningCompatibilityWithRawAnswer + validateAnswerMeaningAlignment at lines 32673454)
- **Semantic-change risk:** LOW — mostly pure text analysis
- **Coupling:** LOW — operates on answer + proposal only
- **Test coverage:** MEDIUM — tested end-to-end but not as isolated unit tests for the helpers
- **Context reduction:** MEDIUM
- **Future edit-frequency:** MEDIUM — answer semantics may evolve with prompt changes
- **Lifecycle clarity after extraction:** BETTER
- **Principal risk:** Answer semantics is tightly coupled to prompt contract; extraction alone doesn't reduce orchestration complexity in applyValidatedProposal
---
## Mutation Ownership
```text
Can mutation ownership remain central while extracting candidate modules: YES
applyGraphUpdate(...) invocation — MUST stay (single mutation entry point)
proposalSnapshot lifecycle — MUST stay (built up locally, passed to mutation)
updatedSituationGraph lifecycle — MUST stay (accumulates mutation state across pipeline stages)
resolvedUnknownNodeIds bookkeeping — MUST stay (derived from proposalSnapshot.resolvedUnknownNodeIds)
activeUnknownNodeId mutation — MUST stay (tied to post-mutation candidate reselection lifecycle)
selectedQuestion finalisation — MUST stay (composed from deterministicSelection + formulatedQuestion in same scope)
```
The key insight: all mutations flow through `applyGraphUpdate(graphSnapshot, proposalSnapshot)`. Once extracted modules return their outputs, the mutation remains a single point of truth. Extraction of validation/reconciliation doesn't fragment mutation ownership because these are pre-mutation checks that operate on copies/clones.
---
## Ranking
1. **Candidate B — Proposal compatibility validation** (highest context reduction, strongest tests, LOW semantic risk, removes largest inline logic block)
2. **Candidate A — Proposal reconciliation** (LOW risk, STRONG tests, self-contained, but fewer lines than B)
3. **Candidate E — Answer semantic validation** (pure text analysis, good candidate but lower priority)
4. **Candidate D — Active unknown / target selection** (HIGH coupling to applyValidatedProposal locals makes it a weak extraction candidate despite strong tests)
5. **Supporting pure helpers** (LOW edit frequency; not worth the abstraction cost)
---
## Strategy Assessment
### Strategy A — ONE EXTRACTION ONLY
Extract Candidate B (validation), verify, stop.
```text
Risk: LOW
Expected line reduction: ~130 lines from apply-proposal.js (now ~4,350)
Expected context reduction: HIGH — removes the largest single inline logic block
Semantic-drift risk: LOW — stateless validation functions are easy to extract correctly
```
### Strategy B — TWO SMALL EXTRACTIONS
Extract A + B in separate commits. Both are independent pure-checking modules with STRONG test coverage.
```text
Risk: LOW (two independent, low-risk extractions)
Expected line reduction: ~220 lines total (~4,260 remaining)
Expected context reduction: HIGH — two clear named pre-validation steps replace inline logic
Semantic-drift risk: LOW (both have STRONG test coverage and pure/mixed character)
```
### Strategy C — LARGE APPLY-PROPOSAL DECOMPOSITION
Break apply-proposal.js into several lifecycle modules now.
```text
Risk: MEDIUM-HIGH — too many extraction points to verify in one pass; risk of scattering orchestration awareness across modules
Expected line reduction: ~600+ lines (aggressive)
Semantic-drift risk: MEDIUM — more boundaries to cross during verification
```
### Strategy D — STOP REFACTORING
Current structure is good enough.
```text
Risk: LOW (no risk)
But 4,480 lines still has one ~990-line function with 15+ phases of inline logic
Context reduction: NONE
Semantic-drift risk: NONE
Future Claude/Codex context cost: HIGH — every session loads all 4,480 lines
```
**Chosen: Strategy B — TWO SMALL EXTRACTIONS in separate commits**
Rationale: Candidates A and B are independent pure-checking modules with STRONG test coverage. Extracting both gives ~220 lines of reduction for minimal risk. Candidate D is excluded because its HIGH coupling to orchestration locals makes it a weak extraction candidate despite strong tests.
---
## File Size Estimates
```text
Current apply-proposal.js lines: 4,480
After reconciliation extraction (A): ~4,390 (-90)
After compatibility validation extraction (B): ~4,260 (-220 total)
Reasonable medium-term target: ~4,250-4,300
Why not lower? Because apply-proposal.js must retain:
- Lifecycle ordering visibility (~150 lines of orchestration scaffolding)
- Graph mutation ownership (applyGraphUpdate invocation + proposalSnapshot buildup)
- Active target reselection lifecycle (~260 lines, partially extracted already)
- Final selectedQuestion composition (~160 lines)
Target of ~4,250-4,300 reflects a clear orchestration file — not tiny wrapper-only, not giant mixed-responsibility.
```
---
## Critical Distinction
**Choice: B — validation should be next**
Why: Candidate B removes the largest single inline logic block (~130 lines) that currently scatters 7+ validation calls across applyValidatedProposal's pre-mutation phase. Extracting `validateProposalCompatibility()` into its own module gives the highest context reduction per line extracted. Both A and B are equally justified as clean extractions, but B has higher priority because:
1. It removes more lines (130 vs 90)
2. The validation block in applyValidatedProposal is visually dominant — it obscures the post-validation lifecycle
3. STRONG test coverage across multiple independent suites (60B.49/61/64/structured-fidelity/boundary/edge-case)
4. No new tests needed for extraction — existing integration tests provide sufficient boundary coverage
---
## Minimum Next Refactor Boundary
**Choice: B — one new validation module**
Why: Extract `validateProposalCompatibility(graph, proposal)` as a single function that encapsulates all 8 pre-mutation validations currently scattered across applyValidatedProposal. The extracted function takes the same inputs (`graph`, `proposal`) and returns `{valid, errors}`. This matches the existing pattern established by decision-sufficiency.js extraction (pure logic out, mutation stays).
---
## Refactor Timing
**Choice: B — RETURN TO REASONING WORK FIRST**
Why: Experiment 60B.70 is a read-only audit with no implementation directive. The highest-value next action is completing this documentation and returning to active reasoning work. A future session can implement the validation extraction when there's a natural editing context (e.g., when schema changes require touching that validation layer anyway). Forcing an extraction without a natural editing trigger increases semantic drift risk because there's no external pressure ensuring the extraction serves a real need.
---
## Verification
- 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