WORK-614
ID:WORK-614Status:done

Add the slot declaration and generate a rune's transform from it

SPEC-143's mechanism, without migrating any rune. That happens in WORK-616 and WORK-617.

createContentModelSchema gains an emits declaration carrying the renderable's identity (rune, tag, property), its properties in SPEC-140's data form (fieldMetas, which already takes no function values), and its slots:

Priority:highComplexity:complexMilestone:v0.39.0Source:SPEC-143
claude/v039-slot-labelling View source

Criteria completion

Criteria completion: 16 of 16 (100%) checked; history from Oct 7 to Oct 70%25%50%75%100%Oct 7Oct 7
Branches 3
History 2
  1. 9d1c2a9
    • ☑ A rune can declare its content slots: source field, slot name, wrapper element, and omit-when-empty
    • ☑ A slot whose name equals its field name needs no declaration beyond being listed
    • ☑ A slot declares whether it is a `value` or a `region`; a region emits a boundary element defaulting to `div`, and only a non-default element is stated
    • ☑ A region's children carry no `data-name`, so neither `layout` nor `projection` can address inside an authored body
    • ☑ The declaration cannot express nesting, ordering or container creation (D2), and a declaration attempting it is rejected rather than silently partially applied
    • ☑ Both section arrival modes work, read from the content model's `emitTag` rather than declared again (D5)
    • ☑ The declaration carries the renderable's identity — `rune`, `tag` and `property` — since `createComponentRenderable` is no longer called from a transform
    • ☑ No declaration can produce two nodes carrying the same `data-name`; one that would is rejected at schema construction, naming the slot
    • ☑ The same one-node-per-`data-name` assertion is added to the structure contract, so it covers hand-written transforms too
    • ☑ Declaring both `transform` and the slot declaration on one rune is rejected at schema construction, naming the rune (D8)
    • ☑ Declaring neither is rejected the same way
    • ☑ The slot declaration contains no function values, and a declaration carrying one is rejected at schema construction (D9)
    • ☑ The generated transform is invoked at the existing call site, with no second code path through `createContentModelSchema`
    • ☑ The slot declaration is sufficient to derive the rune's `sections` config entry, so the identity half of `RuneConfig` is not authored a second time alongside it
    • ☑ `refrakt inspect` and the generated reference describe a declaratively-labelled rune at least as completely as a transform-built one
    • ☑ The rune authoring guide documents the slot declaration and the family test (D4) as the way to decide whether a new rune needs a transform
    by bjornolofandersson
  2. 44bd72d
    Created (in-progress)by bjornolofandersson

Acceptance Criteria

  • A rune can declare its content slots: source field, slot name, wrapper element, and omit-when-empty
  • A slot whose name equals its field name needs no declaration beyond being listed
  • A slot declares whether it is a value or a region; a region emits a boundary element defaulting to div, and only a non-default element is stated
  • A region's children carry no data-name, so neither layout nor projection can address inside an authored body
  • The declaration cannot express nesting, ordering or container creation (D2), and a declaration attempting it is rejected rather than silently partially applied
  • Both section arrival modes work, read from the content model's emitTag rather than declared again (D5)
  • The declaration carries the renderable's identity — rune, tag and property — since createComponentRenderable is no longer called from a transform
  • No declaration can produce two nodes carrying the same data-name; one that would is rejected at schema construction, naming the slot
  • The same one-node-per-data-name assertion is added to the structure contract, so it covers hand-written transforms too
  • Declaring both transform and the slot declaration on one rune is rejected at schema construction, naming the rune (D8)
  • Declaring neither is rejected the same way
  • The slot declaration contains no function values, and a declaration carrying one is rejected at schema construction (D9)
  • The generated transform is invoked at the existing call site, with no second code path through createContentModelSchema
  • The slot declaration is sufficient to derive the rune's sections config entry, so the identity half of RuneConfig is not authored a second time alongside it
  • refrakt inspect and the generated reference describe a declaratively-labelled rune at least as completely as a transform-built one
  • The rune authoring guide documents the slot declaration and the family test (D4) as the way to decide whether a new rune needs a transform

References

  • SPEC-143 — mechanism, D1–D9
  • SPEC-081 — the constraints carried forward (flat bag, semantic IR)
  • SPEC-140 — fieldMetas, the properties channel (D6)

Resolution

Completed: 2026-10-07

Branch: claude/v039-slot-labelling PR: refrakt-md/refrakt#668

What was done

  • packages/runes/src/lib/slots.ts (new): EmitsDeclaration / SlotDeclaration types, validateEmits, makeSlotTransform (a closure built once at construction), slotSections (derives the sections join table), describeSlots / formatSlotLine for the review surfaces.
  • packages/runes/src/lib/index.ts: transform is optional and emits added. D8 errors for both/neither. const transform = options.transform ?? makeSlotTransform(…) at the single call site. sections is derived from the slots (a sections option may add layout-created names but not restate a slot). The declaration is recorded on a new schemaEmits WeakMap.
  • packages/runes/src/lib/resolver.ts: selectStructuralModel picks the conditional branch the resolver took, so D5 reads emitTag from the right model.
  • Rejected at construction, naming rune and slot: function values / undefined / class instances (D9); nesting, ordering and container keys, and a compound el (D2); a value slot from a greedy field or from sections (one data-name on several nodes); a slot that shares a name with a property; a from naming no field; custom/delimited models.
  • packages/lumina/test/data-name-uniqueness.test.ts: the one-node-per-data-name assertion over the whole fixture corpus (core fixtures, every plugin rune fixture, the SEO fixtures), beside the structure contract.
  • refrakt inspect gets a "Slots (declared)" section and a slots JSON field (null for transform-built runes). refrakt reference gets an "Output slots" section and an emits JSON field.
  • Docs: output-contract.md documents the declaration and the D4 family test; authoring-overview.md points to it. The CLI docs for inspect and reference are updated.
  • Tests: packages/runes/test/slots.test.ts (24), including byte-identity against an equivalent hand-written transform.

Notes

  • Existing violations, reported rather than hidden. 17 runes break one-node-per-data-name today. They are recorded in a shrink-only KNOWN_DUPLICATES list (a stale entry fails the test).
    • Four repeat a name in the root slot bag that layout reads: deflist row, palette group, plan-progress group, spacing section.
    • The rest name repeated item elements (recipe ingredient/step, diff line, grid cell, …).
    • The structure contract is config-derived and cannot see transform output, so the assertion is a corpus test beside it rather than a contract field.
  • Resolved section entries render as buildSections did. Each entry gets <section data-name=slug>; a known section's heading gets data-known-section; top-level hr is dropped. Those per-entry slugs are each section's own identity. The mechanism never names a region's children.
  • Attribute values (from: 'attrs.x', as: 'value') render as text in el (default span). The spec did not spell this out, but the storytelling sub-runes' name needs it.
  • Follow-ups not filed (to avoid ID collisions):
    • decide what to do about the 17 known duplicates;
    • SPEC-143's own AC list still names character, realm and faction (see WORK-617).