Relationships
Related 18
Branches 3
Summary
Several runes have near-identical content models, templates or attribute groups. Before designing any reuse mechanism for compositions, every rune that is a composition candidate was measured: its current native content model and output, its planned composition, its attribute groups, and the imperative transforms it performs. This spec records the measurement and decides on its evidence.
- Attribute presets are justified, and are decided here (D1–D5). The same attribute groups are declared over and over, and composition frontmatter has no way to share them.
- A shared "shape" (content model plus template) is deferred (D6). Only three clusters of runes are truly identical, all first-party, and a plugin can already share a template without a new mechanism.
- The measurement found bigger blockers than reuse: content-model types and config features that compositions cannot express. They are listed for a separate spec (D7).
The measurement
About 45 runes across core and all nine plugins, each read from its current code (createContentModelSchema and its config entry) and from its planned composition in SPEC-145, SPEC-147–SPEC-157, SPEC-160–SPEC-167.
Whole shapes: content model and template together
| Cluster | Runes | How identical |
|---|---|---|
| Entity profile | character, realm, faction; lore is a subset | Templates byte-identical between realm and faction (packages/content/test/fixtures/composed-storytelling/runes/); content models identical apart from the image field's name (portrait vs scene) |
| Plan document | spec, work, bug, decision, milestone | One planned output tree (SPEC-152), varying only in the metadata field list. Two content models: work, bug, decision are sections at ## with per-rune knownSections (plugins/plan/src/tags/work.ts:94-131); spec and milestone are sequences (spec.ts:49-56, milestone.ts:44-53) |
| Page-section entity | event, organization | Content models byte-identical (plugins/places/src/tags/event.ts:59-65, plugins/business/src/tags/organization.ts:74-80) |
| Split hero | hero, feature, step; cta without media | One tree, section[split] > media / --- / header + X, where X is actions, an arranged deflist or a body |
| Header plus child list | cast, timeline, itinerary | One tree; extraction differs (list itemModel vs heading sections with headingExtract) |
| Each section as a titled region | palette, ledger, comparison, the storytelling fixtures | One pattern; the primitive placed per item differs every time |
Only the first three clusters are truly identical: about ten runes in three shapes. In the other three, what varies is exactly what a template exists to vary, so a shared shape would need parameters.
Attribute groups
| Group | Declared by | Evidence |
|---|---|---|
Split: media-position, media-ratio, valign, collapse | 8 native runes via base: SplitLayoutModel (card, hero, feature, step, recipe, playlist, realm, faction); partial inline copies in bento and bento-cell (plugins/marketing/src/tags/bento.ts:54-72, :356-376); both composed fixtures repeat all four in YAML | The default differs: top for card, bottom for hero, feature and step |
Cover settings: content-place, height, aspect | card (packages/runes/src/tags/card.ts:62-82), hero (plugins/marketing/src/tags/hero.ts:39-56) | Declared identically twice |
Taxonomy: tags | storytelling via taxonomyAttributes (plugins/storytelling/src/tags/common.ts:20-31); all five composed fixtures by hand; four plan runes | |
| Plan identity | all five plan runes: id (or name), status, created, modified | status has a different value set in each rune (VALID_STATUS.work, .bug, …) |
| Page-section chrome defaults | hero, cta, feature: density full, width full, elevation flush, content measure anchored | Universal attributes, not declared ones |
Why compositions repeat them. Composition frontmatter accepts only tag, aliases, description, attributes, content, schema, registers, metaFields, blocks and provides (packages/runes/src/lib/composition.ts:1014-1025). There is no base key, so the composed realm and faction declare the four split attributes in full, and pass each one through to card by hand.
What already reuses itself
- The page-section header (eyebrow, headline, blurb) is the most repeated pattern:
pageSectionPropertieshas 21 call sites, and 17 of those runes rebuild the header from rendered nodes. A composition placingsectiongets the header fromsection's own content model (packages/runes/src/tags/section.ts:35-43). No shape is needed; what blocks it is structured data crossing the section boundary (SPEC-146). - Small child runes (
timeline-entry,itinerary-stop,cast-member, the*-sectionrunes) mostly disappear: in a composition they becomeeachitems placed into primitives. - Date prefixes split off headings by
headingExtractregexes intimelineanditinerary-day(plugins/business/src/tags/timeline.ts:106,plugins/places/src/tags/itinerary.ts:106), and by hand inchangelog, are SPEC-166's date kind.headingExtractcould name a value kind instead of a regex; that is SPEC-166's to decide.
Decisions
D1 — a preset is a registered, named group of attribute declarations
The registry exists: registerAttributePreset(record, { name, description }) in packages/runes/src/attribute-presets.ts, used by core's split preset (packages/runes/src/tags/common.ts:49) and storytelling's taxonomy preset. It is keyed by the record object, and name is display text ("split layout"). A preset gains a stable id that a definition can name:
registerAttributePreset(splitLayoutAttributes, { id: 'split', name: 'split layout', description: 'Where the media zone sits and how the two zones share the row.', });
- Core and plugins define presets, in TS, as today. A site's
runes/directory can use them but not define them. A preset is a reviewed, shared declaration, not a local macro. - The initial set is what the measurement supports:
split(exists),cover(new:content-place,height,aspect, fromcardandhero), andtaxonomy(exists in storytelling; it moves to core because the plan runes usetagstoo). - Plan presets are decided when the plan runes convert (SPEC-152), under D4's rule.
D2 — a definition names its presets, and may override only a default
presets: [split, taxonomy] attributes: name: { type: string, required: true } type: { type: string } scale: { type: string }
A rune that needs a different default overrides only that, which the measurement shows is needed (hero's bottom against card's top):
presets: split: { media-position: bottom }
- Only
defaultcan be overridden. Type, values,requiredand descriptions stay the preset's. A rune that needs different values needs its own attribute, not a modified preset. This is what keeps one preset meaning one thing. - An override value must be one of the preset's values. Overriding a default with a value the preset does not accept is an error at construction.
- Collisions are errors. An attribute declared both by a preset and in
attributes, or by two presets, is reported at construction, naming both sources. presetsis added toDEFINITION_KEYS. Native runes keepbase:and are unaffected.
D3 — a template forwards a preset to the rune it places
Today's realm template:
{% card media-position=$attrs["media-position"] media-ratio=$attrs["media-ratio"] valign=$attrs.valign collapse=$attrs.collapse %}