From ea5948aa0f8937926a34dbc4ad72c1fb1ff9482d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Przemys=C5=82aw=20Galarowicz?= Date: Mon, 28 Sep 2026 17:59:47 +0200 Subject: [PATCH] feat(floor): /pharn-regress reuses verified BASE evidence within one delivery run (6.33.0) Within one /pharn-loop or /pharn-ship run, a later /pharn-regress skips the base worktree, the install and every base gate when tested code shows the retained BASE evidence agrees with the current BASE requirement (base SHA, the copied gate spec, install decision, timeout, format versions, and root-level HEAD files in scope). The HEAD side always runs; check-regress.mjs and validateStamp are unchanged. Any mismatch runs the base side exactly as before. - regress-base-reuse-core.mjs (pure rule) + regress-base-reuse.mjs (storage): a record in the git dir bound to the run marker's bytes and age, the stamp and its logs; a persisted HIT is re-decided at the verdict; publication goes through the same predicate for the decision's run and requirement. - regression-report.json gains the additive, advisory base_evidence block. - Progress record pharn-stage-regress-progress/2. - Measured: the second invocation of a run makes 0 worktrees, 0 installs and 0 base gate runs (~10.1 s -> ~4.4 s on the fixture). No token saving claimed. Co-Authored-By: Claude Opus 5.5 --- .claude/commands/pharn-loop.md | 7 +- .claude/commands/pharn-regress.md | 7 +- .dev/features/regress-base-reuse/GRILL.md | 142 +++ .../regress-base-reuse/MEASUREMENT.md | 54 ++ .dev/features/regress-base-reuse/PLAN.md | 428 +++++++++ .../features/regress-base-reuse/REGRESSION.md | 32 + .dev/features/regress-base-reuse/REVIEW.md | 100 +++ .dev/features/regress-base-reuse/SHIP.md | 69 ++ .dev/features/regress-base-reuse/VERIFY.md | 40 + .dev/features/regress-base-reuse/measure.mjs | 190 ++++ .../regress-base-reuse/regression-report.json | 49 ++ .../regress-base-reuse/verify-report.json | 18 + CHANGELOG.md | 41 + CLAUDE.md | 20 +- README.md | 4 +- SKILLS_VERSION | 2 +- pharn/floor/gate-run-core.mjs | 43 + pharn/floor/gate-run-core.test.mjs | 65 ++ pharn/floor/regress-base-reuse-core.mjs | 355 ++++++++ pharn/floor/regress-base-reuse-core.test.mjs | 711 +++++++++++++++ pharn/floor/regress-base-reuse.mjs | 235 +++++ pharn/floor/regress-base-reuse.test.mjs | 295 +++++++ pharn/floor/render-regression.mjs | 22 + pharn/floor/render-regression.test.mjs | 44 + pharn/floor/run-gates.mjs | 37 +- pharn/floor/stage-regress-core.mjs | 106 ++- pharn/floor/stage-regress-core.test.mjs | 72 ++ pharn/floor/stage-regress.mjs | 140 ++- pharn/floor/stage-regress.test.mjs | 829 +++++++++++++++++- pharn/pharn-contracts/regression-report.md | 56 +- pharn/pharn-contracts/stage-exit.md | 4 +- 31 files changed, 4150 insertions(+), 67 deletions(-) create mode 100644 .dev/features/regress-base-reuse/GRILL.md create mode 100644 .dev/features/regress-base-reuse/MEASUREMENT.md create mode 100644 .dev/features/regress-base-reuse/PLAN.md create mode 100644 .dev/features/regress-base-reuse/REGRESSION.md create mode 100644 .dev/features/regress-base-reuse/REVIEW.md create mode 100644 .dev/features/regress-base-reuse/SHIP.md create mode 100644 .dev/features/regress-base-reuse/VERIFY.md create mode 100644 .dev/features/regress-base-reuse/measure.mjs create mode 100644 .dev/features/regress-base-reuse/regression-report.json create mode 100644 .dev/features/regress-base-reuse/verify-report.json create mode 100644 pharn/floor/regress-base-reuse-core.mjs create mode 100644 pharn/floor/regress-base-reuse-core.test.mjs create mode 100644 pharn/floor/regress-base-reuse.mjs create mode 100644 pharn/floor/regress-base-reuse.test.mjs diff --git a/.claude/commands/pharn-loop.md b/.claude/commands/pharn-loop.md index a1f5ba12..c4335c99 100644 --- a/.claude/commands/pharn-loop.md +++ b/.claude/commands/pharn-loop.md @@ -82,9 +82,10 @@ Load the trusted prefix and obey it: (a clarification stop, S6b) simply stays a `Draft`. - **The human decision still exists; it moves to after the run.** A person reviews the branch (or the working tree) and decides what to merge. -- **It is expensive unattended.** Every iteration re-runs `/pharn-regress` (a base worktree, an install, - and the project's suite at base and at HEAD) plus every `/pharn-verify` gate; the worst case is `M` times - that with nobody watching (a quick run skips `/pharn-regress` — `## Quick mode`). +- **It is expensive unattended.** Every iteration re-runs `/pharn-regress` (the project's suite at HEAD, and a + base worktree, an install and the suite at base — which a later iteration reuses when its base requirement is + unchanged, `regression-report.json` `base_evidence`) plus every `/pharn-verify` gate; the worst case is `M` + times that with nobody watching (a quick run skips `/pharn-regress` — `## Quick mode`). ## Step 1 — Entry diff --git a/.claude/commands/pharn-regress.md b/.claude/commands/pharn-regress.md index 7f99c177..9c4a3ab0 100644 --- a/.claude/commands/pharn-regress.md +++ b/.claude/commands/pharn-regress.md @@ -61,6 +61,10 @@ Load the trusted prefix and obey it: - **The residual, named not hidden:** `/pharn-regress` catches **exactly what the project's suite catches — nothing more.** A regression no deterministic check covers is **invisible**. Never read a `done` exit as "nothing broke." +- **A reused BASE side** (the report's `base_evidence.reused`, 6.33.0) is reused because the run marker, the reuse + record, the stamp and its logs agree with this invocation's BASE requirement — a floor decision over hashes and + enums. That an earlier `/pharn-regress` of this run produced it, and that it equals a fresh base run, are + **advisory** (L43; a marker an interrupted run left, ≤ 24 h, also binds — `pharn/pharn-contracts/regression-report.md`). ## Step 0 — Resolve ``, then set the writes-scope (fix #7, fail-closed; amendment A1) @@ -115,7 +119,8 @@ Read the printed `pharn-stage-exit/1` JSON object and branch on the **exit code an earlier run's `.pharn/pharn-regress/` scratch. That run's progress record and base worktree survive, and a `--resume` run now would revive that earlier run. Run `--resume` only after a `5`, or after a Bash-tool timeout (below); - - every later `unusable` has removed the stale report and cleared that scratch, and from "drain-head" + - every later `unusable` has removed the stale report and cleared that scratch (all but a retained + `base-gates/`, kept for reuse), and from "drain-head" onward may have written new state: this run's own progress record, a base-commit checkout, install logs, gate stamps; - a stale-report removal that FAILED (anything but absence) is a crash (below), never a `2` (since 6.26.0). diff --git a/.dev/features/regress-base-reuse/GRILL.md b/.dev/features/regress-base-reuse/GRILL.md new file mode 100644 index 00000000..caee149f --- /dev/null +++ b/.dev/features/regress-base-reuse/GRILL.md @@ -0,0 +1,142 @@ +# GRILL — regress-base-reuse + +Plan: `.dev/features/regress-base-reuse/PLAN.md`. Spec-hash check: `node .dev/floor/hash-doc.mjs pharn/ARCHITECTURE.md` +→ `d831d30d399a37dc403080072763d13383de6f6f31875e7e8cb4eadeb642f4f4`, equal to the plan's `spec_content_hash` (no +drift). **Step 1b (FLOOR):** `node pharn/floor/check-plan-lessons.mjs .dev/features/regress-base-reuse/PLAN.md +.dev/memory-bank/lessons-learned.md` → exit **0**, `GREEN — applied_lessons: L5, L24, L29, L34, L35, L36, L41, L42, +L43, L45, L54, L58, L59, L60, L65 … all 15 cited id(s) resolve … and are referenced in the plan body`. + +**How this grill was run (recorded, advisory).** The interrogation was done by an independent, read-only Opus agent +with a fresh context (it wrote nothing and ran no setter), briefed to test the plan against the live code rather than +the plan's own descriptions. The orchestrator wrote this file from its report. The agent's free text below inherits +the plan's untrusted tag and is quoted as DATA (P2). The 13 registered grillers (`node pharn/floor/count-grillers.mjs +.`) were applied by axis; a11y, i18n and privacy do not apply (no UI, no user-facing strings beyond fixed render +lines, no personal data — the record stores a marker digest). + +## Findings + +```yaml +- type: FINDING + rule_id: "P0" + severity: important + file: ".dev/features/regress-base-reuse/PLAN.md:101" + problem: "'Only the fresh path is safe from a Write-tool forgery' and 'the write tools cannot forge a HIT → floor' are false: stage.json, base-gates/ and the markers are all in .pharn/**, which the Write tool can always reach, during a chain paused at `continue`." + evidence: "runResume trusts .pharn/pharn-regress/stage.json wholesale (stage-regress.mjs:870-891); the planned verdict re-check compares the stamp to a stampSha256 that itself comes from stage.json." +- type: FINDING + rule_id: "P0" + severity: important + file: ".dev/features/regress-base-reuse/PLAN.md:50" + problem: "'A fresh BASE execution is fully determined by …' is false: the base worktree is nested at .pharn/pharn-regress/base inside the HEAD tree, so base gates can resolve HEAD files through parent-directory walk-up (node_modules, npm's .bin PATH, tsc/prettier/eslint config search)." + evidence: "REGRESS_PATHS.base = '.pharn/pharn-regress/base'; gates inherit process.env and run with cwd = base (run-gates.mjs:674-675)." +- type: FINDING + rule_id: "P0" + severity: important + file: ".dev/features/regress-base-reuse/PLAN.md:302" + problem: "'The verdict is not changed by reuse → floor' contradicts the plan's own advisory line that reused evidence equals a fresh run's." + evidence: "PLAN:295 labels the equivalence advisory; PLAN:302 calls the unchanged verdict floor." +- type: FINDING + rule_id: "P6" + severity: important + file: ".dev/features/regress-base-reuse/PLAN.md:117" + problem: "Parsing marker contents makes shipped header sentences false, one of them in a human-only hook." + evidence: "run-marker.mjs:22-35 ('THE GUARD NEVER PARSES A MARKER … session_id … Nothing reads it'); require-loop-record.cjs:73-74 ('The writer and the reader of pharn-loop-active/1 are this one file')." +- type: FINDING + rule_id: "P7" + severity: important + file: ".dev/features/regress-base-reuse/PLAN.md:11" + problem: "The cited trigger is the pre-6.23.0 token-cost measurement that stage-regress-script already answered, while the plan claims no token saving and roadmap 4.2 was gated on an unmeasured M3." + evidence: "PLAN:11-13 cites '~63% of relative cost'; PLAN:284 'No token saving is claimed'." +- type: FINDING + rule_id: "P0" + severity: minor + file: ".dev/features/regress-base-reuse/PLAN.md:105" + problem: "The git-metadata deny holds only under a guarded root; for a linked worktree, a submodule or --separate-git-dir the denial comes from enforce-writes-scope's other-tree/out-of-project rules, and `git rev-parse --git-dir` is relative." + evidence: "gitMetaRelKey matches only keys under ROOT_PREFIXES (protect-trusted-paths.cjs:710-723); `git rev-parse --git-dir` → '.git' (measured)." +- type: FINDING + rule_id: "P0" + severity: minor + file: ".dev/features/regress-base-reuse/PLAN.md:300" + problem: "The delivery-run bound omits that markers are Write-reachable, and publication reads the marker open at publish time rather than at decision time." + evidence: "PLAN:157 'publish … exactly one open run' is evaluated at publish time." +- type: FINDING + rule_id: "P6" + severity: minor + file: ".dev/features/regress-base-reuse/PLAN.md:166" + problem: "'A run straddling the upgrade re-runs fresh' is false: a /1 record read by --resume is unusable progress-malformed, which the command presents and stops on (S9 in the loop)." + evidence: "pharn-regress.md '2 unusable — present … and stop'." +- type: FINDING + rule_id: "P7" + severity: minor + file: ".dev/features/regress-base-reuse/PLAN.md:164" + problem: "Two persisted shapes change (progress /2, a new git-dir record) with no rollback or cleanup note." + evidence: "No Files entry touches the run-close steps; nothing removes the record." +- type: FINDING + rule_id: "P3" + severity: minor + file: ".dev/features/regress-base-reuse/PLAN.md:201" + problem: "The reuse I/O in stage-regress.mjs is a second reason to change, and the log re-hash duplicates check J with a different follow rule." + evidence: "loop-fresh-core.mjs:541 hashes .out/.err with sha256File (follows links); the plan reads them O_NOFOLLOW." +- type: FINDING + rule_id: "P0" + severity: minor + file: ".dev/features/regress-base-reuse/PLAN.md:336" + problem: "Retention stretches check J's exposure: a detached base-gate descendant appending to a retained log after a HIT trips J → STOP at a later iteration, where today a fresh start unlinked the old log." + evidence: "loop-fresh-core.mjs:541-552; stage-regress.mjs:333." +- type: FINDING + rule_id: "P1" + severity: minor + file: ".dev/features/regress-base-reuse/PLAN.md:249" + problem: "The test list misses a budgeted MISS chain that publishes then HITs, retained base-gates that is a link or a file, a foreign feature's evidence, a forged stage.json HIT, a pharn-review marker, and the record's location in a linked worktree." + evidence: "PLAN:251-276." +- type: FINDING + rule_id: "P6" + severity: minor + file: ".dev/features/regress-base-reuse/PLAN.md:171" + problem: "base_evidence cannot tell a run that could not publish from one that did." + evidence: "The block is {reused, miss, requirement_sha256}." +``` + +## Disposition (orchestrator, under the GATE-1 delegation — a model decision, not a human approval) + +All thirteen are accepted, and the plan is amended in place before the build (its `## Grill amendments` section): + +- **F1** — a persisted HIT is never trusted: the verdict phase re-runs the FULL predicate over the disk (the git-dir + record must still bind the stamp). Publication goes through the predicate too — the stage publishes only a record + the predicate would accept NOW — bound to the run identity read at DECISION time (F7). The Write-tool claim is + narrowed: a HIT needs a record only the stage's own publication (or Bash) writes; the one remaining Write-tool path + is forging the base side's in-progress scratch during a chain paused at `continue` — a pre-existing exposure of the + fresh path, which reuse extends to later invocations of the run. Named, with the follow-up + `regress-paused-chain-integrity`. +- **F2** — the requirement is restated as "every input PHARN can enumerate". Two narrowings are built: a run with no + install is never reused (`evidence-unreliable` — dependency resolution may walk up into the HEAD tree), and the + requirement binds the content of every root-level HEAD path in `inside` (the only HEAD files a parent-directory + search from the nested base worktree can reach that differ from base). Ignored root content (`node_modules/`, + `.env`) stays a named residual. +- **F3** — split: floor = the unchanged checker computes the verdict over the stamps on disk; advisory = that it equals + a fresh-base verdict. +- **F4** — markers are no longer parsed. The identity is the write guard's own rule (present, a regular file, mtime + within 24 h in either direction) plus sha256 of the bytes, so no header sentence changes and no human edit is + needed. +- **F5** — the trigger is restated as the maintainer's explicit direction (this run's prompt); M3 was not measured + here; the benefit is wall-clock and compute, measured in `MEASUREMENT.md`. +- **F6** — `git rev-parse --absolute-git-dir`; the ★ HOOK test runs both real hooks, on a main checkout and on a + linked worktree. +- **F7** — the decision-time run identity is persisted and must equal the publish-time one. +- **F8** — reworded: a `/1` record stops as `progress-malformed`; a person re-runs fresh. +- **F9** — rollback note added; the record is inert once its run ends and is named as never removed. +- **F10** — the I/O is its own module (`regress-base-reuse.mjs`, already amended before this grill returned); the log + rule is J's, with a stricter no-follow read, stated. +- **F11** — named residual. +- **F12** — the listed cases are added to the test plan. +- **F13** — the block gains `recorded` (whether a reuse record binds this report's BASE evidence when the invocation + ends). + +## Summary + +The interrogation found no false HIT on the normal paths and confirmed that check-loop-fresh's C/D/E/H/J read the +retained files unchanged. Its substantive concerns were three over-claims (the Write-tool surface, full +determination, the verdict), one design choice that would have falsified shipped headers (marker parsing), and a +trigger stated from the wrong measurement. Each is resolved above by a design change or a narrowed claim. + +ADVISORY VERDICT: 13 concerns raised (0 blocking-severity, 5 important, 8 minor) — all accepted and folded into the +plan before /pharn-dev-build. This is model judgment; it guarantees nothing about the plan (P0). diff --git a/.dev/features/regress-base-reuse/MEASUREMENT.md b/.dev/features/regress-base-reuse/MEASUREMENT.md new file mode 100644 index 00000000..234fed3e --- /dev/null +++ b/.dev/features/regress-base-reuse/MEASUREMENT.md @@ -0,0 +1,54 @@ +# MEASUREMENT — regress-base-reuse (2026-09-28) + +Measured on the maintainer's machine (darwin, Node 24), with `.dev/features/regress-base-reuse/measure.mjs`, 3 +repetitions per variant. Each repetition builds a FRESH fixture repo, opens a `/pharn-loop` run marker, and runs +`stage-regress.mjs` twice over the same BASE requirement, with an in-scope implementation edit between the runs (what +a later loop iteration's build does). Pinned flags: `--timeout-ms 540000 --budget-ms 570000 --base `. + +**The fixture.** An npm lockfile whose `postinstall` sleeps 1.5 s (so the base-commit `npm ci` is a real install +with a cost). Three gates: `test` (node:test, one outside test, 0.8 s), `typecheck` (1 s), `build` (1 s). Every gate +and the install append one line to a counter file outside the repo, and a `post-checkout` hook counts `git worktree +add` checkouts. The COUNTS are the result. The wall-clock shows what the counts save on this fixture's shape. It is not +a claim about any real project. + +- **before** — `main`'s floor at `a2b5f6b` (`git archive a2b5f6b pharn/floor`); +- **after** — this branch's floor. + +## Counts (identical in all 3 repetitions) + +| variant | invocation | worktree checkouts | base installs | base gate runs | head gate runs | all gate runs | +| ------- | ---------- | -----------------: | ------------: | -------------: | -------------: | ------------: | +| before | 1st | 1 | 1 | 3 | 3 | 6 | +| before | 2nd | 1 | 1 | 3 | 3 | 6 | +| after | 1st | 1 | 1 | 3 | 3 | 6 | +| after | 2nd | **0** | **0** | **0** | 3 | **3** | + +After the first invocation, the reports read: + +- `base_evidence = {reused: false, miss: "no-record", recorded: true}`; +- `base_evidence = {reused: true, miss: null, recorded: true}`, with the same `requirement_sha256`. + +## Wall-clock (ms, per repetition) + +| variant | 1st invocation | 2nd invocation | +| ------- | --------------------- | --------------------- | +| before | 10606 / 10181 / 10093 | 10107 / 10114 / 10068 | +| after | 10225 / 10218 / 10173 | 4400 / 4436 / 4365 | + +- **The first invocation is unchanged in behaviour.** It does the same work and adds the decision and one record + write. The difference, −381 ms to +125 ms, is within run-to-run noise. +- **The second invocation drops by ~5.7 s (−56%) on this fixture.** That saving is the base worktree checkout, the + install and the three base gates, and the counts say those are gone. What remains is the HEAD side plus fixed costs: + the spec chain, the partition, git, node start-up and the render. +- **The decision's own cost, timed in-process over the second run's fixture (20 calls), is 17.3–18.4 ms per decision.** + That covers one `git rev-parse` spawn, reading and hashing the record, the stamp, three gates' logs and the markers. + It runs once per invocation, plus once more at the verdict on a HIT. + +## Not measured, and not claimed + +- **Tokens.** The stage is deterministic code; the orchestrator's model turns per regress call are unchanged. +- **A real project's saving.** It scales with the base install and base suite time. The fixture's 1.5 s install and + 2.8 s of base gates stand in for minutes in a real project. +- **How often a real loop iteration keeps its BASE requirement.** A build that touches another outside test file, a + style config or a root-level file misses, correctly. The miss categories in `base_evidence` are the data to count + that later. diff --git a/.dev/features/regress-base-reuse/PLAN.md b/.dev/features/regress-base-reuse/PLAN.md new file mode 100644 index 00000000..00a725e7 --- /dev/null +++ b/.dev/features/regress-base-reuse/PLAN.md @@ -0,0 +1,428 @@ +# PLAN — regress-base-reuse: reuse verified BASE regression evidence within one delivery run + +- spec_content_hash: d831d30d399a37dc403080072763d13383de6f6f31875e7e8cb4eadeb642f4f4 +- applied_lessons: [L5, L24, L29, L34, L35, L36, L41, L42, L43, L45, L54, L58, L59, L60, L65] +- increment: a later `/pharn-regress` of the same `/pharn-loop` or `/pharn-ship` run reuses the BASE-side evidence an earlier one produced — skipping the base worktree, the install and every base gate — only when tested code proves the evidence was produced for exactly the BASE requirement this invocation has; otherwise the base side runs exactly as today. +- layer(s): pharn-floor (product), pharn-contracts (two contracts), product `.claude/commands` (two sentences) +- constitution_refs: [P0, P2, P3, P4, P5, P6, P7] + +## Why (P7) + +**The trigger is the maintainer's explicit direction** — this run's prompt ("PR 2: Reuse Verified BASE Regression +Evidence"), recorded as such (P5; the `check-plan-lessons` sub-check D precedent), not a measurement this plan +re-derives. For context only: the roadmap's earlier token-cost measurement (`/pharn-regress` ~63% of relative cost on +three small fixes, from an older installed version) was the trigger of 6.23.0's stage script, which already answered +its token half; roadmap item 4.2 was held until the maintainer's M3 measurement, which was **not** taken here. So the +benefit this plan claims is the one it measures: deterministic work and wall-clock per repeated regress +(`MEASUREMENT.md`). No token saving is claimed. The scope is the maintainer's: BASE-side reuse only, within one +delivery run, no HEAD/VERIFY reuse, no REGRESS/VERIFY deduplication. + +## Confirmed current behavior (read this run, `stage-regress.mjs` at `a2b5f6b`) + +Every fresh `stage-regress.mjs` invocation: + +1. **deletes all prior regress evidence** — `phaseFreshLate` runs `clearBaseWorktree()` then + `rmSync(".pharn/pharn-regress", {recursive})`: the previous base stamp and logs, the head stamp, `scope.json`; +2. **creates the BASE worktree** — phase `worktree`: `git worktree add --detach .pharn/pharn-regress/base `; +3. **installs at BASE** — phase `install`: `spawnGate(install.cmd)` in that worktree (`npm ci` for an npm lockfile) + whenever `INSTALL_RULE` resolves a command; +4. **initializes the BASE gates** — phase `base-init`: `run-gates.mjs init --side base --spec-from head --cwd base` + (wipes `base-gates/`, fingerprints the base worktree, records `git rev-parse HEAD` there); +5. **runs every BASE gate** — phase `drain-base`: one `run-gates.mjs run --next` child per gate, two worktree + fingerprints per gate; +6. **creates the BASE stamp** — the last `run --next` finalizes `base-gates/stamp.json`; +7. **removes the BASE worktree** — phase `cleanup`: `git worktree remove --force`. + +Nothing is carried between invocations; `stage.json` exists only to resume ONE invocation chain. Measured before any +change, with `main`'s floor on a fixture (`measure.mjs`, scratch run): two identical invocations each made 1 worktree +checkout, 1 install, 3 base gate runs and 3 head gate runs (10.9 s and 10.1 s). + +**Where one delivery run invokes regress more than once** (read this run): + +- `/pharn-loop` Step 5 item 2 — every iteration `N` runs `/pharn-regress --base `, the SAME base SHA for + the whole run (resolved once in Step 1a); and Step 5 item 3 — a `check-loop-fresh.mjs` `RERUN` naming `regress` + re-invokes it inside the same iteration ("a verify re-run can cascade into a regress re-run"). +- `/pharn-ship` step 1 (iteration 1) and Step 2b's single build-completion retry (iteration 2) — both without + `--base`, so `BASE_RULE` resolves the base each time (after a build the tree is dirty, so both resolve `HEAD`). + +In every one of those repeats all seven steps above run again, even when nothing about the BASE side changed. + +## The design + +### What the BASE requirement is (the reuse identity, derived — never assumed) + +A fresh BASE execution depends on the inputs below **and on whatever its gates read outside the base worktree** +(grill F2): the worktree is nested at `.pharn/pharn-regress/base`, inside the HEAD tree, so a tool that searches +parent directories (node's module resolution, npm's `.bin` PATH, tsc/prettier/eslint config lookup) can reach the +HEAD tree's root; the environment and the machine are read too. The fresh path has always been exposed to those reads. +The requirement binds every one of them PHARN can enumerate: + +- the base commit; +- the spec `run-gates.mjs init --side base` copies from the head record (`source`, `source_raw`, `style_skipped`, + `required`, and every entry's ordered `id`/`shell`/`argv`/`files` — which already carries the outside-test list, + the outside eval pairs, the style skip, explicit-vs-discovered gates and the e2e exclusion); +- the install decision (`INSTALL_RULE`, a `--install`/`--no-install` override included) and the per-gate timeout; +- the formats the evidence is written in (the gate-run stamp schema and the fingerprint algorithm); +- **`head_root`** — the content sha256 of every ROOT-LEVEL path in `inside` (changed since base, tracked or + untracked-not-ignored): the only HEAD files a parent-directory search from the nested base worktree can reach that + differ from the base's own copies (a root file unchanged since base is shadowed by the base worktree's identical + copy, which the search meets first). + +```text +requirement = {schema: "pharn-regress-base-requirement/1", feature, base, gate_run_schema, fingerprint_algo, + spec: {source, source_raw, style_skipped, required, entries: [{id, shell, argv, files}]}, + install: {kind, cmd}, timeout_ms, head_root: [[path, sha256 | null], …]} +requirement_sha256 = sha256(JSON.stringify(requirement)) // fixed key order by construction +``` + +- **The spec is computed by the SAME function `init --side base` uses** — extracted from `run-gates.mjs` into + `gate-run-core.mjs` as `baseSpecFrom(headRecord, feature)` (one owner, L35), applied to the finalized head stamp + exactly as `init` reads it. +- **The evidence's requirement is read from the evidence itself**: the base stamp records `head`, `feature`, + `schema`, `fingerprint.algo`, `source`, `source_raw`, `style_skipped`, `required` and each run's + `id`/`shell`/`argv`/`files`. Only the install decision, the timeout and `head_root` are not in a stamp, so the + record carries exactly those three about the requirement — no second copy of the spec (L35). +- **Never reused, by rule** (`evidence-unreliable`): a run with NO install (dependency resolution may then walk up + into the HEAD tree's `node_modules/` or npm prefix — grill F2's proportionate mitigation), an install that failed or + timed out, or a base gate that timed out. +- **Deliberately NOT part of it**, each for a stated reason: the rest of the HEAD tree (a new build that leaves the + above alone must still HIT); `--budget-ms` (it decides where an invocation pauses, never a gate's result); the + install's `unmeasured`/`family`/`reason` labels; the install's logs and duration; the head stamp's fingerprints; + `e2e_excluded` (an excluded id is simply absent from `entries`); the base worktree's fingerprint (the stamp records + it; nothing predicts it). +- **Named residual:** ignored HEAD root content (`node_modules/`, `.env`, caches) is reachable by the same search and is + not bound (it cannot be hashed cheaply); with an install, the base worktree's own `node_modules/` shadows the common + case. + +### Where the evidence lives — unchanged, and retained + +The BASE evidence stays exactly where it is produced and read today: `.pharn/pharn-regress/base-gates/` +(`stamp.json` + each gate's `.out`/`.err`/results file) — the path `check-loop-fresh.mjs`'s `DEFAULT_STAMPS` reads. +No copy, no second format. The only lifetime change: the fresh start clears `.pharn/pharn-regress/` **except +`base-gates/`** (kept only when `lstat` says it is a real directory). Everything else is cleared as today. + +### The reuse record — the binding, kept where the write tools cannot reach it (L65) + +`/pharn-regress-base-reuse.json` (per worktree), schema +`pharn-regress-base-reuse/1`, closed keys: + +```json +{ + "schema": "pharn-regress-base-reuse/1", + "feature": "", + "run": { "command": "pharn-loop | pharn-ship", "marker_sha256": "<64 hex>" }, + "stamp_sha256": "<64 hex>", + "install": { "kind": "cmd", "cmd": "npm ci" }, + "install_result": { "ran": true, "exit": 0, "timedOut": false }, + "timeout_ms": 540000, + "head_root": [["package.json", "<64 hex>"]] +} +``` + +**Why the git dir and not `.pharn/`** (probed this run): `enforce-writes-scope.cjs` always allows `.pharn/**` to the +write tools, so a record there could be rewritten between two regress invocations by the very build the next one +judges. The write tools cannot write the git dir: `protect-trusted-paths.cjs` denies any path with a `.git` segment +under a guarded root (a main checkout: `.git/…` → exit 2, `.pharn/…` → exit 0), and for a linked worktree, a submodule +or a separate git dir, `enforce-writes-scope.cjs` denies a path inside another git tree or outside the project in +every posture (grill F6; the ★ HOOK test runs both real hooks on both layouts). The record binds the stamp by sha256 +and the stamp binds its logs by sha256, so a write-tool edit of any retained evidence file reads as a MISS. + +**What remains reachable, stated (grill F1):** a Bash writer reaches the git dir and `.pharn/` alike (L19) and can +forge the record and the evidence together. And the base side's own in-progress scratch (`stage.json`, +`base-gates/state.json`) is Write-reachable while its chain is paused at a `continue`; forging it falsifies that +invocation's base evidence — as it can today on the fresh path — and with reuse the falsified evidence would then be +published and reused by later invocations of the same run until their requirement changes. Named, with the follow-up +`regress-paused-chain-integrity` (a git-dir digest of the in-progress record, which would close it for both paths). + +### Lifetime — one delivery run, bound to its run marker + +The delivery run is identified by the run marker the orchestrator already opens and nothing rewrites during the run: +`.pharn/pharn-loop//active.json` (`require-loop-record.cjs --open`) or `.pharn/pharn-ship//active.json` +(`run-marker.mjs --open`). **The marker is never parsed** (grill F4 — both writers' headers say no reader parses +it): it counts when it is PRESENT and OPEN by the write guard's own rule — a regular file (`lstat`, never followed, +no symlink component) whose mtime is within 24 h of now in either direction — and the identity is +`{command, marker_sha256 = sha256(its bytes)}`. Each `--open` rewrites the file (a new `started_at`), so a new run has +new bytes. Exactly one of the two marker paths present ⇒ an identity; none or both ⇒ no delivery run (a +`pharn-review` marker is never looked at). A standalone `/pharn-regress` (no marker) never publishes and never reuses. + +**Bounds, stated (grill F7):** the markers live in `.pharn/`, so the binding is relative to a Write-reachable file — +restoring an earlier run's marker bytes within 24 h re-binds that run's record; and a marker a crashed run left behind +counts as open for 24 h (the write guard's own rule), so a standalone regress in that window binds to it. The run +identity is read at DECISION time and persisted; publication requires the same identity again. + +### The predicate — tested code, first failure decides (P5) + +`regress-base-reuse-core.mjs` `decideBaseReuse(...)` returns `{reused, miss, requirementSha256, stampSha256, run}`. +`miss` is a member of `BASE_REUSE_MISSES` (owned by `stage-regress-core.mjs` with the stage's other closed +vocabularies, because its G8 pin keeps that module's import list at `gate-run-core.mjs` and the progress validator +must check membership; closure tested both ways, L29/L36), in evaluation order: + +| miss | when | +| --------------------- | --------------------------------------------------------------------------------------------------- | +| `requirement-unknown` | the current spec cannot be derived from the head stamp (base-init would then refuse as today) | +| `no-delivery-run` | not exactly one open run marker for this feature | +| `no-record` | no reuse record | +| `record-malformed` | the record is unreadable, not a regular file, or fails its closed schema | +| `other-run` | the record names another feature, or another run's marker | +| `evidence-missing` | no `base-gates/stamp.json` | +| `evidence-unbound` | sha256(stamp bytes) ≠ the record's `stamp_sha256` | +| `evidence-invalid` | a link or unreadable stamp; not JSON; `validateStamp` (regress, base, feature) refuses it — | +| | unfinalized included; or a recorded stdout/stderr/results sha256 is not the file on disk (check J's | +| | rule, read no-follow — stricter than J, which follows a link) | +| `version-changed` | the stamp's fingerprint algorithm is not the current `ALGO` (its schema is `validateStamp`'s) | +| `base-changed` | the stamp's `head` is not this invocation's base SHA | +| `gates-changed` | the stamp's spec differs from the current one (source, raw, style skip, required, ordered entries) | +| `execution-changed` | the install decision, the timeout, or `head_root` differs | +| `evidence-unreliable` | no install, an install that failed or timed out, or a timed-out base gate | + +HIT only when every row passes. A MISS never asks and never guesses: it runs the BASE side exactly as today. + +### The flow + +```text +fresh start (clear scratch EXCEPT base-gates/) → chain → base → partition → head-init → drain-head + → decide (after the HEAD side is finalized; re-done on a resume at drain-head — the drain is idempotent) + HIT → verdict: RE-RUN the full predicate over the disk (a persisted HIT is never trusted — grill F1); it must + HIT on the same stamp bytes, else fall back to MISS (its own category) and run the base side + → cleanup SKIPPED (no worktree was made) → render + MISS → worktree (first discard: record, then base-gates/) → install → base-init → drain-base + → verdict → publish ONLY a record the predicate accepts now, bound to the DECISION-time run identity + → cleanup → render +``` + +- The HEAD side always runs (drain-head precedes the decision). +- `check-regress.mjs` and `validateStamp()` are untouched: a reused stamp is the same file, read by the same checker, + under the same validation. check-loop-fresh C/D/E/H/J read the same retained files and pass unchanged. +- **Publication goes through the predicate.** At verdict (exit 0 or 1: both stamps validated, the specs agree, the + base head matches), the stage builds the record it would publish and publishes it only if `decideBaseReuse` HITs + with it — so a published record always binds evidence that meets the current requirement, is finalized, has + intact logs, was installed, and belongs to the run the decision saw. +- The progress record becomes `pharn-stage-regress-progress/2`: it gains `baseReuse` — `null` at `drain-head`, the + decision from then on (a HIT decision only at `verdict`). A `/1` record read by `--resume` is `unusable +progress-malformed`: the command stops (S9 in `/pharn-loop`) and a person re-runs `/pharn-regress` fresh (grill F8). + No second resume protocol. + +### Observability — minimal, structured + +- `regression-report.json` gains ONE additive advisory block, appended last: + `"base_evidence": {"reused": bool, "miss": | null, "requirement_sha256": | null, "recorded": bool}`. + `reused: true` means this invocation created no base worktree, ran no install and ran no base gate; + `gate_run.base.stamp_sha256` (the checker's own field) names the stamp either way. `recorded` says whether a reuse + record binds that stamp when the invocation ends (grill F13) — so an unwritable git dir or a standalone run is + visible. Every key the checker printed keeps its bytes (a test pins report-minus-block == the checker's stdout). The + contract already ignores extra keys, and no floor op reads the block. +- `REGRESSION.md` gains one line naming reuse or the miss category and whether the evidence is recorded; on a HIT the + install line says no install ran. No large payload. + +## Crash and atomicity (never a false HIT) + +- **While BASE evidence is produced / installed / a base gate runs**: a MISS removes the old record (then the old + evidence) before `base-init`; a resume continues as today; a fresh start finds no record → `no-record` → recompute. +- **Publication**: tmp + `rename` inside the git dir, only after the verdict and only for a record the predicate + accepts. A kill before the rename leaves no record (a stray `.tmp-` file is never read as one — named). A kill + right after leaves a complete record binding a finalized, validated stamp; the resumed invocation re-runs `verdict` + and re-publishes the same bytes. +- **Consumption**: a HIT is re-decided in full at `verdict`, in the same and in any resumed invocation; any change + falls back to a MISS and a recompute. +- A partially written or unfinalized stamp can never pass: `validateStamp` refuses `finalized !== true`, and the sha256 + binding refuses any byte that differs from the published evidence. +- **Rollback (grill F9):** a floor older than this one never reads the git-dir record (inert) and reads a `/2` + progress record as `progress-malformed` (stop, re-run fresh). Nothing removes the record when a run ends; it is inert + once its marker is gone or rewritten, and the next MISS in that worktree removes it. Named. + +## Files + +- `pharn/floor/regress-base-reuse-core.mjs` — NEW. Pure rules: the requirement object + digest, the evidence's + requirement, the delivery-run identity from marker bytes and mtime, the record builder/validator, the evidence file + list, and `decideBaseReuse`. Imports `node:crypto`, `gate-run-core.mjs`, `stage-regress-core.mjs`. — layer + pharn-floor +- `pharn/floor/regress-base-reuse-core.test.mjs` — NEW. Unit tests: every miss row with a one-input mutation that + flips HIT→MISS, the irrelevant inputs that must not, record/marker validation, closure both ways, the age and path + parity with the write guard and the two marker writers, the J-rule parity. — layer pharn-floor (test) +- `pharn/floor/regress-base-reuse.mjs` — NEW (P3: the reuse STORAGE is its own axis, so it does not grow + `stage-regress.mjs` — grill F10). The execution half: `lstat`-first, `O_NOFOLLOW` reads of the markers, the record + and the retained stamp; the evidence and `head_root` hashes; publication through the predicate (tmp + rename in the + git dir); discarding. No CLI. — layer pharn-floor +- `pharn/floor/regress-base-reuse.test.mjs` — NEW. The I/O edges per path kind (L59): a link, a dangling link, a + directory and an oversize file at a marker, record or stamp path are `unusable`; a `pharn-review` marker is ignored; + the record lands in a linked worktree's own git dir; a failed publication leaves no record. — layer pharn-floor + (test) +- `pharn/floor/run-gates.mjs` — `init --side base` calls `baseSpecFrom` (behavior unchanged); exports its + `sha256RegularFile`, so the evidence logs are re-hashed by the function that recorded the results digest. — layer + pharn-floor +- `pharn/floor/stage-regress.mjs` — retention, decision, HIT path, verdict-time re-decision, publication, report + block, render inputs. — layer pharn-floor +- `pharn/floor/stage-regress.test.mjs` — end-to-end (see the test list). — layer pharn-floor (test) +- `pharn/floor/stage-regress-core.mjs` — `BASE_REUSE_MISSES`; `PROGRESS_SCHEMA` → `/2` with `baseReuse` + validation. Still imports only `gate-run-core.mjs` (G8 pin unchanged). — layer pharn-floor +- `pharn/floor/stage-regress-core.test.mjs` — `/2` record cases. — layer pharn-floor (test) +- `pharn/floor/gate-run-core.mjs` — `baseSpecFrom(headRecord, feature)`, extracted from `run-gates.mjs` with the same + refusals (`spec-mismatch`), total over parsed JSON (L62). — layer pharn-floor +- `pharn/floor/gate-run-core.test.mjs` — `baseSpecFrom` unit cases. — layer pharn-floor (test) +- `pharn/floor/render-regression.mjs` — the base-evidence line; the HIT install line. — layer pharn-floor +- `pharn/floor/render-regression.test.mjs` — render cases for HIT and MISS. — layer pharn-floor (test) +- `pharn/pharn-contracts/regression-report.md` — the additive `base_evidence` block and the miss vocabulary; the + "verbatim" sentences corrected. — layer pharn-contracts +- `pharn/pharn-contracts/stage-exit.md` — the scratch-clear sentences now keep `base-gates/`. — layer pharn-contracts +- `.claude/commands/pharn-regress.md` — the N1 `unusable` bullet's "cleared that scratch" corrected, and one claims + line on reuse. — product command +- `.claude/commands/pharn-loop.md` — the "expensive unattended" sentence corrected (base reused when unchanged). — + product command +- `CHANGELOG.md` — `## [6.33.0]`. — repo meta +- `SKILLS_VERSION` — `6.33.0` (minor: a new shipped capability). — repo meta +- `README.md` — the version badge and the generated CURRENT-STATE floor count (`npm run docs:generate`). — repo meta +- `CLAUDE.md` — the stage-regress block: the progress schema id and one paragraph on BASE reuse. — repo meta +- `.dev/features/regress-base-reuse/MEASUREMENT.md` — the before/after counts and timings. — apparatus +- `.dev/features/regress-base-reuse/measure.mjs` — the measurement harness. — apparatus + +`MIN_CLI` stays `0.5.0`: no installed path moves and no existing file changes shape for an older CLI; new floor files +are copied by `pharn update` (roadmap pre-check P1). + +## Contracts satisfied + +- `pharn/pharn-contracts/gate-run-record.md` — a reused BASE stamp is an unmodified `gate-run-record/1` stamp; no + weaker variant exists. +- `pharn/pharn-contracts/regression-report.md` — the verdict fields are `check-regress.mjs`'s; the new block is + additive and advisory ("Extra keys are IGNORED"). +- `pharn/pharn-contracts/stage-exit.md` — exit codes, statuses and the regress registry are unchanged; no new + reason_code. + +## Evals to write (P1) + +No Capability (`role:`) is added or changed, so no eval is owed. The floor modules carry `node --test` suites (Files +above), including the mandatory fresh-vs-reused equivalence test. + +## Tests and negative controls (the build must deliver each) + +- **Equivalence (mandatory).** One fixture with a real regression (an outside test broken), a pre-existing red gate, + a green gate and an outside `structural:` eval pair. Path A: a fresh BASE execution (no run marker → + `no-delivery-run`). Path B: a marker opened, a publishing run, then a run that HITs. Assert: B `reused: true`; A and + B agree on `verdict`, `regressions`, `pre_existing`, `outside_gates`; the reused base stamp and A's fresh base stamp + agree on every run's `id`/`exit`/`argv`/`shell`/`files` and on `head`/`source`/`required`; `check-regress.mjs +verdict` re-run over B's stamps reproduces B. A second HIT fixture has a `no-files` test entry. +- **The HIT path is real**, proven by counters, never by the predicate: a `post-checkout` hook counts worktree + checkouts, the install command and each gate append to a counter file (base vs head by cwd). The first run counts + non-zero (L34); the second counts zero worktree checkouts, zero installs, zero base gate runs, and every head gate. +- **HIT despite a HEAD change**: an implementation edit between the runs; still a HIT, and the head stamp moved. +- **A budgeted MISS chain** (`--budget-ms 1`, the pinned line's shape) that completes through `--resume`, publishes, + and the next invocation HITs. +- **End-to-end MISS controls, one input each**, each also showing the base side ran: `--base` another commit + (`base-changed`); a gate script added (`gates-changed`); an explicit `--gates` command changed (`gates-changed`); + an outside test becomes inside (`gates-changed`); an eval pair becomes inside (`gates-changed`); a style config + touched (`gates-changed`); `--install` changed and `--timeout-ms` changed (`execution-changed`); a new root-level + file added between runs (`execution-changed`); the marker reopened as a new run (`other-run`); no marker, both + markers, a >24 h marker (`no-delivery-run`); record deleted / malformed; stamp edited (`evidence-unbound`); a log + edited, a symlinked log, a `base-gates/` that is a link or a file (`evidence-invalid` / `evidence-missing`); a + foreign feature's retained evidence; a failed install, `--no-install`, a timed-out base gate + (`evidence-unreliable`). +- **A forged `stage.json` HIT at `verdict`** (a record that does not bind the stamp) is not honored: `--resume` runs + the base side. +- **Unit-only controls** where an end-to-end mutation cannot isolate the input: an unfinalized stamp and a foreign + fingerprint algorithm, each re-bound by a matching record sha256, so only that row can fire. +- **Non-vacuity (L34/L60)**: every control asserts its own category. +- **Crash**: a kill during `drain-base` on a MISS run, then a fresh run → no false HIT. +- **★ HOOK**: both real hooks deny a Write to the record path — on a main checkout and on a linked worktree — and + allow the same write under `.pharn/` (the reason the record is not there). +- **★ loop freshness**: `check-loop-fresh.mjs` reads FRESH over a HIT run, D/E/H/J each `pass`. +- **★ WIRING**: the pinned `pharn-regress.md` line reaches `done` twice under one run marker, the second a HIT. +- The e2e controls use a counting shell install, not `npm ci`, to keep `npm test` bounded; the ★ WIRING HIT uses the + real `npm ci` line. + +## Measurement + +`measure.mjs` builds one fixture (an npm lockfile whose `postinstall` sleeps and counts, three gates that sleep and +count, a `post-checkout` counter), opens a loop marker, and runs the stage twice over the same requirement (an +in-scope edit between the runs) — once with `main`'s floor, once with this branch's. It records, per invocation: +worktree checkouts, installs, base gate runs, all gate runs, wall-clock, and the decision's own cost. Measured, never +assumed (L24). No token saving is claimed. + +## Guarantee audit (P0) + +- "Whether BASE evidence is reused is decided by tested code" → floor: enum-regex + content-hash (sha256 equality of + marker, stamp, log and root-file bytes; membership in the closed miss set; equality of the requirement object). + Running the stage at all stays advisory orchestration, as today. +- "A reused BASE stamp meets the same contract as a fresh one" → floor: `check-regress.mjs` and `validateStamp` are + unchanged and read the same file; the predicate additionally re-validates it. +- "The verdict is computed by the unchanged checker over the stamps on disk" → floor (grill F3). **"That verdict + equals the verdict a fresh BASE run would give now" → advisory**: it assumes the gates are deterministic for one + commit, spec, install, timeout and root-file set, and that nothing the requirement does not bind changed (ignored + root content, the environment, the machine) — the assumption the fresh path already makes of its one sample. +- "A HIT uses evidence a publication of this run bound to an identical BASE requirement" → floor, relative to the + record, the stamp and the marker (equality). +- "The write tools cannot forge a HIT" → floor: hooks (`protect-trusted-paths.cjs` git metadata; `enforce-writes- +scope.cjs` other-tree/out-of-project), probed by the ★ HOOK test, **narrowed** (grill F1): the record is out of the + write tools' reach, and a HIT is re-decided at the verdict; forging the base side's in-progress scratch during a + paused MISS chain can still bind falsified evidence (named above). A Bash writer can forge anything (L19). +- "Evidence never crosses delivery runs" → floor relative to the marker bytes and mtime; **not** that the marker + belongs to a live run, and the marker is Write-reachable (bounds above). +- The report block is advisory and read by no floor op. + +## Trust audit (P2) + +- The run markers are hashed and `lstat`ed, never parsed. The record, the stamp and its logs are deterministic-tool + JSON/bytes under `.pharn/` and the git dir, parsed as strings, integers and hex digests, hashed and compared; never + executed or rendered as text. Logs and root files are hashed, never read. +- The report block carries booleans, a closed-enum member and a hex digest; the render line quotes only those. +- No new untrusted input reaches a shell: the stage still passes git and node argument vectors. + +## Determinism audit (P5) + +Every branch is a membership or equality test. There is no fallback to a question: a MISS is the safe, fully defined +default (the base side runs exactly as today). Nothing is classified by a model. + +## Applied lessons + +- L5 — the requirement is computed by code from the head stamp and the scope record, never typed by the orchestrator + or read from a report. +- L24 — the before/after numbers are measured on a fixture built to exercise the base side, with `main`'s script as + the control, never inherited. +- L29 — the miss vocabulary is one materialized enum that every rule and test iterates. +- L34 — the HIT counters are asserted non-zero on the first run, so a zero on the second cannot come from a fixture + that counts nothing. +- L35 — the requirement is read from the stamp itself (no second copy of the spec), and `baseSpecFrom` is extracted + so base-init and the predicate share one owner; publication reuses the predicate instead of a second rule. +- L36 — closure both ways: every miss literal the core returns is a member, and every member is returned by some input. +- L41 — the pinned `pharn-regress.md` line (its real `--timeout-ms` and `--budget-ms`) is executed twice, not only the + tests' own explicit values. +- L42 — the decision answers "was this evidence produced for this requirement", never re-runs a gate to ask "would + it pass now". +- L43 — the record, the stamp and the marker agreeing proves agreement, never provenance; the plan and the code say so. +- L45 — the HIT is proven through the real CLI and the command's pinned line, not only the predicate. +- L54 — every evidence, record and marker read is `lstat`-first; absence is `lstat`'s own ENOENT. +- L58 — the record binds the stamp (immutable once finalized) and the marker (rewritten only by a new run); a log + appended by a detached descendant after binding is re-hashed at decision time and MISSes, and the one window it + cannot see (an append after a HIT, before the loop's check J) is named. +- L59 — log, stamp, record and marker files are read through an `O_NOFOLLOW` descriptor and `fstat`-checked; a symlink + is a miss, never followed. +- L60 — each negative control asserts its own miss category, and the unit controls re-bind the record so only the + targeted row can fire. +- L65 — the record the next regress compares against is kept where the write tools cannot reach it, a ★ HOOK test + executes both real guards to prove it, and the one Write-reachable input left (the paused-chain scratch) is named. + +## Named residuals (not closed here) + +- A Bash writer can forge the record and the evidence together (L19); nothing detects it. +- The paused-chain Write-tool forgery above (`regress-paused-chain-integrity`). +- Gate nondeterminism, ignored HEAD root content and environment drift within one run are reused as sampled. +- A marker a crashed run left (≤ 24 h), or restored marker bytes, binds a later regress. +- Retention stretches check J's detached-descendant trip across iterations: a base gate's detached descendant that + appends to a retained log after a HIT trips J (`output-hash-mismatch`, S11) at a later check, where a fresh start + used to unlink that log (grill F11). +- A kill between the record's tmp write and its rename leaves a `*.tmp-` file in the git dir. +- If the git dir is not writable, nothing is published (`recorded: false`) and every repeat MISSes `no-record` — + never a false HIT. +- `/pharn-dev-regress` (the dev twin, prose-driven) is unchanged. + +## Grill amendments (orchestrator, before the build) + +`GRILL.md` holds the independent grill's 13 findings; every one was accepted and is folded into the sections above: +F1 (full re-decision at verdict, publication through the predicate, narrowed Write-tool claim), F2 (requirement +restated; no-install never reused; `head_root` bound), F3 (verdict claim split), F4 (markers hashed, never parsed), +F5 (trigger restated), F6 (`--absolute-git-dir`, both hooks, both layouts), F7 (decision-time identity), F8 (`/1` +wording), F9 (rollback), F10 (I/O module), F11 (J residual), F12 (tests), F13 (`recorded`). + +## Open questions (HALT) + +None left open. GATE 1 is delegated by the maintainer's own instruction for this run ("Do not stop after writing a +PLAN"); the design choices above, and the grill amendments, are recorded as model decisions made under that +delegation, not as a human approval. diff --git a/.dev/features/regress-base-reuse/REGRESSION.md b/.dev/features/regress-base-reuse/REGRESSION.md new file mode 100644 index 00000000..4d4468ce --- /dev/null +++ b/.dev/features/regress-base-reuse/REGRESSION.md @@ -0,0 +1,32 @@ +# REGRESSION — regress-base-reuse + +- **Iteration:** 3. It re-runs after the GATE-2 review fixes. Iteration 1 over the build and iteration 3 over the + fixed tree both read `no-regressions`. Iteration 2, after the lint fix, was stopped once its regress half finished + (`no-regressions`), because the review fixes changed the tree. +- **Base:** `a2b5f6be027a69c0abd69f373cfb7ba0660b536d`. This is HEAD: a working-tree dogfood build, so + `git status --porcelain` was non-empty. +- **Inside (27 paths):** the changed set. That is the plan's `## Files` set plus this feature's own pipeline + artifacts (`PLAN.md`, `GRILL.md`, `REGRESSION.md`, `regression-report.json`), which are escape-exempt and reported + in `escape_exempt`. `check-regress.mjs scope` exited **0** with `escaped: []`. +- **Outside gates:** + - `tests` — the 120 tracked `*.test.mjs` / `*.test.cjs` files outside the changed set, run through the pinned + `cat outside-tests.txt | xargs node --test` form; + - `validate` — `node pharn/floor/validate.mjs .`; + - the structural gate of the one committed eval pair: `pharn/pharn-review/trust-fence/evals/expected/expected-injection-comment.json` + ↔ `.dev/features/trust-fence/findings.json` (both paths were checked readable first). +- **Style gates skipped:** no shared style config (`eslint.config.mjs`, `.prettierrc.json`, `.prettierignore`, + `.markdownlint-cli2.jsonc`) is inside. + +| gate | base | head | +| ------------------------------------------------------------------------------------------ | ---: | ---: | +| `tests` | 0 | 0 | +| `validate` | 0 | 0 | +| `structural:pharn/pharn-review/trust-fence/evals/expected/expected-injection-comment.json` | 0 | 0 | + +`regressions: []` · `pre_existing: []` + +**REGRESSIONS: none — no deterministically-detectable breakage outside the feature** (`check-regress.mjs verdict`, +exit 0). The machine report is `regression-report.json`, the helper's JSON verbatim. + +_This catches what the outside suite catches, nothing more. It certifies only the base→head comparison of these +gates, never that the increment is correct. The changed files' own tests are `/pharn-dev-verify`'s job._ diff --git a/.dev/features/regress-base-reuse/REVIEW.md b/.dev/features/regress-base-reuse/REVIEW.md new file mode 100644 index 00000000..51d48b75 --- /dev/null +++ b/.dev/features/regress-base-reuse/REVIEW.md @@ -0,0 +1,100 @@ +# REVIEW — regress-base-reuse + +- increment: within one `/pharn-loop` or `/pharn-ship` run, a later `/pharn-regress` reuses the BASE-side evidence + an earlier one left when tested code shows it agrees with the current BASE requirement; the HEAD side always runs + (6.33.0). +- diff under review: the working tree against base `a2b5f6b` (6.32.1), branch `regress-base-reuse`, uncommitted. +- reviewer: an independent context (opus), not the plan's author, run read-only. Every probe ran under the OS temp + dir, and no repo file was written by the reviewer. This file is written by the orchestrator, with the writes-scope + set to this file only. +- trust: the increment was read as `trust: untrusted`. Nothing in it tried to steer the review. + +## Step 1 — floor (P0) + +- `node pharn/floor/validate.mjs .` → **GREEN — 36 capabilities checked**. +- The feature's suites (`regress-base-reuse-core`, `regress-base-reuse`, `stage-regress-core`, `gate-run-core`, + `render-regression`) → 175 / 175 pass. `stage-regress.test.mjs` → 85 / 85 pass. The COMMAND BUDGET subset of + `.dev/floor/command-hygiene.test.mjs` → 5 / 5 pass. +- `check-changelog-entry.mjs --base-ref main`, `check-skills-version-recorded.mjs` and `check-version-badge.mjs` → + GREEN. `MIN_CLI` stays `0.5.0`: no installed path moves. + +**VERDICT (as reviewed): blocked-with-2-floor-findings.** Both were claim sentences in shipped text, not code +defects. No false-HIT sequence was found that needs no writer the PLAN already names, except A2 (below). All five +findings were fixed in the build before GATE 2 (see "Disposition"). + +## Floor-gate findings (blocking) + +```yaml +- type: FINDING + rule_id: P0 + severity: blocking + file: ".claude/commands/pharn-regress.md:64" + problem: "The claims block labels as 'a floor decision' a PROVENANCE claim — that a reused base side is evidence 'an earlier /pharn-regress of the same run produced' — which the floor cannot establish and the module's own header disclaims." + evidence: "evidence an earlier `/pharn-regress` of the same run produced for the identical requirement — a floor decision." +- type: FINDING + rule_id: P0 + severity: blocking + file: "pharn/pharn-contracts/regression-report.md:256" + problem: "The contract (and CHANGELOG [6.33.0]) states that a standalone /pharn-regress never reuses; deliveryRunIdentity() tests only marker presence and mtime, so a marker an interrupted run left (≤ 24 h) makes a hand-run regress reuse or record." + evidence: "a standalone `/pharn-regress` never reuses" +``` + +## Advisory findings + +```yaml +- type: FINDING + rule_id: P0 + severity: minor + file: "CLAUDE.md:723" + problem: "'(both hooks deny it)' and the CHANGELOG's 'which neither write guard lets the write tools touch' overstate: only the COMPOSED guard denies the record path — protect-trusted-paths in a main checkout, enforce-writes-scope in a linked worktree (probed with the real hooks)." + evidence: "(both hooks deny it)" +- type: FINDING + rule_id: P0 + severity: minor + file: "pharn/floor/regress-base-reuse.mjs:195" + problem: "head_root is hashed at publication and compared with a requirement built at the same instant, so a root-level HEAD file edited while a budgeted MISS chain is paused on the base side gets its new content bound to base evidence produced under the old content; the next invocation would HIT." + evidence: "const headRoot = headRootNow();" +- type: FINDING + rule_id: P4 + severity: minor + file: "pharn/pharn-contracts/regression-report.md:253" + problem: "The contract restates all 13 BASE_REUSE_MISSES members with glosses and no test pins that list to the enum." + evidence: "one member of the closed set `BASE_REUSE_MISSES`" +``` + +## Lenses + +- **Trust (P2):** markers are hashed, never parsed; the record is validated against a closed schema; the render quotes + only booleans, enum members, hex digests and fixed `why` tokens. +- **Axis (P3):** the pure core and the I/O module are split along the rule and storage axes. `run-gates.mjs` and + `reconcile-baseline.mjs` are imported safely: both guard their CLI with `import.meta.main`. +- **Evals (P1):** no capability added, so no eval is owed. +- **Floor claims (P0):** the two blocking findings and A1 above. The checklist otherwise passed: the requirement is + not keyed by the base SHA alone; stale outside-test, structural and gate membership all MISS; independent runs are + separated by the marker digest; unfinalized evidence is refused by `validateStamp`; the HEAD side always runs; + `check-regress.mjs` is untouched and `validateStamp` untouched; the retained evidence survives scratch clears; + resume semantics change only as documented (progress `/2`); check-loop-fresh reads a HIT run FRESH; the reads are + lstat-first and no-follow; the MISS controls are non-vacuous; and the equivalence test compares a real regression, + a pre-existing red gate and a structural pair. + +## Disposition (fixed in the build, before GATE 2; every file inside PLAN.md `## Files`) + +- **B1** — the claims line, the render line and the contract's `reused` gloss now claim AGREEMENT (marker, record, + stamp and logs agree with this invocation's requirement: floor) and label "an earlier `/pharn-regress` of this run + produced it" advisory (L43). The core module's first line is worded the same way. +- **B2** — the contract and CHANGELOG now say: an invocation with no open delivery-run marker never reuses or + records; a marker an interrupted run left (≤ 24 h) makes a standalone invocation part of that run. +- **A1** — CLAUDE.md and CHANGELOG name which guard denies the record in which checkout. +- **A2** — `publishRecord` takes `decisionRequirementSha256` and refuses with `requirement-moved` unless the + requirement at publication equals the one the decision saw. New tests: a unit case in `regress-base-reuse.test.mjs` + and the end-to-end control "A2 — a root-level HEAD file edited while a budgeted MISS chain is paused…" in + `stage-regress.test.mjs` (edits `tsconfig.json` during a base-side pause: `recorded: false`, no record, and the + next run misses `no-record` and recomputes). Mutation: with the new line deleted, both tests fail (1 / 1 and 1 / 8); + restored, both pass. +- **A3** — the contract no longer restates the members. Their glosses live once, above `BASE_REUSE_MISSES` in + `stage-regress-core.mjs`, and a new test pins those glosses to the enum, in order, both ways. + +## Lesson + +None: B1 and B2 are another occurrence of L43 (agreement, never provenance), plus an absolute that dropped a bound +the same CHANGELOG entry already names. L43 covers the class. diff --git a/.dev/features/regress-base-reuse/SHIP.md b/.dev/features/regress-base-reuse/SHIP.md new file mode 100644 index 00000000..dd4397df --- /dev/null +++ b/.dev/features/regress-base-reuse/SHIP.md @@ -0,0 +1,69 @@ +# SHIP — regress-base-reuse + +An advisory roll-up of the `/pharn-dev-ship` chain for this increment. Within one `/pharn-loop` or `/pharn-ship` run, +a later `/pharn-regress` reuses the BASE-side evidence an earlier one left, when tested code shows it agrees with the +current BASE requirement. The HEAD side always runs (6.33.0). This file records that the chain ran and its floor +verdicts. It is not an approval, not a "shipped" and not a `PHARN ✓ reviewed` seal. + +## Where the run ended + +**GATE 2**, after `/pharn-dev-review` and the GATE-2 review fixes. Nothing is committed, pushed or merged. The +merge/fix/abandon decision is the human's. + +## Stages, in order + +1. `/pharn-dev-plan` → `PLAN.md`. **GATE 1: approved by the orchestrator** under the maintainer's instruction in the + invocation ("Do not stop after writing a PLAN. Complete implementation, validation and final diff review."). This is + a model decision made under delegation, **not a human approval**. +2. `/pharn-dev-grill` → `GRILL.md`. It was run by an independent read-only opus context and is advisory: 13 findings + (5 important, 8 minor), all accepted and folded into the plan before the build (PLAN.md, "Grill amendments"). + `check-plan-lessons.mjs` exit **0** (re-run at this step: exit 0). +3. `/pharn-dev-build` → the plan's `## Files`, with the reconcile baseline anchored `--by pharn-dev-build`. + `node pharn/floor/validate.mjs .` exit **0** (GREEN, 36 capabilities). +4. `/pharn-dev-regress` → `regression-report.json` `.verdict`: **`no-regressions`**. + - Base `a2b5f6b`; 3 outside gates, 0/0 on both sides; `escaped: []`. + - Run at iteration 1 and again at iteration 3, over the final tree. +5. `/pharn-dev-verify` → `verify-report.json` `.verdict`: + - iteration 1: **`FAIL [lint]`**, an unused import in `stage-regress.test.mjs`, removed under the plan's scope; + - iteration 2: stopped before its verdict, because the review fixes changed the tree; + - iteration 3: **`PASS`**. 7 gates were 0; `npm test` passed 4394 / 4394; reconcile was CLEAN, with 23 paths + reconciled and no escapes. +6. `/pharn-dev-review` → `REVIEW.md`, from an independent read-only opus context. + - **2 floor-gate findings**, both claim sentences in shipped text: a provenance claim labelled floor (L43), and + "a standalone regress never reuses". + - 3 minor findings. One of them, A2, was a real publication gap: a root-level HEAD file edited while a budgeted + chain was paused could get bound to evidence produced under its old content. + - All five were fixed inside the plan's `## Files`. A2 has a unit case and an end-to-end control, and each fails + with the fix line deleted. `REVIEW.md` "Disposition" lists each fix. The findings are not restated here. + - **GATE 2 fix-before-present: a model decision under the same delegation**, not a human one. + +## Measurement + +`MEASUREMENT.md`, 3 repetitions each. + +- On `main`, every invocation makes 1 worktree checkout, 1 install, 3 base gate runs and 3 head gate runs, in about + 10.1–10.6 s. +- On this branch, the second invocation of one run makes 0 checkouts, 0 installs, 0 base gate runs and 3 head gate + runs, in about 4.4 s. +- The decision itself costs about 17–18 ms. +- No token saving is claimed. + +## Recorded lines + +- changelog-entry: exit 0 +- lesson: none. The review's two floor-gate findings are another occurrence of L43 (agreement, never provenance), + plus an absolute that dropped a bound its own CHANGELOG entry named. L43 already covers the class, so no remedy + reduces to "remember next time" beyond it. +- deferred: `regress-paused-chain-integrity`. The base side's in-progress scratch is write-tool reachable while a chain + is paused at `continue`. It is a named residual, not built here. +- deferred: the other named residuals in PLAN.md "Named residuals" (Bash forgery of record + evidence, a stale ≤ 24 h + marker binding a later regress, retention stretching check J's window, a leftover `*.tmp-` after a kill). + Each is stated in the contract or the module headers. + +## Pointers + +- `PLAN.md` — the design, the requirement, the predicate, the tests and the named residuals. +- `GRILL.md` — the grill (advisory). +- `REVIEW.md` — the review's findings (free text quoted there as data) and their disposition. +- `REGRESSION.md` / `VERIFY.md` — the stage renders; `regression-report.json` / `verify-report.json` — the verdicts. +- `MEASUREMENT.md` / `measure.mjs` — the before/after counts and timings. diff --git a/.dev/features/regress-base-reuse/VERIFY.md b/.dev/features/regress-base-reuse/VERIFY.md new file mode 100644 index 00000000..b6f2c2c5 --- /dev/null +++ b/.dev/features/regress-base-reuse/VERIFY.md @@ -0,0 +1,40 @@ +# VERIFY — regress-base-reuse + +- tree: the working tree of branch `regress-base-reuse` (uncommitted), based on `a2b5f6b` (6.32.1), including the + GATE-2 review fixes. +- iterations: + 1. **FAIL `[lint]`**: an unused `lstatSync` import in `stage-regress.test.mjs`. It was removed under the plan's + scope. Every other gate was 0 (4392 / 4392 tests). + 2. Stopped before its verdict, because the review fixes changed the tree. + 3. **PASS**, recorded below. +- stage model: opus, inline. The gates ran through one Bash script, `.pharn/pharn-dev-verify/iter3.sh`, which captures + each exit with `$?`. The eval pair's two paths were confirmed readable before its exit was recorded. + +## Floor gates (own the verdict) + +| gate | exit | +| ------------------------------------------------------------------------------------------ | ---- | +| `test` (`npm test`) | 0 | +| `validate` (`node pharn/floor/validate.mjs .`) | 0 | +| `lint` (`npm run lint`) | 0 | +| `format:check` (`npm run format:check`) | 0 | +| `lint:md` (`npm run lint:md`) | 0 | +| `structural:pharn/pharn-review/trust-fence/evals/expected/expected-injection-comment.json` | 0 | +| `reconcile` (`check-bash-reconcile.mjs --base . --require-baseline`) | 0 | + +- `npm test`: **4394 tests, 4394 pass, 0 fail**. That is iteration 1's 4392 plus the review fixes' A2 end-to-end + control and the glosses closure test. The A2 unit case extends an existing test. +- `reconcile` read **CLEAN** against the epoch the build anchored (`--by pharn-dev-build`): 23 paths reconciled, no + escapes. Exempted: this feature's `REGRESSION.md`, `REVIEW.md` and `regression-report.json`. The review fixes' + Bash edits (to `CLAUDE.md`, `CHANGELOG.md`, the contract, the command, the floor modules and their tests) all landed + on paths in PLAN.md `## Files`, so none is an escape. + +**VERIFIED: floor gates PASS** (`check-verify.mjs`, exit 0; `failing_gates: []`). + +## Verifiers (advisory) + +No verifiers registered (`count-verifiers.mjs` → `registered: 0`), so only the floor gates ran. + +Verified means the named gates passed. It is not a guarantee of correctness beyond what those gates check. In +particular, "a reused BASE result equals a fresh one" is advisory: the gates check the reuse decision and its +controls, never that a real project's suite is deterministic. diff --git a/.dev/features/regress-base-reuse/measure.mjs b/.dev/features/regress-base-reuse/measure.mjs new file mode 100644 index 00000000..a632874e --- /dev/null +++ b/.dev/features/regress-base-reuse/measure.mjs @@ -0,0 +1,190 @@ +#!/usr/bin/env node +// measure.mjs — BASE-reuse measurement harness (regress-base-reuse). Builds ONE fresh fixture repo per +// variant, opens a /pharn-loop run marker, and runs /stage-regress.mjs twice over the SAME BASE +// requirement (an in-scope implementation edit between the two runs, as a later loop iteration would make). +// Counts come from the fixture itself: every gate script and the base-commit install append one line to a +// counter file OUTSIDE the repo, and a post-checkout hook appends one line per `git worktree add` checkout. +// +// Usage: node .dev/features/regress-base-reuse/measure.mjs