ADR-008: One directive vocabulary, one scope rule

Status Active · Filed 2026-08-03

Context

Two checks take instructions from the prose they check: the reference-status report needs “this citation is deliberate”, and the hyperlink lint needs “lint this code block anyway”. Two ad-hoc syntaxes would be two things to learn and two parsers to keep in step — DP-4.

Decision

One parser, one shape, one scope rule.

<name>[-block|-file]: <args> [— <reason>]

and the directive must open its comment, # noqa-style. Read from real comments only: HTML comments in markdown outside code, COMMENT tokens in Python, text after a comment marker elsewhere.

Scope is decided by the suffix, uniformly, with no per-directive defaults:

  • bare — its own line and the line below, so it can sit directly above the line it governs;
  • -block — the run of non-blank lines it sits in. A directive standing alone between blank lines has no content block of its own, so the block it means is the one it introduces. That is the reading of “block”, not an exception to it;
  • -file — the document.

Two directives ship: inactive-ok (this reference to a retired document is deliberate) and unexempt (lint this normally-exempt region). A third is a name, not a new syntax.

Documenting the syntax must not invoke it. This is not a hypothetical — it happened four times while building this:

  1. A module docstring’s example silently annotated its own module.
  2. Comments explaining that bare numbers are rejected parsed as annotations with bare numbers, and reported themselves as malformed.
  3. An annotation naming a code counted as a citation of that code, so every annotation satisfied itself and none could ever go stale.
  4. Writing “the difference is SG-ADR-060” into a decision gave that decision a citation, silently reconciling two counts the prose still claimed differed.

Hence two rules rather than one: a directive fires only from a real comment, and directive-shaped text — live or illustrative, in a fence, a docstring, anywhere — is never a citation.

Alternatives considered

  • Per-directive default scopes, so a bare unexempt: reached the fence below it across a blank line. This shipped briefly, to make a plausible-looking example parse. It bought one example and cost the rule its predictability: the same bare form governed a line here and a paragraph there depending on whitespace nobody was thinking about. The example was wrong, not the rule. When a proposed example doesn’t parse, correct the example rather than widening the rules until it does — widening is invisible when you do it and expensive forever after.
  • A wildcard (inactive-ok-file: *). Would let a page mute everything, including references added later — the drift the whole package exists to stop.
  • Matching the directive anywhere in a comment. What was tried first; see failures 1 and 2 above.

Consequences

  • One vocabulary to learn, and the scope rule fits in a table.
  • unexempt: codeblock carries an inherent caveat, not a bug: markdown inside a fence renders literally, so the link the lint then demands shows as [ADR-157](…) in the sample. That is the trade the directive exists to let an author make, per block, rather than being settled once for a whole corpus.
  • The self-reference rules are the part most likely to be re-broken by someone extending the vocabulary, which is why they have tests naming the incidents.