11 KiB
Phase 2: Complexity and duplication paydown
Goal: pay down the complexity and duplication debt that fallow flags but no issue tracks.
Phase 1 is scoped to enabling #266 (the preview formatter).
Phase 2 is a distinct theme: it eliminates the five fallow refactoring targets and drives down the test-tree duplication.
Four of the five targets sit on the security-critical tool_call decision path, where high untested complexity is a correctness risk, not only a maintainability one.
The two phases are otherwise independent and can run in either order, with one exception: do #285 before Phase 1 step 2, since both modify the describeToolGate call site inside handleToolCall, and decomposing that function first lets the formatter thread through a clean pipeline.
Current health metrics
| Metric | Value |
|---|---|
| Health score | 74 B |
| LOC | 31,416 |
| Dead files / exports | 0% |
| Avg cyclomatic | 1.4 |
| Maintainability | 91.2 (good) |
| Duplication | 9.2% (after #286) |
| Refactoring targets | 3 (2 medium, 1 high) - after #290; config-loader.ts no longer a target |
| Worst CRAP risk | permission-gate-handler.ts 79.4 (handleInput) - after #290 |
Findings
All findings are fallow-confirmed and untracked before this phase.
| # | Finding | Category | Files | Impact | Risk | Priority |
|---|---|---|---|---|---|---|
| 1 | handleToolCall runs six gates with a repeated bypass/runner/short-circuit shape - cognitive 52, CRAP 172, the package's worst |
B: god function | handlers/permission-gate-handler.ts |
5 | 2 | 20 |
| 2 | ✅ resolvePermissions interleaves scope merge with parallel origin-map bookkeeping - cognitive 33, CRAP 97 - resolved by #286 |
B: god function | permission-manager.ts |
4 | 2 | 16 |
| 3 | ✅ runGateCheck carried the full check→log→emit→approve cycle as six inline phases - cognitive 32 - resolved by #287 |
B: god function | handlers/gates/runner.ts |
4 | 2 | 16 |
| 4 | ✅ Two token classifiers share a 31-line rejection prelude (production clone); collectPathCandidateTokens (37) and collectPatternCommandTokens (33) are complexity hotspots - resolved by #289 |
A: duplication / B: god function | handlers/gates/bash-path-extractor.ts |
4 | 3 | 12 |
| 5 | ✅ stripJsonComments is a five-variable character scanner - cognitive 31 - resolved by #290 |
B: god function | config-loader.ts |
2 | 2 | 8 |
| 6 | ✅ 9.1% duplication concentrated in the test tree - the single largest health deduction (-4.1) - resolved by #288 | D: test duplication | test/ (clone families) |
3 | 1 | 15 |
Steps
-
✅ Decompose
handleToolCall(#285) - completed- Extracted
validateRequestedTool(pure, exported) for the tool-name validation prelude. - Extracted
runGateclosure (insidehandleToolCall) for the unified bypass/runner/short-circuit shape. - Collapsed the body to validate → build context → ordered producer-array pipeline.
- Outcome:
handleToolCallno longer appears as a refactoring target; CRAP risk for the file dropped from 172 → 79.4 (nowhandleInput); refactoring targets 5 → 4.
- Extracted
-
✅ Decompose
resolvePermissions(#286) - completed- Extracted
mergeScopesWithOrigins(scopes)(into newsrc/scope-merge.ts) returning{ mergedPermission, origins }, isolating origin-map bookkeeping from the resolve pipeline. - The remaining body reads as load scopes → merge with origins → extract universal fallback → build config rules → compose.
- Outcome:
resolvePermissionsno longer appears as a refactoring target;permission-manager.tsdropped from the CRAP-risk list.
- Extracted
-
✅ Thin
runGateCheck(#287) - completed- Introduced
SessionApprovalvalue object (src/session-approval.ts) owning the{pattern}|{patterns}union; exposedrepresentativePatternandtoGateApproval(). SessionRules.record(approval)absorbs the per-pattern loop;GateRunnerDepsseam renamed torecordSessionApproval(approval)- runner tells the store, never interrogates the union.- Extracted
buildDecisionEventintohelpers.tsto deduplicate theorigin/agentName/matchedPattern ?? nullnormalization across both emit sites. - Outcome:
runner.tsno longer appears as a refactoring target; refactoring targets 4 → 3.
- Introduced
-
✅ Decompose
bash-path-extractor.ts(#289) - completed- Extracted pure token classifiers into new
src/handlers/gates/bash-token-classification.ts; privaterejectNonPathTokenpredicate eliminates the 31-line rejection-prelude clone. - Extracted
classifyPatternCommandFlag(returns aPatternCommandFlagDirectivediscriminated union) to replace the inline flag state machine incollectPatternCommandTokens. - Extracted
collectCommandTokens,collectGenericCommandTokens,collectRedirectTokensfromcollectPathCandidateTokens; converted both walkers from output-argument accumulator to return-basedstring[]. - Category: A + B (production clone + god functions)
- Outcome: clone removed;
collectPathCandidateTokensandcollectPatternCommandTokensdecomposed into focused helpers;bash-token-classification.tshas dedicated unit tests (43 tests) covering every rejection and acceptance branch.
- Extracted pure token classifiers into new
-
✅ Reduce
stripJsonCommentscomplexity (#290) - completed- Replaced the five-flag single-loop scanner with a stateless dispatcher delegating to three private consume helpers:
consumeLineComment,consumeBlockComment, andconsumeString, each returning aScanSegmentvalue ({ output, nextIndex }). - Added 14 direct unit tests for
stripJsonComments(the function was exported but had no dedicated coverage) to pin the contract before the refactor. - Category: B (god function)
- Outcome:
stripJsonCommentsno longer appears as a refactoring target;config-loader.tsdropped from the CRAP-risk list; refactoring targets 4 → 3. - Commits:
test: add direct stripJsonComments unit tests,refactor: model stripJsonComments as consume helpers
- Replaced the five-flag single-loop scanner with a stateless dispatcher delegating to three private consume helpers:
-
✅ Extract shared test fixtures (#288) - completed
- Created
test/helpers/handler-fixtures.ts(makeCtx,makeEvents,makeSession,makeToolRegistry,makeToolCallEvent,makeCheckResult,makeHandler,getDecisionEvents),test/helpers/gate-fixtures.ts(makeDescriptor,makeRunnerDeps,makeTcc,makeGateCheckResult), andtest/helpers/manager-harness.ts(createManager). - Migrated handler-event clone family (
tool-call-events.test.ts,tool-call.test.ts,input-events.test.ts,input.test.ts,permission-session.test.ts), external-directory family, gate family (runner.test.ts,bash-path.test.ts,path.test.ts), manager harness (permission-system.test.ts), and lifecycle setup (before-agent-start.test.ts,lifecycle.test.ts). - Category: D (test duplication)
- Outcome: duplication 9.1% → 7.1%; clone groups 122 → 113; health deduction -4.1 → -2.1.
- Created
Step dependency diagram
Steps 1-5 are independent. Step 6 is best sequenced after the production refactors whose tested call sites it touches (dashed edges) - those refactors are behavior-preserving, so the soft ordering only avoids re-migrating fixtures, it does not block.
flowchart TD
S1["Step 1: Decompose handleToolCall (#285)"]
S2["Step 2: Decompose resolvePermissions (#286)"]
S3["Step 3: Decompose runGateCheck (#287)"]
S4["Step 4: Decompose bash-path-extractor (#289)"]
S5["Step 5: Reduce stripJsonComments (#290)"]
S6["Step 6: Extract shared test fixtures (#288)"]
S1 -.-> S6
S2 -.-> S6
S3 -.-> S6
S4 -.-> S6
Tracks
| Track | Steps | Description |
|---|---|---|
| A: Decision-path complexity | 1, 2, 3 | Decompose the three tool_call hotspots (independent, parallel) |
| B: bash-path-extractor | 4 | Remove the production clone and reduce the two collect-function hotspots |
| C: config-loader | 5 | Reduce stripJsonComments complexity (lowest priority) |
| D: Duplication | 6 | Extract shared test fixtures; best sequenced after Tracks A and B |