Resolution
Completed: 2026-09-15
Branch: claude/milestone-v0-34-0-5zoab0
Recommendation
Proceed inside WORK-558. Phase 2 does not need to split out of the milestone. Eight genuine findings across both dogfooded sites, each a one-line fix, plus one upstream artifact that needs a decision rather than work.
How it was measured
scripts/validation-blast-radius.mjs — added by this item. It loads every site in refrakt.config.json through loadContent exactly as the adapter does (including each plugin's configure hook, without which the plan site reports 6 pages instead of 786), forces the full measured id set on regardless of the shipped allow-list, and groups the findings.
Measurement only — it fixes nothing and does not change the shipped default. Re-runnable: node scripts/validation-blast-radius.mjs.
variable-undefined is deliberately excluded from the measurement as well as from the product. SPEC-132 D4 already measured it as unusable, and including it would have buried every other number.
The numbers
| site | pages | findings |
|---|
site/ (main) | 235 | 3,310 |
plan-site/ (plan) | 786 | 0 |
Per error id, site/ — plan-site/ reported nothing under any id:
| id | count |
|---|
attribute-type-invalid | 3,309 |
attribute-value-invalid | 1 |
attribute-missing-required | 0 |
tag-undefined | 0 |
attribute-undefined | 0 |
The headline number is one message repeated 3,302 times, which is why the raw total is misleading:
| count | message |
|---|
| 3,302 | attribute-type-invalid: Attribute 'primary' must be type of 'Object' |
| 2 | attribute-type-invalid: Attribute 'limit' must be type of 'String' |
| 1 | attribute-value-invalid: Attribute 'frame-displace' … Got 'both' instead. |
| 1 | attribute-type-invalid: Attribute 'showContrast' must be type of 'Boolean' |
| 1 | attribute-type-invalid: Attribute 'showA11y' must be type of 'Boolean' |
| 1 | attribute-type-invalid: Attribute 'showCharset' must be type of 'Boolean' |
| 1 | attribute-type-invalid: Attribute 'route' must be type of 'Boolean' |
| 1 | attribute-type-invalid: Attribute 'limit' must be type of 'Number' |
By rune
Not a useful axis here, and the reason is worth recording. The item asked for it to distinguish "a single misbehaving rune" from "a broad problem", and the answer is the first — but the grouping that shows it is by message, not by rune. Markdoc's attribute messages name the attribute, not the owning tag, so a rune histogram comes out empty for this id class. The 3,302 all originate from one construct ({% if %}), and the remaining 8 are spread one apiece across collection, showcase, palette, typography, map and plan-activity.
Custom validators: zero
SeparatedString, SpaceSeparatedNumberList and the two in plugins/media/src/attributes.ts produce 0 findings across both sites.
They are told apart from Markdoc's own type check by message — all four share the attribute-type-invalid id, but only they emit is not a string or contains non-numeric value. Neither phrase appears anywhere in the output.
So the least predictable part of phase 2 turns out to be the quietest. That is a real result and not a null one: it means WORK-558's criterion that SpaceSeparatedNumberList demonstrably rejects non-numeric input in a build has to be proved by a test with deliberately bad input, because our own content will never exercise it.
Critical vs error
Zero critical findings on either site under the measured ids, after WORK-556's fixes. Before them there were 6,613 — 6,609 tag-placement-invalid from the bindRow inline defect, and 4 from a malformed {% ref %} — all resolved in that item.
One correction to how this criterion was framed. It asked for critical counted separately from error "since D11 makes them non-suppressible", on the spec's assumption that the phase 1 and 2 ids are all error. They are not: tag-undefined is critical in Markdoc 0.4.0, and attribute-type-invalid / attribute-value-invalid take their level from the attribute schema's own errorLevel — so hint.type, which sets errorLevel: 'critical', would have reported its finding as critical too. WORK-556's resolution has the full table.
The 3,302: an upstream artifact, not a defect in our content
{% if %} is Markdoc's own tag, and its schema declares primary: { type: Object, render: false } (@markdoc/markdoc@0.4.0, src/tags/conditional.ts:37). That type holds for what an author writes — {% if $var %} and {% if equals(a, b) %} both pass an expression node, which is an object.
It stops holding after substitution. bindRow replaces $row.required with the row's actual value before the page is validated, so the schema sees {% if true %} and rejects a boolean against Object. Verified in isolation:
{% if $row.required %} → clean
{% if true %} → attribute-type-invalid