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

@claudeclaudeBot 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)declarationnodes dispatchedverdict
83page3 (page:header, record:details, record:activity)green
137page4 (object-grid, page:tabs, + 2 nested in items[].children[])green
233, 262, 295FormSectionSchema key=section0green
346, 375, 441FormSectionSchema[] key=sections0green
488FormViewSchema0green
571ResponsiveStylesSchema key=responsiveStyles0green
611PageComponentSchema1 (record:related_list)green
665PageComponentSchema1 (object-metric)green
697PageComponentSchema1 (record:activity)green
711PageComponentSchema0 — custom.approval_timelineskippedgreen (by design)
753PageComponentSchema1 (object-master-detail-form)green
779PageComponentSchema1 (object-kanban)green
980page6 (page:header, record:details, 2x record:related_list, object-metric, record:activity)green
1040FormViewSchema0green

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

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 queueSep 1, 2026
Merged via the queue into main with commit def3b7cSep 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 freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size/mskip-changesetPR has no user-facing published change; bypasses the changeset gatetooling

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

@os-support-ai@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { // Add copy buttons to all
 blocks
(function() {
function addCopyButtons() {
document.querySelectorAll('pre code').forEach(function(codeBlock) {
if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;
codeBlock.parentElement.setAttribute('data-copy-added', 'true');
var btn = document.createElement('button');
btn.textContent = 'Copy';
btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';
btn.onmouseover = function() { this.style.opacity = '1'; };
btn.onmouseout = function() { this.style.opacity = '0.7'; };
btn.onclick = function() {
navigator.clipboard.writeText(codeBlock.textContent).then(function() {
btn.textContent = 'Copied!';
setTimeout(function() { btn.textContent = 'Copy'; }, 1500);
});
};
codeBlock.parentElement.style.position = 'relative';
codeBlock.parentElement.appendChild(btn);
});
}
addCopyButtons();
// Re-run on dynamic content
var observer = new MutationObserver(addCopyButtons);
observer.observe(document.body, { childList: true, subtree: true });
})();
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
fix(spec): check:yaml-examples dispatches ComponentPropsMap over a tagged component example’s properties by claude[bot] · Pull Request #14086 · objectstack-ai/objectstack · GitHub
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

@claudeclaudeBot 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)declarationnodes dispatchedverdict
83page3 (page:header, record:details, record:activity)green
137page4 (object-grid, page:tabs, + 2 nested in items[].children[])green
233, 262, 295FormSectionSchema key=section0green
346, 375, 441FormSectionSchema[] key=sections0green
488FormViewSchema0green
571ResponsiveStylesSchema key=responsiveStyles0green
611PageComponentSchema1 (record:related_list)green
665PageComponentSchema1 (object-metric)green
697PageComponentSchema1 (record:activity)green
711PageComponentSchema0 — custom.approval_timelineskippedgreen (by design)
753PageComponentSchema1 (object-master-detail-form)green
779PageComponentSchema1 (object-kanban)green
980page6 (page:header, record:details, 2x record:related_list, object-metric, record:activity)green
1040FormViewSchema0green

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

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 queueSep 1, 2026
Merged via the queue into main with commit def3b7cSep 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 freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size/mskip-changesetPR has no user-facing published change; bypasses the changeset gatetooling

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

@os-support-ai@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { // Force GitHub README to respect dark mode (function() { var style = document.createElement('style'); style.textContent = ' .markdown-body { color-scheme: dark light; } .markdown-body pre { background: #161b22 !important; } .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; } .markdown-body table th, .markdown-body table td { border-color: #30363d !important; } .markdown-body img { background: #0d1117; } .markdown-body blockquote { border-left-color: #8b949e; } .markdown-body hr { border-color: #30363d; } '; document.head.appendChild(style); })(); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' fix(spec): check:yaml-examples dispatches ComponentPropsMap over a tagged component example’s properties by claude[bot] · Pull Request #14086 · objectstack-ai/objectstack · GitHub
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

@claudeclaudeBot 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)declarationnodes dispatchedverdict
83page3 (page:header, record:details, record:activity)green
137page4 (object-grid, page:tabs, + 2 nested in items[].children[])green
233, 262, 295FormSectionSchema key=section0green
346, 375, 441FormSectionSchema[] key=sections0green
488FormViewSchema0green
571ResponsiveStylesSchema key=responsiveStyles0green
611PageComponentSchema1 (record:related_list)green
665PageComponentSchema1 (object-metric)green
697PageComponentSchema1 (record:activity)green
711PageComponentSchema0 — custom.approval_timelineskippedgreen (by design)
753PageComponentSchema1 (object-master-detail-form)green
779PageComponentSchema1 (object-kanban)green
980page6 (page:header, record:details, 2x record:related_list, object-metric, record:activity)green
1040FormViewSchema0green

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

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 queueSep 1, 2026
Merged via the queue into main with commit def3b7cSep 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 freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size/mskip-changesetPR has no user-facing published change; bypasses the changeset gatetooling

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

