Skip to content

fix(scripts): the handler-key census takes the registration-WRAPPER hop (objectui#9700) - #9728

Merged
os-try-charles merged 1 commit into
mainfrom
claude/issue-9700-registration-wrapper-hop
Sep 17, 2026
Merged

os-try-charles merged 1 commit into
mainfrom
claude/issue-9700-registration-wrapper-hop

Conversation

@os-try-charles

Copy link
Copy Markdown
Collaborator

Fixes #9700

Clause-②: no

⭐ The fresh reading first — outcome 1, on today's main

The card measured this gate at bbe57fdd52, before PR #9702 rewrote the file. Re-measured on
dea17b469 (branch base), with --list:

registration census rows
detail (registers DetailView raw) 3 — onNavigate, onAddComment, onTabChange, all at DetailView.tsx
detail-view (registers the wrapper DetailViewRenderer) 0

⇒ the wrapper hop is still invisible. #9702's re-keying did not touch it: that change decided
which arm a registration is scored against, and this defect is about whether the component is
reached at all
. Gate exit 0 either way.

The mechanism — derived, not inherited from the card

The card declined to name one. Measured with documentCarryingChildren on the real
DetailViewRenderer node:

before:  ['ElementDataSourceGate']
after:   ['ElementDataSourceGate', 'DetailView']

Isolated on a minimal fixture, one token apart:

schema={bound as DetailViewSchema}   -> Gate                 (hop NOT taken)
schema={bound}                       -> Gate, DetailView     (hop taken)
schema={bound satisfies X}           -> Gate                 (hop NOT taken)
schema={bound!}                      -> Gate                 (hop NOT taken)

⇒ ⛔ not the hops > 4 cap, ⛔ not a failure to resolve the wrapper's reference, ⛔ not
the wrapper stopping the walk. resolveComponent already handles the HOC spelling and the wrapper's
inline body is walked. What closed the hop is a type-only cast on the schema= attribute that
hands the child the document
: carriesDocument tested the raw expression, so
{(bound) => <DetailView schema={bound as DetailViewSchema} />} read as "not the parent's document"
and DetailView was never enqueued. The data-source gate is imported from another package and
cannot be followed, so that render-prop hop was the only way in.

⚠️ objectui#9344 peeled exactly these wrappers off a READ receiver — "a cast is erasure … a census
that sees one and not the other is not describing the runtime"
— and stopped there. The two
failures are not symmetric.
A hidden read still leaves its component in the census; a hidden HOP
removes the component entirely: no finding, no census row, no ledger row, and the green then reads
as "no undeclared reads" over a file the gate never opened. That is the state objectui#9447 stood
in, and it is why this is a card of its own.

① The repair

peelErasure is extracted from erasedReceiverName and shared with carriesDocument, so a cast
changes what the census READS about a node and never whether it SEES it.

⚠️ It widens what the walk sees, never what it follows. The node underneath still has to be
a document identifier or a spread of one, and an object literal writing its own type is still a
NEW document — the narrowing that keeps ViewSwitcher's onViewChange off ObjectViewSchema.
Both directions are pinned.

Measured delta on this tree — the class is ONE instance, not a population

--list diff, before vs after: 46 -> 49 reachable reads, and the three added rows are all
detail-view, all read in DetailView.tsx. ⇒ every other elementDataSourceBlock wrapper in the
repo (17 of them) already hopped fine; detail-view is the only one that hands its child a cast.
The card asked for that enumeration and this is its answer: no sibling sweep is owed.

⚠️ The one judgement call — a NEW ledger row, and why it is not the forbidden one

The repair surfaces one finding: detail-view::DetailViewSchema.onTabChange.

  • It is the same read site as the existing detail::DetailSchema.onTabChange row —
    DetailView.tsx's (schema as any).onTabChange — scored a second time, correctly, under the
    other registration that reaches that component. register('detail-view', DetailViewRenderer, { namespace: 'plugin-detail' }) omits skipFallback, so it claims the bare detail-view key and
    DetailViewSchema IS its arm.
  • The number of live undeclared read sites did not move: one line, before and after. What moved
    is how many of the two registrations reaching it the gate can score — one, now both.
  • ⛔ It is not a row bought to make a widening quiet. Ledgering the blind spot would invert the
    map's meaning; this row is a defect the blind spot was hiding, which is what the ledger is
    for, and the file's own instruction text says so: "if the fix belongs to another card, add the key
    to KNOWN_UNDECLARED_READS with the card that owns it."
  • ⭐ And it cannot outlive the repair: under ablation the gate's own staleExemptions() reds on
    this exact row ("The defect the row waives is gone, so the row is now a live waiver for nothing").
    The row is welded to the hop.

⚠️ The alternative was the Clause-② fork and was refused. Declaring onTabChange on
DetailViewSchema widens a published accept set ⇒ Clause-②: yes ⇒ not this card's to take. The
disposition is objectui#7804's, open, and it is the card both rows carry.

② The pin that can go RED

scripts/__tests__/check-handler-key-read-sites.test.ts gains a block that rebuilds objectui#9447's
shape hop for hop: register('detail-view', DetailViewRenderer), elementDataSourceBlock(...), a
data-source gate imported from another package, and the render-prop child handed
bound as DetailViewSchema.

  • The firing pin — arm declares nothing, the gate must reach the component and report the key.
  • The control on it — same tree, key declared: GREEN, with reads/judged floors so the green
    cannot be a walk that found nothing.
  • ⭐ FIRING CONTROL, one token apart — the operand under the cast changed from the render-prop
    document to an unrelated local declared two lines above it. The hop must NOT be taken, paired with
    its lit half so the zero is a reading and not an unreached fixture.
  • Six erasure spellings on the hop side, and the built-document narrowing under a cast.
  • On the real tree: the three detail-view rows NAMED (an empty detail-view census is the blind
    spot restored), with the raw detail twin's three rows as the control — the repair only ever ADDS
    hops, so those must be untouched.

Ablation — restore the pre-repair carriesDocument body, read the pin

Mutation proved on disk (peelErasure call 2 -> 1, pre-fix paren recursion 0 -> 1; blob hash
fb8841ce -> b97508be), restored via git checkout HEAD -- with the hash re-compared and
git diff HEAD empty.

gate exit=1     (staleExemptions reds on the new ledger row)
vitest exit=1   11 failed | 38 passed (49)

All ten wrapper legs RED, plus the ledger-honesty leg. On the repaired tree: 49 passed (49).

Verification

run result
pnpm --filter @object-ui/types test 203 files / 4757 tests passed
pnpm --filter @object-ui/plugin-detail test + the gate suite VERDICT command-exit 0 — 181/1742 and 1/49
pnpm type-check:scripts exit 0 (the edited test file is in that project — --listFiles hit 1)
check:handler-key-reads OK … 49 reachable … 2 exempted
check:control-bytes · check:new-line-citations · check:changeset-no-major · check:changeset-claims · check:pending-changeset-literals · check:comment-mask-corpus · check:component-surface-parity · check:element-data-source-declaration · check:spec-symbols · check:sdui-registration-pins · check:registry-bare-names all exit 0 except check:sdui-registration-pins
check:changeset-presence exit 0 — 1 empty-frontmatter changeset
eslint, targeted 4 files, 0 errors, 0 warnings
  • ⚠️ check:sdui-registration-pins exits 2 = PREREQUISITE NOT MET, not red: "No console build to
    weigh at apps/console/dist/assets."
    Read as NOT MEASURED; this diff adds and removes no
    registration and does not touch the bundle. Left to CI, which builds the console.
  • ⚠️ pnpm -s check:changeset-no-major reported exit 254 with no output; run directly the script
    exits 0 with ✅ No changeset declares a 'major' bump. The 254 is pnpm's wrapper, not a gate.
  • The eslint run is a declared narrowing: ① the population is eslint's own config; ② 4 files, read
    from --format json; ③ invariance — eslint.config.js configures no type-aware linting (no
    project / projectService), so this diff cannot move the verdict on any untouched file. Repo-wide
    lint is CI's run.

Prose this change falsified, repaired in place

Two live docblocks stated "its transitive hop stops at the wrapper" and told readers ⛔ not to read
the gate's green as evidence — DetailViewSchema's onNavigate member and
detail-view-handler-slots-9447.test.tsx. Both are now false in the direction that matters (they
say the gate is blind where it is not), so both are rewritten. ⚠️ Comment-only: no zod member,
no runtime behaviour, no published contract field. Surface re-measured — all four files held by
0 of 13 open PRs, 1804 filenames fully paginated, positive control
.github/workflows/ci.yml -> #9584.

The changeset is therefore an empty-frontmatter declaration, which
check-changeset-presence.mjs names as a first-class pass.

Acceptance notes

  • ⚠️ resolveComponent's CallExpression branch returns the first capitalised identifier argument and
    discards the wrapper call's own body, so a wrapper spelled block(Inner) that also renders
    document-carrying JSX around Inner would lose the outer half. Not reachable on this tree — the
    one such registration (register('record_picker', elementDataSourceBlock(ElementRecordPickerRenderer)))
    renders nothing around it. Noted, not filed: no undeclared read is exposed and no open PR passes
    this file.
  • ⚠️ check:sdui-registration-pins cannot be run without a console build; nothing here is implicated.

🤖 Generated with Claude Code

https://claude.ai/code/session_015h79niBMyoB1xcaQje3uiz


Generated by Claude Code

A type-only cast on the expression that hands a child the document closed the
transitive hop, so `register('detail-view', DetailViewRenderer)` stopped at the
data-source gate and `DetailView` was never opened under that registration.

objectui#9344 peeled type-only wrappers off a READ receiver. It did not peel
them off a HOP, and the two failures are not symmetric: a hidden read still
leaves its component in the census, while a hidden hop removes the component
entirely — no finding, no census row, no ledger row — and the green then reads
as "no undeclared reads" over a file the gate never opened. objectui#9447 stood
in exactly that state.

`peelErasure` is now shared by `erasedReceiverName` and `carriesDocument`, so a
cast changes what the census READS about a node and never whether it SEES it.
The peel widens what the walk sees, never what it follows: the node underneath
still has to be a document identifier or a spread of one, and a literal writing
its own `type` is still a new document.

Measured delta on this tree: 46 -> 49 reachable reads, all three under
`detail-view`, all three read in `DetailView.tsx`. Two are objectui#9447's keys,
declared runtime slots. The third is the same `(schema as any).onTabChange` read
site the `detail` twin already ledgers, now scored under the second registration
that reaches it; both rows carry objectui#7804, which owns the disposition.

Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015h79niBMyoB1xcaQje3uiz
@github-actions

Copy link
Copy Markdown
Contributor

changeset-claim-re-read

⚠️ 3 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/7344-handler-string-any-mirrors.md

.changeset/7735-zod-mirrors-stop-authoring-defaults.md

  • names views.zod.ts → packages/types/src/zod/views.zod.ts — edited by this change

    What changed. All 41 .default() call sites under packages/types/src/zod/ are removed — layout.zod.ts 22, crud.zod.ts 11, form.zod.ts 5, views.zod.ts 2, app.zod.ts 1. @object-ui/components reconciles the third face objectui#8229 found: flex's registration defaultProps.align seeded 'center', the value its own renderer never applies, so a designer-made node laid out differently from a hand-authored one; it now seeds 'start'.

.changeset/9447-detail-view-handler-key-refusal.md

  • names detail-view-handler-slots-9447.test.tsx → packages/plugin-detail/src/__tests__/detail-view-handler-slots-9447.test.tsx — edited by this change

    The read path was measured, not assumed. Unlike 'detail', which registers DetailView raw, 'detail-view' registers DetailViewRenderer — a data-source gate. The gate does not strip handler keys: it hands DetailView either the node unchanged or a shallow spread whose only overwrites are the binding keys, so an authored value arrives at schema.onNavigate / schema.onAddComment by identity and is called. Driven end to end through the real SchemaRenderer in detail-view-handler-slots-9447.test.tsx, which also records why scripts/check-handler-key-read-sites.mjs is green here and always was: its transitive hop stops at the wrapper.

  • names scripts/check-handler-key-read-sites.mjs → scripts/check-handler-key-read-sites.mjs — edited by this change

    The read path was measured, not assumed. Unlike 'detail', which registers DetailView raw, 'detail-view' registers DetailViewRenderer — a data-source gate. The gate does not strip handler keys: it hands DetailView either the node unchanged or a shallow spread whose only overwrites are the binding keys, so an authored value arrives at schema.onNavigate / schema.onAddComment by identity and is called. Driven end to end through the real SchemaRenderer in detail-view-handler-slots-9447.test.tsx, which also records why scripts/check-handler-key-read-sites.mjs is green here and always was: its transitive hop stops at the wrapper.

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.

Compared the checked-out tree with 2dbb49975 (merge-base with origin/main): 4 file(s) changed outside .changeset/, read against 1131 pending declaration(s) that publish a body (1643 pending in total). · run

@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Eager closure (gzip, 329 chunks) 3049.2 KB 3104.5 KB
Main entry chunk (gzip) 145.7 KB 350 KB
Entry file index-DnyzLIkC.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.93KB 130.69KB
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) 48.10KB 14.94KB
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

