sprig and prose docs classic docs
Reference› ADRs› ADR010 TitleBlock

ADR010 TitleBlock

Title: The title block

  • Status: Accepted
  • Decided: D2026-09-05
  • Follows: ADR016 UsingMarkdown
  • Affects: Parser, Title, Ui
  • Pairings:
    • Parser — spec text only

Context

title was introduced to escape an algorithm. The original UI derived a display name from every identifier — deriveTitleFromIdentifier in packages/sprig-ui/src/lib/format/title.js splits camelCase, spaces out acronyms, and separates digits, so CountBasedEffect renders as "Count Based Effect" and isAllowedTo as "is allowed to". That algorithm is lossy in one direction and wrong in another, so authors needed a way to say "not that, this". title was that way.

The beta field guide, which came out of the UI bake-off, does not humanize. It renders what the manifest says. That change quietly promoted title from an override on an algorithm into the only path to a readable name, and it did so by substitution: buildModel computed label = trimmed(title) || name, and the concept page used the label as its heading with the raw name demoted to a secondary span.

That is what raised the question. The discipline for adding to prose asks whether a feature is a display concern or a graph concern, and a field whose only job is to change what a heading says looks like a display concern. Two recent decisions had already pushed rendering out of the language: ADR MarkdownInDescribe rejected markdown outright, and UsingMarkdown settled that describe blocks store whatever the author wrote and downstream consumers decide how it renders. If describe is content and rendering is the consumer's business, title looked like the exception that had not been examined yet.

The examination turned up something different from what was expected. The fear was that title duplicates the identifier — that authors write concept ConceptName { title { Concept Name } } and now the same fact lives in two places, with nothing keeping them honest. That has never happened. Not one title anywhere in the ecosystem is a camelCase expansion of its own identifier. What title had actually drifted into was three separate jobs, only one of which it was specified for.

Decision

title stays in prose, and it is a graph concern, not a display concern.

The useful test is not "does this affect what the reader sees" — describe affects that too. The test is whether a second renderer could legitimately disagree with the author about this string. For markdown, it can: whether a phrase is bold is a rendering choice, and a plain-text export is right to drop it. For humanization, it can: a dense table legitimately wants ConceptName while a reading view legitimately wants "Concept Name". For Carpenter's Workshop, it cannot. There is one correct answer, the author is the only one who knows it, and no algorithm run over CarpentersWorkshop recovers the apostrophe. That makes it authored knowledge, which is the same category as describe.

The identifier is the name. Renderers display a concept's own name, verbatim, everywhere structure is being shown — index, chips, headings, neighbourhood, sequences, breadcrumbs.

A title is additive, never a substitution. Where a title exists, it is surfaced as one more stated fact about the concept, labelled as such, alongside the name rather than in place of it. A reader always sees the identifier.

Renderers must not humanize identifiers. A consumer may not split camelCase or re-case a name to guess at a display form.

The clause in universe/prose.prose that offers title "to be used in place of rendering the raw concept name" is retired. The sentence before it — that a name's permitted character set cannot always carry the real title — stands, and is the whole justification for keeping the block.

Consequences

What this makes easy. The duplication worry becomes structurally impossible rather than merely unobserved: a string that never stands in for the identifier cannot be a second copy of it. It also makes the display-versus-graph test answerable for title without special pleading, which matters because that test is how future additions get judged.

What this costs. Readers see CarpentersWorkshop as the heading of that page, and "Carpenter's Workshop" only as a line beneath it. That is a real readability cost and it is accepted deliberately: the identifier is the thing that is stable across recompiles, citable in a URL, and greppable in the repo that the universe describes. A prettier heading that is not any of those is a worse anchor for a page.

What this complicates. label can no longer be a single derived display string. Search has to consider name and title as separate fields rather than ranking one merged label, and the universe and dimension records need the same additive treatment as concepts, which they do not have yet. label also becomes redundant with name for concepts while remaining meaningful for dimensions, where it still carries same-name disambiguation.

