feat(scanners): the subtraction axis can now remove what it proposes (SUB-WRITE)

`optimize --subtract` has only ever proposed. `--apply` executes the blocks the
operator picks, behind a backup whose coverage is verified and a scope gate the
engine enforces rather than describes.

The open design decision from plan §C6 was settled by two measurements, not by
taste. It is NOT a fix-engine action: the subtraction axis appears nowhere in
scan-orchestrator or optimization-lens-scanner, so verifyFixes' re-scan would
mark every removal `verified` whether or not it happened -- a success-shaped
no-op, the same shape that made restoreBackup silently do nothing. It is NOT a
plan/implement step either: that pipeline needs a finding code, and OPT declares
exactly one, for the deterministic check.

The approval artifact is written by main context, not by the lens agent. That is
where the operator's decision actually happens, and it keeps the feature off the
still-unmeasured agent write surface (M-BUG-18 lists optimize as open).

Three properties are load-bearing, and each was seen red against its own defect:
removals validate against the ORIGINAL content and apply in descending line
order; 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 manifest coverage is asserted before a byte changes.

Two guards were green on their own defect and were fixed after measuring:
`/\b80\s*%\b/` never matches "80% of the file" -- `%` is a non-word character, so
the trailing `\b` demands a word character next. And the caller-arm sweep passed
vacuously against HEAD, iterating an empty list; only the added non-emptiness
assertion caught it.

The floor is repeated, 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. `mv` to `_archive/` is a file-level rule and
does not apply to a block excision -- the timestamped backup is the recovery
artifact, and a second copy with no restorer would be worse than none.

strongestGate moves into write-scope.mjs so the gate ordering has one owner.

Dogfooded DRY-RUN against the real ~/.claude/CLAUDE.md: 29 candidates, gate
refused all 29 with exit 0 until the scope was approved, then 29/29 spans
validated with nothing written. ~789 tokens, ~18% of the file -- corroborating
the #40 fasit's ~850, and well short of what a deletion feature is tempted to
promise.

Suite 1625 -> 1659/0. Frozen v5.0.0 and default-output baselines untouched.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017A6vrtPKsVuM4DJ27p7jzw
This commit is contained in:
Kjell Tore Guttormsen 2026-08-10 06:10:42 +02:00
commit 000e47f9d2
11 changed files with 1103 additions and 26 deletions

View file

@ -264,7 +264,8 @@ Your team configuration changes over time. Track it:
| `/config-audit manifest` | Ranked table of every token source (CLAUDE.md, rules, agents, skills, output styles, MCP, hooks) sorted by estimated tokens — each tagged with its **load pattern** (always-loaded / on-demand / external) plus an **always-loaded subtotal** ("≈X tokens enter context every turn before you type"). Component-level: no coarse plugin roll-up (it would double-count) |
| `/config-audit feature-gap` | Context-aware feature recommendations grouped by impact |
| `/config-audit optimize` | Optimization lens (mechanism-fit): config that works but fits a better mechanism — procedure→skill, lifecycle→hook, unscoped path→rule, "never"→permission. Hybrid motor (deterministic pre-filter + opus precision gate), every finding cites a best-practices-register rule |
| `/config-audit optimize --subtract` | **Subtraction lens** — the inverse question no other command asks: what no longer earns its always-loaded rent? Ranks CLAUDE.md blocks that correct general model *behaviour* rather than stating a local fact, split into **dead** (never missed) and **earned** (returns if the model stumbles), with the token payoff (`BP-SUB-001`). **Load-bearing local facts are excluded deterministically before the judge sees anything** — remotes, versions, paths, filenames, policy invariants and unresolvable entity names are never candidates, and an ordered list is treated as a contract. Opt-in, proposes only, never writes. Pair with `--global` to reach the user-level CLAUDE.md, where the always-loaded cost actually sits |
| `/config-audit optimize --subtract` | **Subtraction lens** — the inverse question no other command asks: what no longer earns its always-loaded rent? Ranks CLAUDE.md blocks that correct general model *behaviour* rather than stating a local fact, split into **dead** (never missed) and **earned** (returns if the model stumbles), with the token payoff (`BP-SUB-001`). **Load-bearing local facts are excluded deterministically before the judge sees anything** — remotes, versions, paths, filenames, policy invariants and unresolvable entity names are never candidates, and an ordered list is treated as a contract. Opt-in and proposes only; add `--apply` to execute the removals you pick. Pair with `--global` to reach the user-level CLAUDE.md, where the always-loaded cost actually sits |
| `/config-audit optimize --subtract --apply` | **Execute approved removals.** You pick which blocks go by number; nothing is inferred. Every removal is checked against the file as it reads *now* — an approval that no longer matches is refused rather than applied to whatever moved into those lines — and the floor is re-asserted at write time, so a load-bearing block cannot be removed even by a hand-built approval. A dry run always precedes the write, the backup's manifest is verified to cover the file being written before a byte changes, and `/config-audit rollback` restores it. A removal targeting your machine-wide `~/.claude/CLAUDE.md` is **refused until you approve that scope explicitly** — it costs, and saves, in every project on every turn |
| `/config-audit fix` | Auto-fix deterministic issues with backup + verification |
| `/config-audit rollback` | Restore configuration from a previous backup |
| `/config-audit plan` | Generate prioritized action plan from audit findings |
@ -606,6 +607,15 @@ classification), or when it names a capitalized entity the mechanism cannot reso
dictionary. That last rule is a deliberate conservative default: it declines to decide and
keeps the block, paying in recall rather than risk.
The write half (`--apply`) keeps the same asymmetry. It is not a `fix` action and not a
`plan`/`implement` step, and both exclusions are measurements rather than preferences: the
subtraction axis never enters the orchestrated envelope, so `fix`'s re-scan verification would
report every removal as verified whether or not it happened — a success-shaped no-op — and the
findings pipeline expects a finding code, which by invariant names a deterministic check, not a
prose judgement. `scanners/lib/subtraction-write.mjs` owns the execution instead, and it
re-asserts the floor rather than trusting that the pre-filter already did: the veto stays where
it is, and is simply repeated as the last red line before the delete.
Granularity is the **leaf block** — one list item including its wrapped continuation lines, or
one paragraph — with two structural exceptions: a paragraph ending in `:` merges with the list
it introduces, and an *ordered* list is treated as a contract whose steps inherit floor from