sprig and prose docs classic docs
Reference› ADRs› ADR022 ReceiptBoundary

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.