Files
my-pi/pi-permission-system/docs/architecture/history/phase-2-complexity-duplication.md
T

112 lines
11 KiB
Markdown

# 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
1.**Decompose `handleToolCall`** ([#285]) - **completed**
- Extracted `validateRequestedTool` (pure, exported) for the tool-name validation prelude.
- Extracted `runGate` closure (inside `handleToolCall`) for the unified bypass/runner/short-circuit shape.
- Collapsed the body to validate → build context → ordered producer-array pipeline.
- Outcome: `handleToolCall` no longer appears as a refactoring target; CRAP risk for the file dropped from 172 → 79.4 (now `handleInput`); refactoring targets 5 → 4.
2.**Decompose `resolvePermissions`** ([#286]) - **completed**
- Extracted `mergeScopesWithOrigins(scopes)` (into new `src/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: `resolvePermissions` no longer appears as a refactoring target; `permission-manager.ts` dropped from the CRAP-risk list.
3.**Thin `runGateCheck`** ([#287]) - **completed**
- Introduced `SessionApproval` value object (`src/session-approval.ts`) owning the `{pattern}|{patterns}` union; exposed `representativePattern` and `toGateApproval()`.
- `SessionRules.record(approval)` absorbs the per-pattern loop; `GateRunnerDeps` seam renamed to `recordSessionApproval(approval)` - runner tells the store, never interrogates the union.
- Extracted `buildDecisionEvent` into `helpers.ts` to deduplicate the `origin/agentName/matchedPattern ?? null` normalization across both emit sites.
- Outcome: `runner.ts` no longer appears as a refactoring target; refactoring targets 4 → 3.
4.**Decompose `bash-path-extractor.ts`** ([#289]) - **completed**
- Extracted pure token classifiers into new `src/handlers/gates/bash-token-classification.ts`; private `rejectNonPathToken` predicate eliminates the 31-line rejection-prelude clone.
- Extracted `classifyPatternCommandFlag` (returns a `PatternCommandFlagDirective` discriminated union) to replace the inline flag state machine in `collectPatternCommandTokens`.
- Extracted `collectCommandTokens`, `collectGenericCommandTokens`, `collectRedirectTokens` from `collectPathCandidateTokens`; converted both walkers from output-argument accumulator to return-based `string[]`.
- Category: A + B (production clone + god functions)
- Outcome: clone removed; `collectPathCandidateTokens` and `collectPatternCommandTokens` decomposed into focused helpers; `bash-token-classification.ts` has dedicated unit tests (43 tests) covering every rejection and acceptance branch.
5.**Reduce `stripJsonComments` complexity** ([#290]) - **completed**
- Replaced the five-flag single-loop scanner with a stateless dispatcher delegating to three private consume helpers: `consumeLineComment`, `consumeBlockComment`, and `consumeString`, each returning a `ScanSegment` value (`{ 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: `stripJsonComments` no longer appears as a refactoring target; `config-loader.ts` dropped from the CRAP-risk list; refactoring targets 4 → 3.
- Commits: `test: add direct stripJsonComments unit tests`, `refactor: model stripJsonComments as consume helpers`
6.**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`), and `test/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.
## 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.
```mermaid
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 |
[#266]: https://github.com/gotgenes/pi-packages/issues/266
[#285]: https://github.com/gotgenes/pi-packages/issues/285
[#286]: https://github.com/gotgenes/pi-packages/issues/286
[#287]: https://github.com/gotgenes/pi-packages/issues/287
[#288]: https://github.com/gotgenes/pi-packages/issues/288
[#289]: https://github.com/gotgenes/pi-packages/issues/289
[#290]: https://github.com/gotgenes/pi-packages/issues/290