ADR-139: A Doc Comment Is a Language Feature, and the Ledger Carries It

Status: Superseded by ADR-307 — its D1-D4 are D4-D7 there (D2 as displaced by ADR-251 D2) Date: September 19, 2026 Target Version: Nikaia 0.0.18 Related: ADR-134 D3 (/// and /** … */ are ordinary comments), ADR-024 (the ledger is what a consumer reads), ADR-288 D11 (an entry exists because a program asked for it), Part III 13.5

1. Context

/// is an ordinary comment and ADR-134 D3 keeps it one: that record refused to add a doc comment by the back door and said the question was its own. This is it.

What asks for one. nikaia.contracts ships with a package (ADR-024), and it is the one file a consumer’s compiler reads about a dependency — every signature, every promise, every restriction. It has nowhere to put a sentence. The prompt bundle on the roadmap has the same hole from the other side: a model handed a package’s surface is handed types and no prose. A comment in the source does not travel, because the source of a published package is not what a consumer reads.

2. Decisions

D1 — /// before an item is its documentation, and the parser keeps it.

/// The status line for a response code, as HTTP/1.1 writes it.
///
/// An unknown code is `500 Internal Server Error`, which is what a
/// server owes a client it cannot answer.
pub fn status_line(status: i32) -> String { … }

A run of /// lines immediately before an item — a fn, a struct, an enum, a trait, a field, a variant — is that item’s documentation. Anywhere else /// is an ordinary comment, which is what it is today and what ADR-134 D3 said of it.

/// and not /** … */. One spelling, and it is the one every neighbouring language uses. A block comment stays a comment wherever it stands, which keeps ADR-134 D3 true as written.

D2 — The ledger gains a doc column, and it ships.

[fn."http::status_line"]
pub = true
doc = "The status line for a response code, as HTTP/1.1 writes it.\n\nAn unknown code is …"

On a fn entry and on a type entry, holding the text with its line breaks and nothing else — no markup the ledger has to agree about, and no rendering. Only for a pub item, because the ledger records what a consumer may reach (ADR-288 D11) and a private item’s prose is the source’s.

It is derived and not written, like every other column: the ledger is a pure function of the sources, so a hand-edited doc is overwritten by the package’s own build exactly as a hand-edited sync is (ADR-100).

D3 — A doc comment is prose and the compiler does not read it.

No @param, no directive, no link the compiler resolves, no refusal for a wrong one. What it does is travel. nikaia doc renders it and the prompt bundle carries it; neither is this record’s and both need it to exist first.

A doc comment on a private item is kept in the source and dropped from the ledger, which is the only place the two can disagree — and the answer that costs nothing: a reader of the source sees it, and a consumer was never going to.

D4 — The diff narration is unchanged by it.

A doc that changed is not a contract that changed. --locked compares the ledger byte for byte and a changed sentence fails it, which is correct — the file is derived and the build regenerates it — but NK2401, which narrates what broke a caller, says nothing about prose.

3. Consequences

4. What this does not decide

5. What is built

Steps 1 to 3, for an item: a fn, a method, a trait method, a struct, an enum, a trait. A field’s and a variant’s wait, because nothing reads them until nikaia doc exists and that is §4’s.

What it took is not a lexical rule. The implicit whitespace skip runs before a rule is tried, so by the time item matches, the /// lines in front of it are consumed and gone — the same wall ADR-135 D3 met, one record earlier. So the skip records what it sees (Trivia::comment) and the item reads it back.

And it is read at the item’s first byte rather than in its action, which is the part that took two tries. An action runs when the rule has matched, and by then the skip has been over the whole body: what it recorded is the trivia inside the item. doc_here is a hand-written parser that consumes nothing and asks at the one position where the question has an answer, and it stands first in each of item’s nine alternatives.

The answer is a position and not a flag, which is what makes it survive backtracking. item tries nine alternatives and eight of them fail — but a failed alternative has still consumed tokens and still run the skip after them, so a flag saying the run still reaches here is false by the time the arm that matches asks. doc_reaches is a fact about the input; a later skip somewhere else neither extends it nor spoils it.

An ordinary comment between the prose and the item does not end the run. D1 says immediately before, and the first reading of that — a // line ends it — turns out to forbid the shape this repository is written in: a sentence for whoever reaches the item, then a note for whoever reads the source. A comment is trivia, so no token has been consumed and the prose is still about the item below. What ends a run is code.

The ledger’s format needed one thing: escape writes a line break as \n and unquote reads it back. Nothing else written to that file has ever held one, so every ledger already on disk renders and reads back exactly as it did.

std is the first corpus (step 3), and it is complete. Its one Nikaia entry, text::digit_value, carries prose that is derived — and the test that regenerates std.contracts from its sources is what caught the drift the moment the line was missing, which is the whole argument for deriving it. All hundred and eight of the hand-written entries carry one now, written in that file and reviewed like code, which is the bargain it already makes about sync and crosses — a compiler that does not read Rust cannot derive any of the three.

What keeps the column full is a test and not a habit. every_public_entry_of_the_shipped_std_ledger_has_prose fails on an entry added without a sentence, the way the two beside it fail on one added without a signature — because a column that is mostly empty is a column a reader stops looking at, and the five entries it started with were the shape that was heading for. Its neighbour renders the shipped ledger and parses it again, which is the round trip that holds \n and its reverse together over a hundred real entries rather than one fixture.

nikaia doc is step 4 and its own record.

This site is open source. Improve this page.