From 63d433358fa20e9b64b4b7108c753c76c7d934b5 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 30 Aug 2026 13:21:56 +0000 Subject: [PATCH 1/4] fix(types): declare the three view handler keys as EVENT NAMES, and pin them live MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `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 Claude-Session: https://claude.ai/code/session_013hfmP9hoMd3dJwTh85J4yB --- .../handlerEventNameLiveness.6124.test.tsx | 165 ++++++++++++++++++ packages/types/src/views.ts | 15 +- packages/types/src/zod/views.zod.ts | 9 +- 3 files changed, 183 insertions(+), 6 deletions(-) create mode 100644 packages/plugin-view/src/__tests__/handlerEventNameLiveness.6124.test.tsx diff --git a/packages/plugin-view/src/__tests__/handlerEventNameLiveness.6124.test.tsx b/packages/plugin-view/src/__tests__/handlerEventNameLiveness.6124.test.tsx new file mode 100644 index 0000000000..f6fa7a8f51 --- /dev/null +++ b/packages/plugin-view/src/__tests__/handlerEventNameLiveness.6124.test.tsx @@ -0,0 +1,165 @@ +/** + * ObjectUI + * Copyright (c) 2024-present ObjectStack Inc. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +/** + * objectui#6124 — the three `on*` mirrors in `views.zod.ts` that a handler-key + * census reads as dead callbacks and are in fact LIVE, JSON-authorable + * capabilities. + * + * ## What was measured, and why a pin exists at all + * + * #6182's census bucketed handler keys BY ZOD TYPE, which cannot tell a + * "handler expression" (a dialect this repo does not support — measurably + * dropped at runtime, #4453) from an EVENT NAME (a string the renderer + * dispatches). Three rows are the second kind, and the scope extension that + * swept them in for retirement would have DELETED WORKING BEHAVIOUR: + * + * - `ViewSwitcherSchema.onViewChange` — `ViewSwitcher.tsx:249-255` + * - `FilterUISchema.onChange` — `FilterUI.tsx:99-105` + * - `SortUISchema.onChange` — `SortUI.tsx:93-99` + * + * each `window.dispatchEvent(new CustomEvent(schema., { detail }))`, i.e. + * the AUTHORED STRING IS THE EVENT NAME. Note the dual channel that produced + * the mislabel: `onViewChange?.(next)` right above it is the REACT PROP (a + * function a host passes), while `schema.onViewChange` is the authored string. + * Same name, two channels — which is exactly what a type-shaped census cannot + * see, and why the `.describe()` text on those three rows now says "event name" + * rather than "callback". + * + * ## Why BOTH halves, and why the declared half is not a `safeParse` + * + * A retirement of one of these keys has two independent failure surfaces, so + * the pin has two halves: + * + * 1. DECLARED — the key is in the mirror's `shape`. ⚠️ This deliberately does + * NOT assert via `safeParse`: `BaseSchema` is `.passthrough()`, so a + * retired key still PARSES GREEN and the parsed output still CARRIES the + * value (measured on #6124). A `safeParse`-based pin would therefore stay + * green through the very deletion it exists to catch — absence is not + * refusal here, so the pin has to read the DECLARATION. + * 2. LIVE — the authored string reaches `new CustomEvent(...)` on `window`. + * + * Either half alone is passable by a change that breaks the capability: half 1 + * alone allows the renderer's dispatch to be deleted; half 2 alone allows the + * key to leave the authorable surface while the runtime keeps working for React + * hosts only. + */ + +import { describe, it, expect, vi, afterEach } from 'vitest'; +import { render, screen, cleanup, fireEvent } from '@testing-library/react'; +import { ViewSwitcherSchema, FilterUISchema, SortUISchema } from '@object-ui/types/zod'; +import { ViewSwitcher } from '../ViewSwitcher'; +import { FilterUI } from '../FilterUI'; +import { SortUI } from '../SortUI'; + +vi.mock('@object-ui/react', async (importOriginal) => { + const React = await import('react'); + return { + ...(await importOriginal>()), + SchemaRenderer: ({ schema }: any) =>
, + SchemaRendererContext: React.createContext(null), + subscribeDataChanges: () => () => {}, + notifyDataChanged: () => {}, + }; +}); + +afterEach(() => cleanup()); + +/** Record every CustomEvent of `name` dispatched on `window` while `fn` runs. */ +function captureWindowEvents(name: string, fn: () => void): CustomEvent[] { + const seen: CustomEvent[] = []; + const listener = (e: Event) => seen.push(e as CustomEvent); + window.addEventListener(name, listener); + try { fn(); } finally { window.removeEventListener(name, listener); } + return seen; +} + +describe('#6124 half 1 — the three event-name keys are DECLARED on the authorable surface', () => { + // Reading `.shape`, not `.safeParse`: under `BaseSchema.passthrough()` a + // retired key still parses green, so only the declaration can report a + // deletion. Retiring any of these three turns exactly this half red. + it('ViewSwitcherSchema declares onViewChange', () => { + expect(Object.keys(ViewSwitcherSchema.shape)).toContain('onViewChange'); + }); + it('FilterUISchema declares onChange', () => { + expect(Object.keys(FilterUISchema.shape)).toContain('onChange'); + }); + it('SortUISchema declares onChange', () => { + expect(Object.keys(SortUISchema.shape)).toContain('onChange'); + }); + + it('each is a STRING mirror — an authored event name parses, a function does not', () => { + const base = { type: 'sort-ui', fields: [{ field: 'name' }] }; + expect(SortUISchema.safeParse({ ...base, onChange: 'sort:changed' }).success).toBe(true); + // control: the instrument can say no — the string arm is a real constraint + expect(SortUISchema.safeParse({ ...base, onChange: () => {} }).success).toBe(false); + }); + + it('the describe() text names the EVENT-NAME channel, not "callback"', () => { + // The mislabel is the measured root cause of the wrong bucketing, so the + // corrected wording is pinned rather than left to survive by luck. + for (const d of [ + ViewSwitcherSchema.shape.onViewChange.description, + FilterUISchema.shape.onChange.description, + SortUISchema.shape.onChange.description, + ]) { + // Names the real channel … + expect(d).toMatch(/event name/i); + // … and carries the DISCLAIMER, because "callback" in the old wording is + // what a type-shaped census read to mean "dead handler". The wording has + // to refuse that reading explicitly, not merely omit the word. + expect(d).toMatch(/not a callback or a handler expression/i); + } + }); +}); + +describe('#6124 half 2 — the authored string is LIVE: it is the CustomEvent name', () => { + it('ViewSwitcher dispatches the authored name on view change', () => { + const schema: any = { + type: 'view-switcher', variant: 'buttons', + views: [{ type: 'list' }, { type: 'grid' }], + onViewChange: 'zz6124:view', + }; + render(); + const events = captureWindowEvents('zz6124:view', () => { + fireEvent.click(screen.getByRole('button', { name: /grid/i })); + }); + expect(events).toHaveLength(1); + expect((events[0].detail as any).view).toBe('grid'); + }); + + it('FilterUI dispatches the authored name on filter change', () => { + const schema: any = { + type: 'filter-ui', layout: 'inline', + filters: [{ field: 'qty', label: 'Qty', type: 'number' }], + onChange: 'zz6124:filter', + }; + const { container } = render(); + const input = container.querySelector('input[type="number"]') as HTMLInputElement; + expect(input).toBeTruthy(); + const events = captureWindowEvents('zz6124:filter', () => { + fireEvent.change(input, { target: { value: '7' } }); + }); + expect(events).toHaveLength(1); + expect((events[0].detail as any).values.qty).toBe(7); + }); + + it('SortUI dispatches the authored name on sort change', () => { + const schema: any = { + type: 'sort-ui', variant: 'buttons', + fields: [{ field: 'name', label: 'Name' }], + onChange: 'zz6124:sort', + }; + render(); + const events = captureWindowEvents('zz6124:sort', () => { + fireEvent.click(screen.getByRole('button', { name: /name/i })); + }); + expect(events).toHaveLength(1); + expect((events[0].detail as any).sort[0].field).toBe('name'); + }); +}); diff --git a/packages/types/src/views.ts b/packages/types/src/views.ts index 9c9f3f4fd5..e419576326 100644 --- a/packages/types/src/views.ts +++ b/packages/types/src/views.ts @@ -756,7 +756,10 @@ export interface ViewSwitcherSchema extends BaseSchema { */ position?: 'top' | 'bottom' | 'left' | 'right'; /** - * View change callback + * Event name dispatched on `window` when the view changes + * (`detail: { view }`) — an event NAME, not a callback or a handler + * expression. Read at `plugin-view/src/ViewSwitcher.tsx` as + * `new CustomEvent(schema.onViewChange, …)` (objectui#6124). */ onViewChange?: string; /** @@ -820,7 +823,10 @@ export interface FilterUISchema extends BaseSchema { */ values?: Record; /** - * Filter change callback + * Event name dispatched on `window` when the filters change + * (`detail: { values }`) — an event NAME, not a callback or a handler + * expression. Read at `plugin-view/src/FilterUI.tsx` as + * `new CustomEvent(schema.onChange, …)` (objectui#6124). */ onChange?: string; /** @@ -870,7 +876,10 @@ export interface SortUISchema extends BaseSchema { direction: 'asc' | 'desc'; }>; /** - * Sort change callback + * Event name dispatched on `window` when the sort changes + * (`detail: { sort }`) — an event NAME, not a callback or a handler + * expression. Read at `plugin-view/src/SortUI.tsx` as + * `new CustomEvent(schema.onChange, …)` (objectui#6124). */ onChange?: string; /** diff --git a/packages/types/src/zod/views.zod.ts b/packages/types/src/zod/views.zod.ts index febc03737b..60940616ee 100644 --- a/packages/types/src/zod/views.zod.ts +++ b/packages/types/src/zod/views.zod.ts @@ -136,7 +136,8 @@ export const ViewSwitcherSchema = BaseSchema.extend({ activeView: ViewTypeSchema.optional().describe('Current active view'), variant: z.enum(['tabs', 'buttons', 'dropdown']).optional().describe('Switcher variant'), position: z.enum(['top', 'bottom', 'left', 'right']).optional().describe('Switcher position'), - onViewChange: z.string().optional().describe('View change callback'), + onViewChange: z.string().optional() + .describe('Event name dispatched on window when the view changes (detail: { view }) — an event NAME, not a callback or a handler expression'), persistPreference: z.boolean().optional().describe('Persist view preference'), storageKey: z.string().optional().describe('Storage key for persisting view'), allowCreateView: z.boolean().optional().describe('Show "+" button to add/create a new view'), @@ -160,7 +161,8 @@ export const FilterUISchema = BaseSchema.extend({ placeholder: z.string().optional().describe('Placeholder'), })).describe('Available filters'), values: z.record(z.string(), z.any()).optional().describe('Current filter values'), - onChange: z.string().optional().describe('Filter change callback'), + onChange: z.string().optional() + .describe('Event name dispatched on window when the filters change (detail: { values }) — an event NAME, not a callback or a handler expression'), showClear: z.boolean().optional().describe('Show clear button'), showApply: z.boolean().optional().describe('Show apply button'), layout: z.enum(['inline', 'popover', 'drawer']).optional().describe('Filter layout'), @@ -179,7 +181,8 @@ export const SortUISchema = BaseSchema.extend({ field: z.string().describe('Field to sort by'), direction: z.enum(['asc', 'desc']).describe('Sort direction'), })).optional().describe('Current sort configuration'), - onChange: z.string().optional().describe('Sort change callback'), + onChange: z.string().optional() + .describe('Event name dispatched on window when the sort changes (detail: { sort }) — an event NAME, not a callback or a handler expression'), multiple: z.boolean().optional().describe('Allow multiple sort fields'), variant: z.enum(['dropdown', 'buttons']).optional().describe('UI variant'), }); From ffb0a6575633dcf247824884fd073cb3d0ade07b Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 30 Aug 2026 13:28:28 +0000 Subject: [PATCH 2/4] test(plugin-view): type the event-name liveness pin (AGENTS.md #6, no `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` 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 Claude-Session: https://claude.ai/code/session_013hfmP9hoMd3dJwTh85J4yB --- .../handlerEventNameLiveness.6124.test.tsx | 63 +++++++++++-------- 1 file changed, 37 insertions(+), 26 deletions(-) diff --git a/packages/plugin-view/src/__tests__/handlerEventNameLiveness.6124.test.tsx b/packages/plugin-view/src/__tests__/handlerEventNameLiveness.6124.test.tsx index f6fa7a8f51..8f3368878d 100644 --- a/packages/plugin-view/src/__tests__/handlerEventNameLiveness.6124.test.tsx +++ b/packages/plugin-view/src/__tests__/handlerEventNameLiveness.6124.test.tsx @@ -52,7 +52,16 @@ import { describe, it, expect, vi, afterEach } from 'vitest'; import { render, screen, cleanup, fireEvent } from '@testing-library/react'; -import { ViewSwitcherSchema, FilterUISchema, SortUISchema } from '@object-ui/types/zod'; +import { + ViewSwitcherSchema as ViewSwitcherMirror, + FilterUISchema as FilterUIMirror, + SortUISchema as SortUIMirror, +} from '@object-ui/types/zod'; +import type { + ViewSwitcherSchema, + FilterUISchema, + SortUISchema, +} from '@object-ui/types'; import { ViewSwitcher } from '../ViewSwitcher'; import { FilterUI } from '../FilterUI'; import { SortUI } from '../SortUI'; @@ -61,7 +70,9 @@ vi.mock('@object-ui/react', async (importOriginal) => { const React = await import('react'); return { ...(await importOriginal>()), - SchemaRenderer: ({ schema }: any) =>
, + SchemaRenderer: ({ schema }: { schema?: { type?: string } }) => ( +
+ ), SchemaRendererContext: React.createContext(null), subscribeDataChanges: () => () => {}, notifyDataChanged: () => {}, @@ -71,9 +82,9 @@ vi.mock('@object-ui/react', async (importOriginal) => { afterEach(() => cleanup()); /** Record every CustomEvent of `name` dispatched on `window` while `fn` runs. */ -function captureWindowEvents(name: string, fn: () => void): CustomEvent[] { - const seen: CustomEvent[] = []; - const listener = (e: Event) => seen.push(e as CustomEvent); +function captureWindowEvents(name: string, fn: () => void): D[] { + const seen: D[] = []; + const listener = (e: Event) => seen.push((e as CustomEvent).detail); window.addEventListener(name, listener); try { fn(); } finally { window.removeEventListener(name, listener); } return seen; @@ -84,29 +95,29 @@ describe('#6124 half 1 — the three event-name keys are DECLARED on the authora // retired key still parses green, so only the declaration can report a // deletion. Retiring any of these three turns exactly this half red. it('ViewSwitcherSchema declares onViewChange', () => { - expect(Object.keys(ViewSwitcherSchema.shape)).toContain('onViewChange'); + expect(Object.keys(ViewSwitcherMirror.shape)).toContain('onViewChange'); }); it('FilterUISchema declares onChange', () => { - expect(Object.keys(FilterUISchema.shape)).toContain('onChange'); + expect(Object.keys(FilterUIMirror.shape)).toContain('onChange'); }); it('SortUISchema declares onChange', () => { - expect(Object.keys(SortUISchema.shape)).toContain('onChange'); + expect(Object.keys(SortUIMirror.shape)).toContain('onChange'); }); it('each is a STRING mirror — an authored event name parses, a function does not', () => { - const base = { type: 'sort-ui', fields: [{ field: 'name' }] }; - expect(SortUISchema.safeParse({ ...base, onChange: 'sort:changed' }).success).toBe(true); + const base = { type: 'sort-ui' as const, fields: [{ field: 'name' }] }; + expect(SortUIMirror.safeParse({ ...base, onChange: 'sort:changed' }).success).toBe(true); // control: the instrument can say no — the string arm is a real constraint - expect(SortUISchema.safeParse({ ...base, onChange: () => {} }).success).toBe(false); + expect(SortUIMirror.safeParse({ ...base, onChange: () => {} }).success).toBe(false); }); it('the describe() text names the EVENT-NAME channel, not "callback"', () => { // The mislabel is the measured root cause of the wrong bucketing, so the // corrected wording is pinned rather than left to survive by luck. for (const d of [ - ViewSwitcherSchema.shape.onViewChange.description, - FilterUISchema.shape.onChange.description, - SortUISchema.shape.onChange.description, + ViewSwitcherMirror.shape.onViewChange.description, + FilterUIMirror.shape.onChange.description, + SortUIMirror.shape.onChange.description, ]) { // Names the real channel … expect(d).toMatch(/event name/i); @@ -120,21 +131,21 @@ describe('#6124 half 1 — the three event-name keys are DECLARED on the authora describe('#6124 half 2 — the authored string is LIVE: it is the CustomEvent name', () => { it('ViewSwitcher dispatches the authored name on view change', () => { - const schema: any = { + const schema: ViewSwitcherSchema = { type: 'view-switcher', variant: 'buttons', views: [{ type: 'list' }, { type: 'grid' }], onViewChange: 'zz6124:view', }; render(); - const events = captureWindowEvents('zz6124:view', () => { + const details = captureWindowEvents<{ view: string }>('zz6124:view', () => { fireEvent.click(screen.getByRole('button', { name: /grid/i })); }); - expect(events).toHaveLength(1); - expect((events[0].detail as any).view).toBe('grid'); + expect(details).toHaveLength(1); + expect(details[0].view).toBe('grid'); }); it('FilterUI dispatches the authored name on filter change', () => { - const schema: any = { + const schema: FilterUISchema = { type: 'filter-ui', layout: 'inline', filters: [{ field: 'qty', label: 'Qty', type: 'number' }], onChange: 'zz6124:filter', @@ -142,24 +153,24 @@ describe('#6124 half 2 — the authored string is LIVE: it is the CustomEvent na const { container } = render(); const input = container.querySelector('input[type="number"]') as HTMLInputElement; expect(input).toBeTruthy(); - const events = captureWindowEvents('zz6124:filter', () => { + const details = captureWindowEvents<{ values: Record }>('zz6124:filter', () => { fireEvent.change(input, { target: { value: '7' } }); }); - expect(events).toHaveLength(1); - expect((events[0].detail as any).values.qty).toBe(7); + expect(details).toHaveLength(1); + expect(details[0].values.qty).toBe(7); }); it('SortUI dispatches the authored name on sort change', () => { - const schema: any = { + const schema: SortUISchema = { type: 'sort-ui', variant: 'buttons', fields: [{ field: 'name', label: 'Name' }], onChange: 'zz6124:sort', }; render(); - const events = captureWindowEvents('zz6124:sort', () => { + const details = captureWindowEvents<{ sort: Array<{ field: string }> }>('zz6124:sort', () => { fireEvent.click(screen.getByRole('button', { name: /name/i })); }); - expect(events).toHaveLength(1); - expect((events[0].detail as any).sort[0].field).toBe('name'); + expect(details).toHaveLength(1); + expect(details[0].sort[0].field).toBe('name'); }); }); From 73942c744b38f6445ff90e06352b2efbe1ed2c25 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 30 Aug 2026 13:28:35 +0000 Subject: [PATCH 3/4] docs(changeset): declare the view handler-key describe() correction MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 Claude-Session: https://claude.ai/code/session_013hfmP9hoMd3dJwTh85J4yB --- .../6124-view-handler-keys-are-event-names.md | 37 +++++++++++++++++++ 1 file changed, 37 insertions(+) create mode 100644 .changeset/6124-view-handler-keys-are-event-names.md diff --git a/.changeset/6124-view-handler-keys-are-event-names.md b/.changeset/6124-view-handler-keys-are-event-names.md new file mode 100644 index 0000000000..71ae054755 --- /dev/null +++ b/.changeset/6124-view-handler-keys-are-event-names.md @@ -0,0 +1,37 @@ +--- +'@object-ui/types': patch +--- + +The three view handler keys are declared as EVENT NAMES, not callbacks. + +`ViewSwitcherSchema.onViewChange`, `FilterUISchema.onChange` and +`SortUISchema.onChange` were described as "change callback" on both the zod +mirror and the TS interface. They are not callbacks: the string an author +writes is the NAME of a `CustomEvent` the renderer dispatches on `window` — +`new CustomEvent(schema.onViewChange, { detail: { view } })` and its two +siblings. + +**What an author feels.** Nothing they write breaks — the type is still +`string`, so no accept set moves and no existing document changes verdict. +What changes is that the declaration, the generated JSON Schema description and +the TS JSDoc now tell them what the string is FOR, and what to listen for: + +```json +{ "type": "sort-ui", "fields": [{ "field": "name" }], "onChange": "myapp:sort-changed" } +``` + +```js +window.addEventListener('myapp:sort-changed', (e) => e.detail.sort); +``` + +Previously "Sort change callback" invited the two readings the runtime does not +support — a function (unwritable in JSON) or a handler expression (dropped at +runtime) — with no hint that the working form is an event name. + +The correction also protects the capability. A handler-key census that buckets +by declared TYPE cannot tell an event name from the unsupported +handler-expression dialect, and on that reading these three had been swept in +for retirement, which would have deleted working behaviour. A new pin +(`plugin-view/src/__tests__/handlerEventNameLiveness.6124.test.tsx`) now holds +both halves — that each key is DECLARED on the authorable surface, and that the +authored string reaches `new CustomEvent(...)`. From 017613ecd4ed5b8707cf68c72820985a681fd8b0 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 30 Aug 2026 13:51:17 +0000 Subject: [PATCH 4/4] docs(changeset): state only the two surfaces this change is actually felt on MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 Claude-Session: https://claude.ai/code/session_013hfmP9hoMd3dJwTh85J4yB --- .changeset/6124-view-handler-keys-are-event-names.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.changeset/6124-view-handler-keys-are-event-names.md b/.changeset/6124-view-handler-keys-are-event-names.md index 71ae054755..bc5a710fdf 100644 --- a/.changeset/6124-view-handler-keys-are-event-names.md +++ b/.changeset/6124-view-handler-keys-are-event-names.md @@ -13,8 +13,9 @@ siblings. **What an author feels.** Nothing they write breaks — the type is still `string`, so no accept set moves and no existing document changes verdict. -What changes is that the declaration, the generated JSON Schema description and -the TS JSDoc now tell them what the string is FOR, and what to listen for: +What changes is the two places this contract is published — the zod mirror's +`describe()` text and the TS JSDoc — which now tell them what the string is +FOR, and what to listen for: ```json { "type": "sort-ui", "fields": [{ "field": "name" }], "onChange": "myapp:sort-changed" }