hecks

The language uses everything it declares, and what it does not use is a sub-language

Status: Accepted — implemented. The self-use gate landed as spec/self_use_spec.rb (S16), modelled on spec/fuzzing/meta_domain_coverage_spec.rb as this decision specifies. All three commissioned remodels landed too: Paging was extracted as its own attached sub-language (lib/hecks/language/bluebook/attaches/paging.bluebook); Keyword/Argument carry a real lifecycle :status with declared transitions in place of the old plain-attribute Status; Member, Dispatch, and Handler are genuine entities of ValueObject, Handler, and ProcessManager respectively (S14, S17). Verified directly against the live grammar: entity (5 real declarations), lifecycle/transition (2), and ensures (1) are now genuinely used by the language’s own definition. policy, process_manager, provenance, group_by, authorize, and generic remain honest, itemized SELF_USE_KNOWN_GAPS entries in the gate, each carrying a one-line reason rather than a silent exemption — per this decision’s own Consequences section, that is the intended steady state, not a shortfall. Depends on ADR 0025’s principle 4 (a word earns its place by being used) and on MetaValidator.defer, which landed in 97dfdf9 and made the language bootable as an ordinary domain in the first place.

Context

lib/hecks/language/bluebook/ declares what a bluebook IS. Until this week it could not be booted the way the bluebooks it describes are booted — nine files each say Hecks.bluebook "Bluebook", and each was judged before its siblings had been read. MetaValidator.defer fixed that, and booting it made a second question askable for the first time: does the language use the constructs it declares?

Measured, counting real declarations rather than grammar rows:

construct language banking
value_object 100 47
aggregate 14 10
one_of blocks 23 3
admits: 20 2
list_of 25 6
lifecycle / transition 0 / 0 13 / 37
entity 0 4
policy / process_manager 0 / 0 6 / 3
ensures 0 3
limit offset cursor nulls authorize group_by all 0 6/0/0/0/1/1

So the language is the richest domain in the repository for shape and uses none of the vocabulary for behaviour over time. That is not merely an omission, because in the two places the language genuinely has behaviour, it hand-rolled it:

It has a state machine and modelled it as an attribute. A word moves proposed → admitted → deprecated → retired (syntax.bluebook:127-142), declared as a value_object "Status" with a one_of, and read as attribute :status, String, default: "admitted", admits: "Syntax::Status". Nothing refuses proposed → retired skipping admitted, or retired → admitted resurrecting a dead word. And bin/model_check reports “Bluebook (the language itself) — clean — no dead states, no unreachable protocol steps” — clean because the language declares no states at all, not because its state machine was checked. The checker the language ships cannot see the language’s own state machine.

It has owned pieces and modelled them as references. Every containment in the language is a cross-aggregate reference:

Command -> aggregate_id    ValueObject -> aggregate_id    Member  -> value_object_id
Query   -> aggregate_id    Handler -> process_manager_id  Dispatch -> handler_id

A Member has no life outside its ValueObject; a Dispatch none outside its Handler. That is what entity is for, and entity is declared by the language and used zero times in it.

This is the same defect ADR 0025 catalogues throughout the DSL — a construct that exists, applies exactly here, and is not used — except here the domain declining to use it is the one that defines it.

Decision

The language uses everything the core grammar declares

A gate demands it, modelled directly on spec/fuzzing/meta_domain_coverage_spec.rb, which already reads MetaValidator.grammar_registry and requires every declared feature to be claimed, guaranteed by construction, structurally exempt, or a named gap carrying a one-line reason. The same shape, aimed at construct usage in the language’s own chapters.

What the language does not use is not core — it is a sub-language

The obvious way to make that gate survivable is an exemption list. This decision rejects that. Instead, anything the core language does not use is demoted out of the core grammar into a sub-language, with its own chapter, its own corpus, and its own use obligation.

That inverts the gate from something needing excuses into something that defines the boundary: the core language is exactly what the language itself uses, by construction rather than by assertion.

Sub-languages already exist and set the precedent — lib/hecks/grammar/expression.bluebook (“the subset of regular Ruby a predicate is allowed to be”) and translation.bluebook (“the declared bridge from one era of a domain’s storage shape to the next”) are self-contained chapters describing one bounded concern each, standing apart from the core grammar. This decision names the pattern and gives it a seam.

The sub-language declares where it attaches

Paging says it attaches to Query and ReadModel. The core grammar does not name its extension points, and adding a sub-language changes nothing in the core.

This is the additive choice, and additivity is the whole point: a sub-language that required editing the core grammar to attach would leave the core carrying a list of everything that might ever extend it — which is the same “declares what it does not use” defect this ADR exists to remove, one level up. A core that names Paging is a core that knows about paging.

The objection, and why it is survivable. Under this rule you cannot read Query in syntax.bluebook and know everything a query body admits; you must know which sub-languages are loaded. That is the “which file adds this?” problem, and it is real.

What makes it acceptable here is that nobody reads the grammar to answer that question — bin/reference generates docs/implemented/reference/query.md from the live grammar, and a generated page can enumerate the loaded sub-languages’ contributions as easily as the core’s. The authoritative answer to “what may appear in a query?” is a generated document either way, so the seam’s direction changes where the fact lives without changing where the answer is read. The obligation this creates is on bin/reference: a sub-language’s words must appear on the context page they attach to, marked as coming from that sub-language, or this trade is not paid for.

A related consequence, accepted: the set of legal words in a body becomes a function of what is loaded rather than a constant. Two projects loading different sub-languages accept different bluebooks. That is the intended behaviour — it is what makes a sub-language a language rather than a section — but it means a bluebook is only portable across projects that load the same sub-languages, and uses_framework is the existing precedent for declaring that kind of dependency explicitly.

The first demotion, and the two remodels

Consequences

Open, deliberately

Rejected alternatives