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:
parent
1543830c52
commit
000e47f9d2
11 changed files with 1103 additions and 26 deletions
|
|
@ -35,7 +35,16 @@ opportunities" — that is a good result, not a failure.
|
|||
|
||||
Split `$ARGUMENTS` into a path (first non-flag argument; default: current working
|
||||
directory) and flags. Recognized flags: `--global` (include the user `~/.claude`
|
||||
cascade in discovery) and `--subtract` (add the subtraction axis, below).
|
||||
cascade in discovery), `--subtract` (add the subtraction axis, below) and
|
||||
`--apply` (execute approved removals — Step 7).
|
||||
|
||||
`--apply` only means anything alongside `--subtract`. If it is present without
|
||||
it, say so and continue with the ordinary lens run:
|
||||
|
||||
```
|
||||
`--apply` executes approved subtraction removals, so it needs `--subtract` too.
|
||||
Running the ordinary lens; re-run with `--subtract --apply` to remove anything.
|
||||
```
|
||||
|
||||
**`--subtract` — the inverse question.** Every other lens asks what to *add* or
|
||||
*move*; this one asks what no longer earns its always-loaded rent. It is opt-in
|
||||
|
|
@ -125,7 +134,72 @@ If the agent kept nothing from the candidates (all dropped) but there were
|
|||
deterministic findings, show those; if it kept nothing at all, show the clean
|
||||
result from Step 3.
|
||||
|
||||
### Step 6: Next steps
|
||||
### Step 7: Apply approved removals (`--subtract --apply` only)
|
||||
|
||||
Skip this step entirely unless BOTH flags are present and the agent kept at
|
||||
least one subtraction finding. Removal is the only thing this plugin does that
|
||||
takes configuration away, so nothing here happens without a named choice.
|
||||
|
||||
**7a — show what is on the table, with honest sizing.** List the kept
|
||||
subtraction findings numbered, each with its file, line span and first line of
|
||||
text. Do not imply a bigger win than there is:
|
||||
|
||||
```
|
||||
Removing all of these saves roughly {n} tokens per turn — on a typical
|
||||
always-loaded CLAUDE.md that is around a fifth of the file, not most of it.
|
||||
```
|
||||
|
||||
Ask which to remove: numbers, `all`, or `none`. `none` ends the command.
|
||||
|
||||
**7b — write the approval file.** With the **Write** tool, write the operator's
|
||||
choice to `~/.claude/config-audit/sessions/{session-id}/subtraction-approved.json`
|
||||
(absolute path — a relative one resolves against the user's CWD). Take `file`,
|
||||
`line`, `endLine` and `signalText` verbatim from the Step 3 payload; `text` must
|
||||
be the `signalText` byte-for-byte, because the engine refuses a removal whose
|
||||
text no longer matches the file:
|
||||
|
||||
```json
|
||||
{ "sessionId": "{session-id}",
|
||||
"removals": [ { "file": "...", "line": 12, "endLine": 15, "text": "..." } ] }
|
||||
```
|
||||
|
||||
**7c — dry run first.** Always. It costs one call and proves the spans still
|
||||
match before anything is written:
|
||||
|
||||
```bash
|
||||
node ${CLAUDE_PLUGIN_ROOT}/scanners/subtraction-write-cli.mjs --approved ~/.claude/config-audit/sessions/{session-id}/subtraction-approved.json --repo "<target-path>" --dry-run --output-file ~/.claude/config-audit/sessions/{session-id}/subtraction-dryrun.json 2>/dev/null; echo $?
|
||||
```
|
||||
|
||||
Read the payload. Exit 3 is a real error (bad or unreadable approval file).
|
||||
Report any `refused` entry with its `reason` before going further —
|
||||
`block-mismatch` means the file changed since the scan (re-run the lens),
|
||||
`floor` means the block is load-bearing and will never be removable.
|
||||
|
||||
**7d — the scope gate.** If the dry-run payload has `requiresApproval: true`,
|
||||
show every line in `disclosures` verbatim and ask for an explicit go-ahead. This
|
||||
is the machine-wide case (`~/.claude/CLAUDE.md`): the change costs — and saves —
|
||||
in every project, on every turn, so it is not the same decision as editing the
|
||||
CLAUDE.md in front of you. Without a clear yes, stop here.
|
||||
|
||||
**7e — apply.** Same command without `--dry-run`, adding `--approve-scope` only
|
||||
if the operator gave that go-ahead in 7d:
|
||||
|
||||
```bash
|
||||
node ${CLAUDE_PLUGIN_ROOT}/scanners/subtraction-write-cli.mjs --approved ~/.claude/config-audit/sessions/{session-id}/subtraction-approved.json --repo "<target-path>" --output-file ~/.claude/config-audit/sessions/{session-id}/subtraction-result.json 2>/dev/null; echo $?
|
||||
```
|
||||
|
||||
**7f — report.** From the result payload, tell the user: what was removed (file
|
||||
+ line span + the text, from `applied`), what was refused and why (`refused`
|
||||
with `reason`), and how to undo it:
|
||||
|
||||
```
|
||||
Backed up as {backupId} — `/config-audit rollback {backupId}` restores every
|
||||
file exactly as it was.
|
||||
```
|
||||
|
||||
Never report a run as successful when `counts.applied` is 0.
|
||||
|
||||
### Step 8: Next steps
|
||||
|
||||
End with context-sensitive next steps, explaining WHY each is useful:
|
||||
|
||||
|
|
@ -139,12 +213,20 @@ End with context-sensitive next steps, explaining WHY each is useful:
|
|||
|
||||
- This command is **agent-driven and not byte-stable** — its output is a
|
||||
human-facing report, deliberately outside the deterministic snapshot suite.
|
||||
- `--subtract` **proposes, never writes.** Nothing is deleted; act on a finding
|
||||
via `/config-audit plan` → `/config-audit implement` (backup + rollback).
|
||||
- `--subtract` **proposes; only `--apply` writes**, and only blocks the operator
|
||||
named. Every removal is preceded by a backup whose manifest is verified to
|
||||
cover the file being written, and `/config-audit rollback` restores it.
|
||||
- **Removal is not a `fix` action and not a `plan`/`implement` step**, by
|
||||
measurement rather than preference: the subtraction axis never enters the
|
||||
orchestrated envelope, so `fix`'s re-scan verification would mark every
|
||||
removal verified whether or not it happened, and the findings pipeline would
|
||||
require a finding code — which names a deterministic check, not a prose
|
||||
judgement. `subtraction-write-cli.mjs` owns the execution instead.
|
||||
- The subtraction floor is deterministic and runs *before* the agent, so a
|
||||
load-bearing block is never a candidate. It errs toward keeping: on a
|
||||
well-maintained config this axis is mostly a no-op, and that is a good result.
|
||||
- The deterministic half (CA-OPT-001) also rides in the normal orchestrated
|
||||
audit; this command adds the prose-judgment half on top.
|
||||
- No files are modified. To act on a finding, use `/config-audit plan` →
|
||||
`/config-audit implement` (backup + rollback) or edit by hand.
|
||||
- Without `--apply`, no files are modified. To act on a mechanism-fit finding,
|
||||
use `/config-audit plan` → `/config-audit implement` (backup + rollback) or
|
||||
edit by hand.
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue