Slice 1 — bond as a composed rune
The first composed rune: SPEC-145's small worked example, the ~17-line definition that replaces plugins/storytelling/src/tags/bond.ts. The slice uses:
bond as a composed runeThe first composed rune: SPEC-145's small worked example, the ~17-line definition that replaces plugins/storytelling/src/tags/bond.ts. The slice uses:
17fec979a7c5e9bond is defined as a composed rune, with the definition stored as a .md file in the format SPEC-145's worked example shows, and fixtures of its ownbond registers the same edge as the plugin's — same from, to and kind — asserted against the storytelling registry snapshotbond's rendered tree is compared with the plugin's and every difference is recorded in the item's resolution, each with its reason (SPEC-147 D2)plugins/storytelling/ is unchanged and still passing its own tests (SPEC-147 D1)npm run seo:baseline:check and refrakt contracts --check report no drift on either contract copyCompleted: 2026-10-08
Branch: claude/v040-composed-bond PR: refrakt-md/refrakt#684
packages/content/test/fixtures/composed-storytelling/runes/bond.md: the composed bond definition, in SPEC-145's format. It places {% hint %} and uses {% if %} for the arrow and registers.edge. It has no schema.packages/content/test/fixtures/composed-storytelling/fixtures/bond.{canonical,one-way,defaults}.md: its own fixtures.packages/content/test/composed-bond.test.ts (20 tests). It uses a fixture plugin: storytelling as shipped, with bond swapped for { template } and the hand-written Bond theme entry dropped. It goes through composedPluginRune → mergePlugins → loadContent.@refrakt-md/storytelling (SPEC-147 D1). plugins/storytelling/ has no diff.packages/content/test/:plugins/storytelling/ would break "unchanged";plugins/* sibling would be read as a plugin package by sync-plugin-versions.mjs, plugin-manifests.test.ts and imperative-schema-migration.test.ts.bond reproduces the committed registry-snapshot.json exactly, compared as text. That covers every registration, the name index, the cross-links and the warnings.bidirectional, and getRelated returns exactly the edges in bond-relationships.json.registers.edge.kind: { field: type } fails construction. SPEC-144 requires kind.field to name a key of the edge's data bag (from, to, name, declared data). A test pins the error.
The example declares no bidirectional, so it defaults to false and the reverse edges are lost (getRelated('Aria') is empty). A test pins this.
To match the plugin's bag, the definition declares:
data: [{ bondType: type }, status, bidirectional];kind: { field: bondType };bidirectional: { field: bidirectional }.It also drops the example's type default (fellowship). The plugin has none: unset registers bondType: "" and the kind falls back to bond.
provides: [prose] is added. Without it, reading and dropcap are no longer accepted. With it, the attribute set equals the plugin's.
The definition is therefore 29 lines, not 17. SPEC-145's text is unchanged; the findings are recorded here and in the PR.
Same in both: data-rune="bond", data-status and data-density on the root. The placed hint's HTML equals a hand-written {% hint %} with the same content, apart from data-slot; data-field="content-section" and marker="**" are hint's own. No data-owner leaks.
div → aside: the definition's tag, from the example.rf-bond* class (D2/D2a). Lumina's 53-line bond.css stops matching; D16 contextModifiers is the route back.data-bond-type and no data-bidirectional. The generated config makes modifiers of matches attributes only; type is open and bidirectional is boolean.data-elevation="flat": the plugin's defaultElevation has no counterpart in a generated config. The hint brings sunken, and an author's elevation= on the root would double-paint (D14, open).from / connector / arrow / to data-names are gone, and the endpoints are <strong> text. The rune names nothing of its own (D3). The plugin's editHints have no counterpart: the definition has no editHints key, and the editor half (D11) is deferred.↔ / → glyph in the text ({% if %}), not a CSS-drawn empty span.div[data-name=body][data-section=body] is gone; its paragraph carries data-slot="body" (D10c).status is a closed set (matches in the example): status="estranged" is attribute-value-invalid where the plugin accepted it.{ field } names must be declared in data, in the snapshot's key order.character's entity block has no { field } references, so finding 1 does not recur there. But tags is in its data and is not a declared attribute in SPEC-145's character example; the snapshot carries tags: "".refrakt.stale check applied: no documented source file was changed.