Skip to content

fix(types): the three view handler keys are EVENT NAMES, not callbacks (part of #6124) - #6899

Merged
os-sam merged 5 commits into
mainfrom
claude/issue-6124-callables-to-json-conversion
Aug 30, 2026
Merged

fix(types): the three view handler keys are EVENT NAMES, not callbacks (part of #6124)#6899
os-sam merged 5 commits into
mainfrom
claude/issue-6124-callables-to-json-conversion

Conversation

@claude

@claudeclaudeBot commented Aug 30, 2026

Copy link
Copy Markdown
Contributor

Part of #6124 — this lands the executable half and escalates the rest. It is deliberately NOT a closing PR: the ruled conversion sweep is not executable as ruled, and the measurement is below.

⚠️Clause ② — published-validator surface.@object-ui/types' zod mirrors are objectui's own published surface. This PR stays draft, its review goes at CONTRACT_REVIEW_TIER, and it is not the PM's to close.


What this PR changes

Three on* mirrors in views.zod.ts were described as "... change callback". Measured, the authored string is the name of a CustomEvent the renderer dispatches on window:

keyread pointdispatch
ViewSwitcherSchema.onViewChangeplugin-view/src/ViewSwitcher.tsx:249-255new CustomEvent(schema.onViewChange, { detail: { view } })
FilterUISchema.onChangeplugin-view/src/FilterUI.tsx:99-105new CustomEvent(schema.onChange, { detail: { values } })
SortUISchema.onChangeplugin-view/src/SortUI.tsx:93-99new CustomEvent(schema.onChange, { detail: { sort } })

These are live, JSON-authorable capabilities. The type is unchanged (z.string() / string), so no accept set moves. What changes is the two surfaces this contract is published on — the mirror's describe() text and the TS JSDoc — which now say which channel the string is, plus a pin that holds it.

The mislabel was load-bearing. A handler-key census that buckets by declared TYPE cannot tell an event name from the handler-expression dialect this repo does not support, and on that reading all three were swept in for retirement — which would have deleted working behaviour. The dual channel is what hides it: onViewChange?.(next) one line above is the React prop, while schema.onViewChange is the authored string. Same name, two channels.

The pin, and why it has two halves

packages/plugin-view/src/__tests__/handlerEventNameLiveness.6124.test.tsx

  1. DECLARED — the key is in the mirror's shape. ⚠️ Deliberately not a safeParse assertion: BaseSchema is .passthrough(), so a retired key still parses green and the parsed output still carries the value. A parse-based pin would stay green through the very deletion it exists to catch.
  2. LIVE — the authored string reaches new CustomEvent(...) on window.

Ablation proving both halves are load-bearing (mutation and restore both verified on disk). Retiring onViewChange from the mirror:

MUTATION_PROOF after=0 (expect 0) # occurrences 1 -> 0, confirmed by grep
Tests 2 failed | 6 passed (8)
x ViewSwitcherSchema declares onViewChange
x the describe() text names the EVENT-NAME channel

⭐ The three runtime tests stayed GREEN through that retirement — the runtime keeps working while the authorable surface silently loses the key. That is precisely why half 1 exists, and it is the passthrough hazard demonstrated on the pin itself.

Restore leg: git diff HEAD empty, blob 60940616 matched against the HEAD blob, git status clean.

The test resolves the package from source, not dist: the wording assertion went red before the edit and green after it with no build run, which is the control that the ablation was reading the mutated file.


⛔ Why the ruled sweep is NOT in this PR

The 2026-08-29 ruling adopts conversion on the server-side lowerCallables precedent, in three census-first buckets. All three are non-executable as ruled, measured on today's main (merge-base 05b832407).

Census moved — 28 distinct keys is now 29

z.function() in packages/types/src/zod/, measured on the merged tree: 29 distinct keys / 60 anchored named-key call sites / 9 files. The z.string() and z.any() halves are unchanged at 11 and 3 (anchored recount; the unanchored pattern from #6182 over-reports by matching mid-identifier — buttonLabel, actionUrl, conversationId, locationField, actionName, descriptionField).

⚠️Erratum, corrected here. An earlier revision of this body said 61 call sites. That was the rawz.function( occurrence count, not a count of named keys. The two differ by exactly one, and the residue is:

packages/types/src/zod/base.zod.ts:349
export const EventHandlersSchema = z.record(z.string(), z.function()).describe('Event handlers');

a record-valued declaration, not a named key, so it does not belong in a named-key call-site count. Anchored on key: z.function( the figure is 60. 29 and 9 were correct and are unchanged. ⛔ EventHandlersSchema is deliberately not in this PR's scope — it does not bear on the three z.string() keys changed here; it is recorded on the card as a separate uncounted surface.

The one key present today that is absent from the card's own enumerated list of 28 is onSend (complex.zod.ts:327). That is a set difference against the list the card body prints, not an archaeology claim: this checkout is shallow (19 commits), so git log -S cannot date an introduction and none is asserted.

⚠️ The declared-but-unauthorable surface is still growing while the card sits.

Bucket 1 (declarative action object) — measured EMPTY

Zero of the keys is consumed as a declarative object. Every read is a function call, a React-prop forward, or an authored string used as a CustomEventname. Exhaustive per-key census; representative reads:

  • schema.onCancel()plugin-form/src/SplitForm.tsx:308-309
  • schema.onColumnsReorder(newColumns)components/src/renderers/complex/data-table.tsx:1429-1430
  • onCardMove={schema.onCardMove}plugin-kanban/src/index.tsx:193
  • column.cell(value, row)plugin-grid/src/VirtualGrid.tsx:153

And the plumbing itself cannot be reached from a handler key:

  • ActionRunner.builtinExecutors is script | url | modal | flow | api | form | navigation (core/src/actions/ActionRunner.ts:845-856) — dispatch is keyed on an ActionDef's type, reached from action:*node types, never from a handler key. ButtonRenderer (components/src/renderers/form/button.tsx) does not read schema.onClick at all.
  • schema.events — the ActionDef channel AGENTS.md section 4 documents — is consumed nowhere.

⭐ Two landed facts point the opposite way from bucket 1:

Bucket 2 (hooks-style ref form) — no consumer exists

No resolver anywhere turns a string on cell / validate / custom into a function. registerHandler is keyed to action names dispatched by ActionRunner, not to handler keys. Declaring the ref arm anyway mints an inert declaration — which this ruling itself forbids ("a declared form nothing reads is the disease, not the cure"). Building the resolver is a new capability, and AGENTS.md #6 bans runtime dynamic imports / eval() for exactly this.

Bucket 3 (ADR-0049 narrow) — the stop condition, re-verified end to end

Everything falls into bucket 3, and there the narrow is a deletion, and the deletion is SILENT. Measured on today's main, with controls:

legresult
today — ButtonSchema.safeParse of the card's payloadsuccess:false, invalid_type, expected:'function', path ['onClick']
the narrow, simulated — ButtonSchema.omit({onClick:true})success:TRUE, and data.onClick deep-equals the object
controla real function parses green — the instrument can say accept
whitelisttoFormControlDomPropsforwards the object-valued onClick (and drops a sibling key — control that it really filters)
DOMclick throws: Expected `onClick` listener to be a function, instead got a value of `object` type.

onClick is in SDUI_DOM_PASS_THROUGH_KEYS (core/src/utils/dom-props.ts:88-99). So bucket 3 converts a clear parse-time error into no error plus an uncaught runtime throw at click — strictly worse than today, on the axis the ruling called decisive. The triage word was 「响亮移除」 — loud. Under passthrough() the removal is silent.

Two further blockers

⛔ Not addressed in this PR, and none of the referenced cards change state here: #6182 remains open, #6152 remains open, #5250 remains open.


Verification

All heavy runs through scripts/pm/os-verify-lock.sh; verdict lines quoted, never a bare exit code. Union re-run on the merged head abd73c18f (branch updated by merging origin/mainfab4802e3 — the repo's convention for a branch I created; no rebase, no force-push).

  • Suitespnpm exec vitest run packages/types/ packages/plugin-view/ (repo-root invocation, not pnpm --filter) — 102 files / 1122 tests, all passed on the merged head. os-verify-lock: VERDICT command-exit 0
    • Pre-merge this PR measured 100 → 101 files / 1108 → 1116 tests, the delta being exactly this PR's pin (baseline taken by removing the pin file, restore verified).
    • The further +1 file / +6 tests on the merged head is main's, not this PR's, and is attributed rather than assumed: running main's new packages/types/src/__tests__/report-schema-authoring-face.test.ts together with this PR's pin gives 2 files / 14 tests, and the pin is 8 of them, so main's file is the 6.
  • Type-checkpnpm --filter @object-ui/types type-checktsc --noEmit && tsc -p tsconfig.examples.json && tsc -p tsconfig.test.json. tsconfig.json excludes tests, so tsconfig.test.json is the leg that judges the type-level ledger pins. VERDICT command-exit 0
  • Gates derived from the actual diff, re-run on the merged head: check:control-bytes OK (5752 tracked text files) · check:vi-mock-inherit OK (108 call sites on @object-ui/react, 108 inherit, 0 auto-mocked) · check:vi-mock-specifiers OK · check-changeset-presence OK (now against merge-base fab4802e3: "3 source file(s) of 2 released package(s) changed, and this change declares 1 changeset(s)") · check-changeset-no-major OK.
  • Lint — narrowed, and the narrowing is measured, not assumed: population read from eslint's own --format json output = 3 files, each a changed file; 0 errors; my files contribute 0 warnings; views.ts's 8 warnings are proven pre-existing (identical count on the same file at merge-base 05b832407). Config invariance: type-aware linting is not enabled (no projectService / project in eslint.config.js), so this diff cannot move the verdict of any file it does not contain.

Provenance

Authored by the domain:ui execution seat, session session_013hfmP9hoMd3dJwTh85J4yB (https://claude.ai/code/session_013hfmP9hoMd3dJwTh85J4yB) — recorded here in prose because editing a PR body rewrites the footer's link form.

Changeset

.changeset/6124-view-handler-keys-are-event-names.md@object-ui/types: patch. What an author feels: nothing they wrote breaks, and the declaration now tells them the string is an event name and what to listen for.

⚠️Corrected in review. The changeset previously claimed the change is felt in "the generated JSON Schema description". Measured, and it is not — every z.toJSONSchema call in this repo generates from @objectstack/spec, never from @object-ui/types/zod:

call sitegenerates from
metadata-admin/view-schema.ts · page-schema.ts · dashboard-schema.ts · report-schema.ts@objectstack/spec/ui
metadata-admin/package-schema.ts@objectstack/spec/kernel

The complete set of toJSONSchema inputs is DashboardSchema, ListColumnSchema, ManifestSchema, PageSchema, ReportSchema, ViewSchema — none from these mirrors; the only importers of the three schemas are import type plus this PR's own pin. No baselined artifact carried the old text either: a repo-wide grep for the three old describe() strings returns nothing but this changeset's own quotation of one, with the new text as the hit-control. The claim named a consumer that does not exist, so it is gone. The changeset now states only the two surfaces that do change.

Generated by Claude Code


Generated by Claude Code

…in them live
`ViewSwitcherSchema.onViewChange`, `FilterUISchema.onChange` and
`SortUISchema.onChange` were described as "change callback" on both the zod
mirror and the TS interface. Measured, they are none of the three things that
wording suggests: the authored string is the NAME of a `CustomEvent` the
renderer dispatches on `window` —
ViewSwitcher.tsx:249-255 new CustomEvent(schema.onViewChange, { detail: { view } })
FilterUI.tsx:99-105 new CustomEvent(schema.onChange, { detail: { values } })
SortUI.tsx:93-99 new CustomEvent(schema.onChange, { detail: { sort } })
i.e. live, JSON-authorable capabilities. The mislabel is not cosmetic: a
handler-key census that buckets BY ZOD TYPE cannot tell an event NAME from the
handler-EXPRESSION dialect this repo does not support, and on that reading all
three were swept in for retirement — which would have deleted working
behaviour. The dual channel is what hides it: `onViewChange?.(next)` one line
above is the REACT PROP, while `schema.onViewChange` is the authored string.
Same type (`z.string()` / `string`), so no accept set moves. What changes is
that the declaration now says which channel it is, and a pin holds it:
half 1 the key is in the mirror's `shape`. Deliberately NOT a `safeParse`:
`BaseSchema` is `.passthrough()`, so a retired key still parses GREEN
and the output still CARRIES the value — a parse-based pin would stay
green through the very deletion it exists to catch.
half 2 the authored string reaches `new CustomEvent(...)` on `window`.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013hfmP9hoMd3dJwTh85J4yB
… `any`)
The pin landed with seven `@typescript-eslint/no-explicit-any` warnings. Typed
against the real contracts instead: the TS `ViewSwitcherSchema` /
`FilterUISchema` / `SortUISchema` for the authored nodes (imported `as type`,
aliasing the zod mirrors to `*Mirror` so the two faces stay distinguishable),
and a generic `captureWindowEvents<D>` that returns typed `CustomEvent` details.
Typing the node literals is not cosmetic here: it means the pin now also fails
if one of these keys leaves the TS face, which is the other half of the surface
a retirement would touch.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013hfmP9hoMd3dJwTh85J4yB
Required by `changeset-presence.yml`: published source of `@object-ui/types`
changed. Patch, never major (AGENTS.md — a `major` in the fixed group pushes
all 39 packages off objectstack's cadence).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013hfmP9hoMd3dJwTh85J4yB
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 45 chunks)3177.9 KB3222.7 KB
Main entry chunk (gzip)143.6 KB350 KB
Entry fileindex-DhdAhpfP.js
StatusPASS

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

PackageSizeGzipped
app-shell (consoleActionDispatch.js)0.20KB0.19KB
app-shell (index.js)12.46KB4.71KB
app-shell (runtime-config.js)20.61KB7.35KB
app-shell (types.js)0.01KB0.04KB
app-shell (urlParams.js)10.06KB3.86KB
auth (ActiveOrganizationStorage.js)25.05KB9.16KB
auth (AuthContext.js)0.31KB0.24KB
auth (AuthGuard.js)2.07KB1.00KB
auth (AuthProvider.js)40.18KB10.59KB
auth (AuthShell.js)3.49KB1.40KB
auth (ForgotPasswordForm.js)12.21KB3.45KB
auth (LoginForm.js)18.15KB5.39KB
auth (PreviewBanner.js)0.90KB0.50KB
auth (RegisterForm.js)6.65KB2.22KB
auth (SocialSignInButtons.js)9.61KB3.89KB
auth (UserMenu.js)3.41KB1.23KB
auth (auth-gate-events.js)1.29KB0.66KB
auth (authStyles.js)5.04KB1.72KB
auth (createAuthClient.js)40.21KB10.80KB
auth (createAuthenticatedFetch.js)8.46KB3.43KB
auth (index.js)3.19KB1.44KB
auth (invitation-status.js)1.22KB0.70KB
auth (org-roles.js)6.66KB2.78KB
auth (phone-identifier.js)1.11KB0.66KB
auth (types.js)0.59KB0.35KB
auth (useAuth.js)5.30KB1.02KB
auth (useWorkspaceAdminStatus.js)5.13KB2.35KB
collaboration (CommentThread.js)26.08KB7.56KB
collaboration (LiveCursors.js)3.17KB1.27KB
collaboration (PresenceAvatars.js)6.49KB2.64KB
collaboration (PresenceProvider.js)2.79KB1.13KB
collaboration (index.js)1.68KB0.73KB
collaboration (useCollaborationTranslation.js)6.05KB2.52KB
collaboration (useCommentSearch.js)1.98KB0.88KB
collaboration (useConflictResolution.js)7.75KB1.86KB
collaboration (useMentionNotifications.js)1.81KB0.68KB
collaboration (usePresence.js)6.33KB1.84KB
collaboration (useRealtimeSubscription.js)7.91KB2.01KB
components (index.js)512.13KB116.43KB
core (index.js)5.30KB2.13KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)174.02KB48.28KB
fields (index.js)243.65KB61.63KB
i18n (LocalizationContext.js)1.76KB0.96KB
i18n (currency.js)1.22KB0.64KB
i18n (fallbackInterpolation.js)6.25KB2.77KB
i18n (i18n.js)4.28KB1.75KB
i18n (index.js)3.44KB1.39KB
i18n (pickLocalized.js)7.62KB3.26KB
i18n (provider.js)26.89KB9.04KB
i18n (useDisplayLocale.js)2.85KB1.45KB
i18n (useObjectLabel.js)33.40KB8.71KB
i18n (useSafeTranslation.js)5.60KB2.33KB
layout (index.js)38.95KB10.97KB
mobile (MobileProvider.js)0.92KB0.49KB
mobile (ResponsiveContainer.js)0.94KB0.38KB
mobile (breakpoints.js)1.51KB0.70KB
mobile (createOfflineDataSource.js)5.61KB1.75KB
mobile (index.js)1.55KB0.62KB
mobile (offlineQueue.js)3.91KB1.35KB
mobile (pwa.js)0.97KB0.49KB
mobile (serviceWorker.js)1.48KB0.62KB
mobile (serviceWorkerSource.js)3.41KB1.48KB
mobile (useBreakpoint.js)1.54KB0.65KB
mobile (useGesture.js)6.96KB1.98KB
mobile (useOfflineSync.js)1.99KB0.72KB
mobile (usePullToRefresh.js)2.53KB0.85KB
mobile (useResponsive.js)0.72KB0.42KB
mobile (useResponsiveConfig.js)1.37KB0.63KB
mobile (useSpecGesture.js)4.32KB1.64KB
mobile (useTouchTarget.js)1.01KB0.54KB
permissions (MePermissionsProvider.js)11.71KB4.29KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)6.24KB2.16KB
permissions (discardProofCache.js)1.04KB0.55KB
permissions (evaluator.js)5.12KB1.74KB
permissions (index.js)0.93KB0.41KB
permissions (store.js)0.91KB0.42KB
permissions (useFieldPermissions.js)1.28KB0.53KB
permissions (usePermissions.js)4.83KB2.27KB
plugin-ai (index.js)15.75KB3.80KB
plugin-calendar (index.js)46.92KB12.93KB
plugin-charts (index.js)64.68KB18.35KB
plugin-chatbot (index.js)190.53KB45.18KB
plugin-dashboard (index.js)133.48KB34.51KB
plugin-designer (index.js)212.87KB43.19KB
plugin-detail (index.js)245.43KB62.46KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)133.32KB32.69KB
plugin-gantt (index.js)165.23KB40.37KB
plugin-grid (index.js)201.69KB54.58KB
plugin-kanban (index.js)53.14KB14.64KB
plugin-list (index.js)113.15KB27.59KB
plugin-map (index.js)20.20KB6.66KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)43.51KB11.94KB
plugin-timeline (index.js)28.95KB8.33KB
plugin-tree (index.js)9.00KB3.08KB
plugin-view (index.js)85.83KB21.11KB
providers (DataSourceProvider.js)0.75KB0.39KB
providers (MetadataProvider.js)1.37KB0.59KB
providers (ThemeProvider.js)1.90KB0.85KB
providers (UploadProvider.js)11.66KB3.50KB
providers (index.js)0.45KB0.23KB
providers (types.js)0.01KB0.04KB
react-runtime (index.js)5.62KB2.34KB
react (LazyPluginLoader.js)4.47KB1.63KB
react (SchemaRenderer.js)76.75KB25.49KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)3.11KB1.48KB
react (schema-input.js)2.32KB1.24KB
react (spec-input.js)0.20KB0.18KB
sdui-parser (codegen.js)5.41KB2.34KB
sdui-parser (dashboard-widget-options.js)3.08KB1.30KB
sdui-parser (index.js)4.93KB2.24KB
sdui-parser (input-type.js)2.84KB1.40KB
sdui-parser (parse.js)20.57KB5.88KB
sdui-parser (provenance.js)3.66KB1.82KB
sdui-parser (types.js)0.28KB0.23KB
sdui-parser (validate.js)10.35KB3.60KB
types (ai.js)0.20KB0.17KB
types (api-types.js)0.20KB0.18KB
types (app.js)2.87KB0.99KB
types (base.js)0.20KB0.18KB
types (blocks.js)0.20KB0.18KB
types (complex.js)2.74KB1.41KB
types (crud.js)0.20KB0.18KB
types (dashboard-filter-alias.js)6.23KB2.74KB
types (data-display.js)3.75KB1.85KB
types (data-protocol.js)0.20KB0.19KB
types (data.js)0.20KB0.18KB
types (designer.js)1.85KB0.85KB
types (disclosure.js)0.20KB0.18KB
types (error-code.js)1.54KB0.88KB
types (feedback.js)0.20KB0.18KB
types (field-types.js)0.20KB0.18KB
types (form.js)0.20KB0.18KB
types (http-inflight.js)8.87KB3.73KB
types (http-retry.js)4.32KB2.02KB
types (icon-key-migration.js)4.26KB1.63KB
types (index.js)4.72KB2.24KB
types (layout.js)0.20KB0.18KB
types (managed-by.js)0.19KB0.18KB
types (mobile.js)2.59KB1.31KB
types (navigation.js)0.20KB0.18KB
types (objectql.js)0.20KB0.18KB
types (overlay.js)0.20KB0.18KB
types (permissions.js)0.20KB0.18KB
types (plugin-scope.js)0.20KB0.18KB
types (record-components.js)0.20KB0.19KB
types (record-semantics.js)1.28KB0.67KB
types (registry.js)0.20KB0.18KB
types (reports.js)0.20KB0.18KB
types (spec-report.js)5.05KB1.93KB
types (spec-ui-namespace.js)0.20KB0.19KB
types (system-fields.js)3.33KB1.54KB
types (theme.js)6.28KB2.87KB
types (ui-action.js)3.40KB1.71KB
types (views.js)0.20KB0.18KB
types (widget.js)0.20KB0.18KB

Size Limits

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

@os-samClaude

Copy link
Copy Markdown
Collaborator

Clause ② review — CONTRACT_REVIEW_TIER — verdict: ACCEPT WITH FOLLOW-UP

Reviewed at head 73942c744 in a fresh detached read-only worktree (the branch was not touched; mutation legs below were applied and restored inside that worktree, restore verified by clean git status and an empty git diff HEAD). Clause ② is a floor, never a clearance.

The central claim — verified, not accepted

No accept set moves. Re-read from the diff itself (three-dot vs origin/mainfab4802e3, merge-base 05b8324): exactly 4 files, +231/−6, matching the PR header. On all three keys the zod arm stays z.string().optional() and the TS property stays ?: string — the only changes are .describe() text (an annotation; it cannot move an accept set), JSDoc, the new pin test, and the changeset. No other file in the diff touches a validator.

The three keys are event names — verified at head AND at current origin/main:

  • ViewSwitcher.tsx:251new CustomEvent(schema.onViewChange, { detail: { view } })
  • FilterUI.tsx:101new CustomEvent(schema.onChange, { detail: { values } })
  • SortUI.tsx:95new CustomEvent(schema.onChange, { detail: { sort } })

all dispatched on window, all within the line ranges the PR states, and the detail payload keys match the new describe() text exactly. The dual channel is real: in all three components the React prop call (onViewChange?.(next) / onChange?.(…)) sits immediately above the schema.<key> string dispatch — same name, two channels, which substantiates the PR's explanation of the census mislabel.

The pin — design reasoning re-demonstrated first-hand

  • BaseSchema is .passthrough() (base.zod.ts:211) — confirmed.
  • Pin at head: 8/8 pass, in a worktree with no packages/types/dist — the control that it resolves source, not a build artifact.
  • Ablation re-run (retire the two onViewChange mirror lines): 2 failed / 6 passed, the failures being exactly ViewSwitcherSchema declares onViewChange and the describe() text names the EVENT-NAME channel — and all three LIVE runtime tests stayed GREEN, reproducing the PR's load-bearing result: the runtime keeps working while the authorable surface silently loses the key.
  • Passthrough hazard probed directly: with the key retired (control: key absent from shape), safeParse still returned success: trueand the parsed output carried the authored value. So a safeParse-based DECLARED half would indeed stay green through the very deletion the pin exists to catch. The stated reason for reading shape instead of parsing holds.
  • The pin's own internal controls are sound: the string-arm test includes a refusal control (a function fails parse), so the instrument can say no.

Re-run vs taken on report

Re-ran at head: the 4-file diff and its stat; the three dispatch sites (head and fab4802e3); the dual channel; .passthrough(); pin 8/8; the full ablation and restore; the passthrough probe; the two-package suite — 101 files / 1116 tests, all pass, matching the PR's "after" figures exactly; pnpm --filter @object-ui/types type-check — exit 0 with the real three-legged tsc command visible; a repo-wide grep for the old describe strings — zero carriers, with the new text as the hit-control.

Taken on report: the "before 100 files / 1108 tests" baseline; the escalation section's measurements (census figures, bucket-1/2/3 legs, corpus recount, lint narrowing, os-verify-lock protocol). These justify the escalation, not the shipped diff.

Scope discipline — verified

The diff implements no part of the escalated conversion ruling: no bucket-1/2/3 code, no ledger or registry edits, no content/docs/releases/ changes, nothing beyond the two type files, the pin, and the changeset. Main-side drift since merge-base (reports.ts, data-display.*) has zero file overlap with this PR.

Changeset — read as a release-notes reader

@object-ui/types: patch is the right level: nothing an author wrote breaks, and both changeset CI gates agree. The example payload and listener match the verified runtime behavior (e.detail.sort etc.). No direction-of-change claim is false and nothing reads more additive than it is.

CI

At dispatch CI was not terminal (5 checks including Type Check in progress) and nothing here rested on that reading. At my own read during review the check-runs table on 73942c744 was terminal: 30 total = 27 success + 3 skipped + 0 failed + 0 in progress. The 3 skipped (Test (coverage) ×2, dependabot) are conditional no-readings, not greens. The branch is behind main; the eventual update re-runs the gates — CI gating applies as usual, and nothing in this verdict substitutes for it.

Follow-ups (named, non-blocking, both fixable in this PR)

  1. PR body census erratum — body edit only. The body says "61 call sites"; the PM-side re-derivation on fab4802e3 reads 60 anchored key: z.function( sites, the 61st occurrence being EventHandlersSchema (base.zod.ts:349) — a record-valued declaration, not a named key, exempted from the parity ledger as an index signature. This does not bear on the three z.string() keys in this diff, but the number ships in the merge commit; a one-line body correction before undraft is cheap. (Already recorded on the card.)
  2. Wording precision in the changeset/body, optional. "the generated JSON Schema description" — verified: no in-repo z.toJSONSchema call consumes these three mirrors today (metadata-admin generates from @objectstack/spec/ui), and no baselined artifact carried the old text. The claim is true for any consumer generating from the published mirrors, but the definite article implies an existing in-repo artifact. One-word softening ("any generated JSON Schema description") would make it exact. Cosmetic; not a blocker.

Standing

The PR stays draft and is not mine to close, mark ready, or merge. This verdict is the clause-② floor; maintainer disposition of the escalated conversion questions is untouched by it.


Generated by Claude Code

…felt on
The changeset claimed the correction is felt in "the generated JSON Schema
description". Measured, and it is not: every `z.toJSONSchema` call in this repo
generates from `@objectstack/spec` —
view-schema.ts / page-schema.ts / dashboard-schema.ts / report-schema.ts
-> @objectstack/spec/ui
package-schema.ts
-> @objectstack/spec/kernel
— and none from `@object-ui/types/zod`. The only importers of the three
schemas are `import type` (the TS face) plus this PR's own pin. No baselined
artifact carried the old `describe()` text either: a repo-wide grep for the
three old strings returns nothing but this changeset's own quotation of one,
with the new text as the hit-control.
So the claim named a consumer that does not exist. Replaced with the two
surfaces that do change and that an author can actually read: the mirror's
`describe()` text and the TS JSDoc.
This matters more than its size: the changeset is the only artifact here that
ships to people who never read the PR thread, and the only one written before
review is complete.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013hfmP9hoMd3dJwTh85J4yB
@claude

claudeBot commented Aug 30, 2026

Copy link
Copy Markdown
ContributorAuthor

Follow-up round addressed — head abd73c18f

Both named follow-ups fixed, branch updated. ⛔ No part of the escalated Q1–Q4 decision was implemented, and the PR stays draft.

1. Census erratum — verified independently before changing the number

Anchored on the merged tree, packages/types/src/zod/:

RAW z.function( occurrences: 61
ANCHORED named-key sites: 60
DISTINCT keys: 29
FILES: 9

and the raw-minus-anchored residue is exactly one line:

base.zod.ts:349
export const EventHandlersSchema = z.record(z.string(), z.function()).describe('Event handlers');

60 is right; my 61 was the raw occurrence count mislabelled as call sites. 29 and 9 unchanged. Body corrected, with the distinction spelled out so the conflation is not repeated. ⛔ EventHandlersSchema kept out of scope.

⚠️ One correction to my own earlier report while I was here: I had said onSend "landed since the previous census's merge-base". This checkout is shallow (19 commits, all dated 2026-08-30), so git log -S returns the oldest available commit, not the introducing one — it cannot date anything. The onSend finding does not depend on that: it is a set difference against the 28 keys the card body itself enumerates (card list 28, today 29, present today / absent from card = ['onSend'], absent today = []). The body now says so and asserts no introduction date.

2. The JSON Schema claim — measured, and the reviewer's negative reproduced

Every z.toJSONSchema call in the repo generates from @objectstack/spec, none from @object-ui/types/zod:

call sitegenerates from
metadata-admin/view-schema.ts · page-schema.ts · dashboard-schema.ts · report-schema.ts@objectstack/spec/ui
metadata-admin/package-schema.ts@objectstack/spec/kernel

Complete set of toJSONSchema inputs: DashboardSchema, ListColumnSchema, ManifestSchema, PageSchema, ReportSchema, ViewSchema. The only importers of the three schemas are import type plus this PR's own pin. Repo-wide grep for the three old describe() strings: no carrier anywhere except this changeset's own quotation of one, with the new text as the hit-control.

⇒ the claim named a consumer that does not exist. Changeset and body now state only the two surfaces that do change — the mirror's describe() text and the TS JSDoc. Taking the point that the changeset is the artifact that ships to people who never read this thread, that is where the fix landed first, in its own commit.

3. Branch update

Merged origin/mainfab4802e3 (repo convention for a branch I created — no rebase, no force-push). Clean, no conflicts; mergeable: true, and 0 commits in origin/main are missing from the branch. My three-dot diff vs merged main is still exactly 4 files, +232/−6.

Verification on the merged head

  • Suites, repo-root invocation pnpm exec vitest run packages/types/ packages/plugin-view/102 files / 1122 tests, all passed. VERDICT command-exit 0. The +1 file / +6 tests versus this PR's pre-merge 101/1116 is main's, attributed not assumed: main's new report-schema-authoring-face.test.ts plus this PR's pin run as 2 files / 14 tests, and the pin is 8 of them.
  • Type-checkpnpm --filter @object-ui/types type-check, echoed as tsc --noEmit && tsc -p tsconfig.examples.json && tsc -p tsconfig.test.json. VERDICT command-exit 0.
  • check:control-bytes OK (5752 tracked text files) · check:vi-mock-inherit OK (108 call sites, 108 inherit, 0 auto-mocked) · check:vi-mock-specifiers OK.
  • check-changeset-presence OK against the new merge-base fab4802e3 — "3 source file(s) of 2 released package(s) changed, and this change declares 1 changeset(s)". check-changeset-no-major OK.
  • Lint population 3 files, 0 errors; my two authored files 0 warnings; views.ts's 8 warnings pre-existing.
  • This round changed no source file. I authored exactly one commit (017613ecd), touching only the changeset (+3/−2); non-changeset files touched by me this round = 0. The other commits in the range arrived with the merge.

Generated by Claude Code


Generated by Claude Code

@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 45 chunks)3178.0 KB3222.7 KB
Main entry chunk (gzip)143.6 KB350 KB
Entry fileindex-BBahf6Vp.js
StatusPASS

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

PackageSizeGzipped
app-shell (consoleActionDispatch.js)0.20KB0.19KB
app-shell (index.js)12.46KB4.71KB
app-shell (runtime-config.js)20.61KB7.35KB
app-shell (types.js)0.01KB0.04KB
app-shell (urlParams.js)10.06KB3.86KB
auth (ActiveOrganizationStorage.js)25.05KB9.16KB
auth (AuthContext.js)0.31KB0.24KB
auth (AuthGuard.js)2.07KB1.00KB
auth (AuthProvider.js)40.18KB10.59KB
auth (AuthShell.js)3.49KB1.40KB
auth (ForgotPasswordForm.js)12.21KB3.45KB
auth (LoginForm.js)18.15KB5.39KB
auth (PreviewBanner.js)0.90KB0.50KB
auth (RegisterForm.js)6.65KB2.22KB
auth (SocialSignInButtons.js)9.61KB3.89KB
auth (UserMenu.js)3.41KB1.23KB
auth (auth-gate-events.js)1.29KB0.66KB
auth (authStyles.js)5.04KB1.72KB
auth (createAuthClient.js)40.21KB10.80KB
auth (createAuthenticatedFetch.js)8.46KB3.43KB
auth (index.js)3.19KB1.44KB
auth (invitation-status.js)1.22KB0.70KB
auth (org-roles.js)6.66KB2.78KB
auth (phone-identifier.js)1.11KB0.66KB
auth (types.js)0.59KB0.35KB
auth (useAuth.js)5.30KB1.02KB
auth (useWorkspaceAdminStatus.js)5.13KB2.35KB
collaboration (CommentThread.js)26.08KB7.56KB
collaboration (LiveCursors.js)3.17KB1.27KB
collaboration (PresenceAvatars.js)6.49KB2.64KB
collaboration (PresenceProvider.js)2.79KB1.13KB
collaboration (index.js)1.68KB0.73KB
collaboration (useCollaborationTranslation.js)6.05KB2.52KB
collaboration (useCommentSearch.js)1.98KB0.88KB
collaboration (useConflictResolution.js)7.75KB1.86KB
collaboration (useMentionNotifications.js)1.81KB0.68KB
collaboration (usePresence.js)6.33KB1.84KB
collaboration (useRealtimeSubscription.js)7.91KB2.01KB
components (index.js)512.13KB116.43KB
core (index.js)5.30KB2.13KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)174.02KB48.28KB
fields (index.js)243.65KB61.63KB
i18n (LocalizationContext.js)1.76KB0.96KB
i18n (currency.js)1.22KB0.64KB
i18n (fallbackInterpolation.js)6.25KB2.77KB
i18n (i18n.js)4.28KB1.75KB
i18n (index.js)3.44KB1.39KB
i18n (pickLocalized.js)7.62KB3.26KB
i18n (provider.js)26.89KB9.04KB
i18n (useDisplayLocale.js)2.85KB1.45KB
i18n (useObjectLabel.js)33.40KB8.71KB
i18n (useSafeTranslation.js)5.60KB2.33KB
layout (index.js)38.95KB10.97KB
mobile (MobileProvider.js)0.92KB0.49KB
mobile (ResponsiveContainer.js)0.94KB0.38KB
mobile (breakpoints.js)1.51KB0.70KB
mobile (createOfflineDataSource.js)5.61KB1.75KB
mobile (index.js)1.55KB0.62KB
mobile (offlineQueue.js)3.91KB1.35KB
mobile (pwa.js)0.97KB0.49KB
mobile (serviceWorker.js)1.48KB0.62KB
mobile (serviceWorkerSource.js)3.41KB1.48KB
mobile (useBreakpoint.js)1.54KB0.65KB
mobile (useGesture.js)6.96KB1.98KB
mobile (useOfflineSync.js)1.99KB0.72KB
mobile (usePullToRefresh.js)2.53KB0.85KB
mobile (useResponsive.js)0.72KB0.42KB
mobile (useResponsiveConfig.js)1.37KB0.63KB
mobile (useSpecGesture.js)4.32KB1.64KB
mobile (useTouchTarget.js)1.01KB0.54KB
permissions (MePermissionsProvider.js)11.71KB4.29KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)6.24KB2.16KB
permissions (discardProofCache.js)1.04KB0.55KB
permissions (evaluator.js)5.12KB1.74KB
permissions (index.js)0.93KB0.41KB
permissions (store.js)0.91KB0.42KB
permissions (useFieldPermissions.js)1.28KB0.53KB
permissions (usePermissions.js)4.83KB2.27KB
plugin-ai (index.js)15.75KB3.80KB
plugin-calendar (index.js)46.92KB12.93KB
plugin-charts (index.js)64.68KB18.35KB
plugin-chatbot (index.js)190.53KB45.18KB
plugin-dashboard (index.js)133.48KB34.51KB
plugin-designer (index.js)212.87KB43.19KB
plugin-detail (index.js)245.43KB62.46KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)133.32KB32.69KB
plugin-gantt (index.js)165.23KB40.37KB
plugin-grid (index.js)202.08KB54.61KB
plugin-kanban (index.js)53.14KB14.64KB
plugin-list (index.js)113.15KB27.59KB
plugin-map (index.js)20.20KB6.66KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)43.51KB11.94KB
plugin-timeline (index.js)28.95KB8.33KB
plugin-tree (index.js)9.00KB3.08KB
plugin-view (index.js)85.83KB21.11KB
providers (DataSourceProvider.js)0.75KB0.39KB
providers (MetadataProvider.js)1.37KB0.59KB
providers (ThemeProvider.js)1.90KB0.85KB
providers (UploadProvider.js)11.66KB3.50KB
providers (index.js)0.45KB0.23KB
providers (types.js)0.01KB0.04KB
react-runtime (index.js)5.62KB2.34KB
react (LazyPluginLoader.js)4.47KB1.63KB
react (SchemaRenderer.js)76.75KB25.49KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)3.11KB1.48KB
react (schema-input.js)2.32KB1.24KB
react (spec-input.js)0.20KB0.18KB
sdui-parser (codegen.js)5.41KB2.34KB
sdui-parser (dashboard-widget-options.js)3.08KB1.30KB
sdui-parser (index.js)4.93KB2.24KB
sdui-parser (input-type.js)2.84KB1.40KB
sdui-parser (parse.js)20.57KB5.88KB
sdui-parser (provenance.js)3.66KB1.82KB
sdui-parser (types.js)0.28KB0.23KB
sdui-parser (validate.js)10.35KB3.60KB
types (ai.js)0.20KB0.17KB
types (api-types.js)0.20KB0.18KB
types (app.js)2.87KB0.99KB
types (base.js)0.20KB0.18KB
types (blocks.js)0.20KB0.18KB
types (complex.js)2.74KB1.41KB
types (crud.js)0.20KB0.18KB
types (dashboard-filter-alias.js)6.23KB2.74KB
types (data-display.js)3.75KB1.85KB
types (data-protocol.js)0.20KB0.19KB
types (data.js)0.20KB0.18KB
types (designer.js)1.85KB0.85KB
types (disclosure.js)0.20KB0.18KB
types (error-code.js)1.54KB0.88KB
types (feedback.js)0.20KB0.18KB
types (field-types.js)0.20KB0.18KB
types (form.js)0.20KB0.18KB
types (http-inflight.js)8.87KB3.73KB
types (http-retry.js)4.32KB2.02KB
types (icon-key-migration.js)4.26KB1.63KB
types (index.js)4.72KB2.24KB
types (layout.js)0.20KB0.18KB
types (managed-by.js)0.19KB0.18KB
types (mobile.js)2.59KB1.31KB
types (navigation.js)0.20KB0.18KB
types (objectql.js)0.20KB0.18KB
types (overlay.js)0.20KB0.18KB
types (permissions.js)0.20KB0.18KB
types (plugin-scope.js)0.20KB0.18KB
types (record-components.js)0.20KB0.19KB
types (record-semantics.js)1.28KB0.67KB
types (registry.js)0.20KB0.18KB
types (reports.js)0.20KB0.18KB
types (spec-report.js)5.05KB1.93KB
types (spec-ui-namespace.js)0.20KB0.19KB
types (system-fields.js)3.33KB1.54KB
types (theme.js)6.28KB2.87KB
types (ui-action.js)3.40KB1.71KB
types (views.js)0.20KB0.18KB
types (widget.js)0.20KB0.18KB

