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
|
|
@ -87,37 +87,32 @@ AskUserQuestion:
|
|||
|
||||
### Step 3: Create backup
|
||||
|
||||
Create backup silently, and **print the backup ID** — Step 6 has to tell the user
|
||||
how to roll back, and a timestamp that only ever existed inside a command
|
||||
substitution cannot be quoted later. Shell state does not survive to the next
|
||||
block, so capture the printed value and substitute it literally from here on:
|
||||
Create the backup through the code that owns the format. Pass one `--target` per
|
||||
pre-existing file the plan will MODIFY, and one `--created` per file the plan
|
||||
will CREATE — a backup cannot hold a file that does not exist yet, so those are
|
||||
recorded rather than copied, and `/config-audit rollback` reads them back to tell
|
||||
the user which files it is leaving behind.
|
||||
|
||||
```bash
|
||||
BACKUP_ID=$(date +%Y%m%d_%H%M%S)
|
||||
mkdir -p ~/.claude/config-audit/backups/"$BACKUP_ID"/files/ 2>/dev/null
|
||||
echo "$BACKUP_ID"
|
||||
node ${CLAUDE_PLUGIN_ROOT}/scanners/rollback-cli.mjs --create --target "<file-1>" --target "<file-2>" --created "<new-file-1>" --repo "$PWD" --output-file /tmp/config-audit-implement-backup.json 2>/dev/null; echo $?
|
||||
```
|
||||
|
||||
Use the printed ID wherever `{backup-id}` appears below. Never invent or re-derive
|
||||
it with a second `date` call — a run that straddles a second boundary would hand
|
||||
the user a rollback ID that does not exist.
|
||||
| Exit | Meaning |
|
||||
|------|---------|
|
||||
| 0 | every target was backed up |
|
||||
| 1 | at least one target was not there — read `skipped[]` before continuing |
|
||||
| 3 | the CLI could not run (show the stderr message); do not edit anything |
|
||||
|
||||
Copy each file to be modified. Generate `manifest.yaml` with checksums.
|
||||
Read `/tmp/config-audit-implement-backup.json`. The payload's `backupId` is the
|
||||
ID to quote from here on — **never re-derive it**. Shell state does not survive
|
||||
to the next block, and a second `date` call that straddles a second boundary
|
||||
would hand the user a rollback ID that does not exist. Use it wherever
|
||||
`{backup-id}` appears below.
|
||||
|
||||
The manifest is what `/config-audit rollback` reads, so it MUST carry both lists:
|
||||
|
||||
```yaml
|
||||
files: # pre-existing files this run will MODIFY
|
||||
- backup: files/root/CLAUDE.md
|
||||
original: /abs/path/CLAUDE.md
|
||||
sha256: <sha256 of the pre-change content>
|
||||
created: # files this run will CREATE (no backup can exist)
|
||||
- /abs/path/.claude/rules/post-quality.md
|
||||
```
|
||||
|
||||
Record every `create`-type action under `created:`. Rollback cannot restore a
|
||||
file that never existed, but it must be able to tell the user which files it is
|
||||
leaving behind — a half-restored target is only dangerous when it is silent.
|
||||
On exit 1, name the `skipped[]` paths before doing anything else: the plan is
|
||||
about to change files that have no backup behind them. If any skipped path is one
|
||||
the plan MODIFIES (rather than creates), stop and report — that action cannot be
|
||||
rolled back.
|
||||
|
||||
Tell the user: **"Backup created. Implementing actions..."**
|
||||
|
||||
|
|
@ -222,8 +217,10 @@ A full-file Write that names only two of the four silently deletes the other two
|
|||
## Rollback
|
||||
|
||||
If the user requests rollback at any point:
|
||||
1. Read `manifest.yaml` from backup
|
||||
2. Restore each file and verify checksums
|
||||
1. Run `/config-audit rollback {backup-id}` — it drives `scanners/rollback-cli.mjs`,
|
||||
which verifies each checksum before and after writing. Do not restore by hand:
|
||||
a copy performs neither check, and the result cannot be reported as verified.
|
||||
2. Read the restore payload and report the per-file `status` it returns
|
||||
3. **Report — do not delete — the files this run created.** Rollback restores from
|
||||
backup, and no backup can exist for a file that did not exist before. Those
|
||||
paths stay on disk; `/config-audit rollback` lists them under "Left in place"
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue