feat: vendor permission system source

This commit is contained in:
云服务部-叶林立
2026-08-19 14:35:19 +08:00
parent 198584daf8
commit 410c50a3e5
809 changed files with 157793 additions and 139 deletions
@@ -0,0 +1,315 @@
---
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 13, 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 13 ([#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 13 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 27 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