mirror of
https://bitbucket.org/siakitem/my-pi.git
synced 2026-08-28 08:35:57 +00:00
121 lines
12 KiB
Markdown
121 lines
12 KiB
Markdown
---
|
||
issue: 510
|
||
issue_title: "Thread an injected platform/path-semantics seam through the bash path pipeline"
|
||
---
|
||
|
||
# Retro: #510 — Thread an injected platform/path-semantics seam through the bash path pipeline
|
||
|
||
## Stage: Planning (2026-06-28T00:00:00Z)
|
||
|
||
### Session summary
|
||
|
||
Planned the refactor that completes the half-built platform seam in the bash path pipeline.
|
||
The operator's `ask_user` answers reshaped the design away from the issue's literal "thread a `PathSemantics`/`NodeJS.Platform` knob" framing toward a single injected collaborator — `PathNormalizer` — constructed at the edge with both `platform` and `cwd` baked in, handed raw tokens, and returning the prepared values (`AccessPath`s + routing answers) the gates expect ("prepare the data before evaluation, not during evaluation").
|
||
Confirmed a behavior-preserving `refactor:` that batches into [#508]'s `fix:` release, full enforcement scope (lint guard + `rule.ts`/`subagent-context.ts` cleanup), and a 10-step lift-and-shift TDD order.
|
||
|
||
### Observations
|
||
|
||
- **Design steer (via `ask_user`).**
|
||
Rejected both "bare `NodeJS.Platform`" and a passive `PathSemantics` value bag.
|
||
The operator's framing: hand paths to a collaborator that *owns* platform + cwd and figures out the normalized forms.
|
||
Result: `PathNormalizer` (name chosen over `AccessPathFactory`/`PathInterpreter`), single collaborator owning both `AccessPath` construction and routing (`isAbsolute`/`resolveBase`/`joinBase`/`isWithinDirectory`/`isOutsideWorkingDirectory`).
|
||
- **Two distinct edges.**
|
||
`process.platform` is process-global → read once in `index.ts` (factory body), injected into `PermissionManager` (for `rule.ts`) and `PermissionSession`.
|
||
`cwd` is session-scoped → not available in the factory body; arrives at `session_start` via `resetForNewSession(ctx)`, so `PathNormalizer` is built there and exposed via `getPathNormalizer()` on the existing `ToolCallGateInputs` seam.
|
||
- **`cwd` source change is the main risk.**
|
||
Today the pipeline reads `ctx.cwd` per tool call; baking it into the session normalizer assumes per-session stability.
|
||
Holds for Pi (a session is bound to one project dir), and `resetForNewSession` rebinds on every `/new`/`/resume`/`/fork`.
|
||
Flagged with a composition-root regression test.
|
||
- **Behavior-preserving by construction.**
|
||
Every converted interior op already used host `node:path`/`process.platform`, so the host-default result is identical; the only POSIX-hard-coded drift (`isRelativeCandidate`'s `startsWith("/")`) is deliberately left as-is and deferred to [#508], keeping this a pure `refactor:` with no observable POSIX change.
|
||
`canonicalizePath`'s new `win32` split is a no-op on POSIX and a latent fix validated by injected-`win32` unit tests.
|
||
- **Release framing.**
|
||
Not in the Phase 7 roadmap (the issue proposes it as a new step); marker is "ship independently" with the rationale that a `refactor:` does not cut its own release — it auto-batches into [#508]'s `fix:`.
|
||
Avoided the formal batch-tail marker since there is no named roadmap batch.
|
||
- **Scope boundary vs. [#505].**
|
||
`PathNormalizer` is a facade over the platform-parameterized `path-utils`/`AccessPath`, not a relocation; the Phase 7 [#505] path-utils dissolution can later move internals behind it without re-touching the seam.
|
||
No new follow-up issue filed (the deferred `isRelativeCandidate` conversion already lives in [#508]).
|
||
- **Testability payoff.**
|
||
The whole point is exercising Windows behavior on a POSIX CI by injecting a `win32` `PathNormalizer` — no `vi.mock("node:path")`.
|
||
|
||
## Stage: Implementation — TDD (2026-06-28T18:35:00Z)
|
||
|
||
### Session summary
|
||
|
||
Completed the 10-step plan: steps 1–6 (leaf-normalizer platform flavor, `AccessPath` platform option, the `PathNormalizer` collaborator, session-edge construction, the `BashPathResolver` rename, and the gate migration) landed in a prior session; this session executed steps 7–10 (inject `platform` into `rule.ts` evaluation, into `subagent-context.ts` detection, the `process.platform` ESLint guard + removal of all interior defaults, and the architecture/SKILL docs).
|
||
The test count rose from 2183 to 2189 (+6: the `rule.ts` and `subagent-context.ts` win32/posix injection assertions).
|
||
Final state: all 10 `#510` commits green on `pnpm run check` / `pnpm run lint` / 2189 tests / `pnpm fallow dead-code`; pre-completion reviewer returned **PASS**.
|
||
|
||
### Observations
|
||
|
||
- **`required` vs defaulted platform param (operator decision via `ask_user`).**
|
||
Chose required params (no `= process.platform` default) on `rule.ts`, `subagent-context.ts`, and every `path-utils`/`canonicalize-path` leaf — fully `tsc`-enforced threading — over a lower-churn posix-literal default.
|
||
`PermissionManager`'s constructor option `platform?` keeps an internal `?? "linux"` default (its only production caller, `index.ts`, passes `hostPlatform`), which contained the manager-construction test churn.
|
||
- **`evaluate` param reorder.**
|
||
`evaluate`'s optional `defaultAction` blocked a required trailing `platform`, so the signature became `evaluate(surface, pattern, rules, platform, defaultAction?)`.
|
||
Migrated ~84 `rule.ts`/`synthesize`/`session-rules` test call sites with a paren-balancing Python script (append `"linux"` for 3-arg calls, insert before `"deny"` for `defaultAction` calls, drop the old `undefined,` slot for the win32 calls).
|
||
- **Step-9 blast radius exceeded the plan (operator-confirmed deviation).**
|
||
The plan named only `isPathWithinDirectory`/`isPiInfrastructureRead`, but the package-wide lint guard forbids the *text* `process.platform`, so **all** leaf defaults had to go.
|
||
That forced threading `platform` to ~6 production sites the plan did not enumerate: `input-normalizer` (via `manager.platform`), the `tool`/`skill-read`/`external-directory` gates (via a new `ToolCallGateInputs.getPlatform()` off the session), and `skill-prompt-sanitizer` (via `before-agent-start` → `session.getPlatform()`).
|
||
Test churn (~93 path-utils call sites + `AccessPath.forPath({ cwd })` object injection) was automated with a second paren-balancing script.
|
||
- **Lint-guard sanity check gotcha.**
|
||
Verifying the guard fires (temporarily adding an interior `process.platform`) used `git checkout` to revert — which also reverted the *uncommitted* step-9 change to `canonicalize-path.ts`.
|
||
Caught it (`grep "= process.platform"`) and re-applied before committing.
|
||
- **`getPlatform()` alongside `getPathNormalizer()`.**
|
||
Two session accessors on `ToolCallGateInputs`: the normalizer for the bash pipeline, and the bare `platform` for the rule/gate sites that call raw `path-utils`/`isPiInfrastructureRead` rather than an `AccessPath` op.
|
||
The reviewer confirmed the split is correct (path-interpretation vs. rule case-folding are distinct concerns).
|
||
- **Pre-completion reviewer: PASS** — all deterministic checks green, all cross-step invariants ([#418], [#393], [#308], [#382], [#478]) preserved, all 4 Mermaid diagrams validated, no dead code, docs forward/reverse complete.
|
||
No WARN findings.
|
||
- **Doc note.** `architecture.md` recorded the seam as "### Related: PathNormalizer platform seam ([#510])" under Phase 7 (a precursor refactor, not one of the five Phase 7 steps), updated the `cwd-projection.ts` → `bash-path-resolver.ts` rename, the `BashProgram.parse` signature, the `evaluate()` pseudo-code, and added a `path-normalizer.ts` module entry.
|
||
|
||
## Stage: Final Retrospective (2026-06-28T20:30:00Z)
|
||
|
||
### Session summary
|
||
|
||
Shipped [#510] as `pi-permission-system` v17.1.0 (the lone `feat:` — `add PathNormalizer collaborator` — promoted the otherwise-`refactor:` batch into a minor release, exactly as the plan predicted).
|
||
During the post-implementation review the operator's question "what besides `PathNormalizer` references the platform-taking utilities?"
|
||
surfaced residual `getPlatform()` threading the seam had left behind, which was documented in `architecture.md` and filed as follow-up [#511].
|
||
The dominant arc-wide theme: the operator's `required`-param choice generated large mechanical test churn, and the step-9 lint-guard blast radius repeatedly exceeded the plan's stated scope.
|
||
|
||
### Observations
|
||
|
||
#### What went well
|
||
|
||
- **Paren-balancing migration scripts.**
|
||
Two disposable Python scripts (top-level-arg splitter + balanced-paren insertion) migrated ~177 call sites across `rule.ts`/`synthesize`/`session-rules` (step 7) and the path-utils/`AccessPath.forPath` consumers (step 9) — a novel, effective answer to a required-param signature change with noise-arg churn that hand-editing would have made error-prone.
|
||
- **Operator Q&A surfaced real debt.**
|
||
The "what else references these utilities?"
|
||
question caught the residual `getPlatform()` straggler sites (infra-read containment, skill-prompt sanitization) the implementation left un-folded; it was mapped to the Phase 7 roadmap and tracked as [#511] rather than left implicit.
|
||
A clean bidirectional win.
|
||
- **Clean ship.**
|
||
The release-please PR's `UNSTABLE`-no-checks state was the expected `GITHUB_TOKEN` case; the documented `gh pr merge --rebase` fallback worked, and v17.1.0 landed by rebase with the baseline auto-advanced.
|
||
|
||
#### What caused friction (agent side)
|
||
|
||
- `missing-context` (planning) — step 9 underscoped the lint-guard blast radius: the plan named only `isPathWithinDirectory`/`isPiInfrastructureRead`, but a `no-restricted-syntax` guard bans the *text* `process.platform` everywhere, so **all** seven leaf defaults had to go in one atomic commit, making each param required and cascading to ~6 unplanned production callers (`input-normalizer`, the `tool`/`skill-read`/`external-directory` gates, `skill-prompt-sanitizer`) plus ~93 test edits.
|
||
Impact: self-identified (caught by `tsc`), no wrong-direction rework, but two mid-TDD `ask_user` rounds, a new `getPlatform()` session accessor, and a second migration script.
|
||
- `premature-convergence` — the first param-style `ask_user` ("required vs posix-default") was framed around `rule.ts` before the path-utils blast radius was scoped, so the same underlying decision re-opened for a second `ask_user` at step 9.
|
||
Impact: two ask rounds for one decision; the second could have been folded in had the full caller set been enumerated before the first.
|
||
- `other` (tooling hazard) — the lint-guard sanity check (temporarily adding an interior `process.platform` to confirm the guard fires) used `git checkout` to revert, which also reverted the *uncommitted* step-9 change to `canonicalize-path.ts`.
|
||
Impact: caught immediately via `grep "= process.platform"` and re-applied; ~3 tool calls, no rework.
|
||
|
||
#### What caused friction (user side)
|
||
|
||
- Opportunity, not criticism: the `required`-vs-`defaulted` preference was asked twice across the TDD session.
|
||
Presenting the full blast radius (path-utils leaves + ~177 test edits) in the *first* question would have made it a single decision boundary — but that gap was the agent's scoping, not the operator's.
|
||
|
||
### Diagnostic details
|
||
|
||
- **Unused-tool (planning).**
|
||
A `grep -rn "process.platform"` plus a caller-enumeration pass at plan time would have surfaced the true blast radius (all seven leaf functions + ~6 production callers) instead of the representative two the plan named.
|
||
This is the root of the step-9 deviation.
|
||
- **Feedback-loop.**
|
||
Verification cadence was healthy: `tsc` after each interface-changing step, `vitest` per affected file, full suite + lint + `fallow` at TDD end, and lint + `fallow` pre-push.
|
||
No end-only-verification gap.
|
||
- **Model-performance.**
|
||
The only subagent (the `pre-completion-reviewer`, dispatched in the TDD session) ran fresh-context on judgment-heavy review work — an appropriate match; no mismatch.
|
||
|
||
### Changes made
|
||
|
||
1. `.pi/skills/testing/SKILL.md` — added a TDD-planning rule (after the "parameter that flows through callback chains" rule) that a plan adding a lint guard forbidding a global read bans the *text* everywhere (including `= process.platform` default params), so every default must be removed in the guard's commit and every occurrence + caller enumerated at plan time.
|