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
29
CLAUDE.md
29
CLAUDE.md
|
|
@ -197,6 +197,35 @@ per-candidate `modelScope` appear **only** when the flag is passed, so a plain
|
|||
`--subtract` run is byte-identical to the pre-flag payload (asserted on the
|
||||
serialized bytes, since a key set to `undefined` passes a shallow key check).
|
||||
|
||||
**Backup/restore is one code path (invariant).** `scanners/rollback-cli.mjs` is the only entry to
|
||||
the backup engine, and BOTH pipelines use it: `fix` backs up through `createBackup` directly,
|
||||
`implement` Step 3 through `--create`. Before R1/R2 the two halves each hid the other's failure.
|
||||
The engine verified every checksum before and after each write, but had no `process.argv` (16 CLIs
|
||||
under `scanners/` had one, it did not), so `commands/rollback.md` restored as model prose — an ESM
|
||||
`import` block a template cannot execute, `cp` offered underneath as the runnable path, and
|
||||
"(checksum verified)" pre-rendered in the success output. `cp` establishes no checksum, so the
|
||||
verification was a property of the template. Meanwhile `implement` hand-built its manifest in the
|
||||
template while `parseManifest` knew one frozen sample of that format, pinned by a HAND-WRITTEN
|
||||
fixture rather than by the template's own text — the #63 shape on the data side, where a renamed
|
||||
key yields zero parsed files and a rollback that reports success having restored nothing. Four
|
||||
properties are load-bearing. (1) **Neither half fixes alone**: a CLI over a prose format still
|
||||
parses prose; a clean format with no runnable entry still cannot restore. (2) **A gated restore is
|
||||
exit 1, not 3** — "this write leaves your project" is a verdict about a write that WAS examined and
|
||||
rides in the payload, where a command running under `2>/dev/null` can act on it (F3's class); 3
|
||||
stays reserved for argv errors and a backup id that resolves in neither root. (3) **`--created`
|
||||
records, it does not copy** — no backup can hold a file that does not exist yet, so those paths go
|
||||
into the manifest for `rollback` to report as left in place; `serializeManifest` emits the bare
|
||||
`created:` key, which is why the pre-R2 `created: <timestamp>` (a VALUE, meaning the backup id)
|
||||
never collides with it. (4) **`parseManifest`'s implement-format branch stays** even though nothing
|
||||
writes that shape now — backups already on disk in it must remain restorable, the same reason
|
||||
`getLegacyBackupDir()` is still read; its fixture changed meaning from "a stand-in for the
|
||||
template's text" to "a golden sample of historical bytes". The output contract is guarded by
|
||||
running the CLI: `tests/commands/backup-restore-contract.test.mjs` checks every field
|
||||
`rollback.md` renders against a real payload, so a renamed key fails there instead of becoming a
|
||||
confident sentence in front of a user who is already in trouble. Distinct from the scope gate,
|
||||
which classifies *where* a restore lands — `rollback.md` still calls `write-scope-cli` before its
|
||||
approval surface, and classifying is still not approving.
|
||||
|
||||
**Subtraction write path (invariant).** `--apply` routes through `scanners/lib/subtraction-write.mjs`, never through `fix-engine` or the `plan`/`implement` pipeline, and both exclusions are **measured**: the subtraction axis is absent from the orchestrated envelope, so `verifyFixes`' re-scan would mark every removal `verified` whether or not it happened (a success-shaped no-op), and the findings pipeline needs a finding code — which names a deterministic check, not a prose judgement. Three properties are load-bearing and each has a guard seen red against its own defect: removals are validated against the ORIGINAL content and applied in **descending** line order (an ascending pass shifts later spans out from under themselves); the **range** check is not redundant with the text check (`line: 0` makes `slice(-1, 0)` empty, so an empty `text` matches and `splice(-1, 1)` deletes the file's LAST line); and `createBackup` skips a nonexistent path while still returning an id, so coverage of every file about to be written is **asserted from the manifest** before a byte changes. The floor is *repeated* here, not moved: `floor-exclusion` still vetoes before anything is proposed, and the engine refuses a load-bearing block again so a hand-built approval cannot route around it. The archive rule (`mv` to `_archive/`) is file-level and does not apply to a block excision — the timestamped backup is the recovery artifact, and inventing a second copy with no restorer behind it would be worse than none.
|
||||
|
||||
## Testing
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue