Repository navigation
finding(react): a key authored under the props envelope never reaches a renderer that reads schema — measured as a silent empty data-table next to a working properties twin #6708
Description
Activity
- addeddomain:uiobjectui ui stream: fix lands on the published library or apps — objectui execution seatobjectui ui stream: fix lands on the published library or apps — objectui execution seat
on Aug 28, 2026 huangyiirene commented
on Aug 28, 2026 CollaboratorMore actions分诊:入决策箱 + 四棱块
定级:
needs-user-decision·domain:ui·priority:p1· typeBug。卡面自己写明「a taker would need to decide first: whether this is a defect or the declared design」,且若判缺陷,其中一条手臂是已发布组件的行为变更 ⇒ 命中人工地板。<!-- os-decision-facets -->
一句话问题:
props是文档承认的 config bag 别名,BaseSchema是.passthrough()所以每道门禁都收,但对只读schema的组件渲染器家族它完全不生效 —— 键被静默丢弃,表头照常渲染。先要裁的是:这是缺陷,还是既定设计。这不是推演:卡面 leg 2 与 leg 3 同键、同值、只差一层信封,
properties出两行、props出No results found。第一层裁决 × 真实客户可感成本
判定 代价 设计 props是 React prop 通道,只有element:*家族的readProps()合并两个 bag零代码。但文档必须改口 —— 否则「支持的别名」这句承诺继续骗人 缺陷 别名应当在两个家族都生效 ⇒ 进第二层,三条手臂 若判缺陷,三条手臂
- A — 把
props提升到与properties同权(hoist)。⚠️ 已发布组件的行为变更:今天靠props传真 React prop 的宿主,会突然看到这些键被 hoist 到 node 上。且props与properties同现时,alias 优先级按「读法」相反 —— 配置袋读到 properties,React prop 读到 props #5123 已裁过两 bag 的优先级,动这里必须与那条裁决对齐。 - B — 在
SchemaRenderer层诊断。零行为变更、一个地方、覆盖所有渲染器。⭐ 与data-tableaccepts abindand silently renders its header over an empty body #6575 /data-table: a${...}expression authored in node-leveldatais not evaluated and renders an empty body silently #6665 同形,这条路本仓已经走通两次。 - C — parse 层拒绝。⛔ 今天做不到:受
.passthrough()天花板阻挡(finding(types): BaseSchema's[key: string]: anyleaves every component schema open, so a "declare the surface" fix can never reject a misspelled TOP-LEVEL key #5155 / finding(types): ObjectViewSchema'stableandformslots declare ZERO properties — the same Omit-under-index-signature collapse as #6151, in property position #6269)。
四棱
① 长远合理性:同一拼写在一半渲染器家族生效、另一半静默失效 —— 二义性长期必然继续咬人。B 不消除二义性但让它可见;A 消除二义性但动已发布行为。
② 业务拉动:实测的作者体验是「写了文档承认的别名 → 空表格 + 正确表头」。⚠️ 但今天有没有真实客户正踩着,没数过(见置信缺口)。
③ 防 AI 犯错 —— 分水岭。⭐ 这是标准的成功回执形状:无报错、无警告、表头看起来完全正确。AI 作者从element:*的例子学到「props可用」,搬到组件渲染器上就静默失效,而且每一道门禁都会放行。B 用最小代价把静默失效变成可见失败。
④ 不扩散:B 是一处诊断,净加法极小;A 需清点宿主;C 被天花板挡住。推荐:⛔ 本席不裁「缺陷 or 设计」这一层 —— 它是契约语义问题。若判缺陷,推荐 B:零行为变更 + 两次同形先例已验证这条路。A 留到
props别名整体退役时一起做,不要单独动。⭐ 置信缺口
没有人数过今天有多少 authored 节点把键写在
props下。 本仓 fixture / schema-catalog、hotcrm、以及第三方 authored 元数据都没清点过。这个数字直接改变裁决:- 若为 0 ⇒「设计 + 改文档」就够,连 B 都可能是过度反应;
- 若不为 0 ⇒ 每一个都是今天正在静默失效的页面,优先级要上调,且 B 的诊断得能回溯已有内容。
⇒ 建议裁前先在本仓与 hotcrm 跑一次 authored
props节点清点 —— 成本低,且它可能直接决定答案。低摩擦裁决格式:回「设计 + 改文档」/「缺陷 → B」/「缺陷 → A」/「先清点」。
裁后执行段:判设计 ⇒ 转
pm:queue,落点content/docs/**(domain:ui),只改文档措辞。缺陷 → B ⇒ 转pm:queue,落点packages/react的SchemaRenderer层。缺陷 → A ⇒ ⛔ 必须先与 #5123 的两 bag 优先级裁决对齐,并清点依赖props传真 React prop 的宿主,再走契约档复核。先清点 ⇒ 留决策箱,数字回帖后再裁。
Generated by Claude Code
- A — 把
Claim:
domain:uiexecution seat, sessionsession_01CRJge11jso9TpXRWFt1Z49.Branch:
claude/issue-6708-props-bag-component-renderer-diagnostic.Dispatched as ruled work, executing option B (SchemaRenderer-tier diagnostic) per the 2026-08-29 director ruling — verbatim 「同意」. Option A (hoisting
propsto parity withproperties) was refused in that ruling and is ⛔ out of scope; option C stays blocked on the.passthrough()ceiling (#5155 / #6269).The ruling's precondition travels with the dispatch: the census runs BEFORE the diagnostic level is chosen, and it lands in the PR as evidence — the triage block's confidence gap is explicit that a count of zero and a count above zero point at different answers, so the number is not a formality.
Declared file face for the cross-lane in-flight check:
packages/react/src/— theSchemaRenderertier, specifically the hoist seam that readsproperties.*againstHOIST_PROTECTED_KEYS— plus new pins alongside it.packages/components/src/renderers/**is read-only here: this card changes no renderer's behaviour, which is the whole reason option B was the one taken.
Generated by Claude Code
claude commented
on Aug 29, 2026 claudeboton Aug 29, 2026 – with ClaudeContributorAuthorMore actionsos-dev-report
{ "issue": 6708, "status": "done", "branch": "claude/issue-6708-props-bag-component-renderer-diagnostic", "pr": "https://github.com/objectstack-ai/objectui/pull/6753", "premise_still_valid": true, "summary": "Executed the 2026-08-29 ruling's option 2: a SchemaRenderer-tier console.warn naming a `props` config bag on a component-renderer node and pointing at `properties`. New module packages/react/src/utils/propsBagDiagnostic.ts (pure predicate + message builder + reporter, module-internal, not re-exported), called once per node under __DEV__ beside its #4795 neighbour; `propsWithoutCanonicalKeys(...)` was hoisted out of the createElement call into a local so the spread and the diagnostic read the same bag. Silent for `element:*` and for `view:simple`, the one non-element type measured to read `schema.props`. NO hoisting, no behaviour change. Census ran FIRST per the ruling's precondition and is in the PR body; it is NOT zero (5 authored non-test occurrences, 2 of them live mis-authoring in published teaching material) and those two were filed as #6751 rather than fixed here. The ruling's precondition, the reproduction, the census, the ablation and the NOT MEASURED items are all in the PR body.", "tests": "Union re-run AFTER the final commit, on head aac3e65bc (tree clean). (1) vitest packages/react/src + all 11 census-flagged at-risk suites (the files that author `props` on a component-renderer node, incl. #6665's four-leg pin file and the domProps/DOM-leak sweeps that spy on the console): 'Test Files 74 passed (74)' / 'Tests 1259 passed (1259)', exit 0. (2) pnpm --filter @object-ui/react run type-check -> exit 0 (tsc --noEmit && tsc -p tsconfig.test.json); both new files confirmed present via tsc --listFiles, so this is not a silent exclusion. (3) pnpm --filter @object-ui/react run lint -> '380 problems (0 errors, 380 warnings)', exit 0; all 380 are pre-existing no-explicit-any and none is on a changed line (warning lines all at or below 1265, my edits at 33 and 1334-1406). (4) Gates: check-changeset-presence '1 source file(s) of 1 released package(s) changed, and this change declares 1 changeset(s)'; check-changeset-no-major 'No changeset declares a major bump'; check-control-bytes 'OK (scanned 5604 tracked text file(s))'; check-package-self-import 'No package names itself inside its own src/'; check-lint-coverage '46/46 packages linted, 0 with outstanding errors'; check-vi-mock-specifiers OK; check-shell-escape-residue OK. All exit codes captured by redirect-then-capture, never after a pipe. REPRODUCTION on my base faac0d935 before any edit: probe through the real SchemaRenderer showed `props: { data: expr }` -> React prop data = evaluated array, schema.data absent; `properties: { data: expr }` -> schema.data = the array. Asymmetry holds; expression IS evaluated on both legs. ZERO-BEHAVIOUR-CHANGE pinned directly, not asserted: 7 node shapes captured on faac0d935 with SchemaRenderer.tsx reverted to its committed blob 57c0beb3f, then re-captured with the diagnostic present -- identical sha256 387e04a93bc8bcfa04ac56a84bb2db8f2b5eac574847830a94fedd41af871a0b and empty diff; that reading is embedded in the test as BASE_READING. ABLATION against the committed implementation aac3e65bc: collectDroppedPropsKeys(...) replaced with a null literal; mutation CONFIRMED ON DISK before the run (injected marker grep -c = 1, removed anchor grep -c = 0, blob d54ef23d -> b478043) and the replacement asserts its own anchor hit count so a no-op edit fails loudly. Mutated leg: 5 failed / 35 passed -- the 5 are exactly the pins asserting the diagnostic FIRES, and the 35 that stayed green are the whole behaviour surface incl. BASE_READING (pin 4 from the other side). Restored leg: 40 passed. RESTORE PROVEN not assumed: git diff HEAD empty, restored blob d54ef23d equal to the path's HEAD blob, ablation marker count 0. Mutation ran under trap ... EXIT INT TERM with absolute paths. No rebuild needed and none claimed: this repo's vitest config aliases @object-ui/react to src, so the mutated source is what ran and the red is the proof it was read. NOT MEASURED (reported as neither pass nor fail): check:readme-exports exits 1 with 'type entry ./dist/index.d.ts is not on disk -- run pnpm build first' for 330 bindings across ~32 unbuilt packages -- a full-workspace-build prerequisite; this diff adds no export to any package entry and touches no README, so it cannot move that gate. check:sdui-registration-pins exits 2 with 'Build the console first' -- same class. Repo-wide `pnpm lint` (turbo run lint) not run; the per-package lint task for the only package touched was run instead and is green.", "mcp_calls": "0 - the container's repo-scoped REST reads and writes both work here (probe: GET /repos/objectstack-ai/objectui -> HTTP 200), so the issue body, both comments, the open-issue list used for dedupe, the two new issues and the PR all went through REST. Global /search/issues IS blocked ('sessions are bound to their configured repositories'), so dedupe used the repo-scoped list endpoint plus local grep, exactly the prescribed REST path.", "open_questions": [], "out_of_scope_findings": [ "filed as #6751: two in-repo authored nodes put config under the inert `props` envelope on a component-renderer node -- packages/types/examples/data-display-examples.json authors `flex` with props {direction, gap}, and skills/objectui/guides/page-builder.md authors `statistic` with props {label, value}. Both are published teaching material presented as correct; both render as if the bag were empty. This is the census's non-zero answer. Not fixed here: the PR changes no authored metadata, and the skills half would engage the published-skills budget clause.", "filed as #6752 (label `finding`, observation-class): a non-object `props` is object-spread into indexed React props -- SchemaRenderer's evaluation memo rebuilds the bag with `{ ...newSchema.props }` under a bare truthiness guard, so `props: 'not-a-bag'` reaches createElement as nine props named 0..8. Measured, not deduced: it turned an early version of the degenerate-input pin red. The sibling `properties` branch carries a deliberately WIDER guard and documents exactly this hazard; the `props` branch has no equivalent. Zero in-repo authored nodes carry the shape, so it is latent." ] }
Generated by Claude Code
✅ ACCEPT — PR #6753, head
aac3e65bcReviewed by the dispatching
domain:uiseat. Checks: 29 rows read againsttotal_count29, zeroin_progress, 26 success + 3 conditionally skipped, all fourTest (shard n/4)success. Ready flipped before auto-merge was armed.⭐ The ruling's precondition was discharged, and the answer is NOT zero
The ruling fixed the order — census before diagnostic level — and the triage block said why in as many words: a count of 0 means "design + fix the docs" might suffice, while a non-zero count means every one of them is a page silently failing today.
The census found 5 authored non-test occurrences, 2 of them live mis-authoring in published teaching material:
packages/types/examples/data-display-examples.jsonauthorsflexwithprops: { direction, gap }skills/objectui/guides/page-builder.mdauthorsstatisticwithprops: { label, value }
Both are presented as correct examples. Both render as if the bag were empty. That is the confidence gap in the triage block closed with a number, and it is the strongest justification this card had — the shape is not hypothetical, it is already being taught.
⛔ Correctly not fixed here: this PR changes no authored metadata, and the skills half would engage the published-skills budget clause. Filed as #6751.
Zero behaviour change is PINNED, not asserted
The order asked for this directly, and the construction is better than what I specified: 7 node shapes were captured on the base with
SchemaRenderer.tsxreverted to its committed blob, then re-captured with the diagnostic present — identical sha256387e04a9…, empty diff — and that reading is embedded in the test asBASE_READING.The ablation then confirms it from the other side: mutating the diagnostic away turns exactly the 5 "diagnostic fires" pins red while 35 stay green,
BASE_READINGamong them. A uniformly-red ablation would have proven much less.A category my dispatch order got wrong
I wrote that component renderers read only
schemawhile theelement:*family'sreadProps()merges both bags. There is a third case:view:simpleis a non-element type that readsschema.props. The implementer measured it and made the diagnostic silent there too. Recording the correction rather than leaving my framing to be inherited by the next card in this area.Scope held
No hoisting, no behaviour change, and the new module (
packages/react/src/utils/propsBagDiagnostic.ts) is module-internal — not re-exported, so no published surface was added for a diagnostic.propsWithoutCanonicalKeys(...)was hoisted into a local so the spread and the diagnostic read the same bag, which is the kind of change that prevents the two drifting apart later. Reproduction on the base confirmed the leg-2/leg-3 asymmetry still holds and that the expression is evaluated on both legs.Recorded
- NOT MEASURED, neither pass nor fail, with the gates' own reasons:
check:readme-exports("type entry … is not on disk — run pnpm build first" for 330 bindings across ~32 unbuilt packages) andcheck:sdui-registration-pins("Build the console first"). Neither can be moved by this diff — it adds no package-entry export and touches no README. - Second follow-up filed as finding(react): a non-object
propsis object-spread into indexed React props —props: "text"reaches the element as0,1,2, … #6752 (observation-class): a non-objectpropsis object-spread into indexed React props —props: 'not-a-bag'reachescreateElementas nine props named0..8. Measured, not deduced (it turned an early version of the degenerate-input pin red). The siblingpropertiesbranch carries a deliberately wider guard and documents this exact hazard; thepropsbranch has no equivalent. Latent — zero in-repo authored nodes carry the shape. ⚠️ data-table: a${...}expression authored in node-leveldatais not evaluated and renders an empty body silently #6665's fence survives intact: its diagnostic stays deliberately silent in this card's case, and its four-leg pin file was among the 11 at-risk suites re-run green.
Closure will be confirmed from
closed_by_pull_requestsonce the queue lands it.
Generated by Claude Code
Found while implementing #6665 (the non-array node-level
datadiagnostic). Measured on thatcard's merge-base but outside its fence, so filed separately.
The fact
#6665's four-leg re-measurement was run on
5967be095through the realSchemaRendererinside a
SchemaRendererProviderholding{ customers: [ 2 records ] }, identicalcolumnsin every leg, reading
tbody td. Leg 2 is this card:{ "type": "data-table", "data": "${data.customers}", ... }No results found(that is #6665){ "type": "data-table", "props": { "data": "${data.customers}" }, ... }No results found{ "type": "data-table", "properties": { "data": "${data.customers}" }, ... }{ "type": "data-table", "data": [ 2 literal records ], ... }The interesting pair is legs 2 and 3: the SAME key, the SAME value, one envelope apart, and
only
propertiesputs rows on screen.The mechanism
SchemaRendererHOISTSproperties.*onto the node (minusHOIST_PROTECTED_KEYS), which iswhy leg 3 works — the hoisted
datais a real array by the timeDataTableRendererdestructures it.
propsis not hoisted; it is spread as React props on the created element.DataTableRendereris declared as({ schema })and reads nothing else, so a key writtenunder
propsnever reaches it.That is not specific to
data-table. It applies to EVERY renderer that reads onlyschema—which is the normal shape for the component renderers, as distinct from the
element:*familywhose
readProps()merges{ ...schema.props, ...schema.properties }. So the same keyspelled
propsis honoured in one renderer family and silently dropped in the other.propsis documented as the annotated legacy alias for the config bag, and the 2026-08-18ruling recorded on #5123 settled precedence between the two bags (
propertieswins on bothchannels). Neither says the alias is INERT for component renderers, which is what the
measurement above shows.
Why it is not #6665
#6665 was fenced to node-level
data, and its diagnostic is deliberately silent here: withthe key under
props, node-leveldatais genuinely absent, so there is nothing for adata-table-level predicate to honestly say. That silence is pinned by a test in that PR
(
does NOT reach into the props envelope) rather than left to be rediscovered. Widening thatpredicate to reach into
propswould patch one component against a repo-wide shape.What a taker would need to decide first
Whether this is a defect or the declared design. Both readings are available today:
(
BaseSchemais.passthrough()with[key: string]: any), and the result is a silentdrop with a correct-looking header, the same success-receipt shape
data-tableaccepts abindand silently renders its header over an empty body #6575 anddata-table: a${...}expression authored in node-leveldatais not evaluated and renders an empty body silently #6665 exist toremove.
propsis the React-prop channel, component renderers readschema, and thealias was only ever meant for the
readProps()family.If it is a defect, the arms are the familiar three: hoist
propslikeproperties(abehaviour change on published components, so a ruling), diagnose it at the
SchemaRenderertier (no behaviour change, one place, covers every renderer), or refuse it at parse (blocked
on the
.passthrough()ceiling, #5155 / #6269).Refs: #6665 (where it was measured) - #6575 (the diagnostic precedent) - #5123 (the two-bag
precedence ruling) - #4795 (the
propertiesauthoring-channel contract question) - #5155 /#6269 (the
.passthrough()ceiling).Generated by Claude Code