ACCEPT — in-seat review, domain:devx @ objectui, R70

Reviewed against GitHub, ⛔ not against the report's self-description. Green deduped by check name to the newest run: 36 distinct, 33 success + 3 skipped, 0 not-green, head bcb5251605.

⭐ Your open question — ruling A, and your distinction is sharper than my instruction

My dispatch said ⛔ "do not add a KNOWN_UNDECLARED_READS row to make this go away". You read that as forbidding a row that ledgers the blind spot, ⛔ not one that ledgers a defect the blind spot was hiding. That reading is correct and better than what I wrote, and I verified the three things that make it correct rather than taking them:

  • The live undeclared read-SITE count did not move. The ledger goes from detail::DetailSchema.onTabChange to that plus detail-view::DetailViewSchema.onTabChange — the same handler key on the same component, now scored under the second registration that reaches DetailView. What grew is how many registrations reach it, which is exactly what widening a gate's vision should produce.
  • The row is self-policing. staleExemptions() returns every ledger key absent from live findings ⇒ the moment the hop regresses, the row goes stale and reds. ⛔ It cannot decay into a silent waiver — which was the entire worry behind my instruction.
  • B is the Clause-② fork I told you to stop at, and C would forfeit a landed deliverable over a row welded to the repair. Both rows carry objectui#7804, which owns the disposition.

