ADR022 ReceiptBoundary
Title: Receipt boundary
- Status: Accepted
- Decided: D2026-09-08
- Follows: ADR014
Literals - Followed by: ADR028
RecordFactsInTheUniverse - Affects:
Cli,Prose,Receipts,Ui
Context
014 refused literals in prose. That refusal has a consequence nobody wrote down: things that genuinely have to be values need somewhere else to live.
The earlier language put them in prose. It had a document form that attached
external files to a concept, still visible at
archive/universe/test/fixtures/named-document.prose:
document ItemsDesignDoc {
kind { 'internal' }
path { '/docs/items/overview.md' }
describe { High-level items design notes. }
}
path and kind are quoted string literals, so that feature could not
survive 014 whatever else was true of it. The original intent was that prose
would hold references to all the disparate sources a universe drew on,
including links out to physical documentation. When receipts arrived, that job
moved to the side that is allowed to hold a URL, and prose stopped
constructing documentation at all.
So the arrangement already exists in practice. What it has never had is a statement, which means it cannot be applied to the next case — and there are three next cases queued. Citations are meant to become elaborate enough to navigate from a concept in the field guide to the documentation that expands on it. ADR metadata wants a home. And 018 is open partly because dates have nowhere legal to go.
The invariant currently guarding receipts is that a receipt never defines
anything. It is stated in the package README, in the receipt.js fileoverview,
and in package Receipts in universe/universe.prose, which calls it "the
whole discipline of it." That invariant is correct and it is not sharp enough
for the queued cases, because "documentation attached to a concept" sounds like
attachment, and attachment sounds like a fact about the concept.
Decision
Prose holds no literals. Receipts may. That is the dividing line. A value that has to be a value — a URL, a revision, a timestamp, a path — belongs on the receipt side of it.
A receipt describes itself and where it is. It never describes its target. Every field a receipt carries is a fact about the evidence: where it sits, what it looked like, how to reach it. Beyond the bare acknowledgment that a name was referenced, a receipt says nothing about the concept it names.
A receipt still never authors an edge. The marker grammar is a target and nothing else (020), and it stays that way.
Consequences
What this permits. Elaborate citations pass the test cleanly. A URL, a
document title, a revision are facts about the evidence — the same category as
file and line, only addressable from further away. Opening the
documentation for a concept from its page in the field guide is legal under
this rule, and it is the case this record exists to unblock.
What this refuses. ADR-style metadata in a marker — a status, a
supersession, an affects list — fails the test, because those are facts about
the decision rather than about the file. A receipt carrying them would be
defining, and the invariant would be gone well before anyone noticed, because
each field individually looks like harmless bookkeeping.
The known hard case, recorded rather than solved. For code, the two sides
are different objects: packages/prose-parser/src/errors.js is evidence of
Sprig.ExpectedTokenError but it is not that error. The receipt points across
a real gap, and "describes itself" versus "describes its target" is an easy
call. For an ADR, the markdown file is the decision. There is no gap to
point across, so a fact about the document and a fact about the concept are the
same fact, and this test loses its grip precisely where it is most wanted.
The practical consequence is an ordering. Documentation about a concept is the easy case and should ship first. An artifact that is its concept needs its own record, and that record has to decide something this one does not.
What this costs. Prose is permanently less self-contained. A universe on
its own cannot tell a reader where to read more; that takes a scan of a repo
that may not be present. And the value side is unversioned by prose, so a URL
can rot without the universe noticing — which is the same trade receipts
already accept for file and line, extended to something less stable than a
path in the same repository.
Schema. There is no slot for any of this today. details is
{ note?: string } and nothing more. The bundle is schema: 1, and
meta.origin is the only space currently reserved for growth. Elaborate
citations are additive work on a schema that has not had to grow yet.
What to watch, and it should be settled before citations grow. Receipt ids
hash manifestId (receipt.js, the identity object), so every receipt id in
every repository rotates whenever the universe recompiles, including for an
edit that changes nothing about reality. That is tolerable while a receipt is a
disposable local scan artifact. It is much less tolerable once receipts are the
durable home for authored values, because a rotating id cannot be referenced,
aggregated per environment, or diffed to answer "did this move."
Erosion changes sides. 014's warning now applies here in mirror image. The pressure will be to let a receipt say slightly more about its target, one reasonable field at a time, and the rule above is the only thing that will answer it. A receipt that describes its target is not a receipt.
Alternatives considered
Keep documentation construction in prose, with a literal exception for paths. What the archived form did. Rejected: it is 014's exception request wearing different clothes, and a filesystem path or URL is the least durable value in the system — the worst possible first exception, because it rots on its own schedule and prose has no way to notice.
Let prose name a document by identifier and let receipts supply the URL. A
document concept in the graph, with the location on the receipt side. This is
genuinely attractive, because it keeps the graph authoritative and puts only
the volatile part where volatility is allowed. Deferred rather than rejected,
because it belongs to whatever syntax Document becomes — that block is
Candidate and currently about code samples and embedded concepts, not external
files, and deciding this here would decide that by accident. The cost to weigh
when it is taken up: a node whose only content lives elsewhere has nothing to
say when the scan is absent.
A marker that carries an edge, such as @sprig affects Sprig.Prose.Describe.
Rejected. It is the shortest path from citation to authoring, and it ends the
invariant outright: receipts would become a second surface where the graph is
written, with no compiler checking it and no record of who asserted what.
Arbitrary structured details, with meaning decided per consumer. Let
details hold any object and let each reader interpret it. Rejected. It is the
same collapse with extra steps — a free-form bag means the boundary is enforced
by nobody, and the first consumer to read a status field out of it establishes
that receipts carry statuses.
State no rule and judge each case as it arrives. Rejected, and this is the reason the record exists rather than a strawman. Three cases are already queued and every one of them looks locally reasonable. That is the shape of a boundary moving without a decision, which is exactly what 014 named and what this record is the other half of.
Evidence
The archived document form is the direct evidence, because it shows the
alternative arrangement in working code with the literal in plain sight:
path { '/docs/items/overview.md' }. Documentation linkage was built in prose
first, and it is the quoted string that made it incompatible with what prose
became.
The current receipt shape already obeys the rule, which is why the rule is a
ratification rather than a change. target, targetKind and resolution are
the acknowledgment itself. details.note is what the author wrote beside the
marker. source is file, line, column, subject, subjectLine and
contextHash — six facts about where the evidence sits and what it looked like
when it was read. Not one field describes the concept.
020 is the same lesson one level down, and it is worth reading alongside this record. There, "for @sprig doc-comments" was lexically indistinguishable from a real marker, so no amount of tightening the characters around the tag could separate them; something other than the local text had to decide. Here, a status field and a URL field are both just fields, and no amount of schema care distinguishes them either. In both cases the answer is a stated rule about what the thing is for.