5.7 KiB
issue, issue_title
| issue | issue_title |
|---|---|
| 304 | Consolidate bash command analysis behind a single parsed representation and a candidate-combination helper |
Retro: #304 — Consolidate bash command analysis
Stage: Planning (2026-06-01T20:26:00Z)
Session summary
Issue #304 was created during the planning session for #301 in response to the question "what architectural changes would make this easier?". While planning the #301 bash command-chain fix, the friction analysis surfaced two structural gaps in the bash permission path: no shared parsed-bash representation (three independent tree-sitter parses) and a duplicated most-restrictive candidate-selection loop across the two bash gates. The owner chose Beck-style "refactor first, then a trivial fix", so #304 captures the behavior-preserving enabler and #301 becomes a follow-up that builds on it.
Observations
- Scope was deliberately trimmed from the issue's high-level framing.
The issue text mentioned a dual-strategy combinator (
first-non-default/most-restrictive); the plan narrows #2 to a result-levelpickMostRestrictiveonly, becausefirst-non-defaultlives at the rule level (evaluateFirst) one layer below and merging the two layers is out of scope. Adding an unused strategy parameter would be a speculative export (fallow would flag it). - The two bash gates share a most-restrictive core but wrap it in different filters — the path gate's #58 backward-compat ("token matching only the universal default is unrestricted") plus session-coverage, and the external-directory gate's "uncovered =
state !== allow". SopickMostRestrictiveis the right shared seam; the filters stay gate-specific. The external-directory gate is a clean drop-in; the path gate needs care to preserve #58 and loses its deny short-circuit (output-identical, slightly more in-memorycheckPermissioncalls). BashProgram(#1) is honestly the lower-leverage of the two enablers near-term: the two extractors already share the AST walker, so #1's win is cohesion and an extensible seam for #301, not fewer parses. Parse-once-and-inject across gates was deferred — it changes gate signatures and drifts into the deferred gate-consolidation enabler (#4).- Kept the existing extractor exports (
extractTokensForPathRules,extractExternalPathsFromBashCommand) as thin facades overBashProgramspecifically to avoid rewriting the 900-linetest/bash-external-directory.test.ts(lift-and-shift / large-test-file rule). - Risk flagged: moving the parse/walk primitives into
bash-program.tsto avoid a circular import is the largest single edit; it is mechanical and gated by the unchanged extractor suite +pnpm run check. - Labels available are coarse (no
refactor/tech-debt); filed asenhancement+pkg:pi-permission-system.
Diagnostic details
- Feedback-loop gap analysis — Two steps (path-gate refactor; cross-module primitive move) are explicitly paired with
pnpm run checkin the plan because they are behavior-preserving moves that the type checker, not the test suite alone, will catch first.
Stage: Implementation — TDD (2026-06-01T20:46:09Z)
Session summary
Executed all four planned steps as behavior-preserving refactors: extracted pickMostRestrictive (candidate-check.ts) and migrated both bash gates onto it, introduced the BashProgram value object (bash-program.ts) owning the tree-sitter primitives with the old extractors reduced to thin facades, and updated the architecture directory listing.
Test count went from 1674 to 1686 (+12: six pickMostRestrictive cases, six BashProgram cases); the 900-line extractor suite and both bash-gate suites stayed green unchanged, confirming behavior preservation.
Observations
- Pre-completion reviewer: PASS.
- Reviewer warnings (all non-blocking, left as-is):
bash-path.tsrecovers the worst token by reference identity (uncovered.find(({ check }) => check === worstCheck)) afterpickMostRestrictive(uncovered.map(({ check }) => check)). Correct because.map()does not clone; kept the helper checks-only since that is the shared seam with the external-directory gate.bash-external-directory.tsends withpickMostRestrictive(...) ?? uncoveredEntries[0].check; the fallback is logically unreachable (the empty case returns earlier) but is required becausepickMostRestrictivereturnsPermissionCheckResult | undefinedand the type checker needs the narrowing.bash-program.tsplaces the tree-sitter bootstrap (getParseretc.) above the exportedBashProgramclass; all declarations hoist so ordering is safe.
- Baseline was not clean: a pre-existing
MD053lint failure in the0301plan (a self-referential[#301]:link definition left by the prior planning session) was fixed first asdocs: remove self-referential issue link from #301 plan. - Fallow false positive:
BashProgram's private constructor + staticparse()factory defeats fallow's syntactic-only analysis (no compiler), so it reportspathTokens/externalPathsas unused class members. Suppressed with// fallow-ignore-next-line unused-class-member(note: the inline issue kind is singularunused-class-member, even though the.fallowrc.jsonrule key is pluralunused-class-members; the suppression line must contain only the kind — trailing prose is parsed as bogus issue kinds). This suppression landed as its ownrefactor:commit rather than in the trailingdocs:commit. - No deviations from the plan's Module-Level Changes;
v3-architecture.mdwas reviewed and correctly left unchanged (historical pre-refactor narrative, does not enumerate current gate modules). - This unblocks #301, which can now add
BashProgram.topLevelCommands()plus a bash command gate selecting withpickMostRestrictive.