From bc7ddc7ac4e01c6246a2adadd103267a303d48ac Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 21 Aug 2026 15:50:27 +0000 Subject: [PATCH 1/2] =?UTF-8?q?feat(spec):=20retire=20record:highlights=20?= =?UTF-8?q?highlight-field=20icon=20=E2=80=94=20advertised=20on=20six=20su?= =?UTF-8?q?rfaces,=20drawn=20by=20nothing=20(#10054,=20ADR-0049)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Strict deletion + guidance prescription at the union's object arm (strictObject route, the data/Metric:filters precedent — no retiredKey tombstone); the two advertising describes corrected; ADR-0087 registration (retired-key entry ui/RecordHighlightsField:icon, D2 conversion record-highlights-field-icon-removed, step-18 chain); packages/lint entry-shape prose corrected; pin flips + refusal pins through the zod-4 union collapse; regenerated reference docs; minor changeset per the launch-window convention. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01B4h3medzvhB9rpfoja9jcw --- .changeset/record-highlights-icon-retired.md | 77 ++++++++++ content/docs/references/ui/component.mdx | 5 +- packages/lint/src/zod-issue-format.ts | 2 +- packages/spec/src/conversions/registry.ts | 145 ++++++++++++++++++ .../18.ui__RecordHighlightsField__icon.ts | 33 ++++ packages/spec/src/migrations/registry.ts | 45 +++++- packages/spec/src/ui/component.test.ts | 70 ++++++++- packages/spec/src/ui/component.zod.ts | 34 +++- 8 files changed, 401 insertions(+), 10 deletions(-) create mode 100644 .changeset/record-highlights-icon-retired.md create mode 100644 packages/spec/src/migrations/entries/retired-keys/18.ui__RecordHighlightsField__icon.ts diff --git a/.changeset/record-highlights-icon-retired.md b/.changeset/record-highlights-icon-retired.md new file mode 100644 index 0000000000..dc1f25cd76 --- /dev/null +++ b/.changeset/record-highlights-icon-retired.md @@ -0,0 +1,77 @@ +--- +"@objectstack/spec": minor +--- + +feat(spec): retire `record:highlights` highlight-field `icon` — advertised on six surfaces, drawn by nothing (#10054, ADR-0049) + +**BREAKING** accept-set narrowing, landing after the v17.0.0 cut (the lockstep +launch-window convention ships it as `minor`; the migration prescription is +registered under protocol major 18, where `os migrate meta` users will look). + +`icon` on the object arm of `RecordHighlightsField` (`fields: [{ name, label?, +icon?, … }]` on a `record:highlights` component) was a real authoring surface +advertised on six author-facing surfaces — the union's own describe, the +`fields` describe, the lint entry-shape prose, the reference docs, and +objectui's input description — with ZERO read points, measured at the +2026-08-20 census in every direction: objectui's renderer normalizes the +authored object and carries `icon: f?.icon` into `HeaderHighlight`, whose chip +has no icon slot (its only `icon` occurrence is a button `size="icon"`); the +key is structurally unable to travel `useRegisterHighlightFields`, which +registers `names: string[]`; the Studio block designer publishes the field +list as a `string[]` input, so the key was never designer-publishable; and +every in-tree `record:highlights` producer authors bare string arrays. So an +authored `icon` parsed clean and was drawn by nothing — the #8691 +reference-rail-`icon` shape, on the highlight chip. + +**What is refused:** `icon` on an object-form highlight field. The arm is +`strictObject`, so the key is deleted from the shape and the unknown-key +rejection carries the retirement prescription via the arm's `guidance` entry +(fully-qualified key, why it was inert, the no-replacement guidance, the +`os migrate meta` pointer) — surfaced through the zod-4 union collapse by +`packages/lint/src/zod-issue-format.ts`'s arm unpacking. + +**What stays accepted:** bare-string entries and `{name, label?, type?, +readonly?}` objects parse byte-identically. `readonly` behaviour is untouched +— it is the arm's one enforced key (#5176, HeaderHighlight's inline-edit +gate). There is no replacement for `icon`: the highlight chip renders label +and value only. + +The retirement kit: + +- strict deletion + `guidance` prescription at the schema + (`packages/spec/src/ui/component.zod.ts`); the two advertising describes + (the union's and `RecordHighlightsProps.fields`') no longer spell the key +- ADR-0087 registration: retired-key entry `ui/RecordHighlightsField:icon` and + the D2 conversion `record-highlights-field-icon-removed` (protocol 18), + wired into the step-18 chain — `os migrate meta --from 17` strips the key + from the object entries of every `record:highlights` `fields[]` (pure + lossless delete; it never had an effect to lose) +- pin tests (`component.test.ts` — the old parse-survival pin respells to the + surviving surface; a refusal pin asserts the named `unrecognized_keys` + rejection and its prescription through the union collapse) +- generated baselines/docs follow the schema (spec-changes, upgrade guide, + reference docs); `packages/lint`'s entry-shape prose corrected +- objectui's plugin-detail input-description advertisement is cross-repo and + follows on its own card + +## FROM → TO + +```ts +// before — parsed green; the renderer normalized `icon` into a chip with no +// icon slot, so the strip rendered identically with or without it +{ + type: 'record:highlights', + properties: { + fields: ['status', { name: 'budget', label: 'Budget', icon: 'dollar-sign' }], + }, +} + +// after — delete the key; nothing replaces it (the chip renders label and +// value only) +{ + type: 'record:highlights', + properties: { + fields: ['status', { name: 'budget', label: 'Budget' }], + }, +} +``` diff --git a/content/docs/references/ui/component.mdx b/content/docs/references/ui/component.mdx index 20c9e899db..5269c1a6e3 100644 --- a/content/docs/references/ui/component.mdx +++ b/content/docs/references/ui/component.mdx @@ -551,7 +551,7 @@ const result = AIChatWindowProps.parse(data); ## RecordHighlightsField -Highlight field: bare name, or `{name,label?,icon?,type?,readonly?}` +Highlight field: bare name, or `{name,label?,type?,readonly?}` ### Union Options @@ -571,7 +571,6 @@ Type: `string` | :--- | :--- | :--- | :--- | | **name** | `string` | ✅ | Field name on the record | | **label** | `string` | optional | Display label (overrides schema label) | -| **icon** | `string` | optional | Icon name (lucide icon key) | | **type** | `string` | optional | Override cell renderer type (rare) | | **readonly** | `boolean` | optional | Render this chip read-only — suppresses inline editing on the highlight card. Use for hook/automation-maintained columns that must not be hand-edited from the record header. | @@ -586,7 +585,7 @@ Type: `string` | Property | Type | Required | Description | | :--- | :--- | :--- | :--- | -| **fields** | `(string \| { name: string; label?: string; icon?: string; type?: string; … })[]` | ✅ | Key fields to highlight (1-7 fields max, typically displayed as prominent cards). Each item may be a bare field name or `{name, label?, icon?, type?, readonly?}` for inline overrides. | +| **fields** | `(string \| { name: string; label?: string; type?: string; readonly?: boolean })[]` | ✅ | Key fields to highlight (1-7 fields max, typically displayed as prominent cards). Each item may be a bare field name or `{name, label?, type?, readonly?}` for inline overrides. | | **layout** | `Enum<'horizontal' \| 'vertical'>` | optional (default: `"horizontal"`) | Layout orientation for highlight fields | | **aria** | `{ ariaLabel?: string \| Record; ariaDescribedBy?: string; role?: string }` | optional | ARIA accessibility attributes | diff --git a/packages/lint/src/zod-issue-format.ts b/packages/lint/src/zod-issue-format.ts index 1b6c8808bf..79444461d2 100644 --- a/packages/lint/src/zod-issue-format.ts +++ b/packages/lint/src/zod-issue-format.ts @@ -22,7 +22,7 @@ * exist to replace. `aggregate.groupBy` is a union (`ChartGroupBySchema` — * bare field name or `{ field, dateGranularity?, alias? }`) and so is * `RecordHighlightsProps.fields[]` (`RecordHighlightsField` — bare field - * name or `{ name, label?, icon?, type?, readonly? }`), so this is the + * name or `{ name, label?, type?, readonly? }`), so this is the * common path on both surfaces. **As of #5583 it is also the only thing * carrying a STRICT rejection out of a union arm**: `ChartGroupBySchema`'s * object arm is a `strictObject` now, and the `unrecognized_keys` it raises diff --git a/packages/spec/src/conversions/registry.ts b/packages/spec/src/conversions/registry.ts index 2d6333e119..0361fc6e52 100644 --- a/packages/spec/src/conversions/registry.ts +++ b/packages/spec/src/conversions/registry.ts @@ -7256,6 +7256,150 @@ const metricFiltersRemoved: MetadataConversion = { }, }; +/** + * `record:highlights` highlight-field `icon` — a declared, advertised key with + * zero read points (#10054, ADR-0049 enforce-or-remove; maintainer ruling + * 2026-08-21, executing the 2026-08-20 census verdict). + * + * The dead-end is complete in every direction the census measured: objectui's + * renderer normalizes the authored object and carries `icon: f?.icon` into + * `HeaderHighlight`, whose chip has NO icon slot (its only `icon` occurrence + * is a button `size="icon"`); the key is structurally unable to travel + * `useRegisterHighlightFields`, which registers `names: string[]`; and the + * Studio block designer publishes the field list as a `string[]` input, so the + * key was never designer-publishable either. Meanwhile six author-facing + * surfaces advertised it — the union's own describe, the `fields` describe, + * the lint entry-shape prose, the reference docs, and objectui's input + * description. The exact #8691 reference-rail-`icon` shape, on the highlight + * chip. + * + * **A pure lossless delete.** The key never had an effect to lose: the chip + * renders label and value only, so there is no rewrite target — deleting the + * key preserves observed behaviour exactly. + * + * ⚠️ Scoped by component `type` AND by entry shape: `icon` is live on half a + * dozen other components (`element:button`, header actions, …), and the + * bare-string entries of the same `fields` array are not carriers — only the + * OBJECT entries of a `record:highlights` `fields[]` are touched. The + * neighbouring `readonly` key is LIVE (#5176 — HeaderHighlight's inline-edit + * gate reads it) and survives untouched. + */ +const recordHighlightsFieldIconRemoved: MetadataConversion = { + id: 'record-highlights-field-icon-removed', + toMajor: 18, + retiredFromLoadPath: true, + surface: 'page.component.record:highlights.fields[].icon', + summary: + "record:highlights highlight-field key 'icon' removed (#10054, ADR-0049 — no render path: " + + 'the highlight chip has no icon slot, the register hook carries field names only, and the ' + + 'Studio designer publishes the field list as plain strings, so an authored icon was ' + + 'accepted and drawn by nothing)', + apply(stack, emit) { + return mapPageComponents(stack, (component, path) => { + if (component.type !== 'record:highlights') return component; + const properties = component.properties; + if (!isDict(properties)) return component; + const fields = properties.fields; + if (!Array.isArray(fields)) return component; + let touched = false; + const nextFields = fields.map((entry, i) => { + // Bare-string entries are not carriers — untouched, same reference. + if (!isDict(entry)) return entry; + const stripped = stripKeys(entry, ['icon'], emit, `${path}.properties.fields[${i}]`); + if (stripped !== entry) touched = true; + return stripped; + }); + if (!touched) return component; + return { ...component, properties: { ...properties, fields: nextFields } }; + }); + }, + fixture: { + before: { + pages: [ + { + name: 'project_detail', + regions: [ + { + name: 'main', + components: [ + { + type: 'record:highlights', + properties: { + fields: [ + // Bare-string entry — the shape every in-tree producer + // authors; not a carrier, rides through untouched. + 'status', + // The retired shape: an object entry carrying `icon`. + { name: 'budget', label: 'Budget', icon: 'dollar-sign' }, + // The LIVE neighbour keys survive — `readonly` is the + // #5176 enforced key, `type` the renderer override. + { name: 'health', type: 'number', readonly: true }, + ], + }, + }, + // `icon` on a component that is not record:highlights — live + // there, and not this entry's key. + { type: 'element:button', properties: { label: 'Open', icon: 'external-link' } }, + ], + }, + ], + }, + // The named-slot shape (#6776 reach): a highlights strip authored into + // a slotted record page's `highlights` slot. + { + name: 'task_detail', + kind: 'slotted', + regions: [], + slots: { + highlights: { + type: 'record:highlights', + properties: { fields: [{ name: 'priority', icon: 'flag' }] }, + }, + }, + }, + ], + }, + after: { + pages: [ + { + name: 'project_detail', + regions: [ + { + name: 'main', + components: [ + { + type: 'record:highlights', + properties: { + fields: [ + 'status', + { name: 'budget', label: 'Budget' }, + { name: 'health', type: 'number', readonly: true }, + ], + }, + }, + { type: 'element:button', properties: { label: 'Open', icon: 'external-link' } }, + ], + }, + ], + }, + { + name: 'task_detail', + kind: 'slotted', + regions: [], + slots: { + highlights: { + type: 'record:highlights', + properties: { fields: [{ name: 'priority' }] }, + }, + }, + }, + ], + }, + // Two notices: the region-level object entry and the slotted one. + expectedNotices: 2, + }, +}; + export const CONVERSIONS_BY_MAJOR: Readonly> = { 11: [flowNodeHttpRename, pageKindJsxToHtml, flowNodeFilterAlias, objectCompactLayoutRename], 13: [stackRolesToPositions, owdLegacyReadAliases, sharingRecipientRoleToPosition], @@ -7335,6 +7479,7 @@ export const CONVERSIONS_BY_MAJOR: Readonly> // `element-input-target-variable-removed` (a page component IS a stack // collection member, unlike the `kernel/Manifest:loading` family). 'ui/ElementTextInputProps:targetVariable', + // #10054 — ADR-0049 enforce-or-remove (maintainer ruling 2026-08-21, executing + // the 2026-08-20 census verdict). `icon` on the object arm of + // `RecordHighlightsField` was declared, described (`Icon name (lucide icon + // key)`), and advertised on six author-facing surfaces — with ZERO read + // points, measured in every direction: objectui's renderer normalizes the + // authored object and carries `icon: f?.icon` into `HeaderHighlight`, whose + // chip has NO icon slot (its only `icon` occurrence is a button + // `size="icon"`); the key is structurally unable to travel + // `useRegisterHighlightFields`, which registers `names: string[]`; the Studio + // block designer publishes the field list as a `string[]` input, so the key + // was never designer-publishable; and all in-tree `record:highlights` + // producers author bare string arrays. The exact shape #8691 recorded for the + // reference-rail `icon`: declared and normalized, drawn by nothing — an + // authored value parsed clean and cost the author silently. The neighbouring + // `readonly` key is LIVE (#5176, HeaderHighlight's inline-edit gate) and is + // untouched. + // + // Registered under 18, not 17: v17.0.0 was cut before this landed, so the + // removal ships on the 17.x line (launch-window convention: accept-set + // narrowings ride minor releases) and the prescription lives at the major + // boundary where `migrate meta` users look (the #8495 / PR #8666 precedent). + // The object arm is `strictObject`, so the route is strict deletion + a + // `guidance` entry carrying the prescription (no retiredKey tombstone — the + // key is out of the walked shape entirely, and the refusal is the arm's own + // named `unrecognized_keys`, unpacked through the zod-4 union collapse by + // `packages/lint/src/zod-issue-format.ts`; the `data/Metric:filters` route). + // Sources are rewritten by the D2 conversion + // `record-highlights-field-icon-removed`, which strips the key from the object + // entries of every `record:highlights` `fields[]` (pure lossless delete — the + // chip renders label and value only, so the key never had an effect to lose). + 'ui/RecordHighlightsField:icon', // ], }; diff --git a/packages/spec/src/ui/component.test.ts b/packages/spec/src/ui/component.test.ts index 64157d477a..c47a4eef57 100644 --- a/packages/spec/src/ui/component.test.ts +++ b/packages/spec/src/ui/component.test.ts @@ -703,11 +703,77 @@ describe('RecordHighlightsProps', () => { }); it('should still accept bare-string and other object-form highlight fields', () => { + // #10054 fixture triage: this pin used to author `icon: 'flag'` — the one + // in-repo writer of the key that change retired. Respelled rather than + // deleted: the pin guards the SURVIVING object-arm surface, which is + // {name, label?, type?, readonly?}. const result = RecordHighlightsProps.parse({ - fields: ['name', { name: 'status', label: 'State', icon: 'flag' }], + fields: ['name', { name: 'status', label: 'State', type: 'text', readonly: true }], }); expect(result.fields[0]).toBe('name'); - expect(result.fields[1]).toEqual({ name: 'status', label: 'State', icon: 'flag' }); + expect(result.fields[1]).toEqual({ name: 'status', label: 'State', type: 'text', readonly: true }); + }); + + // ── #10054 — `icon` retired from the object arm (ADR-0049 / ADR-0087) ──── + // + // Measured dead (census 2026-08-20): the renderer normalized `icon` into a + // chip with no icon slot, `useRegisterHighlightFields` carries names only, + // and the Studio designer publishes the field list as `string[]` — while six + // author-facing surfaces advertised the key. The arm is `strictObject`, so + // the route is strict deletion + a `guidance` prescription and the refusal + // is the arm's own named `unrecognized_keys` (the `data/Metric:filters` + // route, not a `retiredKey` tombstone). + // + // Reverse-verified from the committed state: restoring the `icon` line turns + // the refusal pin red (the parse succeeds) — the pin reads the live schema, + // not a cached shape. + describe('retired icon on the RecordHighlightsField object arm (#10054)', () => { + /** + * Dig the object arm's own issues out of the zod-4 union collapse: the + * union reports ONE `invalid_union` whose `errors` tucks each arm's real + * issues away (the #5583 shape — `zod-issue-format.ts` unpacks this for + * authors; here the pin asserts the raw material it unpacks). + */ + const collapsedUnion = (input: unknown) => { + const r = RecordHighlightsProps.safeParse(input); + expect(r.success).toBe(false); + if (r.success) throw new Error('unreachable'); + const union = r.error.issues.find((i) => i.code === 'invalid_union') as { + code: string; + path: PropertyKey[]; + errors?: ReadonlyArray>; + } | undefined; + expect(union, 'the union collapse carries the arm issues').toBeDefined(); + return union!; + }; + + it('refuses an authored icon as a named unrecognized_keys rejection carrying the prescription', () => { + const union = collapsedUnion({ fields: [{ name: 'status', icon: 'flag' }] }); + // The right path: the offending entry, not the whole props bag. + expect(union.path).toEqual(['fields', 0]); + const armIssue = (union.errors ?? []).flat().find((i) => i.code === 'unrecognized_keys'); + expect(armIssue, "the object arm's own unrecognized_keys issue").toBeDefined(); + expect(armIssue!.keys).toContain('icon'); + // The named surface and the retirement prescription — citation, the + // "drawn by nothing" story, and the no-replacement guidance. The `s` + // flag is house style: the message spans lines. + expect(armIssue!.message).toMatch(/this `record:highlights` field/); + expect(armIssue!.message).toMatch(/`record:highlights` field `icon` was removed .*#10054.*ADR-0049/s); + expect(armIssue!.message).toMatch(/Delete the key — no replacement: the renderer never drew it/s); + expect(armIssue!.message).toMatch(/os migrate meta --from 17/); + }); + + it('control: the same entry without icon parses clean — the refusal is about the key, not the arm', () => { + const result = RecordHighlightsProps.parse({ fields: [{ name: 'status' }] }); + expect(result.fields[0]).toEqual({ name: 'status' }); + }); + + it('readonly behaviour is untouched by the retirement', () => { + const result = RecordHighlightsProps.parse({ fields: [{ name: 'supply_share', readonly: true }] }); + expect((result.fields[0] as { readonly?: boolean }).readonly).toBe(true); + }); }); }); diff --git a/packages/spec/src/ui/component.zod.ts b/packages/spec/src/ui/component.zod.ts index 33f0530a63..b799b6cf34 100644 --- a/packages/spec/src/ui/component.zod.ts +++ b/packages/spec/src/ui/component.zod.ts @@ -997,10 +997,38 @@ export const RecordHighlightsField = z.union([ // `fieldRefsFrom`, which had to handle both). field: 'name', }, + guidance: { + // REMOVED (#10054, ADR-0049 enforce-or-remove; maintainer ruling + // 2026-08-21): `icon` was declared and advertised on six author-facing + // surfaces with ZERO read points, measured at the 2026-08-20 census: + // objectui's renderer normalized the authored object and carried + // `icon: f?.icon` into `HeaderHighlight`, whose chip has no icon slot + // (its only `icon` occurrence is a button `size="icon"`), and the key + // could travel nowhere else — `useRegisterHighlightFields` registers + // `names: string[]` (structurally unable to carry it) and the Studio + // block designer publishes the field list as a `string[]` input. So an + // authored value parsed clean and was drawn by nothing — the #8691 + // reference-rail `icon` shape, on the highlight chip. This arm is + // `strictObject`, so the route is strict deletion + this `guidance` + // entry carrying the prescription (the `data/Metric:filters` precedent; + // no `retiredKey` tombstone — the key is out of the walked shape + // entirely, and the refusal is the arm's own named `unrecognized_keys`). + icon: + '`record:highlights` field `icon` was removed in @objectstack/spec 17 (#10054, ADR-0049) — ' + + 'no render path ever read it: the renderer normalized the authored object and passed ' + + '`icon` to a highlight chip with no icon slot, and the key could travel nowhere else ' + + '(`useRegisterHighlightFields` registers field NAMES only; the Studio designer publishes ' + + 'the field list as plain strings), so an authored value was accepted and drawn by ' + + 'nothing. Delete the key — no replacement: the renderer never drew it, and the chip ' + + 'renders label and value only. ' + + 'Run `os migrate meta --from 17` to list the mechanical edits for existing sources; apply them by hand.', + }, }, { name: z.string().describe('Field name on the record'), label: z.string().optional().describe('Display label (overrides schema label)'), - icon: z.string().optional().describe('Icon name (lucide icon key)'), + // `icon` was REMOVED here (#10054) — see the `guidance` entry above for + // the full story. There is no replacement mechanism: the highlight chip + // renders label and value only, so the key never had an effect to lose. type: z.string().optional().describe('Override cell renderer type (rare)'), // #5176 — declared because it is already enforced: the renderer's // HeaderHighlight gate refuses inline editing on a chip carrying it. Kept @@ -1010,7 +1038,7 @@ export const RecordHighlightsField = z.union([ // column editable again with no diagnostic anywhere. readonly: z.boolean().optional().describe('Render this chip read-only — suppresses inline editing on the highlight card. Use for hook/automation-maintained columns that must not be hand-edited from the record header.'), }), -]).describe('Highlight field: bare name, or {name,label?,icon?,type?,readonly?}'); +]).describe('Highlight field: bare name, or {name,label?,type?,readonly?}'); export type RecordHighlightsField = z.input; export const RecordHighlightsProps = strictObject({ @@ -1018,7 +1046,7 @@ export const RecordHighlightsProps = strictObject({ history: PROPS_HISTORY, guidanceSets: COMPONENT_LEVEL_GUIDANCE, }, { - fields: z.array(RecordHighlightsField).min(1).max(7).describe('Key fields to highlight (1-7 fields max, typically displayed as prominent cards). Each item may be a bare field name or {name, label?, icon?, type?, readonly?} for inline overrides.'), + fields: z.array(RecordHighlightsField).min(1).max(7).describe('Key fields to highlight (1-7 fields max, typically displayed as prominent cards). Each item may be a bare field name or {name, label?, type?, readonly?} for inline overrides.'), layout: z.enum(['horizontal', 'vertical']).default('horizontal').describe('Layout orientation for highlight fields'), /** ARIA accessibility */ aria: AriaPropsSchema.optional().describe('ARIA accessibility attributes'), From d04044beea75061000cc1ad6b1551466f66ea861 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 21 Aug 2026 16:36:11 +0000 Subject: [PATCH 2/2] chore: adr-0087 disposition marker on the changeset (#10054) Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01B4h3medzvhB9rpfoja9jcw --- .changeset/record-highlights-icon-retired.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.changeset/record-highlights-icon-retired.md b/.changeset/record-highlights-icon-retired.md index dc1f25cd76..0a10fc0730 100644 --- a/.changeset/record-highlights-icon-retired.md +++ b/.changeset/record-highlights-icon-retired.md @@ -4,6 +4,8 @@ feat(spec): retire `record:highlights` highlight-field `icon` — advertised on six surfaces, drawn by nothing (#10054, ADR-0049) + + **BREAKING** accept-set narrowing, landing after the v17.0.0 cut (the lockstep launch-window convention ships it as `minor`; the migration prescription is registered under protocol major 18, where `os migrate meta` users will look).