feat(scanners): a path written in prose is now resolved, not assumed (C3)
`import-resolver` follows @import targets; a path written in ordinary prose was checked by nothing. CA-CML-013 resolves those too — one finding per file, severity low, against both the CLAUDE.md's own directory and the scan root, because a nested file may legitimately write repo-root-relative paths. The design work here is the SILENCE list, and every entry on it was measured against 407 real CLAUDE.md files rather than argued for: - Bare filenames excluded: admitting them tripled the output (2350 vs 810), led by name-drops of tools that exist elsewhere on the machine. - Org/repo slugs, npm packages, pytest node ids and prose enumerations excluded: 111 fires, inspected, all false positives. - Bare folder names excluded on the same reasoning one level up: 183 of the remaining 699 fires (26%), led by `open/` — a Forgejo remote namespace prefix, not a directory. This one overturned a premise the fasit had asserted without measuring; the deviation is recorded rather than the prediction quietly edited. - Containment is checked against the scan root, not the file's own dir: a base a `..` chain can escape is not a base. Measured — without it, `../../../../etc/passwd` resolved to the real file and silenced its own finding, while a legitimate `../docs/x.md` still resolves. Rule ORDER is the reported reason (first match wins), so `npm test` is silenced as a command rather than as a bare token, and two silences with different causes keep their own fixtures. Twelve classes, pinned by name. Both load-bearing rules were seen RED against their own defect: deleting containment fails 1 test, deleting the slug rule fails 6. Dogfooded through the argv the command template itself constructs, which found a true positive in our own CLAUDE.md — `lib/humanizer.mjs` where the file is `scanners/lib/humanizer.mjs`. Fixed here. Suite 1662 -> 1701, 0 failing. Frozen v5.0.0 and default-output baselines: 0 changed files. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HJbfM3N8zWQ1wA2voTrZxz
This commit is contained in:
parent
33bfd5ff5b
commit
dbb6a6a3cf
10 changed files with 478 additions and 3 deletions
19
CLAUDE.md
19
CLAUDE.md
|
|
@ -67,7 +67,7 @@ Per-command flags, patterns, and feature lists live in `README.md` and `/config-
|
|||
|
||||
## Plain-Language Output (v5.1.0)
|
||||
|
||||
Default output of all commands routes through `humanizeEnvelope` (`lib/humanizer.mjs`), decorating each finding with `userImpactCategory`, `userActionLanguage`, and `relevanceContext`. `--raw` and `--json` bypass the humanizer for byte-stable v5.0.0 output. Full detail: `docs/humanizer.md`.
|
||||
Default output of all commands routes through `humanizeEnvelope` (`scanners/lib/humanizer.mjs`), decorating each finding with `userImpactCategory`, `userActionLanguage`, and `relevanceContext`. `--raw` and `--json` bypass the humanizer for byte-stable v5.0.0 output. Full detail: `docs/humanizer.md`.
|
||||
|
||||
## Suppressions
|
||||
|
||||
|
|
@ -95,6 +95,23 @@ Coding style: scanners are zero-dependency Node ESM; new findings use the `CA-{S
|
|||
|
||||
**Write-scope gate (invariant).** Every write target is classified by `scanners/lib/write-scope.mjs` before it reaches an approval surface, and the **scope class decides the gate's strength — never the command asking**. Five command-owned policies would drift apart the way five copies of the lever table did. `SCOPE_CLASSES` is the single source for class, gate (`silent`/`disclose`/`require-ok`), wording and predicate; templates render `disclosures[]` from `write-scope-cli.mjs` rather than restating what a class means. Two orderings in that object are load-bearing and were measured, not reasoned about: `plugin-managed` before `user-scope` (both `~/.claude/config-audit/` and legacy `~/.config-audit/` are live, so the other order fires the gate on every session write and gets it switched off), and `user-scope` before `cross-repo` (`~/.claude/.git` exists, so a plain `.git`-upward walk calls `~/.claude/CLAUDE.md` merely "another repo" and silently downgrades the strongest gate). `disclose` ≠ `require-ok`: `campaign export` is cross-repo *by design*, so tightening it into a refusal breaks the feature. Distinct from the `require-target-dir.mjs` guard, which asks whether a scan **root** is readable (exit 3) — a different invariant, not to be merged.
|
||||
|
||||
**Dead-prose-reference silence list (invariant).** `CA-CML-013` is a precision-first check, so its
|
||||
design lives in what it *declines* to flag, and that list is measured (407 real CLAUDE.md files),
|
||||
never argued. Three rules are load-bearing and each has a guard seen red against its own defect.
|
||||
(1) **Containment is checked against the scan root, not the file's own directory** — a `../` chain
|
||||
that leaves the tree is silenced (`outside-scan-tree`) rather than resolved, because a base a `..`
|
||||
chain can escape is not a base: measured, `../../../../etc/passwd` resolved to the real file and
|
||||
silenced its own finding. A legitimate `../docs/x.md` inside the same repo still resolves.
|
||||
(2) **A bare token is a concept, not a reference** — `README.md` (no separator) and `docs/`
|
||||
(single segment) are excluded on the same reasoning one level apart; admitting bare filenames
|
||||
tripled the output with name-drops of tools living elsewhere, and single-segment folders are 26 %
|
||||
of the remainder, led by a remote namespace prefix. (3) **Rule ORDER is the reported reason** —
|
||||
first match wins, so `npm test` is silenced as `whitespace` (a command), not as `no-separator`,
|
||||
and two silences with different causes keep their own fixtures. The check emits **one finding per
|
||||
file** (the `todo-markers` / `repeated-content` idiom), because per-token emission measured 699
|
||||
findings where per-file measured 128. Silence is the safe failure direction here: a path carrying
|
||||
a trailing `:54-56` locator is a recorded v1 miss, not a bug to fix by loosening a rule.
|
||||
|
||||
**Subtraction floor (invariant).** `optimize --subtract` is the only lens that proposes removing config, so `scanners/lib/floor-exclusion.mjs` runs as a deterministic pre-step *before* the judge — a load-bearing block is never a candidate, and that guarantee must not be moved into the agent prompt. Two rules follow from it: (1) **staleness is not a deletion signal** — an outdated version pin inside a floor block is a `drift`/`CA-CML` dead-reference concern; (2) **tier 2 ≠ tier 3** — a compensatory block that keeps earning its place returns, and reporting it as dead weight is wrong even when the label matches. Norwegian keywords need the Unicode boundaries in `subtraction-prefilter.mjs`; JS `\b` is ASCII-only, so `/\bunngå\b/` silently never matches.
|
||||
|
||||
**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.
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue