mirror of
https://bitbucket.org/siakitem/my-pi.git
synced 2026-08-28 08:35:57 +00:00
316 lines
28 KiB
Markdown
316 lines
28 KiB
Markdown
---
|
||
issue: 505
|
||
issue_title: "pi-permission-system: dissolve the path-utils grab-bag behind AccessPath (Phase 7 Step 4)"
|
||
---
|
||
|
||
# Dissolve the `path-utils.ts` grab-bag into cohesive path modules
|
||
|
||
## Release Recommendation
|
||
|
||
**Release:** ship independently
|
||
|
||
This is Phase 7 Step 4 ([#505]) of the [#487] roadmap, tagged `Release: independent` — a pure structural refactor with no behavior change.
|
||
It is not part of the `symlink-resistant-path-matching` batch (Steps 1–3, already shipped), so it carries no batch obligation.
|
||
The commits are `refactor:` / `docs:` (changelog-`hidden`), so this work lands on `main` and auto-batches into the next `feat:`/`fix:` release rather than cutting one on its own.
|
||
|
||
## Problem Statement
|
||
|
||
`path-utils.ts` is the package's path-derivation grab-bag — 18 symbols spanning four unrelated jobs (representation derivation, geometric containment, tool-input extraction, static lookup sets), 13 fan-in, and an accelerating churn hotspot (266 churn over six months, trending up).
|
||
It is the ad-hoc "re-derive path representations" surface the [#487] vision exists to consolidate.
|
||
After Steps 1–3 ([#502], [#503], [#504]) routed the per-tool gate and service/RPC queries onto `AccessPath`, the lexical/canonical/policy-value derivation in this file is consumed almost entirely by `AccessPath` — so it belongs *with* `AccessPath` in the `access-intent/` domain, and the rest belongs in focused single-job modules.
|
||
|
||
## Goals
|
||
|
||
- Dissolve `path-utils.ts` entirely into cohesive, single-responsibility modules.
|
||
- Relocate the representation derivation (`normalizePathForComparison`, `canonicalNormalizePathForComparison`, `normalizePathPolicyLiteral`, `getPathPolicyValues` + private helpers, `PathPolicyValueOptions`) into `src/access-intent/path-normalization.ts` as `AccessPath`'s backing.
|
||
- Keep the geometric-containment predicates (`isPathWithinDirectory`, `isPathOutsideWorkingDirectory`) together in a focused `src/path-containment.ts`.
|
||
- Split the remaining jobs into focused modules: safe-system paths, Pi infrastructure-read, tool-input extraction, and the surface/tool lookup sets.
|
||
- Preserve behavior exactly — this is non-breaking at every surface (no public API, config, schema, or decision change).
|
||
- Mirror the module split in the test suite: each new module gets a focused test file; `path-utils.test.ts` is dissolved.
|
||
|
||
## Non-Goals
|
||
|
||
- **No config-pattern derivation.**
|
||
Rule patterns (`*.env`, `src/*`) stay raw globs/regex — they are never path-derived (a standing Phase 7 Non-goal).
|
||
Nothing in this work touches the matcher.
|
||
- **No change to `AccessPath`'s outputs or accessors.** `matchValues()` / `boundaryValue()` / `value()` keep their exact results; only the import path of their backing functions moves.
|
||
- **No deeper boundary rework.**
|
||
We do not re-express `isPathOutsideWorkingDirectory` as an `AccessPath` method or collapse it into the gate flow beyond the minimal prep refactor below — the `path-values` boundary formalization is Step 5 ([#506]), a separate issue.
|
||
- **No `PathNormalizer` API change.**
|
||
Its public method surface (`forPath`/`forLiteral`/`isAbsolute`/`isWithinDirectory`/`isOutsideWorkingDirectory`/`comparableValue`/`isInfrastructureRead`/…) is unchanged; only its private delegation targets move.
|
||
- The frozen historical doc `docs/architecture/history/phase-6-access-intent-extraction.md` references `path-utils` as it was — it is a snapshot and is **not** edited.
|
||
|
||
## Background
|
||
|
||
Relevant existing modules:
|
||
|
||
- `src/path-utils.ts` — the grab-bag being dissolved (full inventory in Design Overview).
|
||
- `src/access-intent/access-path.ts` — `AccessPath` value object; `forPath` composes `normalizePathForComparison` + `getPathPolicyValues` + `canonicalNormalizePathForComparison`.
|
||
- `src/path-normalizer.ts` — `PathNormalizer` facade, constructed at the session edge with `platform` + `cwd` baked in; delegates to the `path-utils` leaf functions.
|
||
The **sole** caller of the free `isPathOutsideWorkingDirectory`.
|
||
- `src/access-intent/bash/bash-path-resolver.ts` — imports `isSafeSystemPath`, `normalizePathPolicyLiteral`.
|
||
- `src/rule.ts`, `src/permission-manager.ts`, `src/input-normalizer.ts` — import `PATH_SURFACES`.
|
||
- `src/pattern-suggest.ts`, `src/handlers/gates/tool.ts` — import `PATH_BEARING_TOOLS`.
|
||
- `src/handlers/gates/{path,external-directory,tool,tool-call-gate-pipeline}.ts` — import `getToolInputPath` / `getPathBearingToolPath`.
|
||
|
||
Constraints from AGENTS.md / the package skill that apply:
|
||
|
||
- The ESLint `no-restricted-syntax` guard forbids `process.platform` in `pi-permission-system/src/**` (except `index.ts`).
|
||
Every relocated leaf must keep taking an injected `platform` parameter — no `= process.platform` defaults.
|
||
- `docs/architecture/architecture.md` module-tree, Phase 7 step list, Mermaid node, findings metrics, and the "PathNormalizer platform seam" prose all name `path-utils.ts` and must be updated in the implementation doc commit (mark Step 4 ✅ there, not at ship).
|
||
- `.pi/skills/package-pi-permission-system/SKILL.md` names `src/path-utils.ts` in prose — reworded mechanism prose carries no removed symbol, so it must be grepped and updated.
|
||
- `subagent-context.ts` has its **own** private `isPathWithinDirectoryForSubagent` — it does not consume `path-utils` and is out of scope.
|
||
|
||
## Design Overview
|
||
|
||
### The carve: representation vs geometry, on a shared primitive
|
||
|
||
Every function in `path-utils.ts` operates on the **accessed-path side** (a path the agent is trying to touch), not the config side.
|
||
The real seam is two different jobs done to that path:
|
||
|
||
| Job | Functions | Feeds |
|
||
| --------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------ |
|
||
| Representation (derivation) | `normalizePathForComparison`, `canonicalNormalizePathForComparison`, `normalizePathPolicyLiteral`, `getPathPolicyValues` (+ 2 private helpers) | `AccessPath` match/boundary/display values → resolver pattern test |
|
||
| Geometry (containment) | `isPathWithinDirectory`, `isPathOutsideWorkingDirectory` | gate bypass / auto-allow boundary decisions |
|
||
| Extraction | `getToolInputPath`, `getPathBearingToolPath` | pulls the path out of a tool payload |
|
||
| Lookup sets | `PATH_BEARING_TOOLS`, `READ_ONLY_PATH_BEARING_TOOLS`, `PATH_SURFACES` | static surface membership |
|
||
| Safe-system | `SAFE_SYSTEM_PATHS`, `isSafeSystemPath` | OS device-file allowlist |
|
||
| Infra-read | `isPiInfrastructureRead` (+ private `containsGlobChars`) | Pi infrastructure auto-allow |
|
||
|
||
`isPathWithinDirectory` is the truly foundational piece: pure `path.relative` math, zero dependencies, used by representation (for the cwd-relative alias), by both boundary predicates, and by `PathNormalizer` directly.
|
||
|
||
### Breaking the apparent cycle: "prepare the data, then ask"
|
||
|
||
A naïve split (representation → `access-intent/`, both containment funcs → `path-containment.ts`) appears to force a module cycle: `getPathPolicyValues` (representation) calls `isPathWithinDirectory` (geometry), while `isPathOutsideWorkingDirectory` (geometry) calls `canonicalNormalizePathForComparison` (representation).
|
||
|
||
The cycle is not fundamental — it is an artifact of `isPathOutsideWorkingDirectory` being mis-factored.
|
||
It bundles "prepare the data" with "ask the question":
|
||
|
||
```typescript
|
||
// today: derives BOTH operands inline, then asks — the only geometry→representation edge
|
||
export function isPathOutsideWorkingDirectory(pathValue, cwd, platform) {
|
||
const normalizedCwd = canonicalNormalizePathForComparison(cwd, cwd, platform);
|
||
const normalizedPath = canonicalNormalizePathForComparison(pathValue, cwd, platform);
|
||
if (!normalizedCwd || !normalizedPath) return false;
|
||
if (isSafeSystemPath(normalizedPath)) return false;
|
||
return !isPathWithinDirectory(normalizedPath, normalizedCwd, platform);
|
||
}
|
||
```
|
||
|
||
Its sibling `isPiInfrastructureRead` already follows the right discipline: it receives the already-canonical `accessPath.boundaryValue()` from `PathNormalizer` and never re-derives.
|
||
`isPathWithinDirectory` already follows it too: pure geometry over normalized operands.
|
||
|
||
The fix is to make `isPathOutsideWorkingDirectory` pure geometry as well — receive prepared canonical operands, and push the canonicalization up to its single caller, `PathNormalizer.isOutsideWorkingDirectory`, which already owns `cwd` and `platform`:
|
||
|
||
```typescript
|
||
// path-containment.ts — pure geometry, no representation import
|
||
export function isPathOutsideWorkingDirectory(
|
||
canonicalPath: string,
|
||
canonicalCwd: string,
|
||
platform: NodeJS.Platform,
|
||
): boolean {
|
||
if (!canonicalCwd || !canonicalPath) return false;
|
||
if (isSafeSystemPath(canonicalPath)) return false;
|
||
return !isPathWithinDirectory(canonicalPath, canonicalCwd, platform);
|
||
}
|
||
|
||
// path-normalizer.ts — the caller prepares the operands (canonical cwd cached once)
|
||
isOutsideWorkingDirectory(pathValue: string): boolean {
|
||
const canonicalPath = canonicalNormalizePathForComparison(
|
||
pathValue, this.cwd, this.platform,
|
||
);
|
||
return isPathOutsideWorkingDirectory(canonicalPath, this.canonicalCwd, this.platform);
|
||
}
|
||
```
|
||
|
||
With the inline derivation removed, geometry no longer imports representation, and the dependency graph is a strict DAG:
|
||
|
||
```text
|
||
safe-system-paths.ts ─┐
|
||
▼
|
||
path-containment.ts (isPathWithinDirectory, isPathOutsideWorkingDirectory — pure geometry)
|
||
▲
|
||
access-intent/path-normalization.ts (all representation; calls the geometry primitive downward)
|
||
▲
|
||
access-intent/access-path.ts (AccessPath)
|
||
▲
|
||
path-normalizer.ts (derives canonical operands, hands them to geometry)
|
||
```
|
||
|
||
This honors the issue's literal grouping: representation lands in **one** `access-intent/path-normalization.ts`, and both containment predicates stay together in **one** focused `path-containment.ts`.
|
||
|
||
### Final module set (six modules; `path-utils.ts` deleted)
|
||
|
||
| New module | Residents | Imports |
|
||
| ----------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------ |
|
||
| `src/access-intent/path-normalization.ts` | `normalizePathForComparison`, `canonicalNormalizePathForComparison`, `normalizePathPolicyLiteral`, `getPathPolicyValues`, `PathPolicyValueOptions`, private `getAbsolutePathPolicyValues` / `getCwdRelativePathPolicyValues` | `isPathWithinDirectory` (path-containment), `expandHomePath`, `canonicalizePath`, `node:path` |
|
||
| `src/path-containment.ts` | `isPathWithinDirectory`, `isPathOutsideWorkingDirectory` (pure geometry) | `isSafeSystemPath` (safe-system-paths), `node:path` |
|
||
| `src/safe-system-paths.ts` | `SAFE_SYSTEM_PATHS`, `isSafeSystemPath` | — |
|
||
| `src/pi-infrastructure-read.ts` | `isPiInfrastructureRead`, private `containsGlobChars` | `isPathWithinDirectory` (path-containment), `READ_ONLY_PATH_BEARING_TOOLS` (path-surfaces), `expandHomePath`, `wildcardMatch`, `node:path` |
|
||
| `src/tool-input-path.ts` | `getToolInputPath`, `getPathBearingToolPath` | `PATH_BEARING_TOOLS` (path-surfaces), `getNonEmptyString` / `toRecord` (value-guards), `ToolAccessExtractorLookup` (type) |
|
||
| `src/path-surfaces.ts` | `PATH_BEARING_TOOLS`, `READ_ONLY_PATH_BEARING_TOOLS`, `PATH_SURFACES` | — |
|
||
|
||
DAG verification: `path-surfaces` and `safe-system-paths` are leaves; `path-containment → safe-system-paths`; `path-normalization → path-containment`; `pi-infrastructure-read → {path-containment, path-surfaces}`; `tool-input-path → path-surfaces`.
|
||
No cycles.
|
||
|
||
### Edge cases preserved
|
||
|
||
- `isPathOutsideWorkingDirectory` keeps its empty-operand guard, safe-system short-circuit, and `platform` (for `path.relative` separator choice) — only the canonicalization moves out.
|
||
- `PathNormalizer` canonicalizes its `cwd` once at construction (`this.canonicalCwd`), matching today's per-call result but avoiding a redundant `realpathSync` per query (a behavior-equivalent improvement; cwd symlink target does not change mid-session).
|
||
- All leaf functions keep their injected `platform` parameter (the `no-restricted-syntax` guard stays green).
|
||
|
||
## Module-Level Changes
|
||
|
||
### Source — new files
|
||
|
||
- `src/access-intent/path-normalization.ts` — representation derivation (see table).
|
||
- `src/path-containment.ts` — `isPathWithinDirectory` + pure-geometry `isPathOutsideWorkingDirectory`.
|
||
- `src/safe-system-paths.ts` — `SAFE_SYSTEM_PATHS`, `isSafeSystemPath`.
|
||
- `src/pi-infrastructure-read.ts` — `isPiInfrastructureRead` + `containsGlobChars`.
|
||
- `src/tool-input-path.ts` — `getToolInputPath`, `getPathBearingToolPath`.
|
||
- `src/path-surfaces.ts` — the three lookup sets.
|
||
|
||
### Source — deleted
|
||
|
||
- `src/path-utils.ts` — removed once empty (its last residents, the containment pair, move to `path-containment.ts`).
|
||
|
||
### Source — importer updates (every removed export breaks importers at the type level → updated in the same commit as its move)
|
||
|
||
- `src/path-normalizer.ts` — re-point `normalizePathForComparison`, `canonicalNormalizePathForComparison` → `access-intent/path-normalization`; `isPathWithinDirectory`, `isPathOutsideWorkingDirectory` → `path-containment`; `isPiInfrastructureRead` → `pi-infrastructure-read`.
|
||
Add the `isOutsideWorkingDirectory` prep refactor (canonicalize operands; cache `canonicalCwd`).
|
||
- `src/access-intent/access-path.ts` — re-point the three derivation imports → `access-intent/path-normalization`.
|
||
- `src/access-intent/bash/bash-path-resolver.ts` — `isSafeSystemPath` → `safe-system-paths`; `normalizePathPolicyLiteral` → `access-intent/path-normalization`.
|
||
- `src/rule.ts`, `src/permission-manager.ts`, `src/input-normalizer.ts` — `PATH_SURFACES` → `path-surfaces`.
|
||
- `src/pattern-suggest.ts` — `PATH_BEARING_TOOLS` → `path-surfaces`.
|
||
- `src/handlers/gates/tool.ts` — `getPathBearingToolPath` → `tool-input-path`; `PATH_BEARING_TOOLS` → `path-surfaces`.
|
||
- `src/handlers/gates/path.ts`, `src/handlers/gates/external-directory.ts` — `getToolInputPath` → `tool-input-path`.
|
||
- `src/handlers/gates/tool-call-gate-pipeline.ts` — `getPathBearingToolPath` → `tool-input-path`.
|
||
|
||
### Tests — split to mirror modules
|
||
|
||
- `test/path-utils.test.ts` (695 LOC) is dissolved; each `describe` block moves to a focused file carrying the same `node:os` / `node:fs` mocks where needed:
|
||
- `test/path-normalization.test.ts` — `normalizePathForComparison`, `canonicalNormalizePathForComparison`, `normalizePathPolicyLiteral`, `getPathPolicyValues`.
|
||
- `test/path-containment.test.ts` — `isPathWithinDirectory`; pure-geometry `isPathOutsideWorkingDirectory` (operands pre-canonicalized — no `realpathSync` needed).
|
||
- `test/safe-system-paths.test.ts` — `SAFE_SYSTEM_PATHS`, `isSafeSystemPath`.
|
||
- `test/tool-input-path.test.ts` — `getToolInputPath`, `getPathBearingToolPath`.
|
||
- `test/path-surfaces.test.ts` — the three lookup sets.
|
||
- `test/pi-infrastructure-read.test.ts` — re-point import → `#src/pi-infrastructure-read`; absorb the overlapping `isPiInfrastructureRead` `describe` from `path-utils.test.ts` (de-duplicate).
|
||
- `test/permission-manager-unified.test.ts` — re-point `getPathPolicyValues` → `#src/access-intent/path-normalization`.
|
||
- `test/path-normalizer.test.ts` — gains the symlink/`./link/hosts`/safe-system integration cases that move off `path-utils.test.ts`'s `isPathOutsideWorkingDirectory` block (they exercise canonicalization, now owned by `PathNormalizer`).
|
||
|
||
### Docs — updated in the implementation doc commit
|
||
|
||
- `packages/pi-permission-system/docs/architecture/architecture.md`:
|
||
- Module-tree: replace the `path-utils.ts` entry with the six new module entries; update the `access-path.ts` and `path-normalizer.ts` entries to name `access-intent/path-normalization.ts` + `path-containment.ts` (and note `isOutsideWorkingDirectory` now canonicalizes operands before the pure check).
|
||
- Phase 7 Step 4 heading → ✅; update its Outcome line; flip the Mermaid `S4` node to ✅.
|
||
- Findings metric row (`path-utils.ts` fan-in → distributed) and the prose at the grab-bag references (the "residual ad-hoc path handling" bullet, the findings paragraph) → past-tense / resolved.
|
||
- "Related: PathNormalizer platform seam" section: update "`path-utils`/`AccessPath` primitives … Phase 7 [#505] can later move them behind it" (now done), "leaf `platform` parameters in `path-utils.ts` persist" → "in the relocated path modules persist", and the `isPiInfrastructureRead` / `isPathWithinDirectory` / `isPathOutsideWorkingDirectory` references → new module names.
|
||
- `.pi/skills/package-pi-permission-system/SKILL.md`:
|
||
- `getToolInputPath` (`src/path-utils.ts`) → `src/tool-input-path.ts`.
|
||
- "every path-utils / `canonicalize-path` / `rule.ts` / `subagent-context.ts` leaf takes an injected `platform`" → re-word "path-utils" to the relocated path modules.
|
||
|
||
`README.md` and `docs/configuration.md` name no internal symbols here (verified) — no update.
|
||
|
||
## Test Impact Analysis
|
||
|
||
1. **New tests the split enables.**
|
||
The pure-geometry `isPathOutsideWorkingDirectory(canonicalPath, canonicalCwd, platform)` becomes testable with plain string operands — no `node:fs`/`realpathSync` mock and no symlink fixture, a faster and more direct unit.
|
||
Each focused test file documents one module's surface instead of a 695-line catch-all.
|
||
2. **Tests that become redundant.**
|
||
`path-utils.test.ts`'s `isPathOutsideWorkingDirectory` cases that exercised *canonicalization* (symlink resolution, `./link/hosts`, safe-system device files) are redundant at the pure-geometry layer — they move to `path-normalizer.test.ts`, which now owns canonicalization (some overlap already exists there).
|
||
The `isPiInfrastructureRead` `describe` duplicated across `path-utils.test.ts` and `pi-infrastructure-read.test.ts` collapses into the latter.
|
||
3. **Tests that must stay as-is.**
|
||
The representation tests (`normalizePathForComparison`, `canonicalNormalizePathForComparison`, `normalizePathPolicyLiteral`, `getPathPolicyValues`) genuinely exercise derivation and move verbatim to `path-normalization.test.ts`.
|
||
The `isPathWithinDirectory` geometry tests move verbatim to `path-containment.test.ts`.
|
||
The lookup-set and extraction tests move verbatim.
|
||
|
||
## Invariants at risk
|
||
|
||
This refactor touches surfaces that Phase 7 Steps 1–3 and the [#510]/[#511] seam already refactored:
|
||
|
||
- **[#502]/[#503] — per-tool & service/RPC paths match lexical ∪ canonical.**
|
||
`AccessPath` outputs must not change.
|
||
Pinned by `test/path-normalizer.test.ts` (`forPath().matchValues()` / `value()` / `boundaryValue()`) and the external-directory / gate integration tests.
|
||
Keep green.
|
||
- **[#382] — win32 canonical lowercasing.**
|
||
Pinned by `path-normalizer.test.ts` win32 cases and the (relocating) `canonicalNormalizePathForComparison` tests.
|
||
- **[#418] — symlink pattern matching.**
|
||
Pinned by the external-directory integration tests (unchanged).
|
||
- **[#510]/[#511] — no interior `process.platform`; the `PathNormalizer` facade is the platform seam.**
|
||
The ESLint `no-restricted-syntax` guard must stay green — every relocated leaf keeps its injected `platform`.
|
||
`PathNormalizer.isOutsideWorkingDirectory(pathValue)`'s observable contract is unchanged; pinned by `path-normalizer.test.ts` (augmented with the moved integration cases).
|
||
Run `pnpm run lint` after the prep refactor to confirm the guard.
|
||
|
||
## TDD Order
|
||
|
||
Step 1 is a genuine behavior-contract change (red→green).
|
||
Steps 2–7 are pure relocations: the safety net is the **existing** suite staying green after each move + importer update (`pnpm run check` + `pnpm -r run test`).
|
||
Each step leaves the build valid and is committed separately.
|
||
|
||
1. **Prep refactor — make `isPathOutsideWorkingDirectory` pure geometry ("tidy first").**
|
||
Red: add `test/path-containment.test.ts` (or extend in place) asserting `isPathOutsideWorkingDirectory(canonicalPath, canonicalCwd, platform)` over **pre-canonicalized** operands (within/outside/equal/empty/safe-system).
|
||
Green: change the signature to receive prepared operands (drop the inline `canonicalNormalizePathForComparison` calls); move canonicalization into `PathNormalizer.isOutsideWorkingDirectory` (cache `canonicalCwd` in the constructor); update the `isPathOutsideWorkingDirectory` cases in `path-utils.test.ts` to pass canonical operands and migrate the symlink/safe-system integration cases into `path-normalizer.test.ts`.
|
||
Run `pnpm run check` + `pnpm run lint` (interface change + guard).
|
||
Commit `refactor(pi-permission-system): make isPathOutsideWorkingDirectory pure geometry over prepared operands`.
|
||
|
||
2. **Extract `path-surfaces.ts`.**
|
||
Move the three lookup sets; update `rule.ts`, `permission-manager.ts`, `input-normalizer.ts`, `pattern-suggest.ts`, `handlers/gates/tool.ts`, and the `path-utils.ts` internal `READ_ONLY` user; move the lookup-set `describe`s to `test/path-surfaces.test.ts`.
|
||
Commit `refactor(pi-permission-system): extract path-surfaces module`.
|
||
|
||
3. **Extract `safe-system-paths.ts`.**
|
||
Move `SAFE_SYSTEM_PATHS` + `isSafeSystemPath`; update `bash-path-resolver.ts`, the `path-utils.ts` `isPathOutsideWorkingDirectory` user, and `path-containment.ts` (once it exists, step 7 — until then `path-utils.ts`); move the `describe`s to `test/safe-system-paths.test.ts`.
|
||
Commit `refactor(pi-permission-system): extract safe-system-paths module`.
|
||
|
||
4. **Extract `tool-input-path.ts`.**
|
||
Move `getToolInputPath` + `getPathBearingToolPath`; update `handlers/gates/{path,external-directory,tool,tool-call-gate-pipeline}.ts`; move the `describe`s to `test/tool-input-path.test.ts`.
|
||
Commit `refactor(pi-permission-system): extract tool-input-path module`.
|
||
|
||
5. **Extract `pi-infrastructure-read.ts`.**
|
||
Move `isPiInfrastructureRead` + `containsGlobChars`; update `path-normalizer.ts`; re-point and de-duplicate `test/pi-infrastructure-read.test.ts` (absorb the `path-utils.test.ts` `isPiInfrastructureRead` block).
|
||
Commit `refactor(pi-permission-system): extract pi-infrastructure-read module`.
|
||
|
||
6. **Extract `access-intent/path-normalization.ts` (representation).**
|
||
Move `normalizePathForComparison`, `canonicalNormalizePathForComparison`, `normalizePathPolicyLiteral`, `getPathPolicyValues`, `PathPolicyValueOptions`, and the two private helpers; update `access-path.ts`, `bash-path-resolver.ts`, `path-normalizer.ts`, `permission-manager-unified.test.ts`; move the derivation `describe`s to `test/path-normalization.test.ts`.
|
||
Run `pnpm run check`.
|
||
Commit `refactor(pi-permission-system): relocate path representation into access-intent`.
|
||
|
||
7. **Rename the residue to `path-containment.ts` and delete `path-utils.ts`.**
|
||
The only remaining residents are `isPathWithinDirectory` + `isPathOutsideWorkingDirectory` — rename the file to `src/path-containment.ts`; update its importers (`path-normalizer.ts`, `pi-infrastructure-read.ts`, `path-normalization.ts`); finish migrating `test/path-containment.test.ts`; delete `test/path-utils.test.ts`.
|
||
Run `pnpm fallow dead-code`.
|
||
Commit `refactor(pi-permission-system): rename path-utils residue to path-containment`.
|
||
|
||
8. **Docs — mark Step 4 complete and re-point module references.**
|
||
Update `docs/architecture/architecture.md` (module tree, Step 4 ✅ + Outcome, Mermaid `S4` ✅, findings metric, PathNormalizer-seam prose) and `.pi/skills/package-pi-permission-system/SKILL.md` (module-name references).
|
||
Commit `docs(pi-permission-system): record path-utils dissolution (Phase 7 Step 4)`.
|
||
|
||
## Risks and Mitigations
|
||
|
||
- **Silent default drift from a moved lookup set or interface.**
|
||
`esbuild` does not reject unknown properties at runtime, so a mis-pointed import could fail only at runtime.
|
||
Mitigation: run `pnpm run check` (a moved export becomes `TS2305` if mis-pointed) plus the full `pnpm -r run test` after each step.
|
||
- **The prep refactor (Step 1) changing observable behavior.**
|
||
Mitigation: it is behavior-preserving by construction (canonicalization merely relocates from callee to single caller); the `path-normalizer.test.ts` integration cases pin the end-to-end `isOutsideWorkingDirectory(pathValue)` contract, and the new pure-geometry tests pin the extracted function.
|
||
- **An orphaned import after a test `describe` moves.**
|
||
Biome's `noUnusedImports` is warning-level (exit 0).
|
||
Mitigation: re-check each touched test file's imports as part of its step; the pre-completion reviewer is the backstop.
|
||
- **A missed `path-utils` reference in prose.**
|
||
Mitigation: after Step 8, `grep -rn "path-utils" packages/pi-permission-system/{src,test,docs/architecture/architecture.md} .pi/skills/package-pi-permission-system/SKILL.md` returns only the frozen `history/` snapshot.
|
||
- **`fallow dead-code` flagging a newly-unreferenced export.**
|
||
Mitigation: these are all moves (every export keeps its consumer); run `pnpm fallow dead-code` at Step 7.
|
||
|
||
## Open Questions
|
||
|
||
- None blocking.
|
||
Step 5 ([#506], the `path-values` boundary decision) is already a separate roadmap issue and is unaffected by this relocation.
|
||
|
||
[#487]: https://github.com/gotgenes/pi-packages/issues/487
|
||
[#502]: https://github.com/gotgenes/pi-packages/issues/502
|
||
[#503]: https://github.com/gotgenes/pi-packages/issues/503
|
||
[#504]: https://github.com/gotgenes/pi-packages/issues/504
|
||
[#505]: https://github.com/gotgenes/pi-packages/issues/505
|
||
[#506]: https://github.com/gotgenes/pi-packages/issues/506
|
||
[#510]: https://github.com/gotgenes/pi-packages/issues/510
|
||
[#511]: https://github.com/gotgenes/pi-packages/issues/511
|
||
[#382]: https://github.com/gotgenes/pi-packages/issues/382
|
||
[#418]: https://github.com/gotgenes/pi-packages/issues/418
|