Files
my-pi/pi-permission-system/docs/plans/0474-extract-bash-token-collection.md

267 lines
19 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
issue: 474
issue_title: "pi-permission-system: extract bash token collection from bash-program.ts (Phase 6 Step 2)"
---
# Extract bash token collection from `bash-program.ts` (Phase 6 Step 2)
## Release Recommendation
**Release:** mid-batch — defer (batch "bash-program-decomposition"); confirm at ship time
This is Step 2 of the three-step bash-program decomposition track (Steps 13, [#473]/[#474]/[#475]).
The roadmap tags it `Release: batch "bash-program-decomposition"`, whose tail is Step 3 ([#475]).
Step 1 has already landed on `main` (commits `999dc52`/`02058e3`/`7626425`) with its release deferred; Step 2 lands the same way and ships together with Step 3.
All three commits are `refactor:` (non-bumping under `release-please-config.json`), so the batch produces no release until Step 3 carries a `feat:`/`fix:` commit — flagged for the Step 3 ship decision, not this one.
## Problem Statement
`bash-program.ts` is the package's #1 churn × complexity hotspot (1,143 LOC at Phase 5 close; 1,045 LOC after Step 1).
It mixes several concerns behind the `BashProgram` value object: argument/flag tokenization, command enumeration, and the `cd`-fold projection.
Argument and flag tokenization — deciding which tokens of a command are filesystem paths versus inline patterns/scripts (`sed`, `awk`, `grep`, `rg`, `sd`) — is a distinct concern from the `BashProgram` API.
It is the single largest cohesive block in the file (~350 LOC), so moving it is the biggest single reduction in the decomposition.
## Goals
- Extract the pattern-first command table, the flag classifier, and the token collectors into a focused `src/access-intent/bash/token-collection.ts`.
- Relocate the `ARG_NODE_TYPES` grammar set into `node-text.ts` (alongside its peer `SKIP_SUBTREE_TYPES`).
- Leave `bash-program.ts` importing the collectors it still drives from the cwd-projection walk.
- Pure lift-and-shift: no behavior change.
- Add direct unit tests for the newly-isolated collectors (a testability win the extraction enables).
This change is **not breaking** — it relocates private internals with no public-API, config, schema, or behavior change.
## Non-Goals
- Command enumeration (`collectCommands` / `collectCommandsInto` / substitution descent) and the `cd`-fold projection (`collectPathCandidates`, `walkCurrentShellSequence`, `walkPipeline`, `foldCd`, helpers) stay in `bash-program.ts` — that is Step 3 ([#475]).
- Relocating `bash-program.ts` itself out of `handlers/gates/` — Step 3.
- Renaming `extractCommandName` — it stays a bash-domain command-identity query under its current name (see Design Overview).
- Any change to the `PATTERN_FIRST_COMMANDS` contents, flag arity, or tokenization semantics.
## Background
After Step 1, `bash-program.ts` already imports two leaf modules from the seeded `src/access-intent/bash/` domain directory:
- `parser.ts``getParser`, exported `TSNode` type.
- `node-text.ts``resolveNodeText`, `SKIP_SUBTREE_TYPES`.
The block this step moves currently sits between the `BashProgram` class and the command-enumeration section.
It comprises (current line numbers in `src/handlers/gates/bash-program.ts`):
- `PatternCommandConfig` (interface) + `PATTERN_FIRST_COMMANDS` (table) — lines 238337.
- `ARG_NODE_TYPES` (grammar node-type set) — lines 339344.
- `extractCommandName` (command-node → basename query) — lines 346357.
- `PatternCommandFlagDirective` (type) + `classifyPatternCommandFlag` — lines 369401.
- `collectPatternCommandTokens` — lines 419494.
- `collectGenericCommandTokens` — lines 497533.
- `collectRedirectTokens` — lines 535549.
- `collectCommandTokens` — lines 552561.
- `collectPathCandidateTokens` — lines 573584.
- The "Token classification is delegated to `bash-token-classification.ts`" note — lines 586588.
An orphan `// ── AST walker ──` section header at line 234 (emptied when Step 1 removed the parser/node-text it labeled) sits directly above this block.
**Shared-dependency constraint (the crux of this extraction).**
Two symbols in the block are also used by code that stays behind (the cwd-projection that Step 3 extracts):
- `extractCommandName` — used by `collectCommandTokens` (moves) **and** `foldCd` (stays, line 973).
- `ARG_NODE_TYPES` — used by the collectors (move) **and** `cdLiteralTarget` (stays, line 996).
Neither can stay in `bash-program.ts`: `token-collection.ts` will import the collectors' dependencies, and if `extractCommandName`/`ARG_NODE_TYPES` stayed in `bash-program.ts` while `token-collection.ts` imported them, `bash-program.ts` would import the collectors back — a circular import.
Three collectors are consumed by the staying cwd-projection walk and so must be **exported** from the new module:
- `collectCommandTokens``walkForCandidates`, line 760.
- `collectRedirectTokens``foldPipelineFirstStage`, line 871.
- `collectPathCandidateTokens``walkForCandidates`/`walkPipeline`/`foldPipelineFirstStage`/`foldListExceptTerminal`, lines 785/844/881/909.
A repo-wide grep (`src/`, `test/`, `.pi/skills/package-pi-permission-system/SKILL.md`) confirms **no external consumer** references any moved symbol — the collectors are exercised only through `BashProgram`'s public slices.
The SKILL has no reference to these internals and needs no edit.
AGENTS.md / SKILL constraints that apply:
- `docs/architecture/architecture.md` carries a layout listing that names the affected modules — it must be updated (see Module-Level Changes).
- Mark the roadmap step complete (`✅` on the Step 2 heading **and** the Mermaid `S2` node) as part of this change once the code lands — do not defer the marker.
## Design Overview
### Layer the two shared symbols by meaning, not by mechanics
`extractCommandName` and `ARG_NODE_TYPES` are different *kinds* of thing, and they go to different homes:
- `ARG_NODE_TYPES` is **tree-sitter grammar mechanics** — "which node types are argument values," a direct peer of `SKIP_SUBTREE_TYPES`.
It carries no bash-program meaning, so it sinks into `node-text.ts` alongside its peer.
- `extractCommandName` answers a **bash-program-domain** question — "what command is being invoked here?"
That it is *implemented* by calling `resolveNodeText` + `basename` is incidental mechanics, not its identity.
It belongs with the bash command-interpretation logic (`token-collection.ts`, whose `collectCommandTokens` is its primary consumer), and it keeps its name — a `resolve*` rename would falsely advertise it as a generic tree-sitter primitive and pull it toward the wrong layer.
This split is justified by layer, not convenience: it creates **no new module dependency edge**.
The staying cwd-projection already depends on `token-collection.ts` (for the three exported collectors) and on `node-text.ts` (for `SKIP_SUBTREE_TYPES`), so it simply imports `extractCommandName` and `ARG_NODE_TYPES` along edges that already exist.
### Module dependency graph after this step
```text
parser.ts ──┐
├─► token-collection.ts ──► bash-program.ts (BashProgram + enumeration + cwd-projection)
node-text.ts┘ ▲ │
(resolveNodeText, └─────────────────────┘
SKIP_SUBTREE_TYPES, bash-program imports collectors + extractCommandName
ARG_NODE_TYPES) from token-collection; ARG_NODE_TYPES from node-text
```
`token-collection.ts` is a leaf-plus-one: it depends only on `parser.ts` and `node-text.ts`, never on `bash-program.ts`.
`bash-program.ts` depends on `token-collection.ts`.
No cycle.
### `token-collection.ts` public surface and call site
```typescript
// token-collection.ts — exported surface
export function extractCommandName(node: TSNode): string | undefined;
export function collectCommandTokens(node: TSNode): string[];
export function collectRedirectTokens(node: TSNode): string[];
export function collectPathCandidateTokens(node: TSNode): string[];
// private: PatternCommandConfig, PATTERN_FIRST_COMMANDS,
// PatternCommandFlagDirective, classifyPatternCommandFlag,
// collectPatternCommandTokens, collectGenericCommandTokens
```
The staying cwd-projection in `bash-program.ts` consumes them exactly as today (Tell-Don't-Ask: each takes a `TSNode` and returns a value; no shared mutable bag, no output arguments):
```typescript
// bash-program.ts — walkForCandidates (unchanged behavior)
case "command":
tagTokens(collectCommandTokens(node), base, out); // imported
return foldCd(node, base); // foldCd calls extractCommandName (imported)
// ...
default:
tagTokens(collectPathCandidateTokens(node), base, out); // imported
return base;
```
### Design-review pass (extraction checklist)
- **Dependency width.**
All four exported functions take a single `TSNode` and return `string[]` (or `string | undefined`).
No options bag, no per-consumer field subset.
- **Law of Demeter / output arguments.**
The collectors were already converted to return-based `string[]` (no accumulator output argument) by [#289]; this move carries that property forward unchanged.
- **Tell-Don't-Ask.**
Walking a `TSNode` is intrinsic AST traversal, not a reach-through into a stranger collaborator.
- **Missing abstraction.**
`token-collection.ts` is the cohesive concept (argument/flag tokenization); `extractCommandName` is its command-identity primitive; `ARG_NODE_TYPES` is a grammar primitive that belongs with `node-text.ts`'s existing grammar set.
No new intermediate abstraction is warranted for a lift-and-shift.
No structural smells are introduced; the fixes are inline (this PR).
## Module-Level Changes
### `src/access-intent/bash/node-text.ts`
- **Add** `export const ARG_NODE_TYPES` (`new Set(["word", "concatenation", "string", "raw_string"])`) with a short doc comment, placed beside `SKIP_SUBTREE_TYPES`.
### `src/access-intent/bash/token-collection.ts` (new)
- **Add** the moved block: `PatternCommandConfig`, `PATTERN_FIRST_COMMANDS`, `extractCommandName`, `PatternCommandFlagDirective`, `classifyPatternCommandFlag`, `collectPatternCommandTokens`, `collectGenericCommandTokens`, `collectRedirectTokens`, `collectCommandTokens`, `collectPathCandidateTokens`.
- **Export** `extractCommandName`, `collectCommandTokens`, `collectRedirectTokens`, `collectPathCandidateTokens`; keep the rest private.
- **Imports**: `basename` from `node:path`; `type TSNode` from `#src/access-intent/bash/parser`; `resolveNodeText`, `SKIP_SUBTREE_TYPES`, `ARG_NODE_TYPES` from `#src/access-intent/bash/node-text`.
- Order the file per the stepdown rule: public collectors first, then the private helpers/table they call.
### `src/handlers/gates/bash-program.ts`
- **Remove** the moved block (lines 238588) and the orphan `// ── AST walker ──` header (line 234), but **leave `ARG_NODE_TYPES`'s usage** at `cdLiteralTarget` (now imported).
- **Add** import: `{ collectCommandTokens, collectPathCandidateTokens, collectRedirectTokens, extractCommandName }` from `#src/access-intent/bash/token-collection`.
- **Update** the `node-text` import: drop now-unused `resolveNodeText` (every call site moved); keep `SKIP_SUBTREE_TYPES` (used by the staying walk at lines 805/836/898); add `ARG_NODE_TYPES`.
- **Update** the `node:path` import: drop `basename` (its only call site, `extractCommandName`, moved); keep `isAbsolute`, `join`, `resolve`.
- Keep `getParser` + `type TSNode` (`parser`) — both still used by the staying code.
### `docs/architecture/architecture.md`
- **Layout tree**: add a `token-collection.ts` entry under `access-intent/bash/`; update the `node-text.ts` entry to note it now also exports `ARG_NODE_TYPES`; update the `bash-program.ts` entry to note the collectors + `extractCommandName` are imported from `access-intent/bash/token-collection.ts` and `ARG_NODE_TYPES` from `access-intent/bash/node-text.ts`.
- **Steps**: mark Step 2 complete — `✅` on the `#### 2.` heading and on the Mermaid `S2` node.
- **Step 2 prose tidy**: the entry's prose names the target file `bash-token-collection.ts` while its bullet (and #475) name it `token-collection.ts`; correct the prose to `token-collection.ts` to match the actual filename.
Note the `ARG_NODE_TYPES → node-text.ts` and `extractCommandName → token-collection.ts (kept name)` layering in the Step 2 entry so the roadmap reflects what shipped.
No `README.md`, schema, config, or `SKILL.md` change — none reference these internals or any user-facing command.
Historical references in `docs/plans/archive/`, prior plans, and retros are not edited.
## Test Impact Analysis
1. **New tests the extraction enables.**
A new `test/token-collection.test.ts` can directly unit-test the collectors, which today are reachable only through `BashProgram`'s public slices:
- `extractCommandName` — basename of `/usr/bin/sed``sed`; `undefined` for a variable-expansion command name.
- `collectCommandTokens` dispatch — pattern-first command (`sed -e 's/x/y/' file.txt` collects `file.txt`, skips the script positional), generic command, `sd`'s two pattern positionals, `--` end-of-flags, arg-consuming vs file-consuming flags.
- `collectRedirectTokens` — redirect-destination tokens from a `file_redirect` node.
- `collectPathCandidateTokens` — skips `SKIP_SUBTREE_TYPES` subtrees (heredoc/comment), recurses into substitutions.
These tests parse a command via `getParser()` and pass the resulting node, mirroring the Step 1 `node-text.test.ts` pattern.
**Testability win**: `token-collection.ts` imports only `parser.ts` + `node-text.ts` (not `#src/canonicalize-path`), so its tests run without the canonicalize mock that any `bash-program.ts` importer needs (retro 0345) — the same isolation Step 1 gained.
2. **Tests that become redundant.**
None.
The existing bash suites (`test/bash-arity.test.ts`, `test/bash-external-directory.test.ts`, `test/detect-permissive-bash-fallback.test.ts`) exercise the collectors only *indirectly* through `BashProgram.externalPaths` / `pathRuleCandidates` / `commands`.
The new unit tests are strictly lower-level; the integration tests remain the behavior-preservation net and are not simplified or removed here.
3. **Tests that must stay as-is.**
All existing bash integration tests — they pin the end-to-end tokenization → policy behavior that this lift-and-shift must not change.
## Invariants at risk
This is a behavior-preserving lift-and-shift; the risk is a silent behavior change in tokenization or in the staying `cd`-fold walk.
- **Step 1 invariant** ([#473]): `bash-program.ts` imports the parser and node-text resolver; behavior unchanged.
Adding `ARG_NODE_TYPES` to `node-text.ts` extends that module without altering `resolveNodeText`/`SKIP_SUBTREE_TYPES` — pinned by `test/node-text.test.ts` (parser/resolver behavior) and the bash integration suites.
- **Tokenization behavior** ([#289] decomposition, [#307] cwd-projection, [#454] redirect-then-pipe fold): the candidate set and order, and the projected effective base at each token, must be identical.
Pinned by `test/bash-external-directory.test.ts`, `test/bash-arity.test.ts`, and `test/detect-permissive-bash-fallback.test.ts`.
The invariant lives in tests, not only prose — run the full bash suite after the extraction commit, not just the new file.
No new test is required to protect an otherwise-unpinned invariant; run `pnpm run check` + the full suite after each cycle.
## TDD Order
1. **`refactor:` — move `ARG_NODE_TYPES` to `node-text.ts`.**
Add the exported set (with doc comment) to `node-text.ts`; import it back into `bash-program.ts` and delete the local definition in the **same commit** (Biome `noRedeclare` / `noUnusedImports` gate the two-step add-then-remove order).
The still-resident collectors and `cdLiteralTarget` reference the imported constant.
Run `pnpm --filter @gotgenes/pi-permission-system run check` + the full package suite.
Commit: `refactor(pi-permission-system): move ARG_NODE_TYPES grammar set to node-text.ts (#474)`.
2. **`refactor:` — extract the token collectors to `token-collection.ts` (atomic).**
Write `test/token-collection.test.ts` first importing from `#src/access-intent/bash/token-collection` (red: module absent → `tsc`/import failure).
Then create `token-collection.ts` with the moved block (exporting the four symbols), and in the **same commit** rewire `bash-program.ts`: add the `token-collection` import, remove the moved block + orphan `AST walker` header, drop the now-dead `basename` and `resolveNodeText` imports.
Removing the local definitions and exporting from the new module must land together — `tsc` will not allow the export/removal and the consumer rewire in separate commits.
Run `pnpm run check` + the full suite (green).
Commit: `refactor(pi-permission-system): extract bash token collection to access-intent/bash/token-collection.ts (#474)`.
3. **`docs:` — record the extraction and mark the roadmap step complete.**
Update `docs/architecture/architecture.md`: layout tree (`token-collection.ts` entry, `node-text.ts` `ARG_NODE_TYPES` note, `bash-program.ts` import note), `✅` on the Step 2 heading + Mermaid `S2` node, and the `bash-token-collection.ts``token-collection.ts` prose tidy with the layering note.
Run `pnpm run lint` (rumdl).
Commit: `docs(pi-permission-system): record token-collection extraction in architecture (#474)`.
The pre-completion-reviewer subagent runs after Cycle 3 per the `pre-completion` skill.
## Risks and Mitigations
- **Silent tokenization change.**
Mitigation: pure relocation — no logic edits; the full bash integration suite (run after Cycles 1 and 2) pins the candidate set, order, and projected base.
- **Circular import between `bash-program.ts` and `token-collection.ts`.**
Mitigation: `extractCommandName` moves *into* `token-collection.ts` (not left behind), so the dependency is strictly `bash-program → token-collection`, never back.
- **Dropped-import false green.**
`tsc` does not error on a leftover unused `import type`, but `basename`/`resolveNodeText` are value imports — Biome `noUnusedImports` flags them, and the autoformatter runs after each edit.
Mitigation: re-read the `bash-program.ts` import block after Cycle 2 and confirm `basename` and `resolveNodeText` are gone while `SKIP_SUBTREE_TYPES`/`ARG_NODE_TYPES`/`TSNode`/`getParser` remain.
- **LOC target.**
Removing ~355 lines lands `bash-program.ts` at ~690 LOC — slightly above the roadmap's "≤ 670" estimate for this step, but the projection is approximate and the remainder clears in Step 3 (target ≤ 350).
Not a blocker for a behavior-preserving move; note the actual figure in the retro.
## Open Questions
None.
The shared-symbol placement (the only design ambiguity) is resolved above: `ARG_NODE_TYPES``node-text.ts`, `extractCommandName``token-collection.ts` (name kept).
No follow-up issues are needed — command enumeration and cwd-projection extraction are already tracked by [#475].
[#289]: https://github.com/gotgenes/pi-packages/issues/289
[#307]: https://github.com/gotgenes/pi-packages/issues/307
[#454]: https://github.com/gotgenes/pi-packages/issues/454
[#473]: https://github.com/gotgenes/pi-packages/issues/473
[#474]: https://github.com/gotgenes/pi-packages/issues/474
[#475]: https://github.com/gotgenes/pi-packages/issues/475