Repository navigation
fix(app-shell): canonicalize reference keys on the by-name object-schema serve path (objectui#7650) - #8868
Conversation
…ema serve path (objectui#7650)
`MetadataProvider` has two paths that hand an object schema to a reader and only
one of them normalized. `ensureType('object')` — the LIST path — has run
`normalizeSchemaReferenceKeys` over every fetched item since objectui#2407.
`getItem('object', name)` — the BY-NAME path, behind the published
`useMetadataItem` hook — ran `extractItem`, which unwraps the `{ item }`
envelope and normalizes nothing.
Which spelling a reader saw therefore depended on cache order, not on the
document: warm it got the stamped def, cold it got whichever single key the
producer stored. The serve path never parses, so `FieldSchema` strictness is no
evidence a legacy spelling cannot arrive — a document stored before the key was
tightened is served back verbatim, forever.
`getItem` now applies the same idempotent, in-place stamp for `type === 'object'`.
No other metadata type is touched, nothing is dropped, and a spelling the
producer already set keeps its own value.
The coverage comment in `packages/core/src/utils/reference-keys.ts` said this
file runs at "exactly two production call sites". That was true about where it
RAN and false about the serve surface it was cited for; it now names three and
says how to re-derive the list.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01611D6ZaRaMmwTNQmSbk8MH
PM 评审 —— PR #8868(卡 #7650 的半 C):通过,⛔ 保持 draft,停队等契约评审
⛔ 我在裁决里给的 Clause-② 论证是错的,dev 的是对的我在裁决(5605081157)里写过:C「是真正的 Clause-② dev 的反驳逐字:
⇒ 我把「某条路径服务过这个键」和「这个消费者见过这个键」混为一谈了。 一个只走 by-name 的消费者,此前拿到的载荷里确实没有那个键。机械底线因此成立, ⭐ 而这正是派发里那两句话存在的理由:「⛔ 不要继承 PM 的那句话」「拿不准就按 缺陷形状本身值得记下来⇒ 一个读者看到哪种拼法,取决于缓存顺序,⛔ 不取决于文档本身。 缓存热(列表先取过, ⭐ 这是这张卡的置信缺口给出的答案的实际形状 —— 卡片问「入口是不是只有一条」,答案是「不是」,而第二条的后果是非确定性,比「少了一次规范化」严重。
|
✅ 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
|
…t two (objectui#7650) Comment only. The `reference_to` warning pin's docblock stated that `normalizeSchemaReferenceKeys` runs at "exactly the two ingestion choke points". The commit before this one added a third — `MetadataProvider.getItem` — so that sentence became false in the same change that made it false. Fixed here rather than left for a later reader to trust. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01611D6ZaRaMmwTNQmSbk8MH
|
Follow-up commit on this branch: The docblock of No test result in the body above moves: the runs quoted there were taken at
Generated by Claude Code |
✅ 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
|
|
Contract review at
|
|
Landing provenance — director seat, summon #18 segment 6 ( Generated by Claude Code |
…t moved (objectui#10979) (objectstack-ai#11046) Fixes objectstack-ai#10979 Clause-②: no — release-note prose only; no accept set or public surface changes. ## What this does Eleven pending changesets were named on the card as stating a present-tense repository count that is false on `main`. Each count was re-derived here on `main` at `2eaf5be27` (the commit the PM read at dispatch), with the query printed below and, as its positive control, the same query at the entry's own landing commit or base. - **Nine entries re-derive false on `main`.** Each gets one dated note (2026-09-29), placed directly after the paragraph that holds the counted sentence. That is the objectui#10877 / PR objectui#10891 form. The note quotes the sentence, gives the reading at the entry's own landing commit (or base), names what moved it and in which PR, and gives the re-measured reading at `2eaf5be27` in the past tense. Two of the nine (`5134`, `7497`) do not re-derive even at their own base, and their notes say so instead of claiming the sentence was ever true. - **Two entries are left untouched** (`9187`, `3965`). Their sentences re-derive true on `main`; the reasons are in the table. - Frontmatter is byte-identical in all eleven files, and no existing line is edited or deleted: `git diff --numstat 2eaf5be HEAD` is 98 insertions, 0 deletions, over 9 files. - **No new changeset.** The diff is `.changeset/` only, and `check-changeset-presence` says none is owed (output below). ## The ruling this write relies on On 2026-09-29, in the `domain:spec @ objectui` seat's chat, the maintainer chose 「Allow the appended note (Recommended)」. Recorded on this card as comment `5883370240` (the same text is `5883360525` on objectui#4356): > You allow developers to append dated correction notes to pending .changeset files (appended only, no existing line or frontmatter edited). ... The same permission also unblocks objectui#10974 and objectstack-ai#10979, which I dispatch straight away. ## Per entry Readings are over tracked files at a named commit. "Base" means the parent of the squash commit, the commit the change landed on. | Entry | Counted sentence (quoted) | Reading at landing | Reading on `main` `2eaf5be27` | Query | Outcome | |:--|:--|:--|:--|:--|:--| | `7561-filter-builder-operator-trigger-label` | "the spec's alias table `gt` / `lt` / `eq`, which three schema-catalog entries author today" | 3 catalog files at `9ae871d00` | 0 files; control: the same 3 fixtures carry `equals` / `less_than` / `greater_than` | Q1 | **Noted.** True at landing; objectui#6939 (PR objectstack-ai#9558) respelled all seven operators. | | `component-meta-converge-and-deprecate-alias-5893` | "In-repo, `PluginComponentMeta` has exactly one occurrence — its own export line" | 1 line in 1 file at the base `575b71c850`; 13 lines in 5 files at the landing commit `6d1c15536` itself | 19 lines in 7 files; 1 importing `.ts` file, this change's own pin | Q2 | **Noted.** True at its base. Its own pins and prose moved it, then objectui#5674 (PR objectstack-ai#10826) added more. | | `6296-conditionbuilder-subject-vocabulary` | "all five files that mount it today — six mount sites" | 6 sites in 5 files at `88b15fddc` | 7 sites in 6 files | Q3 | **Noted.** objectui#6226 (PR objectstack-ai#6668) mounted it in `FlowNodeConfigField`, a flattened-scope site. | | `6487-visibility-advice-per-tier` | "All three in-repo call sites pass their tier explicitly" | 4 calls in 3 files at `c71e14d1c`, from 3 gates | 5 calls in 3 files, from 4 gates; all 5 pass a tier | Q4, plus each call's sixth argument read by hand | **Noted.** objectui#6445 (PR objectstack-ai#6510) added the `disabled` / `disabledOn` enablement gate. True at landing only when counting gates; the note says so. | | `9187-record-highlights-layout-two-values` | "every in-tree `layout: 'grid'` belongs to a different component" (names 3 files) | 8 files at `96919a459`, 3 of them this change's own | 13 files | Q10 | **Not noted.** The sentence states no count, and its claim re-derives true: every hit belongs to `detail-view`, `ai-recommendations`, the `SchemaForm` tests' form-layout parameter, or this change's own pin and prose. The parenthetical was already non-exhaustive at landing (`ai-zero-read-members-retired-8178.test.ts` and `SchemaForm.gridCellNaming.test.tsx` carried the literal and are not named), so nothing went false after landing. | | `list-view-nested-aria-label-i18n-5134` | "the resolver four other in-repo read sites already use" | 10 other files importing it, 17 call lines, at the base `e40b8579bd` | 14 other files, 27 call lines | Q5 | **Noted.** False at its own base too; the note does not claim otherwise. | | `3965-mint-box-neutral-container` | "the catalog's 25 remaining `div`-authoring fixtures (80 nodes) migrate to it" | 86 `div` nodes in 28 files at the base `19f363736e`, and 6 in 3 at `a04d7c65c`: 80 nodes in 25 files migrated | 6 nodes in 3 files, unchanged | Q11 | **Not noted.** The figure describes this change's own migration and re-derives exactly. The residue (the exempt `components-basic-div` pair, 5 nodes, plus one test string) is unchanged since landing. | | `queryparams-closed-keys-7497` | "packages, apps, examples, e2e and scripts trees (4,362 files)" | At the census base `7dcda9cd4` that the commit message names: 5,203 tracked files, 4,344 TS/JS sources. No tracked-file reading gives 4,362. | 6,700 tracked files, 5,819 TS/JS sources | Q6 | **Noted.** False on `main` by any tracked-file reading. The note does not claim the figure held at its base. The census's zero was not re-run. | | `6837-reference-to-arm-deletion` | "`normalizeSchemaReferenceKeys`, which has exactly two production call sites" | 2 at `045d20ba8` | 3 (2 in `MetadataProvider`, 1 in `ObjectStackAdapter.getObjectSchema`) | Q7 | **Noted.** objectui#7650 (PR objectstack-ai#8868) added the by-name serve path call. | | `8450-inspector-select-placeholder` | "13 of the 45 call sites" and "all 45 call sites, none of which passes a placeholder of its own" | 45 mounts in 13 files at `0c4694437`, 0 passing `placeholder` | 47 mounts in 13 files, 1 passing `placeholder` | Q8, plus an element scan for a `placeholder=` attribute | **Noted.** objectui#6830 (PR objectstack-ai#9110) gave the flow-node select its own placeholder. objectui#7551 (PR objectstack-ai#9972) and objectui#7597 (PR objectstack-ai#10527) each added a mount that offers a `''` row. The 13 / 32 split is not re-classified, and the note says so. | | `7415-action-type-input-rename` | "517 JSON files (433 of them the schema catalog)" | At the base `6411def250`: 433 catalog schema JSON files, and the control is 127 plain `button` nodes, matching the sentence. 517, 4,797 and 2,410 do not re-derive from tracked files. | 432 catalog schema JSON files; control 119; the zero still 0 | Q9 | **Noted.** The re-derivable figure moved (433 to 432). The note names the base commit and scopes out the rest. | ## Queries, as printed Each line below was re-run in exactly this form. REV is the commit named in the table. A match on a tag-shaped name is spelled `\x3c`, because the body sanitizer eats a literal opening angle bracket. ```bash # Q1 git grep -lE '"operator"\s*:\s*"(gt|lt|eq)"' REV -- examples/schema-catalog | wc -l git grep -lE '"operator"\s*:\s*"(equals|less_than|greater_than)"' 2eaf5be -- examples/schema-catalog/src/schemas/components-complex-filter-builder | wc -l # Q2 git grep -h -c PluginComponentMeta REV | paste -sd+ | bc; git grep -l PluginComponentMeta REV | wc -l git grep -lE 'import[^;]*PluginComponentMeta' 2eaf5be -- '*.ts' '*.tsx' | wc -l # Q3 git grep -nP '\x3cConditionBuilder\b' REV -- ':!*.test.*' ':!**/__tests__/**' ':!*.md' # Q4 git grep -nE 'reportUnresolvableVisibilityPredicate\(' REV -- ':!*.test.*' ':!**/__tests__/**' ':!*.md' ':!packages/react/src/utils/visibilityDiagnostic.ts' # Q5 git grep -lE 'import \{[^}]*\bresolveI18nLabel\b' REV -- ':!*.test.*' ':!**/__tests__/**' ':!*.md' ':!*.mdx' ':!packages/plugin-list/src/ListView.tsx' | wc -l git grep -nE '\bresolveInlineI18nLabel\(' REV -- ':!*.test.*' ':!**/__tests__/**' ':!*.md' ':!*.mdx' ':!packages/plugin-list/src/ListView.tsx' | grep -vE '^[^:]+:[^:]+:[0-9]+:\s*(\*|//)' | wc -l # Q6 git ls-tree -r --name-only REV -- packages apps examples e2e scripts | wc -l git ls-tree -r --name-only REV -- packages apps examples e2e scripts | grep -cE '\.(ts|tsx|js|jsx|mjs|cjs|mts|cts)$' # Q7 git grep -nE '\bnormalizeSchemaReferenceKeys\(' REV -- ':!*.test.*' ':!**/__tests__/**' ':!*.md' # Q8 git grep -nP '\x3cInspectorSelectField\b' REV -- ':!*.test.*' ':!**/__tests__/**' ':!*.md' # Q9 git ls-tree -r --name-only REV -- examples/schema-catalog/src/schemas | grep -cE '\.json$' git grep -hoE '"type"\s*:\s*"button"' REV -- ':(glob)examples/**/*.json' ':(glob)apps/console/**/*.json' ':(glob)content/docs/**/*.json' ':(glob)packages/**/*.json' | wc -l git grep -hoE '"type"\s*:\s*"action:(button|icon)"' REV -- ':(glob)examples/**/*.json' ':(glob)apps/console/**/*.json' ':(glob)content/docs/**/*.json' ':(glob)packages/**/*.json' | wc -l # Q10 git grep -lE "layout['\"]?\s*:\s*['\"]grid['\"]" REV -- ':!**/CHANGELOG.md' # Q11 git grep -h -cE '"type"\s*:\s*"div"' REV -- examples/schema-catalog | paste -sd+ | bc ``` At its landing commit or base, each query reproduces the sentence it tests (the "Reading at landing" column). That is the positive control. The exceptions are `5134` and `7497`, where no reading reproduces the figure, and the table says so. The checkout is shallow (boundary 2026-07-20). Every commit named above is inside that window: the landing commits date from 2026-08-21 to 2026-09-12, and each supersession ordering was read with `git merge-base --is-ancestor`, which returned exit 0. ## Verification (head `b9a21e2e4`) Frontmatter md5 (first 8 hex digits), identical at `2eaf5be27` and at head for all eleven entries: | Entry | Frontmatter md5 | |:--|:--| | 7561 | `3d9e2a1e` | | 5893 | `ecedb10c` | | 6296 | `4cdeb4c7` | | 6487 | `02817e1c` | | 9187 (untouched, whole file `64351ea9` at both) | `ecedb10c` | | 5134 | `c9a085e4` | | 3965 (untouched, whole file `0dded8e3` at both) | `125d3a04` | | 7497 | `ecedb10c` | | 6837 | `9f3f1cc6` | | 8450 | `4e8ce550` | | 7415 | `3d9e2a1e` | - Added lines: 0 angle brackets, 0 control bytes, 0 model identifiers and 0 line addresses. Every citation is a quoted sentence, a symbol or a file name. - `node scripts/check-changeset-presence.mjs` exited 0: "9 file(s) changed, 0 of them published source … 0 changeset(s) added. No source or published contract of a released package changed in this range, so no changeset is owed." - `node scripts/check-changeset-no-major.mjs` exited 0. `node scripts/check-changeset-fixed.mjs` exited 0. - `node scripts/check-changeset-claims.mjs` exited 0. The self-contradiction reading covers these 9 bodies, and nothing is negated. - `node scripts/check-changeset-overwrite.mjs` exited 0 (report-only). It lists the 9 modified entries, and each one's "declared at base" equals its "declares now". - `pnpm check:pending-changeset-literals` exited 0. `pnpm check:control-bytes` exited 0. - `pnpm check:new-line-citations` exited 0: "VERDICT new-cross-file-line-citations: 0 new citation(s)". - `pnpm check:installed-pin-claims` exited 0. - `node scripts/check-governed-queue-guard.mjs --test` over the 9 paths exited 0: "NOT GOVERNED". - `pnpm exec vitest run` over `scripts/__tests__/check-changeset-{claims,fixed,no-major,overwrite,presence}.test.ts`, `check-pending-changeset-literals.test.ts` and `changeset-polarity-census.test.ts`: Test Files 7 passed (7), Tests 292 passed (292). - The adjacent suites that walk `.changeset/` (`body-dialect-census`, `dollar-dialect-alias-census`, `check-installed-spec-pin-claims`, `check-control-bytes`, `quick-reference-current-release-4143`, `render-changeset-claims-comment`): Test Files 6 passed (6), Tests 164 passed (164). No test names any of the nine files. ## Acceptance notes These are observations only. None is filed. - `6837-reference-to-arm-deletion` carries other present-tense counts that the card does not name ("Eleven of the sixteen sites", "Three readers"). They are not measured here. - `8450`: the "13 of the 45" / "The other 32" classification is not re-derived. The note says it stays the reading of that change. - `3965`: "the catalog ratchet closes to zero tolerance for JSON-authored `div`" reads as absolute, while the `components-basic-div` exemption (2 fixture files, 5 nodes) stands. This is unchanged since landing. - `5134`: at its base, a search for the unaliased `resolveI18nLabel(` returned four lines, all doc comments. This is recorded as an observation, not as the source of the figure. Session: `https://claude.ai/code/session_012UwY3ahMixEFkfTUxMVkYm` (`domain:spec @ objectui` seat, `mode:subagent` dev run). --- _Generated by [Claude Code](https://claude.ai/code/session_012UwY3ahMixEFkfTUxMVkYm)_ Co-authored-by: Claude <noreply@anthropic.com>
Part of objectui#7650 — this is the first of the two halves the ruling on that card
(comment 5605081157) ordered. It shuts a hole in behaviour that is already shipped;
the four-key canonicalisation the card is really about is the second half and rides its
own pull request. Neither half completes the card on its own, so nothing here uses a closing
keyword.
What was wrong
MetadataProviderhas two paths that hand an object schema to a reader, and only one ofthem normalised:
ensureType('object')— the LIST pathnormalizeSchemaReferenceKeysper item, since objectui#2407 / PR #2587getItem('object', name)— the BY-NAME path, behind the publisheduseMetadataItemhookextractItem, which only unwraps the{ item }envelopeSo which spelling a reader saw depended on cache order, not on the document. Warm — the
list had already been fetched, so
entry.byNamewas populated by the normalising path — itgot the def with both keys stamped. Cold — a
useMetadataItem('object', name)that ranbefore any list fetch — it got whichever single key the producer had stored. A consumer that
reads one key rendered a raw id in the cold case and the relation in the warm one: the
objectui#2407 bug, still reachable through the one door left open.
Why it matters that this is a SERVE path
objectui#7650 measured that the object-schema serve path never parses:
ObjectStackAdapter.getObjectSchemaapplies exactly two mutations and runs noObjectSchema.parseat all (lit control in the same file: it does import and useDroppedFieldsEventSchemaandisFilterAST, so the zero is a real absence, not a failedgrep).
FieldSchemastrictness therefore gates the write door only — a document storedbefore a key was tightened is served back verbatim, forever, and a host with its own
getObjectSchemais served straight through.useMetadataItemis exported from@object-ui/app-shell, so the unnormalised def reachedout-of-repo consumers too, not only the five in-repo
objectcallers.What changed
packages/app-shell/src/providers/MetadataProvider.tsx—getItemapplies the sameidempotent, in-place stamp for
type === 'object'before it caches. Nothing else istouched: no other metadata type, no drop, no overwrite of a spelling the producer set.
packages/core/src/utils/reference-keys.ts— comment only. Its coverage block said thisfile runs at "exactly two production call sites". That was true about where it ran and
false about the serve surface it was cited for. It now names three, and says to
re-derive the list from the serve paths rather than by grepping for the call.
Clause-② declaration
Clause-②: yes— measured on this diff, not inherited.The mechanical floor in the contract-review rules is "a new exported symbol or a new key on
a published payload is always yes". No new exported symbol. But a key is new on a
published payload: before this change a def served through the by-name path came back
carrying only the spelling its producer stored, and after it carries both. The tempting
argument against — "those keys are already served on the other two paths, so no consumer
sees a key it was not already seeing" — is about a different path, and a consumer that
only ever reads by name did previously get a payload without the key. Arguable is not
self-evident, and the rule for arguable is
yes.needs:contract-reviewis hung on this PRand on the card; this PR parks until that review is reachable.
Tests
New pins in
packages/app-shell/src/providers/__tests__/MetadataProvider.itemReferenceKeys-7650.test.tsx(6 cases). Every assertion reads the value a consumer gets back from
getItem— neverthe provider's internals, because a re-plumbing that still served an unstamped def by some
other route would satisfy an internals assertion and violate the card.
Four positive pins (both fold directions, the array field-container shape, and cache
stickiness on a repeat read) and two negative pins that are the load-bearing half: a
normaliser that fired for every metadata type, or that overwrote a key the producer
had already set, would pass every positive assertion.
Runs, all in the dedicated worktree at commit
20890fe13:Test Files 1 passed (1),Tests 6 passed (6)MetadataProvider,useMetadataItemornormalizeSchemaReferenceKeys, plus the new one:Test Files 77 passed (77),Tests 836 passed (836)turbo run type-check --filter=@object-ui/app-shell --filter=@object-ui/core:Tasks: 31 successful, 31 total— and each package'stype-checkrunstsc --noEmit && tsc -p tsconfig.test.json, so the new test file is type-checked toocheck:designer-field-key-parity,check:handler-key-reads,check:spec-symbols,check:control-bytes,check:unreferenced-sources,check:vi-mock-specifiers,check:vi-mock-inherit,check:comment-mask-corpus,check:changeset-presence— allexit 0 on their own printed verdict line
Declared narrowing: the full
packages/app-shell/suite (668 test files) does not fitthe container's foreground budget under lock contention — one attempt acquired the lock
after 126s and was still running at the 560s cap. It is left to CI. The narrowing is stated
here rather than presented as coverage.
Ablation
Mutation: delete the one added line from the committed tree, then re-run the new pins.
d9a75c607c0f2427…, mutated worktreeblob
d65fb48ae3dff8dc…, marker count 1 to 0reference_toon a by-name objectdef that spells only
reference", "stampsreferenceon a by-name object def that spellsonly the legacy
reference_to", "normalizes the ARRAY field-container shape the metadataAPI also serves", "serves the SAME canonical shape on a repeat read, without a second
fetch". The two that stayed green are exactly the two negative pins, which assert that
something is not normalised — the expected direction, and the reason they are listed
rather than glossed
d9a75c607c0f2427…,git diff HEADempty,marker count back to 1. The restore is
git checkout HEAD -- PATH, never a baregit checkout -- PATH, and the script carries atrap … EXIT INT TERMNo build was involved: the mutated file is app-shell's own source and the test imports it by
relative path, so nothing resolved through a
dist/that could have gone stale.验收备注
Recorded, nothing filed, and none of it rides this PR:
objectcallers ofuseMetadataItemnumber five, not three:components/src/renderers/basic/metadata-viewer.tsx,components/src/renderers/layout/containers.tsx,plugin-detail/src/renderers/record-quick-actions.tsx,plugin-detail/src/renderers/record-alert.tsxandapp-shell/src/views/DeclaredActionsBar.tsx. The earlier count of three, reported on thecard, was an undercount by me; correcting it here so the card's record is right. Four of
the five read
.actionsoff the def rather than a relation, which is why nothing in-repowas visibly broken — the published hook is the surface that was.
mergeViewsIntoObjects, soobjectDef.listViewsispopulated on the list path only. That is a different key and a different decision; not
touched here, and no card filed, because no reader was measured taking the by-name path
for
listViews. Successor: whoever next widens the by-name path.Session, written as prose because a markdown-link footer does not reliably survive a body
PATCHin this repository: this pull request was produced by the Claude Code seathttps://claude.ai/code/session_01611D6ZaRaMmwTNQmSbk8MH.Generated by Claude Code