From 46f4180a01a6dfd3882d1bff2faa19af10761c5f Mon Sep 17 00:00:00 2001 From: os-sam Date: Mon, 14 Sep 2026 11:06:59 +0000 Subject: [PATCH 1/3] feat(types): refuse `shortcut` by name on app action items (objectui#7719) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `AppAction.items` is the legacy `AppMenuItem` face. An authored `shortcut` there was accepted and STRIPPED in silence by `MenuItemSchema`, so an author got no signal at all. Director seat decision batch #70 of 2026-09-07 refused both widening options — a real `shortcut` member on the deprecated type, and re-typing `AppAction.items` to the overlay `MenuItem` — and changed the diagnostic instead: - `AppMenuItem.shortcut?: never` on the TypeScript face - `MenuItemSchema.shortcut` via `retirementTombstone()` on the zod mirror, one guidance string feeding both the parse message and `.describe()`, pointing the author at `NavigationItem` `retirementTombstone` rather than the sibling `handlerKeyRefusal`: that helper's message says JSON has no function value, which is false of a string-valued key, and its `z.custom` primitive makes `z.toJSONSchema` throw. `MenuItemSchema` is representable today and stays so. No read is re-added in `LayoutRenderer`, and its renderer pin's assertions are unchanged. Two stale prose passages that still described the question as open are repaired, and the `packages/types` half of the same pin is split so the two keys no longer share one fixture. `AppAction.shortcut` — the header button's own shortcut, one level up — is declared, authorable and deliberately untouched, with control assertions on both faces. Co-Authored-By: Claude Claude-Session: https://claude.ai/code/session_01L5xpA5q533BgTTNADibEFt --- ...19-app-menu-item-shortcut-named-refusal.md | 30 ++ packages/runner/src/LayoutRenderer.tsx | 14 +- ...ayoutRenderer.appActionItems-6854.test.tsx | 22 +- .../app-action-onclick-refusal-6854.test.ts | 39 +- ...pp-menu-item-shortcut-refusal-7719.test.ts | 346 ++++++++++++++++++ packages/types/src/app.ts | 28 ++ packages/types/src/zod/app.zod.ts | 31 +- 7 files changed, 489 insertions(+), 21 deletions(-) create mode 100644 .changeset/7719-app-menu-item-shortcut-named-refusal.md create mode 100644 packages/types/src/__tests__/app-menu-item-shortcut-refusal-7719.test.ts diff --git a/.changeset/7719-app-menu-item-shortcut-named-refusal.md b/.changeset/7719-app-menu-item-shortcut-named-refusal.md new file mode 100644 index 0000000000..e4d8e6a288 --- /dev/null +++ b/.changeset/7719-app-menu-item-shortcut-named-refusal.md @@ -0,0 +1,30 @@ +--- +'@object-ui/types': minor +--- + +`AppAction.items` 上的 `shortcut` 由「静默剥掉」改为「具名拒收」 + +⚠️ **这是一次已发布 mirror 的收窄**:今天一份在 app action 的菜单项上写了 `shortcut` 的文档 +`safeParse` 是**绿**的(键被 `MenuItemSchema` 无声丢弃,作者拿不到任何提示),此后在该键上**转红**, +issue 指向 `NavigationItem`。TS 面同步收窄:`AppMenuItem.shortcut?: never`,在编写处就被 `tsc` 拒掉。 + +零迁移面:objectui#6854 的普查(两把各自带活正控制的仪器,594 个 JSON + 全部 TS/TSX)在本仓测得 +**零作者**写过这个键,且 `@object-ui/runner` 不发布库入口,没有绕过校验器塞进来的路径。 + +裁决:director seat decision batch #70(objectui#7719,2026-09-07,维护者「同意」)。 +⛔ 选项 B(给已弃用的 `AppMenuItem` 长一个真正的 `shortcut` 成员并渲染它)与选项 C(把 +`AppAction.items` 改成 overlay 的 `MenuItem`)**均被拒绝**。唯一的改动是**诊断**: + +- TS 面 —— `AppMenuItem.shortcut?: never`; +- zod 面 —— `MenuItemSchema.shortcut` 走 `retirementTombstone()`,一条 guidance 同时喂 parse + 消息与 `.describe()`(即已发布的 JSON-Schema 描述)。 + +⛔ `LayoutRenderer` 未恢复任何读点,objectui#6854 的那枚 pin 断言原样保留;本次只修了它与 +`LayoutRenderer.tsx` 里已经过期的散文(两处都在把一个已裁决的问题描述成悬而未决)。 +`@object-ui/runner` 的改动**仅为注释**,无任何已发布行为变化。 + +⚠️ 受影响的**不是** `AppAction.shortcut`:那是 header 按钮自己的快捷键,一直声明着、本次不动, +并由控制断言钉住——只有它下面一层的 `items[]` 被收窄。 + +键盘快捷键若确实需要,它是 `NavigationItem` 那条线的能力(`@objectstack/spec` 在 17.0.0 的审计 +收尾里已把 `action.shortcut` 连同 tombstone 一起移除),⛔ 不在这条已弃用的 legacy 面上补。 diff --git a/packages/runner/src/LayoutRenderer.tsx b/packages/runner/src/LayoutRenderer.tsx index 7e45da193e..d7c8cba1cb 100644 --- a/packages/runner/src/LayoutRenderer.tsx +++ b/packages/runner/src/LayoutRenderer.tsx @@ -302,15 +302,21 @@ export const LayoutRenderer = ({ app, children, currentPath, onNavigate }: Layou * (objectui#6854, maintainer ruling of 2026-09-05, option B2). * * `items` is `AppMenuItem[]` (`@object-ui/types` `app.ts`), and the zod - * mirror parses it with the legacy eight-member `MenuItemSchema`. - * Neither declares `onClick` or `shortcut`; this map used to reach both + * mirror parses it with the legacy `MenuItemSchema`. Neither makes + * `onClick` or `shortcut` AUTHORABLE; this map used to reach both * through `as any`, i.e. past the type it was handed. The `onClick` read * is also what made the retirement refusal's own sentence — "no renderer * reads this key, so nothing could ever run it" — false. `type` and * `label` ARE declared on `AppMenuItem` and stay. * - * Whether `shortcut` should become authorable on `AppAction.items` is a - * separate contract question; do not re-add either read to answer it. + * `shortcut` is SETTLED, and the answer left this map alone + * (objectui#7719, director seat decision batch #70 of 2026-09-07): it + * does not become authorable on `AppAction.items`. What changed is the + * DIAGNOSTIC on the types side — `shortcut?: never` on `AppMenuItem` + * and a named refusal on `MenuItemSchema`, so an authored value is + * refused instead of stripped in silence. A keyboard shortcut on a + * navigation entry is a capability of the `NavigationItem` line. + * ⛔ No read is re-added here; that is the ruling, not an open question. */} {userAction.items?.map((item, idx) => { if (item.type === 'separator') { diff --git a/packages/runner/src/__tests__/LayoutRenderer.appActionItems-6854.test.tsx b/packages/runner/src/__tests__/LayoutRenderer.appActionItems-6854.test.tsx index 409cb37853..00f1ddae9e 100644 --- a/packages/runner/src/__tests__/LayoutRenderer.appActionItems-6854.test.tsx +++ b/packages/runner/src/__tests__/LayoutRenderer.appActionItems-6854.test.tsx @@ -12,9 +12,10 @@ * * `AppAction.items` is `AppMenuItem[]` — `type` / `label` / `icon` / `path` / * `href` / `children` / `badge` / `hidden` — and the zod mirror parses it with - * the legacy eight-member `MenuItemSchema`, which drops anything else in - * silence. This map used to reach two keys that are on neither list through - * `as any`: `onClick` and `shortcut`. + * the legacy `MenuItemSchema`, which drops anything else in silence, `shortcut` + * excepted since objectui#7719 (see the note below). This map used to reach two + * keys that are on neither authorable list through `as any`: `onClick` and + * `shortcut`. * * Deleting them is what makes `AppActionSchema.onClick`'s refusal message true * again. It tells an author "no renderer reads this key, so nothing could ever @@ -27,9 +28,18 @@ * none of in this repo — and require the renderer to ignore both. Re-adding * either read turns one of them red. * - * ⛔ NOT a ruling that `shortcut` must stay unrendered for ever: whether it - * should become AUTHORABLE on `AppAction.items` is its own contract card. This - * pins the contract as it stands, not the answer to that question. + * ⭐ THE RULING THIS NOTE USED TO BE WAITING FOR HAS LANDED. This paragraph read + * "⛔ NOT a ruling that `shortcut` must stay unrendered for ever: whether it + * should become AUTHORABLE on `AppAction.items` is its own contract card" — + * that card is objectui#7719, and director seat decision batch #70 of + * 2026-09-07 answered it: `shortcut` does NOT become authorable here, and ⛔ no + * read is re-added in this renderer. What that card changed is the DIAGNOSTIC on + * the types side — `shortcut?: never` on `AppMenuItem` and a named refusal on + * `MenuItemSchema`, so an authored value is refused by name instead of stripped + * in silence, pinned in + * `packages/types/src/__tests__/app-menu-item-shortcut-refusal-7719.test.ts`. + * ⇒ The assertions below are unchanged BY THAT RULING, which says the #6854 pin + * stays as is. They now pin a settled contract rather than an interim state. * * The `packages/types` half of the same claim — that the refusal message still * makes it — is pinned in diff --git a/packages/types/src/__tests__/app-action-onclick-refusal-6854.test.ts b/packages/types/src/__tests__/app-action-onclick-refusal-6854.test.ts index 8460690a12..d6e0e50416 100644 --- a/packages/types/src/__tests__/app-action-onclick-refusal-6854.test.ts +++ b/packages/types/src/__tests__/app-action-onclick-refusal-6854.test.ts @@ -80,22 +80,42 @@ describe('AppActionSchema.onClick — the retirement message states a measured f }); describe('why the cast could never have been fed by an author (objectui#6854 Zone 2, the premise)', () => { - // `AppAction.items` is parsed by the LEGACY eight-member `MenuItemSchema`, a - // plain `z.object` — so `onClick` and `shortcut` are not refused there, they - // are STRIPPED in silence. An author therefore has no declared route to send - // either key, which is what made deleting the two reads a cleanup rather than - // a behaviour removal. Whether `shortcut` SHOULD become authorable here is a - // separate contract question and deliberately not answered by this file. - const authored = { label: 'Profile', onClick: 'goProfile', shortcut: 'Ctrl+P' }; + // `AppAction.items` is parsed by the LEGACY `MenuItemSchema`, a plain + // `z.object` — so `onClick` is not refused there, it is STRIPPED in silence. + // An author therefore has no declared route to send it, which is what made + // deleting the two reads a cleanup rather than a behaviour removal. + // + // ⭐ UPDATED by objectui#7719 (director seat decision batch #70 of 2026-09-07): + // this block used to carry `shortcut` in the same fixture and the same + // sentence, and recorded "whether `shortcut` SHOULD become authorable here is + // a separate contract question" as OPEN. It is answered — `shortcut` does not + // become authorable, and it is no longer stripped here either: it is a named + // `retirementTombstone()` refusal pointing at `NavigationItem`. So the two + // keys no longer share a fate and no longer share a fixture. + // ⛔ The Zone-2 premise this file exists for is UNCHANGED, and in fact + // stronger: an author still has no declared route to send either key, and is + // now told so for one of them. The `shortcut` contract itself is pinned in + // `./app-menu-item-shortcut-refusal-7719.test.ts`, not here. + const authored = { label: 'Profile', onClick: 'goProfile' }; - it('the items mirror accepts the document and drops both undeclared keys', () => { + it('the items mirror accepts the document and drops the undeclared `onClick`', () => { const result = MenuItemSchema.safeParse(authored); expect(result.success).toBe(true); if (!result.success) return; const parsed = result.data as Record; expect(parsed.label).toBe('Profile'); expect('onClick' in parsed).toBe(false); - expect('shortcut' in parsed).toBe(false); + }); + + it('an authored `shortcut` on the same item is REFUSED, not dropped (objectui#7719)', () => { + // Kept HERE, beside the `onClick` row, rather than only in the #7719 pin: + // the two keys were one sentence in objectui#6854 and are two contracts now, + // and this pair is what stops them being conflated again. Swap either + // expectation and the file says which key it is describing. + const result = MenuItemSchema.safeParse({ ...authored, shortcut: 'Ctrl+P' }); + expect(result.success).toBe(false); + if (result.success) return; + expect(result.error.issues.some((i) => String(i.path[0]) === 'shortcut')).toBe(true); }); it('a whole action carrying such an item parses green, with the item scrubbed', () => { @@ -115,6 +135,5 @@ describe('why the cast could never have been fed by an author (objectui#6854 Zon // from the parsed value, which no static type can answer. const [first] = (result.data as unknown as { items: Record[] }).items; expect('onClick' in first).toBe(false); - expect('shortcut' in first).toBe(false); }); }); diff --git a/packages/types/src/__tests__/app-menu-item-shortcut-refusal-7719.test.ts b/packages/types/src/__tests__/app-menu-item-shortcut-refusal-7719.test.ts new file mode 100644 index 0000000000..a3efcd8a8c --- /dev/null +++ b/packages/types/src/__tests__/app-menu-item-shortcut-refusal-7719.test.ts @@ -0,0 +1,346 @@ +/** + * 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#7719 — `shortcut` is REFUSED BY NAME on an app action ITEM + * (`AppAction.items`, i.e. the legacy `AppMenuItem`), on BOTH faces. + * + * ## The ruling, quoted rather than paraphrased + * + * Director seat decision batch #70, 2026-09-07, maintainer verbatim 「同意」: + * + * > The deprecated `AppMenuItem` does not grow a `shortcut` member (option B + * > refused: zero measured pull, and the type is being retired in favour of + * > `NavigationItem`); `AppAction.items` is not re-typed to the overlay + * > `MenuItem` (option C refused, as on #6854). The one change: an authored + * > `shortcut` on an app action item is **refused by name** on the legacy + * > mirror (`shortcut?: never` on the TS face, a named refusal on + * > `MenuItemSchema` in `app.zod.ts`) with a message pointing at + * > `NavigationItem`, so the key is no longer stripped in silence. ⛔ No read is + * > re-added in `LayoutRenderer`; the #6854 pin stays as is. + * + * ⇒ This file pins the ruling's THREE clauses: the TS `never`, the named mirror + * refusal, and — block (e) — the untouched renderer contract. + * + * ## ⚠️ The failure mode here is STRIP, not KEEP — measured, and it differs + * ## from the family's other retirements + * + * objectui#7997 (`DetailViewSchema.related`) recorded that a dropped member key + * is KEPT: `BaseSchemaCore` ends `.passthrough()` and the TypeScript + * `BaseSchema` closes with an any-valued index signature. ⛔ That mechanism does + * NOT apply to this surface and assuming it would misdescribe the change. + * `MenuItemSchema` is a plain `z.object` under a `z.lazy`, built on no base, and + * `AppMenuItem` declares no index signature ⇒ an undeclared key is STRIPPED, + * silently, and never reaches `.data`. Block (c) takes that reading rather than + * restating it, and the distinction is the point: both mechanisms are a SILENT + * ACCEPT, so a bare non-declaration refuses nothing either way — but only one of + * them leaves the value visible afterwards, and here it does not. "Stripped in + * silence" is the card's own phrase and it is the accurate one. + * + * ## Which refusal helper, and why it is a measurement rather than a taste + * + * `retirementTombstone()` — `z.never({ error }).optional().describe()`. + * + * - ⛔ NOT `handlerKeyRefusal()`, despite the in-file precedent on the sibling + * `AppActionSchema.onClick`. Two independent reasons, the second decisive: + * its message says JSON has no function value, which is FALSE of a + * string-valued key an author can perfectly well write; and its `z.custom` + * primitive makes `z.toJSONSchema` THROW. Measured before this change: + * `z.toJSONSchema(MenuItemSchema)` SUCCEEDS in both `io` directions while + * `z.toJSONSchema(AppActionSchema)` already throws — because of that very + * `onClick` arm. Choosing `handlerKeyRefusal` here would have taken + * `MenuItemSchema` from representable to throwing. Block (f) pins it. + * - ⛔ NOT `aliasKeyRefusal()`: that helper composes "Did you mean `shortcut` → + * ``?", and there is no canonical member of `MenuItemSchema` + * meaning a keyboard shortcut. The remedy here is a different TYPE + * (`NavigationItem`), not a sibling spelling, so that helper could only be + * made to say something untrue. + * - ✅ `retirementTombstone()` is the helper whose own contract is this exact + * situation — "a key that stays declared but is unwritable, so an authored + * value is REFUSED loudly instead of being silently stripped the way an + * undeclared key would be". Declaration history does not separate it from its + * siblings: the overlay `MenuItemSchema.type` (objectui#6523) is a + * `retirementTombstone` for a spelling that was NEVER declared either, which + * is precisely this case. + * - ⛔ And not `@objectstack/spec`'s own `retiredKey`, whose describe contract + * prefixes `[REMOVED] `; these describe strings are this package's published + * metadata. + * + * ## Which program checks this file + * + * `packages/types`' `type-check` runs THREE programs; this file is in the third + * (`tsconfig.test.json` — `tsc --noEmit` builds `tsconfig.json`, which excludes + * `__tests__/` by directory). ⚠️ The type-level rows in block (a) are therefore + * pinned by `tsc`, ⛔ NOT by vitest, which strips types and would pass them + * vacuously. Both are run and reported. + */ + +import { describe, it, expect } from 'vitest'; +import { z } from 'zod'; +import type { AppAction, AppMenuItem } from '../app'; +import { AppActionSchema, MenuItemSchema } from '../zod/app.zod'; + +/** Mutual assignability, the standard invariant `Eq` — not `extends`. */ +type Eq = (() => T extends A ? 1 : 2) extends (() => T extends B ? 1 : 2) + ? true + : false; + +/* ── (a) the TypeScript face ──────────────────────────────────────────────── */ + +describe('objectui#7719 — the TypeScript face refuses `shortcut` on `AppMenuItem`', () => { + it('the member is a `never` tombstone — neither a value type nor an absent key', () => { + // ⚠️ Pinned by `tsc -p tsconfig.test.json`; vacuous under vitest. + // + // PROVING REMOVAL: delete `shortcut?: never` from `AppMenuItem` and this + // line stops COMPILING (TS2339 — the property does not exist), which is the + // pre-change state of this tree. Re-declare it as `shortcut?: string` + // (option B, the thing the ruling refused) and mutual assignability fails in + // the other direction. Both directions matter and a one-way `extends` would + // have caught only one of them. + const _tombstoned: Eq = true; + expect(_tombstoned).toBe(true); + }); + + it('authoring a shortcut on a menu item no longer compiles', () => { + // PROVING REMOVAL: widen the member to `shortcut?: string` and this row + // reds — the `@ts-expect-error` goes unused (TS2578). That is the direction + // worth guarding: this row is what turns a future option-B patch red. + const item: AppMenuItem = { + label: 'Profile', + // @ts-expect-error `shortcut` is refused on an app action item — author navigation as `NavigationItem` (objectui#7719) + shortcut: 'Ctrl+P', + }; + expect(item.label).toBe('Profile'); + }); + + it('CONTROL — the REST of `AppMenuItem` still type-checks; this is one member, not a closed face', () => { + const item: AppMenuItem = { + type: 'item', + label: 'Profile', + icon: 'user', + path: '/profile', + badge: 3, + hidden: false, + }; + expect(item.path).toBe('/profile'); + }); + + it('CONTROL — `AppAction.shortcut` is a DIFFERENT key, still declared and still a `string`', () => { + // ⭐ The non-target control, and the reason it is in this file: the first + // reading of a `grep` for `shortcut` over `app.ts` finds THIS member and + // reads it as "option B is already landed". It is the header BUTTON's own + // shortcut, one level up from `items[]`, and the ruling does not touch it. + // + // PROVING REMOVAL: retire `AppAction.shortcut` too — over-applying this card + // to every `shortcut` in the file — and this row reds in both halves. + const _stillAString: Eq = true; + expect(_stillAString).toBe(true); + const action: AppAction = { type: 'button', label: 'Search', shortcut: 'Ctrl+K' }; + expect(action.shortcut).toBe('Ctrl+K'); + }); +}); + +/* ── (b) the zod mirror ───────────────────────────────────────────────────── */ + +describe('objectui#7719 — the JSON face refuses `shortcut` by name', () => { + const authored = { type: 'item' as const, label: 'Profile', shortcut: 'Ctrl+P' }; + + it('a menu item authoring `shortcut` is refused', () => { + // It parsed GREEN before this card — green, and one key lighter. That is the + // accept-set narrowing the changeset declares. + expect(MenuItemSchema.safeParse(authored).success).toBe(false); + }); + + it('the issue is addressed to `shortcut` and points the author at `NavigationItem`', () => { + // A refusal an author cannot act on is half a refusal. `retirementTombstone` + // feeds ONE guidance string into both the parse-time message and + // `.describe()`, so what an author reads and what generated docs publish + // cannot drift apart. + const r = MenuItemSchema.safeParse(authored); + expect(r.success).toBe(false); + if (r.success) return; + const issue = r.error.issues.find((i) => i.path[0] === 'shortcut'); + expect(issue, 'no issue was addressed to `shortcut`').toBeDefined(); + expect(issue!.message).toContain('NavigationItem'); + expect(issue!.message).toContain('objectui#7719'); + // ⚠️ The message must not read as though `shortcut` is refused on app + // actions generally — it is refused on the ITEMS. It says so by naming the + // surviving sibling explicitly. + expect(issue!.message).toContain('AppAction.shortcut'); + }); + + it('the refusal reports `invalid_type` at the key path — the `z.never` code, not `custom`', () => { + // Pins WHICH helper landed, from the outside. `handlerKeyRefusal`'s + // `z.custom` would report `custom` here; see block (f) for why that would + // also have broken JSON-Schema emission. + const r = MenuItemSchema.safeParse(authored); + expect(r.success).toBe(false); + if (r.success) return; + const issue = r.error.issues.find((i) => i.path[0] === 'shortcut'); + expect(issue!.code).toBe('invalid_type'); + }); + + it('CONTROL — the same document parses green with `shortcut` removed', () => { + // Says the refusal above is about this member and not about the fixture. + const { shortcut: _dropped, ...withoutShortcut } = authored; + expect(MenuItemSchema.safeParse(withoutShortcut).success).toBe(true); + }); + + it('CONTROL — the mirror still refuses a genuinely malformed item', () => { + // And says the green above is a reading rather than a mirror that accepts + // anything: `type` is an enum. + expect(MenuItemSchema.safeParse({ type: 'not-a-menu-item-type' }).success).toBe(false); + }); + + it('the refusal is inherited by nested `children`, which are the same schema', () => { + // `children: z.array(MenuItemSchema)` is recursive through `z.lazy`, so a + // refusal that only held at the top level would be a half-refusal. + const r = MenuItemSchema.safeParse({ + type: 'group', + label: 'More', + children: [{ label: 'Profile', shortcut: 'Ctrl+P' }], + }); + expect(r.success).toBe(false); + if (r.success) return; + expect(r.error.issues.some((i) => i.path.join('.') === 'children.0.shortcut')).toBe(true); + }); +}); + +/* ── (c) why it had to be a refusal — the SILENT STRIP, measured here ─────── */ + +describe('objectui#7719 — a bare non-declaration strips in silence; it refuses nothing', () => { + it('an UNDECLARED sibling key is accepted and STRIPPED — it never reaches `.data`', () => { + // THE LOAD-BEARING ROW of this file: without it, "leaving `shortcut` + // undeclared would not have refused it" is unfalsifiable prose. Here it is a + // reading taken on this very schema. `keyboardShortcut` is declared nowhere; + // the parse SUCCEEDS and the key is gone from the output. + // + // ⚠️ Deliberately different from objectui#7997's reading on + // `DetailViewSchema`, where the undeclared key SURVIVES into `.data` because + // `BaseSchemaCore` ends `.passthrough()`. This mirror has no base and no + // passthrough. Same conclusion, different mechanism; ⛔ do not copy that + // file's wording onto this surface. + // + // PROVING REMOVAL: append `.passthrough()` to the object inside the `z.lazy` + // and the second assertion reds (the key survives) while the first stays + // green — which is exactly how this row tells the two mechanisms apart. + const r = MenuItemSchema.safeParse({ type: 'item', label: 'Profile', keyboardShortcut: 'Ctrl+P' }); + expect(r.success).toBe(true); + if (!r.success) return; + expect(Object.keys(r.data as Record)).not.toContain('keyboardShortcut'); + }); + + it('and the tombstoned key does NOT get that treatment — the pair is the whole argument', () => { + // The same parse, one key swapped. Undeclared: accepted and stripped. + // Tombstoned: refused by name. That difference IS this card. + expect( + MenuItemSchema.safeParse({ type: 'item', label: 'Profile', shortcut: 'Ctrl+P' }).success, + ).toBe(false); + }); +}); + +/* ── (d) the real authoring position: `AppAction.items[]` ─────────────────── */ + +describe('objectui#7719 — the refusal holds where an author actually writes it', () => { + const action = { + type: 'user' as const, + label: 'Ada Lovelace', + items: [{ label: 'Profile', shortcut: 'Ctrl+P' }], + }; + + it('an app action whose item carries `shortcut` is refused at `items.0.shortcut`', () => { + // Block (b) parses `MenuItemSchema` directly; this one goes through the + // declaration an author actually writes, so a refusal that existed only on + // the standalone const would show up here as green. + const r = AppActionSchema.safeParse(action); + expect(r.success).toBe(false); + if (r.success) return; + expect(r.error.issues.some((i) => i.path.join('.') === 'items.0.shortcut')).toBe(true); + }); + + it('CONTROL — the SAME action parses green carrying `shortcut` on the ACTION itself', () => { + // ⭐ The mirror half of block (a)'s non-target control, and the row that + // stops this card from being read as "shortcut is refused on app actions". + // The header button's own shortcut is declared, untouched, and still green. + // + // PROVING REMOVAL: move the tombstone one declaration up — onto + // `AppActionSchema` instead of `MenuItemSchema` — and this row reds while + // block (d)'s first row goes green. The pair localises the change to the + // right declaration, which a single row could not do. + const r = AppActionSchema.safeParse({ + type: 'user' as const, + label: 'Ada Lovelace', + shortcut: 'Ctrl+K', + items: [{ label: 'Profile' }], + }); + expect(r.success).toBe(true); + }); +}); + +/* ── (e) the renderer contract the ruling left ALONE ──────────────────────── */ + +describe('objectui#7719 — the eight authorable members are untouched: this narrows one key, not the face', () => { + it('every declared member of the legacy item still parses green', () => { + // ⛔ The ruling refused option C (re-typing `AppAction.items` to the overlay + // `MenuItem`), whose cost was precisely that `path` / `href` / `badge` / + // `type` would start being refused and the divider spelling would change. + // This row is what turns such a re-type red, and it is the reason a + // retirement pin is not allowed to only prove absence. + const r = MenuItemSchema.safeParse({ + type: 'item', + label: 'Profile', + icon: 'user', + path: '/profile', + href: 'https://example.com', + badge: 7, + hidden: false, + children: [{ type: 'item', label: 'Nested' }], + }); + expect(r.success).toBe(true); + }); + + it('the legacy divider spelling `{ "type": "separator" }` still parses green', () => { + // Option C would have moved this to `{ "separator": true }`. + expect(MenuItemSchema.safeParse({ type: 'separator' }).success).toBe(true); + }); +}); + +/* ── (f) the helper choice, pinned from the outside ───────────────────────── */ + +describe('objectui#7719 — `MenuItemSchema` stays representable in JSON Schema, because the arm is `z.never`', () => { + it('`z.toJSONSchema(MenuItemSchema)` still succeeds in both io directions', () => { + // MEASURED BEFORE THIS CHANGE: it succeeded. `handlerKeyRefusal`'s + // `z.custom` would have made it throw ("Custom types cannot be represented + // in JSON Schema") — the same reason objectui#7694 gives for `aliasKeyRefusal` + // not borrowing that primitive. This row is the helper choice stated as a + // consequence an outsider can check. + expect(() => z.toJSONSchema(MenuItemSchema, { io: 'input' })).not.toThrow(); + expect(() => z.toJSONSchema(MenuItemSchema, { io: 'output' })).not.toThrow(); + }); + + it('the emitted `shortcut` property is the unsatisfiable `{ not: {} }`, carrying the guidance', () => { + // ONE string, BOTH channels: the parse message asserted in block (b) and + // this published description are the same argument to `retirementTombstone`, + // so they cannot drift apart. + const js = z.toJSONSchema(MenuItemSchema, { io: 'input' }) as { + properties?: Record; + }; + const prop = js.properties?.shortcut; + expect(prop, 'no `shortcut` property was emitted').toBeDefined(); + expect(prop!.not).toEqual({}); + expect(prop!.description).toContain('NavigationItem'); + }); + + it('CONTROL — a `z.custom` arm really does throw, so the row above is not vacuous', () => { + // Without this, "we avoided `z.custom` for a reason" is a claim about a + // library rather than a reading of it. + expect(() => z.toJSONSchema(z.object({ k: z.custom(() => false).optional() }))).toThrow( + /cannot be represented in JSON Schema/, + ); + }); +}); diff --git a/packages/types/src/app.ts b/packages/types/src/app.ts index 108b410366..a286698a02 100644 --- a/packages/types/src/app.ts +++ b/packages/types/src/app.ts @@ -578,6 +578,34 @@ export interface AppMenuItem { * Visibility Condition */ hidden?: boolean | string; + + /** + * REFUSED (objectui#7719, director seat decision batch #70 of 2026-09-07, + * maintainer verbatim 「同意」, in the objectui#6124 / ADR-0049 shape). + * + * `shortcut` is not authorable on an app action ITEM. The ruling refused BOTH + * widening alternatives — growing this deprecated type a `shortcut` member + * (zero measured pull, and the type is being retired in favour of + * {@link NavigationItem}), and re-typing {@link AppAction.items} to the + * overlay `MenuItem`. What it changed is the DIAGNOSTIC: an authored value + * used to be stripped in silence by the zod mirror, and is now refused by + * name there, with this face's `never` refusing it at the authoring site + * before anything runs. + * + * ⚠️ NOT the same key as {@link AppAction.shortcut}, which is declared, + * authorable and deliberately untouched — that one is the header BUTTON's own + * shortcut, one level up from these items. A `grep` for `shortcut` over this + * file finds that member first, and reading it as this one is how the widening + * the ruling refused looks like work already done. + * + * ⛔ No read was re-added in the standalone runner's `LayoutRenderer`; the + * objectui#6854 pin stands. Both halves of this refusal are pinned in + * `__tests__/app-menu-item-shortcut-refusal-7719.test.ts`. + * + * @deprecated Not part of this contract — author the menu as a + * {@link NavigationItem} and put the shortcut capability there. + */ + shortcut?: never; } // ============================================================================ diff --git a/packages/types/src/zod/app.zod.ts b/packages/types/src/zod/app.zod.ts index 6bd61d6544..edba7cd23e 100644 --- a/packages/types/src/zod/app.zod.ts +++ b/packages/types/src/zod/app.zod.ts @@ -30,7 +30,7 @@ import { objectNavTargetExclusivity, } from '@objectstack/spec/ui'; import { BaseSchema, specFieldsExcept } from './base.zod.js'; -import { handlerKeyRefusal } from './tombstone.zod.js'; +import { handlerKeyRefusal, retirementTombstone } from './tombstone.zod.js'; import type { AppMenuItem } from '../app.js'; import { stripImportedDefaults } from './imported-defaults.js'; @@ -274,6 +274,35 @@ export const MenuItemSchema: z.ZodType = z.lazy(() => children: z.array(MenuItemSchema).optional().describe('Child items (submenu)'), badge: z.union([z.string(), z.number()]).optional().describe('Badge or count'), hidden: z.union([z.boolean(), z.string()]).optional().describe('Visibility condition'), + // REFUSED (objectui#7719, director seat decision batch #70 of 2026-09-07, + // maintainer verbatim 「同意」). The ruling refused both widening options — a + // `shortcut` member on this deprecated type, and re-typing `AppAction.items` + // to the overlay `MenuItem` — and changed the DIAGNOSTIC instead: an authored + // value used to be stripped here in silence, and is refused by name now. + // + // WHY `retirementTombstone` and not `handlerKeyRefusal`, which the sibling + // `AppActionSchema.onClick` uses in this same file: that helper's message + // says JSON has no function value, which is false of a string-valued key, and + // its `z.custom` primitive makes `z.toJSONSchema` THROW. Measured: this const + // is representable today and `AppActionSchema` is NOT, precisely because of + // that `onClick` arm. Nor `aliasKeyRefusal`, which would have to name a + // canonical sibling key; the remedy here is a different TYPE. + // + // ⛔ No read was re-added in the standalone runner's `LayoutRenderer`; the + // objectui#6854 pin stands. Pinned in + // `../__tests__/app-menu-item-shortcut-refusal-7719.test.ts`. + shortcut: retirementTombstone( + 'REFUSED (objectui#7719, director seat decision batch #70, 2026-09-07; ADR-0049) — `shortcut` ' + + 'is not authorable on an app action ITEM. `AppAction.items` is this legacy `AppMenuItem` ' + + 'face, deprecated in favour of `NavigationItem`, and no renderer reads a shortcut here: the ' + + 'standalone runner renders an item\'s `label` and its `type: "separator"` and nothing else ' + + '(objectui#6854). Until this refusal an authored value was STRIPPED in silence by this ' + + 'mirror, which is the outcome the ruling closed; growing the deprecated type instead was ' + + 'refused. NOT to be confused with `AppAction.shortcut`, which is declared, authorable and ' + + 'unchanged — that is the header BUTTON\'s own shortcut, one level up from these items. ' + + 'A keyboard shortcut on a navigation entry is a capability of the `NavigationItem` line: ' + + 'author the menu as `NavigationItem`, and file the capability there if it is wanted.', + ), })); // ============================================================================ From da2e7191016df8679feeed59115beb038316b98b Mon Sep 17 00:00:00 2001 From: os-sam Date: Mon, 14 Sep 2026 12:10:10 +0000 Subject: [PATCH 2/3] fix(types): repair six false prose claims from contract review round 1 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Round-1 contract review returned FAIL on six defects, all false published prose or figures rather than substance. The ruling implementation itself was verified compliant and is unchanged by this commit. 1. `app-action-onclick-refusal-6854.test.ts` carried a comment saying "the assertions below are unchanged" and "both undeclared keys are gone" — this branch had removed one of the two assertions directly beneath it, and `shortcut` is no longer an undeclared key at all. Rewritten to describe the single surviving assertion and the two-contract split, citing the sibling row by NAME rather than by position so it cannot go stale on insertion. The deleted assertion is deliberately NOT restored: the parse now fails, so there is no parsed value left to read the key out of. 2. `.changeset/6854-layout-renderer-retired-onclick.md` publishes verbatim to users and carried three sentences this branch falsifies (an "eight-member" mirror that now has nine, "neither declares", "strips in silence", and an open contract question that is now answered). Corrected in place, prose only, frontmatter untouched. The changeset-claims gate cannot see this file because it matches backticked file names and this one names only symbols. 3. The card's own changeset claimed `@objectstack/spec` removed `action.shortcut` together with its tombstone. Measured against the RESOLVED installed package: the tombstone is live and refuses the key by name. Corrected to the measurement, with the positive control that separates a key-specific refusal from generic strictness, and with the difference in route stated rather than flattened — nothing was removed as authorable here, because the key was never declared on this face. 4-6. Three figures frozen into prose that nothing re-derives. Replaced with pointers to the instruments that produce them, rather than corrected in place: a corrected number re-stales on the next push, which is exactly what happened to the stale test line citation when this branch's own docblock moved the test down ten lines. Also replaces the migration census in the card's changeset with a re-runnable structural rule. The textual rule stated there was handed an empty population, so its zero was vacuous even though its conclusion was right. Co-Authored-By: Claude Claude-Session: https://claude.ai/code/session_01L5xpA5q533BgTTNADibEFt --- .../6854-layout-renderer-retired-onclick.md | 13 +++++++--- ...19-app-menu-item-shortcut-named-refusal.md | 26 ++++++++++++++----- .../app-action-onclick-refusal-6854.test.ts | 16 +++++++++--- 3 files changed, 42 insertions(+), 13 deletions(-) diff --git a/.changeset/6854-layout-renderer-retired-onclick.md b/.changeset/6854-layout-renderer-retired-onclick.md index 94b2bee660..ee542966aa 100644 --- a/.changeset/6854-layout-renderer-retired-onclick.md +++ b/.changeset/6854-layout-renderer-retired-onclick.md @@ -8,7 +8,9 @@ makes `AppActionSchema.onClick`'s retirement message true again (objectui#6854, maintainer ruling of 2026-09-05, option B2). `AppAction.items` is `AppMenuItem[]`, and the zod mirror parses it with the legacy -eight-member `MenuItemSchema` — neither declares `onClick` or `shortcut`. +`MenuItemSchema`, which declared neither `onClick` nor `shortcut` when this change was +made. (`shortcut` has since become a declared refusal there — objectui#7719 — while +`onClick` remains undeclared on that mirror and is still dropped in silence.) `LayoutRenderer` reached both through `as any`, past the type it was handed, and that left three mutually exclusive signals about the same key: the TypeScript face said `?: never`, the validator's refusal said "no renderer reads this key, so @@ -25,7 +27,9 @@ its sentence true rather than restating it. - `@object-ui/runner`: `LayoutRenderer` no longer reads `onClick` or `shortcut` on a `type: 'user'` action's `items`. The `onClick` branch was an empty body and could never run a JSON value; the `shortcut` read rendered a `DropdownMenuShortcut` from - a key the mirror strips in silence, so no validated document could reach it. A + a key the mirror stripped in silence at the time, so no validated document could + reach it. (objectui#7719 has since replaced that silent strip with a named refusal; + either way the read was unreachable, which is what made deleting it a cleanup.) A census of every JSON and TypeScript app document in this repository found zero authors of either key (positive controls recorded on the issue). - `@object-ui/types`: the rationale comments on `AppAction.onClick` and @@ -33,5 +37,6 @@ its sentence true rather than restating it. That was false — the runner renders both the `'button'` and the `'user'` arm. Corrected to what was measured: `actions[]` is read, `onClick` is not. -Whether `shortcut` should become authorable on `AppAction.items` is a separate -contract question and is filed on its own. +Whether `shortcut` should become authorable on `AppAction.items` was a separate +contract question, filed as objectui#7719 and since answered: it does not become +authorable, and the mirror refuses it by name instead of stripping it. diff --git a/.changeset/7719-app-menu-item-shortcut-named-refusal.md b/.changeset/7719-app-menu-item-shortcut-named-refusal.md index e4d8e6a288..0d2f24595c 100644 --- a/.changeset/7719-app-menu-item-shortcut-named-refusal.md +++ b/.changeset/7719-app-menu-item-shortcut-named-refusal.md @@ -4,12 +4,16 @@ `AppAction.items` 上的 `shortcut` 由「静默剥掉」改为「具名拒收」 -⚠️ **这是一次已发布 mirror 的收窄**:今天一份在 app action 的菜单项上写了 `shortcut` 的文档 -`safeParse` 是**绿**的(键被 `MenuItemSchema` 无声丢弃,作者拿不到任何提示),此后在该键上**转红**, +⚠️ **这是一次已发布 mirror 的收窄**:本次改动**之前**,一份在 app action 的菜单项上写了 `shortcut` +的文档 `safeParse` 是**绿**的(键被 `MenuItemSchema` 无声丢弃,作者拿不到任何提示);**之后**在该键上**转红**, issue 指向 `NavigationItem`。TS 面同步收窄:`AppMenuItem.shortcut?: never`,在编写处就被 `tsc` 拒掉。 -零迁移面:objectui#6854 的普查(两把各自带活正控制的仪器,594 个 JSON + 全部 TS/TSX)在本仓测得 -**零作者**写过这个键,且 `@object-ui/runner` 不发布库入口,没有绕过校验器塞进来的路径。 +零迁移面:本仓**零作者**写过这个键,且 `@object-ui/runner` 不发布库入口,没有绕过校验器塞进来的 +路径。⛔ 这里刻意不冻结任何总数 —— objectui#6854 当初普查的那个 JSON 总体此后已经变动,冻进散文 +的数字没有东西会去重新求值。判据写成可复跑的规则:**结构化**地 `JSON.parse` 每一个 tracked +`*.json`,无限深度遍历,报出「经由字面名为 `items` 的键抵达的数组、其元素对象自带 `shortcut`」者; +在本次改动的头上跑出 0 命中,注入 fixture 的正控制会开火(含一处嵌套命中),且不会把 action 层的 `shortcut` +兄弟键误计。总体数随树变化,规则不变。 裁决:director seat decision batch #70(objectui#7719,2026-09-07,维护者「同意」)。 ⛔ 选项 B(给已弃用的 `AppMenuItem` 长一个真正的 `shortcut` 成员并渲染它)与选项 C(把 @@ -26,5 +30,15 @@ issue 指向 `NavigationItem`。TS 面同步收窄:`AppMenuItem.shortcut?: nev ⚠️ 受影响的**不是** `AppAction.shortcut`:那是 header 按钮自己的快捷键,一直声明着、本次不动, 并由控制断言钉住——只有它下面一层的 `items[]` 被收窄。 -键盘快捷键若确实需要,它是 `NavigationItem` 那条线的能力(`@objectstack/spec` 在 17.0.0 的审计 -收尾里已把 `action.shortcut` 连同 tombstone 一起移除),⛔ 不在这条已弃用的 legacy 面上补。 +键盘快捷键若确实需要,它是 `NavigationItem` 那条线的能力,⛔ 不在这条已弃用的 legacy 面上补。 + +⚠️ 协议侧的现状,实测于**已解析安装**的 `@objectstack/spec@17.4.0`(⛔ 不是读源码推断): +`action.shortcut` 在 17.0.0 的审计收尾里被移除的是**可编写性**,它的 tombstone 是**保留着的** —— +`ActionSchema.safeParse({ name, type, label, shortcut })` 返回 `success: false`,issue 落在 +`shortcut` 上、`code` 为 `invalid_type`,消息开头即 +「`action.shortcut` was removed in @objectstack/spec 17.0.0 (audit close-out)」。 +正控制:换一个协议从未声明过的兄弟键,同样被拒,但**没有**落在 `shortcut` 上的 issue —— +⇒ 上面那条读数是关于这个键的,不是 strict 对象的通用效果。 +⚠️ 两边的**终局形状**一致 —— 键不可编写、拒收具名;但**路径不同**,不要把它们说成一回事: +协议那边是先声明、再移除可编写性、留下 tombstone;本仓这个键在 `AppMenuItem` 上**从未声明过**, +它走的是「静默剥掉」→「具名拒收」,⛔ 没有任何可编写性被移除。 diff --git a/packages/types/src/__tests__/app-action-onclick-refusal-6854.test.ts b/packages/types/src/__tests__/app-action-onclick-refusal-6854.test.ts index d6e0e50416..a271f22ac4 100644 --- a/packages/types/src/__tests__/app-action-onclick-refusal-6854.test.ts +++ b/packages/types/src/__tests__/app-action-onclick-refusal-6854.test.ts @@ -130,9 +130,19 @@ describe('why the cast could never have been fed by an author (objectui#6854 Zon // element mirror `MenuItemSchema` was annotated `z.ZodType` to break its own // recursion. objectui#7760 gave that mirror its declaration as both type arguments, // so the element is `AppMenuItem` now and a direct assertion to an index-signature - // type no longer overlaps. ⛔ The assertions below are unchanged and still read the - // RUNTIME object — the point of this file is that both undeclared keys are gone - // from the parsed value, which no static type can answer. + // type no longer overlaps. The assertion below still reads the RUNTIME object, + // which is the point of this file: whether a key survived a parse is not something + // any static type can answer. + // + // ⭐ It used to be a PAIR, asserting `onClick` and `shortcut` both gone. objectui#7719 + // split the two contracts, so only the `onClick` half belongs here: `shortcut` is no + // longer an UNDECLARED key that gets scrubbed, it is a declared `retirementTombstone` + // that makes this very document FAIL to parse. ⛔ Do not restore the second assertion + // — `'shortcut' in first` is unreachable now, because there is no `first` to read. + // The refused case is the row named "an authored `shortcut` on the same item is + // REFUSED, not dropped" — cited by NAME, because a positional reference goes stale + // the moment a row is inserted. The contract itself is pinned in + // `./app-menu-item-shortcut-refusal-7719.test.ts`. const [first] = (result.data as unknown as { items: Record[] }).items; expect('onClick' in first).toBe(false); }); From d0a7b1133644156b17a9573c3d9a86dc6af96fa9 Mon Sep 17 00:00:00 2001 From: os-sam Date: Mon, 14 Sep 2026 12:53:39 +0000 Subject: [PATCH 3/3] fix(types): repair three prose claims the round-1 repair introduced MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit All three are the same failure mode: prose describing a prior state, published at a head that falsifies it. 1. `app-action-onclick-refusal-6854.test.ts` — the comment added last round said `shortcut` "makes this very document FAIL to parse" and that `'shortcut' in first` is "unreachable now, because there is no `first` to read". Both are false of the code six lines below: the fixture no longer carries `shortcut`, so the document parses green and `first` is destructured and read. Rewritten to say what is actually true — restored alone the assertion would still PASS and pin nothing, and making it mean anything would require putting the key back into the fixture, which is what makes the parse fail and the early return fire. 2. `app.ts` — the new docblock claimed a `grep` for `shortcut` over the file "finds that member first", meaning `AppAction`'s. This PR's own 28-line insertion reversed that: `AppMenuItem`'s refused member now comes first. A claim born false, true at the merge base and falsified by the same diff that wrote it. The ordering clause is DROPPED rather than inverted — an ordering claim is falsified by the next insertion, so the durable statement is the one that survives motion: the file declares the key twice, on two interfaces, and a position is not an owner. Co-Authored-By: Claude Claude-Session: https://claude.ai/code/session_01L5xpA5q533BgTTNADibEFt --- .../app-action-onclick-refusal-6854.test.ts | 25 ++++++++++++------- packages/types/src/app.ts | 11 +++++--- 2 files changed, 24 insertions(+), 12 deletions(-) diff --git a/packages/types/src/__tests__/app-action-onclick-refusal-6854.test.ts b/packages/types/src/__tests__/app-action-onclick-refusal-6854.test.ts index a271f22ac4..f395aaa622 100644 --- a/packages/types/src/__tests__/app-action-onclick-refusal-6854.test.ts +++ b/packages/types/src/__tests__/app-action-onclick-refusal-6854.test.ts @@ -134,15 +134,22 @@ describe('why the cast could never have been fed by an author (objectui#6854 Zon // which is the point of this file: whether a key survived a parse is not something // any static type can answer. // - // ⭐ It used to be a PAIR, asserting `onClick` and `shortcut` both gone. objectui#7719 - // split the two contracts, so only the `onClick` half belongs here: `shortcut` is no - // longer an UNDECLARED key that gets scrubbed, it is a declared `retirementTombstone` - // that makes this very document FAIL to parse. ⛔ Do not restore the second assertion - // — `'shortcut' in first` is unreachable now, because there is no `first` to read. - // The refused case is the row named "an authored `shortcut` on the same item is - // REFUSED, not dropped" — cited by NAME, because a positional reference goes stale - // the moment a row is inserted. The contract itself is pinned in - // `./app-menu-item-shortcut-refusal-7719.test.ts`. + // ⭐ It used to be a PAIR, asserting `onClick` and `shortcut` both gone, driven by a + // fixture that carried both keys. objectui#7719 split the two contracts, so only the + // `onClick` half belongs here — and `authored` above no longer carries `shortcut` at + // all. `shortcut` is no longer an UNDECLARED key that gets scrubbed; it is a declared + // `retirementTombstone`. + // + // ⛔ Do not restore the second assertion. Restored ALONE it would still pass, because + // this fixture has no `shortcut` for the parse to refuse — a green row asserting the + // absence of a key nobody wrote, which pins nothing. Making it mean anything would + // require putting `shortcut` back into `authored`, and THAT is what turns this block + // red: the parse fails, `result.success` is false, and the early return above fires + // before `first` is ever destructured. So the two halves cannot share one fixture any + // more, which is the whole reason they were split. The refusal is pinned by the row + // named "an authored `shortcut` on the same item is REFUSED, not dropped" — cited by + // NAME, because a positional reference goes stale the moment a row is inserted — and + // the contract itself by `./app-menu-item-shortcut-refusal-7719.test.ts`. const [first] = (result.data as unknown as { items: Record[] }).items; expect('onClick' in first).toBe(false); }); diff --git a/packages/types/src/app.ts b/packages/types/src/app.ts index a286698a02..850ee60718 100644 --- a/packages/types/src/app.ts +++ b/packages/types/src/app.ts @@ -594,9 +594,14 @@ export interface AppMenuItem { * * ⚠️ NOT the same key as {@link AppAction.shortcut}, which is declared, * authorable and deliberately untouched — that one is the header BUTTON's own - * shortcut, one level up from these items. A `grep` for `shortcut` over this - * file finds that member first, and reading it as this one is how the widening - * the ruling refused looks like work already done. + * shortcut, one level up from these items. This file declares `shortcut` TWICE, + * on two different interfaces, and reading one as the other is how the widening + * the ruling refused looks like work already done. ⛔ Resolve which declaration + * OWNS a hit before acting on it; a `grep` reports positions, and a position is + * not an owner. (⚠️ This paragraph deliberately states no ORDER between the two: + * an ordering claim is falsified by the next insertion into this file — including + * the one that introduced this very docblock, which reversed the order a previous + * draft of this sentence asserted.) * * ⛔ No read was re-added in the standalone runner's `LayoutRenderer`; the * objectui#6854 pin stands. Both halves of this refusal are pinned in