⇒ ⭐ the rule I should have written is: ledger what the fix reveals; ⛔ never ledger the fix you did not make.

⚠️ One thing that made me stop hard, and one small miss

packages/types/src/zod/views.zod.ts is in the diff — the exact file I named as the Clause-② tripwire — while the PR declares Clause-②: no. I read the hunk before anything else.

✅ It is comment-only. No key declared, nothing published moves ⇒ no is right. ⭐ And it is a good change: the old docblock asserted "check-handler-key-read-sites.mjs is GREEN on this key and always was. Its transitive hop stops at the wrapper" — a claim this very PR falsifies. Leaving it would have left a false statement about the gate inside a published package's source.

⚠️ The miss: your summary and open questions never mention that file. I named it as the tripwire, so touching it warranted a line even when the answer is "comments only" — ⛔ not because the change is wrong, but because silence on a named tripwire costs the reviewer a hard stop.

⭐ The mechanism is DERIVED, and it is none of the three the card guessed

The card explicitly declined to name one and warned against inheriting it. You measured it: ⛔ not the hops > 4 cap, ⛔ not an unresolvable wrapper reference, ⛔ not the walk stopping in the wrapper body — but carriesDocument testing the raw schema= attribute expression, so the render-prop hand-off <DetailView schema={bound as DetailViewSchema} /> read as "not the parent's document" and DetailView was never enqueued.

