Uh oh!
There was an error while loading. Please reload this page.
feat(react): name a props config bag that no schema-reading renderer sees (#6708) - #6753
Merged
os-sales merged 1 commit intoAug 29, 2026
Conversation
…rer sees
`SchemaRenderer` hoists `properties.*` onto the node; `props` — the annotated
legacy alias of the same bag — is evaluated and then spread as React props
instead. A renderer declared as `({ schema })`, the normal component-renderer
shape, therefore never sees it, and every gate accepts the spelling because
`BaseSchema` is `.passthrough()`. The `element:*` family is the exception: its
`readProps()` merges both bags.
Emit a `console.warn` at the SchemaRenderer tier naming the node and the
dropped keys and pointing at `properties`. Silent for the `element:*` family
and for `view:simple`, the one non-element type measured to read the raw bag.
Zero behaviour change, pinned against a reading captured on the tree before
the diagnostic existed. Level and dedupe follow the ruling's census
precondition.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CRJge11jso9TpXRWFt1Z49Contributor
✅ Console Performance Budget
The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it. 📦 Bundle Size Report
Size Limits
|
os-sales
marked this pull request as ready for review
August 29, 2026 06:20
Uh oh!
There was an error while loading. Please reload this page.
os-sales
deleted the
claude/issue-6708-props-bag-component-renderer-diagnostic
branch
August 29, 2026 06:33
This was referenced Aug 29, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for freeto join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes#6708
Executes the maintainer ruling of 2026-08-29 (verbatim 「同意」), option 2 — the
SchemaRenderer-tier diagnostic. One seam, every renderer family covered, zero behaviour
change. Option 1 (hoisting
propsto parity withproperties) was refused at ruling and isnot implemented here; option 3 stays blocked on the
.passthrough()ceiling.The defect
SchemaRendererHOISTS everyproperties.*value onto the node, so a key written underpropertiesis a real value on the node under that key name by the time a renderer destructures it.props— the annotated legacy alias of the same bag — is NOT hoisted: it is evaluated and then spread
as React props on the created element. A renderer declared as
({ schema }), which is thenormal shape for the component renderers, therefore never sees it. The
element:*family isthe exception, because its
readProps()merges{ ...schema.props, ...schema.properties }.Every gate accepts the spelling —
BaseSchemais.passthrough()with[key: string]: any—and the docs call
propsa supported alias, so nothing between the author and the screen saida word.
Reproduced on this base before anything was changed
The card was filed off a reading taken on
5967be095. Re-measured onfaac0d935throughthe real
SchemaRendererwith a probe renderer that records both channels:dataschema.dataprops: { data: "${data.customers}" }properties: { data: "${data.customers}" }Same key, same value, one envelope apart. Note the first column: the expression is
evaluated on both legs, so this is a dropped value, not an unevaluated one. The card's own
four-leg reading through a real
data-table(#6665's pin file) still reproduces unchanged —No results foundagainst the two rows — and that file stays green with no edits.The census — the ruling's precondition, run before the level was chosen
The ruling fixed the order: measure component-level
propsusage across the in-repo corpusfirst, so the diagnostic's level and dedupe inform rather than flood.
Method. Three scanners over the whole repo at
faac0d935, one classifier:.jsondocument, parsed and walked for any object carrying bothtypeandprops;json/json5/jsoncfence in every.md/.mdx, parsed and walked the sameway, with a textual probe over the fences that fail to parse so a fragment could not hide;
.ts/.tsxfile, walked with the TypeScript compiler API for object literalscarrying both a
typeand apropsproperty — an AST walk rather than a regex, so a hit isa real sibling pair rather than two nearby lines.
How a component-renderer node was told from an
element:*one. By the registry, not byguesswork: every
readProps()in this repo that merges both bags belongs to a componentregistered with
namespace: 'element'(all five files read on this commit —elements.tsx,data-list.tsx,text-input.tsx,record-picker.tsx,metadata-viewer.tsx).A repo-wide grep for reads of
schema.propsreturns that family plus exactly one other type,view:simple, whoseSimpleViewRendererreadsschema.props.columns. Everything else is acomponent-renderer node.
Result.
typeandpropselement:*or a measuredpropsreadertypenot statically knowable*.test.tsxfixtures exercising the shape on purposeThe 5 authored, non-test occurrences, named.
packages/types/examples/data-display-examples.jsonflexdirection,gapskills/objectui/guides/page-builder.mdstatisticlabel,valueskills/objectui/rules/protocol.mdcardtitleskills/objectui/guides/schema-expressions.mdcardtitleskills/objectui/guides/schema-expressions.mdtextvalueSo the answer is not zero: two published artifacts teach the broken spelling as if it were
correct. Both are filed as #6751 and not touched here — this PR changes no authored
metadata. The other three are already marked wrong where they stand and must stay.
What could not be scanned, and why it is a bound rather than a hole. 70
.jsonfilesfailed to parse; every one is a
tsconfig*.json(JSONC with comments), none of which is UImetadata. Two markdown fences failed to parse; both were recovered by the textual probe and are
in the table above. The AST scan requires
typeandpropsto be siblings in ONE objectliteral, so a fixture assembled by spreading a base object would be missed — that affects test
files only, and the diagnostic's behaviour on them is measured directly by the run below rather
than inferred.
PageDesignerwrites apropsbag onto every canvas component it creates, butDesignerComponentis a canvas model withposition/locked/zIndexand is never handedto
SchemaRenderer, so it is not a node in this sense today.What the number bought. Nothing floods, so the level is not softened for volume. But the
failure the census makes plausible is a metadata generator emitting one wrong envelope across
many nodes — those are distinct schema objects, so the object-keyed
WeakSetits neighbouruses would print one line per node for one bug. The dedupe is therefore keyed on the message,
which collapses that to one line while still giving two genuinely different nodes two lines.
Both directions are pinned.
What this adds
packages/react/src/utils/propsBagDiagnostic.ts— a pure predicate, a pure message builder anda reporter, matching the shape of the two diagnostics next to it.
SchemaRenderercalls it onceper node under
__DEV__, beside its #4795 neighbour.Channel and level:
console.warn. Two conventions cross here and the choice follows theSHAPE, not the tier.
unevaluatedExpression.tssits at this same tier and usesconsole.error,but its subject is a raw
${...}placed verbatim in front of a user. Nothing is placedhere; a value is dropped — which is exactly #6575's and #6665's subject, and both of those emit
console.warn. The card names this the third instance of that shape, so it joins that family.The message anatomy follows those two as well: address, the keys, the mechanism, the
consequence, the way out, the card number.
One computation, two readers.
propsWithoutCanonicalKeys(...)was hoisted out of thecreateElementcall into a local that both the spread and the diagnostic read. Same pure call,same arguments, same object, same spread position — but the diagnostic can no longer name a set
of keys that is not the set actually handed to the component. It also gets the #5123 precedence
for free: a key BOTH bags declare has already been subtracted, so the author is getting the
canonical answer and the diagnostic correctly says nothing.
One deliberate silence, with its cost stated.
view:simpleis excluded becauseSimpleViewRendererreally does readschema.props.columns. It reads only that one key, so adifferent
propskey on aview:simplenode IS dropped and is NOT diagnosed. Silence there waspreferred to a message asserting a drop it had not checked — the same trade #6665's
describeIgnoredBindmakes. The exclusion is a measurement of this tree, and the docblock saysso.
Acceptance — the ruling's pins, both directions
propsbag on a component-renderer node ⇒ fires, naming the node and pointing atpropertiesPIN 1 …element:*node carryingprops⇒ silentPIN 2 …propertiesnode ⇒ silentPIN 3 …Plus: silent on a node with neither bag, on an empty
props: {}, whenpropertiesalreadydeclares every
propskey, onview:simple, and on a degenerate non-objectprops; names onlythe key
propertiesdoes not also declare; one line for one bug repeated across nodes; twolines for two different nodes; no repeat across a re-render.
Pin 4 is measured, not asserted. The test's
BASE_READINGconstant is not a snapshot thefile wrote for itself. Seven node shapes were rendered on
faac0d935withpackages/react/src/SchemaRenderer.tsxreverted to its committed blob57c0beb3f— the treewith no diagnostic in it — and the capture was pasted in verbatim. Re-running the identical
capture with the diagnostic present produced a file with the same SHA-256
(
387e04a9…both times, over the richer first capture) and an emptydiff. #5123's precedenceis separately pinned in the same reading: for a key both bags declare,
propertiesstill wins.Ablation
Against the committed implementation (
aac3e65bc),collectDroppedPropsKeys(...)was replacedwith a
nullliteral. The mutation was confirmed on disk before the run — injected markergrep -c= 1, removed anchorgrep -c= 0, blob movedd54ef23d…tob478043…— and theanchor replacement asserts its own hit count, so a no-op edit fails loudly instead of going
green. No rebuild is involved: this repo's vitest config aliases
@object-ui/reacttosrc, sothe mutated source is what runs, and the red below is the proof it was read.
The 35 that stayed green under ablation are the whole behaviour surface,
BASE_READINGincluded. That is pin 4 from the other side: delete the diagnostic entirely and nothing about
what renderers receive moves.
Restore proven rather than assumed:
git diff HEADempty, restored blobd54ef23d…equal tothe path's HEAD blob, ablation marker count back to 0. The mutation ran under a
trap … EXIT INT TERMwith absolute paths throughout.Verification — union re-run on
aac3e65bc, after the final commitpackages/react+ all 11 census-flagged at-risk suitesTest Files 74 passed (74)·Tests 1259 passed (1259)pnpm --filter @object-ui/react run type-checktsc --noEmit && tsc -p tsconfig.test.json)pnpm --filter @object-ui/react run lint380 problems (0 errors, 380 warnings)— all 380 pre-existingno-explicit-any; none on a changed linecheck-changeset-presence1 source file(s) of 1 released package(s) changed, and this change declares 1 changeset(s)check-changeset-no-majorNo changeset declares a major bumpcheck-control-bytesOK (scanned 5604 tracked text file(s))check-package-self-importNo package names itself inside its own src/check-lint-coverage46/46 packages linted, 0 with outstanding errorscheck-vi-mock-specifierscheck-shell-escape-residueThe 11 at-risk suites are every test file the census showed authoring
propson acomponent-renderer node — the files most likely to be disturbed by a new console line, including
#6665's own four-leg pin file and the
domProps/ DOM-leak sweeps that spy on the console. Allgreen, unedited.
Both new files were confirmed to be genuinely inside the typecheck's file set with
tsc --listFiles, rather than assumed: the package'stype-checkcoverssrcandtsconfig.test.json, and both appear.NOT MEASURED locally, reported as such rather than as a pass or a failure:
check:readme-exports— exits 1 withtype entry ./dist/index.d.ts is not on disk — run pnpm build firstfor 330 bindings across ~32 unbuilt packages. A full-workspace buildprerequisite, not a finding. This diff adds no export to any package entry and touches no
README, so it cannot move this gate's answer. CI builds and runs it.
check:sdui-registration-pins— exits 2 withBuild the console first. Same class.pnpm lint(turbo run lint) — the per-package task for the only package this difftouches was run instead, and is green. CI runs the farm.
Scope
No behaviour change; no hoisting; #6665's
does NOT reach into the props envelopefence isuntouched and still green;
content/docs/releases/not touched; noskills/**file touched(0 files), so the published-skills budget clause does not apply. Nothing is added to the
published surface: the predicate, message builder, prefix constant and test-only reset are
module-internal and are not re-exported from the package entry, matching #6575's own symbols.
Changeset added (
patch,@object-ui/react).Filed out of scope by the census, and deliberately not fixed here: #6751 (the two authored nodes
that teach the broken spelling) and #6752 (a non-object
propsis object-spread into indexedReact props — measured while writing the degenerate-input pin).
Session: https://claude.ai/code/session_01CRJge11jso9TpXRWFt1Z49
Generated by Claude Code