@os-support-ai@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { // Highlight search terms from Google/DuckDuckGo/Bing referrer (function() { var ref = document.referrer; var terms = []; if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) { var url = new URL(ref); var q = url.searchParams.get('q') || url.searchParams.get('p'); if (q) { terms = q.split(/\s+/).filter(function(t) { return t.length > 2; }); } } if (terms.length === 0) return; var style = document.createElement('style'); style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }'; document.head.appendChild(style); function highlight(node) { if (node.nodeType === 3) { // text node var text = node.textContent; var found = false; terms.forEach(function(term) { var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\]\\]/g, '\\') + ')', 'gi'); if (regex.test(text)) { found = true; var frag = document.createDocumentFragment(); var parts = text.split(regex); parts.forEach(function(part, i) { if (i % 2 === 0) { frag.appendChild(document.createTextNode(part)); } else { var span = document.createElement('span'); span.className = 'userscript-highlight'; span.textContent = part; frag.appendChild(span); } }); node.parentNode.replaceChild(frag, node); } }); } else if (node.nodeType === 1 && node.childNodes) { // element var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT']; if (!skipTags.includes(node.tagName)) { Array.from(node.childNodes).forEach(highlight); } } } highlight(document.body); // Re-highlight on dynamic content var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1 || node.nodeType === 3) highlight(node); }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' fix(spec): check:yaml-examples dispatches ComponentPropsMap over a tagged component example’s properties by claude[bot] · Pull Request #14086 · objectstack-ai/objectstack · GitHub
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

@claudeclaudeBot 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)declarationnodes dispatchedverdict
83page3 (page:header, record:details, record:activity)green
137page4 (object-grid, page:tabs, + 2 nested in items[].children[])green
233, 262, 295FormSectionSchema key=section0green
346, 375, 441FormSectionSchema[] key=sections0green
488FormViewSchema0green
571ResponsiveStylesSchema key=responsiveStyles0green
611PageComponentSchema1 (record:related_list)green
665PageComponentSchema1 (object-metric)green
697PageComponentSchema1 (record:activity)green
711PageComponentSchema0 — custom.approval_timelineskippedgreen (by design)
753PageComponentSchema1 (object-master-detail-form)green
779PageComponentSchema1 (object-kanban)green
980page6 (page:header, record:details, 2x record:related_list, object-metric, record:activity)green
1040FormViewSchema0green

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

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 queueSep 1, 2026
Merged via the queue into main with commit def3b7cSep 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 freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size/mskip-changesetPR has no user-facing published change; bypasses the changeset gatetooling

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

