ADR 0000: Record each explanation once, in the place that owns it

Status

Accepted

Amended after acceptance under the reader’s-position rule below; the Notes section records the clause added.

Context

This package documents itself heavily and deliberately. Its public getters are thin wrappers whose numpydoc parameter tables are the deliverable: 55% of all docstring lines in dataretrieval/ sit in the service adapters, at a ratio of 2.5 prose lines per line of code. CONTRIBUTING already requires those tables.

The problem is in the internal modules behind them. Rationale – the argument for why a rule holds – accumulated in module and function docstrings alongside the ADRs that already owned it, because a paragraph can be written where the reader is standing while a citation sends them to a record they have to open. Those modules hold 82% of the package’s comment lines, and an audit of that prose found roughly 500 lines restating decisions already recorded in ADRs 0003 through 0011: configuration.py re-derives the layered-resolution design in 65 docstring lines while citing ADRs 0009, 0010, and 0011 in the course of it, the no-progress budget is argued from first principles in five places across transport/, and which failures may be retried is enumerated in three lists that can drift apart.

Duplication is not a tidiness problem here; it is a correctness problem. Every copy is a place the rule can be updated while the others are not, and the audit found copies that had already gone stale – an overview paragraph describing concurrency caps that a later ADR had removed, and an ADR clause describing a credential rejection the code deliberately no longer performs. A reader has no way to tell which copy is current.

Decision

Each explanation is recorded once, in the venue that owns that kind of knowledge, and referenced from anywhere else that needs it.

Docstrings own the contract. What a caller must know to use the documented object: the numpydoc Parameters, Returns, Raises, and Examples sections, what the function does, and what it guarantees. Public getters keep their full parameter tables, however long. A private helper’s docstring says what it does and what its callers may rely on.

Inline comments own the local constraint. Why these lines are written this way, when a name cannot carry it – an ordering that matters, an upstream quirk, a bail-out that looks removable. One or two lines, adjacent to the code they explain. A comment that outgrows that is describing something wider than the lines beneath it, and belongs in one of the venues below.

Commit messages own the history. Benchmark numbers, the symptom that prompted a change, what the code used to do, what was tried and rejected. This is the venue with a date and a diff attached. It is the one place where “was once optional” or “measured 1.6x slower” stays true forever without maintenance. Source files carry the current state, not the route to it.

ADRs own the cross-cutting decision. A choice that constrains code outside the file stating it, or that a future contributor could plausibly undo from somewhere else. The code cites the record by number rather than restating its argument. Adding a clause to an existing ADR is preferred over a new record; number a new one sequentially and follow ADR template.

The glossary owns the vocabulary. CONTEXT.md defines terms with package-wide meaning. Documents use those terms rather than redefining them, and where a term and the code disagree, the term wins.

Three rules follow:

  • Cite, do not restate. Prose that argues for a rule an ADR already owns is replaced by a reference to that ADR’s number. A pointer that does not resolve is visible; a paraphrase that has drifted is not.

  • Write from the reader’s position. A citation replaces an argument only if the sentence left behind stands on its own. Prose that assumes the reader has the cited record already open, or leans on a term the glossary does not define, has moved the cost of the duplication rather than removed it.

  • An accepted ADR is not edited to reverse its meaning. A later decision supersedes it and links back, as the decisions index already requires. Additive clauses and corrections are recorded in the amended record’s Notes, and its Status says the record was amended, so a reader meets that fact before the Decision text rather than after it.

Consequences

  • A rule has one current statement, so updating it cannot leave stale copies behind in modules nobody thought to grep.

  • Reading a module gets slower in one respect: some rationale now requires opening an ADR. That cost is accepted – the reader who needs the argument is rarer than the reader who needs the contract, and the ADR is the version that is maintained.

  • Rationale is not deleted when it moves. Prose that leaves a docstring lands in an ADR clause or in the commit message that removes it. The commit message is where a reviewer looks for what a documentation change discarded.

  • Docstring volume in the service adapters is expected to stay high and is not a metric to optimize. A ratio measured over a public adapter says nothing about whether it is over-documented.

  • The policy applies going forward. Existing prose is migrated when a module is being changed for another reason, rather than in a sweep that would touch every file at once.

Compliance

Reviewers apply two questions to added prose. First: does this explain the lines beneath it, or does it argue for a rule that binds another file? The second belongs in an ADR, cited by number. Then: could a reader who has not opened the cited record follow this sentence? If not, the citation has hidden the explanation rather than relocated it. The repair is to restore the reader’s footing – name the term, resolve the pronoun, say which venue owns the rest – not to restate the argument the citation replaced.

The mechanical part is checkable, and it is the part that goes stale: a docstring or comment that names an ADR must name one that exists. tests/architecture_test.py asserts that every ADR NNNN reference in dataretrieval/ resolves to a record in docs/source/architecture/decisions/, so a renumbered or deleted record fails the suite rather than leaving a dangling pointer. Whether a given paragraph should have been a citation remains a review judgement. No test is proposed: a proxy metric here would push contributors to delete parameter documentation to move a number.

Notes

The reader’s-position rule and the second review question were added after acceptance. Review of the pull request that introduced this record found prose this record had put in the right venue and left unreadable from outside the author’s head: undefined jargon, a pronoun with no antecedent, and a mapping between two numbering schemes that needed a second document open. One instance broke this record’s own history rule. The venue rules say where an explanation goes; none of them asked who it reads for.

Context and the measurements below are this package’s. Decision, Consequences, and the review questions in Compliance are written to hold for any project; the paragraph naming tests/architecture_test.py is not. A project adopting this record writes its own Context from its own audit and keeps the rest.

This record was written after ADRs 0001 through 0011. It is numbered 0000 because it governs how every record is written, not because it came first. Its Context describes the package as the audit found it, and the migration it authorizes is incremental, so some of the prose described there is still in place.

Prose measurements were taken over dataretrieval/ on 2026-08-26: 9,283 docstring lines and 1,157 comment lines against 6,127 lines of code; public service adapters at 2.46 prose lines per code line, internal modules at 1.29. The ~500-line restatement estimate comes from the subsystem audit recorded in the pull request that introduced this ADR.