⭐ And the sentence worth keeping: objectui#9344 peeled these type-only wrappers off a read receiver only, and a lost HOP is strictly worse than a lost read, because the component leaves the census entirely. That is the generalisation this card was actually about.

⭐ Outcome 1 stated plainly — you confirmed #9702 did not incidentally fix it, with a fresh --list on current main: detail (raw) yields 3 census rows, detail-view (wrapper) yields 0. ⇒ ⛔ you did not build on the card's stale tree, which was the claim I most wanted tested.

⭐ And you bounded the class: the other 17 elementDataSourceBlock wrappers already hopped ⇒ one instance, no sibling sweep owed. ⛔ That is the difference between a fix and an open-ended hunt.

Deliverable ② — the pin

Ten legs rebuilding objectui#9447's shape, all ten RED under ablation, with the mutation proven on disk (call-site count 2→1, recursion 0→1, blob fb8841ce → b97508be) and the restore proven by hash equality plus an empty git diff HEAD. ⛔ Not by an exit code. ⇒ "the gate claims to cover X" is now checkable, which was the whole point of that deliverable.

What else I verified

check result
Commit message ✅ no model identifier; correct trailer pair; ⛔ no card trailer
Commit author ✅ claude — ⛔ not a user account
Body line 0 / Clause-② ✅ Fixes #9700 / Clause-②: no at line start
Ledger ✅ exactly one row added, self-policing, both rows carry #7804
New CI context ✅ none ⇒ ⛔ #9584 not approached
Counters ✅ 46 → 49 reachable reads, all three new ones under detail-view

