WORK-625
ID:WORK-625Status:done

Slice 2 — character as a composed rune, against the SEO baseline

SPEC-145's full worked example and its spike criterion. It is the first composed rune with a schema row: Person, which a first-party definition may declare under D25. It is also the first that uses sections with each and $each.heading, and a preamble with an image slot.

character is the case WORK-617 was cancelled for. Its transform failed SPEC-143's D4 family test, so it is replaced rather than declared. The composed version drops the character-section child rune entirely.

Like WORK-624, it ships beside the plugin (SPEC-147 D1). The reference is the storytelling character fixture in contracts/seo-baseline/. The gate is that every difference from it is explained, not that there are none (SPEC-147 D2). Two differences are already decided:

Priority:highComplexity:complexMilestone:v0.40.0Source:SPEC-145,SPEC-147

Criteria completion

Criteria completion: 8 of 8 (100%) checked; history from Oct 8 to Oct 80%25%50%75%100%Oct 8Oct 8
Branches 3
History 6
  1. 4bf1f1d
    • ☑ `character` is defined as a composed rune with a `Person` schema row, a `sections` slot placed with `each`, and the portrait and description preamble slots
    • ☑ Its JSON-LD is compared against the `character` fixture's recorded output in `contracts/seo-baseline/baseline.json`, and every difference is explained in writing (SPEC-147 D2)
    • ☑ One storytelling rune is reimplemented as a composed rune in a spike, and the emitted tree is compared against today's — not necessarily identical, but every difference explained
    • ☑ A composed rune's content-derived `properties` resolve — with `image: portrait` added to the row, the `image` from a slot-placed portrait reaches the entity, asserted against the graph and not just the attributes; the added property is an explained difference from the baseline (D10)
    • ☑ A composed `character` with both sections and other body content renders that content, and the plugin's discarding of it is recorded as an explained difference (SPEC-147 Finding 6, D6)
    • ☑ The composed `character` emits no `data-field="section"` on its sections (SPEC-147 D6)
    • ☑ The composed `character` registers the same entity, id, data and aliases as the plugin's, asserted against the storytelling registry snapshot
    • ☑ `plugins/storytelling/` is unchanged and still passing its own tests (SPEC-147 D1)
    by bjornolofandersson
  2. 2b16b24
    Content editedby Claude
    plan: WORK-625 done; criteria checked, pr set (#686)
  3. 5ca3432
    Content editedby Claude
    test(content): character as a composed rune, against the SEO baseline (W
  4. b061c7a
    Content editedby Claude
    plan: pull {% metablock %} into v0.40.0 as WORK-630
  5. 481b6b4
    Created (ready)by bjornolofandersson
  6. b3823f6
    Content editedby Claude
    plan: draft v0.40.0 — The first composed rune

Blocked by

  • WORK-624
  • WORK-630

Acceptance Criteria

  • character is defined as a composed rune with a Person schema row, a sections slot placed with each, and the portrait and description preamble slots
  • Its JSON-LD is compared against the character fixture's recorded output in contracts/seo-baseline/baseline.json, and every difference is explained in writing (SPEC-147 D2)
  • One storytelling rune is reimplemented as a composed rune in a spike, and the emitted tree is compared against today's — not necessarily identical, but every difference explained
  • A composed rune's content-derived properties resolve — with image: portrait added to the row, the image from a slot-placed portrait reaches the entity, asserted against the graph and not just the attributes; the added property is an explained difference from the baseline (D10)
  • A composed character with both sections and other body content renders that content, and the plugin's discarding of it is recorded as an explained difference (SPEC-147 Finding 6, D6)
  • The composed character emits no data-field="section" on its sections (SPEC-147 D6)
  • The composed character registers the same entity, id, data and aliases as the plugin's, asserted against the storytelling registry snapshot
  • plugins/storytelling/ is unchanged and still passing its own tests (SPEC-147 D1)

Resolution

Completed: 2026-10-08

Branch: claude/v040-composed-character (stacked on claude/v040-metablock, #685) PR: refrakt-md/refrakt#686

What was done

  • packages/content/test/fixtures/composed-storytelling/runes/character.md: the composed character, beside bond. It places {% card %} with the portrait in card's media zone, the name as card's title, {% metablock name="metadata" /%} for role and status, the description, a catch-all body, and each H2 section as {% details %} through {% slot name="sections" each %}. The character-section child rune is gone. Its schema row is Person { name, role: jobTitle, portrait: image }.
  • Fixtures: character.{canonical,portrait,body-and-sections}.md.
  • packages/content/test/composed-character.test.ts (27 tests). The JSON-LD comparison uses the SEO baseline generator's own harvest().
  • packages/content/test/composed-storytelling.ts: the fixture-plugin machinery, now shared with composed-bond.test.ts.
  • SPEC-145: the character worked example is replaced by the measured definition, with a note on each change. The schema-bearing section records that image now resolves.
  • SPEC-147 Finding 4: the cross-link constraint below.
  • Not registered in @refrakt-md/storytelling (SPEC-147 D1); plugins/storytelling/ has no diff.

Against the baseline and the plugin

  • JSON-LD: the character fixture publishes the recorded graph exactly, both jsonLd and rendered.jsonLd. The one RDFa difference: name is on a <meta>, not the title <span>. The title is template text, so the value rides the field bag.
  • image: portrait: "image": "veshra.jpg" at both harvest points (D10). The plugin's row maps no portrait, which is the explained difference from the baseline.
  • Registry: the snapshot's registrations, name index, types and warnings match exactly. Its links differ (finding 4).

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

Same: data-rune, typeof, data-role, data-status, data-density, the root <article>, and the metadata <dl> apart from classes.

  1. No rf-character* class (D2/D2a); Lumina's character.css stops matching.
  2. No data-aliases / data-tags. A generated config makes modifiers of enum attributes and of attributes a metaFields entry reads only. Registration is unaffected.
  3. No data-elevation="flat" (plugin defaultElevation) and no data-field="content-section" (the plugin's property).
  4. The metadata list has rf-card__metadata / rf-card__row classes: card's BEM pass reaches it.
  5. The name is card's <h1 data-name="title"> (with an id), not a <span data-name="name"> in a preamble header.
  6. Sections are details with content marked data-slot="sections", not character-section runes, and there is no sections wrapper.
  7. No data-field="section" (D6).
  8. Content alongside sections renders; the plugin drops it (Finding 6, D6).
  9. The description is a <p data-slot="description">, not a body div with data-section.
  10. The portrait is in card's media zone (<img data-slot="portrait" property="image">), not a floated root div[data-name=portrait][data-media=portrait]. With no portrait there is no zone (D17).

Findings

  1. SPEC-145's example did not construct: $each.heading needs emitAttributes (D26). It also missed tags (base is not a definition key) and aliases: [npc, pc] (an {% npc %} page registered nothing), and it used badges, which its own authoring note rules out. All are corrected in the spec.
  2. D11 gap: content that no preamble field matches is dropped silently. D11 checks that fields are placed, not that every node is matched. The stored definition adds a catch-all body, and a test pins the silent drop. Recorded in SPEC-145, not fixed here.
  3. image resolves through the slot, in card's body or its media zone (after extractMediaImage).
  4. The cross-links inside character pages are lost: the plugin's auto-linking skips nested runes, and the prose now sits inside card. Recorded in SPEC-147 Finding 4 as a constraint on D5.

Notes

  • No changeset: no package change ships. SPEC-147 D6's changeset note belongs to the change that ships composed runes.
  • Contracts (both copies) and the SEO baseline are unchanged. npm test: 5532 passed.