V0.40.0
Name:v0.40.0Status:complete

v0.40.0 — The first composed rune

v0.38.0 and v0.39.0 built the prerequisites: the ownership marker, declarative registration, slot labelling and tree-order preprocess. This milestone uses them to ship the mechanism they were for. A rune defined as a Markdoc template placing other runes (SPEC-145), proved on two real runes from the storytelling plugin, with the packaging groundwork composed runes will need to be delivered (SPEC-153).

The decision pass that settled SPEC-145's blocking questions is done (D2a, D10a–c, D25, D26). What remains open in the spec does not block the two slices below.

Milestone burndown: 0 open work items remaining; peak 1, started Oct 801Oct 8Oct 11
Open work items Ideal burndown
Progress 10/10 work items
Related 21

Work Items

Done 10
WORK-619 main
The identity guard is path-granular
Implement SPEC-158. IDENTITY_FIELDS (packages/transform/src/identity-fields.ts) becomes a list of paths, and findReservedFields resolves a wildcard segment. This adds three protections:
high simple
10/10 criteria
WORK-620 main
A rune config may omit block
SPEC-145 D2a. A composed rune has no BEM block and ships no CSS, but it still needs an engine config: its modifiers, universal attributes and meta blocks have to render. Today RuneConfig.block is required. The engine builds ${prefix}-${config.block} unconditionally (packages/transform/src/engine.ts), so a config without one emits rf-undefined. A rune with no config at all leaks data-rune-fields into the HTML.Make block optional:
high moderate
4/4 criteria
WORK-621 main
data-owner and data-slot survive a primitive's transform
SPEC-145 D10a, landed ahead of composition so it can be proved inert.Markdoc keeps only the attributes a node's schema declares. A marker set on a node before a primitive transforms it is therefore dropped, unless every node and tag schema declares it. Declare data-owner and data-slot once, at config assembly, on every node and tag schema, never per rune. Teach the schema-table resolvers (packages/runes/src/lib/schema-table.ts) to admit a node past a boundary by data-owner plus data-slot. A primitive's own data-name on the same node is left alone.In the output, releaseOwnedNodes strips data-owner and keeps data-slot (D10a step 4, D10c).Nothing sets either attribute yet, so the gate is that nothing moves. The survival test runs the markers through every rune in D12's placement set. That set is "no peer schema, no requiresParent". Any rune that drops either attribute is a finding, named by the test.
high moderate
5/5 criteria
WORK-622 main
A rune can be defined by a composition template
The mechanism at the centre of SPEC-145. A definition is frontmatter (the input declaration: tag, attributes, content model, schema, registers) plus a Markdoc body (the output template). Slot names are the join between them.This item builds the definition → PluginRune path and the template's render. It covers only the vocabulary the two slices need:
high complex
17/17 criteria
WORK-623 main
A composition definition is checked at construction
Every rejection SPEC-145 decides at definition load or schema construction, in one place, so that a bad definition fails with its rune and the offending name rather than rendering silently wrong. Between them, D11 and D26 match every content-model field to exactly one slot.The bulk of this item is the error messages. Each check names the rune and the thing it rejected, and the tests assert the message as well as the throw.
high moderate
11/11 criteria
WORK-624 main
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:
high moderate
5/5 criteria
WORK-625 main
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:
high complex
8/8 criteria
WORK-626 main
Every plugin package exports its manifest and publishes what it declares
SPEC-153 implementation note 1: the packaging prerequisites everything else in that spec rests on. All three failures are invisible in the monorepo, so this step must not be assumed. Measured in SPEC-153:
medium simple
5/5 criteria
WORK-627 main
An npm pack → install → load harness for plugins
SPEC-153 implementation note 2. It is the only gate that catches the measured breakages, and nothing like it exists today. A monorepo test resolves packages through workspace links, so it passes whatever the tarball contains.The harness packs a plugin, installs the tarball into a temporary fixture project outside the workspace, and loads it through the same loadPlugin() path a site uses. The first target is the plugins as they ship today. That proves the harness on known-good input, and proves WORK-626's fix against the tarball rather than the workspace. Shipping composed runes in a tarball is a later SPEC-153 step that reuses this harness.
medium moderate
4/4 criteria
WORK-630 main
A composition template places a declared meta block with {% metablock %}
SPEC-145 D7. A composed rune has no layout, and the engine renders a declared blocks entry only through layout. So a composed rune can declare metaFields and blocks in full and get nothing, with no diagnostic.WORK-622 landed the generated config carrying both but left its "meta blocks render" criterion unchecked for this reason (#683). character's blocks.metadata (role and status as a definition list) needs it for WORK-625. Pulled into v0.40.0 on 2026-10-08 rather than dropping the block from the composed character. Placing a badge or deflist rune instead is the channel SPEC-145's authoring note rules out for attribute values.
high moderate
11/11 criteria