WORK-636
ID:WORK-636Status:done

realm and faction as composed runes

SPEC-147: realm (Place) and faction (Organization) share character's shape, sections plus a preamble, and each has a -section child rune that composition removes. Their media split maps to {% mediatext %}. Both follow WORK-625's pattern:

Priority:mediumComplexity:moderateMilestone:v0.41.0Source:SPEC-147

Criteria completion

Criteria completion: 7 of 7 (100%) checked; history from Oct 8 to Oct 80%25%50%75%100%Oct 8Oct 8
Branches 3
History 5
  1. a9a7dc6
    • ☑ `realm` and `faction` are each defined as composed runes with their schema rows, `sections` placed with `each`, and their declared meta blocks placed with `{% metablock %}`
    • ☑ Each one's JSON-LD is compared against its fixture's recorded output in `contracts/seo-baseline/baseline.json`, and every difference is explained (SPEC-147 D2)
    • ☑ Each registers the same entity, id, data and aliases as the plugin's rune, asserted against the storytelling registry snapshot
    • ☑ Content written alongside sections renders, and no `data-field="section"` is emitted (SPEC-147 D6)
    • ☑ The media split uses `{% mediatext %}`, or the reason it cannot is recorded
    • ☑ SPEC-145 D7's `realm` example matches the measured definition
    • ☑ `plugins/storytelling/` is unchanged and still passing its own tests (SPEC-147 D1)
    by bjornolofandersson
  2. 5f9b78c
    Content editedby Claude
    plan: WORK-636 done; criteria checked, pr set (#694)
  3. 644be66
    Content editedby Claude
    test(content): realm and faction as composed runes, against the SEO base
  4. c2186ab
    Created (ready)by bjornolofandersson
  5. fad3ced
    Content editedby Claude
    plan: draft v0.41.0 — Composed runes you can ship

Acceptance Criteria

  • realm and faction are each defined as composed runes with their schema rows, sections placed with each, and their declared meta blocks placed with {% metablock %}
  • Each one's JSON-LD is compared against its fixture's recorded output in contracts/seo-baseline/baseline.json, and every difference is explained (SPEC-147 D2)
  • Each registers the same entity, id, data and aliases as the plugin's rune, asserted against the storytelling registry snapshot
  • Content written alongside sections renders, and no data-field="section" is emitted (SPEC-147 D6)
  • The media split uses {% mediatext %}, or the reason it cannot is recorded
  • SPEC-145 D7's realm example matches the measured definition
  • plugins/storytelling/ is unchanged and still passing its own tests (SPEC-147 D1)

Resolution

Completed: 2026-10-08

Branch: claude/v041-composed-realm-faction PR: refrakt-md/refrakt#694

What was done

  • packages/content/test/fixtures/composed-storytelling/runes/{realm,faction}.md: the composed runes, beside bond and character. Each places:

    • {% card %}, with the scene in its media zone and the name as its title;
    • {% metablock name="metadata" /%};
    • the description, then a catch-all body;
    • each H2 section as {% details %}, through {% slot name="sections" each %}.

    The -section child runes are gone.

  • Schema rows: Place { name, type: additionalType, scene: image } and Organization { name, scene: image }.

  • Registered data: [{ realmType: type }, scale, tags, parent, name] and [{ factionType: type }, alignment, size, tags, name].

  • Aliases: [location, place] and [guild, order].

  • The SplitLayoutModel attributes are declared by hand and passed to card via $attrs["media-position"] and the rest.

  • Realm's type defaults to place, as the plugin's meta does.

  • Fixtures: {realm,faction}.{canonical,body-and-sections,defaults}.md.

  • packages/content/test/composed-realm-faction.test.ts (64 tests, plus a file snapshot of both trees). The JSON-LD is measured with the SEO baseline generator's own harvest().

  • composed-storytelling.ts: capture(pkg, dir?), the same edit as #690.

  • SPEC-145 D7: the realm example is replaced by the measured definition, with a note on each change. The marker prose now says card.

  • SPEC-147: Finding 6 gains a measured note; the table row and D2's parenthetical say card.

  • Not registered in @refrakt-md/storytelling (SPEC-147 D1); plugins/storytelling/ has no diff.

  • WORK-633 had not merged, so the definitions are loaded as strings. WORK-631 had not merged, so each keeps a catch-all body.

Against the baseline and the plugin

  • JSON-LD: both fixtures publish the recorded graph exactly, at both harvest points, including image. The one RDFa difference is that name is on a <meta>. Defaults match the plugin's graph.
  • Registry: for each rune, and with all four composed runes in one site, the snapshot's registrations, name index, types and warnings match exactly. Unset-attribute data bags match too, aliases register, and the nested King Edric still registers.
  • Links: realm's are unchanged. Faction loses King Edric -> /realms/aldermere, because its prose is in card (SPEC-147 Finding 4).

Rendered-tree differences (SPEC-147 D2), each asserted

Same:

  • <article>, data-rune, typeof, data-density, data-media-position (which also reaches card);
  • data-scale, data-alignment and data-size when set;
  • the metadata <dl>, apart from classes and the type row's data-field.

Different:

  1. No rf-realm* / rf-faction* classes. The metadata gets card's.
  2. data-type and data-field="type", not data-realm-type / data-faction-type and realmType, because modifiers are keyed by attribute.
  3. No data-tags or data-parent. An unset optional modifier is absent rather than "".
  4. No data-elevation="flat" and no data-field="content-section".
  5. The name is card's <h1 data-name="title">, which joins the page heading index (#690).
  6. Sections are details: no data-field="section" (D6), no child rune, no sections wrapper.
  7. Content alongside sections renders (Finding 6, D6).
  8. The scene is in card's media zone (img[data-slot=scene][property=image]). With no scene there is no zone.
  9. The description is <p data-slot="description">, not a body div.
  10. prominence and the ten frame-* attributes are not accepted. A composition declares no sections and no mediaSlots.

Findings

  1. The split maps to card, not mediatext, contrary to SPEC-147. mediatext cannot carry SplitLayoutModel (the baseline's media-position="start" has nowhere to go). It also leaves an empty media zone with no scene, and it moves description images into the media zone without their data-slot. The tests keep the mediatext form as evidence.
  2. The plugin drops a list written after the description, and everything after it, with or without sections. Finding 6 names only the sections case.
  3. SPEC-145 D7's example did not construct: it had no body field and no emitAttributes. It also used a realmType attribute that no page writes.

Notes

  • No changeset: no package change ships.
  • Contracts (both copies) and the SEO baseline are unchanged. npm test: 5596 passed.