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:
Kjell Tore Guttormsen 2026-08-12 20:11:12 +02:00
commit dbb6a6a3cf
10 changed files with 478 additions and 3 deletions

40
tests/fixtures/dead-prose-ref/CLAUDE.md vendored Normal file
View file

@ -0,0 +1,40 @@
# Dead prose reference fixture
## Project overview
This fixture pins the C3 silence taxonomy. Every token below is here on purpose;
each silence class carries a distinct cause and gets its own token.
## Commands and workflows
Three dead path-shaped references (these, and only these, must fire):
- The runbook lives in `docs/missing-runbook.md`.
- Deploy with the script at `scripts/deploy.sh`.
- Generated output lands in `build/artifacts/`.
A live reference that must stay silent: `docs/real.md`.
## Architecture
One token per silence class:
- whitespace, a command not a path: `node scripts/build.mjs`
- url, existence on disk is meaningless: `https://example.com/a/b.md`
- glob, a pattern not a path: `CA-GAP-*`
- placeholder, unresolved until expanded: `${CLAUDE_PLUGIN_ROOT}/hooks/x.mjs`
- absolute or home, outside project scope: `~/.claude/settings.json`
- key or flag, not a path at all: `model:`
- bare token, a concept not a reference: `README.md`
- ambiguous slug, a Forgejo remote: `ktg/from-ai-to-chitta`
- bare folder name, a concept not a reference: `vendor/`
- escapes the scanned tree: `../../../../etc/passwd`
- trailing locator, a known v1 gap: `docs/plan.md:54-56`
## Conventions and patterns
Fenced code is illustrative, never a reference. The dead path below must stay silent:
```bash
cat docs/fenced-and-dead.md
```

View file

@ -0,0 +1,3 @@
# Real
This file exists so `docs/real.md` resolves and stays silent.

View file

@ -0,0 +1,4 @@
# Nested
Repo-root-relative reference: `docs/real.md` resolves from the scan root,
not from this directory. It must stay silent.