Files
my-pi/pi-permission-system/docs/retro/0562-path-flavor-platform-language.md
T

98 lines
10 KiB
Markdown
Raw 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: 562
issue_title: "Leaf path modules re-derive the win32 path flavor from a raw platform parameter"
---
# Retro: #562 — Leaf path modules re-derive the win32 path flavor from a raw platform parameter
## Stage: Planning (2026-07-10T00:00:00Z)
### Session summary
Planned Phase 10 Step 3: introduce `PathFlavor`, the resolved product of the single `platform === "win32"` decision, and thread it into the path leaves in place of the raw `platform` string.
The design deepened well past the issue's original "value object with a fold" framing through operator questioning: `PathFlavor` became a **behavioral collaborator** — the platform's path *language* (syntax `hasPathSeparator`, semantics `bashTokenShape`, equivalence `fold`/`comparable`/`isWithin`/`matchOptions`) — injected once from `index.ts`, dissolving `PathNormalizer`'s two `!== "win32"` guards and removing `NodeJS.Platform` from every domain signature.
Plan committed with a 10-step bottom-up lift-and-shift TDD order; follow-up [#571] filed for the deferred subagent-containment unification.
### Observations
- The operator explicitly pushed scope wider than the issue: three rounds of `ask_user` converged on (a) behavioral over data-bag, (b) tell-don't-ask (`hasPathSeparator` replacing the leaked `usesWindowsSeparators()` accessor read by `bash-path-resolver`), and (c) threaded construction from the composition root over internal construction.
- Zoom-out finding: every platform-conditional in the package factors into exactly three capability groups (syntax / semantics / equivalence), which is what justifies one cohesive `PathFlavor` object rather than a config bag.
- Two genuine findings surfaced during the full platform-shaped sweep: a second divergent containment algorithm in `subagent-context` (`isPathWithinDirectoryForSubagent`, the same must-agree smell — deferred to [#571] because unifying it is behavior-affecting), and the `BashDialect` axis (kept as one object because pi core fixes the win32⇔Git-Bash pairing — track-and-watch).
- Decided `impl: PlatformPath` is exposed, not wrapped — its post-migration consumers are all path-domain primitives and `PlatformPath` is Node's own strategy; wrapping would be pure ceremony.
Sealable later in two lines.
- `permission-manager.ts` can consume `PathFlavor` without violating ADR-0002 — the `no-restricted-imports` guard bans only `access-intent/access-path`, and `PathFlavor` is a plain value object in `src/path/`.
- Verified the whole change is behavior-preserving, so every implementation commit is `refactor:` (hidden changelog type) — the roadmap's `Release: independent` means it lands on `main` and auto-batches, not that it cuts its own release (Refs [#479]).
- Lift-and-shift bridge is safe: `pathFlavorForPlatform` returns cached singletons, so the transitional inline `pathFlavorForPlatform(platform)` at not-yet-migrated call sites cannot diverge and stays bypass-safe until step 8 removes it.
## Stage: Implementation — TDD (2026-07-11T21:40:00Z)
### Session summary
Executed all 10 planned steps as 11 commits (10 `refactor:` + 1 `docs:`), a bottom-up lift-and-shift that introduced `PathFlavor` and threaded it in place of the raw `platform` discriminator across every path leaf, `rule.ts`/`PermissionManager`, `PathNormalizer`, and subagent detection, injecting it once from `index.ts`.
The suite moved 2321 → 2329 (net +8: +16 `path-flavor.test.ts`, 8 from removed `usesWindowsSeparators` tests and merged duplicate classifier cases); `check`/`lint`/`fallow`/full-suite all green throughout, and the behavior-preserving invariants ([#382]/[#508], [#533], [#520], [#510]) stayed pinned.
Pre-completion reviewer: PASS — ready for `/ship-issue`.
### Observations
- The design over-delivered on the roadmap's headline metric: `platform === "win32"` *code* comparisons are exactly 1 (the factory).
The naive `grep 'platform === "win32"'` initially reported 3 because two `path-flavor.ts` doc comments quoted the phrase — reworded them (separate `refactor:` commit) so the metric grep honestly reports 1.
- Planned-metric deviation, recorded transparently rather than forced: the roadmap predicted `caseInsensitive` derivations “≤ 2” but the grep reports 4.
The real win32 match-options *literal* derivation dropped 2 → 1 (sole literal now in `path-flavor.ts`); the other 3 grep hits are the intrinsic `WildcardMatchOptions` definition in `wildcard-matcher.ts`, which is not a win32 derivation.
The architecture health-metrics row now decomposes the raw count instead of contorting code to hit “≤ 2”.
- `posixFlavor.bashTokenShape()` returning `{ kind: "plain" }` for every token is the keystone that let `PathNormalizer` drop *both* `!== "win32"` guards into one uniform `switch` — the posix “plain” branch exactly reproduces the old posix early-return, so no behavior changed.
- `hasPathSeparator` collapsed the classifier's two separator checks (`includes("/")` + `windowsSeparators && includes("\\")`) into one call and let `RuleCandidateOptions` be deleted outright — the tell-don't-ask win from the planning `ask_user` rounds paid off cleanly.
- Two perl-scripting hazards hit during the ~30-site test migration: (1) a bash `for f in $FILES` loop silently failed to apply (re-ran with explicit file args), and (2) a `classifyTokenAsRuleCandidate\(([^,)]+)\)` regex corrupted a string literal containing `)` (`"\\(group\\)"`), which `pnpm run check` did not catch (esbuild ran, the string was just wrong) — caught by rewriting that describe block by hand.
Reinforces the AGENTS.md warning against scripted multi-line substitution across similar blocks.
- ADR-0002 needed no edit: the manager now consumes `PathFlavor` but still never imports `AccessPath`, so the string boundary holds — the `no-restricted-imports` guard bans only `access-intent/access-path`.
## Stage: Final Retrospective (2026-07-11T22:10:00Z)
### Session summary
Executed and shipped the `PathFlavor` refactor across TDD (11 commits, all `refactor:` + docs) and ship (auto-batched, no release cut) stages.
Execution was notably clean — every step ran `check` + the affected test file + the full suite before committing, all stayed green, the pre-completion reviewer returned PASS, and nothing required post-commit rework.
The only friction was scripting/path hygiene on the ~30-site test migration, all caught before commit; the sole user intervention was a mis-guessed skill path.
### Observations
#### What went well
- Model-task correlation was well-matched across the arc: the deep design work (the polymorphism / behavioral-collaborator zoom-out) ran on `claude-fable-5` during planning, mechanical TDD execution on `claude-opus-4-8`, deterministic ship steps on `deepseek-v4-flash`, and the judgment-heavy pre-completion review on `claude-sonnet-5`.
No reasoning-weak-on-judgment or high-cost-on-mechanical mismatch.
- The lift-and-shift cached-singleton bridge held exactly as planned: every one of the 9 refactor steps compiled and passed the full suite before commit, so the 13-site discriminator removal never had a red intermediate state.
- Verification cadence was incremental, not end-loaded: `pnpm run check` after each shared-type change and the full 2329-test suite before each commit — the feedback-loop-gap lens found nothing.
#### What caused friction (agent side)
- `other` (scripted-edit delimiter trap) — a single-line `perl -pi -e 's/classifyTokenAsRuleCandidate\(([^,)]+)\)/...($1, posixPathFlavor)/g'` to inject a second call argument corrupted the string literal `"\\(group\\)"`: the `[^,)]+` capture truncated at the `)` *inside* the string, injecting the new arg mid-literal.
`pnpm run check` did not catch it (esbuild accepts the wrong-but-valid string); caught only by re-reading the block (turn 229) and rewriting it by hand (turn 230).
Impact: ~3 tool calls, one describe-block rewrite, no committed rework.
This is a distinct failure mode from the existing #525 multi-line `.*?` boundary-spanning trap — a capture-and-re-emit regex whose captured span can contain the delimiter.
- `other` (scripted-edit silent no-op) — a `FILES=$(grep -rl ...); for f in $FILES; do perl ...; done` loop applied nothing (turn 188); re-running with explicit file arguments in one `perl` invocation worked (turn 190).
Impact: 2 tool calls; caught immediately by the post-substitution `grep` verification, no rework.
- `other` (edit path hygiene) — two `Edit` calls used a doubled absolute prefix (`/Users/.../pi-packages/packages/pi-permission-system/packages/...`) and were rejected by the permission gate as an external directory (turns 151, 153); retried with the repo-relative path.
Impact: 2 rejected calls, minor.
- `missing-context` (skill path) — reached for a filesystem-wide `find` to locate the `ask-user` skill after guessing a wrong path, when the `<available_skills>` index in the system context already listed its exact location.
Impact: one aborted `find`; user-caught.
#### What caused friction (user side)
- The `ask-user` skill mis-guess (above) was the only user touchpoint in the session — mechanical redirection, not strategic.
Opportunity: the retro/plan prompts say “Load the `ask-user` skill” by name; consulting the `<available_skills>` index (which carries the resolved path) is the reliable lookup, and I should default to it rather than guessing a conventional path.
### Diagnostic details
- **Model-performance correlation** — four distinct models across the arc, each matched to task weight (design → `claude-fable-5`; execution → `claude-opus-4-8`; ship → `deepseek-v4-flash`; review → `claude-sonnet-5`).
No mismatch.
- **Escalation-delay tracking** — no `rabbit-hole`; the longest same-error streak was 23 tool calls (the perl re-runs), each resolved by the next action.
- **Unused-tool detection** — none material; the one misstep (a broad `find` for a skill) should have been an `<available_skills>`-index lookup, not a subagent dispatch.
- **Feedback-loop gap analysis** — no gap; `check` ran after every shared-type change and the full suite before every commit, so the corrupted-string-literal trap would also have surfaced in the suite even if the manual re-read had missed it.
### Changes made
1. Appended this Final Retrospective stage entry to `packages/pi-permission-system/docs/retro/0562-path-flavor-platform-language.md`.
2. Proposed sharpening the AGENTS.md scripted-substitution rule (line 55) with the single-line capture-and-re-emit delimiter trap; operator declined — kept as a retro observation only, no `AGENTS.md` change.
[#479]: https://github.com/gotgenes/pi-packages/issues/479
[#571]: https://github.com/gotgenes/pi-packages/issues/571