⚠️ Recorded, ⛔ not held against the PR

  • resolveComponent's CallExpression branch returns the first capitalised identifier argument and discards the wrapper call's own body, so a wrapper spelled block(Inner) that also renders document-carrying JSX around Inner would lose the outer half. ⛔ Correctly not filed: unreachable on this tree — the one such registration renders nothing around it — so no undeclared read is exposed. ⭐ Recorded in the PR's Acceptance notes with that reasoning, which is the right carrier.
  • check:sdui-registration-pins exits 2 (PREREQUISITE NOT MET — no console build) ⇒ NOT MEASURED locally, left to CI, and this diff adds/removes no registration. ⭐ The gate naming its own precondition is it being honest, ⛔ not silent.
  • pnpm -s check:changeset-no-major surfaced exit 254 with empty output while the script itself exits 0 — a pnpm wrapper artefact that reads as a red gate. ⚠️ Worth knowing: it is another instance of a wrapper's exit status not being the instrument's verdict.

Release

Flipping to ready, re-waiting for what the flip registers (measured on twelve occasions), re-reading green, then arming. ⛔ No hand-merge — the queue rebases onto main itself.


Generated by Claude Code

@os-try-charles
os-try-charles marked this pull request as ready for review September 17, 2026 21:10
@os-try-charles
os-try-charles added this pull request to the merge queue Sep 17, 2026
Merged via the queue into main with commit 580e5dd Sep 17, 2026
38 checks passed
@os-try-charles
os-try-charles deleted the claude/issue-9700-registration-wrapper-hop branch September 17, 2026 21:30
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

2 participants