Found by WORK-628, which ran @adobe/structured-data-validator over the SEO baseline. These are the only schema.org-layer findings on today's baseline, and every one of them is real. The baseline never recorded them as defects: SPEC-130 D5 leaves a schema row to the reviewer, and the rows that carry these properties say in their own comments that nothing checks them.
Found from reading the code while designing SPEC-178. Not yet reproduced.{% expand %} and ref preview="drawer" inline another entity's content into the current page. The expand pass re-transforms the entity's source AST with the site's full config (packages/runes/src/expand-pipeline.ts), so any rune inside it emits its typeof and property markers as it would on its own page. Drawers are hoisted into the same page renderable.JSON-LD is harvested after postProcess, deliberately (packages/content/src/site.ts, the extractSeo loop after the pipeline). collectJsonLd walks the whole renderable for typeof. So the embedded entity's structured data would be published on the embedding page too. A page that embeds a recipe would claim a second Recipe.It does not happen today, which is why it is minor. The only embeddable entities are plan entities (sourceFile + extract in plugins/plan/src/pipeline.ts), and plan runes emit no structured data. It becomes real when:
{% testimonial %} parses its attribution line as **Name** — Role, stripping a leading -, – or — from the text after the <strong>. It does not strip a comma, so the comma form — which is the obvious alternative and which nothing rejects — carries the separator into the role.
data-meta-rank is the channel by which density compaction is meant to drop secondary metadata. It is specified, it is styled, and no code path ever puts it on an element.
howto, recipe, steps and track each hand-roll a CSS counter that the sequence dimension already provides for them. The duplication is invisible because the output looks identical — but the two rules land at different specificities, so which one is actually in effect differs from rune to rune.
When two files claim an ID, runMigrateIds sorts the claimants by path and keeps the first (plugins/plan/src/commands/migrate-ids.ts:165). Which claimant is already published on the base ref plays no part, so on a branch the tool will happily renumber the entity that is already on main and leave the local draft holding the ID.The safety argument for that choice is stated in the code:
resolveSections has two branches. The emitTag branch (packages/runes/src/lib/resolver.ts:480-502) builds a tag node per section and returns — it never calls matchKnownSection:
{% plan-history %} declares five attributes (plugins/plan/src/tags/plan-history.ts): id, limit, type, since and group. The rune emits all five as metas, and the post-process resolver that builds the output (resolvePlanHistory, plugins/plan/src/pipeline.ts:910) uses only three of them.
Found while implementing v0.41.3 (BUG-034 touches the same file). It is on main before that change, and CI does not typecheck the editor app, so nothing catches it.packages/editor/app/src/lib/preview/block-renderer.ts:54 copies communityStyles back onto the rune configs. communityStyles is typed Record<string, unknown>, so each styles value is unknown. RuneConfig.styles expects Record<string, string | { prop: string; template?: string; transform?: NamedTransform }>.
The shared stacked-layout contract (packages/lumina/styles/layouts/split.css) assumes media-first source order: top is plain DOM order, bottom flips visually via a generic column-reverse. But hero, feature, and step emit content-first DOM (text before media — the right reading order for their classic media-beneath-text presentation). Result: both stacked labels lie — the default top rendered media at the bottom, and an explicit bottom hoisted it to the top. Found in SPEC-101 review (PR #432); pre-existing.
Found while auditing section roles for WORK-530: Character's entity-level body slot is emitted, placed by layout, and always empty. Prose an author writes directly inside {% character %} never reaches the output.Realm and Faction — the two runes that otherwise share Character's exact shape — handle the same content correctly, which is what makes this a bug rather than a design choice.
Markdoc.validate() is never called on site content. A rune typo, an unknown attribute, a missing required attribute and an out-of-enum modifier value all render silently, and the only user who ever learns is one running the VS Code extension.It is called in two places, neither of which sees a user's pages:
Six Lumina rules style runes by selecting on the RDFa property attribute rather than the BEM element class the rune's refs already produce. Four of them now match nothing: the schema property was renamed, moved onto a <meta>, or removed entirely, and the stylesheet was never updated.Nothing detected it. The CSS coverage test checks that the selectors derived from config exist in the CSS; it has no way to notice a hand-written selector that matches no element.
/runes/media/track tells authors that "tracks can be used independently or inside a playlist rune". The composition does not work. A {% track %} inside a {% playlist %} is not a track of that playlist in the rendered HTML or in the structured data — it renders as a bare <li> in the prose body and publishes a detached top-level entity.Found while settling SPEC-130's open question about the two MusicRecording emitters. The question turned out to rest on this composition working; it does not.
deriveParentUrl returns a parent URL with a trailing slash; the router registers page URLs without one. So parentOf maps /docs/guide to /docs/, no page is registered under that key, and the ancestor walk stops dead. {% breadcrumb auto=true %} renders the current page and nothing else.Found while landing WORK-563, which moved the SEO harvest past the pipeline and so put a page-level assertion on this hook's output for the first time.
Three live pages point a file-ref at a line range that no longer contains the symbol it is labelled with — and no longer exists in the file at all. This is the exact failure SPEC-131 describes as its motivating case, observed rather than predicted.
WORK-559 added validation to RefraktConfig "as a deprecated shorthand, matching sandbox". The type is there, the JSON Schema declares it, the generated reference documents it — and nothing reads it.A user who configures validation the documented shorthand way gets editor autocomplete, schema shape-checking, no warning, and no effect.
ADR-024 made themes framework-agnostic: no target, and layouts declare regions rather than pointing at framework components. validateManifest (packages/transform/src/validate.ts:467) was never updated.It still requires target and layouts.*.component, so it rejects the manifest create-refrakt generates and the manifest Lumina ships. Nobody noticed because nothing ran it — until WORK-579 gave it a command of its own.
collection, aggregate and backlog all accept group="<field>". When that field is multi-valued — tags, source, pr — the group key becomes the entire comma-separated string, so "runes, data, csv" is one group rather than three memberships.The filter side of the same grammar already disagrees: candidates() (packages/runes/src/field-match.ts:126) splits a comma-string into members, so filter="tags:runes" matches an entity tagged runes, data, csv. Grouping that same entity puts it in a group called runes, data, csv. One field, two incompatible readings, in one rune.
The core preprocess phase runs three whole-AST passes in a fixed order — include, then snippet, then data (packages/runes/src/config.ts:2464). A row template that contains a {% snippet %} is therefore walked by snippet's pass before data has bound any row, so path=$row.path is unresolvable and the snippet fails. data then clones the resulting error fence once per row.
figure's transform builds its children from imgs — the nodes passing isMediaNode — plus an optional figcaption (packages/runes/src/tags/figure.ts:82-87):
For a rune whose primary syntax is a Markdown list, refrakt reference documents that a list is accepted and nothing about what goes in it. Three facts are lost, at two different points in the pipeline.
sandbox declares a context attribute ("Shared context scope for multiple sandboxes", packages/runes/src/tags/sandbox.ts:92), and the design plugin reads it to choose which design-context token set to inject. The sandbox transform never puts the value anywhere. The attribute is read nowhere in sandbox.ts (lines 142–155 read the others), and createComponentRenderable is called with no properties (:247), so the rune carries no data-rune-fields entry and no <meta data-field="context">.The consumers therefore always fall back:
The design plugin's postProcess (plugins/design/src/pipeline.ts) and the editor preview's client-side stand-in (packages/editor/app/src/lib/preview/block-renderer.ts) both deliver a sandbox's design-context tokens as a child element: <meta data-field="design-tokens" content="{…}">. The identity transform leaves it in place (it is not a modifier), so it ships inside <rf-sandbox>.The rf-sandbox behaviour never reads that child. It looks for the tokens on a host attribute, this.dataset.designTokens (packages/behaviors/src/elements/sandbox.ts:112), and otherwise falls back to RfContext.designTokens. Nothing in the repo sets either: no transform emits data-design-tokens, and no code assigns RfContext.designTokens. So this._tokens is always null and the iframe never receives a token set, whichever context the sandbox names.Found while fixing BUG-032. That fix makes the build pick the right token set per context. This bug is what keeps that set from reaching the page.
Found by WORK-634 (#695), which scaffolds a project in its tests.The starters ship content/docs/getting-started.md, but five of the six _layout.md files list the nav item as getting-started instead of docs/getting-started:
Found by WORK-634 (#695).packages/create-refrakt/template-html/build.ts calls loadContent without the plugins option. Its own comment (around line 127) says plugins should be passed through, matching the option shape createRefraktLoader uses for the Vite-based adapters. As a result:
Found while probing palette as a composition (SPEC-165).A composition's frontmatter accepts itemModel, emitTag and emitAttributes on a content field (FIELD_KEYS, packages/runes/src/lib/composition.ts). This is the same item extraction cast uses in TypeScript (plugins/business/src/tags/cast.ts). An each over the field then binds $each.<name> for every emitted attribute.But in YAML a pattern is a string. parseHeadingExtract compiles its patterns into a RegExp, while parseFields passes itemModel through untouched. The resolver applies a pattern only when it is a RegExp (field.pattern instanceof RegExp, packages/runes/src/lib/resolver.ts:788), so a string pattern is skipped with no error.
refrakt plan validate --against <ref> exists to catch an ID collision on the branch, before the merge makes every reference to that ID ambiguous (SPEC-135 D8, WORK-582). It does not catch the most common one: two branches that each create the next free ID. It reports "No ID collisions" and exits 0, so the PR job passes it.The cause is the deletion check in collisionsFrom (plugins/plan/src/commands/against.ts):
Milestones keep their semver name as the filename (v1.0.0.md). CLAUDE.md states the convention, and so does the comment above the line in runCreate that breaks it (plugins/plan/src/commands/create.ts):
An open drawer is meant to sit --rf-drawer-gutter (8px) from every viewport edge. On a page using the default layout it stops 64px short at the top and the bottom. Measured in Chromium at 1280×800: the panel spans y 72 → 728 instead of 8 → 792, with a computed margin of 64px 0. A docs-layout page (/runes/xref) is unaffected.The cause is the default layout's section rhythm (packages/lumina/styles/layouts/default.css):
Pages that render plan entities read git history during the build, and the cost grows with the repository's history rather than with the content being built. In this repository, two tests that build real plan content take ~17s on a shallow clone (96 commits) and ~170s on the full history (3,473 commits):