v0.46.0 · The theme contract
A theme renders a vocabulary and configures no rune (A theme renders a vocabulary; it configures no rune); a plugin rune speaks the same vocabulary, and only its novel parts bring their own structure, in tokens (Plugin runes speak the vocabulary; novel parts bring token-only structure). Both decisions were accepted on 2026-10-28. This milestone builds what makes them checkable.
Changes
- A composition's narrative field is checked when its definition loads (ADR-047). A content-model field whose
matchnames only the editorial runes (tag:textblock,tag:mediatext) is the rune's narrative field: the story a template can place between the rune's own parts. Two rules are now enforced:- One per rune. A definition with two narrative fields is rejected (
narrative-field-twice). - Never structured data. A schema table that maps the narrative field is rejected (
narrative-field-mapped).
- One per rune. A definition with two narrative fields is rejected (
- A field that mixes editorial runes with other matches is not a narrative field.
refrakt inspectandrefrakt referencename the narrative field. The rune-authoring patterns guide and the composed-runes guide document it. - Two checks make ADR-043's and ADR-044's contracts checkable. Both are reports, not gates.
refrakt theme coveragereports, for every value of the theme vocabulary, whether a theme's CSS styles it, leaves it to the base stylesheet (@refrakt-md/skeleton), or nothing does. The vocabulary is each meta type, sentiment, zone layout, sequence, section role, media kind and universal-axis value. It is read from the registries that define it (themeVocabulary()in@refrakt-md/runes;META_TYPES,SENTIMENTS,LAYOUT_PRIMITIVESandvocabularyCoverage()in@refrakt-md/transform), so a new value appears without updating anything. Lumina runs it as a test, with its unstyled values recorded as a set that may only shrink.refrakt plugins validatenow reports, for a plugin's themed runes, the elements that carry no vocabulary role (ADR-044's tier 3), and the literal colours, sizes and font stacks in itsstyles/stylesheets (novelElements(),lintTokenOnly()). A plugin whose stylesheets have none is theme-portable.- Baseline:
npm run plugins:vocabularyrecords both for the nine first-party plugins incontracts/plugin-vocabulary.json. This is the baseline Lumina's per-rune CSS drains from.
- The engine no longer adds
rf-badgeto field values (ADR-043 decision 3). A value states what it is (data-meta-type, anddata-meta-sentimentwhen its field maps one), and the base stylesheet drawsstatus,categoryandtagvalues as pills by that attribute. Every value is now one element: a definition-list<dd>holds a<span data-meta-type>for every kind, where a bare value used to put the type on the<dd>itself.{% badge %}keeps itsrf-badgeclass, and the pill rule covers it. Lumina renders every page unchanged. - For theme authors: a rule on
.rf-badgenow reaches only{% badge %}. Style field pills with[data-meta-type="status" | "category" | "tag"], and a definition-list value withdd > [data-meta-type]rather thandd[data-meta-type]. - All of
metaFieldsis identity (ADR-043 decision 2). A theme that overrides any part of a rune's meta fields (metaType,label,sentimentMaportransform) has the override reported and dropped, as it already was formetaTypealone. A label is copy, a sentiment map says what a value means, and a transform says how it is formatted: all are claims about the content, not its look. No first-party theme overrides them. - For third-party themes: move a label, sentiment or format change into the rune's definition (or the plugin), or style the field through
[data-field="…"],[data-meta-type="…"]and[data-meta-sentiment="…"].blocks, which fields are grouped and how they are arranged, stays the theme's.