Found while doing the corpus clean-up in #13266 (PR #13337), which tags page-component
examples with os:check-yaml PageComponentSchema for the first time.
The gap
check:yaml-examples validates a tagged block with the declared schema's safeParse.
For a page or a page component that closes the node's own keys — and stops there.
PageComponentSchema.properties is an open record of string to unknown
(packages/spec/src/ui/page.zod.ts), and Zod strictness does not recurse, so nothing
inside properties is validated by the tag. component.zod.ts's own header states
the same fact from the other side: "strictness does NOT recurse, so it closes the
component node's own keys and leaves everything under properties unchecked. Nothing
dispatches ComponentPropsMap by type."
This matters specifically for docs, because properties is where almost all of a
component example's authored content lives. A tagged fence reads as "this example is
verified against the live schema", and for the half an author is most likely to get
wrong, it is not.
Measured instance
While authoring the #13266 rewrites I wrote this Customer 360 component:
type: record:details
properties:
columns: 2
fields: [name, status, industry, employee_count, website, phone]
RecordDetailsProps.columns is the enum "1"|"2"|"3"|"4" — the numeric 2 is
refused. Dispatching ComponentPropsMap['record:details'].safeParse(properties) by
hand reports:
columns: Invalid option: expected one of "1"|"2"|"3"|"4"
but os:check-yaml page over the whole page is green with that fence in place. I
caught it only because I ran the props dispatch as a separate hand audit; the committed
gate would not have. The PR ships columns: '2'.
Why it is not just "tag the props schema instead"
An author could tag a props-only fence (os:check-yaml RecordDetailsProps), but that
loses the thing the example is teaching — a component in its page context, which is the
shape a reader copies. The two claims are not interchangeable.
Prior art for the fix, already in the tree
The dispatch this gate lacks exists elsewhere: the #5068 authoring-rules gate dispatches
ComponentPropsMap by type and refuses a misspelled prop, which is why the map's rows
are maintained per component (see the objectBlockHistory / #8691 / #8744 notes in
component.zod.ts). So the verdict is available and owned — check-yaml-examples.ts
simply does not call it.
Sketch, not a prescription: after a block validates against its declared schema, walk the
parsed value for nodes carrying a type string plus properties, and dispatch
ComponentPropsMap[type] where a row exists (skipping unregistered/custom.* types the
way the authoring gate already does). That keeps this gate's "no vocabulary of its own"
posture — the verdict stays the props schema's own message, verbatim.
Scope note
Filed unassigned, recording only. Deliberately not fixed inside #13266: that card's
ruled scope is docs content triage on one page, and this is a change to a gate's
validation depth affecting every page that ever tags a component — a separate decision
about how much a green os:check-yaml should be allowed to claim.
Re-check
pnpm --filter @objectstack/spec run check:yaml-examples
is green on content/docs/protocol/objectui/layout-dsl.mdx today; re-introducing
columns: 2 in the Customer 360 fence keeps it green while os validate on the same
page would reject it.
Generated by Claude Code
Found while doing the corpus clean-up in #13266 (PR #13337), which tags page-component
examples with
os:check-yaml PageComponentSchemafor the first time.The gap
check:yaml-examplesvalidates a tagged block with the declared schema'ssafeParse.For a page or a page component that closes the node's own keys — and stops there.
PageComponentSchema.propertiesis an open record of string to unknown(
packages/spec/src/ui/page.zod.ts), and Zod strictness does not recurse, so nothinginside
propertiesis validated by the tag.component.zod.ts's own header statesthe same fact from the other side: "strictness does NOT recurse, so it closes the
component node's own keys and leaves everything under
propertiesunchecked. Nothingdispatches
ComponentPropsMapbytype."This matters specifically for docs, because
propertiesis where almost all of acomponent example's authored content lives. A tagged fence reads as "this example is
verified against the live schema", and for the half an author is most likely to get
wrong, it is not.
Measured instance
While authoring the #13266 rewrites I wrote this Customer 360 component:
RecordDetailsProps.columnsis the enum"1"|"2"|"3"|"4"— the numeric2isrefused. Dispatching
ComponentPropsMap['record:details'].safeParse(properties)byhand reports:
but
os:check-yaml pageover the whole page is green with that fence in place. Icaught it only because I ran the props dispatch as a separate hand audit; the committed
gate would not have. The PR ships
columns: '2'.Why it is not just "tag the props schema instead"
An author could tag a props-only fence (
os:check-yaml RecordDetailsProps), but thatloses the thing the example is teaching — a component in its page context, which is the
shape a reader copies. The two claims are not interchangeable.
Prior art for the fix, already in the tree
The dispatch this gate lacks exists elsewhere: the #5068 authoring-rules gate dispatches
ComponentPropsMapbytypeand refuses a misspelled prop, which is why the map's rowsare maintained per component (see the
objectBlockHistory/#8691/#8744notes incomponent.zod.ts). So the verdict is available and owned —check-yaml-examples.tssimply does not call it.
Sketch, not a prescription: after a block validates against its declared schema, walk the
parsed value for nodes carrying a
typestring plusproperties, and dispatchComponentPropsMap[type]where a row exists (skipping unregistered/custom.*types theway the authoring gate already does). That keeps this gate's "no vocabulary of its own"
posture — the verdict stays the props schema's own message, verbatim.
Scope note
Filed unassigned, recording only. Deliberately not fixed inside #13266: that card's
ruled scope is docs content triage on one page, and this is a change to a gate's
validation depth affecting every page that ever tags a component — a separate decision
about how much a green
os:check-yamlshould be allowed to claim.Re-check
is green on
content/docs/protocol/objectui/layout-dsl.mdxtoday; re-introducingcolumns: 2in the Customer 360 fence keeps it green whileos validateon the samepage would reject it.
Generated by Claude Code