SPEC-168
ID:SPEC-168Status:draft

Attribute presets for composed runes, and what the reuse measurement found

claude/site-changelog-readability-k4p902 View source
Branches 2
claude/site-changelog-readability-k4p902 current draft
claude/v041-plan-runes-keep-content draftmain draft
History 5
  1. f1b561c
    Created (draft)by bjornolofandersson
  2. 810ed9f
    Content editedby Claude
    Templates become {% template %} tags (SPEC-168 D9); one selecting attrib
  3. 7612767
    Content editedby Claude
    SPEC-168 D8: within one definition, reuse is a fragment
  4. fac0aa5
    Content editedby Claude
    SPEC-169: what the composition parser must accept, and what belongs else
  5. 17b56df
    Content editedby Claude
    SPEC-168: attribute presets for composed runes, and the reuse measuremen

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

ClusterRunesHow identical
Entity profilecharacter, realm, faction; lore is a subsetTemplates 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 documentspec, work, bug, decision, milestoneOne 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 entityevent, organizationContent models byte-identical (plugins/places/src/tags/event.ts:59-65, plugins/business/src/tags/organization.ts:74-80)
Split herohero, feature, step; cta without mediaOne tree, section[split] > media / --- / header + X, where X is actions, an arranged deflist or a body
Header plus child listcast, timeline, itineraryOne tree; extraction differs (list itemModel vs heading sections with headingExtract)
Each section as a titled regionpalette, ledger, comparison, the storytelling fixturesOne 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

GroupDeclared byEvidence
Split: media-position, media-ratio, valign, collapse8 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 YAMLThe default differs: top for card, bottom for hero, feature and step
Cover settings: content-place, height, aspectcard (packages/runes/src/tags/card.ts:62-82), hero (plugins/marketing/src/tags/hero.ts:39-56)Declared identically twice
Taxonomy: tagsstorytelling via taxonomyAttributes (plugins/storytelling/src/tags/common.ts:20-31); all five composed fixtures by hand; four plan runes
Plan identityall five plan runes: id (or name), status, created, modifiedstatus has a different value set in each rune (VALID_STATUS.work, .bug, …)
Page-section chrome defaultshero, cta, feature: density full, width full, elevation flush, content measure anchoredUniversal 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: pageSectionProperties has 21 call sites, and 17 of those runes rebuild the header from rendered nodes. A composition placing section gets the header from section'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 *-section runes) mostly disappear: in a composition they become each items placed into primitives.
  • Date prefixes split off headings by headingExtract regexes in timeline and itinerary-day (plugins/business/src/tags/timeline.ts:106, plugins/places/src/tags/itinerary.ts:106), and by hand in changelog, are SPEC-166's date kind. headingExtract could 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, from card and hero), and taxonomy (exists in storytelling; it moves to core because the plan runes use tags too).
  • 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 }
  1. Only default can be overridden. Type, values, required and 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.
  2. 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.
  3. Collisions are errors. An attribute declared both by a preset and in attributes, or by two presets, is reported at construction, naming both sources.
  4. presets is added to DEFINITION_KEYS. Native runes keep base: 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 %}