@os-support-ai@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { // Strip utm_, fbclid, gclid, etc. from all links on page (function() { var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid', 'ref', 'ref_src', 'source', 'medium', 'campaign']; function cleanUrl(url) { try { var u = new URL(url, window.location.origin); var changed = false; trackingParams.forEach(function(p) { if (u.searchParams.has(p)) { u.searchParams.delete(p); changed = true; } }); return changed ? u.toString() : url; } catch (e) { return url; } } function cleanLinks() { document.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } cleanLinks(); var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1) { if (node.tagName === 'A') cleanLinks(); node.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + ' fix(spec): check:yaml-examples dispatches ComponentPropsMap over a tagged component example’s properties by claude[bot] · Pull Request #14086 · objectstack-ai/objectstack · GitHub
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

@claudeclaudeBot 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)declarationnodes dispatchedverdict
83page3 (page:header, record:details, record:activity)green
137page4 (object-grid, page:tabs, + 2 nested in items[].children[])green
233, 262, 295FormSectionSchema key=section0green
346, 375, 441FormSectionSchema[] key=sections0green
488FormViewSchema0green
571ResponsiveStylesSchema key=responsiveStyles0green
611PageComponentSchema1 (record:related_list)green
665PageComponentSchema1 (object-metric)green
697PageComponentSchema1 (record:activity)green
711PageComponentSchema0 — custom.approval_timelineskippedgreen (by design)
753PageComponentSchema1 (object-master-detail-form)green
779PageComponentSchema1 (object-kanban)green
980page6 (page:header, record:details, 2x record:related_list, object-metric, record:activity)green
1040FormViewSchema0green

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

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 queueSep 1, 2026
Merged via the queue into main with commit def3b7cSep 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 freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size/mskip-changesetPR has no user-facing published change; bypasses the changeset gatetooling

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

@os-support-ai@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { // Auto-enable theater mode on YouTube (function() { function tryTheater() { var btn = document.querySelector('button[aria-label="Theater mode"], ytd-player #player button[title="Theater mode"]'); if (btn && !btn.classList.contains('activated')) { btn.click(); } } // Try immediately tryTheater(); // Try after navigation (SPA) var lastUrl = location.href; setInterval(function() { if (location.href !== lastUrl) { lastUrl = location.href; setTimeout(tryTheater, 500); } }, 1000); // Also try on player load var observer = new MutationObserver(tryTheater); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' fix(spec): check:yaml-examples dispatches ComponentPropsMap over a tagged component example’s properties by claude[bot] · Pull Request #14086 · objectstack-ai/objectstack · GitHub
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

@claudeclaudeBot 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)declarationnodes dispatchedverdict
83page3 (page:header, record:details, record:activity)green
137page4 (object-grid, page:tabs, + 2 nested in items[].children[])green
233, 262, 295FormSectionSchema key=section0green
346, 375, 441FormSectionSchema[] key=sections0green
488FormViewSchema0green
571ResponsiveStylesSchema key=responsiveStyles0green
611PageComponentSchema1 (record:related_list)green
665PageComponentSchema1 (object-metric)green
697PageComponentSchema1 (record:activity)green
711PageComponentSchema0 — custom.approval_timelineskippedgreen (by design)
753PageComponentSchema1 (object-master-detail-form)green
779PageComponentSchema1 (object-kanban)green
980page6 (page:header, record:details, 2x record:related_list, object-metric, record:activity)green
1040FormViewSchema0green

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

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 queueSep 1, 2026
Merged via the queue into main with commit def3b7cSep 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 freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size/mskip-changesetPR has no user-facing published change; bypasses the changeset gatetooling

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

@os-support-ai@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { // Remove or un-stick sticky/fixed headers that block content (function() { function unstick() { document.querySelectorAll('header, nav, [role="banner"], .header, .navbar, .sticky, .fixed-top, [style*="position: fixed"], [style*="position:sticky"]').forEach(function(el) { if (el.style.position === 'fixed' || el.style.position === 'sticky' || getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') { el.style.position = 'static'; el.style.top = 'auto'; el.style.zIndex = 'auto'; } }); } unstick(); var observer = new MutationObserver(unstick); observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] }); })(); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' fix(spec): check:yaml-examples dispatches ComponentPropsMap over a tagged component example’s properties by claude[bot] · Pull Request #14086 · objectstack-ai/objectstack · GitHub
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

@claudeclaudeBot 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)declarationnodes dispatchedverdict
83page3 (page:header, record:details, record:activity)green
137page4 (object-grid, page:tabs, + 2 nested in items[].children[])green
233, 262, 295FormSectionSchema key=section0green
346, 375, 441FormSectionSchema[] key=sections0green
488FormViewSchema0green
571ResponsiveStylesSchema key=responsiveStyles0green
611PageComponentSchema1 (record:related_list)green
665PageComponentSchema1 (object-metric)green
697PageComponentSchema1 (record:activity)green
711PageComponentSchema0 — custom.approval_timelineskippedgreen (by design)
753PageComponentSchema1 (object-master-detail-form)green
779PageComponentSchema1 (object-kanban)green
980page6 (page:header, record:details, 2x record:related_list, object-metric, record:activity)green
1040FormViewSchema0green

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

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 queueSep 1, 2026
Merged via the queue into main with commit def3b7cSep 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 freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size/mskip-changesetPR has no user-facing published change; bypasses the changeset gatetooling

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

