config-audit/scanners/lib
Kjell Tore Guttormsen 749b710de7 feat(scanners): the write gate now runs in code, not in the templates' prose
`write-scope.mjs` has existed since M-BUG-41, but only one writer ever called
it. Measured 2026-08-12: 9 files under `scanners/` write to disk, 1 imported
the gate; 21 command templates, 17 mention a write, 5 call `write-scope-cli`.
Five templates paraphrasing one policy is the shape that put the lever table in
five copies (#61) — one level up.

The defect was never "8 ungated writers = 8 bugs". Four of them write the
plugin's own bookkeeping and must STAY ungated: a gate that fires on every run
gets switched off, and then it guards nothing. The defect is that nothing
declared WHICH, so the question was answered by reading, and answered
differently each time it was asked.

`tests/lib/write-gate-coverage.test.mjs` makes the answer structural: every
writer either imports the gate or holds an EXEMPT entry naming where the bytes
land. Seen RED against today's tree before the fix (4 ungated writers), and
each of its four assertions was separately seen red against its own defect.

Two premises in the plan text were falsified by measuring them first:

  - `scan-orchestrator` was carried as "plugin-managed, legitimately exempt".
    `--save-baseline` derives its path from the SCAN TARGET, so `--global`
    lands `~/.claude/.config-audit-baseline.json` — user-scope, require-ok. It
    is gated. `lib/baseline.mjs` is the genuinely exempt one.
  - the first sweep scored 9 writers with a regex that could not match
    `writeFileSync(`, so `lib/backup.mjs` — a real writer — read as clean. The
    guard covers sync and async forms, strips comments before matching, and
    asserts non-emptiness so a regex that stops matching cannot make every
    other assertion vacuously green (#63, #64).

Gated: fix-engine, rollback-engine, campaign-export-cli, scan-orchestrator.
All five call sites share ONE reduction, `evaluateWriteTargets` — four copies
of classify/strongestGate/dedup is the drift this exists to prevent.

`campaign export` still DISCLOSES rather than refuses: cross-repo is by design
there, and tightening it into a refusal would break the feature. A dry run is
still not a write, so it is never gated (#63). A refusal is a verdict about a
config that WAS examined, so it rides in the payload and keeps the 0/1/2 exit
contract (#62) — and the verdict now reaches the success payload too, since
stderr is discarded by `2>/dev/null` (F3's class).

commands/fix.md carries `--approve-scope` from the answer the user gives, with
the rule stated where it can be read: classifying is not approving.

Dogfooded end to end: a target outside the session root refuses with zero bytes
written, then applies under `--approve-scope`.

Suite 1703 -> 1707/0. Frozen v5.0.0 + default-output snapshots: 0 changed files.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pkn22uGCgk6QZA738zNmHL
2026-08-12 21:15:16 +02:00
..
active-config-reader.mjs feat(scanners): model/effort routing becomes a lever, not a 25th dimension (C4) 2026-08-10 05:07:23 +02:00
active-model.mjs feat(skl,cml): --context-window auto model→window probe (v5.12 B8b) [skip-docs] 2026-06-23 22:29:12 +02:00
agent-listing-budget.mjs feat(scanner): add AGT per-agent description bloat advisory (v5.9 B1-rest) 2026-06-23 16:27:24 +02:00
backup.mjs fix(rollback): restore the backup path contract the engine and the commands disagreed on 2026-07-31 17:23:30 +02:00
baseline.mjs feat(ultraplan-local): v1.6.0 — /ultraresearch-local deep research command 2026-04-08 08:58:35 +02:00
best-practices-register.mjs feat(knowledge): best-practices register foundation — v5.7 Fase 1 Chunk 1 2026-06-20 22:37:47 +02:00
campaign-export.mjs feat(campaign): plan export + execution-by-reuse (v5.7 Fase 2 Block 4c) 2026-06-23 10:08:04 +02:00
campaign-ledger.mjs feat(campaign): machine-wide always-loaded token roll-up — pure layer (v5.9 B2a) 2026-06-23 16:57:31 +02:00
cli-args.mjs fix(scanners): close the CLI argument class across all fourteen CLIs 2026-08-09 21:42:00 +02:00
context-window.mjs feat(skl,cml): --context-window auto model→window probe (v5.12 B8b) [skip-docs] 2026-06-23 22:29:12 +02:00
diff-engine.mjs feat(ultraplan-local): v1.6.0 — /ultraresearch-local deep research command 2026-04-08 08:58:35 +02:00
file-discovery.mjs fix(acr): file-discovery skips backups/ dirs — never live config (M-BUG-8) 2026-06-26 12:23:43 +02:00
finding-codes.mjs feat(scanners): a path written in prose is now resolved, not assumed (C3) 2026-08-12 20:11:12 +02:00
floor-exclusion.mjs feat(optimize): add --subtract, the subtraction axis, behind a deterministic floor 2026-07-31 16:24:51 +02:00
hook-additional-context.mjs feat(hooks): additionalContext injection advisory + filter-before lever (v5.10 B5) [skip-docs] 2026-06-23 20:28:33 +02:00
humanizer-data.mjs feat(scanners): a path written in prose is now resolved, not assumed (C3) 2026-08-12 20:11:12 +02:00
humanizer.mjs fix(acr): AGT findings humanize to "Wasted tokens" not "Other" (M-BUG-17) 2026-06-30 13:51:55 +02:00
knowledge-refresh.mjs fix(knowledge): freshness can no longer be green on outdated evidence 2026-08-03 11:36:57 +02:00
lens-prefilter.mjs feat(opt): optimization lens Chunk 2b — opus prose-judgment analyzer + /config-audit optimize 2026-06-21 14:01:18 +02:00
mcp-deferral.mjs feat(tokens): MCP tool-schema deferral check + CLI-over-MCP lever (v5.10 B4) 2026-06-23 19:59:14 +02:00
output.mjs fix(scanners)!: a finding ID names the check, not the emission (M-BUG-28) 2026-08-09 23:26:36 +02:00
permission-rules.mjs feat(dis): flag forbidden-param permission rules CC silently ignores 2026-06-19 14:04:25 +02:00
report-generator.mjs feat(ultraplan-local): v1.6.0 — /ultraresearch-local deep research command 2026-04-08 08:58:35 +02:00
require-target-dir.mjs fix(scanners): a target path that does not exist is an error, not a grade 2026-08-09 21:18:18 +02:00
scoring.mjs fix(scanners): retire the autoMode GAP dimension, a /doctor duplicate (D1) 2026-08-09 22:43:04 +02:00
severity.mjs feat(config-audit): export WEIGHTS from severity.mjs (v5 F3 prep) 2026-05-01 06:16:28 +02:00
skill-listing-budget.mjs fix(scanners): retire the autoMode GAP dimension, a /doctor duplicate (D1) 2026-08-09 22:43:04 +02:00
string-utils.mjs fix(acr): SET typo-gates unknown-key false positives (M-BUG-10) 2026-06-26 15:15:14 +02:00
subtraction-prefilter.mjs feat(optimize): add --subtract, the subtraction axis, behind a deterministic floor 2026-07-31 16:24:51 +02:00
subtraction-write.mjs fix(commands): the removal gate was classified against the wrong root 2026-08-10 06:15:25 +02:00
suppression.mjs fix(scanners)!: a finding ID names the check, not the emission (M-BUG-28) 2026-08-09 23:26:36 +02:00
tokenizer-api.mjs feat(config-audit): --accurate-tokens API calibration (v5 N5) [skip-docs] 2026-05-01 09:15:02 +02:00
write-output.mjs fix(commands): stop answering questions the caller did not ask 2026-08-01 21:26:39 +02:00
write-scope.mjs feat(scanners): the write gate now runs in code, not in the templates' prose 2026-08-12 21:15:16 +02:00
yaml-parser.mjs feat(acr,yaml): v5.6 Foundation — load-pattern enumeration + block-seq parser 2026-06-20 16:57:52 +02:00