Why is ctxgrd needed?

If you already keep architecture decision records, you have probably not felt a need for a linter. The documents are written, reviewed, and merged. Nothing is visibly broken.

That is the situation this guide is about. The argument is not that your documents are messy. It is that they have quietly become infrastructure, and nothing in your toolchain checks what depends on them.

Every number below was measured against ctxgrd’s own repository at version 2.5.0. It is a repo with a mature corpus and no unusual discipline, which makes it a fair stand-in for yours.

Documents become dependencies without anyone deciding they should

A decision record starts as prose. Then someone writes a code comment that says why a function is shaped the way it is, and cites the record by id.

At 2.5.0, ctxgrd’s own source and test files contain 1,724 such citations, pointing at 113 distinct records out of 125 that exist. That is not documentation about the code. That is the code depending on the documentation.

The dependency runs the direction people find surprising. Deleting a source file breaks the build immediately and loudly. Deleting a decision record breaks 1,724 potential explanations silently, and the build stays green.

Renumbering is worse than deleting, because it succeeds. Every citation still resolves to something, just to the wrong decision. There is no error, only a comment that now confidently explains the wrong thing.

“Nothing has broken yet” is not evidence

This next part is why the guide uses ctxgrd’s own repository rather than a hypothetical one.

Until the change that accompanied this guide, ctxgrd’s configuration had no [references] block. The reference scanner is dormant without one. So the project that ships a tool for checking code-to-document references was carrying 1,724 unchecked ones of its own.

Switching the scanner on reported 64 unresolved ids. Every one turned out to be a deliberate test fixture or an illustrative id inside a doc comment.

Not a single real citation was broken. Once the fixtures were suppressed, the scan settled at 1,768 resolved citations and no failures, and it now runs on every lint.

That result is good news and terrible evidence. The corpus was clean, but nobody could have known that, because nothing had ever looked. A clean corpus and an unverified corpus are indistinguishable from the outside, and they stay indistinguishable right up until the moment they are not.

That is the whole case for a linter. The value is not that it finds problems today. The value is that “we have never had a broken reference” stops being a belief and becomes a check.

Conventions decay because they are enforced by attention

The second kind of dependency is structural. Records are useful to read in bulk because they share a shape, and that shape is held together by whoever last reviewed a pull request.

ctxgrd’s own convention requires seven sections in every decision record. Across 125 records that is 879 headings, of which the mandated ones account for almost exactly 125 x 7. The corpus also carries 851 distinct requirement identifiers.

No reviewer holds that in their head. Uniformity at that scale is either machine-checked or it is a story people tell about the corpus.

The cost of losing it is not aesthetic. Tooling that reads records – changelog generation, dependency graphs, status rollups – is written against the shape. When the shape drifts, the tooling degrades quietly rather than failing, which is the worst way for it to go wrong.

You cannot solve this by making the corpus smaller

The intuitive escape is consolidation. If 125 records are hard to keep consistent, merge them into one well-maintained document.

Measured, that does not work in either form it can take.

Concatenating the corpus produces roughly 1.85 MB. The default file budget is 150,000 characters, so the result lands 12.3 times over. It exhausts a reader’s useful attention long before it trips any limit.

Distilling instead is feasible. One line per requirement is about 1,000 lines, which fits comfortably. But it discards the Context sections, and those are the records’ actual payload. A bare statement of what was decided cannot stop the decision being relitigated next quarter, because it no longer carries the reasoning that settled it. Rejected decisions show this most sharply: their entire value is the argument for the “no”.

There is a structural cost too. Today a new decision is a new file, which costs nothing to add. In a merged document it is an edit to a hot file, which reintroduces merge conflicts and rereading costs on every change.

Append-only logs are cheap to extend and expensive to summarise. Merging inverts both properties. The corpus is not going to get smaller, so the only available move is keeping it correct.

What checking actually looks like

ctxgrd does not walk your repository looking for markdown to complain about. A file is linted only when it claims to be a document, either through an id field or a configured path glob. Everything else is skipped in silence, which is why the first run in a new repository reports nothing at all. How ctxgrd decides what to lint explains that mechanism in full.

Reference checking is opt-in on top of that. A [references] block names the non-markdown files to scan, and ctxgrd resolves every document id it finds there against the corpus:

[references]
scan = [
  "ctxgrd.toml",
  "Makefile",
  "src/**/*.rs",
]

Matches surface under the same core.cross-ref rule that checks markdown bodies, so you enable no new rule. Without the block, the scanner does nothing. See scanning non-markdown files for the full contract.

The honest boundary

Reference scanning has a real limit, and it is better to meet it here than in your own repository.

The scan list is the only scope control. It selects files, and it cannot select regions within a file. That matters in any language that co-locates tests with the code they test.

Pointing the scanner at ctxgrd’s own Rust sources first reported 64 unresolved ids. Every one was a test fixture or an illustrative id in a doc comment. Of those, 54 sat in test modules embedded in the same files as the production code, where no glob reaches them.

Inline suppression is the remedy, but it costs more than the diagnostic count suggests. The markers work per line, and the diagnostic relocates: silence the first mention of a target and the scanner reports the next one instead. You pay one marker per mention, not per diagnostic. Here that came to 129.

Four mentions could take no marker at all, because they sat inside multi-line string literals. Appending a comment there has two failure modes. It breaks the parse when the line ends in a \ continuation, and it silently corrupts expected test data when the string is a snapshot.

Two of the four were solved by placing ignore-next on the adjacent code line above the string. The last had no such neighbour, so its fixture id was renamed to a document that exists.

This makes ctxgrd’s repository the pathological case for its own feature. Its test suite is unusually dense in invented document ids precisely because that is what the tool is about. An application citing decision ids in comments carries no such fixtures, and can scan its whole source tree cleanly.

When a suppression is genuinely warranted, the markers are per-line rather than per-rule:

# ctxgrd: ignore-next
# See ADR-999 for the retired approach.

The configuration above is what ships in this repository. It resolves 1,768 real citations on every run and reports no failures.

The marker tax is permanent. Every new fixture id needs one, and forgetting turns the build red.

That is the right direction for the failure to point. The alternative is to list only the files that happen to be fixture-free, and that fails silently: a new file goes unscanned with nothing to say so.

What this buys you

Nothing about writing records changes. You do not adopt a new format, and existing documents are not rewritten.

What changes is the class of failure that becomes visible:

  • A deleted record that something still cites.
  • A renumbered id that now resolves to the wrong decision.
  • A record that lost a required section during a rushed merge.

You stop finding these while reading. They start failing a check instead.

If your corpus is small enough that you can hold it in your head, you do not need this. The threshold is not a document count; it is the first time something outside the corpus starts depending on it.

Past that point, the question is not whether the documents are correct. It is whether you can demonstrate it.