@os-support-ai@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { // Universal Dark Mode - works on any site (function() { var enabled = true; function applyDarkMode() { if (!enabled) return; // Create style element if it doesn't exist var style = document.getElementById('universal-dark-mode-style'); if (!style) { style = document.createElement('style'); style.id = 'universal-dark-mode-style'; document.head.appendChild(style); } // Dark mode CSS - inverts colors but preserves images/video style.textContent = ' /* Invert everything except media */ html { filter: invert(1) hue-rotate(180deg) !important; background: #1a1a2e !important; } /* Restore images, videos, iframes, canvas */ img, video, iframe, canvas, svg, picture, [style*="background-image"] { filter: invert(1) hue-rotate(180deg) !important; } /* Preserve specific elements that should not be inverted */ .no-dark-mode, .no-dark-mode *, [data-theme="light"], [data-theme="light"], .ace_editor, .ace_editor *, .CodeMirror, .CodeMirror *, .monaco-editor, .monaco-editor *, .markdown-body pre, .markdown-body pre *, .highlight, .highlight *, pre code, pre code * { filter: none !important; } /* Fix common UI elements */ .modal, .popup, .dropdown-menu, .tooltip, .popover { filter: invert(1) hue-rotate(180deg) !important; background: #2d2d44 !important; border-color: #444 !important; } /* Scrollbars */ ::-webkit-scrollbar { background: #1a1a2e !important; } ::-webkit-scrollbar-thumb { background: #444 !important; } ::-webkit-scrollbar-thumb:hover { background: #555 !important; } /* Selection */ ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; } ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; } '; } function removeDarkMode() { var style = document.getElementById('universal-dark-mode-style'); if (style) style.remove(); } // Toggle with Alt+Shift+D document.addEventListener('keydown', function(e) { if (e.altKey && e.shiftKey && e.key === 'D') { e.preventDefault(); enabled = !enabled; if (enabled) { applyDarkMode(); console.log('[Universal Dark Mode] Enabled'); } else { removeDarkMode(); console.log('[Universal Dark Mode] Disabled'); } } }); // Apply on load applyDarkMode(); // Re-apply on dynamic content var observer = new MutationObserver(function(mutations) { if (enabled && !document.getElementById('universal-dark-mode-style')) { applyDarkMode(); } }); observer.observe(document.head, { childList: true }); console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle'); })(); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })(); fix(spec): check:yaml-examples dispatches ComponentPropsMap over a tagged component example’s properties by claude[bot] · Pull Request #14086 · objectstack-ai/objectstack · GitHub
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

@claudeclaudeBot 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)declarationnodes dispatchedverdict
83page3 (page:header, record:details, record:activity)green
137page4 (object-grid, page:tabs, + 2 nested in items[].children[])green
233, 262, 295FormSectionSchema key=section0green
346, 375, 441FormSectionSchema[] key=sections0green
488FormViewSchema0green
571ResponsiveStylesSchema key=responsiveStyles0green
611PageComponentSchema1 (record:related_list)green
665PageComponentSchema1 (object-metric)green
697PageComponentSchema1 (record:activity)green
711PageComponentSchema0 — custom.approval_timelineskippedgreen (by design)
753PageComponentSchema1 (object-master-detail-form)green
779PageComponentSchema1 (object-kanban)green
980page6 (page:header, record:details, 2x record:related_list, object-metric, record:activity)green
1040FormViewSchema0green

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

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 queueSep 1, 2026
Merged via the queue into main with commit def3b7cSep 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 freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size/mskip-changesetPR has no user-facing published change; bypasses the changeset gatetooling

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

@os-support-ai@claude