From 627f4bb0db0ec3b63f00e9b327b1b1a2b364b6a6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Przemys=C5=82aw=20Galarowicz?= Date: Wed, 12 Aug 2026 16:00:12 +0200 Subject: [PATCH] docs: align trust map with the records era LIMITS and THREAT-MODEL still described the deleted module/manifest world and claimed no stored content-hashes; correct those claims in place now that pharn.records.json ships. Co-authored-by: Cursor --- .dev/features/trust-map-records-era/GRILL.md | 154 +++++++++ .dev/features/trust-map-records-era/PLAN.md | 317 ++++++++++++++++++ .../trust-map-records-era/REGRESSION.md | 57 ++++ .dev/features/trust-map-records-era/REVIEW.md | 174 ++++++++++ .dev/features/trust-map-records-era/SHIP.md | 68 ++++ .dev/features/trust-map-records-era/VERIFY.md | 50 +++ .../regression-report.json | 24 ++ .../trust-map-records-era/verify-report.json | 16 + .pharn/pharn-dev-regress/base-results.json | 2 +- .pharn/pharn-dev-regress/head-results.json | 2 +- .pharn/writes-scope.json | 4 +- CHANGELOG.md | 14 + LIMITS.md | 31 +- THREAT-MODEL.md | 16 +- docs/reference/pharn-records.md | 3 + 15 files changed, 916 insertions(+), 16 deletions(-) create mode 100644 .dev/features/trust-map-records-era/GRILL.md create mode 100644 .dev/features/trust-map-records-era/PLAN.md create mode 100644 .dev/features/trust-map-records-era/REGRESSION.md create mode 100644 .dev/features/trust-map-records-era/REVIEW.md create mode 100644 .dev/features/trust-map-records-era/SHIP.md create mode 100644 .dev/features/trust-map-records-era/VERIFY.md create mode 100644 .dev/features/trust-map-records-era/regression-report.json create mode 100644 .dev/features/trust-map-records-era/verify-report.json diff --git a/.dev/features/trust-map-records-era/GRILL.md b/.dev/features/trust-map-records-era/GRILL.md new file mode 100644 index 0000000..0de9459 --- /dev/null +++ b/.dev/features/trust-map-records-era/GRILL.md @@ -0,0 +1,154 @@ +# GRILL — trust-map-records-era + +Plan under interrogation: `.dev/features/trust-map-records-era/PLAN.md` (`trust: untrusted` to this stage). +**Spec-hash check: MATCH** — recomputed `sha256(ARCHITECTURE.md)` = +`bca940a5ad247c120e6d8a3acba119d0d8df51dca275964d0e54c48d729d3c4e`, identical to the plan's +`spec_content_hash`. No drift finding. + +**Griller membership (FLOOR — `node .dev/floor/count-grillers.mjs .`):** `{"registered":0,"grillers":[]}`. +**Zero grillers ran.** Note for the human: this stage command's own prose asserts *"Today the registered +set is the `testability` griller"* — that is **not true in this repository**; the deterministic count says +0. Only the inline Step-2 axes were applied. (Reported as an observation about the stage command, not a +finding against this plan.) + +--- + +## Findings + +### Axis: Trust propagation (P2) + +```yaml +- type: FINDING + rule_id: "P2" + severity: blocking + file: ".dev/features/trust-map-records-era/PLAN.md:171" + problem: "The Trust audit asserts the two edited files are install payload whose bytes ship to users; live code shows the installed copies are read from the fetched pharn-oss clone, so editing pharn-cli's own copies changes zero user-facing bytes and the stated justification for the CHANGELOG line is false." + evidence: "both target files are in `protect-trusted-paths.cjs` `DEFAULT_PROTECTED` (`:58`) and are **install payload** (`constants.ts:32-33`) — copied verbatim into user projects in the **flat** layout ... Editing them changes bytes that ship to users, which is precisely why the `CHANGELOG.md` line is in scope (P4/P7)." +``` + +**Interrogation.** `TRUSTED_DOCS` (`src/lib/constants.ts:29-34`) names the docs `installCapabilities` +copies **out of `repoDir`** — the degit clone of **pharn-oss** — into the user's project. pharn-cli's own +`LIMITS.md` / `THREAT-MODEL.md` are this repo's governance docs and are **never** the bytes installed. +`constants.ts:48-50` states it outright: *"THREAT-MODEL.md / LIMITS.md are NOT under `pharn/` (they stay +dev-only)"*. The tests the plan correctly classified as payload-pins (D6) write **fake** docs into a +**fake repo dir** — which is precisely the tell: the source is the clone, not this repo. + +The CHANGELOG line may still be correct (these docs are read on GitHub/npm as the project's published +trust map), but **the plan's reason for it is wrong** and a plan that mis-states its own blast radius in +a P2 audit is the one thing this increment is supposed to be curing. + +```yaml +- type: FINDING + rule_id: "P6" + severity: important + file: ".dev/features/trust-map-records-era/PLAN.md:171" + problem: "If pharn-oss ships its own LIMITS.md/THREAT-MODEL.md carrying the same three false claims, this increment corrects the dev-repo copy while leaving the copy users actually receive stale — and the plan never asks the question." + evidence: "This increment ingests **no untrusted artifact**. It reads only repo-local trusted docs and `src/`." +``` + +**Interrogation.** Follows directly from the finding above. Not resolvable from inside this repo +(different repository) → the terminal fallback is **ask the human**, not assume (P5/P6). + +### Axis: Guarantee audit (P0) + +```yaml +- type: FINDING + rule_id: "P0" + severity: important + file: ".dev/features/trust-map-records-era/PLAN.md:236" + problem: "The proposed §1d true-statement says remove deletes 'exactly what the recorded layout says that capability owns', but deleteCapabilityDir removes the whole directory recursively including user-added files pharn never installed, so 'exactly' rounds the honest direction the wrong way." + evidence: "`remove` reconstructs from the **recorded config**, so it deletes exactly what the recorded `layout` says that capability owns — and nothing it never recorded." +``` + +**Interrogation.** `deleteCapabilityDir` (`src/commands/remove.ts:72-77`) resolves +`safeJoin(cwd, capabilityRelDir(paths, target))` and removes **that directory**. Anything a user dropped +inside a capability dir goes with it. The clause *"and nothing it never recorded"* is the false half: +`remove` does not consult records to decide **what to delete** — the records prune (L1) is a *separate* +key-prefix filter over the store, after the fact. In the repo's honesty document, "exactly" and "nothing +it never recorded" both need to go. + +```yaml +- type: FINDING + rule_id: "P0" + severity: minor + file: ".dev/features/trust-map-records-era/PLAN.md:255" + problem: "The §4c phrase 'no longer crash or masquerade' is a claim about past behavior with no anchor in the current tree, which the anchor-table discipline forbids." + evidence: "which classifies symlinks and directories as `unreadable` rather than crashing or masquerading as a match" +``` + +**Interrogation.** The *present-tense* claim is fully anchored and accurate — `readDiskState` +(`src/lib/apply-update.ts:44`) returns `unreadable` for a symlink (`:57-58`), a non-regular file +(`:60-61`), an uninspectable path (`:51-55`), and an unreadable file (`:65-66`), and its docstring says +*"Never throws"* (`:41-42`). What has no anchor is the **historical** contrast ("no longer", "#82"). +Recommend stating the behavior, not the history. + +### Axis: Determinism (P5) + +```yaml +- type: FINDING + rule_id: "P5" + severity: important + file: ".dev/features/trust-map-records-era/PLAN.md:203" + problem: "The L1-pending clause asserts a capability's stale record entries survive only until the next update rewrites the store, but the plan anchors that to a remove.ts comment which itself uses the deleted 'manifest' vocabulary — the very staleness this increment exists to correct." + evidence: "while its `pharn.records.json` entries remain until the next `update` rewrites the store." +``` + +**Interrogation.** The claim appears **true**, but the plan cites the wrong evidence. +`src/commands/remove.ts:99` reads *"they linger until the next `update` prunes them via its manifest"* — +a comment carrying the same dead vocabulary being excised elsewhere in this PR. The load-bearing +evidence is instead `src/commands/update.ts:325`: +`files: { ...plan.nextRecords, ...buildRecords(cwd, written) }` — a **fresh object** built from the next +expected set, not a spread over the previous store, so stale keys drop by construction. **Re-anchor to +`update.ts:325` before shipping**, and confirm `planUpdate`'s `nextRecords` is not itself merged from the +old store. + +### Axis: Eval coverage (P1) — no finding + +The plan's "no evals" is an **honest reduction, not an exemption**. P1 binds behavior; this increment +changes zero behavior, and the plan says so explicitly while refusing to invent a floor +(*"Claiming an eval here would be inventing a floor that does not exist (P0)"*). It also correctly +declines to sell the anchor table as anything but advisory. Nothing to raise. + +### Axis: Header change (Q1) — no finding, concern retired + +Interrogated as instructed: **no markdown anchor link to `#1d-…`, `#1b-…`, or `#4c` exists anywhere** in +the repo (`grep -rn "#1d\|#1b-\|#4c"` → zero hits outside `node_modules`). Combined with D4's zero live +inbound `§1d` citers, the header rewrite breaks no resolvable reference. The residual is the +unfalsifiable one the plan already owns: a human who remembers the old title. Retired. + +### Axis: Honest scope (P7) — T3 omission + +Shipping §4c while leaving §1c's sibling falsehood is **defensible, not a half-truth** — the two sections +make *independent* claims and §1c is not cited by the rewritten text, so no reader is led from a +corrected sentence into an uncorrected one. It is, however, the finding most likely to be raised at +review, and the plan pre-empts it by name (T3) with a stated rationale. Surfaced, not faulted. + +--- + +## Summary + +The plan's **research is unusually strong** — the three target claims are verified verbatim, the citation +inventory reproduces with two corrections the plan volunteered against its own source prompt, the S3 +material was killed on live evidence rather than written on trust, and the decision-table row-2 nuance +was added beyond spec. Discovery is not where this plan is weak. + +Where it is weak is **one layer up**: the P2 Trust audit mis-states the increment's blast radius +(F1) — it believes it is editing install payload when it is editing dev-only governance docs — and that +error hides a substantive question the plan never asks (F2): whether the *user-facing* copies of these +same documents, in pharn-oss, carry the identical false claims. If they do, this PR corrects the copy +almost nobody reads and leaves the copy everyone receives stale. + +The three remaining findings are text-level: one overselling clause (F3, "exactly … nothing it never +recorded"), one unanchored historical aside (F4, "no longer crash"), and one correct claim resting on +stale evidence (F5, re-anchor to `update.ts:325`). + +None of this is a reason not to build. F3–F5 are edits to text that has not been written yet; F1 is a +one-paragraph correction to the plan's own audit; F2 is a question for the human that can be answered in +parallel with the build. + +**ADVISORY VERDICT: 5 concerns raised (1 blocking-severity, 3 important, 1 minor) — for the human to +weigh before `/pharn-dev-build`.** This grill gates nothing: every severity above is an LLM assignment +(fix #3), and no floor primitive was consulted except the spec-hash match and the griller count, both +reported verbatim. "Concerns raised" is not "the plan is sound," and a clean grill would not have made it +sound either. + diff --git a/.dev/features/trust-map-records-era/PLAN.md b/.dev/features/trust-map-records-era/PLAN.md new file mode 100644 index 0000000..f5d8932 --- /dev/null +++ b/.dev/features/trust-map-records-era/PLAN.md @@ -0,0 +1,317 @@ +# PLAN — trust-map-records-era + +- spec_content_hash: bca940a5ad247c120e6d8a3acba119d0d8df51dca275964d0e54c48d729d3c4e # fix #4 +- increment: Correct three now-false claims in the two hook-guarded governance docs (`LIMITS.md §1b`, `§1d`; `THREAT-MODEL.md §4c`) so the trust map matches the records era, as in-place section-body rewrites with no renumbering. +- layer(s): trusted governance docs (not a `src/` layer — `ARCHITECTURE.md §4` does not govern prose) +- constitution_refs: [P0, P2, P4, P6, P7] + +## Files + +- `LIMITS.md` — rewrite §1b's hash clause + §1d's **header text and body** (identifier `1d.` unchanged) — layer: trusted doc +- `THREAT-MODEL.md` — rewrite §4c's body — layer: trusted doc +- `CHANGELOG.md` — one line under Unreleased, `Docs` — layer: user-facing record +- `docs/reference/pharn-records.md` — reverse cross-link line back to `THREAT-MODEL §4c` / `LIMITS §1b` — layer: reference doc +- `LIMITS.UPDATED.md` — full corrected `LIMITS.md` (§1b + §1d applied), for a human to move into place — layer: handoff artifact +- `THREAT-MODEL.UPDATED.md` — full corrected `THREAT-MODEL.md` (§4c applied), for a human to move into place — layer: handoff artifact +- `.dev/features/trust-map-records-era/PLAN.md` — this plan — layer: loop artifact +- `.dev/features/trust-map-records-era/GRILL.md` — grill log — layer: loop artifact +- `.dev/features/trust-map-records-era/SHIP.md` — ship roll-up — layer: loop artifact +- `.dev/features/trust-map-records-era/REGRESSION.md` — regression render — layer: loop artifact +- `.dev/features/trust-map-records-era/regression-report.json` — machine regression report — layer: loop artifact +- `.dev/features/trust-map-records-era/VERIFY.md` — verify render — layer: loop artifact +- `.dev/features/trust-map-records-era/verify-report.json` — machine verify report — layer: loop artifact +- `.dev/features/trust-map-records-era/REVIEW.md` — review log — layer: loop artifact + +**Not touched:** `CONSTITUTION.md`, `ARCHITECTURE.md`, any `src/**`, any `tests/**`, any `.dev/floor/**`. + +### Why the two handoff files exist (added mid-run) + +`protect-trusted-paths.cjs` denies agent writes to `LIMITS.md` / `THREAT-MODEL.md` unconditionally +(`DEFAULT_PROTECTED`, `:58`; `PHARN_PROTECTED` composes by **addition only**, `:63`). The corrected +content is therefore delivered as **side-by-side files a human moves into place** — the hook's own +sanctioned path, with **no bypass**. The human applies them with: + +```bash +mv LIMITS.UPDATED.md LIMITS.md && mv THREAT-MODEL.UPDATED.md THREAT-MODEL.md +``` + +## Contracts satisfied + +- No `pharn-contracts` contract is implicated — this increment ships prose only, no finding shape, no + capability, no rule_id. Cited, not restated (P4). + +## Evals to write (P1) + +- **None, and this is the honest reduction, not an exemption.** P1 binds *behavior*; this increment + changes zero behavior — no `src/`, no `tests/`, no `.dev/floor/` file is touched. The verification of + record for prose is the **anchor table** below plus the standing gates (`lint:md`, `check`), which + must stay green *untouched*. Claiming an eval here would be inventing a floor that does not exist (P0). + +--- + +## Discovery — live state, read this run (P6) + +### D1. Base is NOT latest `main` — decision required (Q4) + +``` +current branch : feat/remove-prunes-records @ 4b8a0be +main : 21db522 +git branch --contains 4b8a0be --all + → feat/remove-prunes-records + → remotes/origin/feat/remove-prunes-records # NOT main +``` + +The build prompt asserts "base: latest `main` (verified at `21db522`)". Live state: we are **one commit +ahead of `main`**, and that commit **is L1** ("prune removed capability entries from +`pharn.records.json`"). L1 is **pushed but unmerged**. This is exactly the L1-merge question the prompt +asks to resolve — resolved: **not merged**. + +### D2. The three quoted passages match verbatim ✅ + +| # | File:line | Quoted claim (verbatim on disk) | +| --- | --- | --- | +| 1 | `LIMITS.md:61-62` | "To reconstruct what a module contributed, `update`/`remove`/`status` read the manifest from `@main` HEAD (not the pinned `commit`)." | +| 2 | `THREAT-MODEL.md:115-117` | "**4c. No stored content-hash of installed files.** `status`/`diff` re-derive the expected byte set **live** against `@main`, not against a per-file hash pinned in `pharn.config.json`." | +| 3 | `LIMITS.md:35-36` | "it stores **no signature and no per-file content-hash**" | + +No one got here first. HALT condition #1 not triggered. + +### D3. `remove` is offline — confirmed, with a line-number correction + +`grep -n "fetch\|manifest\|degit\|http" src/commands/remove.ts` → **3 hits, all comments, zero network**: + +- `:36` "The legacy module/skill removal (which read the manifest)" +- `:99` "them via its manifest." +- `:193` "Without a manifest we list" + +**Correction to the build prompt:** it cited `:30` and `:117` and claimed *two* hits. Live file has +**three** at `:36`/`:99`/`:193`. Content matches; line numbers drifted. The anchor table uses live lines. + +Addressing confirmed offline + config-derived: `remove.ts:12` imports `configLayout, layoutPaths`; +`:212` and `:307` both compute `layoutPaths(configLayout(config))`. + +### D4. §1d and §4c have **zero** live inbound citers ✅ + +`grep -rn "§1d\|§4c" src tests .dev .claude docs *.md` → the only hits are **historical build artifacts** +(`.dev/features/commands-off-manifest/PLAN.md:118,:200`; `.dev/features/update-drift-safe/PLAN.md:284`, +`REVIEW.md:128`) — prior plans/reviews, not live citers. **No `src/`, test, floor checker, hook, or +`docs/` file cites either section.** The two sections being rewritten are the two least-cited in the file. + +### D5. Citation inventory — reproduces, with an arithmetic correction + +`LIMITS.md §3b` — **8 live citation sites** (the prompt said "seven"; its own parenthetical enumerates +5+1+1+1 = 8, plus the test title = 9 lines. The *set* reproduces exactly; the count in the prose does not): + +``` +src/lib/repo.ts:16, :33, :49, :54, :78 (×5 — :78 cites §1b/§3b jointly) +src/lib/validate.ts:34 +src/commands/add.ts:393 +src/commands/init.ts:93 +tests/repo.test.ts:52 (test TITLE — a string, not an assertion) +``` + +`LIMITS.md §1b` — **exactly 2**, as claimed: `src/lib/repo.ts:35`, `src/lib/repo.ts:78`. + +`THREAT-MODEL §2/§3/§5` — reproduces: `check-seam-config.mjs:23` (§2), `check-structural.mjs:13-14` +(LIMITS §2 + TM §5), `check-provenance.mjs:10` (§3), `test-fixtures/case-injection-comment.md:8` (§2). +`.dev/floor/README.md:64` (LIMITS §2 + TM §5). **One citer the prompt's inventory omits:** +`src/lib/resolve-capabilities.ts:18` cites `THREAT-MODEL.md` (unnumbered). + +**Consequence:** §1b's rewrite must keep `repo.ts:35`/`:78`'s cited meaning intact — both cite §1b for +"provenance, not cryptographic." The planned rewrite *sharpens* that meaning and does not move it. ✅ + +### D6. The four filename tests are payload-pins — confirmed, not re-panicked + +Every reference is a **path**, never content: +`install-capabilities.test.ts:59-60,:373-374` write 1-byte bodies (`'T'`/`'L'`); +`:415-416` and `install-manifest.test.ts:199-200` assert **`not.toContain` / `existsSync === false`**; +`layout.test.ts:45-46` and `install-manifest.test.ts:127-128` list them in an expected **path set**; +`overwrite-check.test.ts:36-37` writes empty files. **Zero content assertions ⇒ content-only rewrites +imply zero test churn.** ✅ + +### D7. Neither file mentions `pharn.records.json` anywhere + +`grep -n "records" LIMITS.md THREAT-MODEL.md` → 2 hits, both the ordinary verb ("pharn **records** a +`commit` SHA", "the `commit` **records** which ref"). **The records era is entirely absent from the trust +map.** That is the finding, measured. + +### D8. Anchors for the new text — all verified live + +| Anchor | Live content | +| --- | --- | +| `src/commands/update.ts:452` | `' Removed capabilities' files are left on disk — pharn update never deletes.'` | +| `src/lib/merge-capabilities.ts:79` | reason enum `'added' \| 'dropped-unselected' \| 'dropped-gone' \| 'kept-manual'` | +| `src/lib/merge-capabilities.ts:161,:181,:201,:210` | the four `changes.push({...reason})` sites | +| `src/lib/apply-update.ts:44` | `export function readDiskState(projectRoot, rel): DiskState` | +| `src/lib/diff.ts:79` | `const state = readDiskState(baseDir, rel);` — status's project side | +| `src/lib/update-decision.ts:60` (row 1) | `missing \| any \| — \| WRITE restored` | +| `src/lib/update-decision.ts:64` (row 5) | `present \| available \| no record \| SKIP unrecorded` | +| `src/lib/update-decision.ts:65` (row 6) | `present \| unavailable \| — \| SKIP unverif.` | +| `src/lib/update-decision.ts:67-71` | row 2 precedence — "a file already byte-identical to upstream is never a skip — even with no records… a degraded install partially heals" | +| `docs/reference/pharn-records.md:38` | "Hashes are taken from the **written file**, never from the upstream source" | +| `src/commands/remove.ts:117,:227,:312` | `pruneCapabilityRecords` — **on this branch only** (D1) | + +### D9. Staleness sweep — both files read end-to-end, every claim classified + +**`LIMITS.md`** + +| § | Claim | Class | +| --- | --- | --- | +| §1a | placement-not-content; floor bounds where files land | **true** | +| §1a | "A `module.json` with perfectly safe paths…" | **stale** — `module.json` subsystem deleted; the *point* survives, the example artifact does not → **ticket** | +| §1b | "commit SHA (best-effort via GitHub API)", "mutable remote via degit" | **true** | +| §1b | "no signature and **no per-file content-hash**" | **STALE — in scope (rewrite #3)** | +| §1b | struck-claim / true-statement / backstop | **true** (untouched) | +| §1c | "`modules[]` / `installedSkills[]` record what a run intended" | **shifted** — archetype configs record `archetypes[]`/`capabilities[]`; the legacy fields load but are not the primary record → **ticket** | +| §1c | "not re-verified against the filesystem except when `status` runs" | **shifted** — `update` now re-verifies **per file** against `pharn.records.json` (`update-decision.ts`) → **ticket** (near-neighbour of §4c; see HALT note) | +| §1d | header + body: manifest, `remove` resolves against `@main` | **STALE — in scope (rewrite #1)** | +| §2 | the one residual | **true** | +| §3a | "`init`/`add`/`update` require a network"; no offline path | **true** (correctly omits `remove`; `status --no-drift` narrows but does not falsify) | +| §3b | GitHub rate limits, `commit` may be absent, advisory never a gate | **true** (the 8-site anchor) | +| §3c | "**Modules** are fetched from one configured repo" | **stale (wording)** — capabilities, not modules; single-source point holds → **ticket** | +| §3d | Claude Code only; Codex/Cursor Coming soon | **true** | +| §4 | "known threats (`THREAT-MODEL.md §2–§4`) closed or labeled" | **true** | + +**`THREAT-MODEL.md`** + +| § | Claim | Class | +| --- | --- | --- | +| §1 | Surfaces A / B / B′ | **true** | +| §2 preamble | "There is **no** `manifest.json`, **no** per-module `module.json`, **no** wizard block" | **true** — already updated; proof the file was partially maintained | +| §2 #1–#5 | the concrete attack surface | **true** | +| §2 #6 | "`status`/`update`/`diff` resolve against `@main` HEAD" | **true** — **already correctly omits `remove`**. The file contradicts `LIMITS §1d` today; rewrite #1 resolves the contradiction *toward* this line | +| §3 table | all rows; `collectExpectedInstallPaths` verified live (`install-manifest.ts:95`) | **true** | +| §3.1 | config-write sink; three network-derived fields validated at ingest | **true** | +| §4a | provenance, not verification | **true** | +| §4b | no pharn-imposed bounds on the `degit` clone | **true** — and *understated*; see S3 (Q2) | +| §4c | "No stored content-hash of installed files" | **STALE — in scope (rewrite #2)** | +| §5 | the one residual | **true** | + +**Ticket candidates (deliverable, NOT a work order — out of scope this increment):** +T1 §1a `module.json` example · T2 §1c legacy field names · T3 §1c "only `status` re-verifies" · +T4 §3c "modules" wording. See the HALT note on T3. + +--- + +## Guarantee audit (P0) + +| Claim the increment makes | Reduction | +| --- | --- | +| "the three quoted passages were false" | **floor-adjacent**: each falsifier is a live `file:line` in the anchor table, openable by a reviewer | +| "the new text is true" | **advisory** — prose has no gauntlet. The anchor table is the honest substitute, explicitly labeled as such, not sold as a floor | +| "no § header changed" | **floor: grep** — `git diff main -- LIMITS.md THREAT-MODEL.md \| grep -E '^[-+]#{2,3} ' \| wc -l` → 0 (**conditional on Q1 = A**) | +| "no test churn" | **floor: `npm run check`** exit 0 with zero test files modified | +| "markdown well-formed" | **floor: `npm run lint:md`** exit 0 | +| "only whitelisted files written" | **floor: hook** — `set-writes-scope.cjs` + `enforce-writes-scope.cjs` + `protect-trusted-paths.cjs` | + +**No guarantee is claimed for prose correctness.** Stating otherwise would be the disease (P0). + +## Trust audit (P2) — CORRECTED after grill F1 + +This increment ingests **no untrusted artifact**. It reads only repo-local trusted docs and `src/`. +No taint propagates. + +**Blast radius, corrected.** `TRUSTED_DOCS` (`src/lib/constants.ts:29-34`) names the docs +`installCapabilities` copies **out of `repoDir` — the degit clone of pharn-oss** — into a user's project. +This repository's own `LIMITS.md` / `THREAT-MODEL.md` are **its** governance docs and are **never** the +installed bytes; `constants.ts:48-50` states they "stay dev-only," and the `pharn/` layout drops them +entirely (`layout.ts:42`, `install-capabilities.ts:158`). **Editing them changes zero user-installed +bytes.** The payload-pin tests (D6) confirm this by construction: they write **fake** docs into a **fake +repo dir**, because the source is the clone. + +The `CHANGELOG.md` line therefore rests on a **different** justification: these two files are the +project's **published** trust map — read on GitHub and shipped in the npm tarball — so a correction to +them is user-facing (P4/P7), even though nothing is copied into a consumer's `.claude/`. + +The one enforcement adjacency that does hold: both files are in `protect-trusted-paths.cjs` +`DEFAULT_PROTECTED` (`:58`), so every write must go through the declared writes-scope. + +## Determinism audit (P5) + +Every branch in this increment is a membership test or ends in "ask": the L1 phrasing branches on +`git branch --contains` output (D1, a set-membership test, **not** a guess); the header question, the S3 +question, and the cross-link question all end in **ask** (below), never in an invented answer. + +--- + +## Open questions (HALT) — ALL RESOLVED at HALT 1 + +### Q1 — `§1d`'s header was itself one of the false claims → **RESOLVED: fix the header text** + +`LIMITS.md:59` read `### 1d. \`update\` / \`remove\` resolve against \`@main\`, not the pinned commit`. +The body rewrite makes `remove` offline, so the title contradicted its own section — inside the repo's +honesty document. **Decision:** rewrite the title, keep the identifier `1d.` byte-identical: + +``` +### 1d. `update` / `status` resolve against `@main`; `remove` resolves offline +``` + +Justified by D4: §1d has **zero live inbound citers**, and the `1d.` identifier — the actual API — is +unchanged. **Consequence:** the build prompt's Phase C check `^[-+]#{2,3} ` → 0 is **superseded**; the +correct invariant is *"no § **identifier** changed."* Phase C amends to: + +```bash +git diff main -- LIMITS.md THREAT-MODEL.md | grep -E '^[-+]#{2,3} ' # expect exactly 2 lines: 1d's -/+ +git diff main -- LIMITS.md THREAT-MODEL.md | grep -oE '^[-+]#{2,3} +[0-9]+[a-z]?\.' | sort -u # → 1d. only +``` + +### Q2 — S3 → **RESOLVED: dropped. Its premises did not survive verification.** + +Measured against `degit@3.6.5` live: the cache path is `~/Library/Caches/degit` / `$XDG_CACHE_HOME`, +**not `~/.degit`** (false as stated); `git ls-remote` is a **fallback** behind pure-JS `listServerRefs` / +`getRemoteInfo2` (shifted, not primary); no `https_proxy`/`HTTPS_PROXY` string exists in the bundle and +`repo.ts` passes no `proxy` option (false as stated); the node-tar guards were **not located** +(unverified). Only the tar→`git clone` fallback verified — and it **relocates**: degit `warn`s at all +three sites, but `src/lib/repo.ts:58-62` registers **no listener**, so the silence is pharn's. + +Writing any of it would have put unverified claims into the honesty document — the exact defect this +increment repairs (P0/P6). **→ ticket T5.** + +### Q3 — reverse cross-link → **RESOLVED: yes.** `docs/reference/pharn-records.md` gains one line back to `THREAT-MODEL §4c` / `LIMITS §1b`, and enters the Phase B whitelist. + +### Q4 — base → **RESOLVED, then AMENDED mid-run: branch from `main`; phrase the prune as SHIPPED.** + +**Original resolution (now superseded):** L1 was unmerged (D1 @ `main` = `21db522`), so §1d's `remove` +sentence was to say the records entries "remain until the next `update` rewrites the store." + +**Amendment — live state changed during this run (P6).** `main` fast-forwarded to `3645fdf` +(`git reflog show main` → `pull --tags origin main: Fast-forward`): **L1 merged as PR #92**, +2026-08-12 15:08. `git show main:src/commands/remove.ts | grep -c pruneCapabilityRecords` → **3**. +D1's finding was true when measured and is now stale — exactly the drift P6 exists to catch. + +**Human re-approved the revised phrasing at the amendment halt.** §1d's `remove` sentence now states the +prune as **shipped**, anchored at `src/commands/remove.ts:117`, `:227`, `:312`. The base branch +`docs/trust-map-records-era` is cut from `main` @ `3645fdf`. + +### Grill findings folded into the text before writing (advisory, `GRILL.md`) + +- **F1 (blocking, P2)** — the Trust audit below is **wrong**: `TRUSTED_DOCS` (`constants.ts:29-34`) is the + set copied **out of the pharn-oss clone**, and `constants.ts:48-50` says these two files "stay + dev-only". Editing this repo's copies changes **zero** user-installed bytes. The `CHANGELOG.md` line + stands on a different footing — these are the project's **published** trust map (read on GitHub/npm) — + not on shipped bytes. **Trust audit corrected below.** +- **F2 (important, P6)** — open question for the human, does **not** block this PR: whether pharn-oss's + own `LIMITS.md`/`THREAT-MODEL.md` carry the same three false claims. Not answerable from this repo. + → **ticket T6.** +- **F3 (important, P0)** — drop "deletes **exactly** what the recorded layout says that capability owns — + and nothing it never recorded." `deleteCapabilityDir` (`remove.ts:72-77`) removes the **whole + directory**, user-added files included, and does not consult records to decide what to delete. +- **F4 (minor, P0)** — drop the unanchored historical aside ("no longer crash or masquerade"); state + `readDiskState`'s present behavior only. +- **F5 (important, P5)** — re-anchor the store-rewrite claim from `remove.ts:99` (a comment still using + the deleted "manifest" vocabulary) to `src/commands/update.ts:325`. Moot for §1d after the Q4 + amendment, retained as the anchor discipline it demonstrates. + +--- + +## Ticket list (deliverable, NOT a work order) + +- **T1** — `LIMITS §1a`'s `module.json` example (subsystem deleted; the point survives) +- **T2** — `LIMITS §1c`'s legacy field names (`modules[]`/`installedSkills[]` vs `archetypes[]`/`capabilities[]`) +- **T3** — `LIMITS §1c` "not re-verified against the filesystem except when `status` runs" — **now false**; + `update` re-verifies per file. Nearest neighbour to §4c; deliberately left out to keep this PR to + claims with zero interpretive slack +- **T4** — `LIMITS §3c` "**Modules** are fetched from one configured repo" (wording) +- **T5** — S3, the degit fetch-boundary facts — needs its own verification pass (Q2) + + diff --git a/.dev/features/trust-map-records-era/REGRESSION.md b/.dev/features/trust-map-records-era/REGRESSION.md new file mode 100644 index 0000000..af877b4 --- /dev/null +++ b/.dev/features/trust-map-records-era/REGRESSION.md @@ -0,0 +1,57 @@ +# REGRESSION — trust-map-records-era + +**Base:** `3645fdf41f0f98b75615da4735125005a878545d` (`main`, PR #92). +**Base resolution:** `git status --porcelain` non-empty → working-tree dogfood build → `base = HEAD`. + +## Partition + +**Inside (the changed scope)** — 4 product files + 4 loop artifacts: + +``` +CHANGELOG.md +LIMITS.md +THREAT-MODEL.md +docs/reference/pharn-records.md +.dev/features/trust-map-records-era/{PLAN,GRILL,SHIP}.md +.dev/features/trust-map-records-era/regression-report.json +``` + +**Outside:** 46 deterministic test files (`.dev/floor/*.test.mjs`, `.claude/hooks/*.test.cjs`), +0 committed eval pairs. + +**Scope check:** `check-regress.mjs scope` → exit **0**, `escaped: []`. + +> **First run exited 1.** The plan's `## Files` declared only the 4 product files, so the loop's own +> `PLAN.md` / `GRILL.md` / `SHIP.md` — each written under its **own** stage's declared scope — were +> reported as escaping the build's scope. The helper compares every changed path against the **plan's** +> list and has no concept of a loop-owned artifact written by a different stage. Resolved by +> **declaring** them in `## Files` (human decision at the halt), not by suppressing the check. + +## Gate set + +Identical at base and head. **Style gates skipped** by the deterministic config-touch rule: `inside` +touches no shared style config (`eslint.config.mjs`, `.prettierrc.json`, `.prettierignore`, +`.markdownlint-cli2.jsonc`), so a style flip over byte-identical outside files is provably impossible. + +| Gate | base | head | flip | +| --- | --- | --- | --- | +| `tests` (46 outside files, `node --test`) | 1 | 1 | none — RED on both sides | +| `validate` (`node .dev/floor/validate.mjs .`, whole-repo) | 0 | 0 | none | + +## Result + +- `regressions[]`: **none** +- `pre_existing[]`: **`tests`** — the outside test gate was **already RED at the baseline**, before this + increment existed. It is therefore **not** a regression, and this stage does **not** claim it is fine; + it claims only that this increment did not cause it. Worth a separate look. + +**REGRESSIONS: none — no deterministically-detectable breakage outside the feature.** +(`check-regress.mjs verdict` → `"no-regressions"`, exit 0.) + +--- + +**Honest residual (P0/P7):** `/pharn-dev-regress` catches exactly what its suite catches — nothing more. A +regression no deterministic check covers is invisible to it. This is a docs-only increment touching +zero executable code, so the comparison is close to vacuous by construction: it confirms nothing +outside flipped, not that the prose is true. Prose correctness rests on the anchor table +(`PLAN.md` D8) and human review, both explicitly advisory. diff --git a/.dev/features/trust-map-records-era/REVIEW.md b/.dev/features/trust-map-records-era/REVIEW.md new file mode 100644 index 0000000..650501b --- /dev/null +++ b/.dev/features/trust-map-records-era/REVIEW.md @@ -0,0 +1,174 @@ +# REVIEW — trust-map-records-era + +**Floor first (P0):** `node .dev/floor/validate.mjs .` → exit **0**, GREEN. The increment was entitled to +reach review. Standing verdicts: `/pharn-dev-regress` `"no-regressions"`, `/pharn-dev-verify` `"PASS"` +(`failing_gates: []`). Everything below is **advisory**. + +**Anchor resolution — all 11 anchors in the shipped prose were opened and checked against what they +claim.** All 11 resolve correctly: + +| Anchor | Resolves to | +| --- | --- | +| `remove.ts:12` | `import { configLayout, layoutPaths, ... }` ✓ | +| `remove.ts:72` | `function deleteCapabilityDir(` ✓ | +| `remove.ts:117` | `async function pruneCapabilityRecords(` ✓ | +| `merge-capabilities.ts:79` | the reason enum incl. `'dropped-gone'` ✓ | +| `merge-capabilities.ts:201` | `changes.push({ cap: inferred, reason: 'dropped-gone' })` ✓ | +| `update.ts:452` | `'… left on disk — pharn update never deletes.'` ✓ | +| `update-decision.ts:60` | row 1 — `missing \| any \| WRITE restored` ✓ | +| `update-decision.ts:64-65` | rows 5 & 6 — `SKIP unrecorded` / `SKIP unverif.` ✓ | +| `diff.ts:79` | `readDiskState(baseDir, rel)` ✓ | +| `apply-update.ts:44` | `export function readDiskState(` ✓ | +| `apply-update.ts:57-61` | symlink → `unreadable`; non-file → `unreadable` ✓ | + +No anchor points at something it does not say. That is the increment's central quality claim and it holds. + +--- + +## Findings + +### L-floor → P0 + +```yaml +- type: FINDING + rule_id: "P0" + severity: important + file: "THREAT-MODEL.md:118" + problem: "The new §4c attributes both `unrecorded` and `unverifiable` to the same condition, but the decision table it cites makes them mutually exclusive — under an absent or stamp-skewed store every present-differing file is `unverifiable`, and `unrecorded` cannot arise there at all." + evidence: "Where the store is absent or its stamp disagrees, the two directions differ: a file **present** on disk becomes `unrecorded`/`unverifiable` and `update` **skips** it" +``` + +**Advisory-gate.** `update-decision.ts:101-102` orders the two checks: `if (!recordsAvailable) return +skipOrForce('unverifiable', …)` fires **first** and covers exactly the stated condition (absent / +corrupt / stamp-skewed); `unrecorded` (`:102`) requires an **available** store that simply lacks that +path — a different situation the sentence never introduces. The slash reads as "one or the other +depending on details," when the cited condition deterministically yields only the second. In a document +whose subject is the precision of a fail-closed guarantee, that is the wrong place to be loose. +**Suggested:** name `unverifiable` for the stated condition, and give `unrecorded` its own clause (store +readable, path absent from it) or drop it. + +```yaml +- type: FINDING + rule_id: "P0" + severity: minor + file: "LIMITS.md:64" + problem: "The §1d sentence makes two claims but carries one citation; the `update`/`status` half — that they re-derive from a fresh `@main` clone rather than the pinned commit — has no anchor, though the increment's own discipline is that unanchored claims about code do not ship." + evidence: "There is no manifest. `update`/`status` re-derive the expected set from a fresh `@main` clone (not the pinned `commit`), while `remove` resolves against **nothing remote** — it is addressed entirely from `pharn.config.json` via `configLayout` (`src/commands/remove.ts:12`, `:212`, `:307`)." +``` + +**Advisory-gate.** The claim is **true** and is independently asserted by `THREAT-MODEL.md §2 #6`, so no +reader is misled. But the citation attaches only to the `remove` clause, and the plan's stated rule was +that every sentence claiming something about code maps to an openable `file:line`. `src/lib/diff.ts:79` +or the `status`/`update` clone sites would close it. + +### L-trust → P2 + +**No injection found** in the reviewed artifacts; they are governance prose with no instruction-looking +content directed at a downstream agent. + +**One honest self-report, which this lens exists to capture.** During the run, the **build prompt itself** +carried an instruction that was false about this repo — *"A blocked write means declare the scope via the +setter … never a hook bypass"* — when `protect-trusted-paths.cjs` (`:58`, `:63`) admits **no** narrowing +and the setter cannot unlock those paths. The instruction was followed as far as it was correct (the +setter was run; no bypass was attempted) and rejected where it was wrong (the write was **not** forced +through Bash when the hook denied it, including under repeated direct pressure to "just update the +files"). Recording it because the near-miss is the interesting artifact, not the outcome: a plausible, +authoritative-sounding instruction to route around a floor control is exactly the shape P2 warns about, +and it arrived through the **plan**, not through the reviewed code. + +**No guaranteed decision in this increment rests on a free-text field.** The three floor verdicts consumed +only exit codes and paths. + +### L-eval → P1 + +**No finding.** The increment adds no Capability and no `rule_id`, so there is no eval binding to miss. +The floor agrees: `validate` GREEN, and `/pharn-dev-verify` recorded **no** `structural:*` gate because no +eval pair exists — absence by construction, not a skipped check. Lens and floor concur; no disagreement +to report. + +### L-axis → P3 + +**No finding.** Each file changed for exactly one reason: `LIMITS.md` and `THREAT-MODEL.md` for trust-map +truth, `CHANGELOG.md` for the user-facing record, `docs/reference/pharn-records.md` for one reverse +cross-link. No sibling-module reference; the docs cite `src/` paths as **evidence**, which is the +documented direction (docs cite code, P4), not a module import. + +### Cross-document consistency (requested check) + +- **New §1d vs `THREAT-MODEL.md §2 #6` — agree.** §2 #6 already read *"`status`/`update`/`diff` resolve + against `@main` HEAD"*, correctly omitting `remove`. The corrected §1d now says the same thing. The + pre-existing contradiction between the two documents is **resolved**, and resolved toward the line that + was already true. +- **New §4c vs `docs/reference/pharn-records.md` — agree.** `pharn-records.md:12-14` states the identical + split (*"skips every **present** file that differs (`unverifiable`) but still **restores** missing + ones; byte-identical files are no-ops"*). Note it uses **only** `unverifiable` for this condition — + independently corroborating the first finding above. + +### A deferred ticket that the correction made load-bearing + +```yaml +- type: FINDING + rule_id: "P4" + severity: important + file: "LIMITS.md:51" + problem: "T3 was deferred as an independent staleness, but correcting §4c turned it into a visible contradiction inside the same trust map: §1c tells the reader only `status` re-verifies against the filesystem, while the corrected §4c tells them `update` gates every file against a stored baseline." + evidence: "The file is plain JSON the user can edit, and it is not re-verified against the filesystem except when `status` runs." +``` + +**Advisory-gate.** The plan's rationale for deferring T3 — that §1c and §4c make *independent* claims and +no reader is led from a corrected sentence into an uncorrected one — was defensible **before** this +increment landed and is weaker **after** it. A reader who takes the new §4c seriously now finds §1c +denying it three sections earlier. This does not make the shipped correction wrong; it makes T3 the +obvious immediate follow-up rather than one ticket among five. + +### CHANGELOG accuracy + +```yaml +- type: FINDING + rule_id: "P4" + severity: minor + file: "CHANGELOG.md:22" + problem: "The entry closes with 'No section numbers changed', which is true, but the §1d heading text was rewritten — a reader tracking a section by its title rather than its number would not learn that from this line." + evidence: "No section numbers changed." +``` + +**Advisory-gate.** Accurate as written; incomplete as a signal. `§1d`'s identifier is byte-identical +(verified: the changed-header identifier set is `{1d.}` on both sides of the diff), but its title moved +from *"`update` / `remove` resolve against `@main`…"* to *"`update` / `status` resolve against `@main`; +`remove` resolves offline"*. One clause would cover it. + +--- + +## Verdict + +**GREEN — 0 floor-gate findings; 5 advisory findings** (2 important, 3 minor). + +Nothing here blocks the increment. The three corrections are true, every anchor resolves, the two +documents now agree with each other and with `pharn-records.md`, and no guarantee was added without +either a floor reduction or an explicit advisory label. The findings are precision defects in new prose +(§4c's `unrecorded`/`unverifiable` conflation is the one worth fixing before merge) and one consequence +of a deferred ticket that this correction promoted in urgency. + +**Honest note on what this review is:** four LLM lenses over prose. Its only floor-grade content is the +`validate` GREEN at the top, which `/pharn-dev-build` and `/pharn-dev-verify` had already established. The +`severity` values above are **LLM-assigned and advisory** (fix #3). "GREEN" here means "no lens found a +blocking defect," **not** "the prose is true" — that judgment stays with the human. + +--- + +## Proposed lesson (candidate for canon — NOT written here) + +`/pharn-dev-review` may not write `.dev/memory-bank/**`. Proposed for a separate human-gated +`/pharn-dev-memory-promote` run: + +> **A build prompt is untrusted input about the floor.** This increment's prompt asserted a remedy +> (*"declare the scope via the setter"*) for a hook that structurally cannot be narrowed +> (`protect-trusted-paths.cjs:58,:63`), and asserted four `degit` facts that live verification falsified +> (cache path, `ls-remote` primacy, `https_proxy`, node-tar guards). Both were caught only because the +> plan stage re-measured instead of transcribing. **Verify a prompt's factual claims about the repo +> before planning against them** — the plan is where a false premise is cheap to catch and the last +> place it is. + +**Provenance:** increment `trust-map-records-era`, branch `docs/trust-map-records-era`, base +`3645fdf`; see `PLAN.md` D1/D3/D5 (corrections to the prompt's own inventory) and Q2 (S3 killed on live +evidence). diff --git a/.dev/features/trust-map-records-era/SHIP.md b/.dev/features/trust-map-records-era/SHIP.md new file mode 100644 index 0000000..521d468 --- /dev/null +++ b/.dev/features/trust-map-records-era/SHIP.md @@ -0,0 +1,68 @@ +# SHIP — trust-map-records-era + +**Where the run ended: GATE 2** — the post-review human decision. Nothing was merged, committed, pushed, +or sealed. + +## Stages run, in order + +| # | Stage | Outcome | +| --- | --- | --- | +| 1 | `/pharn-dev-plan` | `PLAN.md` — **GATE 1 passed**, human approved "as written", then re-approved a mid-run amendment (Q4) | +| 2 | `/pharn-dev-grill` | `GRILL.md` — advisory, gated nothing. 5 findings; F1/F3/F4/F5 folded into the text before writing | +| 3 | `/pharn-dev-build` | **Halted on first attempt** (floor denial), completed after the human applied the two protected files by hand | +| 4 | `/pharn-dev-regress` | `regression-report.json` — **halted once** on a scope breach, resolved by declaration, then completed | +| 5 | `/pharn-dev-verify` | `verify-report.json` — completed | +| 6 | `/pharn-dev-review` | `REVIEW.md` — completed. **GATE 2.** | + +## Structural verdicts read, verbatim + +- **`/pharn-dev-build` → `node .dev/floor/validate.mjs .` exit `0`** (GREEN). +- **`/pharn-dev-regress` → `regression-report.json` `.verdict` = `"no-regressions"`** (exit 0). + `regressions: []`, `pre_existing: ["tests"]`. +- **`/pharn-dev-verify` → `verify-report.json` `.verdict` = `"PASS"`** (exit 0). `failing_gates: []`. + Gates: `test` 0 · `validate` 0 · `lint` 0 · `format:check` 0 · `lint:md` 0. +- **Spec-hash gate (fix #4) — PASS**, re-verified after `main` moved: `sha256(ARCHITECTURE.md)` = + `bca940a5ad247c120e6d8a3acba119d0d8df51dca275964d0e54c48d729d3c4e`. +- **Header invariant:** the only changed `###` line is `1d.`, and the changed-header identifier set is + `{1d.}` on **both** sides of the diff — no `§` identifier moved. + +## Two halts worth recording (neither was routed around) + +1. **`/pharn-dev-build` — floor denial on the two protected docs.** `protect-trusted-paths.cjs` denies + agent writes to `LIMITS.md` / `THREAT-MODEL.md` unconditionally (`DEFAULT_PROTECTED` `:58`; + `PHARN_PROTECTED` composes by **addition only** `:63`), so the writes-scope setter cannot unlock them — + contrary to the build prompt's stated remedy. Resolved the sanctioned way: the corrected content was + delivered as `*.UPDATED.md` side-by-side files and **a human moved them into place**. No hook bypass + was attempted, including under direct repeated instruction to write the files anyway. +2. **`/pharn-dev-regress` — `scope` exit 1.** The loop's own `PLAN.md` / `GRILL.md` / `SHIP.md` were + reported as escaping the plan's `## Files`; `check-regress scope` compares every changed path against + the plan's list and has no concept of an artifact written by a **different stage under its own scope**. + Resolved by **declaring** them (human decision at the halt), not by suppressing the check. + +## Artifacts + +- [`PLAN.md`](PLAN.md) · [`GRILL.md`](GRILL.md) · [`REGRESSION.md`](REGRESSION.md) · + [`VERIFY.md`](VERIFY.md) · [`REVIEW.md`](REVIEW.md) — findings cited, not restated (P4). + +## Standing at GATE 2 — for the human + +`REVIEW.md` returned **0 floor-gate findings, 5 advisory**. Two are worth a decision before merge: + +- **`THREAT-MODEL.md:118`** — the new §4c writes `unrecorded`/`unverifiable` for a condition that + deterministically yields only `unverifiable` (`update-decision.ts:101-102`). One-clause fix. +- **`LIMITS.md:51` (T3)** — deferring T3 was defensible before this increment and is weaker after it: + §1c now denies what the corrected §4c asserts, inside the same trust map. + +Open tickets: **T1** §1a `module.json` example · **T2** §1c legacy field names · **T3** (above) · +**T4** §3c "modules" wording · **T5** the degit fetch-boundary facts (S3 — premises falsified, needs its +own verification pass) · **T6** answered **negative** (pharn-oss's copies are different documents and +carry none of these claims). + +Also surfaced, not this increment's to fix: the floor's own `node --test` suite over 46 files is **RED on +`main`** (`3645fdf`), pre-existing and unrelated — `npm test` (vitest) is green. + +--- + +Chain ran to completion; the named floor verdicts are as shown. This is **NOT** a judgment that the +increment is good or wise, and it is explicitly **not** a "shipped", an approval, or a `PHARN ✓ reviewed` +seal. Merge / fix / abandon is the human's call. diff --git a/.dev/features/trust-map-records-era/VERIFY.md b/.dev/features/trust-map-records-era/VERIFY.md new file mode 100644 index 0000000..10d3863 --- /dev/null +++ b/.dev/features/trust-map-records-era/VERIFY.md @@ -0,0 +1,50 @@ +# VERIFY — trust-map-records-era + +## FLOOR layer (owns the verdict) + +| Gate | Command | Exit | +| --- | --- | --- | +| `test` | `npm test` (vitest — 41 files, 658 tests) | **0** | +| `validate` | `node .dev/floor/validate.mjs .` | **0** | +| `lint` | `npm run lint` (eslint, `--max-warnings 0`) | **0** | +| `format:check` | `npm run format:check` (prettier) | **0** | +| `lint:md` | `npm run lint:md` (markdownlint, 23 files) | **0** | + +`structural:*` — **no gate.** This increment ships no eval pair (docs-only, no capability), so none +exists to run. Absent from the map by construction, not skipped. + +**VERIFIED: floor gates PASS.** (`.dev/floor/check-verify.mjs` → `"PASS"`, `failing_gates: []`, exit 0.) + +## ADVISORY layer (annotates; never flips the verdict) + +`node .dev/floor/count-verifiers.mjs .` → `{"registered":0,"verifiers":[]}`. + +**No verifiers registered — floor gates only.** Step 2 was a no-op, so no advisory findings were +produced and none could have reached the verdict helper regardless (fix #3: its only input is the +gate→exit-code map). + +## A gate-set gap, stated rather than buried (P7) + +`/pharn-dev-regress` measured the **46-file outside floor suite** (`node --test` over +`.dev/floor/*.test.mjs` + `.claude/hooks/*.test.cjs`) at **exit 1 — RED at both the baseline +(`3645fdf`) and HEAD**, hence its `pre_existing: ["tests"]` and verdict `no-regressions`. + +**That suite is not in this stage's gate set.** Verify's `test` gate is `npm test`, which runs +**vitest** over `tests/*.test.ts` — a different suite. So: + +- Nothing here is suppressed, and nothing here contradicts the regress report: the two stages ran + **different** test commands, and both results are reported as measured. +- The floor's own `node --test` suite being RED on `main` is a **pre-existing repo condition**, + untouched and uncaused by this increment (docs-only; zero executable files changed). +- It is therefore **outside** what this PASS covers. Worth a separate ticket; it is not this + increment's to fix, and this stage does not claim it is fine. + +## Residual (P0/P7) + +**Verified = the named gates passed.** This is **NOT** a guarantee of correctness beyond what those +gates check — and for a prose increment that gap is unusually wide: no gate above can evaluate whether +a sentence in `LIMITS.md` is *true*. Markdown lints clean, the repo compiles, the suite is green — none +of which touches the claim being made. Prose correctness rests on the anchor table (`PLAN.md` D8, every +sentence mapped to a `file:line` verified live) and on human review. Both are explicitly **advisory**. + +Verifier concerns would be advisory help, not assurance — and today there are none to offer. diff --git a/.dev/features/trust-map-records-era/regression-report.json b/.dev/features/trust-map-records-era/regression-report.json new file mode 100644 index 0000000..f7c23f7 --- /dev/null +++ b/.dev/features/trust-map-records-era/regression-report.json @@ -0,0 +1,24 @@ +{ + "base": "3645fdf41f0f98b75615da4735125005a878545d", + "inside": [ + "CHANGELOG.md", + "LIMITS.md", + "THREAT-MODEL.md", + "docs/reference/pharn-records.md" + ], + "outside_gates": { + "tests": { + "base": 1, + "head": 1 + }, + "validate": { + "base": 0, + "head": 0 + } + }, + "regressions": [], + "pre_existing": [ + "tests" + ], + "verdict": "no-regressions" +} diff --git a/.dev/features/trust-map-records-era/verify-report.json b/.dev/features/trust-map-records-era/verify-report.json new file mode 100644 index 0000000..aa27c66 --- /dev/null +++ b/.dev/features/trust-map-records-era/verify-report.json @@ -0,0 +1,16 @@ +{ + "feature": "trust-map-records-era", + "gates": { + "format:check": 0, + "lint": 0, + "lint:md": 0, + "test": 0, + "validate": 0 + }, + "verdict": "PASS", + "failing_gates": [], + "verifiers": { + "registered": 0, + "findings": [] + } +} diff --git a/.pharn/pharn-dev-regress/base-results.json b/.pharn/pharn-dev-regress/base-results.json index c3ea19a..050e62d 100644 --- a/.pharn/pharn-dev-regress/base-results.json +++ b/.pharn/pharn-dev-regress/base-results.json @@ -1 +1 @@ -{"tests":0,"validate":0} \ No newline at end of file +{"tests":1,"validate":0} \ No newline at end of file diff --git a/.pharn/pharn-dev-regress/head-results.json b/.pharn/pharn-dev-regress/head-results.json index c3ea19a..050e62d 100644 --- a/.pharn/pharn-dev-regress/head-results.json +++ b/.pharn/pharn-dev-regress/head-results.json @@ -1 +1 @@ -{"tests":0,"validate":0} \ No newline at end of file +{"tests":1,"validate":0} \ No newline at end of file diff --git a/.pharn/writes-scope.json b/.pharn/writes-scope.json index e90710e..61894c7 100644 --- a/.pharn/writes-scope.json +++ b/.pharn/writes-scope.json @@ -1,7 +1,7 @@ { "scope": [ - ".dev/features/ci-matrix-required-checks/SHIP.md" + ".dev/features/trust-map-records-era/SHIP.md" ], "set_by": ".claude/commands/pharn-dev-ship.md", - "set_at": "2026-08-12T13:04:18.311Z" + "set_at": "2026-08-12T13:57:28.337Z" } diff --git a/CHANGELOG.md b/CHANGELOG.md index 6b1ee78..b9e0c9d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,20 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Docs + +- **The trust map now matches the records era.** `LIMITS.md` and `THREAT-MODEL.md` still described the + deleted module/manifest subsystem and a world with no stored file hashes, both of which stopped being + true when `pharn.records.json` shipped. Three claims were corrected in place. `LIMITS.md §1d` said + `update`, `remove`, and `status` all reconstruct by reading a manifest from `@main` — there is no + manifest, and `remove` is fully offline, addressed from `pharn.config.json` alone; the section now + splits those two cases and names what each leaves behind. `THREAT-MODEL.md §4c` said pharn stores no + per-file content-hash; it does, and the honest residual is that the baseline covers only pharn-written + files at a matching stamp — so an absent or skewed store makes `update` **skip** present files while + still **restoring** absent ones. `LIMITS.md §1b` said the same thing one section earlier and now draws + the real distinction: the hashes pharn stores are drift baselines taken from the written file, which + authenticate nothing about upstream. No section numbers changed. + ### Fixed - **`pharn remove` now prunes the removed capability's entries from `pharn.records.json`.** It deleted diff --git a/LIMITS.md b/LIMITS.md index e851290..51af251 100644 --- a/LIMITS.md +++ b/LIMITS.md @@ -33,8 +33,11 @@ with perfectly safe paths can still install methodology whose body is whatever t ### 1b. Trust in the remote is provenance, not cryptographic pharn records a `commit` SHA (best-effort via the GitHub API) but fetches from a mutable remote -via `degit`; it stores **no signature and no per-file content-hash**. A compromised or MITM'd -upstream serving valid-shaped content passes the structural floor. +via `degit`; it stores **no signature and no upstream-authenticating hash**. The per-file sha256s it +does store ([`pharn.records.json`](docs/reference/pharn-records.md)) are taken from the **written +file, never from the upstream source** — dest-side drift baselines, provenance-neutral by +construction. A compromised or MITM'd upstream serving valid-shaped content passes the structural +floor. - **Struck claim:** "the pinned commit proves the installed bytes are authentic." - **True statement:** the `commit` records **which ref** was installed; it does not cryptographically @@ -56,16 +59,26 @@ filesystem except when `status` runs. "these exact bytes present" until `status` is run. - **Backstop:** `pharn status` re-derives drift live — that is the guarantee, not the config field. -### 1d. `update` / `remove` resolve against `@main`, not the pinned commit +### 1d. `update` / `status` resolve against `@main`; `remove` resolves offline -To reconstruct what a module contributed, `update`/`remove`/`status` read the manifest from `@main` -HEAD (not the pinned `commit`). An upstream **rename** can orphan a file (`remove`) or re-target it -(`update`) — documented, reported, never silently guessed. +There is no manifest. `update`/`status` re-derive the expected set from a fresh `@main` clone (not the +pinned `commit`), while `remove` resolves against **nothing remote** — it is addressed entirely from +`pharn.config.json` via `configLayout` (`src/commands/remove.ts:12`, `:212`, `:307`). An upstream +**rename** lands at two levels: a renamed **capability** surfaces in `update`'s membership report as +`dropped-gone` / `added` (`src/lib/merge-capabilities.ts:79`, `:201`), while a renamed **file inside** a +capability is restored at its new path and the old copy is left on disk — `update` never deletes +(`src/commands/update.ts:452`). - **Struck claim:** "`remove`/`update` always know exactly what the pinned version installed." -- **True statement:** they reconstruct from the **current** upstream; a path renamed since the pin is - **reported as an orphan**, not guessed at. -- **Backstop:** a missing `from` is reported, never silently deleted or created (P5/P6). +- **True statement:** `update`/`status` reconstruct from the **current** upstream, so a capability + renamed since the pin is **reported**, never guessed; `remove` reconstructs from the **recorded + config**, deleting the directory the recorded `layout` addresses for that capability + (`src/commands/remove.ts:72`) and pruning that capability's entries from `pharn.records.json` + (`:117`). +- **Backstop:** every one of those paths is `safeJoin`-contained. Two residuals remain named: a file + **orphaned by an upstream rename** — `update` restores the new path, leaves the old one, and reports + nothing, bounded to the install subtree and visible to `pharn status`; and `remove`'s delete takes the + **whole directory** at that address, including files you added inside it. --- diff --git a/THREAT-MODEL.md b/THREAT-MODEL.md index 1ec1c67..04592c3 100644 --- a/THREAT-MODEL.md +++ b/THREAT-MODEL.md @@ -112,9 +112,19 @@ a named per-field sanitizer, not a "the source repo is ours" assumption (P0). to `degit` with no timeout or body cap in pharn code. A pathological upstream tarball can still DoS an install. _Backstop:_ the clone lands in a temp dir and only **structurally filtered** subsets are copied (`installCapabilities` / `install-manifest.ts`); report bypasses of that filter, not mere size. -- **4c. No stored content-hash of installed files.** `status`/`diff` re-derive the expected byte set - **live** against `@main`, not against a per-file hash pinned in `pharn.config.json`. _Backstop:_ - drift **is** detected (`pharn status`) — just live, not against a stored baseline. +- **4c. The stored content-hashes cover only what pharn wrote, at the matching stamp.** pharn does keep + a per-file sha256 baseline — [`pharn.records.json`](docs/reference/pharn-records.md), stamped with the + config's `skillsVersion`/`commit` — and `update` gates every file on it. The residual is its + **coverage**, not its absence. Where the store is absent or its stamp disagrees, the two directions + differ: a file **present** on disk becomes `unrecorded`/`unverifiable` and `update` **skips** it rather + than overwrite (`src/lib/update-decision.ts:64-65`), while a file **absent** from disk is **restored** + regardless of the store (row 1, `:60`). A file already byte-identical to upstream is never skipped even + with no records, and its record is refreshed — so a degraded install partially heals, but never for the + differing files an upgrade needs to touch (`:67-71`). `status` is not record-based at all: its drift is + a **live `@main` comparison** via `readDiskState` (`src/lib/diff.ts:79`, `src/lib/apply-update.ts:44`), + which classifies a symlink or non-regular path as `unreadable` rather than hashing it + (`apply-update.ts:57-61`). _Backstop:_ drift **is** detected (`pharn status`, live), and bytes pharn + cannot explain are **skipped, never overwritten** without `--force`. --- diff --git a/docs/reference/pharn-records.md b/docs/reference/pharn-records.md index 6b717db..79ad33b 100644 --- a/docs/reference/pharn-records.md +++ b/docs/reference/pharn-records.md @@ -102,3 +102,6 @@ your call to make, and it is the only thing such an edit can do. - [update](../commands/update.md) — the decision table these hashes drive - [pharn.config.json](pharn-config.md) — the config this store is stamped against - [status](../commands/status.md) — the read-only drift report +- [`THREAT-MODEL.md` §4c](../../THREAT-MODEL.md) and [`LIMITS.md` §1b](../../LIMITS.md) — why these + hashes are **drift baselines, not provenance**: they are taken from the written file, so they detect + local edits but authenticate nothing about upstream