RUL-028 — Traceability is a query, never a file sweep

Ruling

Requirement and test traceability questions are answered by querying the code-intelligence KB, not by reading or rewriting files in bulk.

Concretely:

  1. Which requirements exist, which are untraced, which symbols are orphaned, what a use case narrates → scripts/trace_intel.py (orphans | untraced-reqs | unspecced | narrative).
  2. Where a requirement is CITED, and whether a citation conforms → scripts/serialize_codebase_ast.py (check-traces | trace-audit | trace-lint).
  3. Blast radius of a change → scripts/code_intel.py (impact | callers | cycles | unused | dep-path).
  4. Requirement / Spec nodes and their edges in the operational KG → scripts/graph_query.py.
  5. Grepping one named file is still fine. What is refused is crawling a tree for REQ- ids — recursive grep/rg, or an inline rglob/os.walk that hunts requirement vocabulary.

This is CLAUDE.md's "deterministic-first for code intelligence" rule, promoted from prose to a mechanism.

Why

A regex cannot tell a citation from fixture data. The KB can, because it is built from @traces decorator nodes — real AST — while a sweep matches text that merely looks like one.

Measured, session 260 (2026-08-10). Renumbering SPEC-001's 280 story-numbered requirement ids was attempted three times:

Cost of passes 1 and 2: two full-suite runs (~20 minutes of wall clock) plus the reversals, to rediscover a distinction the KB already encoded.

The gates did not catch it either — gates.py returned 13/13 PASS on the corrupted tree, because no gate validates requirement-citation semantics. The first thing that could catch it was the 10-minute suite. That is precisely the shape of defect a PreToolUse guard exists to stop at the point of typing.

Enforcement

Notes on the mechanism

force-trace-intel.sh is a PreToolUse(Bash) deny hook, wired in .claude/settings.json. Its test corpus in tests/scripts/test_force_trace_intel.py is the verbatim set of commands that caused the reversals above — a guard tested against invented strings proves nothing about the failure it was built for.

Three defects in the hook itself, each surfaced by that corpus, are worth carrying forward to any sibling guard:

  1. Serialize the deny payload; never printf it. The reason quotes the offending command back, and commands contain double quotes (grep -rn "REQ-008-013" src/). A printf template emits invalid JSON, and a harness that cannot parse the decision reads it as allow — a guard that denies into a syntax error is indistinguishable from no guard. All eight deny cases failed this way while the detection logic was already correct.
  2. Read the SUBJECT from the raw command, the STRUCTURE from the masked one. hook_bare blanks quoted segments so a command carried as data is not read as a pipeline. But here the subject is a pattern, which lives inside quotes — masking blanked exactly the part identifying the violation.
  3. Match the bare prefix REQ-. A sweep does not write ids out, it writes a pattern for them: re.findall(r"REQ-[0-9]{3}"), where the next character is [. Requiring REQ-[0-9A-Za-z] matched every literal id and missed every regex hunting them — backwards, since the regex is the sweep.

Scope is deliberately narrow, per the precedent in force-test-observation.sh, which once demanded an orchestrata-only pytest plugin of every repo and made another project's suite unrunnable: the guard fires only on a sweep of a tree this repo's KB indexes, exempts the deterministic tools and the gates that legitimately read the same vocabulary, and leaves single-file reads alone. A guard whose only satisfying action is impossible does not enforce a ruling — it teaches the operator to route around it.