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.