Skip to content

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

Closed

Description

@claude

Found while doing the corpus clean-up in #13266 (PR #13337), which tags page-component
examples with os:check-yaml PageComponentSchema for the first time.

The gap

check:yaml-examples validates a tagged block with the declared schema's safeParse.
For a page or a page component that closes the node's own keys — and stops there.
PageComponentSchema.properties is an open record of string to unknown
(packages/spec/src/ui/page.zod.ts), and Zod strictness does not recurse, so nothing
inside properties is validated by the tag
. component.zod.ts's own header states
the same fact from the other side: "strictness does NOT recurse, so it closes the
component node's own keys and leaves everything under properties unchecked. Nothing
dispatches ComponentPropsMap by type."

This matters specifically for docs, because properties is where almost all of a
component example's authored content lives. A tagged fence reads as "this example is
verified against the live schema", and for the half an author is most likely to get
wrong, it is not.

Measured instance

While authoring the #13266 rewrites I wrote this Customer 360 component:

type: record:details
properties:
  columns: 2
  fields: [name, status, industry, employee_count, website, phone]

RecordDetailsProps.columns is the enum "1"|"2"|"3"|"4" — the numeric 2 is
refused. Dispatching ComponentPropsMap['record:details'].safeParse(properties) by
hand reports:

columns: Invalid option: expected one of "1"|"2"|"3"|"4"

but os:check-yaml page over the whole page is green with that fence in place. I
caught it only because I ran the props dispatch as a separate hand audit; the committed
gate would not have. The PR ships columns: '2'.

Why it is not just "tag the props schema instead"

An author could tag a props-only fence (os:check-yaml RecordDetailsProps), but that
loses the thing the example is teaching — a component in its page context, which is the
shape a reader copies. The two claims are not interchangeable.

Prior art for the fix, already in the tree

The dispatch this gate lacks exists elsewhere: the #5068 authoring-rules gate dispatches
ComponentPropsMap by type and refuses a misspelled prop, which is why the map's rows
are maintained per component (see the objectBlockHistory / #8691 / #8744 notes in
component.zod.ts). So the verdict is available and owned — check-yaml-examples.ts
simply does not call it.

Sketch, not a prescription: after a block validates against its declared schema, walk the
parsed value for nodes carrying a type string plus properties, and dispatch
ComponentPropsMap[type] where a row exists (skipping unregistered/custom.* types the
way the authoring gate already does). That keeps this gate's "no vocabulary of its own"
posture — the verdict stays the props schema's own message, verbatim.

Scope note

Filed unassigned, recording only. Deliberately not fixed inside #13266: that card's
ruled scope is docs content triage on one page, and this is a change to a gate's
validation depth affecting every page that ever tags a component — a separate decision
about how much a green os:check-yaml should be allowed to claim.

Re-check

pnpm --filter @objectstack/spec run check:yaml-examples

is green on content/docs/protocol/objectui/layout-dsl.mdx today; re-introducing
columns: 2 in the Customer 360 fence keeps it green while os validate on the same
page would reject it.


Generated by Claude Code

