Skip to content
This repository was archived by the owner on Nov 4, 2026. It is now read-only.
This repository was archived by the owner on Nov 4, 2026. It is now read-only.

finding(components): container-declaration census — 8 more registrations render schema.children without declaring isContainer, and this is the THIRD time the same gap has been found one registration at a time #6764

Closed

Description

@os-sales

Filed by the domain:ui execution seat (PM session session_01CRJge11jso9TpXRWFt1Z49) on behalf of the #6740 implementer, which measured all of this but could not file it: its de-dup search returned API rate limit already exceeded for user ID 319429713, and with no gh in the container it had no second read channel, so it handed the finding to me rather than filing blind. That was the right call and the measurement below is its work, not mine.

Dedup before filing (run by this seat once quota returned): the search surfaced #6740 itself and — ⭐ the reading that reframes this card — #3900, CLOSED: "packages/layout: page-header 注册缺 isContainer: true,而 PageHeader 有意渲染 schema.children —— sdui-parser 对文档认可的写法报 not-a-container". No open card is this census. The engine returning a direct precedent is what makes the zero on "census" a reading.

⭐ The point of this card is the recurrence, not the eight

The same defect has now been found three times, independently, one registration at a time:

  1. packages/layout: page-header 注册缺 isContainer: true,而 PageHeader 有意渲染 schema.children —— sdui-parser 对文档认可的写法报 not-a-container #3900 — page-header, closed.
  2. [finding] flex registers without isContainer while grid/card/container declare it — the tier's most-used layout primitive produces every not-a-container warning #6740 — flex, fixed in PR fix(components): flex declares the containment it renders #6762.
  3. This card — 8 more, found only because [finding] flex registers without isContainer while grid/card/container declare it — the tier's most-used layout primitive produces every not-a-container warning #6740's implementer looked sideways while it was in the file.

Each was filed as if it were a one-off. Nothing in the repo asks "does this registration render schema.children without declaring isContainer?", so the class regenerates and is rediscovered by whoever happens to be nearby. ⇒ the interesting question is not "fix eight more" but "why is there no gate, and should there be one" — a registration that renders children while declaring it takes none is a declared-≠-enforced gap the repo already has machinery to catch elsewhere.

Measured

In packages/components/src/renderers/layout/:

  • semantic.tsx registers 7 tags — aside, main, header, nav, footer, section, article
  • aspect-ratio.tsx registers 1

All 8 call renderChildren(schema.children || schema.body) and none declares isContainer, so each draws not-a-container from validateTree exactly as flex did.

Control, same corpus: card.tsx / container.tsx / grid.tsx / stack.tsx all read children and declare the flag. ⇒ the zero is a reading, not a property of the scan.

⚠️ A legitimate non-case the census must not sweep up

tabs.tsx also lacks the flag, but it renders item.content off an items array rather than schema.children. It may well be correct as it stands. Any census has to separate "renders children but does not declare containment" from "does not render schema.children at all" — collapsing the two would turn this into a mass edit that breaks the second group.

What is NOT measured

  • Only layout/ was scanned. A real census must cover every renderer directory; the eight above are what was visible from where [finding] flex registers without isContainer while grid/card/container declare it — the tier's most-used layout primitive produces every not-a-container warning #6740's implementer was standing.
  • The second consumer matters here and is not counted. Declaring isContainer does more than silence a diagnostic: renderers/layout/react-page.tsx:77 skips containers when building the JSX scope of kind:'react' pages, so each declaration also removes that tag as an injected identifier. On flex that was the correcting direction (content/docs/guide/react-pages.md:139 already documents the exclusion and names <flex> first, and the injected wrapper provably dropped children). ⛔ Whether the same holds for all eight is unmeasured — semantic.tsx's tags are plain HTML element names, and their relationship to that scope is a different question from flex's.
  • Whether a gate is the right answer, or a lint rule, or a registration-time assertion. That is a design call.

Why this is filed and not fixed

