Context
#298 proposed building vocabularies as unions of sub-vocabularies
(pets: union_of: [cats, dogs]). Behind it was a larger question: whether
Luria can express a hierarchical ontology at all.
It could not, as a vocabulary. A field’s groups: names a subset of its
values with a cardinality rule. That is one level deep, a group is not itself
a value anyone can tag with, and tagging with a member places a document
under nothing broader. A union would add composition (one field accepting a
cat or a dog). It would not add subsumption: “cat” is not a term, a Calico
note does not roll up under Cat, and nothing can say “exactly one value under
species”.
It could, as records. A TERM scheme with a broader/narrower converse
pair, a chain walking broader, and notes citing terms through a topic:
reference rendered the hierarchy as a tree, refused a citation of a term that
does not exist, and gave every term standing and history — a term can be
retired with a named successor, which a vocabulary value cannot. Only two
things were missing. Nothing in the index grouped notes under the terms they
cite, as a vocabulary field’s per-value pages did. And moving a vocabulary
there meant rewriting the config and every document by hand.
Decision
A vocabulary is a shorthand for a tiny constrained scheme, and stays one. Its values are named, labelled and described, and that is all. Vocabularies get no hierarchy, no unions and no relations of their own. A record that needs a value to carry more has outgrown the shorthand — a type error, in the sense that the thing being stored is not the kind of thing the field was typed as — and the repair is the long form.
promote_vocabulary is the long form, as a migration operation
(ADR-040), so it is planned, dry-run, committed and blame-ignored like a
scheme rename:
operations:
- op: promote_vocabulary
vocabulary: area
to: AREA- One document per value, numbered in the vocabulary’s order, then any value
in use that an open vocabulary never declared, sorted. The value’s
labelbecomes the title, itsblurbthe summary and body, and its old spelling is kept asslug:— declaredunique, because it was an identity. The scheme derives its alias from it (alias: AREA-{slug}, ADR-088); a value an alias tail cannot spell is slugged (long_term→long-term), and two values slugging alike, or a value that would read as a number, are refused. - Every field drawing from the vocabulary becomes a reference to the new
scheme with
group: true.requiredis written out, because the two defaults differ: a reference is required unless it says otherwise, a vocabulary field was optional unless it said so. - Documents now hold each term’s alias,
AREA-runtime: a typed reference that still reads as the value did. Only that frontmatter field is rewritten. Prose is never swept for a value’s spelling: a word in a sentence is not a citation of it. - The vocabulary is removed. Its own
labelandblurb, if it had them, become the scheme’stitleandblurb. The new directory gets the_template.mdandREADME.stubluria initwould have written, so the next term isluria new area.
What a reference cannot carry is refused, not dropped: a default, value
groups or primary_for, a derivation, an alert. A migration that silently
loses a constraint reports success over a record that now checks less
(DP-1). A vocabulary behind a status field is refused too: standing is read
off those words, and they are not documents.
A reference can be grouped like a vocabulary field. group: true on a
reference, or naming it as the scheme’s axis, gives each target document a
page under <view>/<field>/<CODE>.md listing the documents that cite it,
titled and described by the target itself and linked to it. A target nobody
cites still gets its page, as a declared value nobody uses does; a code that
resolves to nothing gets none, as a closed vocabulary’s unknown value gets
none. Off by default, because most relations (extends, superseded_by)
are not taxonomies. This is what makes promotion lossless: without it, the
move a record should make would cost it its views, and the shorthand would
win by default.
A reference field accepts a derived alias. ADR-088 kept a derived
alias as a first-class spelling in prose, but a reference field still
demanded the code, so area: AREA-runtime was “not a code”. That would have
forced promotion to trade every interpretable value for an opaque number.
Now one resolution, aliases.derived_code, serves every reader of a
reference value: the lint, the index’s grouping, typed edges, relation and
chain walks, invariants and luria relate. A tool writing a reference (the
converse fixer) writes the alias where the target scheme declares one, and
the fixer leaves an alias as written. A formerly: spelling is not accepted
in a reference field: it is a past the fixer rewrites, not a name to keep.
A hierarchy, then, is a scheme of terms with a broader relation (and its
converse), cited through a grouped reference and drawn by a chain.
Alternatives considered
- Union vocabularies (#298). Composition without subsumption: the sub-vocabularies’ names never become terms, so the hierarchy has no inner nodes anyone can tag with or query by. Worth having only for reusing independently maintained lists, which is a different need and not this one.
- A
broader:parent pointer on each vocabulary value. The general hierarchy primitive, and the one closest to SKOS. Rejected because it rebuilds the scheme machinery — identity, relations, converses, chains — inside the config, second-class: an ontology change would be a config edit with no standing, no history and no successor, which is exactly what a record exists to give a claim. A term that matters enough to sit in a hierarchy matters enough to be a document. - Union plus a
specializesrelation (catsspecializespets.cat). Gets most of the way, but attaches a whole list under one term, so every level of depth needs another named vocabulary — and it inherits the previous alternative’s second-class standing. - A converse to list membership on each term. No index change: promotion
would declare
area↔rfcsandluria link --fixwould write each member’s code into its term. Rejected because every tagged document would then edit its term’s file, making popular terms merge-conflict hot spots — the opposite of DP-2’s one artifact, one writer. - Promotion without reference grouping. Smaller, and it makes the right move cost a record its per-value pages. A mechanism that punishes the repair teaches people not to make it.
- Deriving the alias from the title. Titles are labels: they carry
spaces and capitals, and may differ from the value outright (
Alabelled “The unbroken line”). An alias from the title would change every value documents already hold. The slug is the value, so the alias keeps it. - A
luria promotecommand. It rewrites the config and documents mechanically, wants a dry run and a blame-ignore entry, and its spec is an audit trail worth keeping: that is a migration, and ADR-040 already describes one.
Consequences
- #298 is answered by this rather than implemented.
- Promotion is one-way. Demoting a scheme to a vocabulary would discard every term’s standing, history and relations — the things that made it a scheme.
- A promoted field used as a chain’s
facet_byrenders codes, not titles. - The
luria new migrationscaffold is YAML now. It had stayed TOML after ADR-098, andluria migratereads specs withyaml.safe_load. - A field that stops being grouped still leaves its old per-value pages
behind with nothing reporting them. Promotion keeps the field grouped, so
luria indexreplaces them, but the general gap remains. - Not done here: invariants that hold at an ancestor term (a chain line of Tabby and Calico notes bound at Cat), and index pages that roll a term’s narrower terms up into it. Both read the hierarchy this makes expressible; neither is needed to express it.