What to watch. title is currently prose's only string-valued slot, and it has been used as one — the field guide stores URL patterns in it. Additive rendering removes the incentive for that, because a title that no longer replaces a heading stops looking like a display tweak, but it does not remove the capability. If that use recurs, the answer is a scalar attribute feature, not a broader title.

Also watch whether the readability cost bites. If real universes end up with enough unspellable names that pages become hard to scan, the additive rule is the part to revisit, not the humanization ban.

Alternatives considered

Remove title entirely; the identifier is the only name. Rejected. Sluggification is lossy and irreversible, and prose would have no way to record an entity's actual name. The fixture universes already contain names that no identifier can hold: Carpenter's Workshop, E-bike (written Ebike), BBTool, Torquewrench, Lowwater. Refusing the feature does not make those names expressible; it makes them unrecorded.

Remove title and put humanization back in the UI. Rejected, and this is the option that looked most attractive at first. Humanization is a lossy inverse of sluggification. It cannot produce an apostrophe or a hyphen, so it does not solve the case title exists for. It also actively mangles the cases it does touch — BBTool becomes "Bb Tool", Lowwater becomes "Lowwater" where "Low Water" would be wrong anyway, and Ebike stays "Ebike". Worse, it launders real modelling mistakes: the freewheel fixture contains both CheckIn and Checkin, and both Deraileur and Derailleur, and a humanizing renderer makes those near-duplicates look more alike than they are, hiding a problem the author needs to see.

Keep title as a substitution — the status quo. Rejected. This is the one configuration in which the original worry is real. When a title replaces the name, an author who writes a prettified duplicate gets exactly the behaviour they wanted, nothing flags it, and the identifier disappears from the page that is supposed to be about it.

Keep title and also humanize — the original UI's behaviour. Rejected most firmly. Two systems both claim to produce the label, the author cannot tell which one is in charge for any given concept, and the answer depends on whether a title happens to be present. This is the worst of the four for exactly the reason title came under suspicion.

Remove title and add a general scalar attribute block. Deferred rather than rejected. It is the honest answer to the route-pattern usage, and it would let title be one attribute among several instead of a privileged one. It is also a much larger language addition than this decision needs, and it should be argued on its own merits with real demand behind it. Recorded here so the connection is not lost.

Evidence

Usage is small and unanimous. Sixteen titles exist across every compiled manifest in sprig, sprig-village, and sprig-bakeoff. Zero are camelCase expansions of their own identifier. The theory that title duplicates the identifier has no instances.

The field guide's own universe, at 165 concepts, has 15. Eleven are section names and one is a surface name, where the title drops a suffix the graph needs for uniqueness but a heading does not — ShapeSection titled "Shape", WhereToStartSection titled "Where to start". That is not prettification and no renderer can derive it; it is the author knowing which part of an identifier exists for the compiler's benefit.

The remaining three are not display names at all. The route table stores URL patterns: ConceptRoute is titled #/c/:id. This is the usage that prompted the scalar-attribute alternative above, and it was found only because the prose capture bug corrupts it — #/c/:id compiles to c/:id and OverviewRoute's #/ compiles to nothing at all, so the field guide currently claims its own overview route has no title. That is a separate parser defect, planned separately; it is cited here because it is the reason the abuse surfaced.

The one textbook case is in sprig-village and the bake-off's clockwork-village fixture: place CarpentersWorkshop { title { Carpenter's Workshop } }, one title in roughly 906 concepts. It is also the single strongest argument for keeping the feature, because it is precisely the case no algorithm can reach.

Every bake-off submission independently chose title || name and none humanized. Both contestants converged on reading the field when present and falling back to the bare identifier, and a search across all seven submissions for camelCase splitting finds nothing. The behaviour this record forbids is not one anyone was reaching for; the behaviour it forbids is the substitution, which everyone did reach for.