Size Limits

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

@os-samClaude

Copy link
Copy Markdown
Collaborator

Clause ② reviewer — follow-up discharge confirmed

Both follow-ups from my ACCEPT WITH FOLLOW-UP verdict are discharged, verified at head abd73c18f (merge of origin/mainfab4802e3 — no force-push, my reviewed 73942c744 intact in history — plus one authored changeset-only commit 017613ecd, +3/−2).

Follow-up 1 (census erratum) — discharged. The body now states 29 keys / 60 anchored named-key call sites / 9 files, with an explicit erratum block: 61 was the raw z.function( occurrence count, the residue is EventHandlersSchema (base.zod.ts:349), record-valued and deliberately out of scope. Read directly from the current body. The additional voluntary downgrade of the onSend dating claim to a set-difference (shallow clone cannot date an introduction) is an evidence-quality improvement beyond what was asked.

Follow-up 2 (JSON Schema wording) — discharged, and upgraded from softening to removal. Verified in the commit diff: the changeset now names only the two surfaces that change — the mirror's describe() text and the TS JSDoc. Grep at the new head finds no residual "JSON Schema" mention in the changeset (the old text as prior hit-control). The body carries the same correction with the measurement table; its findings match my own first-pass measurement (all z.toJSONSchema call sites generate from @objectstack/spec, none from these mirrors, no baselined carrier of the old text). Removal is the more accurate fix than the softening I suggested.

Re-verified at the merged head, first-hand: the PR diff vs current main dbd519450 is still exactly the same 4 files (+232/−6, the +1 being the changeset edit — no scope creep from the merge); and the two-package suite at abd73c18f gives 102 files / 1122 tests, all pass, matching the body's reported figures, with the +1 file / +6 tests attributable to main's report-schema-authoring-face.test.ts rather than this PR.

Drift re-check at the new base:fab4802e3..dbd519450 touches 16 files (issue-6730 app-shell activity work, i18n locales, one changeset) — zero overlap with this PR's four files, control being the full drift file list from the same query. My zero-overlap assessment stands; the branch being behind again is routine.

Taken on report: the terminal CI reading at abd73c18f (30 = 27 success + 3 skipped as no-readings + 0 failed/in-progress), per the PM's re-read of the check-runs table. CI gating on the eventual up-to-date merge applies as usual; nothing here substitutes for it.

Nothing is outstanding from the clause-② review. The PR stays draft — not mine to close or mark ready; card #6124 remains needs-user-decision on Q1–Q4, untouched by this PR.


Generated by Claude Code

@os-sam
os-sam marked this pull request as ready for review August 30, 2026 14:16
@os-sam
os-sam added this pull request to the merge queueAug 30, 2026
Merged via the queue into main with commit 905b21fAug 30, 2026
32 checks passed
@os-sam
os-sam deleted the claude/issue-6124-callables-to-json-conversion branch August 30, 2026 14:29
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@os-sam@claude