mirror of
https://bitbucket.org/siakitem/my-pi.git
synced 2026-08-28 16:45:22 +00:00
309 lines
27 KiB
Markdown
309 lines
27 KiB
Markdown
---
|
||
issue: 562
|
||
issue_title: "Leaf path modules re-derive the win32 path flavor from a raw platform parameter"
|
||
---
|
||
|
||
# PathFlavor: pass the platform's path language, not the raw discriminator
|
||
|
||
## Release Recommendation
|
||
|
||
**Release:** ship independently
|
||
|
||
This is Step 3 of the pi-permission-system Phase 10 roadmap, tagged `Release: independent` (a member of no batch).
|
||
It is a behavior-preserving refactor, so every implementation commit is `refactor:` (a hidden changelog type) — the work lands on `main` and auto-batches into the next `feat:`/`fix:` release rather than cutting one of its own (Refs [#479]).
|
||
The closing documentation commit touches only release-excluded paths (`docs/architecture`, `.pi/`), so it does not cut a release either.
|
||
|
||
## Problem Statement
|
||
|
||
The win32-vs-POSIX path flavor is re-derived from a raw `platform: NodeJS.Platform` parameter at 13 sites across the package.
|
||
Six leaf functions open with the identical `const impl = platform === "win32" ? winPath : posixPath` ternary; the win32 case fold (`toLowerCase()`), the separator pick, and the `{ caseInsensitive: true, windowsSeparators: true }` match-options literal are each independently re-derived at further sites.
|
||
|
||
The variant set is closed (win32 vs. POSIX), so the risk is not variant growth — it is **connascence of algorithm**: every site must re-derive the same mapping identically, and in a permission system a leaf that misses the case fold or separator fold is a silent bypass (the [#382]/[#508] bug class).
|
||
The [#505]/[#510] seam made the leaves pure platform-parameterized functions behind the `PathNormalizer` facade — it fixed *where the platform is read* but threaded the raw discriminator rather than the resolved product, so each leaf still re-interprets it.
|
||
|
||
## Goals
|
||
|
||
- Introduce `PathFlavor`, a value object that is the resolved product of the single `platform === "win32"` decision, and thread it into the leaves in place of the raw `platform` string.
|
||
- Make the [#382]/[#508] must-agree bug class **structurally impossible**: the case fold, the containment geometry, and the match-options each have exactly one implementation, owned by the flavor.
|
||
- Remove `NodeJS.Platform` from every domain signature: after this change, no `src/` module outside `index.ts` and the `PathFlavor` factory names `platform`.
|
||
- Preserve behavior exactly — identical decisions on every input, on both platforms.
|
||
This is not a breaking change.
|
||
- Advance the roadmap's structural metrics: `platform === "win32"` sites 13 → 1; `caseInsensitive` derivations → 1; flat `src/` root 62 → 59 (three leaves relocate into `src/path/`).
|
||
|
||
## Non-Goals
|
||
|
||
- Unifying `subagent-context`'s bespoke prefix-containment onto `PathFlavor.isWithin`.
|
||
That check (`isPathWithinDirectoryForSubagent`) uses a different algorithm than `path.relative`-based containment and diverges on `..` segments and cross-root paths, so collapsing it is **behavior-affecting**, not a pure refactor.
|
||
Filed as a follow-up: [#571].
|
||
- Splitting a separate `BashDialect` collaborator out of the win32 flavor.
|
||
Pi core guarantees bash-on-win32 is always Git Bash (no cmd/PowerShell branch in core's `shell.ts`), so the win32⇔MSYS pairing is fixed; a second object would be polymorphism over an axis that cannot vary independently.
|
||
Track-and-watch: extract it only if core ever un-fixes the pairing (WSL, PowerShell).
|
||
- Any change to config, schema, the permission model, or user-facing docs — this is an internal structural refactor with no observable surface change.
|
||
|
||
## Background
|
||
|
||
The relevant modules and their current platform coupling:
|
||
|
||
- `src/path-normalizer.ts` — the facade constructed at the session edge with `(platform, cwd)`.
|
||
Selects `this.impl` once, but also carries two `this.platform !== "win32"` guards in `forBashToken` / `interpretBashCdTarget` for Git Bash/MSYS semantics, and a `usesWindowsSeparators()` accessor.
|
||
- `src/access-intent/path-normalization.ts` — `normalizePathForComparison` (resolve + normalize + fold), `canonicalNormalizePathForComparison` (+ realpath + fold), `getPathPolicyValues` / `getAbsolutePathPolicyValues` / `getCwdRelativePathPolicyValues`.
|
||
Called by `AccessPath.forPath`.
|
||
- `src/path-containment.ts` — `isPathWithinDirectory` (the `path.relative` containment geometry) and `isPathOutsideWorkingDirectory` (geometry + `isSafeSystemPath` exclusion).
|
||
- `src/canonicalize-path.ts` — `canonicalizePath` (best-effort `realpathSync` walk using `impl.parse`/`impl.sep`/`impl.join`).
|
||
- `src/pi-infrastructure-read.ts` — `isPiInfrastructureRead` (re-derives the win32 match-options literal, calls `isPathWithinDirectory`).
|
||
- `src/authority/subagent-context.ts` — `normalizeFilesystemPath` (normalize + fold) and `isSubagentExecutionContext`, plus a private prefix-containment helper.
|
||
- `src/rule.ts` — `pathMatchOptions` (re-derives the match-options literal, gated by `PATH_SURFACES`) and the `evaluate`/`evaluateFirst`/`evaluateAnyValue`/`evaluateMostRestrictive`/`ruleMatches` family, all threaded `platform`.
|
||
- `src/permission-manager.ts` — holds `this.platform`, relays it to the `rule.ts` family.
|
||
Consumes `ResolvedAccessIntent` and **must not import `AccessPath`** (ADR-0002, enforced by a `no-restricted-imports` rule scoped to this file).
|
||
`PathFlavor` is a plain value object, not `AccessPath`, so it is safe to consume here.
|
||
- `src/access-intent/bash/token-classification.ts` — `classifyTokenAsRuleCandidate` takes a `windowsSeparators` boolean derived from `PathNormalizer.usesWindowsSeparators()` by `bash-path-resolver.ts` ([#520]).
|
||
- `src/index.ts` — the sole `process.platform` reader (the ESLint `no-restricted-syntax` guard exempts only this file); threads `hostPlatform` into `PermissionManager`, `PermissionSession` (→ `PathNormalizer`), and `SubagentDetection`.
|
||
|
||
Constraints from AGENTS.md and the package skill that apply:
|
||
|
||
- Platform handling has a single home (`PathNormalizer`); no `src/` module reads `process.platform` (ESLint-guarded).
|
||
`PathFlavor` becomes a second platform-semantics home, still fed exclusively from `index.ts`'s one read.
|
||
- To test Windows behavior on POSIX CI, inject a win32 flavor — never `vi.mock("node:path")`.
|
||
The documented `new PathNormalizer("win32", cwd)` idiom becomes `new PathNormalizer(win32PathFlavor, cwd)`.
|
||
- `permission-manager.ts` stays string-based (ADR-0002); a `PathFlavor` import is allowed (the guard bans only `access-intent/access-path`).
|
||
|
||
## Design Overview
|
||
|
||
### The `PathFlavor` value object
|
||
|
||
`PathFlavor` is the platform's **path language** expressed as one object: syntax recognition, token semantics, and an equivalence relation — the three things the leaves currently re-derive.
|
||
It is pure (no filesystem access) and immutable; two cached singletons are selected by a factory that holds the package's one remaining `=== "win32"` comparison.
|
||
|
||
```typescript
|
||
// src/path/path-flavor.ts
|
||
import type { PlatformPath } from "node:path";
|
||
import type { BashTokenShape } from "#src/access-intent/bash/msys-bash-tokens";
|
||
import type { WildcardMatchOptions } from "#src/wildcard-matcher";
|
||
|
||
export interface PathFlavor {
|
||
/** Node's own platform strategy (path.win32 | path.posix). Path-domain primitives use it directly. */
|
||
readonly impl: PlatformPath;
|
||
/** Win32 { caseInsensitive, windowsSeparators } | undefined — the match-options product for the wildcard engine. */
|
||
readonly matchOptions: WildcardMatchOptions | undefined;
|
||
/** Comparison case fold: win32 => value.toLowerCase(), posix => value. */
|
||
fold(value: string): string;
|
||
/** resolve + normalize + fold against a base — the #382 invariant's single home. */
|
||
comparable(pathValue: string, base: string): string;
|
||
/** path.relative-based containment geometry. */
|
||
isWithin(pathValue: string, directory: string): boolean;
|
||
/** True when the token contains a path separator under this platform (posix: "/"; win32: "/" or "\"). */
|
||
hasPathSeparator(token: string): boolean;
|
||
/** MSYS/Git-Bash token shape on win32; always { kind: "plain" } on posix. */
|
||
bashTokenShape(token: string): BashTokenShape;
|
||
}
|
||
|
||
export const posixPathFlavor: PathFlavor;
|
||
export const win32PathFlavor: PathFlavor;
|
||
|
||
/** The one `platform === "win32"` decision in the package. */
|
||
export function pathFlavorForPlatform(platform: NodeJS.Platform): PathFlavor;
|
||
```
|
||
|
||
Ownership rule (prevents god-object drift): the flavor owns **platform semantics** (fold, geometry, token shape, match options, separator syntax).
|
||
**Domain policy** stays in the functions that consume it — the lexical cleanup in `normalizePathForComparison` (trim / strip quotes / strip `@` / `expandHome`), the alias generation in `getPathPolicyValues`, the `isSafeSystemPath` exclusion in `isPathOutsideWorkingDirectory`, the infra-read rules, and `rule.ts`'s `PATH_SURFACES`-gated dispatch.
|
||
|
||
`impl` is exposed rather than wrapped: post-migration its consumers are exclusively path-domain primitives, and `PlatformPath` is itself a strategy object Node maintains — wrapping it would add ~7 forwarding methods with no semantics.
|
||
The doc comment states the ownership rule; sealing it later (make it private, mirror the used methods) is a two-line change if it ever itches.
|
||
|
||
### How the flavor dissolves each site
|
||
|
||
`fold` / `comparable` / `matchOptions` collapse the equivalence family — the #382/#508 must-agree class.
|
||
A leaf can no longer re-derive-and-diverge because it never derives at all:
|
||
|
||
```typescript
|
||
// path-normalization.ts — after
|
||
export function normalizePathForComparison(pathValue: string, base: string, flavor: PathFlavor): string {
|
||
const cleaned = lexicalCleanup(pathValue); // domain policy stays here
|
||
return cleaned ? flavor.comparable(cleaned, base) : "";
|
||
}
|
||
```
|
||
|
||
`bashTokenShape` dissolves `PathNormalizer`'s two `!== "win32"` guards into uniform dispatch — the posix flavor returns `{ kind: "plain" }` (semantically correct: every posix token is an ordinary path), so the switch runs unchanged on both platforms:
|
||
|
||
```typescript
|
||
// path-normalizer.ts — after; no platform conditional remains
|
||
forBashToken(token: string, options?: { resolveBase?: string }): AccessPath {
|
||
const shape = this.flavor.bashTokenShape(token);
|
||
switch (shape.kind) {
|
||
case "device": return AccessPath.forDevice(token);
|
||
case "drive-mount": return this.forPath(shape.windowsPath, options);
|
||
case "posix-absolute": { /* literal-only + backslash alias, unchanged */ }
|
||
case "plain": return this.forPath(token, options);
|
||
}
|
||
}
|
||
```
|
||
|
||
`hasPathSeparator` dissolves the ask-leak in the bash rule-candidate classifier — the resolver stops asking `usesWindowsSeparators()` and relaying a boolean through an options bag:
|
||
|
||
```typescript
|
||
// token-classification.ts — after; the two includes() lines become one
|
||
if (token.startsWith(".")) return token;
|
||
if (flavor.hasPathSeparator(token)) return token; // was: includes("/") + (windowsSeparators && includes("\\"))
|
||
if (token.includes("..")) return token;
|
||
if (WINDOWS_DRIVE_PATH_PATTERN.test(token)) return token;
|
||
```
|
||
|
||
`PathNormalizer` exposes its flavor (`readonly flavor: PathFlavor`) so `bash-path-resolver.ts` passes `this.normalizer.flavor` to the classifier; `PathNormalizer.usesWindowsSeparators()` and `RuleCandidateOptions.windowsSeparators` are deleted.
|
||
|
||
### Construction and threading
|
||
|
||
`index.ts` performs the one `process.platform` read, resolves it into the flavor once, and injects that collaborator into the three holders — "instantiate the right collaborator as soon as we know the platform":
|
||
|
||
```typescript
|
||
// index.ts
|
||
const flavor = pathFlavorForPlatform(process.platform);
|
||
const permissionManager = new PermissionManager({ agentDir, flavor, isYoloEnabled });
|
||
const subagentDetection = new SubagentDetection({ subagentSessionsDir, flavor, registry });
|
||
// session ctor: new PermissionSession(..., flavor) -> new PathNormalizer(flavor, cwd)
|
||
```
|
||
|
||
`PathNormalizer` and `PermissionManager` hold `this.flavor` and drop their `platform` fields; `PermissionManager`'s constructor option becomes `flavor?: PathFlavor` (defaulting to `posixPathFlavor`, mirroring the old `platform ?? "linux"`); `SubagentDetection`'s dep becomes `flavor: PathFlavor`.
|
||
|
||
### Lift-and-shift bridge
|
||
|
||
Migrating a leaf's signature from `platform` to `PathFlavor` breaks its callers at the type level.
|
||
To keep every commit compiling and green, the migration is bottom-up: each leaf switches to `PathFlavor` first, and its not-yet-migrated callers bridge with an inline `pathFlavorForPlatform(platform)` at the call site.
|
||
Because `pathFlavorForPlatform` returns cached singletons, the bridge is cheap and cannot diverge — the transitional state is still bypass-safe.
|
||
The final threading step removes every inline bridge once the holders carry the flavor directly.
|
||
|
||
## Module-Level Changes
|
||
|
||
New:
|
||
|
||
- `src/path/path-flavor.ts` — the `PathFlavor` interface, `posixPathFlavor` / `win32PathFlavor` singletons, `pathFlavorForPlatform` factory.
|
||
- `test/path/path-flavor.test.ts` — unit tests for every capability, on both flavors.
|
||
|
||
Relocated into `src/path/` (tidy-first — these leaves reach their final home; three fewer files at the flat `src/` root, 62 → 59):
|
||
|
||
- `src/path-containment.ts` → `src/path/path-containment.ts`.
|
||
Delete the standalone `isPathWithinDirectory` export (geometry moves onto `flavor.isWithin`); keep `isPathOutsideWorkingDirectory(canonicalPath, canonicalCwd, flavor)`.
|
||
- `src/canonicalize-path.ts` → `src/path/canonicalize-path.ts`; `platform` → `flavor` (`flavor.impl`).
|
||
- `src/pi-infrastructure-read.ts` → `src/path/pi-infrastructure-read.ts`; `platform` → `flavor` (`flavor.matchOptions`, `flavor.isWithin`); delete the re-derived match-options literal.
|
||
- Move each module's test to `test/path/` and update the `#src/...` import.
|
||
|
||
Edited (signature/body, no relocation):
|
||
|
||
- `src/access-intent/path-normalization.ts` — all five exports `platform` → `flavor`; `normalizePathForComparison` = lexical cleanup + `flavor.comparable`; `canonicalNormalizePathForComparison` = `flavor.comparable` + `canonicalizePath` + `flavor.fold`; `getCwdRelativePathPolicyValues` uses `flavor.impl.relative` + `flavor.isWithin`.
|
||
- `src/access-intent/access-path.ts` — `AccessPath.forPath` option `platform` → `flavor` (its only caller is `PathNormalizer.forPath`).
|
||
- `src/path-normalizer.ts` — constructor `platform` → `flavor`; hold `this.flavor`; expose `readonly flavor`; drop both `!== "win32"` guards via `flavor.bashTokenShape`; delete `usesWindowsSeparators()`; relay `this.flavor` to every leaf; import the relocated leaves from `#src/path/...`.
|
||
- `src/authority/subagent-context.ts` — `normalizeFilesystemPath` and `isSubagentExecutionContext` `platform` → `flavor` (`flavor.impl`, `flavor.fold`, `flavor.impl.sep`).
|
||
- `src/authority/subagent-detection.ts` — `SubagentDetectionDeps.platform` → `flavor: PathFlavor`.
|
||
- `src/rule.ts` — `pathMatchOptions(surface, flavor)` returns `PATH_SURFACES.has(surface) ? flavor.matchOptions : undefined`; `ruleMatches` / `evaluate` / `evaluateFirst` / `evaluateAnyValue` / `evaluateMostRestrictive` `platform` → `flavor`.
|
||
- `src/permission-manager.ts` — option `platform?` → `flavor?: PathFlavor` (default `posixPathFlavor`); hold `this.flavor`; relay to the `rule.ts` family.
|
||
Import `#src/path/path-flavor` (allowed by the `no-restricted-imports` guard, which bans only `access-intent/access-path`).
|
||
- `src/permission-session.ts` — constructor `platform` → `flavor`; build `new PathNormalizer(flavor, "")` and rebuild on `activate`.
|
||
- `src/access-intent/bash/token-classification.ts` — `classifyTokenAsRuleCandidate(token, flavor)` via `flavor.hasPathSeparator`; delete `RuleCandidateOptions.windowsSeparators` (and the interface if it empties).
|
||
- `src/access-intent/bash/bash-path-resolver.ts` — pass `this.normalizer.flavor` to the classifier; drop the `usesWindowsSeparators()` read.
|
||
- `src/index.ts` — construct the flavor once; inject into `PermissionManager`, `PermissionSession`, `SubagentDetection`. `hostPlatform` stays only as the argument to `pathFlavorForPlatform`.
|
||
|
||
Verify `BashTokenShape` is exported from `src/access-intent/bash/msys-bash-tokens.ts` for `path-flavor.ts` to import (it holds the discriminated union `classifyWin32BashToken` returns); export it if it is currently local.
|
||
|
||
Test fixtures/harnesses:
|
||
|
||
- `test/helpers/session-fixtures.ts` — `makeRealSession`'s `platform?` override → `flavor?: PathFlavor` (or accept `platform` and map to `pathFlavorForPlatform` at the boundary); it builds a real `PermissionManager` + `PermissionSession`.
|
||
- `test/helpers/gate-fixtures.ts` — the `new PathNormalizer(process.platform, ...)` construction → `pathFlavorForPlatform(process.platform)`.
|
||
- Every `new PathNormalizer("win32"|"linux"|process.platform, cwd)` site (~30 across `test/`) → the corresponding singleton (`win32PathFlavor` / `posixPathFlavor`) or `pathFlavorForPlatform(process.platform)`.
|
||
- `test/rule.test.ts` (49 platform references) and the manager tests → flavor singletons.
|
||
|
||
Documentation (final commit; all release-excluded, so no release impact):
|
||
|
||
- `.pi/skills/package-pi-permission-system/SKILL.md` — rework the prose naming the removed/changed mechanisms: the leaf list ("every `path-containment` / `path-normalization` / `pi-infrastructure-read` / `canonicalize-path` / `rule.ts` / `subagent-context.ts` leaf takes an injected `platform` parameter"), the `usesWindowsSeparators()` reference in the bash `external_directory` note, and the "pass `platform: 'win32'`" test idiom → "pass `win32PathFlavor`".
|
||
These are reworded-prose updates carrying no removed symbol, so grep the skill for each mechanism name.
|
||
- `packages/pi-permission-system/docs/architecture/architecture.md` — mark Step 3 complete (`✅` on the step heading and the `S3` Mermaid node); update the health-metric rows (`platform === "win32"` 13 → 1, flat `src/` root 62 → 59, `caseInsensitive` derivations → 1); refresh any module-layout listing or narrative that names the relocated files or the `platform` threading.
|
||
`architecture.md` inline-copies the `rule.ts` `Rule`/`RuleOrigin`/`Ruleset` types — those are unchanged (only function signatures change), so that listing needs no edit; confirm during the pass.
|
||
- `packages/pi-permission-system/docs/decisions/0002-path-values-string-boundary.md` — confirm it stays accurate; the manager now consumes a `PathFlavor` but still not `AccessPath`, so the boundary holds.
|
||
Add a clarifying sentence only if the ADR's wording implies the manager holds a raw `platform`.
|
||
|
||
## Test Impact Analysis
|
||
|
||
1. New unit tests the extraction enables (previously impractical): `path-flavor.test.ts` directly exercises `fold`, `comparable`, `isWithin`, `hasPathSeparator`, `bashTokenShape`, and `matchOptions` on both singletons — the win32 case/separator semantics that were previously only reachable transitively through `AccessPath` / gate tests now have a focused home.
|
||
2. Tests that become redundant: the win32-specific assertions scattered in `path-containment.test.ts` (containment geometry) and portions of `path-normalization.test.ts` (fold behavior) overlap with the new flavor tests.
|
||
Keep them for now — they exercise the leaf functions' domain-policy wrapping (lexical cleanup, alias generation), not the flavor's raw geometry — but simplify any assertion that only re-checks the fold once `path-flavor.test.ts` owns it.
|
||
3. Tests that must stay as-is: the `PathNormalizer` win32 tests (`path-normalizer.test.ts`), the bash MSYS token tests (`msys-bash-tokens.test.ts`, `bash-external-directory.test.ts`), and the gate acceptance tests genuinely exercise the composed behavior (token shape → AccessPath → decision) and pin the [#533]/[#520] semantics end-to-end; they only swap their construction idiom to the flavor singleton.
|
||
|
||
## Invariants at risk
|
||
|
||
This change touches surfaces earlier phases refactored; each documented outcome must stay green:
|
||
|
||
- [#382]/[#508] — win32 case/separator fold on path-surface matching.
|
||
Pinned by the win32 path-matching tests in `rule.test.ts` and `permission-manager-unified.test.ts` (`new PathNormalizer("win32", ...)`).
|
||
The fold moving onto `flavor.fold`/`flavor.matchOptions` must not change any decision — these tests are the guard.
|
||
- [#533] — Git Bash/MSYS bash-token semantics (safe devices preserved, `/c/` mounts translated, other POSIX absolutes literal-only).
|
||
Pinned by `bash-external-directory.test.ts` and `msys-bash-tokens.test.ts`; `bashTokenShape` dispatch must reproduce them exactly.
|
||
- [#520] — win32 backslash-relative token recognized as path-shaped.
|
||
Pinned by the backslash-token cases in the bash path tests; `hasPathSeparator` replacing the `windowsSeparators` flag must keep the same classification.
|
||
- [#510]/[#505] — `PathNormalizer` is the single platform home fed from one `process.platform` read.
|
||
Pinned by the ESLint `no-restricted-syntax` guard (still exempting only `index.ts`) and the `test/composition-root.test.ts` wiring tests.
|
||
|
||
No new test is needed — each invariant already lives in a test, not only prose.
|
||
|
||
## TDD Order
|
||
|
||
Every step below is behavior-preserving; each keeps the suite green and compiling.
|
||
Commit type is `refactor:` throughout (the closing docs commit is `docs:`), so nothing cuts a release on its own.
|
||
`pnpm fallow dead-code` is a final-state gate — the flavor's methods are consumed incrementally across steps 2–9 and are all live by step 8; do not expect a clean fallow run mid-sequence.
|
||
|
||
1. **Add `PathFlavor` (pure addition).**
|
||
Red: `test/path/path-flavor.test.ts` asserts `fold` / `comparable` / `isWithin` / `hasPathSeparator` / `bashTokenShape` / `matchOptions` / `impl` on `win32PathFlavor` and `posixPathFlavor`, plus `pathFlavorForPlatform` selection.
|
||
Green: implement `src/path/path-flavor.ts`; export `BashTokenShape` from `msys-bash-tokens.ts` if needed.
|
||
`refactor(pi-permission-system): add PathFlavor value object`.
|
||
2. **Relocate + migrate `canonicalize-path`.**
|
||
Move to `src/path/`, signature `platform` → `flavor`; bridge its one caller (`path-normalization`) inline; move its test; update importers.
|
||
`refactor(pi-permission-system): thread PathFlavor through canonicalize-path`.
|
||
3. **Relocate + migrate `path-containment`; move geometry onto `flavor.isWithin`.**
|
||
Delete standalone `isPathWithinDirectory`; migrate all callers (`path-normalization`, `pi-infrastructure-read`, `path-normalizer`) to `flavor.isWithin` (inline bridge where the holder still has `platform`); keep `isPathOutsideWorkingDirectory(..., flavor)`; move test.
|
||
`refactor(pi-permission-system): move containment geometry onto PathFlavor`.
|
||
4. **Migrate `path-normalization` + `access-path.forPath`.**
|
||
All five `path-normalization` exports → `flavor` (`comparable`/`fold`/`impl`); `AccessPath.forPath` option `platform` → `flavor`; bridge the caller (`PathNormalizer.forPath`); update `path-normalization` tests.
|
||
`refactor(pi-permission-system): thread PathFlavor through path normalization`.
|
||
5. **Relocate + migrate `pi-infrastructure-read`.**
|
||
Move to `src/path/`, `platform` → `flavor` (`matchOptions`, `isWithin`); delete the re-derived match-options literal; bridge the caller; move test.
|
||
`refactor(pi-permission-system): thread PathFlavor through infrastructure-read`.
|
||
6. **Migrate `subagent-context` + `subagent-detection`.**
|
||
Leaves → `flavor` (`impl`, `fold`, `impl.sep`); `SubagentDetectionDeps.platform` → `flavor`; bridge `index.ts`'s construction inline; update subagent tests.
|
||
`refactor(pi-permission-system): thread PathFlavor through subagent detection`.
|
||
7. **Migrate `rule.ts` + `permission-manager`.**
|
||
`pathMatchOptions` and the `evaluate` family `platform` → `flavor`; manager option `platform?` → `flavor?` (default `posixPathFlavor`); migrate `rule.test.ts` (49 sites) and the manager tests to the singletons via a lift-and-shift within the step.
|
||
`refactor(pi-permission-system): thread PathFlavor through rule evaluation`.
|
||
8. **Thread the flavor from `index.ts`; dissolve `PathNormalizer`'s platform conditionals.**
|
||
`index.ts` constructs the flavor once and injects it into `PermissionManager` / `PermissionSession` / `SubagentDetection`; `PermissionSession` + `PathNormalizer` constructors `platform` → `flavor`; `PathNormalizer` exposes `readonly flavor`, drops both `!== "win32"` guards via `bashTokenShape`; remove every inline `pathFlavorForPlatform(platform)` bridge from steps 2–7; update all ~30 `PathNormalizer` test constructors, `session-fixtures`, `gate-fixtures`, and `composition-root.test.ts`.
|
||
`refactor(pi-permission-system): inject PathFlavor from the composition root`.
|
||
9. **Migrate the bash rule-candidate classifier; delete `usesWindowsSeparators`.**
|
||
`classifyTokenAsRuleCandidate(token, flavor)` via `flavor.hasPathSeparator`; `bash-path-resolver` passes `this.normalizer.flavor`; delete `usesWindowsSeparators()` and `RuleCandidateOptions.windowsSeparators`; update classifier + resolver tests.
|
||
`refactor(pi-permission-system): answer path-separator syntax on PathFlavor`.
|
||
10. **Documentation.**
|
||
Update the package skill's reworded prose and the architecture roadmap (Step 3 `✅` + Mermaid node + health metrics); confirm ADR-0002 accuracy.
|
||
`docs(pi-permission-system): record PathFlavor and complete roadmap Step 3`.
|
||
|
||
## Risks and Mitigations
|
||
|
||
- **Silent decision drift (the exact bug class this fixes).**
|
||
Mitigation: behavior-preserving throughout, guarded by the [#382]/[#508]/[#533]/[#520] invariant tests listed above; each step keeps the suite green before committing.
|
||
- **Large mechanical test migration (`rule.test.ts`, ~30 `PathNormalizer` constructors).**
|
||
Mitigation: lift-and-shift with the cached-singleton bridge so no step rewrites a whole test file at once; the singleton swap is a construction-idiom change, not a behavior change.
|
||
- **Transitional dead code failing a mid-sequence `fallow` run.**
|
||
Mitigation: treat fallow as a final-state gate (documented above); the pre-completion reviewer runs it once at the end when every flavor method is live.
|
||
- **Intermediary inline `pathFlavorForPlatform(platform)` bridges left behind.**
|
||
Mitigation: step 8 explicitly removes every bridge; a grep for `pathFlavorForPlatform(` outside `index.ts`, the factory, and tests must return nothing at the end.
|
||
- **`permission-manager.ts` accidentally importing `AccessPath` while adding the `PathFlavor` import.**
|
||
Mitigation: the `no-restricted-imports` rule already bans it; `PathFlavor` lives in `src/path/`, a different module, so the guard is untouched.
|
||
|
||
## Open Questions
|
||
|
||
- None blocking.
|
||
The `subagent-context` containment unification is deferred by design to [#571]; the `BashDialect` split is track-and-watch (Non-Goals).
|
||
|
||
[#382]: https://github.com/gotgenes/pi-packages/issues/382
|
||
[#479]: https://github.com/gotgenes/pi-packages/issues/479
|
||
[#505]: https://github.com/gotgenes/pi-packages/issues/505
|
||
[#508]: https://github.com/gotgenes/pi-packages/issues/508
|
||
[#510]: https://github.com/gotgenes/pi-packages/issues/510
|
||
[#520]: https://github.com/gotgenes/pi-packages/issues/520
|
||
[#533]: https://github.com/gotgenes/pi-packages/issues/533
|
||
[#571]: https://github.com/gotgenes/pi-packages/issues/571
|