17 KiB
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 320–409)
- Approx line count: 90 lines
- Primary responsibility: Normalize
resolvedUnknownNodeIds↔updatedNodessymmetry; 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+proposalas 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 ~3581–3648)
- 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) andsituationGraph(param); calls importedvalidateGraphUpdate - 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 902–1261; 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 ~2556–3488 + inlined orchestration at 3907–4121)
- 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
determineGraphBackedQuestionis fully self-contained. The inlined 257 lines at 3860–4121 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 ~3267–3454)
- 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+proposalonly - 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 ~4152–4312 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 38–569)
- 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
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 320–409)
- 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 3581–3648 inline within applyValidatedProposal)
- Semantic-change risk: LOW — stateless validation logic; all callers pass through same helpers
- Coupling: MEDIUM — imports
validateGraphUpdatefrom 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 902–1261)
- No remaining inline logic to extract
Candidate D — Active Unknown / Target Selection
- Approx removable lines: ~215 (inlined orchestration at 3870–4121 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 3267–3454)
- 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
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
- Candidate B — Proposal compatibility validation (highest context reduction, strongest tests, LOW semantic risk, removes largest inline logic block)
- Candidate A — Proposal reconciliation (LOW risk, STRONG tests, self-contained, but fewer lines than B)
- Candidate E — Answer semantic validation (pure text analysis, good candidate but lower priority)
- Candidate D — Active unknown / target selection (HIGH coupling to applyValidatedProposal locals makes it a weak extraction candidate despite strong tests)
- Supporting pure helpers (LOW edit frequency; not worth the abstraction cost)
Strategy Assessment
Strategy A — ONE EXTRACTION ONLY
Extract Candidate B (validation), verify, stop.
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.
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.
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.
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
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:
- It removes more lines (130 vs 90)
- The validation block in applyValidatedProposal is visually dominant — it obscures the post-validation lifecycle
- STRONG test coverage across multiple independent suites (60B.49/61/64/structured-fidelity/boundary/edge-case)
- 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