---
issue: 290
issue_title: "Reduce stripJsonComments complexity in config-loader.ts"
---

# Retro: #290 — Reduce stripJsonComments complexity in config-loader.ts

## Stage: Planning (2026-05-31T15:14:25Z)

### Session summary

Produced a numbered implementation plan to lower `stripJsonComments` cognitive complexity (31 → < 15) by replacing the five-flag single-loop scanner with a stateless dispatcher delegating to three private consume helpers (`consumeLineComment`, `consumeBlockComment`, `consumeString`), each returning a `ScanSegment` value.
The plan is behavior-preserving, adds direct unit tests for the already-exported `stripJsonComments`, and is structured as three TDD commits (`test:` pin contract → `refactor:` dispatcher → `docs:` architecture update).

### Observations

- Chose the issue's consume-helper option over the mode-discriminant step-function option: a `step(state, char)` function would mutate a shared state bag (output-argument smell) and re-encode the same five flags, so it relocates rather than removes the interleaving.
  Each consume helper returns a value and owns one JSONC sub-grammar — genuine decomposition per the `code-design` heuristics.
  Did not invoke `ask_user` — the choice is resolvable by project design principles and the change is small and reversible.
- `stripJsonComments` is `export`ed and consumed by both `config-loader.ts` (`loadUnifiedConfig`) and `policy-loader.ts`, but had no dedicated unit test — Step 1 pins its full contract directly before the refactor, so the new tests pass against today's implementation and act as the behavior-preservation net.
- No exports change and no symbol is renamed, so no `index.ts` barrel, package skill, or other doc needs updating — only `docs/architecture/architecture.md` (Phase 2 Step 5, findings row 5, worst-CRAP-risk line, metrics).
- `design-review` skill judged not applicable: the change is one self-contained pure function with no shared-interface or layer-wiring impact.
- Block-comment scan is planned to switch from a character loop to `indexOf("*/")` (behavior-identical, including the unterminated-to-EOF branch) — flagged as a risk with a dedicated test.
- markdownlint is not installed locally (`markdownlint-cli2` not found; no `.markdownlint*` config); relied on the `markdown-conventions` skill. `rumdl fmt` ran in the pre-commit hook and passed.

## Stage: Implementation — TDD (2026-05-31T15:23:14Z)

### Session summary

Completed all 3 TDD steps: pinned 14 direct unit tests for `stripJsonComments` (Step 1), replaced the five-flag scanner with the stateless dispatcher + three consume helpers (Step 2), and updated `docs/architecture/architecture.md` to mark Phase 2 Step 5 complete (Step 3).
Test count: 1614 → 1628 (+14).
A `style:` cleanup commit was added after the pre-completion review to fix helper ordering.

### Observations

- Step 1 required two assertion corrections: (1) the space before `//` is emitted verbatim, so the expected output was `'{ \n"k": 1}'` not `'{\n"k": 1}'`; (2) the combined JSONC round-trip test had a stray `,` after a stripped block comment rendering the output invalid JSON — restructured the document so comments are inline on value lines.
  Both caught before the step 1 commit; the pre-existing implementation was never at fault.
- ESLint auto-fixed bracket notation to dot notation (`parsed["debugLog"]` → `parsed.debugLog`) during the pre-commit hook; accepted the change.
- The `refactor:` commit placed the three consume helpers *before* `stripJsonComments`, inverting the stepdown rule (plan said "placed directly below `stripJsonComments`").
  The pre-completion reviewer flagged this as WARN; fixed in a `style:` commit (`4ff870a1`) after the review.
- `fallow health --targets` confirmed `config-loader.ts` / `stripJsonComments` no longer appears as a refactoring target after the refactor; architecture doc updated accordingly (targets 4 → 3).
- Pre-completion reviewer: **WARN** (one finding — stepdown order, resolved before final commit).
  All deterministic checks PASS.

## Stage: Final Retrospective (2026-05-31T15:39:45Z)

### Session summary

Shipped issue #290 across three stages (plan → TDD → ship) with no logic rework: a behavior-preserving decomposition of `stripJsonComments` into a stateless dispatcher plus three pure consume helpers, with 14 new unit tests pinning the contract (1614 → 1628).
CI passed first try; no release-please PR (all commits were `test:`/`refactor:`/`style:`/`docs:`).
The single follow-up was a `style:` commit (`4ff870a1`) fixing helper ordering, prompted by the pre-completion reviewer's one WARN.

### Observations

#### What went well

1. The plan's design reasoning held up end-to-end.
   The consume-helper approach (chosen over the mode-discriminant alternative) was behavior-preserving as predicted, and `fallow health --targets` confirmed `config-loader.ts` dropped off the refactoring-target list (4 → 3) exactly as the plan's Open Question anticipated.
2. Verification ran incrementally, not end-loaded.
   Green baseline (`check`/`lint`/`test`) before any TDD cycle, each cycle ran the affected file red-then-green, and the full suite plus `check`/`lint`/`fallow dead-code` ran after the last step.
3. The two Step 1 test-assertion bugs were caught during the red phase, before the commit — the space-before-`//` preservation and the stray-comma invalid-JSON case were both fixed without touching committed code or the production implementation.

#### What caused friction (agent side)

1. `instruction-violation` (reviewer-caught) — the `refactor:` commit (`483be378`) placed the three consume helpers *above* `stripJsonComments`, inverting the stepdown rule.
   The plan explicitly prescribed "placed directly below `stripJsonComments` per the stepdown rule," so the implementation had a written instruction and did not follow it.
   Impact: one follow-up `style:` commit (`4ff870a1`), no logic rework, no user intervention.
   This is the **second consecutive issue** with the identical friction: #289 fixed the same private-helper-before-export ordering in `style:` commit `55d2774a`, also reviewer-caught.
   Both sessions wrote the extracted helper above its caller (a "define before use" instinct that JS/TS hoisting makes unnecessary) and relied on the pre-completion reviewer to catch the stepdown inversion.

#### What caused friction (user side)

1. None.
   The design choice was resolvable from `code-design` principles, so no `ask_user` was warranted; the user's only involvement was launching each stage.

### Diagnostic details

- Model-performance correlation — the only subagent dispatch was the `pre-completion-reviewer` (pinned to `anthropic/claude-sonnet-4-6`, a valid registry alias); a judgment-heavy review on an appropriate model, no mismatch.
- Feedback-loop gap analysis — verification was incremental throughout; no end-loaded-verification flag.
- Escalation-delay and unused-tool lenses found nothing notable: the two Step 1 assertion bugs were each fixed in one edit, no sequence exceeded five tool calls on the same error, and no rabbit-holes or missing-context gaps arose that a subagent or `colgrep` would have prevented.

### Changes made

1. Appended this Final Retrospective stage entry to `packages/pi-permission-system/docs/retro/0290-decompose-strip-json-comments.md`.
2. Added a one-line note to the Stepdown rule in `.pi/skills/code-design/SKILL.md`: extracted helpers go *below* their caller, not above (hoisting makes "define before use" unnecessary).
   This closes the recurring stepdown-order friction caught by the pre-completion reviewer in both #289 (`55d2774a`) and #290 (`4ff870a1`).
