fix(scripts): resolve a registration namespace that arrives by reference (objectui#9641) - #9686
Conversation
`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
|
changeset-claim-re-read
|
✅ Console Performance Budget
The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it. 📦 Bundle Size Report
Size Limits
|
At-tier contract review record — PR #9686 / card objectui#9641 — FAILAdopted VERBATIM by the Tier, from the reviewer's OWN transcript (⛔ not 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 Working-directory fence: every mutation (clone, worktrees, vitest/typescript install, ablation, probe fixtures) happened under this review's own scratchpad directory ( What I measured
What I checked and found
Blocking items
Non-blocking observations (not gating)
Artefacts (all under the scratchpad review dir): ⭐ — Record posted by PM 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
✅ Console Performance Budget
The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it. 📦 Bundle Size Report
Size Limits
|
At-tier contract review record — PR #9686 @ head
|
| 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
- Close the mutation shapes on the identifier/spread route (
scripts/check-doc-component-types.mjs,declaredObjectBodyor the point a name is followed). Refuse the name — reportunresolved-registration-meta— when the comment-stripped file assigns to itsnamespaceorskipFallbackproperty (dot or bracket form, including the logical-assignment operators), deletes either, or passes it as the target ofObject.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 onb902cc1and green after, using the ablation discipline the PR already uses. Re-measure the live tree: must remain 649 keys / 0 findings /metaViaReference5 andregenerate --check0 (the prototype shows it does). - Make the
declaredObjectBodyheader true to the guard that ships. After item 1 it may say the name is followed only when it is declared once, withconst, and itsnamespace/skipFallbackare never written after declaration in the file. It must also state, per AGENTS 完善设计器的每一个细节 #9, what is not guarded: bindings other thanconst|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 withconstin 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.tsline ~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 readfamily covers "everything it refuses"; the phantom and shadowing pins are titledrefuses …. 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
undefinedornullmatches 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
topLevelPartsdoes 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.
⭐
— 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
✅ Console Performance Budget
The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it. 📦 Bundle Size Report
Size Limits
|
At-tier contract review record — PR #9686 @ head
|
| 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
-
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 —deriveKnownSchemaTypesis a call toderiveRegistryKeysplus a sort, no scanner of its own. Landing the resolver inscripts/check-doc-component-types.mjsis 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.mjsimport the same function; no sibling script carries its own namespace regex). -
Regenerated, not hand-edited. Proven: the render of the derivation is byte-identical to the committed file (
--checkexit 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. -
Acceptance exactly right. Each new key maps to a real registration:
page.tsx:711-715register the five kinds with a'ui'namespace, and the registry stores the namespaced key plus the bare key (noskipFallback). 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. -
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/varreassignment; a dot write, a quoted-bracket write, a logical-assignment write; adeleteof the property; anObject.assignonto 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
skipFallbackwrite → PHANTOM in the bare half; a destructuring-assignment target → MISS;Object.defineProperty→ MISS;Reflect.set→ MISS;Reflect.deleteProperty→ PHANTOM;Object.setPrototypeOf→ MISS; a computed-bracket write → MISS; an alias write reached through a spread → MISS. - Silently wrong, UNDECLARED, name-resolution route: a destructured
constinside the calling function shadowing a module-level const → PHANTOM; likewise an array-destructured binding, a later declarator in the same statement, and acatchbinding. 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 explicitskipFallback: falsearriving 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.
- Refused loudly (correct):
-
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.assignonto the name. (b) thedeclaredObjectBodyheader's "The declaration count seesconst,let,varand 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. -
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. -
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;
--checkthrows on any finding, so a refused shape cannot shrink the universe silently. One omission in the census prose: see non-blocking (c). -
Hard constraint.
page.tsxandRegistry.tsare at 0 diff lines (lit control shown); the diff touches nothing underpackages/componentsorpackages/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
-
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/skipFallbackproperty (dot or quoted bracket), as a delete of that property, or as anObject.assignonto 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 calleeskipFallbackwrite (PHANTOM) as KNOWN GAP readings, the way the parameter gap is pinned, so closing them later fails a test. ⛔ Closing them is not required. -
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/varor 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. -
readMetaBody's "a later one wins, which is what the runtime does" is not enforced for a doubly-spread base (PHANTOM) or an explicitskipFallback: falsearriving 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.
⭐
— 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
✅ Console Performance Budget
The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it. 📦 Bundle Size Report
Size Limits
|
1 similar comment
✅ Console Performance Budget
The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it. 📦 Bundle Size Report
Size Limits
|
Contract reviewServed-tier: 73/73 Rendered and adopted by the ⭐ 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 Adopted verbatim. ① Derived judgmentsThe 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 Did the diff stay inside the ruling? 「Yes. The only behavioural code change is the two ⭐ 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 Seven ablation legs, two of them declared controls, each from the committed state with the mutated blob verified different, restored by The generated file: blob ② Semver level
③ Boundary flagsSeven non-blocking observations, ⛔ none a gate. The two worth surfacing:
The rest: a changeset under-claim in the loud direction ( ⭐ This seat routed one of the reviewer's and the implementer's shared findings rather than letting it ride: 「No conflict was found between the ruling and what the tree shows.」 Implemented-by: VERDICT: PASS Generated by Claude Code |
Fixes #9641
Closing round (round 4) by the
domain:spec@ objectui execution seat, sessionsession_01UanLVj6xvbS6puBCewLr8L, on the ruling recorded at objectui#9641 comment5716921048.Angle-bracket-shaped tokens are spelled out in words throughout this body (AGENTS.md, the GitHub-mutates-body-bytes clause).
deriveRegistryKeysreadnamespaceandskipFallbackout of a registration call's own balanced argument span. When the options object is not spelled out at the call, there is nonamespace: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 generatedKNOWN_SCHEMA_TYPES, andobjectui checkcalled 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:72f55c9ec1ui:pageui:appui:utilityui:homeui:recordin the generated listpage:header— namespaced, options spelled out at the callThe generated artefact is
packages/cli/src/utils/known-schema-types.ts— the regeneration script's ownTARGET, 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.tsxregisters five page kinds from one options object — one call passing it whole as a bare identifier, four spreading it to vary a label:So
ui:pagewas 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.
resolveRegistrationOptionsnow works from an allowlist, which is structural rather than a list of idioms to keep up with:namespace, a computedskipFallback.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
letrefusal) while leaving mutation unread. Aconstcannot be rebound, but its contents can be written. Five shapes derived with no finding at all:constwithout a namespace,.namespaceassigned afterObject.assignonto the constconstwith a namespace, that property deleted afterconstwith a namespace,skipFallbackassigned after⭐ 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".declaredObjectBodynow refuses a name whose comment-stripped file assigns to itsnamespaceorskipFallback(dot or bracket, including the logical and compound operators), deletes either, or passes it as anObject.assigntarget. 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
undefinedornullis 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.The tightening reds nothing, which is what made both rounds affordable. On this head the derivation reports 0 findings, the universe is 649 keys,
metaViaReferenceis 5, andnode scripts/regenerate-known-schema-types.mjs --checkis 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:
⛔ 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
deletewhose target is the name spelled exactly followed bynamespace/skipFallbackwritten out (dotted or quoted bracket), or anObject.assignwhose first argument is that name. An alias, a callee, a computed key, a destructuring target,Reflect.set,Reflect.deleteProperty,Object.definePropertyandObject.setPrototypeOfall derive with no findingcatch— all phantom-directioncheck-doc-component-types.mjsheader)const, in the same file, and never written to afterwards … and the one binding kind it cannot see"optionsMutatedAfterDeclarationheaderObject.assignontoname's key-bearing options after it is declared?"register()call refuses the name although the runtime never reached itdeclaredObjectBodyheaderconst,let,varand import bindings"readMetaBodyheaderregenerate-known-schema-types.mjsheader2. 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.
Reflect/defineProperty/setPrototypeOf/computed keycatchThe mutation route reads: alias write (MISS), alias
delete(PHANTOM), alias write through a spread (MISS), callee write (MISS), calleeskipFallbackwrite (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 readsmeta?.namespace, which follows a prototype). The scope route reads: destructuredconst, array-destructured binding, later declarator,catchbinding — 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.
{ ...base, ...mid, ...base }publishesui:widgetview:widget— a PHANTOM and a MISS at onceskipFallback: falsearriving by spread{ skipFallback: true, ...base }withbasecarryingfalsepublishes both keysui:widgetalone — the bare key MISSINGEach 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,
metaViaReference5;node scripts/regenerate-known-schema-types.mjs --checkexit 0; the generatedpackages/cli/src/utils/known-schema-types.tsis at blob45bf0e99, the same blob it has carried since R0. Firing controls on the same instrument:page:header,element:text,action:buttoneach 1; absent-token control 0.The five pending changesets that name a file this diff touches
The
changeset-claim-re-readgate (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 exactlyui:appui:homeui:pageui:recordui:utility; removed set is empty (measured as a set difference between the blob at merge base72f55c9ec1and 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:8802-…-kanban-gantt-family-retirementview:kanban/view:ganttstill on the listkanban/ganttDID leaveplugin-kanban:kanban-ui/…:kanban-enhancedobject-kanban/object-gantt7122-spec-17-3-0-parity-reconciliationuser:profileretired from the regenerated listuser:profile+element:form"@objectstack/spec's own enum, which this diff does not touch;element:formalso reads 0 in the generated list at both refs6416-plugin-report-namespacereport:*spellings droppedplugin-report:*keys name real components7704-schema-registry-chatbot-keyschatbot-enhancedandchatbot-floating8499-node-slot-registered-armsmy-componentcarries a written exemption incheck-doc-component-types.mjs8499is false, and this diff is not what falsified it. It sayscheck:doc-typesjudges 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
register(occurrences inside string literals — an error message incore/src/errors/index.ts, two quoted mentions inRegistry.ts, and a plugin template string. The derivation skips those by design; the hand scanner stripped comments but not strings. 238 is the reading, andcounters.callSitesis 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,indirectandopen. The by-reference population ismetaViaReference; every one of those sites is thepage.tsxfamily, 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
scripts/regenerate-known-schema-types.mjs; the repair is inscripts/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 fromderiveRegistryKeysdeliberately, "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.tsxandpackages/core/src/registry/Registry.tsare at 0 diff lines; no registration was added, removed or renamed, so nothing here touches or pre-empts objectui#9263. If that decision deletes theappregistration, the next regeneration dropsui:appand 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 HEADproving the restore — the restore leg isgit checkout HEAD -- PATH(⛔ never the bare form, which restores from the polluted index), and the trap uses absolute paths.47a9397b517d113559b1a920seen.delete(name)removedfalsetrackingskipFallbackby truthinessReflect,defineProperty,setPrototypeOf, computed keycatchoptionsMutatedAfterDeclarationreturns nullOn 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.jsonand workflows —scripts/pm/dispatch-gates.mjsrefuses objectui paths by design.pnpm changeset:check·node scripts/check-changeset-presence.mjsnode scripts/regenerate-known-schema-types.mjs --checkpnpm check:doc-types(the gate whose derivation this changes)pnpm check:registry-bare-names·check:prompt-keys·check:spec-symbolspnpm check:sdui-registration-pinspnpm check:doc-snippets·check:doc-examplespnpm check:new-line-citationspnpm check:control-bytes·check:test-path-roots·check:changeset-claims·check:pending-changeset-literalspnpm type-check:scriptsturbo run type-check --filter=@object-ui/cli·turbo run lint --filter=@object-ui/clipackages/cli(the generated file is at the same blob), so its input has not movedpnpm lint:rootpnpm check:comment-mask-corpus·check:vi-mock-specifiers·check:shell-escape-residuescripts/__tests__+packages/cli/src/__tests__Declared narrowing on lint (accepted in R0 with this invariance argument, re-measured on this head).⚠️
pnpm lintrepo-wide is CI's run; what ran here islint:rootplus the one package this diff touches. Three readings: (1) the population is read from eslint's own resolved config —lint:rootis 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 jsoncounts 373 files linted, 0 errors, 32 pre-existing warnings; (3) type-aware linting is not enabled ineslint.config.js— zero occurrences ofparserOptions.projectorprojectServiceoutside a comment — so this diff cannot move the verdict on a file it did not touch.check:doc-snippets/check:doc-exampleswere 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-widepnpm lintandpnpm testremain CI's.Repo-wide
pnpm type-checkis CI's. The exported surface has not moved:KNOWN_SCHEMA_TYPESis typedreadonly string[], not a const-asserted literal union, and the generated file has not changed since R0.维护者速读(草稿)
改了什么 ——
objectui check从今天起接受五个它现在会报「未知类型」的写法:ui:page、ui:app、ui:utility、ui:home、ui: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