7.4 KiB
issue, issue_title
| issue | issue_title |
|---|---|
| 289 | Decompose bash-path-extractor.ts: shared token rejection + collect* complexity |
Retro: #289 — Decompose bash-path-extractor.ts
Stage: Planning (2026-05-31T13:44:10Z)
Session summary
Produced a 4-cycle TDD plan for Phase 2 Step 4: extract the shared token-rejection prelude and pure classifiers into a new bash-token-classification.ts module, then reduce the two collect* walker hotspots.
The plan is behavior-preserving — existing bash-external-directory.test.ts integration suites stay unmodified — with new unit tests added only for the extracted classifiers.
Observations
- The file has exactly two exports (
extractExternalPathsFromBashCommand,extractTokensForPathRules); every other symbol is private, and a grep acrosssrc/,test/, and the package SKILL confirmed no external consumer of the internals. This gave the extraction zero external blast radius. - Two design forks were surfaced via
ask_user. Chosen: (1) a newbash-token-classification.tsmodule with public API + dedicated unit tests (over keeping helpers private in-file), and (2) convertingcollect*to return-basedstring[](over preserving the mutatedtokensaccumulator). - Validated each extraction against the
code-design"returns a value / owns state / gives behavior to data" test:rejectNonPathTokenreturns a boolean and removes a genuine clone;classifyPatternCommandFlagreturns a discriminated-union directive (moves the flag decision onto data); the return-based conversion removes an output-argument pattern rather than relocating statements. - Kept
rejectNonPathTokenandclassifyPatternCommandFlagprivate to avoid afallowdead-export flag — only the two classifiers (consumed by the walker) are exported. - Flagged the Biome/ESLint assertion conflict up front: the
consume-argdirective variant carries a non-optionalnextArgActionso theswitchnarrows without!oras. - The
collect*return-based conversion must land in a single commit (Step 3) because the mutual recursion and shared accumulator break at the type level if split.
Stage: Implementation — TDD (2026-05-31T14:40:31Z)
Session summary
All 4 TDD cycles completed: new bash-token-classification.ts module with 43 unit tests (Step 1), clone removal by importing classifiers from the new module (Step 2), walker refactor to return-based string[] with four extracted helpers (Step 3), and architecture doc update marking Phase 2 Step 4 complete (Step 4).
A post-reviewer style: commit addressed two WARNs: removed an unreachable token.startsWith("~/") branch in classifyTokenAsRuleCandidate (covered by the earlier includes("/") check) and reordered the module to put exports first per the stepdown rule.
Test count: 1571 → 1614 (+43).
Observations
- Pre-completion reviewer returned PASS with two WARNs: (1) the unreachable
~/branch copied verbatim from the original classifier; (2) privaterejectNonPathTokenpreceding the exported classifiers against the "Public API first" convention. Both were addressed in astyle:commit before shipping. - Step 3 required exactly one atomic commit as planned — the mutual recursion between
collectPathCandidateTokensandcollectPatternCommandTokensmeant their signatures had to change together. ThePatternCommandFlagDirectivediscriminated union worked cleanly: theswitchondirective.kindnarrowsnextArgActionwithout any!orascasts, avoiding the Biome/ESLint assertion conflict flagged in the plan. collectRedirectTokenswas simplified to useARG_NODE_TYPES.has(child.type)(replacing the inline four-way||check), confirmed identical after comparing the original set literal toARG_NODE_TYPES.fallow dead-codepassed cleanly: both exported classifiers are consumed bybash-path-extractor.ts; private helpers (rejectNonPathToken,classifyPatternCommandFlag) carry no export risk.
Stage: Final Retrospective (2026-05-31T15:01:56Z)
Session summary
Shipped issue #289 across three stages (plan → TDD → ship) with no logic rework: a behavior-preserving decomposition of bash-path-extractor.ts that removed a 31-line classifier clone, extracted four walker helpers, and added 43 unit tests (1571 → 1614).
CI passed first try; no release-please PR (all commits were refactor:/test:/style:/docs:).
The single follow-up was a self-identified style: commit prompted by the pre-completion reviewer's two WARNs.
Observations
What went well
- The plan did real predictive work.
All three pre-identified risks materialized exactly and their mitigations worked first-try: the Biome/ESLint assertion conflict was avoided by the
PatternCommandFlagDirectivediscriminated union (no!/as), Step 3 needed exactly one atomic commit because of the mutual-recursion signature change, andfallow dead-codepassed because only the consumed classifiers were exported. - Verification ran incrementally, not just at the end.
A green baseline (
check/lint/test) was confirmed before any TDD cycle, each cycle ran the affected file red-then-green, and the full suite pluscheck/lint/fallow dead-coderan after the last step — no end-loaded verification gap. - The pre-completion reviewer earned its keep on a behavior-preserving refactor.
It caught latent dead code (
token.startsWith("~/")) that the plan had deliberately copied verbatim, demonstrating that "behavior-preserving verbatim copy" is exactly the situation where a fresh-context review pays off.
What caused friction (agent side)
missing-context(minor) — the unreachabletoken.startsWith("~/")branch inclassifyTokenAsRuleCandidate(subsumed by the earliertoken.includes("/")check) existed in the original code, was not noticed during planning or Step 1 test-writing, and was copied verbatim into the new module. The plan explicitly prescribed line-for-line copying for behavior preservation, so the dead branch rode along and the Step 1 tests pinned current behavior without a distinct case for it. Impact: one follow-upstyle:commit (55d2774a), self-identified via the pre-completion reviewer's WARN — no logic rework, no user intervention.
What caused friction (user side)
- None.
The only user decision point — the two design forks (new module vs. in-file; return-based vs. accumulator) — was surfaced proactively via
ask_userduring planning, and the answers shaped the plan cleanly with no later reversal.
Diagnostic details
- Model-performance correlation — the only subagent dispatch was the
pre-completion-reviewer, pinned toanthropic/claude-sonnet-4-6(a valid registry alias, confirmed against.pi/agents/pre-completion-reviewer.md), so no silent fallback to the parent model occurred. A judgment-heavy review on an appropriate model; no mismatch. - Feedback-loop gap analysis — verification was incremental throughout (baseline before TDD, red/green per cycle, full gate after the last step); no end-loaded-verification flag.
- Escalation-delay and unused-tool lenses found nothing notable (no rabbit-holes, no missing-context beyond the one minor item above).
Changes made
- Appended this Final Retrospective stage entry to
packages/pi-permission-system/docs/retro/0289-decompose-bash-path-extractor.md. NoAGENTS.mdor prompt changes — the session's single minor friction was self-corrected and already covered by the pre-completion reviewer, so no rule was warranted.