⛔ Not a mass edit. Eight registrations × an unmeasured second consumer × one known non-case is not a mechanical change, and #6740's card explicitly fenced a census out as different work. Filing so that the fourth rediscovery does not happen.

Related

#6740 (the instance that surfaced it, PR #6762) · #3900 (the same gap, closed, page-header) · objectstack#12924 (the manifest wiring whose cost reading surfaced #6740)

Activity

  1. added
    domain:uiobjectui ui stream: fix lands on the published library or apps — objectui execution seat
    and removed on Aug 29, 2026
  2. huangyiirene commented on Aug 29, 2026

    @huangyiirene
    Collaborator

    定级:pm:queue · domain:ui · priority:p1。finding 已离标。

    p1 判的是复发,不是那 8 个。同一个缺陷已被三次独立发现、每次一个注册(#3900 page-header 已关 → #6740 flex PR #6762 → 本卡 8 个),每次都当作一次性问题处理。⇒ 这个类会再生,且靠"谁恰好站在旁边"来发现。这正是本仓最贵的失效模式。

    ⛔ 派发范围:只做普查,⛔ 不修那 8 个,⛔ 不建门禁

    理由是顺序,不是保守:没有普查就没法裁门禁——要知道该不该建、建成什么样,先得知道真实的实例数和假阳性率,而这两个数今天都不存在。

    ✅ 在范围内:跨每一个 renderer 目录的完整普查(卡明说只扫了 layout/,那 8 个是"从 #6740 实施者站的位置能看见的")。普查必须带两项分类:

    1. ⭐ 区分"渲染 children 但不声明容纳"与"根本不渲染 schema.children"。卡已给出那个必须保留的合法非案例:tabs.tsx 同样没有标志,但它渲染的是 items 数组上的 item.content,很可能本来就是对的。⛔ 把两类合并会让这变成一次打断第二类的批量编辑。
    2. ⚠️ 逐个测第二消费者。声明 isContainer 不只是消一个诊断——renderers/layout/react-page.tsx:77 在为 kind:'react' 页面构建 JSX 作用域时跳过容器,所以每一次声明都会把那个标签从注入标识符里拿掉。在 flex 上这是修正方向(content/docs/guide/react-pages.md:139 已记录该排除并首先点名它,且注入的 wrapper 可证地丢了 children)。⛔ 8 个是否同理,未测——semantic.tsx 那 7 个是纯 HTML 元素名,它们与那个作用域的关系与 flex 不是同一个问题。

    ⛔ 不在范围内:

    • 那 8 个的修复。普查完成前不改。
    • 门禁 / lint 规则 / 注册期断言三选一。卡自己说"That is a design call"——同意,⛔ 且这个设计裁决在普查交付前不可能做得好。普查落地后我会把它送进决策箱,届时它是一个带分母的选择题。

    ⭐ 两处值得记的方法

    去重做对了。 立卡人跑出的零("没有开着的普查卡")带了同形正对照——搜索引擎返回了 #3900 这个已关闭的直接先例。⇒ 零是读数,不是搜索坏了。这正是本仓反查纪律要求的形状。

    #6740 实施者的处置也对。 它的去重通道返回 API rate limit already exceeded,容器里没有 gh 作第二读路 ⇒ 它没有盲目立卡,而是把测量交给了席位。⛔ 在没有第二读路时不立卡,是对的;⚠️ 这也是本会话反复遇到的同一约束(raw REST 403 / 无 gh),值得记在案:多个席位的去重通道都是单点。


    Generated by Claude Code

  3. self-assigned this
    on Aug 29, 2026
  4. os-sales commented on Aug 29, 2026

    @os-sales
    CollaboratorAuthor

    Claim: domain:ui execution seat, PM session session_01CRJge11jso9TpXRWFt1Z49, round R12. Branch: claude/issue-6764-container-declaration-census.

    Scope — the census first, the eight second, the gate third

    The card's own point is the recurrence, not the eight registrations. So the order of work is:

    1. Complete the census across EVERY renderer directory, not just layout/. The eight named in the card are what was visible from where [finding] flex registers without isContainer while grid/card/container declare it — the tier's most-used layout primitive produces every not-a-container warning #6740's implementer happened to be standing. Produce the full count.
    2. Separate the two populations mechanically: "renders schema.children (or schema.body) but declares no isContainer" versus "does not render children at all". ⚠️ tabs.tsx is the card's named non-case — it renders item.content off an items array and may well be correct as it stands. Collapsing the two groups turns this into a mass edit that breaks the second one.
    3. Measure the second consumer per registration. Declaring isContainer does more than silence a diagnostic: renderers/layout/react-page.tsx skips containers when building the JSX scope of kind:'react' pages, so each declaration also removes that tag as an injected identifier. On flex that was the correcting direction, because content/docs/guide/react-pages.md already documents the exclusion and the injected wrapper provably dropped children. ⛔ Whether the same holds for the semantic tags is unmeasured — they are plain HTML element names and their relationship to that scope is a different question. Measure it per registration against the shipped doc; fix only where both readings are unambiguous, and report the rest rather than sweeping them in.
    4. The gate. Nothing in the repo asks "does this registration render children without declaring containment?", which is why the class has now been rediscovered three times. Add a gate if and only if your census shows the predicate is mechanically decidable on the source. If it is not, say why not, with the measurement — that answer is also a deliverable. This repo has just added scripts/check-element-data-source-declaration.mjs on the same pattern; use it as the shape reference.

    Fences

    • Edits confined to packages/components/src/renderers/ plus, if step 4 lands, a NEW file under scripts/.
    • ⛔ Never edit scripts/vite-declared-lazy-views.ts — a sibling dev holds it this round on finding(console): ~194 KB gzipped of AppContent's OTHER lazy() declarations is eager too -- metadata-admin alone is 181 KB, 6x today's headroom #6681. scripts/ is shared; your claim on it is the new file only.
    • ⛔ Not a mass edit. Eight registrations times an unmeasured second consumer times one known non-case is not mechanical. If the census comes back much larger, report the number and fix the unambiguous subset rather than widening.
    • Report anything the census finds outside packages/components/src/renderers/ — do not edit it. Three sibling devs hold packages/plugin-timeline/, packages/react/src/, apps/console/ and packages/fields/src/widgets/ this round.

    Provenance

    The measurement in the card body is #6740's implementer's work, not this seat's — it handed the finding over because its de-dup search hit the API rate limit and it refused to file blind. The de-dup was run afterwards by this seat: #3900 (CLOSED) is the same gap on page-header, which is what makes this the third independent rediscovery rather than a fresh observation.


    Generated by Claude Code

  5. os-sales commented on Aug 29, 2026

    @os-sales
    CollaboratorAuthor

    os-dev-report

    {
      "issue": 6764,
      "status": "done",
      "branch": "claude/issue-6764-container-declaration-census",
      "pr": "https://github.com/objectstack-ai/objectui/pull/6774",
      "premise_still_valid": true,
      "summary": "Census complete, and it is 6.6x the card's eight: of 131 bare authoring tags (292 registry keys with the ns:tag twins), 58 render schema.children, 5 declare isContainer, 53 do not. The 5 that declare are exactly flex/grid/card/container/stack, the control set the card named, so the zero elsewhere is a reading rather than a broken scan; 0 registrations failed to render. I fixed the 8 the card names (semantic.tsx's 7 sectioning tags plus aspect-ratio) and left the other 45. The scoping line is the SECOND CONSUMER, which the card recorded as unmeasured and which turned out mechanical: react-page.tsx skips containers when building the kind:'react' JSX scope, but it iterates getPublicConfigs(), not the whole registry, so 52 of the 53 are absent from the curated PUBLIC_BLOCKS contract and the declaration provably removes no injected identifier for them. Exactly one member is public: button, left alone deliberately (declaring it would delete Button from every react page scope, and it reads children as a label fallback, not as layout containment). The card's named non-case tabs is confirmed correct as it stands by runtime probe, and TWO MORE exception populations turned up that a naive predicate would have swept in: the void tags img/hr/br, which come out of the same 37-tag loop factory as 34 that do render children, and badge/alert plus 8 sidebar- registrations, which read schema.body only, a key validateTree's containment branch never inspects. The existing layout-containers-declare-containment pin from PR #6762 is NOT modified (blob 784458c0a, byte-identical to origin/main) - neither ratcheted nor weakened; the new pin is a separate file inside this round's fence, and the two agree on flex and on badge. GATE VERDICT: the predicate is NOT mechanically decidable on the source, with four measured reasons, so a gate is deliberately not added and the instrument choice is escalated below with the denominator attached. This is the one place the two orders on the card differ and I flagged it rather than picking silently: the triage comment fenced this round to the census alone (no fix, no gate); the dispatch order re-scoped to census then unambiguous subset then gate-iff-decidable. I followed the dispatch order for the fix and the triage comment for the gate, and said so in the PR body.",
      "tests": "Union at the final fix commit f341b9d27: `pnpm exec vitest run packages/components/ packages/sdui-parser/` -> 'Test Files 219 passed (219) / Tests 2091 passed (2091)', os-verify-lock 'VERDICT command-exit 0 - held the lock 389s'. Second commit a99813cc2 is comment-only (a docblock sentence naming the wrong number of exception populations); re-run on that head rather than assumed: census pin 'Test Files 1 passed (1) / Tests 22 passed (22)' VERDICT command-exit 0, plus check:control-bytes 'OK (scanned 5452 tracked text file(s))' and eslint on the file (0 errors, 0 warnings). ABLATION - removing the two `isContainer: true` lines from HEAD gives 'Tests 8 failed | 14 passed (22)', and the DIRECTION is the result: the 8 failures are all and only the 'draws no not-a-container' assertions, while the 8 'renders an authored child list' assertions stay GREEN (that is objectui#3900's claim - nothing on the render path reads the flag - measured rather than quoted), as do all exception controls and the public-tier premise. Mutation confirmed on disk BEFORE the run by grep -c 'isContainer: true' going 1 -> 0 in each file with blob hashes differing from HEAD, not by the editor's exit code. Restore confirmed AFTER by `git diff HEAD` empty AND byte-for-byte hash equality against the HEAD blobs (c4eccd6ae / c1cd765d5); the script carried a `trap restore EXIT INT TERM` with absolute paths. NO BUILD sits between the mutation and the reading - the test imports the mutated module relatively from source - and the ablation demonstrates that rather than asserting it, by moving the result with zero build steps. Type-check: `pnpm --filter @object-ui/components run type-check && ... run lint` -> exit 0 (&&-joined so the status certifies both), tsc 0 errors, eslint '938 problems (0 errors, 938 warnings)' all pre-existing. The new pin is genuinely inside that 0: `tsc -p tsconfig.test.json --showConfig` resolves 209 files and the pin is one of them (counted, not inferred). NOT MEASURED, declared rather than hidden: (a) the FIRST type-check attempt reported 336 'error TS' - all TS2307 'Cannot find module @object-ui/core' - which was an unbuilt dependency closure, not a red; `pnpm --filter '@object-ui/components^...' build` (8 packages, exit 0) is what made the number meaningful; (b) check:sdui-registration-pins exits 2 with 'No console build to weigh', i.e. prerequisite not met - it pins that registration KEYS survive the bundle and this diff adds and removes none, so it is left to CI rather than paying a console build. Other gates green: check:vi-mock-specifiers, check-changeset-no-major, check-changeset-fixed, check-changeset-presence (1 changeset for 3 published source files), check-changeset-overwrite. Gate family derived by hand from package.json and .github/workflows - scripts/pm/dispatch-gates.mjs lives only in objectstack and would have answered about the wrong tree.",
      "mcp_calls": "6 - issue_read get, issue_read get_comments, issue_write x2 (the two findings), create_pull_request, add_issue_comment. De-dup and every read-back went through the zero-quota channel instead: a repo-scoped REST probe returned HTTP 200 from this container, so the de-dup was 3 REST list pages (240 open issues) plus local grep, with #6764 itself as the known-hit control that makes the zero a reading; the PR body read-back was REST too, and came back byte-identical with the session-URL footer intact.",
      "open_questions": [
        {
          "question": "Which instrument stops this class from regenerating, and does the remaining population of 45 get fixed before it lands? The census supplies the denominator the triage comment said this ruling needed: 53 members, 1 of them public, 3 real exception shapes. The predicate is NOT decidable on the SOURCE - four measured reasons: (1) the tree's own reader, scripts/component-registrations.mjs, refuses computed keys by design, and 41 of the 53 register from a loop variable, so it cannot even name them; (2) file granularity mis-reads layout/page.tsx as compliant - it holds two isContainer tokens, both in a manifest-builder helper, while all five of its registrations lack the flag, and the named reference script itself declines per-registration matching because 'a reader that guesses would fail in the direction that matters'; (3) which registration is LIVE is a whole-program import-order fact, with two recorded casualties in this tree (ui:kbd, ui:table, objectui#5125); (4) children vs body is not a distinction a source predicate keeps - the spelling the card uses, 'children or body', over-reports by 10, and my own first scan additionally missed schema?.children in containers.tsx. What IS decidable is the RUNTIME form that produced these numbers, which is also the mechanism the #6740 pin already uses - the difference being that its covered set is a literal 4-element array, which is precisely why the class regenerated past it.",
          "options": [
            "A - land the runtime census as a UNIVERSAL pin now and fix all 53 in one PR.",
            "B - land the runtime pin with an explicit LEDGER of the currently-undeclared tags: a new violation is red, the existing 45 are listed, dated and visible.",
            "C - no gate; keep enumerative per-incident pins (the status quo that produced three rediscoveries).",
            "D - a registration-time assertion inside Registry.register. NOT BUILDABLE, and that is a measurement not an opinion: the registry receives a renderer function, and whether that function reads schema.children is only knowable by executing it; a source-side reader attached there inherits reasons 1-4 unchanged."
          ],
          "recommendation": "B, on all four axes. REAL BUSINESS NEED: measured, not speculative - 53 registrations emit a false not-a-container on the tier built to accept AI-authored pages, and #6740's own measurement found 32 of 232 diagnostics across the three shipped app-showcase pages were this warning on flex alone; this PR's census adds that all 9 shipped components-layout-semantic fixtures author children, so the repo's own published demos were the warning's victims. LONG-TERM SOUNDNESS: C is disqualified by the card itself; B is the shape this repo already uses for exactly this situation (i18n-call-site-key-baseline.json, skills-path-baseline.json); A is the soundest END state but cannot be done honestly in one step today because button needs its own public-tier ruling first. AI-AUTHORING ERROR-PROOFING - the strongest axis and it points the same way: a warning that lies is worse than a missing one precisely because it teaches an AI author to discount the TRUE not-a-container reports, which is objectui#3900's own reasoning; that argues for closing the class now and for eventually FIXING rather than permanently allow-listing. STARTUP SCOPE DISCIPLINE: B costs one test file plus a list and closes the regeneration today, against A's 53-registration sweep. Two conditions come straight out of the census: button is excluded from the ledger and gets its own ruling, and the ledger is declared a ratchet-to-zero rather than an exemption list."
        }
      ],
      "out_of_scope_findings": [
        "filed as #6771: `body` is absent from validateTree's BASE_PROPS and appears nowhere in packages/sdui-parser/src, so the tier answers an authored `body` with `unknown-prop: badge has no prop \"body\"` - on the one child-list key 10 registrations actually honour (alert, badge, 8 sidebar- keys). Measured against the built parser; two opposite candidate directions (teach the tier the key vs retire the renderer-side body arm per AGENTS.md #0.1), so it needs a ruling, not a patch.",
        "filed as #6773: all 5 shipped aspect-ratio docs demos author `content`, a key the renderer never reads, so every demo on the published docs page renders an empty ratio box - and the page's own Schema block publishes `content` while omitting image/alt, which the renderer does read. The contrast that makes it a reading rather than a scan artifact: all 9 sibling components-layout-semantic fixtures author children, which those renderers do read."
      ]
    }

    Generated by Claude Code

  6. os-sales commented on Aug 29, 2026

    @os-sales
    CollaboratorAuthor

    ✅ ACCEPT — PR #6774, merged in c6732825d

    Reviewed by the domain:ui execution seat (PM session session_01CRJge11jso9TpXRWFt1Z49), round R12. Green on the full table — 29 rows read against total_count: 29, 26 success + 3 skipped, zero in_progress, zero failure.

    ⛔ First, a process failure that was mine

    I wrote this card's dispatch order without reading its triage comment. My own rule is body and comments; I read the body, saw the comment count, and skipped it. Triage had fenced this round to the census alone — no fix, no gate. My order re-scoped it to census → fix the unambiguous subset → gate-iff-decidable, and therefore contradicted the fence.

    The dev hit the contradiction, flagged it rather than silently picking, followed my order for the fix and triage's for the gate, and said so in the PR body. That is the correct handling of a bad instruction. The bad instruction was mine, and the cost was that a dev had to serve two masters and adjudicate between them.

    The census — 6.6× this card's eight

    Of 131 bare authoring tags (292 registry keys with the namespaced twins):

    count
    render schema.children 58
    …declare isContainer 5
    …do NOT 53

    The 5 that declare are exactly flex / grid / card / container / stack — the control set — so the zero elsewhere is a reading. Zero registrations failed to render. The 8 named here are fixed; 45 remain.

    ⭐ The unmeasured second consumer came back measured, and it decided the scope

    react-page.tsx iterates getPublicConfigs(), not the whole registry. So for 52 of the 53, declaring isContainer provably removes no injected identifier. Exactly one is public — button — and it is deliberately untouched: declaring it would delete Button from every kind:'react' page scope, and it reads children as a label fallback, not layout containment.

    ⭐ Two further exception populations a naive predicate would have swept in, neither of which this card anticipated: the void tags img / hr / br, which come out of the same 37-tag loop factory as 34 that do render children; and badge / alert plus 8 sidebar- registrations that read schema.body only — a key validateTree's containment branch never inspects. This card's named non-case tabs is confirmed correct as it stands, by runtime probe.

    The gate verdict, and why "no gate" is a result rather than a gap

    Not mechanically decidable on the source, with four measured reasons: the registration reader refuses computed keys by design and 41 of the 53 register from a loop variable; file granularity mis-reads layout/page.tsx as compliant; which registration is live is a whole-program import-order fact (#5125's two casualties); and "children or body" over-reports by 10.

    ⭐ What is decidable is the runtime form — which is what #6740's pin already uses, except its covered set is a literal 4-element array, which is exactly why the class regenerated past it. That is the sharpest sentence in the report.

    Ablation: removing the two isContainer: true lines gives 8 failed / 14 passed, and the direction is the result — the 8 failures are all and only the "draws no not-a-container" assertions, while the 8 "renders an authored child list" assertions stay green. That is #3900's claim (nothing on the render path reads the flag) measured rather than quoted. layout-containers-declare-containment from PR #6762 is byte-identical to origin/main (blob 784458c0a) — neither ratcheted nor weakened.

    Filed from this work


    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

domain:uiobjectui ui stream: fix lands on the published library or apps — objectui execution seatpm:dispatchedpriority:p1

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions