Relationships
Related 11
Branches 2
Summary
Fourth plugin audited against SPEC-145, and the first that comes back no. Design is 1,958 lines across seven runes, and it should stay a plugin: two runes compose cleanly, two more could with SPEC-147's segmented model, one is a misfiled documentation tool, and the remaining one is uncomposable by nature rather than by any gap the roadmap closes.
That the answer is sometimes no is the point of running the audit, and this is the result that establishes it. The previous three all resolved to retire, dissolve or shed a rune; this one does not.
It also produces a blocker category the first three did not, and shows the per-property method has a precondition.
The five-path method does not apply here
No design rune declares a schema table. Not one of the seven, confirmed by inspection and by their absence from contracts/seo-baseline/fixtures/ — the corpus covers 30 emitting runes and none of them is a design rune.
So SPEC-148's five resolution paths and SPEC-149's B-intrinsic / B-by-choice split have nothing to bite on. That is worth stating rather than passing over: the per-property method has a precondition — that the rune makes a structured-data claim at all. Where it does not, composability is decided entirely by content model, behaviour and pipeline, and the audit is a different shape.
The audit
| Rune | Lines | Verdict |
|---|---|---|
swatch | 76 | Composes today. sequence with no fields; renders a chip from attributes alone |
mockup | 193 | Composes today. sequence body + a named viewport wrapper; its own comment notes the device chrome is data-* attributes "not in a postTransform", so it already follows the declarative convention |
palette | 309 | Needs SPEC-147's segmented model (two groupByHeading sites); the coupling below is corrected and does not block it |
spacing | 270 | Same — two groupByHeading sites, same correction |
typography | 284 | No heading grouping; composes, gated on nothing |
preview | 237 | Triply blocked, and misfiled — see below |
design-context | 99 | Uncomposable by nature — see below |
The new blocker — output that is derived data, not arranged content
design-context dispatches on its children's rune names and calls extractPaletteTokens, extractTypographyTokens and extractSpacingTokens, folding the result into a DesignTokens blob it emits as a field. Its job is not to arrange its children; it is to read them and compute a value.
Composition places content. It has no facility for computing over content, and ADR-036 is the reason it should not grow one — a general "read my children and derive a value" capability is an expression language by another name.
So this is a fourth blocker category, distinct from the three the earlier audits found (a peer schema type, a required parent, a client lifecycle):
A rune whose output is derived data rather than arranged content cannot be composed, and should not be.
Worth naming because it is not obvious from the outside: design-context looks like a container, is described in the catalog as "composing palette, typography, and spacing runes", and is the plugin's least composable rune.
Corrected: it does not constrain its children
This section claimed a coupling that does not exist, and the correction frees three runes. It read: "the extractors walk palette / typography / spacing's output trees… composing them changes that structure and breaks the extraction silently", and called it "the first cross-rune output coupling any audit has turned up". Read, the stage is wrong — and with the stage goes the conclusion.
design-context's transform extracts before its children are transformed, and says so in its own comment:
const children = asNodes(resolved.body) as Node[]; // Extract tokens from child AST nodes before transforming for (const child of children) { if (child.type === 'tag') { const tagName = (child as any).tag; if (tagName === 'palette') tokens.colors = extractPaletteTokens(child);
And the extractors consume AST node types, not emitted tags — extractPaletteTokens(node: Node) walks child.type === 'heading', child.type === 'list', item.type === 'item' (tags/palette.ts:276).
So what design-context depends on is the authored markdown shape inside {% palette %} — headings and lists — which is the content model's input, not any rune's output. Composition changes what a rune emits; it cannot change what the author wrote. The extraction never sees palette's output and is therefore untouched.
Consequences, in order of how much they matter:
palette,spacingandtypographyare composable in place, not merely in isolation.typographyis gated on nothing at all; the other two need only SPEC-147'ssegmentedmodel, as the table says.- The "cross-rune output coupling" category is withdrawn. No audit has found one, and recording a blocker category on a single misread instance is the error ADR-039 rule 2 had to retract at larger scale.
- The fourth blocker category survives intact.
design-contextstill computes a value from its children rather than arranging them, and is still uncomposable. What it does not do is propagate that property to its children.
The surviving dependency is weaker and worth stating so it is not rediscovered as a blocker: design-context dispatches on the child's tag name, so a child must still be a tag named palette / typography / spacing. A composed palette is still a rune named palette, so even that holds.
preview — triply blocked, and in the wrong plugin
Three independent blockers, any one sufficient:
- A custom content model — the plugin's only
type: 'custom'. - Behaviour-driven — listed among the behaviour runes in
packages/svelte/src/registry.ts(tabs, accordion, datatable, form, reveal, preview, details). - A
postTransformthat serialises HTML.plugins/design/src/config.ts:95callsrenderToHtml(contentChildren, { pretty: true })to build a themed-source view, and its comment states why it cannot move: "This must happen in postTransform (not the rune) because it needs the fully-transformed tree with BEM classes and structural elements." Unlikeplot'spostTransform(SPEC-147 Finding 1, which only sets a data attribute) there is no plausible declarative form for rendering a tree to a string.
And it is misfiled. The catalog describes it as "Component preview with theme toggle and adjustable width for documentation" — a docs tool, not a design-domain rune, and adjacent to sandbox, which lives in core.
Third data point for the misfiled-capability pattern, and the second where the misfiled rune is behaviour-driven (map was the first). storytelling shed storyboard, places dissolved around map, business had none, design has preview.
The pipeline — half declarable, and the other half correctly imperative
| Hook | What it does | Declarable |
|---|---|---|
register | Finds design-context runes, reads their tokens field, registers by scope | Yes — SPEC-144's entity with idFrom: scope |
aggregate | Collects contexts into a scope → DesignTokens map | Yes — generic |
postProcess | Finds every sandbox, reads its context attribute, injects a design-tokens meta into that sandbox's children | No |
The postProcess is a different animal from storytelling's. That one was generic entity auto-linking, which SPEC-147 Finding 4 promotes to core because nothing in it knew about stories. This one is a specific wiring between two named runes — design-context feeding sandbox — and sandbox is a client-lifecycle rune composition can never produce. There is nothing generic to promote; it is plugin logic doing exactly what plugin logic is for.
That is a useful contrast to record: an imperative postProcess is a candidate for promotion when it is domain-agnostic, and correct as it stands when it wires two named runes together.
Verdict — design stays a plugin
Not "retires later" or "dissolves". Of its seven runes:
swatchandmockupcould compose, and gain little from it — they are small and already declarativepalette,spacingandtypographycompose in place — the output coupling this audit first recorded was a misread of the extraction stage, corrected abovepreviewbelongs elsewhere and cannot compose regardlessdesign-contextcannot compose and should not
Removing preview would leave a coherent plugin whose core is a computation over its own runes' authored content, wired to an interactive core rune. That is precisely what the plugin mechanism exists for, and ADR-036 already names it as the escape hatch for anything composition cannot express.
Decisions
D1 — design is not a composition target
Recorded so it is not re-examined every time the composition vocabulary grows. The blocker is design-context's nature, not a missing feature, and the three display runes are coupled to it.
D2 — a rune whose output is derived data cannot be composed
The fourth blocker category, alongside a peer schema type (SPEC-145 D9), a required parent (D12) and a client lifecycle (SPEC-148 D3). Added to the authoring guide's list of what disqualifies a rune, because design-context demonstrates it is not visible from the rune's description.
D3 — preview moves out of design
To core beside sandbox, or to the docs plugin. It is a documentation tool by its own description, and its three blockers are independent of where it lives. Relocation does not require composition to exist.
D4 — an imperative postProcess is judged by whether it is domain-agnostic
storytelling's auto-linking is promoted to core (SPEC-147 Finding 4); design's sandbox-token injection stays plugin code. The test is whether the hook names specific runes, not whether it is imperative.
D5 — the per-property method applies only where a rune emits schema
Stated as a precondition. Design's audit is decided by content model, behaviour and pipeline alone, and a future audit should check for schema tables before reaching for the five-path table.
Implementation notes, deliberately not yet work items
- Relocate
previewto core besidesandbox, or to docs. Independent of everything else here, and the only item with a clear payoff. - Leave the rest. No composition work is proposed for design. If
palette,spacingortypographyare ever composed,design-context's extractors must move to reading a declared field rather than walking output trees — a prerequisite, not a side effect.
Non-goals
- Composing any design rune; the plugin stays (D1)
- Promoting design's
postProcessto core (D4) - Changing
design-context's token extraction; it is cited as correct for what it does - Auditing the remaining plugins (
learning,media,docs,marketing)
Acceptance Criteria
previewrenders from its new home with its existing fixtures unchanged, including the themed-sourcepostTransform(D3)- The authoring guide lists all four disqualifying properties, with
design-contextnamed as the example for derived-data output (D2) - The authoring guide records the per-property method's precondition (D5)
plugins/design/still builds and passes its tests afterpreviewleaves- If any display rune is later composed,
design-contextreads a declared field rather than walking its children's output, asserted by a test that changes the child's structure and expects the tokens to survive
References
- SPEC-147 — the storytelling audit; the
segmentedmodelpaletteandspacingwould need, and the promotablepostProcessthis one contrasts with - SPEC-148 — the places audit; the per-property method and the client-lifecycle blocker
previewshares withmap - SPEC-149 — the business audit; the plugin with nothing misfiled, against which this one has
preview - SPEC-145 — composed runes; the mechanism audited
- SPEC-144 — entity registration; covers design's
registerandaggregate - ADR-036 — the plugin escape hatch, which design is a correct use of
- SPEC-151 — the marketing audit; corroborates the derived-data blocker in a second plugin
- SPEC-155 — the media audit; a third instance of the derived-data blocker, in
playlist's inline player payload - SPEC-152 — the plan audit; the third plugin that stays, and the one that shows staying does not mean staying as-is