feat(scanners): the recovery path is code you can run, not prose you can read
R1+R2 as one chunk — both KRITISK rows of the Q3 severity table sit on the restore path, and neither closes alone. R1: rollback-engine.mjs verified every checksum before AND after each write, resolved the legacy backup root and reported createdNotRemoved — and none of it was reachable. Measured: 16 files under scanners/ carry a process.argv entry; the engine was not one of them. commands/rollback.md drove the restore as model prose: an ESM import block a template cannot execute, ad-hoc `cp` offered underneath as the runnable path, and "(checksum verified)" pre-rendered three times in the success output. `cp` establishes no checksum, so the verification was a property of the template rather than of the run — on the one surface that runs when the user is already in trouble. R2: implement.md Step 3 hand-built its backup (mkdir, cp, a date-derived id, a manifest typed out in the template) while parseManifest knew one frozen sample of that format, pinned by a HAND-WRITTEN fixture instead of by the template's own text. Rename a key and parseManifest returns zero files while rollback reports success. Fixing only R1 leaves the new CLI parsing a prose format; fixing only R2 leaves a clean format with no runnable entry. - scanners/rollback-cli.mjs — --list / --create / --restore / --delete over the existing engine, on the shared requireValidArgs gate. Exit 0 done, 1 outstanding (gate refusal with nothing written, or a backup that covered fewer targets than given), 2 a file failed, 3 could not do the job. A gated restore is 1, not 3: "this write leaves your project" is a verdict about a write that WAS examined, and it rides in the payload where a command under 2>/dev/null can act on it. - createBackup gains `created` (recorded, never copied — no backup can hold a file that does not exist) and `skipped`, so a backup covering fewer files than asked is no longer indistinguishable from a clean one. - implement.md Step 3 and rollback.md now call the CLI. parseManifest's implement-format branch stays: nothing writes that shape now, but every backup made before this chunk is on disk in it. - backup-restore-contract.test.mjs checks every field rollback.md renders against a payload produced by RUNNING the CLI. That is what replaced "(checksum verified)". 20 guards seen red against the original state before any production code, then each against its own defect. Two holes that surfaced there were mine: the implement assertion matched `--create` as a substring of `--created` and stayed green when the call was removed; and mutating the argv gate showed requireValidArgs sets exit 3 by itself, so a CLI can report that it could not parse its arguments and still run the restore underneath — that case is now asserted on the bytes. Suite 1752 -> 1777, 0 fail. Frozen tests/snapshots/v5.0.0 untouched. Dogfooded through the templates' own command lines against a sandboxed HOME, including the machine-wide arm: refused with the file unchanged, then restored under --approve-scope. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Logq8GGWKhtyDem63FTEnG
This commit is contained in:
parent
b35ff449e8
commit
44b222859e
13 changed files with 1083 additions and 86 deletions
|
|
@ -60,6 +60,7 @@ everything because they sit on the *recovery* path — the surface that runs
|
|||
exactly when the user is already in trouble, and the least-exercised one.
|
||||
|
||||
### R1 — the restore flow is model-executed prose; the code engine has no CLI entry
|
||||
**CLOSED in #74** — `scanners/rollback-cli.mjs`. The measurement below is kept as written.
|
||||
**Where:** `commands/rollback.md` §Implementation; `scanners/rollback-engine.mjs`.
|
||||
**Measured:** 16 scanner CLIs carry a `process.argv` entry — `rollback-engine.mjs`
|
||||
is not one of them. The template's "Implementation" shows an ESM `import` block a
|
||||
|
|
@ -78,6 +79,7 @@ CLI here, the whole Q2 guard class is structurally blind to this command.
|
|||
then covers it for free. The "(checksum verified)" line becomes payload-driven.
|
||||
|
||||
### R2 — implement's backup manifest is hand-built prose; the parser knows one frozen sample of it
|
||||
**CLOSED in #74** — the real fix, not the minimum: `implement.md` Step 3 calls `rollback-cli.mjs --create`, so the prose format has no author left. The measurement below is kept as written.
|
||||
**Where:** `commands/implement.md` Step 3 (mkdir/cp + hand-written
|
||||
`manifest.yaml` with sha256 lines); `scanners/lib/backup.mjs` `parseManifest`;
|
||||
`tests/scanners/rollback-paths.test.mjs:157-186`.
|
||||
|
|
|
|||
|
|
@ -805,3 +805,71 @@ once the gate made it unreachable, along with the now-redundant `&& args[i + 1]`
|
|||
Exit code is **3** by the exit-code contract: a malformed argument means the scanner never got
|
||||
to do its job, which is categorically different from 0/1/2 — verdicts about a configuration
|
||||
that *was* examined.
|
||||
|
||||
---
|
||||
|
||||
### rollback-cli — the recovery path gets a runnable entry (R1+R2, #74)
|
||||
|
||||
`scanners/rollback-cli.mjs` is the entry to `rollback-engine.mjs` and, via
|
||||
`--create`, to `lib/backup.mjs`. It closes the two KRITISK rows of the Q3
|
||||
severity table as one chunk, because neither half holds alone.
|
||||
|
||||
**R1 — the restore was model prose over a code engine.** Measured at the head of
|
||||
the chunk: 16 files under `scanners/` carried a `process.argv` entry;
|
||||
`rollback-engine.mjs` was not one of them, though it had verified every file's
|
||||
checksum *before and after* each write since M-BUG-22 and reported
|
||||
`createdNotRemoved` since M-BUG-25. `commands/rollback.md` §Implementation
|
||||
showed an ESM `import` block a command template cannot execute and offered
|
||||
ad-hoc `cp` underneath as the runnable alternative, then pre-rendered
|
||||
"`(checksum verified)`" three times in the success output. `cp` establishes no
|
||||
checksum, so the verification was a property of the template, not of the run —
|
||||
on the one surface that runs when the user is already in trouble.
|
||||
|
||||
**R2 — the backup format had two authors, one of them prose.** The fix pipeline
|
||||
backed up through `createBackup`; the implement pipeline hand-built its own —
|
||||
`mkdir`, `cp`, a `date +%Y%m%d_%H%M%S` id, and a manifest typed out in the
|
||||
template. `parseManifest` grew a second branch for that format because the seam
|
||||
had already failed silently once, and the fixture pinning it was **hand-written**
|
||||
rather than derived from the template's own text: the #63 shape on the data
|
||||
side. Rename a key in the template and `parseManifest` returns zero files while
|
||||
`rollback` reports success.
|
||||
|
||||
**Why one chunk.** Fix only R1 and the new CLI still parses a prose format. Fix
|
||||
only R2 and the format is clean with no runnable entry behind it.
|
||||
|
||||
**Exit contract.** 0 done · 1 outstanding (a restore the scope gate will not
|
||||
perform without `--approve-scope`, nothing written; or a backup that covered
|
||||
fewer targets than it was given) · 2 at least one file failed · 3 the CLI could
|
||||
not do its job. A gated restore is **1, not 3**: "this write leaves your project"
|
||||
is a verdict about a write that *was* examined, and it rides in the payload,
|
||||
where a command running under `2>/dev/null` can act on it. That is F3's class,
|
||||
avoided rather than repeated.
|
||||
|
||||
**`--created` records, it does not copy.** No backup can hold a file that does
|
||||
not exist yet. Those paths go into the manifest so `rollback` can list what it is
|
||||
leaving in place. `serializeManifest` emits the bare `created:` key; the pre-R2
|
||||
implement format used `created: <timestamp>` with a VALUE, meaning the backup id,
|
||||
and `parseManifest` tells them apart on exactly that — which is why the two never
|
||||
collide in one file.
|
||||
|
||||
**The implement-format branch stays.** Nothing writes that shape any more, but
|
||||
every backup implement made before this chunk is on disk in it and must remain
|
||||
restorable — the same reasoning that keeps `getLegacyBackupDir()` readable. The
|
||||
hand-written fixture in `tests/scanners/rollback-paths.test.mjs` therefore
|
||||
changed meaning rather than becoming obsolete: it is now a golden sample of
|
||||
historical bytes, which is a legitimate thing to write by hand, instead of a
|
||||
stand-in for a template's own text, which is not.
|
||||
|
||||
**What replaced "(checksum verified)".** `tests/commands/backup-restore-contract.test.mjs`
|
||||
runs the CLI and checks every field `rollback.md` renders against the real
|
||||
payload. A renamed payload key now fails a test instead of turning into a
|
||||
confident sentence. Seen red against its own defect by renaming `{status}`.
|
||||
|
||||
**A guard hole found by mutation.** The first version of the implement-side
|
||||
assertion matched `--create` as a substring, and the same invocation carries
|
||||
`--created` — so replacing the `--create` call with `--list` left the guard
|
||||
green. It matches `--create(?![a-z])` now. Separately, mutating
|
||||
`if (!requireValidArgs(...)) return;` into a bare call showed that
|
||||
`requireValidArgs` sets exit 3 *by itself*: a CLI can report "I could not parse
|
||||
my arguments" and still run the restore underneath. `rollback-cli.test.mjs`
|
||||
asserts on the bytes for that case, not on the exit code.
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue