Skip to content

fix(spec): check:yaml-examples dispatches ComponentPropsMap over a tagged component example’s properties - #14086

Merged
os-support-ai merged 1 commit into
mainfrom
claude/issue-13338-yaml-examples-props-dispatch
Sep 1, 2026
Merged

fix(spec): check:yaml-examples dispatches ComponentPropsMap over a tagged component example’s properties#14086
os-support-ai merged 1 commit into
mainfrom
claude/issue-13338-yaml-examples-props-dispatch

Conversation

@claude

@claude claude Bot commented Sep 1, 2026

Copy link
Copy Markdown
Contributor

Fixes #13338

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 of string to unknown, so the half of a
component example where essentially all of the authored content lives was checked by
nothing — while the os:check-yaml tag read to an author as "verified against the live
schema".

After a block validates against its declared schema, every node carrying a type string
plus a properties mapping is now dispatched through ComponentPropsMap, and the props
schema'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 of
what a component type's props are — the same map the #5068 authoring-rules gate
(@objectstack/lint's validate-component-props) dispatches, whose rows are maintained
per 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 object prop missing is skipped when the node carries dataSource.object. That is
validate-component-props.ts's own rule, with its reasoning — objectui's element
renderers read dataSource.object first, so a component binding through the richer
sibling 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/lint depends on
@objectstack/spec, so this script cannot import it back without a package cycle — and
lint's walkPageComponents is 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 type string plus a
properties mapping
, wherever it sits in the parsed value — not "known page-component
positions only". Measured reasons:

  • Six of the corpus's eighteen tagged fences declare a bare PageComponentSchema — a
    single 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.
  • The interesting nested nodes live inside the open bag anyway
    (properties.items[].children[], .children[], .body[], .footer[]), so a
    position-based walk would have to descend through untyped data regardless.
  • Shape recognition is what mapPageComponents (packages/spec/src/conversions/walk.ts)
    settled on for the same container keys, for the same reason: properties is an open bag
    that nothing validates by type on the load path.

Generic reach is safe because the skip is the filter: only a type with a
ComponentPropsMap row is ever judged, and every row is namespaced (page:*, record:*,
element:*, nav:*, ai:*, object-*, plugin widgets), so no field / view / datasource
type value collides with one. Unregistered and custom.* types are passed over in
silence — the same skip validate-component-props documents, and without it the first run
would 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
walkPageComponents and translatePage both settled on. A YAML anchor may legally alias
one 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 ece65816

The 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.

✅ 18 tagged YAML example(s) across 1 file(s) validate against their declared live spec schemas
   ↳ 18 component node(s) also judged against their ComponentPropsMap props schema; 1 skipped
     (no row for the type — SDUI blocks and custom.* are an open namespace)
fence (line) declaration nodes dispatched verdict
83 page 3 (page:header, record:details, record:activity) green
137 page 4 (object-grid, page:tabs, + 2 nested in items[].children[]) green
233, 262, 295 FormSectionSchema key=section 0 green
346, 375, 441 FormSectionSchema[] key=sections 0 green
488 FormViewSchema 0 green
571 ResponsiveStylesSchema key=responsiveStyles 0 green
611 PageComponentSchema 1 (record:related_list) green
665 PageComponentSchema 1 (object-metric) green
697 PageComponentSchema 1 (record:activity) green
711 PageComponentSchema 0 — custom.approval_timeline skipped green (by design)
753 PageComponentSchema 1 (object-master-detail-form) green
779 PageComponentSchema 1 (object-kanban) green
980 page 6 (page:header, record:details, 2x record:related_list, object-metric, record:activity) green
1040 FormViewSchema 0 green

Newly 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 the
reverse 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's
numeric columns: 2 in the Customer 360 fence), against two gate versions:

=== MUTATION: reintroduce columns: 2 in the customer_360 fence ===
quoted spelling: 1 -> 0 ; numeric spelling now: 1

=== LEG A: base-commit gate (8dc22d68) over the mutated corpus ===
confirmed on disk: base gate has 0 ComponentPropsMap reference(s)
LEG A exit=0
✅ 18 tagged YAML example(s) across 1 file(s) validate against their declared live spec schemas

=== LEG B: deepened gate (HEAD) over the same mutated corpus ===
confirmed on disk: HEAD gate has 7 ComponentPropsMap reference(s)
LEG B exit=1
✗ 1 of 18 tagged YAML block(s) do not hold their claim:

  content/docs/protocol/objectui/layout-dsl.mdx:980  (os:check-yaml page)
    · at regions[1].components[0].properties.columns: Invalid option: expected one of "1"|"2"|"3"|"4"

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_PATH for both files, proven
by blob-hash equality plus an empty git diff HEAD and an empty git status --porcelain,
and the script carries a trap ... EXIT INT TERM restore.

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 green
would 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, ComponentPropsMap and every props schema are
untouched; nothing that parses, stores or rehydrates metadata changes behaviour. The
diff is one file under packages/spec/scripts/; no packages/spec/src/** path is
touched.

Tests

  • pnpm --filter @objectstack/spec run check:yaml-examples48 of 48 self-test cases
    pass (16 of them new for this change), then the corpus run is green as quoted above.
    New pins: the card's columns: 2 refused with the live props schema's message read at
    test time (never a fossilised copy); the corrected columns: '2' green; an undeclared
    prop refused with the schema's rename hint intact; a fence whose root is the
    component; a node nested in properties.items[].children[]; custom.* and an
    unregistered SDUI type skipped and counted; dataSource binding suppressed and its
    unbound 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=2Test 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
    --listFiles confirms scripts/check-yaml-examples.ts is really in that program
    (a typecheck that excluded it would be a green over source nothing read).
  • Gate families re-derived from the actual diff with
    node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack (29 families plus
    2 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-liveness
    four (check:liveness, check:empty-state, check:strictness-ledger,
    check:variant-docs). The two convention-triggered obligations an edit to a gate script
    incurs — scripts/pm/bare-root-worklist.mjs --self-test and pnpm check:pm-dispatch-gates
    — are green too (no stale, missing or contradicted verdict; 1090 self-test cases).
  • node scripts/check-test-completeness.mjs exits 3, PREREQUISITE NOT MET — it grades a
    saved turbo run test log and none was named. Recorded as not measured, which its
    own output insists is not a finding.
  • All of the above ran against a clean tree at ece65816, the branch head.

No changeset — skip-changeset

packages/spec's files array does not carry scripts/, so this diff publishes nothing
from 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-frontmatter
changeset, which is a real input to changesets/action and can stall a release greenly.

Generated by Claude Code


Generated by Claude Code

…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
@claude claude Bot added the skip-changeset PR has no user-facing published change; bypasses the changeset gate label Sep 1, 2026
@github-actions github-actions Bot added the size/m label Sep 1, 2026
@github-actions

github-actions Bot commented Sep 1, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

Nothing 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
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.

Coarse fallback — 0 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json 96e25a8b18828d9c01cd1d8a1307ea2c523cd21cpackageMentionDocs.

@os-support-ai
os-support-ai marked this pull request as ready for review September 1, 2026 04:29
@os-support-ai
os-support-ai added this pull request to the merge queue Sep 1, 2026
Merged via the queue into main with commit def3b7c Sep 1, 2026
38 checks passed
@os-support-ai
os-support-ai deleted the claude/issue-13338-yaml-examples-props-dispatch branch September 1, 2026 04:50
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size/m skip-changeset PR has no user-facing published change; bypasses the changeset gate tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[finding] check:yaml-examples cannot see inside a page component's properties — a tagged component example is green whatever its props say

2 participants