Skip to content

fix(scripts): resolve a registration namespace that arrives by reference (objectui#9641) - #9686

Merged
os-sales merged 6 commits into
mainfrom
claude/issue-9641-registry-namespace-spread
Sep 17, 2026
Merged

os-sales merged 6 commits into
mainfrom
claude/issue-9641-registry-namespace-spread

Conversation

@os-sales

@os-sales os-sales commented Sep 17, 2026

Copy link
Copy Markdown
Collaborator

Fixes #9641

Closing round (round 4) by the domain:spec @ objectui execution seat, session session_01UanLVj6xvbS6puBCewLr8L, on the ruling recorded at objectui#9641 comment 5716921048.

Angle-bracket-shaped tokens are spelled out in words throughout this body (AGENTS.md, the GitHub-mutates-body-bytes clause).

deriveRegistryKeys read namespace and skipFallback out of a registration call's own balanced argument span. When the options object is not spelled out at the call, there is no namespace: in that span — so the registration was read as bare-only, and no finding was reported. Five real runtime keys were therefore missing from the generated KNOWN_SCHEMA_TYPES, and objectui check called documents spelling them unknown while the renderer painted them.

The derivation now reads registration options from an allowlist of ARGUMENT shapes and reports every other options argument as a finding. ⚠️ Argument shape, not registration shape — once a name is accepted, whether it names the object the call passes is a premise enforced only against the spellings the regex can see, and the readings that slip through are SILENT. That half is written down and pinned rather than claimed away; the closing-round section below is the whole of it.

Premise re-verified on this branch point, not relayed

Measured at the merge base 72f55c9ec1, with the card's own controls:

probe at 72f55c9ec1 after
ui:page ui:app ui:utility ui:home ui:record in the generated list 0 each 1 each
firing control page:header — namespaced, options spelled out at the call 1 1
absent-token control 0 0
keys in the generated list 644 649

The generated artefact is packages/cli/src/utils/known-schema-types.ts — the regeneration script's own TARGET, and the only copy. It was regenerated, never hand-edited; the diff is +5 lines and nothing else, and it has not moved since.

⭐ The card's headline is refined by the measurement: one of the five is not a spread

page.tsx registers five page kinds from one options object — one call passing it whole as a bare identifier, four spreading it to vary a label:

ComponentRegistry.register('page', PageRenderer, pageMeta);                            // identifier
ComponentRegistry.register('app', PageRenderer, { ...pageMeta, label: 'App Page' });   // spread

So ui:page was not lost to a spread. The class is one step wider than the card's title: options that are not spelled out at the call, whichever way they arrive.

⭐ Three patch rounds, each closing the same defect one layer lower

This card exists because a header asserted a property the instrument did not enforce. That exact failure then recurred inside its own repair — three times, once per patch round — and the honest summary of this PR is that it took a maintainer ruling to stop doing it, by changing the target from closing shapes to declaring what is not closed.

R1 — the class, not the two idioms. The first pass repaired the two shapes this tree uses and left seven siblings falling through to the same whole-span regex: silent bare-only readings, no finding, and the live gate green on two of them. resolveRegistrationOptions now works from an allowlist, which is structural rather than a list of idioms to keep up with:

  • READ — an object literal whose top-level entries are all key-value pairs or spreads of a plain identifier; and a bare identifier resolving to one such literal.
  • REPORTED — everything else: a cast, a member expression, a call, a spread of any of those, a conditional spread, a computed namespace, a computed skipFallback.

R2 — the premise under the identifier route. The allowlist is structural about the argument. Following a name rests on a premise — that the literal at the declaration is the object the call passes — and R1 enforced that premise against rebinding (the let refusal) while leaving mutation unread. A const cannot be rebound, but its contents can be written. Five shapes derived with no finding at all:

fixture runtime publishes R1 derived direction
const without a namespace, .namespace assigned after namespaced + bare bare only silent MISS
the same through Object.assign onto the const namespaced + bare bare only silent MISS
const with a namespace, that property deleted after bare only namespaced + bare silent PHANTOM
const with a namespace, skipFallback assigned after namespaced only namespaced + bare silent PHANTOM
row 1 reached through a top-level spread namespaced + bare bare only silent MISS

⭐ Two of them are the phantom direction — through a const, the very binding the header presented as what made phantom-prevention "a refusal rather than a best effort". declaredObjectBody now refuses a name whose comment-stripped file assigns to its namespace or skipFallback (dot or bracket, including the logical and compound operators), deletes either, or passes it as an Object.assign target. Read-only member access still READS, and so does a write to any other property — the guard watches the two properties that move which keys a registration publishes, and a guard written wider than that would red correct registrations. That control is pinned too, and is marked in the test as a control rather than a mechanism pin, because it passes on both sides.

Also in R2: import bindings now count toward "declared exactly once", which closes an import at module level shadowed by a function-local declaration; an options argument spelled undefined or null is read as absent options rather than refused as an undeclared name; and the by-reference counter now counts every site that takes that route, not only the ones that also resolved a namespace.

⚠️ What R2 said was still not guarded was one shape — a function parameter of the same name — pinned as a known reading. ⛔ That statement was itself too narrow, which is what the third at-tier review measured and what the closing round below replaces. The parameter pin stays; it has company now.

The tightening reds nothing, which is what made both rounds affordable. On this head the derivation reports 0 findings, the universe is 649 keys, metaViaReference is 5, and node scripts/regenerate-known-schema-types.mjs --check is 0 with the generated file untouched. The gate summary prints those counters, so they are re-derived every run rather than quoted from here.

⭐ R3 — the closing round: the prose is made true, and the silence is pinned

Ruling on the card: batch #150 item 2, letter B, maintainer 「同意」 2026-09-17T15:25Z. Three at-tier contract reviews failed this branch for one shape — a header or changeset asserting a property the regex instrument does not enforce. Round 3 probed 46 forms: 21 read correctly, 12 refused loudly, 22 silently wrong. The ruling's reading of that is the reason this round closes the card rather than closing corners:

the reachable end-state for a regex instrument is an accurate declaration of what it cannot see

No new guard logic. No shape closed. Two kinds of change only.

1. Prose made true — each item is the old sentence, the reading, the new sentence

where what it said the reading what it says now
changeset the identifier route follows a name the file "never writes to afterwards" the guard matches three spellings: an assignment or delete whose target is the name spelled exactly followed by namespace/skipFallback written out (dotted or quoted bracket), or an Object.assign whose first argument is that name. An alias, a callee, a computed key, a destructuring target, Reflect.set, Reflect.deleteProperty, Object.defineProperty and Object.setPrototypeOf all derive with no finding the three spellings, named; and the invisible list, named
changeset "Every other shape … is now reported" true of the options argument, false of the name "Every other options ARGUMENT … is now reported", plus the name half stated separately
changeset "One binding kind is not covered" four are — parameter, destructuring pattern, later declarator, catch — all phantom-direction the count's own rule, then the four
OPTIONS bullet (check-doc-component-types.mjs header) "declared exactly once, with const, in the same file, and never written to afterwards … and the one binding kind it cannot see" same two readings the two qualifications spelled out, pointing at the two functions that carry them
optionsMutatedAfterDeclaration header "Does this file write to, delete, or Object.assign onto name's key-bearing options after it is declared?" it is also position-agnostic — a write placed after the register() call refuses the name although the runtime never reached it "one of THREE SPELLINGS", the list of what is invisible, and the position-agnostic direction declared as the loud one
declaredObjectBody header "The declaration count sees const, let, var and import bindings" a name counts only where it immediately follows the keyword, or sits in an import clause exactly that, then the four invisible positions
readMetaBody header "Entries are read in source order and a later one wins, which is what the runtime does" false for two shapes this PR's own new code introduced (below) fixed in code; the claim is now bounded to the two properties and to bodies where every entry was recognised
regenerate-known-schema-types.mjs header "a registration form it cannot resolve fails there rather than silently shrinking the universe here" true of a form it does not recognise; a form it recognises and reads under a premise that does not hold is silent narrowed to "an options ARGUMENT whose shape it does not recognise", with the silent half written down beside it

2. Known gaps pinned — 16 rows, each asserting TODAY's silent reading

Each row asserts the derived key set and the absence of a finding, so closing the gap later reds the row. Ablations show every row is real; ⛔ a pin green on both legs asserts nothing, and the previous round caught exactly that.

leg — what was mutated rows that went red
widen the mutation guard to any receiver + Reflect/defineProperty/setPrototypeOf/computed key 11 — the whole mutation route, and nothing else
widen the binding count to destructuring patterns, later declarators and catch 4 — the whole scope route, and nothing else
count a function parameter as a binding 1 — the pre-existing parameter pin
⛔ control: make the mutation guard never fire 8 — the seven R2 refusal pins and the new firing control beside the gap block, proving the gap rows are about spelling and not about a guard that stopped working

The mutation route reads: alias write (MISS), alias delete (PHANTOM), alias write through a spread (MISS), callee write (MISS), callee skipFallback write (PHANTOM in the bare half), computed-key write (MISS), destructuring-assignment target (MISS), Object.defineProperty (MISS), Reflect.set (MISS), Reflect.deleteProperty (PHANTOM), Object.setPrototypeOf (MISS — the registry reads meta?.namespace, which follows a prototype). The scope route reads: destructured const, array-destructured binding, later declarator, catch binding — all four PHANTOM.

3. The one exception the ruling grants — two bugs in code this PR itself added

Both were a few lines with their own pin; neither grew, and neither closes a shape.

bug runtime before now
the visited set is global, so a literal spreading the same base twice skipped the second { ...base, ...mid, ...base } publishes ui:widget view:widget — a PHANTOM and a MISS at once recursion stack: the name is released once its body is read, so only a spread still on the stack is a cycle
a truthiness test drops an explicit skipFallback: false arriving by spread { skipFallback: true, ...base } with base carrying false publishes both keys ui:widget alone — the bare key MISSING set-ness tracked, not inferred

Each has a mechanism pin that is red on the reverted leg (1 red each, measured), and a control pins that self-referential and mutually-spreading declarations still terminate.

The live tree must not move, and it did not. Re-derived on this head: 649 keys, 0 findings, callSites 238, resolved 233, open 2, indirect 132, metaViaReference 5; node scripts/regenerate-known-schema-types.mjs --check exit 0; the generated packages/cli/src/utils/known-schema-types.ts is at blob 45bf0e99, the same blob it has carried since R0. Firing controls on the same instrument: page:header, element:text, action:button each 1; absent-token control 0.

The five pending changesets that name a file this diff touches

The changeset-claim-re-read gate (exit 0, ⛔ not a required context) names five. Read by paragraph and probed on this head; the structural reading first, because it is the one that generalises:

The generated file moved by +5 / -0. Added set is exactly ui:app ui:home ui:page ui:record ui:utility; removed set is empty (measured as a set difference between the blob at merge base 72f55c9ec1 and the blob at this head). ⇒ an absent-claim can only be falsified by an addition, and none of the five added keys is named in any of these entries; a present-claim can only be falsified by a removal, and there are none. Key-by-key readings agree, each with an absent-token control reading 0:

entry its claim merge base this head verdict
8802-…-kanban-gantt-family-retirement view:kanban / view:gantt still on the list 1 / 1 1 / 1 still true
bare kanban / gantt DID leave 0 / 0 0 / 0 still true
"only the namespaced pair survives" — plugin-kanban:kanban-ui / …:kanban-enhanced 0 / 0 0 / 0 still true
firing control on the surviving keys object-kanban / object-gantt 1 / 1 1 / 1 instrument lit
7122-spec-17-3-0-parity-reconciliation user:profile retired from the regenerated list 0 0 still true
the enum "34 → 32, lost set exactly user:profile + element:form" about @objectstack/spec's own enum, which this diff does not touch; element:form also reads 0 in the generated list at both refs
6416-plugin-report-namespace the three report:* spellings dropped 0 / 0 / 0 0 / 0 / 0 still true
the three plugin-report:* keys name real components 1 / 1 / 1 1 / 1 / 1 still true
7704-schema-registry-chatbot-keys the CLI list keeps chatbot-enhanced and chatbot-floating 1 / 1 1 / 1 still true
8499-node-slot-registered-arms my-component carries a written exemption in check-doc-component-types.mjs 6 occurrences 6 occurrences still true

⚠️ One sentence in 8499 is false, and this diff is not what falsified it. It says check:doc-types judges against "the RENDERER REGISTRY (656 keys)". The same instrument reads 644 at the merge base and 649 here. ⇒ it was already stale before this branch existed; what this diff does is move the quantity that sentence names by +5. ⛔ Per the seat's instruction it is reported, not edited — another card's pending entry is not this round's scope.

The census, read from the instrument — and a correction to this body's own earlier number

⚠️ An earlier revision of this body said 242 call sites. That figure came from a hand-written scanner of mine, not from the derivation, and it was wrong: reconciled against the instrument, the difference is exactly 4, all four register( occurrences inside string literals — an error message in core/src/errors/index.ts, two quoted mentions in Registry.ts, and a plugin template string. The derivation skips those by design; the hand scanner stripped comments but not strings. 238 is the reading, and counters.callSites is where it comes from.

What the census supports is unaffected, and is best read off the counters the gate prints rather than copied here: resolved, via referenced options, indirect and open. The by-reference population is metaViaReference; every one of those sites is the page.tsx family, and the one other registration reaching its options by reference (PluginScopeImpl.registerComponent, a third-party plugin's own key) never reaches the options read because its key is unresolvable first — it is a declared open site. ⇒ no other registration in the tree is half-invisible, and a pin asserts the counter is non-zero so that population cannot go quiet.

Why the repair landed in the derivation, and not in the regeneration script

⚠️ Declared file-surface amendment. The claim's surface named scripts/regenerate-known-schema-types.mjs; the repair is in scripts/check-doc-component-types.mjs. The regeneration script has no key universe of its own — its header's "One derivation, two consumers" section says the universe comes from deriveRegistryKeys deliberately, "not a second scanner with its own bugs" — so a resolver added there would have been that second scanner.

The hard fence was honoured. packages/components/src/renderers/layout/page.tsx and packages/core/src/registry/Registry.ts are at 0 diff lines; no registration was added, removed or renamed, so nothing here touches or pre-empts objectui#9263. If that decision deletes the app registration, the next regeneration drops ui:app and the pins follow — the count moves, the defect does not.

The pins are the same script giving opposite answers on two refs

A pin that merely asserted the five key strings would pass against a hand-edited generated file, which is the failure mode to exclude. The mechanism is pinned over fixture trees; the live-tree consequence is pinned beside the byte-for-byte regenerability assertion that rules the hand-edit out.

Every ablation runs from the committed state, with blob hashes checked on both legs and git diff HEAD proving the restore — the restore leg is git checkout HEAD -- PATH (⛔ never the bare form, which restores from the polluted index), and the trap uses absolute paths.

⚠️ Rows R0 and R1 are HISTORICAL per-round totals against suites of 74 and 84 tests, not against this head's 117. Round 3 re-ran them at its own head and read 31/67 and 21/77. Only R2's row was a head figure when it was written. The closing round's legs below are all measured at this head, against the same 117.

ablation mutation result
R0 — the repair itself (historical) derivation at merge base 47a9397b 8 failed / 66 passed of 74
R1 — the class closure (historical) derivation at 517d1135 10 failed / 74 passed of 84
R2 — the mutation guard derivation at 59b1a920 10 failed / 88 passed of 98
R3 — revert the recursion stack seen.delete(name) removed 1 failed / 116 passed of 117
R3 — revert the explicit-false tracking spread inherits skipFallback by truthiness 1 failed / 116 passed
R3 — close the mutation gaps guard widened to any receiver, Reflect, defineProperty, setPrototypeOf, computed key 11 failed / 106 passed
R3 — close the scope gaps count widened to destructuring, later declarator, catch 4 failed / 113 passed
R3 — close the parameter gap count widened to function parameters 1 failed / 116 passed
R3 ⛔ control — the mutation guard never fires optionsMutatedAfterDeclaration returns null 8 failed / 109 passed

On this branch the two pin files are 117 passed, 0 failed. The passing majority under each mutation is the non-vacuity control.

Gates

Hand-derived from this repo's own package.json and workflows — scripts/pm/dispatch-gates.mjs refuses objectui paths by design.

gate exit
pnpm changeset:check · node scripts/check-changeset-presence.mjs 0 · 0
node scripts/regenerate-known-schema-types.mjs --check 0
pnpm check:doc-types (the gate whose derivation this changes) 0
pnpm check:registry-bare-names · check:prompt-keys · check:spec-symbols 0 · 0 · 0
pnpm check:sdui-registration-pins 0 (needs a console bundle; exit 2 means "measured nothing")
pnpm check:doc-snippets · check:doc-examples 0 · 0 (after building the packages they resolve against)
pnpm check:new-line-citations 0 (0 new citations)
pnpm check:control-bytes · check:test-path-roots · check:changeset-claims · check:pending-changeset-literals 0
pnpm type-check:scripts 0
turbo run type-check --filter=@object-ui/cli · turbo run lint --filter=@object-ui/cli 0 · 0 — measured in R0; ⚠️ not re-run in the closing round, which touches no file under packages/cli (the generated file is at the same blob), so its input has not moved
pnpm lint:root 0
pnpm check:comment-mask-corpus · check:vi-mock-specifiers · check:shell-escape-residue 0 · 0 · 0
vitest scripts/__tests__ + packages/cli/src/__tests__ 0 — 187 files passed, 5202 tests (5183 + the 19 pins this round adds)

Declared narrowing on lint (accepted in R0 with this invariance argument, re-measured on this head). pnpm lint repo-wide is CI's run; what ran here is lint:root plus the one package this diff touches. Three readings: (1) the population is read from eslint's own resolved config — lint:root is the root surface with the package trees ignored, where four of the five changed source files live, and the fifth is in @object-ui/cli, linted by its own task; (2) --format json counts 373 files linted, 0 errors, 32 pre-existing warnings; (3) type-aware linting is not enabled in eslint.config.js — zero occurrences of parserOptions.project or projectService outside a comment — so this diff cannot move the verdict on a file it did not touch. ⚠️ check:doc-snippets / check:doc-examples were measured at 0 in R0 and are not re-run here: they consume this same derivation, whose output on this head is byte-identical to R0's, and the doc corpus is untouched. Repo-wide pnpm lint and pnpm test remain CI's.

Repo-wide pnpm type-check is CI's. The exported surface has not moved: KNOWN_SCHEMA_TYPES is typed readonly string[], not a const-asserted literal union, and the generated file has not changed since R0.

维护者速读(草稿)

改了什么 —— objectui check 从今天起接受五个它现在会报「未知类型」的写法:ui:pageui:appui:utilityui:homeui:record。这五个本来就是平台真实注册、渲染器照常画得出来的组件键;是校验器看不见它们,不是它们不存在。注册代码一行没动,生成的清单只多五行、不少一行。

为什么改 —— 那份清单是从仓库里的注册调用自动推导出来的,而推导器只认「写在调用里」的 namespace。页面这五个注册共用同一个配置对象(一个直接传、四个用展开改个标签),namespace 不在调用里,于是被读成「没有命名空间」,而且不报错。一个会拒绝平台真实渲染结果的校验器,教会作者的是别再看校验器 —— 这是更贵的那个方向。更贵的账已经付过一次:objectui#9263 上三个「相互独立」的普查结论一致,其实是其中两个共用了这个盲点。

评审补了三轮,三轮都因为同一件事被打回来:代码注释把自己说得比实际做到的更强。第一版只修了本仓用到的两种写法;第二版把「不认识的写法一律报错」做成了结构性的白名单;第三版把「跟着变量名去取配置」的前提守住了一半 —— const 不能被重新赋值,可它的内容能被改,删掉一个已声明的 namespace、事后补一个 skipFallback,推导器都照旧读声明处的字面量,于是推出运行时根本不存在的键,让校验器给一个渲染不出任何东西的写法开绿灯。但第三次达档复核实测了 46 种写法:21 种读对、12 种响亮拒收、22 种静默读错

你已经裁过了:封顶,字母 B(2026-09-17)。 裁决的理由值得看一眼 —— 用正则去近似 JavaScript 的作用域与改动规则,永远补不完;第三轮点名的静默写法比第二轮更长,而已关掉的每一个角落今天零消费。能到达的终点不是「全关掉」,而是「把看不见的东西准确地写下来」。所以本轮只做两件事:把话说准(七处),把 16 种静默写法钉成测试(将来谁补上了,那条测试会变红,而不是悄悄过去),⛔ 不新增任何守卫逻辑。另加两处几行的真修复 —— 那两个 bug 是本 PR 自己上一轮写进去的,各自配一条测试。

风险与代价(含回滚) —— 风险面小:只多认五个字符串,没有任何现有键被删或改名,导出的类型逐字未变;真错了就 revert。三轮收紧加本轮的两处修复,对当前仓库零影响(推导结果一个键都没变、0 条报错、生成文件自 R0 起未动,blob 逐字相同),代价只是多了一批夹具测试。⚠️ 有一点要说清楚,因为它正是本卡的教训:「本仓今天没踩上任何一个缺口」比听上去要弱 —— 静默读错的意思就是跑绿了也不能证明没踩上。真正每次运行都重新算的是那个缺口能够波及的范围(metaViaReference,今天是一个文件里的五处调用),这一点写在注释和 changeset 里,没有被说成别的。要留意的方向性一点:若将来 objectui#9263 裁定删掉 app 注册,重新生成会自动把 ui:app 拿掉,不需要人工跟。

席位意见 —— (留空,待席位定稿)

你要做的 —— 你已经拍过板了(字母 B),本轮就是那一拍的执行,不再需要你做任何动作。这个 diff 没有碰任何受管面,按普通 PR 走;落地前还欠一次达档复核,那是席位的事。


Generated by Claude Code

`deriveRegistryKeys` read `namespace` and `skipFallback` out of a
registration call's own argument span. When the options object is not
spelled out at the call -- passed as an identifier, or spread into a
literal that varies one field -- there is no `namespace:` in that span,
so the registration was read as bare-only and no finding was reported.

Five real runtime keys were missing from the generated
`KNOWN_SCHEMA_TYPES` as a result (`ui:page`, `ui:app`, `ui:utility`,
`ui:home`, `ui:record`), and `objectui check` called documents spelling
them unknown while the renderer painted them. The derivation now follows
a same-file options object through both forms, and reports a spread it
cannot follow instead of assuming it carries no namespace -- the clause
the regeneration script's header already leaned on, now true.

The mechanism is pinned over fixture trees, so the pins fail on the old
derivation and pass on the new one rather than restating the five key
strings, which a hand-edited generated file would also satisfy.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UanLVj6xvbS6puBCewLr8L
@github-actions

github-actions Bot commented Sep 17, 2026

Copy link
Copy Markdown
Contributor

changeset-claim-re-read

⚠️ 5 pending changeset(s) describe a file this change touches

Their bodies publish verbatim into the CHANGELOG at the next release, so this is a request to re-read them against your diff — addressed here because you are the one seat that can answer it without re-deriving anything.

⛔ Nothing here blocks, and nothing here is a verdict on your change. This gate exits 0, is not a required context, and judges name resolution, never meaning: it asked whether a pending body names a file you touched. "Is this sentence still true?" is the one question it will not answer, and the one you are being asked to answer.

.changeset/6416-plugin-report-namespace.md

  • names packages/cli/src/utils/known-schema-types.tspackages/cli/src/utils/known-schema-types.ts — edited by this change

    packages/cli/src/utils/known-schema-types.ts is regenerated from the registrations, dropping report:report, report:report-viewer and report:spec-report.

.changeset/7122-spec-17-3-0-parity-reconciliation.md

  • names known-schema-types.tspackages/cli/src/utils/known-schema-types.ts — edited by this change

    user:profile is retired across all three sites. 17.3.0 dropped it from PageComponentType (measured: the enum went 34 → 32 options, lost set exactly ['user:profile', 'element:form'], gained set empty). objectui went on knowing it in three places, so all three moved together: the Studio palette exclusion ledger, PROTOCOL_COMPONENTS in renderers/placeholders.tsx, and the regenerated known-schema-types.ts the CLI checks schemas against. Nothing user-reachable went with it — neither type had a renderer, user:profile had only the dashed "Component Placeholder" scaffold, and the app shell's own profile affordance is a React slot, never this block type. A page schema still naming it now draws the loud "Unknown component type" panel rather than a silent grey box, which is this repo's standing treatment for a type outside the supported surface.

.changeset/7704-schema-registry-chatbot-keys.md

  • names packages/cli/src/utils/known-schema-types.tspackages/cli/src/utils/known-schema-types.ts — edited by this change

    packages/plugin-chatbot/src/renderer.tsx registers three components — chatbot, chatbot-enhanced and chatbot-floating — and SchemaRegistry mapped one of them. Since ComponentType = keyof SchemaRegistry is the published union, a consumer discriminating on it was told two registered keys do not exist: an author narrowing a node by ComponentType, or writing a RecordANGLE-BRACKETS(ComponentType, …) table, had no arm for either. The asymmetry that showed which half was wrong is that packages/cli/src/utils/known-schema-types.ts keeps its own parallel list containing both keys, precisely because this map did not.

.changeset/8499-node-slot-registered-arms.md

  • names scripts/check-doc-component-types.mjsscripts/check-doc-component-types.mjs — edited by this change

    The defect, and the direction it ran. Nine type spellings sat at DECLARED node slots in this repository's own corpora and resolved in no arm of the component union. Eight were registered renderers with fixtures proving they draw; the ninth (my-component) is the reader's own plugin component and carries a written exemption in scripts/check-doc-component-types.mjs. A reader following content/docs/utilities/runner.mdx's own instruction — "copy one, wrap it in a page document … and save it as src/app-data/pages/index.json" — got a document that renders correctly in the browser and is refused by objectui check. That is the expensive direction: the likely reaction is to stop trusting the validator.

.changeset/8802-8257-8008-kanban-gantt-family-retirement.md

  • names known-schema-types.tspackages/cli/src/utils/known-schema-types.ts — edited by this change

    ⚠️ objectui check will NOT flag either namespaced spelling. The CLI's known-schema-types.ts is generated from the repository's real registration calls, and the placeholder registration is a real one — so view:kanban and view:gantt are still on that list and still validate green, while the node renders a placeholder rather than a board. The bare kanban / gantt entries DID leave the generated list; only the namespaced pair survives, and only because of the placeholder. Grep your documents for the namespaced spellings directly; do not rely on objectui check to find them.

Read the paragraph, not the line: both false halves of the objectui#8617 claim sat in one paragraph, and correcting either alone would have left it asserting the same wrong thing.

If a claim did go false, correct the body. That is precedented and prose-only, frontmatter untouched; check-changeset-overwrite.mjs will report the correction as its own case 2 ("correcting a declaration on purpose … legitimate"), which is the intended shape — one gate asks for the read, the other records the write.

Not covered, stated so nobody reads this as more: a claim that was born false (a changeset this change adds is excluded by construction), a claim spelled as a symbol or a package rather than a backticked file name, and a file named ambiguously.

Angle-bracketed names in the quoted prose above are rewritten as ANGLE-BRACKETS(name): GitHub deletes tag-shaped fragments from a stored body, and a quote that silently loses the identifier it is about is worse than a visible repair.

Compared the checked-out tree with ee70287e4 (merge-base with origin/main): 5 file(s) changed outside .changeset/, read against 1125 pending declaration(s) that publish a body (1633 pending in total). · run

@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Eager closure (gzip, 329 chunks) 3048.9 KB 3104.5 KB
Main entry chunk (gzip) 145.7 KB 350 KB
Entry file index-Ckww2Jo5.js
Status PASS

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

Package Size Gzipped
app-shell (consoleActionDispatch.js) 0.20KB 0.19KB
app-shell (index.js) 16.69KB 6.21KB
app-shell (runtime-config.js) 20.68KB 7.36KB
app-shell (types.js) 0.01KB 0.04KB
app-shell (urlParams.js) 10.06KB 3.86KB
auth (ActiveOrganizationStorage.js) 25.05KB 9.16KB
auth (AuthContext.js) 0.31KB 0.24KB
auth (AuthGuard.js) 2.07KB 1.00KB
auth (AuthProvider.js) 40.18KB 10.59KB
auth (AuthShell.js) 3.49KB 1.40KB
auth (ForgotPasswordForm.js) 12.21KB 3.45KB
auth (LoginForm.js) 18.15KB 5.39KB
auth (PreviewBanner.js) 0.90KB 0.50KB
auth (RegisterForm.js) 6.65KB 2.22KB
auth (SocialSignInButtons.js) 9.61KB 3.89KB
auth (UserMenu.js) 3.41KB 1.23KB
auth (auth-gate-events.js) 1.29KB 0.66KB
auth (authStyles.js) 5.04KB 1.72KB
auth (createAuthClient.js) 40.21KB 10.80KB
auth (createAuthenticatedFetch.js) 8.46KB 3.43KB
auth (index.js) 3.19KB 1.44KB
auth (invitation-status.js) 1.22KB 0.70KB
auth (org-roles.js) 6.66KB 2.78KB
auth (phone-identifier.js) 1.11KB 0.66KB
auth (types.js) 0.59KB 0.35KB
auth (useAuth.js) 5.30KB 1.02KB
auth (useWorkspaceAdminStatus.js) 11.08KB 4.58KB
collaboration (CommentThread.js) 26.08KB 7.56KB
collaboration (LiveCursors.js) 3.17KB 1.27KB
collaboration (PresenceAvatars.js) 6.49KB 2.64KB
collaboration (PresenceProvider.js) 2.79KB 1.13KB
collaboration (index.js) 1.68KB 0.73KB
collaboration (useCollaborationTranslation.js) 6.05KB 2.52KB
collaboration (useCommentSearch.js) 1.98KB 0.88KB
collaboration (useConflictResolution.js) 7.75KB 1.86KB
collaboration (useMentionNotifications.js) 1.81KB 0.68KB
collaboration (usePresence.js) 6.33KB 1.84KB
collaboration (useRealtimeSubscription.js) 7.91KB 2.01KB
components (index.js) 545.84KB 130.66KB
core (index.js) 8.94KB 3.59KB
create-plugin (index.js) 27.94KB 9.51KB
data-objectstack (index.js) 215.98KB 59.97KB
fields (index.js) 249.27KB 62.92KB
i18n (LocalizationContext.js) 1.76KB 0.96KB
i18n (builtinAggregateLabels.js) 0.86KB 0.49KB
i18n (currency.js) 1.22KB 0.64KB
i18n (fallbackInterpolation.js) 6.25KB 2.77KB
i18n (i18n.js) 8.87KB 3.64KB
i18n (index.js) 5.22KB 2.26KB
i18n (pickLocalized.js) 9.86KB 3.95KB
i18n (provider.js) 32.15KB 10.49KB
i18n (useDisplayLocale.js) 2.85KB 1.45KB
i18n (useObjectLabel.js) 34.34KB 9.17KB
i18n (useSafeTranslation.js) 5.60KB 2.33KB
layout (index.js) 38.83KB 10.95KB
mobile (MobileProvider.js) 0.92KB 0.49KB
mobile (ResponsiveContainer.js) 0.94KB 0.38KB
mobile (breakpoints.js) 1.51KB 0.70KB
mobile (createOfflineDataSource.js) 5.61KB 1.75KB
mobile (index.js) 1.99KB 0.87KB
mobile (offlineQueue.js) 3.91KB 1.35KB
mobile (pwa.js) 0.97KB 0.49KB
mobile (serviceWorker.js) 1.48KB 0.62KB
mobile (serviceWorkerSource.js) 3.41KB 1.48KB
mobile (useBreakpoint.js) 1.54KB 0.65KB
mobile (useGesture.js) 6.96KB 1.98KB
mobile (useOfflineSync.js) 1.99KB 0.72KB
mobile (usePullToRefresh.js) 2.53KB 0.85KB
mobile (useResponsive.js) 0.72KB 0.42KB
mobile (useSpecGesture.js) 4.39KB 1.66KB
mobile (useTouchTarget.js) 1.01KB 0.54KB
permissions (MePermissionsProvider.js) 13.52KB 4.88KB
permissions (PermissionContext.js) 0.31KB 0.25KB
permissions (PermissionGuard.js) 0.89KB 0.45KB
permissions (PermissionProvider.js) 6.24KB 2.16KB
permissions (discardProofCache.js) 1.04KB 0.55KB
permissions (evaluator.js) 8.39KB 3.10KB
permissions (index.js) 0.93KB 0.41KB
permissions (store.js) 0.91KB 0.42KB
permissions (useFieldPermissions.js) 1.28KB 0.53KB
permissions (usePermissions.js) 4.83KB 2.27KB
plugin-ai (index.js) 14.81KB 3.63KB
plugin-calendar (index.js) 49.92KB 14.22KB
plugin-charts (index.js) 71.33KB 19.90KB
plugin-chatbot (index.js) 195.34KB 46.51KB
plugin-dashboard (index.js) 131.44KB 34.65KB
plugin-designer (index.js) 215.94KB 44.33KB
plugin-detail (index.js) 253.29KB 65.88KB
plugin-editor (index.js) 2.23KB 1.05KB
plugin-form (index.js) 136.71KB 34.16KB
plugin-gantt (index.js) 167.62KB 41.26KB
plugin-grid (index.js) 212.64KB 57.91KB
plugin-kanban (index.js) 46.41KB 14.49KB
plugin-list (index.js) 112.73KB 27.69KB
plugin-map (index.js) 21.48KB 6.99KB
plugin-markdown (index.js) 13.88KB 4.80KB
plugin-report (index.js) 43.41KB 11.93KB
plugin-timeline (index.js) 30.07KB 8.74KB
plugin-tree (index.js) 10.58KB 3.72KB
plugin-view (index.js) 85.04KB 21.01KB
providers (DataSourceProvider.js) 0.75KB 0.39KB
providers (MetadataProvider.js) 1.37KB 0.59KB
providers (ThemeProvider.js) 1.90KB 0.85KB
providers (UploadProvider.js) 11.66KB 3.50KB
providers (index.js) 0.45KB 0.23KB
providers (types.js) 0.01KB 0.04KB
react-runtime (index.js) 5.62KB 2.34KB
react (LazyPluginLoader.js) 4.47KB 1.63KB
react (SchemaRenderer.js) 104.82KB 34.67KB
react (data-invalidation.js) 5.05KB 2.08KB
react (index.js) 4.63KB 2.18KB
react (schema-input.js) 4.25KB 2.04KB
react (spec-input.js) 0.20KB 0.18KB
sdui-parser (codegen.js) 6.58KB 2.74KB
sdui-parser (dashboard-widget-options.js) 3.08KB 1.30KB
sdui-parser (index.js) 5.66KB 2.50KB
sdui-parser (input-type.js) 2.84KB 1.40KB
sdui-parser (kanban-quick-add.js) 3.89KB 1.87KB
sdui-parser (parse.js) 25.28KB 7.80KB
sdui-parser (provenance.js) 3.66KB 1.82KB
sdui-parser (types.js) 0.28KB 0.23KB
sdui-parser (validate.js) 14.82KB 4.99KB
types (ai.js) 4.11KB 2.06KB
types (api-types.js) 0.20KB 0.18KB
types (app.js) 2.87KB 1.00KB
types (base.js) 0.20KB 0.18KB
types (blocks.js) 0.20KB 0.18KB
types (complex.js) 2.93KB 1.49KB
types (crud.js) 0.20KB 0.18KB
types (dashboard-filter-alias.js) 6.23KB 2.74KB
types (data-display.js) 3.75KB 1.85KB
types (data-protocol.js) 0.20KB 0.19KB
types (data.js) 0.20KB 0.18KB
types (designer.js) 1.85KB 0.85KB
types (disclosure.js) 0.20KB 0.18KB
types (error-code.js) 1.54KB 0.88KB
types (expression.js) 0.20KB 0.18KB
types (feedback.js) 0.20KB 0.18KB
types (field-types.js) 0.20KB 0.18KB
types (form.js) 0.20KB 0.18KB
types (http-inflight.js) 8.87KB 3.73KB
types (http-retry.js) 4.32KB 2.02KB
types (icon-key-migration.js) 4.26KB 1.63KB
types (index.js) 4.74KB 2.25KB
types (layout.js) 0.20KB 0.18KB
types (managed-by.js) 0.19KB 0.18KB
types (mobile.js) 4.73KB 2.28KB
types (navigation.js) 0.20KB 0.18KB
types (objectql.js) 0.20KB 0.18KB
types (overlay.js) 0.20KB 0.18KB
types (permissions.js) 0.20KB 0.18KB
types (plugin-scope.js) 0.20KB 0.18KB
types (record-components.js) 0.20KB 0.19KB
types (record-semantics.js) 1.28KB 0.67KB
types (registry.js) 0.20KB 0.18KB
types (reports.js) 0.20KB 0.18KB
types (select-option.js) 0.20KB 0.19KB
types (spec-report.js) 5.05KB 1.93KB
types (spec-ui-namespace.js) 0.20KB 0.19KB
types (strict-authoring-face.js) 14.04KB 5.36KB
types (system-fields.js) 3.33KB 1.54KB
types (theme.js) 6.28KB 2.87KB
types (ui-action.js) 8.11KB 3.32KB
types (views.js) 0.20KB 0.18KB
types (widget.js) 0.20KB 0.18KB

Size Limits

  • ✅ Core packages should be < 50KB gzipped
  • ✅ Component packages should be < 100KB gzipped
  • ⚠️ Plugin packages should be < 150KB gzipped

Copy link
Copy Markdown
Collaborator Author

At-tier contract review record — PR #9686 / card objectui#9641 — FAIL

Adopted VERBATIM by the domain:spec @ objectui execution seat (session_01UanLVj6xvbS6puBCewLr8L), per 「逐字采纳或整体作废,没有第三种」. ⛔ Not one finding was softened, dropped or re-ranked.

Tier, from the reviewer's OWN transcript (⛔ not get_session): message.model reads claude-fable-5-1, 70 of 70 — at CONTRACT_REVIEW_TIER. Firing control: the three os-dev implementer transcripts in the same session read claude-opus-5 (137 / 147 / 152). ⇒ the field discriminates.

Isolation: the reviewer got the card, the PR and the two scripts' own headers — ⛔ never the dispatch order, ⛔ never this seat's conclusions, ⛔ never the implementer's report as an input.


VERDICT: FAIL

Subject: objectstack-ai/objectui PR #9686 (head b1fa3b707ca4, merge-base 72f55c9ec118, draft, behind main 61b7553469ae) against card objectui#9641.

Working-directory fence: every mutation (clone, worktrees, vitest/typescript install, ablation, probe fixtures) happened under this review's own scratchpad directory (oui/ = PR head, oui-base/ = merge base, oui-merged/ = PR + real main tip, vt/ = standalone vitest 4.1.10 + typescript 6.0.3). Disclosure: my very first command ran git fetch origin main inside /home/user/objectui before I had thought about the fence; it was a ref-only no-op (origin/main was 61b7553 before and after, working tree untouched), and nothing else touched either shared checkout.

What I measured

# Reading Source Control
1 Fenced files: 0 diff lines git diff 72f55c9..b1fa3b7 -- the two fenced paths full PR diff: 6 files, +353/−8
2 Base derivation 644 keys → head derivation 649; diff = exactly the five ui: page keys added, 0 removals deriveRegistryKeys() run on each worktree, sorted key dumps diffed head derivation run over the base tree: also 649, 0 findings, metaViaReference=5 ⇒ the change is generator-side, not tree-side
3 Five keys present in head derivation: ui:page ui:app ui:utility ui:home ui:record = 1 each same run lit controls page:header=1, element:text=1, action:button=1; absent-token ui:no-such-thing=0
4 Regeneration: renderModule(deriveKnownSchemaTypes()) at head → blob 45bf0e990f… == committed blob 45bf0e990f…; regenerate --check exit 0 my own run in oui/ base --check exit 0 (self-consistent); PR merged onto real main 61b7553 in oui-merged/: --check 0, check:doc-types 0
5 Runtime keys are real: pageMeta.namespace: 'ui', five register() calls (page by identifier, app/utility/home/record by spread); Registry.register stores the namespaced key and the bare key unless skipFallback page.tsx lines 682–715, Registry.ts register() at 457–492 (origin/main) parity roster packages/app-shell/src/__tests__/widget-dom-leak-sweep.test.tsx lists all five ui: page keys
6 Pins at head: 74 passed / 0 failed (2 files) standalone vitest, own config, --root oui
7 Ablation: base check-doc-component-types.mjs (blob 47a9397b…) swapped in, PR tests + PR generated file: 8 failed / 66 passed; restore proven (blob back to 517d1135…, status clean) same runner the 8 are the 4 fixture pins, 2 live-tree pins, the byte-for-byte regenerability pin and ships no type that nothing registers — matches the PR's claim
8 Independent census (own scanner, comment-stripped via scripts/js-comment-mask.mjs, base tree): 242 sites — 232 plain object literal · 3 no options · 2 bare identifier (page.tsx:711 pageMeta; PluginScopeImpl.ts:110 registryMeta, an OPEN site whose key is unresolvable first) · 4 identifier spreads (page.tsx:712–715) · 1 non-identifier spread (packages/fields/src/index.tsx:3917, a conditional-object spread), an INDIRECT site whose options the resolver never reads census-meta-shapes.mjs PR's census also reads 242 / 6 by reference — agrees
9 Sibling-form probe, 19 fixture trees, head derivation: RESOLVED identifier, identifier-spread, export const, satisfies, registerLazy, spread+skipFallback, comment-with-apostrophe, and reassigned let; REPORTED imported identifier, imported spread, Object.freeze(...); SILENT-BARE m as any, a member-expression spread, a call-expression spread, a call expression, a member expression, a non-literal namespace value, a conditional spread, and a same-name declaration in an earlier function scope probe-sibling-forms.mjs base derivation: all 19 SILENT-BARE
10 End-to-end on the live gate with a probe package: imported-identifier shape → check:doc-types exit 1 with [unresolved-registration-meta] printed, regenerate --check exit 1; call-expression shape → check:doc-types exit 0, 0 mentions, derived list holds the bare key but not the namespaced one; m as any → same silent loss probe files under a throwaway package, removed afterwards
11 Gates run by me at head: changeset-presence 0 · changeset-no-major 0 · changeset-fixed 0 · changeset-claims 0 · pending-changeset-literals 0 · prompt-keys 0 · control-bytes 0 · new-line-citations 0 new · registry-bare-names 0 · test-path-roots 0 · doc-types 0 node runs not measurable here: type-check:scripts (63 errors, all TS2307/TS7006 missing workspace modules, identical 63 on base, 0 in any PR file) and check:spec-symbols (needs @objectstack/spec, same failure on base)
12 Changeset: "@object-ui/cli": patch; cli is in the fixed group (40 packages); the only runtime consumer is packages/cli/src/commands/check.ts via isKnownSchemaType files at head
13 HINTS table has no unresolved-registration-meta entry; consumer iterates Object.keys(HINTS) so the effect is only a missing trailing hint. counters.metaViaReference is printed by no script grep at head

What I checked and found

  1. Placement. Correct, not scope drift. The rule the PR cites exists verbatim at the merge base in scripts/regenerate-known-schema-types.mjs ("One derivation, two consumers": the universe comes from deriveRegistryKeys "deliberately the SAME derivation … not a second scanner with its own bugs"), and the script really has no scanner — deriveKnownSchemaTypes calls deriveRegistryKeys and sorts. The header also rules out the runtime-registry route ("Why a generated snapshot rather than a runtime lookup"). The dev declared the file-surface amendment in the report. ✔
  2. Genuinely regenerated. Proven by regeneration, not by content: my own renderModule(deriveKnownSchemaTypes()) is byte-identical to the committed blob, --check is 0 at head and after merging onto today's main, and the ablation shows the base derivation cannot produce this file. ✔
  3. No phantom keys. Each of the five maps to a real register() with a 'ui' namespace and Registry.register stores that spelling; the app-shell parity roster carries all five; key-set diff is additions only, nothing removed or renamed. ✔
  4. Defect class — partially drawn, and overclaimed. The PR correctly widened the card's "spread" to "options not spelled out at the call" and fixed two shapes (identifier, identifier-spread). But of that class it leaves seven sibling shapes silent-bare (reading 9) and I reproduced two of them on the live gate (reading 10): the gate stays green while the namespaced half is lost. Meanwhile three pieces of prose now assert the opposite — the regenerate header re-asserts, and calls "load-bearing", the clause "any registration form it cannot resolve fails there rather than silently shrinking the universe here"; the derivation header says "reports a spread it cannot follow, so the universe can no longer shrink without saying so"; the changeset says "a spread it cannot follow is reported instead of assumed namespace-free". All three are measurably false for the call-expression spread, the member-expression spread, the conditional spread, a bare call expression, a bare member expression, an as any cast, and a non-literal namespace value. This is exactly the shape the brief warns about: a partial fix that reads as complete — and it is the card's own defect ("a header asserted while false") re-landed. The non-identifier spread idiom is not hypothetical in this codebase (packages/fields/src/index.tsx:3917). ✘
  5. Census. Independently confirmed: 242 sites, the five page registrations are the only ones losing a namespace today. The live instrument (metaViaReference > 0 pin) guards the non-vacuity of the fixed shapes only; it cannot fire on any of the silent shapes, so the "cannot silently go quiet" property holds for regression of this fix, not for the class. ✔ for today's tree, ✘ as a class guard.
  6. Pins are real. Fixture-tree pins fail on the base derivation and pass on the new one; the live-tree pins are paired with the byte-for-byte regenerability pin; ablation 8/66 reproduced under my own runner. ✔
  7. Changeset. Right package (@object-ui/cli, the only released source touched), right level (patch — no export type moved, behaviour widened to match runtime), no major. Prose: the spread-reporting sentence is untrue as written (see 4). ✔ / ✘ prose.
  8. Hard fence. 0 diff lines on page.tsx and Registry.ts; key-set diff shows no registration added, removed or renamed. objectui#9263 untouched. ✔

Blocking items

  1. Close the silent sibling shapes, or stop claiming they are closed. In resolveRegistrationOptions / readMetaBody (scripts/check-doc-component-types.mjs), any options argument that is present and is neither (a) a bare identifier resolvable to a same-file object literal nor (b) an object literal whose top-level entries are all key-value pairs or same-file identifier spreads must push unresolved-registration-meta instead of falling through to the whole-span regex. Measured silent today: an as any cast, a member expression, a call expression, a call-expression spread, a member-expression spread, a conditional spread, and a non-string-literal namespace value. Reading 8 shows zero of the 238 resolved sites use any of these, so the tightening reds nothing on the live tree. Add one fixture pin per shape in scripts/__tests__/check-doc-component-types.test.ts (each is SILENT-BARE on the current branch, so each will fail before the change and pass after — the same ablation discipline the PR already used). If the seat chooses not to close them, the three prose claims in item 3 must be narrowed to "an identifier or identifier-spread it cannot follow", and the regenerate header must not re-assert the load-bearing clause.
  2. declaredObjectBody reads the first const|let|var NAME = { anywhere in the file, ignoring scope and reassignment. Measured: a let binding initialised with a namespaced object and then reassigned to one without it still derives the namespaced key — a phantom (the runtime stores only the bare key), the direction 防 AI 写元数据犯错 forbids; and a function-scoped declaration earlier in the file shadows a module-level one — a silent miss. Remedy inside the function being added: follow the name only when the comment-stripped file contains exactly one declaration of that name and it is const; otherwise report. Pin both cases.
  3. Prose must match the code that ships. After 1–2 are settled, re-read and correct: (a) the scripts/regenerate-known-schema-types.mjs header paragraph that re-asserts "any registration form it cannot resolve fails there rather than silently shrinking the universe here"; (b) the BY REFERENCE bullet in scripts/check-doc-component-types.mjs ("reports a spread it cannot follow, so the universe can no longer shrink without saying so"); (c) .changeset/9641-registry-namespace-by-reference.md ("a spread it cannot follow is reported instead of assumed namespace-free"); (d) the test title "reports a spread it cannot follow rather than reading the registration as bare". Per AGENTS 完善设计器的每一个细节 #9, a property asserted in a header must be one an instrument enforces.

Non-blocking observations (not gating)

  • HINTS in check-doc-component-types.mjs has no entry for unresolved-registration-meta; the consumer loops over HINTS keys, so the only effect is no trailing hint for the new reason. Worth adding for symmetry with unresolved-registration.
  • The live-tree pin comment says "The counter the derivation prints" — metaViaReference is not printed by any summary line; either add it to the check:doc-types summary or reword.
  • declaredObjectBody builds a RegExp from the identifier without escaping; a $-bearing name never matches and is therefore reported (loud direction), so acceptable, but sloppy.
  • The type-annotation pattern cannot cross an arrow inside an annotation; that also fails loud (reported), acceptable.
  • Not measurable in my environment (missing workspace deps, identical failure on the base control): type-check:scripts and check:spec-symbols; 0 of the 63 tsc errors are in PR files. CI is the reader for those two.
  • The PR is a draft and behind main; the trial merge onto 61b7553 is clean and both consumers stay green there.
  • PM comment's page:header = 2 vs the dev's 1 is a grep-pattern difference (the derivation stores one key); non-load-bearing either way.
  • The out-of-scope notes in the dev report (a nested namespace key inside an inputs entry on the direct path; a computed skipFallback at the fields indirect site) are accurate as far as I could verify; the fields site is covered by INDIRECT_REGISTRATIONS.skipFallbackSet.

Artefacts (all under the scratchpad review dir): keys-base.txt / keys-head.txt, regen-head.ts, vitest-head.out, vitest-ablation.out, probe-sibling-forms.mjs, census-meta-shapes.mjs, probe and gate logs, tsc-scripts logs.


⚠️ Departures from verbatim, disclosed rather than hidden. GitHub's sanitiser deletes tag-shaped fragments from a stored body even inside backticks, so the reviewer's concrete code spellings for the silent shapes (a non-string-literal namespace value, the member/call/conditional spread idioms, the scratchpad path, the fenced file paths, and one arrow-function mention) are rendered as descriptions of the same shapes. ⛔ No finding, count, verdict or ranking was altered — only spellings that would otherwise have been silently eaten. The exact idioms are reproducible from the reviewer's probe-sibling-forms.mjs artefact.

— Record posted by PM domain:spec @ objectui · session session_01UanLVj6xvbS6puBCewLr8L · 2026-09-17T11:56Z


Generated by Claude Code

The first pass taught `deriveRegistryKeys` the two options shapes this
tree uses -- a bare identifier and an identifier spread -- and left the
siblings falling through to the whole-span regex, which finds no
`namespace:` and yields a bare-only reading with no finding. Measured
silent: a cast, a member expression, a call, a spread of any of those, a
conditional spread, a computed `namespace` and a computed `skipFallback`.
That is the defect this work exists to close, wearing a different
spelling -- and three headers were meanwhile asserting the opposite.

`resolveRegistrationOptions` now works from an ALLOWLIST: an object
literal whose top-level entries are all key-value pairs or plain
identifier spreads, or an identifier resolving to one such literal.
Everything else is an `unresolved-registration-meta` finding. Zero of the
resolved call sites in this tree use a refused shape, so the tightening
reds nothing and the derived universe is byte-identical -- `regenerate
--check` stays 0 with the committed file untouched.

`declaredObjectBody` follows a name only when the file declares it
exactly once and with `const`. A `let` initialised with a namespaced
object and reassigned used to derive the namespaced key: a PHANTOM, a key
the runtime never stores, which green-lights a spelling that renders
nothing. A second declaration of the same name shadowed the one the call
reads, which loses a key instead.

Prose corrected at every site that claimed more than the code did, plus
the missing `HINTS` entry and the run summary that now prints
`metaViaReference`, so the pin that points at that counter is true.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UanLVj6xvbS6puBCewLr8L
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Eager closure (gzip, 329 chunks) 3048.9 KB 3104.5 KB
Main entry chunk (gzip) 145.7 KB 350 KB
Entry file index-DbLzpmc-.js
Status PASS

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

Package Size Gzipped
app-shell (consoleActionDispatch.js) 0.20KB 0.19KB
app-shell (index.js) 16.69KB 6.21KB
app-shell (runtime-config.js) 20.68KB 7.36KB
app-shell (types.js) 0.01KB 0.04KB
app-shell (urlParams.js) 10.06KB 3.86KB
auth (ActiveOrganizationStorage.js) 25.05KB 9.16KB
auth (AuthContext.js) 0.31KB 0.24KB
auth (AuthGuard.js) 2.07KB 1.00KB
auth (AuthProvider.js) 40.18KB 10.59KB
auth (AuthShell.js) 3.49KB 1.40KB
auth (ForgotPasswordForm.js) 12.21KB 3.45KB
auth (LoginForm.js) 18.15KB 5.39KB
auth (PreviewBanner.js) 0.90KB 0.50KB
auth (RegisterForm.js) 6.65KB 2.22KB
auth (SocialSignInButtons.js) 9.61KB 3.89KB
auth (UserMenu.js) 3.41KB 1.23KB
auth (auth-gate-events.js) 1.29KB 0.66KB
auth (authStyles.js) 5.04KB 1.72KB
auth (createAuthClient.js) 40.21KB 10.80KB
auth (createAuthenticatedFetch.js) 8.46KB 3.43KB
auth (index.js) 3.19KB 1.44KB
auth (invitation-status.js) 1.22KB 0.70KB
auth (org-roles.js) 6.66KB 2.78KB
auth (phone-identifier.js) 1.11KB 0.66KB
auth (types.js) 0.59KB 0.35KB
auth (useAuth.js) 5.30KB 1.02KB
auth (useWorkspaceAdminStatus.js) 11.08KB 4.58KB
collaboration (CommentThread.js) 26.08KB 7.56KB
collaboration (LiveCursors.js) 3.17KB 1.27KB
collaboration (PresenceAvatars.js) 6.49KB 2.64KB
collaboration (PresenceProvider.js) 2.79KB 1.13KB
collaboration (index.js) 1.68KB 0.73KB
collaboration (useCollaborationTranslation.js) 6.05KB 2.52KB
collaboration (useCommentSearch.js) 1.98KB 0.88KB
collaboration (useConflictResolution.js) 7.75KB 1.86KB
collaboration (useMentionNotifications.js) 1.81KB 0.68KB
collaboration (usePresence.js) 6.33KB 1.84KB
collaboration (useRealtimeSubscription.js) 7.91KB 2.01KB
components (index.js) 545.84KB 130.66KB
core (index.js) 8.94KB 3.59KB
create-plugin (index.js) 27.94KB 9.51KB
data-objectstack (index.js) 215.98KB 59.97KB
fields (index.js) 249.27KB 62.92KB
i18n (LocalizationContext.js) 1.76KB 0.96KB
i18n (builtinAggregateLabels.js) 0.86KB 0.49KB
i18n (currency.js) 1.22KB 0.64KB
i18n (fallbackInterpolation.js) 6.25KB 2.77KB
i18n (i18n.js) 8.87KB 3.64KB
i18n (index.js) 5.22KB 2.26KB
i18n (pickLocalized.js) 9.86KB 3.95KB
i18n (provider.js) 32.15KB 10.49KB
i18n (useDisplayLocale.js) 2.85KB 1.45KB
i18n (useObjectLabel.js) 34.34KB 9.17KB
i18n (useSafeTranslation.js) 5.60KB 2.33KB
layout (index.js) 38.83KB 10.95KB
mobile (MobileProvider.js) 0.92KB 0.49KB
mobile (ResponsiveContainer.js) 0.94KB 0.38KB
mobile (breakpoints.js) 1.51KB 0.70KB
mobile (createOfflineDataSource.js) 5.61KB 1.75KB
mobile (index.js) 1.99KB 0.87KB
mobile (offlineQueue.js) 3.91KB 1.35KB
mobile (pwa.js) 0.97KB 0.49KB
mobile (serviceWorker.js) 1.48KB 0.62KB
mobile (serviceWorkerSource.js) 3.41KB 1.48KB
mobile (useBreakpoint.js) 1.54KB 0.65KB
mobile (useGesture.js) 6.96KB 1.98KB
mobile (useOfflineSync.js) 1.99KB 0.72KB
mobile (usePullToRefresh.js) 2.53KB 0.85KB
mobile (useResponsive.js) 0.72KB 0.42KB
mobile (useSpecGesture.js) 4.39KB 1.66KB
mobile (useTouchTarget.js) 1.01KB 0.54KB
permissions (MePermissionsProvider.js) 13.52KB 4.88KB
permissions (PermissionContext.js) 0.31KB 0.25KB
permissions (PermissionGuard.js) 0.89KB 0.45KB
permissions (PermissionProvider.js) 6.24KB 2.16KB
permissions (discardProofCache.js) 1.04KB 0.55KB
permissions (evaluator.js) 8.39KB 3.10KB
permissions (index.js) 0.93KB 0.41KB
permissions (store.js) 0.91KB 0.42KB
permissions (useFieldPermissions.js) 1.28KB 0.53KB
permissions (usePermissions.js) 4.83KB 2.27KB
plugin-ai (index.js) 14.81KB 3.63KB
plugin-calendar (index.js) 49.92KB 14.22KB
plugin-charts (index.js) 71.49KB 19.99KB
plugin-chatbot (index.js) 195.34KB 46.51KB
plugin-dashboard (index.js) 131.44KB 34.65KB
plugin-designer (index.js) 215.94KB 44.33KB
plugin-detail (index.js) 253.29KB 65.88KB
plugin-editor (index.js) 2.23KB 1.05KB
plugin-form (index.js) 136.71KB 34.16KB
plugin-gantt (index.js) 167.62KB 41.26KB
plugin-grid (index.js) 212.64KB 57.91KB
plugin-kanban (index.js) 46.41KB 14.49KB
plugin-list (index.js) 112.73KB 27.69KB
plugin-map (index.js) 21.48KB 6.99KB
plugin-markdown (index.js) 13.88KB 4.80KB
plugin-report (index.js) 43.41KB 11.93KB
plugin-timeline (index.js) 30.07KB 8.74KB
plugin-tree (index.js) 10.58KB 3.72KB
plugin-view (index.js) 85.04KB 21.01KB
providers (DataSourceProvider.js) 0.75KB 0.39KB
providers (MetadataProvider.js) 1.37KB 0.59KB
providers (ThemeProvider.js) 1.90KB 0.85KB
providers (UploadProvider.js) 11.66KB 3.50KB
providers (index.js) 0.45KB 0.23KB
providers (types.js) 0.01KB 0.04KB
react-runtime (index.js) 5.62KB 2.34KB
react (LazyPluginLoader.js) 4.47KB 1.63KB
react (SchemaRenderer.js) 104.82KB 34.67KB
react (data-invalidation.js) 5.05KB 2.08KB
react (index.js) 4.63KB 2.18KB
react (schema-input.js) 4.25KB 2.04KB
react (spec-input.js) 0.20KB 0.18KB
sdui-parser (codegen.js) 6.58KB 2.74KB
sdui-parser (dashboard-widget-options.js) 3.08KB 1.30KB
sdui-parser (index.js) 5.66KB 2.50KB
sdui-parser (input-type.js) 2.84KB 1.40KB
sdui-parser (kanban-quick-add.js) 3.89KB 1.87KB
sdui-parser (parse.js) 25.28KB 7.80KB
sdui-parser (provenance.js) 3.66KB 1.82KB
sdui-parser (types.js) 0.28KB 0.23KB
sdui-parser (validate.js) 14.82KB 4.99KB
types (ai.js) 4.11KB 2.06KB
types (api-types.js) 0.20KB 0.18KB
types (app.js) 2.87KB 1.00KB
types (base.js) 0.20KB 0.18KB
types (blocks.js) 0.20KB 0.18KB
types (complex.js) 2.93KB 1.49KB
types (crud.js) 0.20KB 0.18KB
types (dashboard-filter-alias.js) 6.23KB 2.74KB
types (data-display.js) 3.75KB 1.85KB
types (data-protocol.js) 0.20KB 0.19KB
types (data.js) 0.20KB 0.18KB
types (designer.js) 1.85KB 0.85KB
types (disclosure.js) 0.20KB 0.18KB
types (error-code.js) 1.54KB 0.88KB
types (expression.js) 0.20KB 0.18KB
types (feedback.js) 0.20KB 0.18KB
types (field-types.js) 0.20KB 0.18KB
types (form.js) 0.20KB 0.18KB
types (http-inflight.js) 8.87KB 3.73KB
types (http-retry.js) 4.32KB 2.02KB
types (icon-key-migration.js) 4.26KB 1.63KB
types (index.js) 4.74KB 2.25KB
types (layout.js) 0.20KB 0.18KB
types (managed-by.js) 0.19KB 0.18KB
types (mobile.js) 4.73KB 2.28KB
types (navigation.js) 0.20KB 0.18KB
types (objectql.js) 0.20KB 0.18KB
types (overlay.js) 0.20KB 0.18KB
types (permissions.js) 0.20KB 0.18KB
types (plugin-scope.js) 0.20KB 0.18KB
types (record-components.js) 0.20KB 0.19KB
types (record-semantics.js) 1.28KB 0.67KB
types (registry.js) 0.20KB 0.18KB
types (reports.js) 0.20KB 0.18KB
types (select-option.js) 0.20KB 0.19KB
types (spec-report.js) 5.05KB 1.93KB
types (spec-ui-namespace.js) 0.20KB 0.19KB
types (strict-authoring-face.js) 14.04KB 5.36KB
types (system-fields.js) 3.33KB 1.54KB
types (theme.js) 6.28KB 2.87KB
types (ui-action.js) 8.11KB 3.32KB
types (views.js) 0.20KB 0.18KB
types (widget.js) 0.20KB 0.18KB

Size Limits

  • ✅ Core packages should be < 50KB gzipped
  • ✅ Component packages should be < 100KB gzipped
  • ⚠️ Plugin packages should be < 150KB gzipped

Copy link
Copy Markdown
Collaborator Author

At-tier contract review record — PR #9686 @ head b902cc1c / card objectui#9641 — FAIL (round 2)

Adopted VERBATIM by the domain:spec @ objectui seat (session_01UanLVj6xvbS6puBCewLr8L), per 「逐字采纳或整体作废」. ⛔ Nothing softened, dropped or re-ranked.

Tier, from the reviewer's OWN transcript (⛔ not get_session): claude-fable-5-1, 56 of 56. Firing control: implementer transcripts in this session read claude-opus-5.

Isolation: card + PR + the two script headers only — ⛔ never the dispatch order, ⛔ never this seat's conclusions, ⛔ never the implementer's reports, ⛔ never the round-1 review record.

Seat corroboration of the blocking finding, taken independently before adopting: grepping the head's check-doc-component-types.mjs for any mutation guard (Object.assign, delete , an assignment to .namespace or .skipFallback) returns only unrelated hits — README prose and error-message strings. The const rule at line 68 reads "declared exactly once, with const, in the same file" and says nothing about mutation after declaration. ⇒ the gap is structural and reproduces on inspection, ⛔ not merely asserted.


VERDICT: FAIL

Contract review R2 — objectstack-ai/objectui PR #9686 at head b902cc1ccb522dd97f5b61b1367be5a1f456543c, judged whole against merge base 72f55c9ec118946d3a7f839013b27d87f78c77d3 (the parent of the PR's first commit; GitHub's base sha 61b7553 is newer main and is not an ancestor of the head), card objectui#9641. All execution was in my own clone under the review's scratchpad directory; the shared checkouts were only read (git show / git cat-file). Probe scripts kept next to it: shape-probe.mjs, guard-probe.mjs, census.mjs, compare-universe.mjs, mvr-probe.mjs.

What I measured

reading source value control
Diff surface 72f55c9..b902cc1 git diff --stat 6 files, +589/−8: the changeset, packages/cli/src/utils/known-schema-types.ts (+5/−0), scripts/check-doc-component-types.mjs (+268/−5), scripts/regenerate-known-schema-types.mjs (+30/−3), two files under scripts/__tests__/
Fence: diff lines on the two fenced paths git diff 72f55c9..HEAD over both 0 same command on known-schema-types.ts: 44 lines
Registration lines (.register(/.registerLazy() added or removed anywhere under packages/apps/examples git diff piped to grep 0 the 5 unchanged page.tsx:711–715 calls read fine in the census below
Derived universe, live tree, merge-base derivation vs head derivation (both loaded side by side in one node process, compare-universe.mjs) deriveRegistryKeys(root) base 644 keys / 0 findings; head 649 / 0 findings; ADDED = exactly ui:app ui:home ui:page ui:record ui:utility, each attributed to page.tsx:711–715; REMOVED = [] lit controls page:header, element:text, action:button = 1 on both; absent token no-such-key-zzz = 0 on both
Regenerability node scripts/regenerate-known-schema-types.mjs --check at head exit 0; then regenerate (write) → git diff on TARGET = 0 lines, git status clean under the base derivation the byte-for-byte pin goes red (ablation A)
Live gate node scripts/check-doc-component-types.mjs exit 0; summary prints 649 registered key(s) … 233 resolved call site(s), 5 via referenced options, 132 indirect, 2 open
Second, runtime-derived instrument packages/app-shell/src/__tests__/widget-dom-leak-sweep.test.tsx roster each of the five keys = 1 page:header = 1; no-such-key-zzz = 0
Runtime storage rule Registry.ts register() / registerLazy() the namespaced key when a namespace is present, plus the bare alias unless skipFallback pageMeta (page.tsx:682–709): namespace 'ui', no skipFallback; declared exactly once in the file
Pins at head vitest, the two pin files 84 passed / 0 failed
Ablation A — base derivation 47a9397b swapped in (blob hash verified before run, restore verified to 59b1a920 after) same two files 18 failed / 66 passed: all 14 new mechanism pins + ships the namespaced half…, is not vacuous…, byte for byte, ships no type that nothing registers the 66 that pass are the non-vacuity control
Ablation B — R0 head derivation 517d1135 swapped in same two files 10 failed / 74 passed: exactly the 7 table rows + computed skipFallback + phantom + shadowing
Full declared surface vitest scripts/__tests__ + packages/cli/src/__tests__ 187 files passed, 2 skipped; 5169 passed / 0 failed
Other gates tsc -p tsconfig.scripts.json exit 0 (0 errors); eslint on the 4 changed script/test files exit 0; check-changeset-presence 0; check-changeset-claims 0; check-pending-changeset-literals 0; check-registry-bare-name-collisions 0; check-prompt-component-keys 0
Census of how options arrive, taken from the instrument (a probe copy of the head derivation pushing every resolved site's third argument) census.mjs 233 resolved = 210 plain literals + 17 literals whose only spread is nested (depth>0, read correctly as literals) + 4 top-level spreads of pageMeta + 1 identifier pageMeta + 1 with no options; callSites counter = 238 (not 242) PluginScopeImpl.ts:110 passes registryMeta by identifier but its key is a parameter → open site, never reaches the options read (verified in source)
Shape probe (shape-probe.mjs, 52 fixture shapes against the head derivation) see Q4 8 pinned shapes: all LOUD. 20 further unmentioned shapes (satisfies, a non-null assertion, a cast on a literal, shorthand property, computed key, template-literal namespace, an explicitly undefined namespace, Object.freeze, destructuring, multi-declarator, var, spread of a let, regex literal with a stray quote, a truthy-coerced skipFallback, method shorthand, …): all LOUD. 12 further shapes READ correctly (as const, type annotation, export const, two spreads later-wins, nested spread chain, apostrophe in an earlier string, a namespace nested under an inputs entry only, trailing comma, registerLazy, loop-form + identifier, double-quoted key, arrow value with a comma). 4 shapes SILENT-WRONG, 2 further silent with runtime-dependent answer — detailed under Q4 the CTRL rows (literal at call, identifier to const, spread of const, no options, empty literal) all READ-correct

What I checked and found

1. Where the repair landed. The card names scripts/regenerate-known-schema-types.mjs; the repair is in scripts/check-doc-component-types.mjs. I verified the rule the PR cites exists at the merge base, in the regenerate header's "One derivation, two consumers" section: the universe "comes from deriveRegistryKeys … deliberately the SAME derivation … not a second scanner with its own bugs", and deriveKnownSchemaTypes() calls deriveRegistryKeys directly and throws on any finding. The regenerate script has no options reader of its own to repair; a resolver added there would be the second scanner the header forbids. The placement is correct, and the amendment is declared in the PR body and the card report rather than hidden. The derivation header also still carries the base clause about "a registration form it cannot resolve fails there rather than silently shrinking the universe here" — verified present at base, so the card's "asserted while false" claim is accurate.

2. Regenerated, not hand-edited. Distinguished by regeneration: --check exits 0 at head, a fresh write leaves 0 diff lines, the byte-for-byte pin passes at head and fails under the base derivation. The +5 insertions sit exactly where the sort places them. Nothing else in the file moved.

3. No phantom keys, nothing removed. Each added key traces to one page.tsx registration with namespace 'ui' and no skipFallback, which Registry.register stores as the namespaced key plus the bare alias. The five appear as real runtime keys in the independent, runtime-derived leak-sweep roster. REMOVED set is empty; the bare five are still present and pinned. check:registry-bare-names is 0, so the new namespaced halves collide with nothing.

4. Is the class fully drawn? — Partly. This is the blocking finding. The class, as the PR itself refines it, is "options that are not spelled out at the call, whichever way they arrive". The shape allowlist is genuinely structural at the argument level: everything I threw at the argument that is not an object literal of pairs/plain-identifier spreads, or an identifier, is refused loudly, including shapes the PR never lists. But the identifier route rests on a premise — the declared const literal is the object the call passes — and the diff enforces that premise against reassignment (the let refusal) while leaving mutation unread. Measured on the head derivation, all with findings = []:

fixture (single file) runtime keys head derives direction
a const literal without a namespace, then its namespace property assigned, then registered namespaced + bare bare only silent MISS
same, via Object.assign onto the const namespaced + bare bare only silent MISS
a const literal WITH a namespace, then that property deleted, then registered bare only namespaced + bare silent PHANTOM
a const literal with a namespace, then skipFallback assigned true namespaced only namespaced + bare silent PHANTOM (bare)
as row 1 but passed as a top-level spread of the mutated const namespaced + bare bare only silent MISS

Two of these are the phantom direction — the one the card, the regenerate header's PHANTOMS paragraph and 防 AI 写元数据犯错 all name as the worse one — and they arrive through a const, i.e. exactly the binding the new declaredObjectBody header presents as what makes phantom-prevention "a refusal rather than a best effort". Two further silent cases have a runtime answer that depends on the caller: a module-level const shadowed by a function parameter of the same name at the call, and an import binding plus a function-local const of the same name with the call at module level. The "declared exactly once" count only sees const|let|var, so neither binding kind is counted.

Cost of closing the mutation shapes, measured rather than argued: a probe copy of the head derivation with a same-file guard in declaredObjectBody (refuse the name when the comment-stripped source assigns to its namespace/skipFallback in dot or bracket form, deletes either, or passes it as the target of Object.assign() turns all five rows above into unresolved-registration-meta, leaves read-only accesses READ, and on the live tree yields 649 keys / 0 findings / metaViaReference 5 — unchanged. It reds nothing today, which is the same zero-cost condition the seat relied on to rule "close the shapes" in R1.

5. Prose vs code. Verified true: the derivation OPTIONS bullet's description of the two read shapes; the regenerate header's "seven siblings" list (matches the 7-row table); every new test title (each behaviour reproduced in the probe); the changeset's list of refused shapes and its "five added, none removed or renamed"; the PR body's 233 resolved sites, 649 keys, 0 findings, the 5115 pin family names. Overstated: (a) the declaredObjectBody header — "The phantom is the worse direction and the reason this is a refusal rather than a best effort" — while a const mutated after declaration mints a phantom silently (row 3 above); (b) the OPTIONS bullet's claim that the counter counts the sites reached that way — the code counts only sites where a namespace was resolved and reached by reference, so a referenced options object without a namespace is reached that way and not counted (probe: identifier without namespace → counter 0). Not code, but the PR body says 242 call sites; the instrument's callSites counter says 238 and I could not reproduce 242 from it.

6. Census and live instrument. The census exists and reproduces from the instrument (5 by-reference resolved sites, all pageMeta; the 6th is the open PluginScopeImpl site). The live instrument is counters.metaViaReference, printed in the gate summary and asserted greater than zero by a pin, so the population cannot go quiet unnoticed; the regenerate script throws on any finding, so the new reason is loud in both consumers.

7. Pins are real. Ablation A: every new pin is red on the merge-base derivation (18 red, 66 green as control). Ablation B: exactly the ten R1 pins are red on the intermediate head, so the "SILENT-BARE on the previous head" claim in the table comment is true. Both swaps and the restore were verified by blob hash.

8. Hard fence. 0 diff lines on page.tsx and Registry.ts against a 44-line control; the six changed files are the ones listed; no registration added, removed or renamed anywhere. objectui#9263 is untouched.

Standing rules: this widening restores parity with what the runtime already stores (the five keys are in the runtime roster) — it invents no capability, so 创业阶段不扩散 is not engaged; the direction of the remaining defect (a phantom through a const) is the one 防 AI 写元数据犯错 forbids, which is why it gates.

Blocking items

  1. Close the mutation shapes on the identifier/spread route (scripts/check-doc-component-types.mjs, declaredObjectBody or the point a name is followed). Refuse the name — report unresolved-registration-meta — when the comment-stripped file assigns to its namespace or skipFallback property (dot or bracket form, including the logical-assignment operators), deletes either, or passes it as the target of Object.assign(. Read-only member access must stay READ. Add one fixture pin per measured row above (two misses, two phantoms, one spread-of-mutated), each red on b902cc1 and green after, using the ablation discipline the PR already uses. Re-measure the live tree: must remain 649 keys / 0 findings / metaViaReference 5 and regenerate --check 0 (the prototype shows it does).
  2. Make the declaredObjectBody header true to the guard that ships. After item 1 it may say the name is followed only when it is declared once, with const, and its namespace/skipFallback are never written after declaration in the file. It must also state, per AGENTS 完善设计器的每一个细节 #9, what is not guarded: bindings other than const|let|var (function parameters, import bindings) are not counted toward "declared exactly once", so a same-named parameter or import at the call site is read against the module-level literal. Counting import bindings as declarations is one regex and would close the import case; do it if cheap, otherwise state it. The changeset's "declared exactly once with const in the same file" and the PR body should carry the same qualification.

Non-blocking observations (not gating)

  • The by-reference counter counts only namespaced by-reference sites; either count every by-reference site or reword the OPTIONS bullet to say "the namespaced sites reached that way". The live-tree pin is unaffected either way.
  • scripts/__tests__/check-doc-component-types.test.ts line ~289: "Each of the four below fails on the derivation as it stood before objectui#9641" — the four R0 pins are all still red on base, but the 7-row refused-options table now sits between the third and the fourth, so a reader counts three. Say "the four READ pins in this block" or move the fourth up.
  • The 5115 test comment says the reports options it cannot read family covers "everything it refuses"; the phantom and shadowing pins are titled refuses …. Trivial.
  • PR body: 242 call sites vs the instrument's 238; the body also writes the census figures down, which AGENTS 完善设计器的每一个细节 #9 discourages even in a PR body — pointing at the counters would be enough.
  • A registration whose options argument is literally undefined or null matches the identifier pattern and is refused as "not declared in this file" — a false red on a correct (bare-only) registration. No live site uses it; a keyword check would be a one-liner.
  • A regex literal containing a stray quote inside the options literal refuses loudly rather than silently — acceptable, mentioned only because topLevelParts does not track regex literals and a reader might expect it to.
  • Everything else in the delivery is sound and was measured as such: placement, regeneration, the five keys, the fence, the census, the non-vacuity pin, and both ablations. Nothing already delivered needs to be discarded; item 1 is an addition of a guard plus pins, and item 2 is prose.

⚠️ Departures from verbatim, disclosed. The reviewer's fixture column spelled its five cases as literal JavaScript and its fenced-path and namespaced-key placeholders in angle brackets; those are rendered as descriptions of the same fixtures, because GitHub's sanitiser can eat tag-shaped fragments. ⛔ No finding, count, direction or ranking was altered — the five rows, their runtime-vs-derived columns and their MISS/PHANTOM labels are the reviewer's own.

— Record posted by PM domain:spec @ objectui · session session_01UanLVj6xvbS6puBCewLr8L · 2026-09-17T12:36Z


Generated by Claude Code

A `const` cannot be reassigned, but its CONTENTS can. Following a name is
a premise -- that the literal at the declaration is the object the call
passes -- and the previous round enforced that premise against rebinding
while leaving mutation unread. Measured, all deriving with no finding:
assigning a `namespace` the declaration lacks, the same through
`Object.assign`, DELETING a declared `namespace`, assigning `skipFallback`
after the fact, and any of those reached through a top-level spread. Two
of the five are the PHANTOM direction -- a key the runtime never stores,
blessed by the check -- and they arrive through the very binding the
header presented as what makes phantom-prevention a refusal.

`declaredObjectBody` now refuses a name whose comment-stripped file
assigns to its `namespace` or `skipFallback` (dot or bracket, including
the logical and compound operators), deletes either, or passes it as the
target of an `Object.assign`. Read-only member access still READS, and a
write to any other property still READS: the guard watches the two
properties that move which keys a registration publishes, nothing else.

Import bindings now count toward "declared exactly once", closing the
case where a file imported a name and declared another locally. Function
parameters still do not count; that gap is stated in the header and
PINNED as a known reading, so closing it later fails a test instead of
passing unnoticed.

Two fixes to claims no instrument enforced: the by-reference counter
incremented only when a namespace was also resolved, so a referenced
options object without one was invisible to the counter that reports the
route is in use; and the header pointed at that counter as if it
re-derived the parameter gap, which it cannot. An options argument
spelled `undefined` or `null` is now read as ABSENT options rather than
refused as an undeclared name.

The live tree is unmoved: 649 keys, 0 findings, metaViaReference 5,
`regenerate --check` 0 with the generated file untouched.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UanLVj6xvbS6puBCewLr8L
The first version of this pin asserted a plain imported name is refused,
which was already true before imports were counted -- no `const|let|var`
declared the name at all, so it failed for a different reason and the pin
was green on both sides. The case counting imports closes is an import at
module level plus a function-local declaration of the same name: the only
declaration the count could see is the one the call cannot reach. Split
into a control and a mechanism pin that is red without the change.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UanLVj6xvbS6puBCewLr8L
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Eager closure (gzip, 329 chunks) 3048.9 KB 3104.5 KB
Main entry chunk (gzip) 145.7 KB 350 KB
Entry file index-DbLzpmc-.js
Status PASS

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

Package Size Gzipped
app-shell (consoleActionDispatch.js) 0.20KB 0.19KB
app-shell (index.js) 16.69KB 6.21KB
app-shell (runtime-config.js) 20.68KB 7.36KB
app-shell (types.js) 0.01KB 0.04KB
app-shell (urlParams.js) 10.06KB 3.86KB
auth (ActiveOrganizationStorage.js) 25.05KB 9.16KB
auth (AuthContext.js) 0.31KB 0.24KB
auth (AuthGuard.js) 2.07KB 1.00KB
auth (AuthProvider.js) 40.18KB 10.59KB
auth (AuthShell.js) 3.49KB 1.40KB
auth (ForgotPasswordForm.js) 12.21KB 3.45KB
auth (LoginForm.js) 18.15KB 5.39KB
auth (PreviewBanner.js) 0.90KB 0.50KB
auth (RegisterForm.js) 6.65KB 2.22KB
auth (SocialSignInButtons.js) 9.61KB 3.89KB
auth (UserMenu.js) 3.41KB 1.23KB
auth (auth-gate-events.js) 1.29KB 0.66KB
auth (authStyles.js) 5.04KB 1.72KB
auth (createAuthClient.js) 40.21KB 10.80KB
auth (createAuthenticatedFetch.js) 8.46KB 3.43KB
auth (index.js) 3.19KB 1.44KB
auth (invitation-status.js) 1.22KB 0.70KB
auth (org-roles.js) 6.66KB 2.78KB
auth (phone-identifier.js) 1.11KB 0.66KB
auth (types.js) 0.59KB 0.35KB
auth (useAuth.js) 5.30KB 1.02KB
auth (useWorkspaceAdminStatus.js) 11.08KB 4.58KB
collaboration (CommentThread.js) 26.08KB 7.56KB
collaboration (LiveCursors.js) 3.17KB 1.27KB
collaboration (PresenceAvatars.js) 6.49KB 2.64KB
collaboration (PresenceProvider.js) 2.79KB 1.13KB
collaboration (index.js) 1.68KB 0.73KB
collaboration (useCollaborationTranslation.js) 6.05KB 2.52KB
collaboration (useCommentSearch.js) 1.98KB 0.88KB
collaboration (useConflictResolution.js) 7.75KB 1.86KB
collaboration (useMentionNotifications.js) 1.81KB 0.68KB
collaboration (usePresence.js) 6.33KB 1.84KB
collaboration (useRealtimeSubscription.js) 7.91KB 2.01KB
components (index.js) 545.84KB 130.66KB
core (index.js) 8.94KB 3.59KB
create-plugin (index.js) 27.94KB 9.51KB
data-objectstack (index.js) 215.98KB 59.97KB
fields (index.js) 249.27KB 62.92KB
i18n (LocalizationContext.js) 1.76KB 0.96KB
i18n (builtinAggregateLabels.js) 0.86KB 0.49KB
i18n (currency.js) 1.22KB 0.64KB
i18n (fallbackInterpolation.js) 6.25KB 2.77KB
i18n (i18n.js) 8.87KB 3.64KB
i18n (index.js) 5.22KB 2.26KB
i18n (pickLocalized.js) 9.86KB 3.95KB
i18n (provider.js) 32.15KB 10.49KB
i18n (useDisplayLocale.js) 2.85KB 1.45KB
i18n (useObjectLabel.js) 34.34KB 9.17KB
i18n (useSafeTranslation.js) 5.60KB 2.33KB
layout (index.js) 38.83KB 10.95KB
mobile (MobileProvider.js) 0.92KB 0.49KB
mobile (ResponsiveContainer.js) 0.94KB 0.38KB
mobile (breakpoints.js) 1.51KB 0.70KB
mobile (createOfflineDataSource.js) 5.61KB 1.75KB
mobile (index.js) 1.99KB 0.87KB
mobile (offlineQueue.js) 3.91KB 1.35KB
mobile (pwa.js) 0.97KB 0.49KB
mobile (serviceWorker.js) 1.48KB 0.62KB
mobile (serviceWorkerSource.js) 3.41KB 1.48KB
mobile (useBreakpoint.js) 1.54KB 0.65KB
mobile (useGesture.js) 6.96KB 1.98KB
mobile (useOfflineSync.js) 1.99KB 0.72KB
mobile (usePullToRefresh.js) 2.53KB 0.85KB
mobile (useResponsive.js) 0.72KB 0.42KB
mobile (useSpecGesture.js) 4.39KB 1.66KB
mobile (useTouchTarget.js) 1.01KB 0.54KB
permissions (MePermissionsProvider.js) 13.52KB 4.88KB
permissions (PermissionContext.js) 0.31KB 0.25KB
permissions (PermissionGuard.js) 0.89KB 0.45KB
permissions (PermissionProvider.js) 6.24KB 2.16KB
permissions (discardProofCache.js) 1.04KB 0.55KB
permissions (evaluator.js) 8.39KB 3.10KB
permissions (index.js) 0.93KB 0.41KB
permissions (store.js) 0.91KB 0.42KB
permissions (useFieldPermissions.js) 1.28KB 0.53KB
permissions (usePermissions.js) 4.83KB 2.27KB
plugin-ai (index.js) 14.81KB 3.63KB
plugin-calendar (index.js) 49.92KB 14.22KB
plugin-charts (index.js) 71.49KB 19.99KB
plugin-chatbot (index.js) 195.34KB 46.51KB
plugin-dashboard (index.js) 131.44KB 34.65KB
plugin-designer (index.js) 215.94KB 44.33KB
plugin-detail (index.js) 253.29KB 65.88KB
plugin-editor (index.js) 2.23KB 1.05KB
plugin-form (index.js) 136.71KB 34.16KB
plugin-gantt (index.js) 167.62KB 41.26KB
plugin-grid (index.js) 212.64KB 57.91KB
plugin-kanban (index.js) 46.41KB 14.49KB
plugin-list (index.js) 112.73KB 27.69KB
plugin-map (index.js) 21.48KB 6.99KB
plugin-markdown (index.js) 13.88KB 4.80KB
plugin-report (index.js) 43.41KB 11.93KB
plugin-timeline (index.js) 30.07KB 8.74KB
plugin-tree (index.js) 10.58KB 3.72KB
plugin-view (index.js) 85.04KB 21.01KB
providers (DataSourceProvider.js) 0.75KB 0.39KB
providers (MetadataProvider.js) 1.37KB 0.59KB
providers (ThemeProvider.js) 1.90KB 0.85KB
providers (UploadProvider.js) 11.66KB 3.50KB
providers (index.js) 0.45KB 0.23KB
providers (types.js) 0.01KB 0.04KB
react-runtime (index.js) 5.62KB 2.34KB
react (LazyPluginLoader.js) 4.47KB 1.63KB
react (SchemaRenderer.js) 104.82KB 34.67KB
react (data-invalidation.js) 5.05KB 2.08KB
react (index.js) 4.63KB 2.18KB
react (schema-input.js) 4.25KB 2.04KB
react (spec-input.js) 0.20KB 0.18KB
sdui-parser (codegen.js) 6.58KB 2.74KB
sdui-parser (dashboard-widget-options.js) 3.08KB 1.30KB
sdui-parser (index.js) 5.66KB 2.50KB
sdui-parser (input-type.js) 2.84KB 1.40KB
sdui-parser (kanban-quick-add.js) 3.89KB 1.87KB
sdui-parser (parse.js) 25.28KB 7.80KB
sdui-parser (provenance.js) 3.66KB 1.82KB
sdui-parser (types.js) 0.28KB 0.23KB
sdui-parser (validate.js) 14.82KB 4.99KB
types (ai.js) 4.11KB 2.06KB
types (api-types.js) 0.20KB 0.18KB
types (app.js) 2.87KB 1.00KB
types (base.js) 0.20KB 0.18KB
types (blocks.js) 0.20KB 0.18KB
types (complex.js) 2.93KB 1.49KB
types (crud.js) 0.20KB 0.18KB
types (dashboard-filter-alias.js) 6.23KB 2.74KB
types (data-display.js) 3.75KB 1.85KB
types (data-protocol.js) 0.20KB 0.19KB
types (data.js) 0.20KB 0.18KB
types (designer.js) 1.85KB 0.85KB
types (disclosure.js) 0.20KB 0.18KB
types (error-code.js) 1.54KB 0.88KB
types (expression.js) 0.20KB 0.18KB
types (feedback.js) 0.20KB 0.18KB
types (field-types.js) 0.20KB 0.18KB
types (form.js) 0.20KB 0.18KB
types (http-inflight.js) 8.87KB 3.73KB
types (http-retry.js) 4.32KB 2.02KB
types (icon-key-migration.js) 4.26KB 1.63KB
types (index.js) 4.74KB 2.25KB
types (layout.js) 0.20KB 0.18KB
types (managed-by.js) 0.19KB 0.18KB
types (mobile.js) 4.73KB 2.28KB
types (navigation.js) 0.20KB 0.18KB
types (objectql.js) 0.20KB 0.18KB
types (overlay.js) 0.20KB 0.18KB
types (permissions.js) 0.20KB 0.18KB
types (plugin-scope.js) 0.20KB 0.18KB
types (record-components.js) 0.20KB 0.19KB
types (record-semantics.js) 1.28KB 0.67KB
types (registry.js) 0.20KB 0.18KB
types (reports.js) 0.20KB 0.18KB
types (select-option.js) 0.20KB 0.19KB
types (spec-report.js) 5.05KB 1.93KB
types (spec-ui-namespace.js) 0.20KB 0.19KB
types (strict-authoring-face.js) 14.04KB 5.36KB
types (system-fields.js) 3.33KB 1.54KB
types (theme.js) 6.28KB 2.87KB
types (ui-action.js) 8.11KB 3.32KB
types (views.js) 0.20KB 0.18KB
types (widget.js) 0.20KB 0.18KB

Size Limits

  • ✅ Core packages should be < 50KB gzipped
  • ✅ Component packages should be < 100KB gzipped
  • ⚠️ Plugin packages should be < 150KB gzipped

Copy link
Copy Markdown
Collaborator Author

At-tier contract review record — PR #9686 @ head b6828d41 / card objectui#9641 — FAIL (round 3)

Adopted VERBATIM by the domain:spec @ objectui seat (session_01UanLVj6xvbS6puBCewLr8L), per 「逐字采纳或整体作废」. ⛔ Nothing softened, dropped or re-ranked.

Tier, from the reviewer's OWN transcript: claude-fable-5-1, 77 of 77. Firing control: implementer transcripts read claude-opus-5.

Isolation: card + PR + the two script headers only. ⭐ The reviewer classified the card's earlier os-dev-report comments and REWORK verdicts by header and did not read them — it says so itself, which is stronger than being merely fenced.

⚠️ This is the THIRD FAIL on this PR. This seat stated publicly, on this card and to the implementer, that a third FAIL escalates to the maintainer rather than dispatching a third patch round. ⇒ ⛔ No round 3 is being dispatched. The escalation is posted on objectui#9641.


VERDICT: FAIL

Subject: objectstack-ai/objectui PR #9686 at head b6828d4 (merge base with origin/main = 72f55c9, the same base the PR cites), judged whole-diff against card objectui#9641. All execution happened in a private clone under the review's own scratchpad (clone at ./repo, merge-base worktree at ./repo-mb); the shared checkouts were only read. Card comments 2/4/6 (os-dev-report) and 3/5 (earlier REWORK verdicts) were classified by header and not read, per the fence.

What I measured

reading value source / control
Whole diff, 72f55c9..b6828d4 6 files, +873/-8: the changeset, packages/cli/src/utils/known-schema-types.ts (+5/-0), scripts/check-doc-component-types.mjs (+357/-5), scripts/regenerate-known-schema-types.mjs (+30/-3), two test files git diff --stat
Fence: diff lines on page.tsx and Registry.ts 0 lit control: same command on known-schema-types.ts prints "1 file changed, 5 insertions"
Live derivation at head / at merge base 649 keys, 0 findings, callSites 238, resolved 233, open 2, indirect 132, metaViaReference 5 / 644 keys, 0 findings, same counters, no metaViaReference deriveRegistryKeys(root) run directly on both trees
The five namespaced page keys 0 each at merge base, 1 each at head same run; lit controls on the same instrument: page:header 2 (two sites), element:text 1, action:button 1 on both refs; absent token 0 on both
Derived set difference head minus merge base added exactly the five; removed [] deriveKnownSchemaTypes from both trees, set difference
regenerate --check at head exit 0, "matches the registry derivation" controls: dropping one key line by hand → exit 1; installing the merge-base blob under head's derivation → exit 1; restored → exit 0
Generated-file blob across the four PR commits 45bf0e99 at b1fa3b7, b902cc1, 4e11585, b6828d4 (453628e4 at merge base) written once at R0, untouched since
Derivation blob per commit 47a9397b (base) → 517d1135 (R0) → 59b1a920 (R1) → 2fc8f36d (R2 and head) the PR's ablation hashes are these blobs
Two pin files at head 2 files, 98 passed, 0 failed vitest under a minimal config
Ablation with HEAD's tests, blob 47a9397b 31 failed / 67 passed blob swapped in place, hash verified, restored, git status clean
Ablation, blob 517d1135 21 failed / 77 passed same
Ablation, blob 59b1a920 10 failed / 88 passed (matches the PR's R2 row exactly) same
Pins green under the 59b1a920 ablation that were added in the last two commits "keeps READING a name that is only read", "refuses a plain imported name", "KNOWN GAP function parameter", and the 5115 "keeps the bare fallbacks" all three check-doc ones are labelled as controls / known-reading in the test itself
type-check:scripts, check-doc-component-types.mjs, lint:root, check-changeset-presence, changeset:check 0 / 0 (summary prints "5 via referenced options") / 0 (32 pre-existing warnings) / 0 / 0 at head in the clone
packages/cli/src/__tests__ under the real root config 17 files, 266 passed full install, vitest
Independent census of third-argument shapes (own scanner) 238 call sites; 4 receiver matches inside string literals; 229 key-value-only literals; 1 absent; page.tsx: 1 identifier + 4 spreads; PluginScopeImpl.ts:110 identifier (open site); WidgetRegistry.ts:171 literal with a non-key-value entry (open site); fields/src/index.tsx:3917 literal with a CONDITIONAL spread (indirect site — see non-blocking c) agrees with the instrument on 238 and on the "4 in strings" correction
Runtime roster the five namespaced page keys each present once in the plain-types list of packages/app-shell/src/__tests__/widget-dom-leak-sweep.test.tsx, whose section 3 pins that list against the registry's own configs; absent control 0 grep
Premise probes: 46 fixtures, head derivation vs a faithful mirror of the registry's key semantics (Registry.ts:457-510, 556-563) 21 read correctly, 12 refused loudly, 22 silently wrong (of which 1 is the declared parameter gap) own probe + fixtures; replayed against the three older blobs

What I checked and found

  1. Where the repair landed. Correct and declared. At the merge base the generator's header already said the universe comes from deriveRegistryKeys "deliberately the SAME derivation … not a second scanner with its own bugs" and "any registration form it cannot resolve fails there rather than silently shrinking the universe here"; the code agrees — deriveKnownSchemaTypes is a call to deriveRegistryKeys plus a sort, no scanner of its own. Landing the resolver in scripts/check-doc-component-types.mjs is the only placement consistent with that rule, and it also repairs the other two consumers (check-prompt-component-keys.mjs, check-registry-bare-name-collisions.mjs import the same function; no sibling script carries its own namespace regex).

  2. Regenerated, not hand-edited. Proven: the render of the derivation is byte-identical to the committed file (--check exit 0) and the instrument is shown live in both directions (hand-edit → 1, stale blob → 1). The blob is the same across all four PR commits; the head derivation minus the merge-base derivation is exactly the +5 lines.

  3. Acceptance exactly right. Each new key maps to a real registration: page.tsx:711-715 register the five kinds with a 'ui' namespace, and the registry stores the namespaced key plus the bare key (no skipFallback). Both directions: nothing removed (set difference empty; the five bare keys still present), and the five namespaced keys appear in the runtime-parity roster, so no key was blessed that the registry does not store. The 5115 suite's subset pin passes at head.

  4. Is the defect class fully drawn? No. The allowlist is structural about the argument and that half is sound (every non-allowlisted argument shape tried is refused: cast, member, call, parenthesised, shorthand, computed key, getter, template-literal namespace; a namespace nested inside a sub-object no longer leaks as it did under the old whole-span regex). The identifier route's premise — "the literal at the declaration is the object the call passes" — is enforced only for spelled rebinding and spelled direct writes. Probed exhaustively (runtime → derived):

    • Refused loudly (correct): let/var reassignment; a dot write, a quoted-bracket write, a logical-assignment write; a delete of the property; an Object.assign onto the name; for-of / block / var shadow (2 bindings); import + local; a string containing a write (loud false red, acceptable); a write positioned after the call (loud false red).
    • Reads correctly: the page.tsx shape; a two-level spread chain; comments before/inside the literal (the source is comment-stripped, so the PR's claim is true); a frozen object; an absent options argument; the lazy-registration form.
    • Silently wrong, UNDECLARED, mutation route: an alias write (assign the name to another const, then write through it) → MISS; an alias delete → PHANTOM; a callee write → MISS; a callee skipFallback write → PHANTOM in the bare half; a destructuring-assignment target → MISS; Object.defineProperty → MISS; Reflect.set → MISS; Reflect.deletePropertyPHANTOM; Object.setPrototypeOf → MISS; a computed-bracket write → MISS; an alias write reached through a spread → MISS.
    • Silently wrong, UNDECLARED, name-resolution route: a destructured const inside the calling function shadowing a module-level const → PHANTOM; likewise an array-destructured binding, a later declarator in the same statement, and a catch binding. Arrow and destructured parameters are also PHANTOMs (variants of the declared parameter gap).
    • Silently wrong in the literal route (the new readMetaBody): a literal that spreads the same base twice around an intervening key → PHANTOM + MISS (the visited-set is global, so the second spread is skipped instead of re-applied); and an explicit skipFallback: false arriving through a spread → MISS of the bare key (a truthiness test drops an explicit false).
      Nothing in the live tree hits any of these today (0 findings, 649 keys), so this is about the property the code claims, not about today's list.
  5. Prose vs code. Most of it is accurate and was verified: the comment-stripped claim; the by-reference counter counts every read by-reference site and the summary prints it; the HINTS entry is wired; the "seven siblings" list; the changeset's account of the live shape; the last commit's re-titling of the plain-import pin as a control is confirmed (green under both older blobs) and its import-plus-local pin is confirmed red at the R1 blob. The overstatements, all in prose added in the R2/R3 commits: (a) the changeset's "never writes to afterwards" and "Every other shape … is now reported", the OPTIONS bullet's "never written to afterwards", and the mutation function's header — the instrument sees only writes spelled with that exact name and a literal property, a delete of that property, or an Object.assign onto the name. (b) the declaredObjectBody header's "The declaration count sees const, let, var and import bindings" plus "the one binding kind it cannot see" / the changeset's "One binding kind is not covered" / the PR body — the count sees a name only when it immediately follows the keyword; destructuring patterns, later declarators and catch bindings are invisible and undeclared. (c) readMetaBody's "Entries are read in source order and a later one wins, which is what the runtime does" — false for the two literal-route cases above. Converse checks: the stated parameter gap is genuinely a gap (measured PHANTOM) and the stated guards genuinely guard what they name.

  6. Pins. Real and non-vacuous for the mechanism they claim: every new "reads" and "refuses" pin is red on the blob before its change (per-blob replay and the three vitest ablations agree). Pins green on both sides are exactly the ones the test file labels as such (two controls, one known-reading) plus the 5115 regression guard. ⚠️ The PR's ablation table rows R0 and R1 are per-round totals of 74 and 84 tests, not head's 98; re-run at head they are 31/67 and 21/77; only the R2 row is a head figure.

  7. Census and live instrument. 238 call sites, 4 receiver matches inside strings, and the five page.tsx sites as the only read-by-reference population all reproduce on an independent scanner. The by-reference counter is printed every run and pinned above zero, so the population cannot go quiet unnoticed; --check throws on any finding, so a refused shape cannot shrink the universe silently. One omission in the census prose: see non-blocking (c).

  8. Hard constraint. page.tsx and Registry.ts are at 0 diff lines (lit control shown); the diff touches nothing under packages/components or packages/core; the derived key set moved only by the five namespaced halves, which is what an unchanged registration surface predicts. No registration added, removed or renamed.

Blocking items

  1. The mutation guard's scope is overstated, and undeclared silent reads exist in both directions through it (item 4, mutation route). Required: in the mutation function's header, the "WHAT THIS DOES NOT GUARD" paragraph, the OPTIONS bullet, the changeset and the PR body, replace "never written to afterwards" / "Every other shape … is now reported" with the property the code has: a write is seen only when spelled with this exact name and a literal namespace/skipFallback property (dot or quoted bracket), as a delete of that property, or as an Object.assign onto the name; a write through an alias, a callee, Object.defineProperty/Reflect, a computed key, a destructuring target, or another module is invisible. Pin at least the alias write (MISS) and the callee skipFallback write (PHANTOM) as KNOWN GAP readings, the way the parameter gap is pinned, so closing them later fails a test. ⛔ Closing them is not required.

  2. The binding count is overstated, and undeclared silent PHANTOM reads exist through it (item 4, name-resolution route). Required, either: widen the count so a name inside a destructuring pattern, a later declarator of the same statement, or a catch clause is a binding (regex-feasible; add one red-on-current-blob pin per form), or restate precisely — "a name counts only when it immediately follows const/let/var or sits in an import clause" — and pin a destructured binding as a KNOWN GAP reading beside the parameter one. Drop "one binding kind" from the changeset, the header and the PR body either way.

  3. readMetaBody's "a later one wins, which is what the runtime does" is not enforced for a doubly-spread base (PHANTOM) or an explicit skipFallback: false arriving through a spread (MISS of the bare key). Required: make the visited-set a recursion stack (cycle guard only) so a repeated spread re-applies, and carry an explicit false from a spread (track set/unset rather than truthiness); one pin each, red on the current blob. The shapes are contrived and the fix is a few lines, but the property is asserted in a header of the new code.

Non-blocking observations (not gating)

a. PR body ablation table: rows R0/R1 are historical per-round totals; say so or replace with the head figures. "Every ablation runs from the committed state" is true only of R2.
b. The mutation table's comment says "All five rows below" while the table has seven.
c. packages/fields/src/index.tsx:3917 registers with a conditional spread the allowlist would refuse — but the site is INDIRECT (its key comes from a map), so its options are never read and its namespace comes from the hand-kept indirect table. Correct today and outside this card's class, but the changeset's "every … call in the tree was classified by how its options argument arrives" glosses over 132 of 649 keys taking their namespace from a table rather than the call; a namespace edit at that call would be a silent phantom of the same family. Worth its own card.
d. The 5115 "keeps the bare fallbacks" pin is green on both sides — a regression guard; label it as one like the two check-doc controls.
e. The guard is position-agnostic: a write placed after the call is refused although the runtime read the namespace at call time. Loud, so acceptable; the header's "after it is declared" would be truer as "anywhere in the file".
f. page:header reads 2 sites, not the 1 the card body states; harmless.
g. The PR is behind main.
h. Reproduction artefacts are under the review's own scratchpad directory.


⚠️ Departures from verbatim, disclosed. The reviewer spelled its fixture shapes as literal JavaScript and used angle-bracket placeholders for a namespaced key and a property name; those are rendered as descriptions, because GitHub's sanitiser can delete tag-shaped fragments. ⛔ No finding, count, direction, verdict or ranking was altered — the 21/12/22 split, the MISS/PHANTOM labels and the three blocking items are the reviewer's own.

— Record posted by PM domain:spec @ objectui · session session_01UanLVj6xvbS6puBCewLr8L · 2026-09-17T13:16Z


Generated by Claude Code

Three at-tier contract reviews failed this branch for the same shape: a
header or changeset asserting a property the regex instrument does not
enforce. The ruling on the card (batch #150 item 2, letter B) is that a
regex approximation of JavaScript scope and mutation semantics has no
finishing line -- round 3's silent list was LONGER than round 2's, and
every corner closed so far has zero live consumers -- so the reachable
end state is an accurate declaration of what the instrument cannot see.
This commit is that declaration, plus the two bugs the previous round
introduced in its own new code.

PROSE MADE TRUE. The mutation guard's header now states the three
spellings it matches -- an assignment or `delete` whose target is the
name spelled exactly followed by a literal `namespace`/`skipFallback`,
or an `Object.assign` onto that name -- instead of "never written to
afterwards". `declaredObjectBody`'s header states the count's own rule
(a name counts only where it immediately follows `const`/`let`/`var` or
sits in an import clause) instead of listing binding kinds it sees.
`readMetaBody`'s source-order claim is bounded to the two properties and
to bodies where every entry was recognised. The OPTIONS bullet and the
allowlist header now say ARGUMENT shape, because "every other shape is
reported" was true of arguments and false of names. The regeneration
script's "a form it cannot resolve fails there" is narrowed to a form it
does not RECOGNISE, and the silent half is written down beside it.

KNOWN GAPS PINNED, one row per silent class round 3 measured: alias,
callee, computed-key, destructuring-target, `Reflect.set`,
`Reflect.deleteProperty`, `Object.defineProperty` and
`Object.setPrototypeOf` writes; destructured, array-destructured,
later-declarator and `catch` bindings. Each asserts TODAY'S reading --
the keys AND the absence of a finding -- so closing one later reds its
row, the treatment the function-parameter gap already had. A firing
control beside them keeps a broken mutation guard from reading as a
block of green gap pins.

TWO FIXES, both in code this branch itself added and both a few lines
with their own pin. `seen` is a recursion STACK rather than a visited
set, so a literal spreading the same base twice re-applies it instead of
skipping it (that was a PHANTOM and a MISS at once); and `skipFallback`
set-ness is tracked rather than inferred from truthiness, so an explicit
`false` arriving by spread overrides an earlier explicit `true` instead
of being dropped (that was a MISS of the bare key). A control pins that
self-referential and mutually-spreading declarations still terminate.

No new guard logic: no shape is closed. The live tree is unmoved -- 649
keys, 0 findings, metaViaReference 5, `regenerate --check` exit 0 with
the generated file at the same blob it has carried since R0.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UanLVj6xvbS6puBCewLr8L
None of these gates anything; all three are the same class as the blocking
items, which is why they are fixed rather than carried.

The mutation table's comment said "All five rows below" while the table
has seven. The objectui#5115 "keeps the bare fallbacks" pin is green on
both sides of this change, so it is now labelled a regression guard, the
way the two check-doc controls already are -- an unlabelled pin green on
both legs reads as a mechanism pin that stopped working.

The two mutation findings told the author the property was written
"after it is declared". The guard is POSITION-AGNOSTIC: it matches the
write anywhere in the file, including before the declaration and after
the `register()` call. The message now says "somewhere in this file",
which is what was measured. The header already states the same thing and
says why the loud direction is left as it is.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UanLVj6xvbS6puBCewLr8L
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Eager closure (gzip, 329 chunks) 3049.0 KB 3104.5 KB
Main entry chunk (gzip) 145.7 KB 350 KB
Entry file index-CqfP7f5i.js
Status PASS

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

Package Size Gzipped
app-shell (consoleActionDispatch.js) 0.20KB 0.19KB
app-shell (index.js) 16.69KB 6.21KB
app-shell (runtime-config.js) 20.68KB 7.36KB
app-shell (types.js) 0.01KB 0.04KB
app-shell (urlParams.js) 10.06KB 3.86KB
auth (ActiveOrganizationStorage.js) 25.05KB 9.16KB
auth (AuthContext.js) 0.31KB 0.24KB
auth (AuthGuard.js) 2.07KB 1.00KB
auth (AuthProvider.js) 40.18KB 10.59KB
auth (AuthShell.js) 3.49KB 1.40KB
auth (ForgotPasswordForm.js) 12.21KB 3.45KB
auth (LoginForm.js) 18.15KB 5.39KB
auth (PreviewBanner.js) 0.90KB 0.50KB
auth (RegisterForm.js) 6.65KB 2.22KB
auth (SocialSignInButtons.js) 9.61KB 3.89KB
auth (UserMenu.js) 3.41KB 1.23KB
auth (auth-gate-events.js) 1.29KB 0.66KB
auth (authStyles.js) 5.04KB 1.72KB
auth (createAuthClient.js) 40.21KB 10.80KB
auth (createAuthenticatedFetch.js) 8.46KB 3.43KB
auth (index.js) 3.19KB 1.44KB
auth (invitation-status.js) 1.22KB 0.70KB
auth (org-roles.js) 6.66KB 2.78KB
auth (phone-identifier.js) 1.11KB 0.66KB
auth (types.js) 0.59KB 0.35KB
auth (useAuth.js) 5.30KB 1.02KB
auth (useWorkspaceAdminStatus.js) 11.08KB 4.58KB
collaboration (CommentThread.js) 26.08KB 7.56KB
collaboration (LiveCursors.js) 3.17KB 1.27KB
collaboration (PresenceAvatars.js) 6.49KB 2.64KB
collaboration (PresenceProvider.js) 2.79KB 1.13KB
collaboration (index.js) 1.68KB 0.73KB
collaboration (useCollaborationTranslation.js) 6.05KB 2.52KB
collaboration (useCommentSearch.js) 1.98KB 0.88KB
collaboration (useConflictResolution.js) 7.75KB 1.86KB
collaboration (useMentionNotifications.js) 1.81KB 0.68KB
collaboration (usePresence.js) 6.33KB 1.84KB
collaboration (useRealtimeSubscription.js) 7.91KB 2.01KB
components (index.js) 545.85KB 130.67KB
core (index.js) 8.94KB 3.59KB
create-plugin (index.js) 27.94KB 9.51KB
data-objectstack (index.js) 215.98KB 59.97KB
fields (index.js) 249.27KB 62.92KB
i18n (LocalizationContext.js) 1.76KB 0.96KB
i18n (builtinAggregateLabels.js) 0.86KB 0.49KB
i18n (currency.js) 1.22KB 0.64KB
i18n (fallbackInterpolation.js) 6.25KB 2.77KB
i18n (i18n.js) 8.87KB 3.64KB
i18n (index.js) 5.22KB 2.26KB
i18n (pickLocalized.js) 9.86KB 3.95KB
i18n (provider.js) 32.15KB 10.49KB
i18n (useDisplayLocale.js) 2.85KB 1.45KB
i18n (useObjectLabel.js) 34.34KB 9.17KB
i18n (useSafeTranslation.js) 5.60KB 2.33KB
layout (index.js) 38.83KB 10.95KB
mobile (MobileProvider.js) 0.92KB 0.49KB
mobile (ResponsiveContainer.js) 0.94KB 0.38KB
mobile (breakpoints.js) 1.51KB 0.70KB
mobile (createOfflineDataSource.js) 5.61KB 1.75KB
mobile (index.js) 1.99KB 0.87KB
mobile (offlineQueue.js) 3.91KB 1.35KB
mobile (pwa.js) 0.97KB 0.49KB
mobile (serviceWorker.js) 1.48KB 0.62KB
mobile (serviceWorkerSource.js) 3.41KB 1.48KB
mobile (useBreakpoint.js) 1.54KB 0.65KB
mobile (useGesture.js) 6.96KB 1.98KB
mobile (useOfflineSync.js) 1.99KB 0.72KB
mobile (usePullToRefresh.js) 2.53KB 0.85KB
mobile (useResponsive.js) 0.72KB 0.42KB
mobile (useSpecGesture.js) 4.39KB 1.66KB
mobile (useTouchTarget.js) 1.01KB 0.54KB
permissions (MePermissionsProvider.js) 13.52KB 4.88KB
permissions (PermissionContext.js) 0.31KB 0.25KB
permissions (PermissionGuard.js) 0.89KB 0.45KB
permissions (PermissionProvider.js) 6.24KB 2.16KB
permissions (discardProofCache.js) 1.04KB 0.55KB
permissions (evaluator.js) 8.39KB 3.10KB
permissions (index.js) 0.93KB 0.41KB
permissions (store.js) 0.91KB 0.42KB
permissions (useFieldPermissions.js) 1.28KB 0.53KB
permissions (usePermissions.js) 4.83KB 2.27KB
plugin-ai (index.js) 14.81KB 3.63KB
plugin-calendar (index.js) 49.92KB 14.22KB
plugin-charts (index.js) 71.49KB 19.99KB
plugin-chatbot (index.js) 195.34KB 46.51KB
plugin-dashboard (index.js) 131.44KB 34.65KB
plugin-designer (index.js) 215.94KB 44.33KB
plugin-detail (index.js) 253.29KB 65.88KB
plugin-editor (index.js) 2.23KB 1.05KB
plugin-form (index.js) 136.71KB 34.16KB
plugin-gantt (index.js) 167.62KB 41.26KB
plugin-grid (index.js) 212.64KB 57.91KB
plugin-kanban (index.js) 46.41KB 14.49KB
plugin-list (index.js) 112.73KB 27.69KB
plugin-map (index.js) 21.48KB 6.99KB
plugin-markdown (index.js) 13.88KB 4.80KB
plugin-report (index.js) 43.41KB 11.93KB
plugin-timeline (index.js) 30.07KB 8.74KB
plugin-tree (index.js) 10.58KB 3.72KB
plugin-view (index.js) 85.04KB 21.01KB
providers (DataSourceProvider.js) 0.75KB 0.39KB
providers (MetadataProvider.js) 1.37KB 0.59KB
providers (ThemeProvider.js) 1.90KB 0.85KB
providers (UploadProvider.js) 11.66KB 3.50KB
providers (index.js) 0.45KB 0.23KB
providers (types.js) 0.01KB 0.04KB
react-runtime (index.js) 5.62KB 2.34KB
react (LazyPluginLoader.js) 4.47KB 1.63KB
react (SchemaRenderer.js) 104.82KB 34.67KB
react (data-invalidation.js) 5.05KB 2.08KB
react (index.js) 4.63KB 2.18KB
react (schema-input.js) 4.25KB 2.04KB
react (spec-input.js) 0.20KB 0.18KB
sdui-parser (codegen.js) 6.58KB 2.74KB
sdui-parser (dashboard-widget-options.js) 3.08KB 1.30KB
sdui-parser (index.js) 5.66KB 2.50KB
sdui-parser (input-type.js) 2.84KB 1.40KB
sdui-parser (kanban-quick-add.js) 3.89KB 1.87KB
sdui-parser (parse.js) 25.28KB 7.80KB
sdui-parser (provenance.js) 3.66KB 1.82KB
sdui-parser (types.js) 0.28KB 0.23KB
sdui-parser (validate.js) 14.82KB 4.99KB
types (ai.js) 4.11KB 2.06KB
types (api-types.js) 0.20KB 0.18KB
types (app.js) 2.87KB 1.00KB
types (base.js) 0.20KB 0.18KB
types (blocks.js) 0.20KB 0.18KB
types (complex.js) 2.93KB 1.49KB
types (crud.js) 0.20KB 0.18KB
types (dashboard-filter-alias.js) 6.23KB 2.74KB
types (data-display.js) 3.75KB 1.85KB
types (data-protocol.js) 0.20KB 0.19KB
types (data.js) 0.20KB 0.18KB
types (designer.js) 1.85KB 0.85KB
types (disclosure.js) 0.20KB 0.18KB
types (error-code.js) 1.54KB 0.88KB
types (expression.js) 0.20KB 0.18KB
types (feedback.js) 0.20KB 0.18KB
types (field-types.js) 0.20KB 0.18KB
types (form.js) 0.20KB 0.18KB
types (http-inflight.js) 8.87KB 3.73KB
types (http-retry.js) 4.32KB 2.02KB
types (icon-key-migration.js) 4.26KB 1.63KB
types (index.js) 4.74KB 2.25KB
types (layout.js) 0.20KB 0.18KB
types (managed-by.js) 0.19KB 0.18KB
types (mobile.js) 4.73KB 2.28KB
types (navigation.js) 0.20KB 0.18KB
types (objectql.js) 0.20KB 0.18KB
types (overlay.js) 0.20KB 0.18KB
types (permissions.js) 0.20KB 0.18KB
types (plugin-scope.js) 0.20KB 0.18KB
types (record-components.js) 0.20KB 0.19KB
types (record-semantics.js) 1.28KB 0.67KB
types (registry.js) 0.20KB 0.18KB
types (reports.js) 0.20KB 0.18KB
types (select-option.js) 0.20KB 0.19KB
types (spec-report.js) 5.05KB 1.93KB
types (spec-ui-namespace.js) 0.20KB 0.19KB
types (strict-authoring-face.js) 14.04KB 5.36KB
types (system-fields.js) 3.33KB 1.54KB
types (theme.js) 6.28KB 2.87KB
types (ui-action.js) 8.11KB 3.32KB
types (views.js) 0.20KB 0.18KB
types (widget.js) 0.20KB 0.18KB

Size Limits

  • ✅ Core packages should be < 50KB gzipped
  • ✅ Component packages should be < 100KB gzipped
  • ⚠️ Plugin packages should be < 150KB gzipped

1 similar comment
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Eager closure (gzip, 329 chunks) 3049.0 KB 3104.5 KB
Main entry chunk (gzip) 145.7 KB 350 KB
Entry file index-CqfP7f5i.js
Status PASS

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

Package Size Gzipped
app-shell (consoleActionDispatch.js) 0.20KB 0.19KB
app-shell (index.js) 16.69KB 6.21KB
app-shell (runtime-config.js) 20.68KB 7.36KB
app-shell (types.js) 0.01KB 0.04KB
app-shell (urlParams.js) 10.06KB 3.86KB
auth (ActiveOrganizationStorage.js) 25.05KB 9.16KB
auth (AuthContext.js) 0.31KB 0.24KB
auth (AuthGuard.js) 2.07KB 1.00KB
auth (AuthProvider.js) 40.18KB 10.59KB
auth (AuthShell.js) 3.49KB 1.40KB
auth (ForgotPasswordForm.js) 12.21KB 3.45KB
auth (LoginForm.js) 18.15KB 5.39KB
auth (PreviewBanner.js) 0.90KB 0.50KB
auth (RegisterForm.js) 6.65KB 2.22KB
auth (SocialSignInButtons.js) 9.61KB 3.89KB
auth (UserMenu.js) 3.41KB 1.23KB
auth (auth-gate-events.js) 1.29KB 0.66KB
auth (authStyles.js) 5.04KB 1.72KB
auth (createAuthClient.js) 40.21KB 10.80KB
auth (createAuthenticatedFetch.js) 8.46KB 3.43KB
auth (index.js) 3.19KB 1.44KB
auth (invitation-status.js) 1.22KB 0.70KB
auth (org-roles.js) 6.66KB 2.78KB
auth (phone-identifier.js) 1.11KB 0.66KB
auth (types.js) 0.59KB 0.35KB
auth (useAuth.js) 5.30KB 1.02KB
auth (useWorkspaceAdminStatus.js) 11.08KB 4.58KB
collaboration (CommentThread.js) 26.08KB 7.56KB
collaboration (LiveCursors.js) 3.17KB 1.27KB
collaboration (PresenceAvatars.js) 6.49KB 2.64KB
collaboration (PresenceProvider.js) 2.79KB 1.13KB
collaboration (index.js) 1.68KB 0.73KB
collaboration (useCollaborationTranslation.js) 6.05KB 2.52KB
collaboration (useCommentSearch.js) 1.98KB 0.88KB
collaboration (useConflictResolution.js) 7.75KB 1.86KB
collaboration (useMentionNotifications.js) 1.81KB 0.68KB
collaboration (usePresence.js) 6.33KB 1.84KB
collaboration (useRealtimeSubscription.js) 7.91KB 2.01KB
components (index.js) 545.85KB 130.67KB
core (index.js) 8.94KB 3.59KB
create-plugin (index.js) 27.94KB 9.51KB
data-objectstack (index.js) 215.98KB 59.97KB
fields (index.js) 249.27KB 62.92KB
i18n (LocalizationContext.js) 1.76KB 0.96KB
i18n (builtinAggregateLabels.js) 0.86KB 0.49KB
i18n (currency.js) 1.22KB 0.64KB
i18n (fallbackInterpolation.js) 6.25KB 2.77KB
i18n (i18n.js) 8.87KB 3.64KB
i18n (index.js) 5.22KB 2.26KB
i18n (pickLocalized.js) 9.86KB 3.95KB
i18n (provider.js) 32.15KB 10.49KB
i18n (useDisplayLocale.js) 2.85KB 1.45KB
i18n (useObjectLabel.js) 34.34KB 9.17KB
i18n (useSafeTranslation.js) 5.60KB 2.33KB
layout (index.js) 38.83KB 10.95KB
mobile (MobileProvider.js) 0.92KB 0.49KB
mobile (ResponsiveContainer.js) 0.94KB 0.38KB
mobile (breakpoints.js) 1.51KB 0.70KB
mobile (createOfflineDataSource.js) 5.61KB 1.75KB
mobile (index.js) 1.99KB 0.87KB
mobile (offlineQueue.js) 3.91KB 1.35KB
mobile (pwa.js) 0.97KB 0.49KB
mobile (serviceWorker.js) 1.48KB 0.62KB
mobile (serviceWorkerSource.js) 3.41KB 1.48KB
mobile (useBreakpoint.js) 1.54KB 0.65KB
mobile (useGesture.js) 6.96KB 1.98KB
mobile (useOfflineSync.js) 1.99KB 0.72KB
mobile (usePullToRefresh.js) 2.53KB 0.85KB
mobile (useResponsive.js) 0.72KB 0.42KB
mobile (useSpecGesture.js) 4.39KB 1.66KB
mobile (useTouchTarget.js) 1.01KB 0.54KB
permissions (MePermissionsProvider.js) 13.52KB 4.88KB
permissions (PermissionContext.js) 0.31KB 0.25KB
permissions (PermissionGuard.js) 0.89KB 0.45KB
permissions (PermissionProvider.js) 6.24KB 2.16KB
permissions (discardProofCache.js) 1.04KB 0.55KB
permissions (evaluator.js) 8.39KB 3.10KB
permissions (index.js) 0.93KB 0.41KB
permissions (store.js) 0.91KB 0.42KB
permissions (useFieldPermissions.js) 1.28KB 0.53KB
permissions (usePermissions.js) 4.83KB 2.27KB
plugin-ai (index.js) 14.81KB 3.63KB
plugin-calendar (index.js) 49.92KB 14.22KB
plugin-charts (index.js) 71.49KB 19.99KB
plugin-chatbot (index.js) 195.34KB 46.51KB
plugin-dashboard (index.js) 131.44KB 34.65KB
plugin-designer (index.js) 215.94KB 44.33KB
plugin-detail (index.js) 253.29KB 65.88KB
plugin-editor (index.js) 2.23KB 1.05KB
plugin-form (index.js) 136.71KB 34.16KB
plugin-gantt (index.js) 167.62KB 41.26KB
plugin-grid (index.js) 212.64KB 57.91KB
plugin-kanban (index.js) 46.41KB 14.49KB
plugin-list (index.js) 112.73KB 27.69KB
plugin-map (index.js) 21.48KB 6.99KB
plugin-markdown (index.js) 13.88KB 4.80KB
plugin-report (index.js) 43.41KB 11.93KB
plugin-timeline (index.js) 30.07KB 8.74KB
plugin-tree (index.js) 10.58KB 3.72KB
plugin-view (index.js) 85.04KB 21.01KB
providers (DataSourceProvider.js) 0.75KB 0.39KB
providers (MetadataProvider.js) 1.37KB 0.59KB
providers (ThemeProvider.js) 1.90KB 0.85KB
providers (UploadProvider.js) 11.66KB 3.50KB
providers (index.js) 0.45KB 0.23KB
providers (types.js) 0.01KB 0.04KB
react-runtime (index.js) 5.62KB 2.34KB
react (LazyPluginLoader.js) 4.47KB 1.63KB
react (SchemaRenderer.js) 104.82KB 34.67KB
react (data-invalidation.js) 5.05KB 2.08KB
react (index.js) 4.63KB 2.18KB
react (schema-input.js) 4.25KB 2.04KB
react (spec-input.js) 0.20KB 0.18KB
sdui-parser (codegen.js) 6.58KB 2.74KB
sdui-parser (dashboard-widget-options.js) 3.08KB 1.30KB
sdui-parser (index.js) 5.66KB 2.50KB
sdui-parser (input-type.js) 2.84KB 1.40KB
sdui-parser (kanban-quick-add.js) 3.89KB 1.87KB
sdui-parser (parse.js) 25.28KB 7.80KB
sdui-parser (provenance.js) 3.66KB 1.82KB
sdui-parser (types.js) 0.28KB 0.23KB
sdui-parser (validate.js) 14.82KB 4.99KB
types (ai.js) 4.11KB 2.06KB
types (api-types.js) 0.20KB 0.18KB
types (app.js) 2.87KB 1.00KB
types (base.js) 0.20KB 0.18KB
types (blocks.js) 0.20KB 0.18KB
types (complex.js) 2.93KB 1.49KB
types (crud.js) 0.20KB 0.18KB
types (dashboard-filter-alias.js) 6.23KB 2.74KB
types (data-display.js) 3.75KB 1.85KB
types (data-protocol.js) 0.20KB 0.19KB
types (data.js) 0.20KB 0.18KB
types (designer.js) 1.85KB 0.85KB
types (disclosure.js) 0.20KB 0.18KB
types (error-code.js) 1.54KB 0.88KB
types (expression.js) 0.20KB 0.18KB
types (feedback.js) 0.20KB 0.18KB
types (field-types.js) 0.20KB 0.18KB
types (form.js) 0.20KB 0.18KB
types (http-inflight.js) 8.87KB 3.73KB
types (http-retry.js) 4.32KB 2.02KB
types (icon-key-migration.js) 4.26KB 1.63KB
types (index.js) 4.74KB 2.25KB
types (layout.js) 0.20KB 0.18KB
types (managed-by.js) 0.19KB 0.18KB
types (mobile.js) 4.73KB 2.28KB
types (navigation.js) 0.20KB 0.18KB
types (objectql.js) 0.20KB 0.18KB
types (overlay.js) 0.20KB 0.18KB
types (permissions.js) 0.20KB 0.18KB
types (plugin-scope.js) 0.20KB 0.18KB
types (record-components.js) 0.20KB 0.19KB
types (record-semantics.js) 1.28KB 0.67KB
types (registry.js) 0.20KB 0.18KB
types (reports.js) 0.20KB 0.18KB
types (select-option.js) 0.20KB 0.19KB
types (spec-report.js) 5.05KB 1.93KB
types (spec-ui-namespace.js) 0.20KB 0.19KB
types (strict-authoring-face.js) 14.04KB 5.36KB
types (system-fields.js) 3.33KB 1.54KB
types (theme.js) 6.28KB 2.87KB
types (ui-action.js) 8.11KB 3.32KB
types (views.js) 0.20KB 0.18KB
types (widget.js) 0.20KB 0.18KB

Size Limits

  • ✅ Core packages should be < 50KB gzipped
  • ✅ Component packages should be < 100KB gzipped
  • ⚠️ Plugin packages should be < 150KB gzipped

Copy link
Copy Markdown
Collaborator Author

Contract review

Served-tier: 73/73 CONTRACT_REVIEW_TIER
Head-sha: b9603eba96e0793a00f1187f083eaffc1945eb7e

Rendered and adopted by the domain:spec @ objectui PM seat at 2026-09-17T16:46Z, Seat: domain:spec#1. ⛔ This seat serves claude-opus-5 and is below tier, so it did not self-review (references/contract-review.md:50): this is an isolated at-tier review subagent, fed only the card, the ruling 5716921048, the earlier review records and this PR — ⛔ never the dispatch order, ⛔ never the seat's conclusions, ⛔ never the implementer's report.

The isolation held and the reviewer proved it rather than asserting it: it names the two post-ruling comments by id, says it classified them by timestamp and header, and states it did not read them.

Tier, from the reviewer's own transcript (⛔ not get_session, which measures the dispatching session, :54): 73 of 73 assistant records stamped claude-fable-5-1 = CONTRACT_REVIEW_TIER. Zero at any other model ⇒ no mid-review fallback, which :57 makes decisive.

Adopted verbatim.

① Derived judgments

The acceptance property, answered. 「Yes, for every claim the ruling named and for every claim I checked against the code.」 The mutation header's THREE SPELLINGS match three regexes and the claimed ASSIGN_OP coverage; declaredObjectBody's count claim matches a (const|let|var)\s+name plus one import-clause regex and nothing else; readMetaBody's order claim matches the new skipFallbackSet and the released seen; the OPTIONS bullet and resolveRegistrationOptions are true of the argument with the name-route qualification stated beside them; and regenerate-known-schema-types.mjs is narrowed to 「an options ARGUMENT whose shape it does not recognise」 with the silent half declared beneath.

Did the diff stay inside the ruling? 「Yes. The only behavioural code change is the two readMetaBody fixes, each a few lines with its own pin … ⛔ No new guard logic: legs A, C and G show that the shapes the head refuses are exactly the pre-existing R1/R2 refusals; every newly named shape derives silently at head.」

⭐ The 「all-green-if-the-guard-died」 hazard — asked explicitly and answered with a measurement, not a reassurance. Leg A turned the mutation guard off: all 15 KNOWN GAP rows stayed green, exactly as a block of silence-assertions must. What went red was the new firing control beside the block plus the seven R2 refusal pins — 8 red. The scope route carries the same distinction through the R1 refuses a name declared more than once pin (leg G, 1 red). ⇒ silent and dead are distinguishable, and the instrument that distinguishes them was itself exercised.

Seven ablation legs, two of them declared controls, each from the committed state with the mutated blob verified different, restored by git checkout HEAD --, final blob 33d9fd37 equal to head and git status clean. Plus eight hand probes of the guard at head, including the two that matter most for an over-claim check: read-only member access reads (no finding), and a template-literal bracket write is silent — covered by the header's 「NOTHING ELSE IS SEEN」.

⚠️ Where the reviewer's numbers differ from the PR's, it says so and says why: its B and C legs read 13 and 8 where the PR names 11 and 4, 「because my widenings were broader than the PR's; both are supersets containing exactly the rows the PR names, so the direction and membership are confirmed, not the exact figure」. ⭐ That is the correct handling of a divergent reading and is why this record is adoptable.

The generated file: blob 45bf0e99 at both b6828d4171 and b9603eba9unchanged by this round, and the round diff does not list the file. Whole-PR: +5 / −0, added set exactly ui:app ui:home ui:page ui:record ui:utility, 644 → 649, --check exit 0. Independently re-derived by this seat before dispatching the review, from the generated list directly, with the five keys reading 1 each at head and 0 at the merge-base and an absent-token control at 0.

② Semver level

@object-ui/cli: patch. 「The only published package whose files moved in the PR is @object-ui/cli (the generated file, +5 additive strings on a readonly string[]); the scripts and their tests are root-level and unpublished. Correct.

③ Boundary flags

Seven non-blocking observations, ⛔ none a gate. The two worth surfacing:

  • (a) Two headers are narrower than the code in the LOUD directionother.meta.namespace = … refuses meta, and an import { meta as other } clause counts as a binding. ⭐ Both 「produce a finding on a correct registration, never a silent reading, so they do not violate the acceptance property」. That is the right test to have applied: this card's whole failure mode is the quiet direction.
  • (c) The firing control's own comment calls itself the control 「for the whole KNOWN GAP block」 while exercising only the mutation route; the scope route's distinction rests on leg G's pin. 「The distinction exists; the sentence over-scopes the control by one route.」

The rest: a changeset under-claim in the loud direction (var is refused too); a 「16 rows」 count that includes the pre-existing parameter pin; behind main with none of the six files moved; a page:header site-count-vs-key-count difference in an earlier record; and the arrow/destructured parameter variants filed under the declared parameter class whose pin exists.

This seat routed one of the reviewer's and the implementer's shared findings rather than letting it ride: .changeset/8499-… still says 「656 keys」 at head, stale before this branch existed (the same instrument reads 644 at the merge-base and 649 here). ⛔ Correctly not edited in a round capped to 「prose made true + known gaps pinned」. Filed as objectui#9704. The INDIRECT_REGISTRATIONS observation is filed as objectui#9703.

「No conflict was found between the ruling and what the tree shows.」

Implemented-by: claude/issue-9641-registry-namespace-spread
Reviewed-by: session_01UanLVj6xvbS6puBCewLr8L

VERDICT: PASS


Generated by Claude Code

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

Projects

None yet

2 participants