Uh oh!
There was an error while loading. Please reload this page.
fix(spec): check:yaml-examples dispatches ComponentPropsMap over a tagged component example’s properties - #14086
Merged
os-support-ai merged 1 commit intoSep 1, 2026
Conversation
…mples check:yaml-examples validated a tagged block with its declared schema and stopped at the component node's own keys -- Zod strictness does not recurse and PageComponentSchema.properties is an open record, so the half of a component example where the authored content lives was unchecked. A page carrying record:details with a numeric columns: 2 (against the enum "1"|"2"|"3"|"4") was green. After a block validates against its declared schema, every node carrying a type string plus a properties mapping is dispatched through ComponentPropsMap -- the #5068 authoring gate's own map -- and its rejection is printed verbatim, in the same line shape as the declared schema's. A type with no row (SDUI blocks, custom.*) is skipped, the same skip validate-component-props documents. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Mciyv38maJ6HYVMiaM26T1
Contributor
📓 Docs Drift CheckNothing in this diff resolved to a documentable surface (no symbol, route or SDK anchor derived from 0 changed package(s)), so this run has no opinion about the docs. What this run could not see
Coarse fallback — 0 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): |
os-support-ai
marked this pull request as ready for review
September 1, 2026 04:29
os-support-ai
enabled auto-merge
September 1, 2026 04:30
Uh oh!
There was an error while loading. Please reload this page.
os-support-ai
deleted the
claude/issue-13338-yaml-examples-props-dispatch
branch
September 1, 2026 04:50
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for freeto join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes#13338
check:yaml-examplesvalidated a tagged block with its declared schema and stopped atthe component node's own keys. Zod strictness does not recurse and
PageComponentSchema.propertiesis an open record of string to unknown, so the half of acomponent example where essentially all of the authored content lives was checked by
nothing — while the
os:check-yamltag read to an author as "verified against the liveschema".
After a block validates against its declared schema, every node carrying a
typestringplus a
propertiesmapping is now dispatched throughComponentPropsMap, and the propsschema's own rejection is printed verbatim, in the same line shape the declared
schema's rejections already use.
The verdict is reused, not re-derived
ComponentPropsMap(packages/spec/src/ui/component.zod.ts) is the one owned source ofwhat a component type's props are — the same map the #5068 authoring-rules gate
(
@objectstack/lint'svalidate-component-props) dispatches, whose rows are maintainedper component against objectui's renderer read points. This gate calls that map and adds
no vocabulary of its own: no rule ids, no hint text, no rewording. The author reads
the rejection the props schema itself writes, exactly as the triage required.
One suppression is ported deliberately rather than invented: an issue that reports the
flat
objectprop missing is skipped when the node carriesdataSource.object. That isvalidate-component-props.ts's own rule, with its reasoning — objectui's elementrenderers read
dataSource.objectfirst, so a component binding through the richersibling has omitted nothing, and reporting it would be a wrong verdict rather than a
strict one. Both directions are pinned in the self-test.
Why the traversal is written here instead of imported.
@objectstack/lintdepends on@objectstack/spec, so this script cannot import it back without a package cycle — andlint's
walkPageComponentsis rooted at a stack (stack.pages[].regions[]/.slots),which a tagged fence usually is not. What is duplicated is a traversal; the props
vocabulary and the verdict text are not.
Recursion boundary: generic, by node shape
The triage left this open. Chosen: any node carrying a
typestring plus apropertiesmapping, wherever it sits in the parsed value — not "known page-componentpositions only". Measured reasons:
PageComponentSchema— asingle component with no page around it. A page-rooted walk skips every one of them,
including the
custom.*fence this gate must be seen to skip deliberately.(
properties.items[].children[],.children[],.body[],.footer[]), so aposition-based walk would have to descend through untyped data regardless.
mapPageComponents(packages/spec/src/conversions/walk.ts)settled on for the same container keys, for the same reason:
propertiesis an open bagthat nothing validates by
typeon the load path.Generic reach is safe because the skip is the filter: only a type with a
ComponentPropsMaprow is ever judged, and every row is namespaced (page:*,record:*,element:*,nav:*,ai:*,object-*, plugin widgets), so no field / view / datasourcetypevalue collides with one. Unregistered andcustom.*types are passed over insilence — the same skip
validate-component-propsdocuments, and without it the first runwould be a wall of false refusals on a namespace the maintainer's 2026-08-05 ruling keeps
open on purpose.
The walk's cycle guard is an ancestor set, not a visited set — the distinction
walkPageComponentsandtranslatePageboth settled on. A YAML anchor may legally aliasone node into two sibling positions and both placements must be judged; only a node that
is its own ancestor is a cycle. Pinned.
Corpus census — the whole tagged corpus, at
ece65816The deepening is a corpus-wide tightening of what a tagged fence must satisfy, so it was
run over every tagged block, not only the card's. All eighteen live in
content/docs/protocol/objectui/layout-dsl.mdx.pagepage:header,record:details,record:activity)pageobject-grid,page:tabs, + 2 nested initems[].children[])FormSectionSchema key=sectionFormSectionSchema[] key=sectionsFormViewSchemaResponsiveStylesSchema key=responsiveStylesPageComponentSchemarecord:related_list)PageComponentSchemaobject-metric)PageComponentSchemarecord:activity)PageComponentSchemacustom.approval_timelineskippedPageComponentSchemaobject-master-detail-form)PageComponentSchemaobject-kanban)pagepage:header,record:details, 2xrecord:related_list,object-metric,record:activity)FormViewSchemaNewly red: none. No doc fence needed a repair in this PR, and no schema-versus-docs
disagreement surfaced. That is not this change being inert — it is #13337 having already
shipped the card's instance as
columns: '2'. The proof that the gate now bites is thereverse verification below, run against the real corpus.
Reverse verification — the card's measured instance, end to end
Run from the committed state, mutating the real corpus (
columns: '2'back to the card'snumeric
columns: 2in the Customer 360 fence), against two gate versions:Leg A reproduces the card's false green exactly. Leg B's message is the props schema's
own, character for character the text the card's hand audit reported. Each leg proved its
mutation had landed on disk before reading a result (occurrence counts, not an editor's
exit code); the restore is
git checkout HEAD -- ABSOLUTE_PATHfor both files, provenby blob-hash equality plus an empty
git diff HEADand an emptygit status --porcelain,and the script carries a
trap ... EXIT INT TERMrestore.A third vacuous-green guard
This gate already refuses to report a green it cannot back (zero fences found; zero
tagged fences). The props dispatch is invisible when it reaches nothing, so a run where
every tagged block validates while zero component nodes were dispatched now fails
loudly: it means the walk stopped finding nodes carrying
type+properties, and greenwould again be a lie about the half this change exists to see. The counts are printed on
every green run.
Clause-②: no
Deepening a gate's validation changes no schema's accept set — the triage's own reading,
adopted verbatim.
PageComponentSchema,ComponentPropsMapand every props schema areuntouched; nothing that parses, stores or rehydrates metadata changes behaviour. The
diff is one file under
packages/spec/scripts/; nopackages/spec/src/**path istouched.
Tests
pnpm --filter @objectstack/spec run check:yaml-examples— 48 of 48 self-test casespass (16 of them new for this change), then the corpus run is green as quoted above.
New pins: the card's
columns: 2refused with the live props schema's message read attest time (never a fossilised copy); the corrected
columns: '2'green; an undeclaredprop refused with the schema's rename hint intact; a fence whose root is the
component; a node nested in
properties.items[].children[];custom.*and anunregistered SDUI type skipped and counted;
dataSourcebinding suppressed and itsunbound control still reported; a declared-schema failure not buried under a props
pile-on; an aliased node judged at both placements.
pnpm --filter @objectstack/spec exec vitest run --maxWorkers=2—Test Files 447 passed | 1 skipped (448),Tests 12022 passed | 1 skipped (12023), exit 0.pnpm --filter @objectstack/spec run check:scripts-typecheck— exit 0, and--listFilesconfirmsscripts/check-yaml-examples.tsis really in that program(a typecheck that excluded it would be a green over source nothing read).
node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack(29 families plus2 convention-triggered by editing a gate script), all run and green:
check:changeset-gate-self-tests,check:doc-authoring,check:merge-driver,check:page-declaration-shape,check:pm-governed-merges,check:published-files,check:slot-lookup,check:test-source-alias,check:type-source-resolution,check:cross-package-test-inputs,check:logger-receiver-detach,check-undeclared-dep-imports,check-comment-mask-adoption,check-keyed-text-bounds,check-plugin-teardown-shape,check-adr-0087-registration,check-ci-filter-parity,check-dispatcher-error-vocabulary,check-shard-attestation, and the spec-livenessfour (
check:liveness,check:empty-state,check:strictness-ledger,check:variant-docs). The two convention-triggered obligations an edit to a gate scriptincurs —
scripts/pm/bare-root-worklist.mjs --self-testandpnpm check:pm-dispatch-gates— are green too (no stale, missing or contradicted verdict; 1090 self-test cases).
node scripts/check-test-completeness.mjsexits 3, PREREQUISITE NOT MET — it grades asaved
turbo run testlog and none was named. Recorded as not measured, which itsown output insists is not a finding.
ece65816, the branch head.No changeset —
skip-changesetpackages/spec'sfilesarray does not carryscripts/, so this diff publishes nothingfrom any package: it changes a repo gate, not a released artifact. Route 2 in
scripts/check-empty-changeset.mjs's own ledger — the label, never an empty-frontmatterchangeset, which is a real input to
changesets/actionand can stall a release greenly.Generated by Claude Code
Generated by Claude Code