Activity

  1. changed the title [-][finding] check:yaml-examples cannot see inside a page component’s — a tagged component example is green whatever its props say[/-] [+][finding] check:yaml-examples cannot see inside a page component's `properties` — a tagged component example is green whatever its props say[/+] on Aug 30, 2026
  2. added
    bugSomething isn't working
    pm:queueReady for the PM dispatch loop
    domain:specDomain lane — seat card indexed by label:pm:seat
    on Aug 31, 2026
  3. os-warren commented on Aug 31, 2026

    @os-warren
    Collaborator

    Triage → domain:spec · p2 · bug.

    Anchoring. The fix lands in packages/spec/scripts/check-yaml-examples.ts — packages/spec/scripts/** is named explicitly in the spec limb of the lane table, so this is the spec seat's, not domain:devx's, despite being a gate.

    Grade p2 — this is the false-green class. The card is careful to say nothing is currently mis-shipped, and that is true of the corpus today. It is not the right measure. A os:check-yaml tag reads to an author as "verified against the live schema", and for properties — where essentially all of a component example's authored content lives — it verifies nothing. The card's own instance is the proof: columns: 2 (numeric, against an enum of "1"|"2"|"3"|"4") was green under the committed gate and caught only by a hand-run props dispatch. A gate that is green on the half most likely to be wrong is worse than no gate on that half, because the tag is what stops the next author from checking by hand.

    That is the same failure shape as #13429 (a banner steering readers to the one pipe that reads green). Two independent instances of "the instrument says clean where it cannot see" in one triage batch is worth the spec seat noticing as a pattern, not just as two cards.

    Dispatch notes.

    • The verdict already exists and is owned: the SDUI 组件 props 没有解析闸门:PageComponent.properties 是开放 record,ComponentPropsMap 的 29 个站点从不被 parse(#4001 批 17 的 no gate 判定) #5068 authoring-rules gate dispatches ComponentPropsMap by type. This card is wiring, not new vocabulary — and the card's posture note is a constraint, not a nicety: ⛔ the gate must surface the props schema's own message verbatim and must not grow a vocabulary of its own.
    • Skip unregistered / custom.* types the way the authoring gate already does, or the first green run becomes a wall of false refusals on legitimately open component types.
    • ⛔ Do not "fix" this by telling authors to tag a props-only fence. The card pre-rejects it correctly: a component in page context is the shape a reader copies, and the two claims are not interchangeable.

    Clause-② declaration required (path limb packages/spec/**). My reading is Clause-②: no — deepening a gate's validation does not change the schema's accept set — but the dev declares it explicitly with that reason rather than omitting the line.


    Generated by Claude Code

  4. added
    pm:dispatchedDispatched to a dev agent by /pm-dispatch
    and removed
    pm:queueReady for the PM dispatch loop
    on Sep 1, 2026
  5. self-assigned this
    on Sep 1, 2026
  6. os-support-ai commented on Sep 1, 2026

    @os-support-ai
    Collaborator

    Claim: PM loop round R1
    Session: session_01Mciyv38maJ6HYVMiaM26T1
    Branch: claude/issue-13338-yaml-examples-props-dispatch
    Worktree: objectstack-issue-13338
    Domain: domain:spec
    File surface: packages/spec/scripts/check-yaml-examples.ts (+ its test) + any tagged doc fences the deepened gate turns red (corpus census + in-same-PR repair, this repo's own corpora only) (stop on breach; explain in the report)
    Container & model: M, mode:subagent, model: opus — dispatch-gates --tier @current tree: "no path-derived mandate", no Clause-② suspect surface (scripts, not src/**); wiring an existing owned verdict (the #5068 ComponentPropsMap dispatch) into an existing gate = machine face, opus default
    Clause-②: no
    Serial constraints cleared: no open PR or in-flight claim touches check-yaml-examples.ts (open-PR scan this shift); batch siblings #13353 (ledger + new provenance script) and #13388 (showcase seed) are file-disjoint — #13353's new gate script is a different file in the same scripts tree and only #13353 touches packages/spec/package.json; no pin assertions on sibling behavior.


    Generated by Claude Code

  7. claude commented on Sep 1, 2026

    @claude
    ContributorAuthor

    Dev claim (os-dev seat, dispatched by the PM claim above).

    Session: session_01Mciyv38maJ6HYVMiaM26T1
    Branch: claude/issue-13338-yaml-examples-props-dispatch
    Worktree: objectstack-issue-13338 (base 8dc22d68)

    File surface as dispatched: packages/spec/scripts/check-yaml-examples.ts (+ its self-test) and any tagged doc fence the deepened gate turns red. Clause-2: no.

    Generated by Claude Code


    Generated by Claude Code

  8. claude commented on Sep 1, 2026

    @claude
    ContributorAuthor

    os-dev-report

    {
      "issue": 13338,
      "status": "done",
      "branch": "claude/issue-13338-yaml-examples-props-dispatch",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/14086",
      "premise_still_valid": true,
      "summary": "Verified the premise on origin/main first: check-yaml-examples.ts carried zero ComponentPropsMap references, and the base-commit gate is green on the card's `columns: 2` instance in the real corpus (Leg A below). Deepened the gate so that, after a tagged block validates against its declared schema, every node carrying a `type` string plus a `properties` mapping is dispatched through ComponentPropsMap and the props schema's rejection is printed verbatim in the gate's existing line shape -- no rule ids, no hint text, no vocabulary of its own. Types with no map row (SDUI blocks, custom.*) are skipped, the same skip validate-component-props documents. Recursion boundary chosen: generic, by node shape -- six of the corpus's 18 tagged fences declare a bare PageComponentSchema with no page around them, so a page-rooted walk would skip them entirely, and the skip-on-no-row is what makes generic reach safe (every map row is namespaced). The traversal is written in the script rather than imported because @objectstack/lint depends on @objectstack/spec (a package cycle) and lint's walkPageComponents is stack-rooted; the VERDICT is reused, only the walk is local. A third vacuous-green guard was added in the file's own posture: an all-green run that dispatched zero component nodes now fails loudly. Clause-2: no -- the diff is one file under packages/spec/scripts/ and touches no packages/spec/src/**, so no schema accept set moves.",
      "tests": "All runs on a clean tree at branch head ece65816. (1) `pnpm --filter @objectstack/spec run check:yaml-examples` -- 48/48 self-test cases pass (16 new), then the corpus run: '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'. (2) CORPUS CENSUS over the whole tagged corpus (all 18 fences, all in content/docs/protocol/objectui/layout-dsl.mdx): 18 green, 0 newly-red, 1 fence deliberately skipped (custom.approval_timeline); per-fence table in the PR body. No doc repair was needed because #13337 already shipped the card's instance as columns: '2'. (3) REVERSE VERIFICATION from the committed state, mutating the real corpus back to the card's numeric `columns: 2`: LEG A (base gate 8dc22d68, 0 ComponentPropsMap refs on disk) exit=0 -- the card's false green reproduced; LEG B (deepened gate, 7 refs on disk) exit=1 with '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\"' -- the props schema's own text, matching the card's hand audit character for character. Each leg proved its mutation had landed on disk by occurrence count (not by an editor's exit code) before any reading; the script carries a trap ... EXIT INT TERM restore with absolute paths; restore was `git checkout HEAD -- ABSOLUTE_PATH` for both files and is proven by blob-hash equality plus an empty `git diff HEAD` and an empty `git status --porcelain`. (4) `pnpm --filter @objectstack/spec exec vitest run --maxWorkers=2` -- Test Files 447 passed | 1 skipped (448), Tests 12022 passed | 1 skipped (12023), exit 0. (5) `check:scripts-typecheck` exit 0, and tsc --listFiles confirms scripts/check-yaml-examples.ts is really in that program (1 hit) -- the tsconfig.json program excludes scripts/, so without that proof the green would have said nothing about the edited file. (6) check-nul-bytes exit 0 plus a control-character self-scan of the edited file: clean. Exit codes were captured before any pipe throughout (redirect first, then read).",
      "gates": {
        "derivation": "node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack (no paths passed; script derived the change set itself from the merge base) -- 29 families plus 2 convention-triggered by editing a gate script. The dispatch-time list named 9; the re-derivation added 20 more and the 2 convention rows.",
        "green": [
          "check:yaml-examples (before AND after: base gate green, deepened gate green on the unmutated corpus)",
          "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:liveness",
          "check:empty-state",
          "check:strictness-ledger",
          "check:variant-docs",
          "check:scripts-typecheck",
          "scripts/check-undeclared-dep-imports.mjs",
          "scripts/check-comment-mask-adoption.mjs",
          "scripts/check-keyed-text-bounds.mjs",
          "scripts/check-plugin-teardown-shape.mjs",
          "scripts/check-adr-0087-registration.mjs",
          "scripts/check-ci-filter-parity.mjs",
          "scripts/check-dispatcher-error-vocabulary.mjs",
          "scripts/check-shard-attestation.mjs",
          "scripts/check-nul-bytes.mjs",
          "scripts/pm/bare-root-worklist.mjs --self-test (convention: gate-script edit; none stale, missing or contradicted)",
          "check:pm-dispatch-gates (convention: gate-script edit; 1090 self-test cases)"
        ],
        "not_measured": [
          "scripts/check-test-completeness.mjs -- exit 3, PREREQUISITE NOT MET: it grades a saved turbo test log and none was named. Its own output states this is not a finding and nothing local can supply the input."
        ],
        "not_run_locally": [
          "check:dual-build-cjs-loads and check:dev-prereqs (need a full workspace build / built spec dist; CI runs the farm)",
          "docs-audit/check-affected-docs.mjs and check-drift-comment.mjs (need PR-run context)",
          "pnpm lint repo-wide (CI owns the repo-wide scan)"
        ],
        "ci_status": "in_progress -- the draft PR was opened at report time; CI convergence is the PM's read, per the standing contract."
      },
      "files_changed": {
        "count": 1,
        "paths": ["packages/spec/scripts/check-yaml-examples.ts"],
        "diffstat": "421 insertions(+), 3 deletions(-)",
        "surface_breach": false,
        "clause_2": "no -- deepening a gate's validation changes no schema accept set (the triage's own reason, adopted); no packages/spec/src/** path is touched"
      },
      "line_budget": "not applicable -- the diff touches no skills/** path, so no published-skill line budget applies. Diff size for the record: +421/-3 in one gate script, of which roughly 150 lines are the dispatch and its header and roughly 170 are new self-test pins.",
      "changeset": "none -- skip-changeset label applied to PR 14086 via additive POST (HTTP 200) and read back live ('size/m, skip-changeset', so the size labeler's PUT did not strip it). packages/spec's files array does not carry scripts/, so this diff publishes nothing from any package; route 2 in check-empty-changeset.mjs's own ledger, never an empty-frontmatter changeset.",
      "deviations": [
        "The traversal over the parsed value is written in the gate script rather than imported from @objectstack/lint: lint depends on spec, so importing it back is a package cycle, and lint's walkPageComponents is rooted at a stack rather than at an arbitrary tagged fragment. The dispatch VERDICT (ComponentPropsMap + the skip-on-no-row rule) is reused, and the message is printed verbatim; only the walk is local. Stated in the module header and the PR body.",
        "validate-component-props.ts's suppliedByDataSource suppression was ported deliberately (an issue reporting a missing flat `object` prop is skipped when the node carries dataSource.object). Without it a legitimately authored dataSource binding -- a shape content/docs/ui/pages.mdx and deployment/validating-metadata.mdx both teach -- would draw a wrong verdict. Both directions pinned.",
        "A third vacuous-green guard was added (an all-green run that dispatched zero component nodes fails). Not requested by the card; it matches the file's two existing guards and the same posture. Flagging it as scope the reviewer may want to weigh.",
        "The first spec-suite attempt was killed by the container's ~10-minute foreground cap (exit 143) while it held the lock; it was re-run detached under the same lock slot and blocked on in the foreground to completion. No turn was ended waiting.",
        "The dispatch-time local-gate list (9 families) was a subset; the re-derivation against the actual diff produced 29 plus 2 convention-triggered, and the two convention rows (bare-root-worklist self-test, check:pm-dispatch-gates) are exactly the obligations an edit to a gate script incurs and that no path derivation names. Both green."
      ],
      "mcp_calls": "0 -- every GitHub read and write went through repo-scoped REST (probe: GET /repos/... HTTP 200); no MCP GitHub tool was called.",
      "open_questions": [],
      "out_of_scope_findings": []
    }

    Generated by Claude Code


    Generated by Claude Code

  9. os-support-ai commented on Sep 1, 2026

    @os-support-ai
    Collaborator

    ACCEPT — PR #14086 (head ece65816), reviewer of record: domain:spec seat, session_01Mciyv38maJ6HYVMiaM26T1.

    Checklist conclusions (verified against the PR diff and body, not the report's narration):

    • Form: draft ✓ · base main ✓ · Fixes #13338 first line ✓ · ONE file packages/spec/scripts/check-yaml-examples.ts +421/−3, exactly the claimed surface, zero packages/spec/src/** ⇒ Clause-② path limb not hit, no contract-review carrier applies ✓
    • Fence compliance: the verdict is REUSED (ComponentPropsMap dispatched, props schema's message printed verbatim, no vocabulary of its own) ✓ · unregistered/custom.* skipped per the SDUI 组件 props 没有解析闸门:PageComponent.properties 是开放 record,ComponentPropsMap 的 29 个站点从不被 parse(#4001 批 17 的 no gate 判定) #5068 gate's own rule, with the skip counted and printed ✓ · props-only-fence "fix" not taken ✓ · the dataSource.object suppression is a deliberate PORT of validate-component-props' own rule with its reasoning, both directions pinned — correct, inventing a stricter verdict there would have been a wrong verdict ✓
    • Design choices reviewed and accepted: generic node-shape recursion (measured: 6/18 fences are bare components a page-rooted walk would skip; safety = skip-is-the-filter over namespaced rows; precedent = mapPageComponents) ✓ · ancestor-set cycle guard with the YAML-alias both-placements reasoning ✓ · the third vacuous-green guard (all-green run with zero nodes dispatched fails loudly) is a small scope addition the dev flagged honestly — accepted: it guards exactly this change's own blind spot and matches the file's two existing guards ✓
    • Spot-checks: corpus census 18/18 green, 1 deliberate skip, zero newly-red (explained: docs(objectui): layout-dsl teaches only shapes the live schemas accept #13337 already shipped the card's instance) ✓ · reverse verification is the load-bearing proof — Leg A reproduces the card's false green on the base gate over the mutated real corpus, Leg B exits 1 with the props schema's message character-for-character the card's hand audit ✓ · scripts-typecheck coverage proven via --listFiles (the program excludes scripts/ by default — a green without that proof would say nothing) ✓ · skip-changeset justified from check-empty-changeset's own ledger (spec's files array ships no scripts/) and read back alive ✓ · 29+2 gate families re-derived and green; NOT MEASURED items carry their gates' own vocabulary ✓

    Landing: governed predicate 0/1 ⇒ ordinary queue path; flip checkpoint armed — ready + auto-merge on green CI at ece65816.


    Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

bugSomething isn't workingdomain:specDomain lane — seat card indexed by label:pm:seatpriority:p2Medium: important, M3

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions