docir — design documents Graph

Documents / Decisions / adr-ae631a356639

A prose link resolves, renders and is checked — and stays out of the graph

Why [[...]] cross-references now resolve against ids, filename stems and title slugs, why the unresolved ones are a Tier 1 warning where unresolved mentions are not, and why they still feed no check.

adr-ae631a356639decisionaccepted#architecture#cli#integrity
View as Markdown◉ View in graph

A body cites another document two ways. related: frontmatter is the typed, authored edge: validated on write, reported by dangling, drawn on the graph. [[...]] written mid-sentence was the other one, and nothing in docir had ever read it — not the renderer, not a check, not the index. It published as literal brackets, and a retitle broke every inbound one in silence.

The decision

Resolve [[...]] against the corpus, render the resolved ones as links, report the unresolved ones from docir check as unresolved-link, a warning. Do not feed them to the graph.

The rule lives in platform.naming.links, beside the tag-key and document-id grammars and for the same reason (adr-289e788719a7): documents reports the broken ones and publishing renders the working ones, neither may import the other, and a rule written twice is how a link comes to resolve for the renderer and dangle for the check.

What a target may be

Four forms, because all four are what people write: the document id, the filename stem, the title slug, and the title itself. A .md suffix and a leading directory are stripped, since both are what a path looks like when pasted. [[target#heading]] and [[target|label]] parse.

The stem is the form that cannot be derived. update --set-title keeps the original filename, so a retitled document's stem is the slug of a title it no longer has — and the links written against it are exactly the ones that must keep working. So the stem is carried as data, from the path the docir get payload already contains.

Resolution reads every document

Inactive and archived included. A link to a resolved issue or a superseded decision is a working link — following a decision to the one that replaced it is the point of publishing the graph — so "not in the default query" is never a reason to call one broken.

That was the reporter's own mistake, twice, in the four hand-written passes that produced the issue: pass two called ten links broken, and the last one that survived was exact and pointed at a resolved document. Anyone validating these outside docir reimplements this rule and gets it wrong the same way, which is the argument for it being here rather than in a consumer.

Ambiguity is not resolution

Two documents can share a title, so a title slug can name both. Neither renderer nor check guesses: the link stays text and the finding lists the candidates. Picking one silently is how a reader lands on the wrong document with no way to tell.

Why this is Tier 1 when unresolved-mention is not

They look like the same check and are not. An id named in a sentence is a citation — writing adr-0007 while explaining the id format is correct usage, and measured on this corpus all 47 unresolved mentions were exactly that, so a Tier 1 warning would fire only on documents doing their job (adr-e86c5040d626). [[...]] is not a citation. It is link syntax with no second reading: whoever typed the brackets meant to point at a document.

The one false positive available is a body demonstrating the syntax, and scan_wikilinks skips code spans and fences. That filter is free here and was not free there: 56 resolved mentions in this corpus live only inside code spans, so filtering them would have deleted 12% of the working mention graph, while a [[...]] inside code was never a link — the renderer works from the markdown token stream, where a code span is opaque. This repository's only two occurrences are both inside code spans, in the document explaining what a wikilink is; read literally they would be the check's two findings, on the document doing its job.

A warning, not an error

dangling is an error because a related: edge is a declared, typed claim the write path validated, so one that resolves to nothing means the corpus was damaged after the fact. A prose link carries no kind, gates no merge and feeds no graph, so a broken one costs a reader a click. Promoting it would red-build a repository whose documents are all intact — the failure every warning in arch-ad342aae8293 is there to avoid.

check --fix leaves it for the same reason it leaves unknown-type: a target that resolves to nothing needs somebody to say which document was meant. The near-miss this was built from is a slug guessed one word short of the title, which no rule can distinguish from a link to a document not written yet.

It stays out of the graph

orphan reads related: alone, and a [[...]] does not clear it — the rule adr-e98749aa457d settled for mentions, for the same reason. A judgement about a queue must not be cleared by the prose that triages the queue, and prose that names an orphan is usually the triage of it.

Making prose links structural was the open question in the report and is deliberately not answered here: related: remains the typed layer, and this is navigation.

What the site renders

The target's current title, not the written target. A slug goes stale the day somebody retitles the document, which is the failure that made these worth resolving, so rendering the title is the difference between a link that ages and one that does not. |label wins where it is given. A target the site does not publish — an archived document under the default build — stays the literal [[...]], which is what a dangling edge does on the same page.

To amend: Re-verify: