Skip to content

Commit c684d00

Browse files
Elon Muskclaude
andauthored
feat(spec): retire record:highlights highlight-field icon — advertised on six surfaces, drawn by nothing (#10054, ADR-0049) (#10910)
* feat(spec): retire record:highlights highlight-field icon — advertised on six surfaces, drawn by nothing (#10054, ADR-0049) 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 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01B4h3medzvhB9rpfoja9jcw * chore: adr-0087 disposition marker on the changeset (#10054) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01B4h3medzvhB9rpfoja9jcw --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 58563be commit c684d00

8 files changed

Lines changed: 403 additions & 10 deletions

File tree

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
---
2+
"@objectstack/spec": minor
3+
---
4+
5+
feat(spec): retire `record:highlights` highlight-field `icon` — advertised on six surfaces, drawn by nothing (#10054, ADR-0049)
6+
7+
<!-- adr-0087: registered record-highlights-field-icon-removed -->
8+
9+
**BREAKING** accept-set narrowing, landing after the v17.0.0 cut (the lockstep
10+
launch-window convention ships it as `minor`; the migration prescription is
11+
registered under protocol major 18, where `os migrate meta` users will look).
12+
13+
`icon` on the object arm of `RecordHighlightsField` (`fields: [{ name, label?,
14+
icon?, … }]` on a `record:highlights` component) was a real authoring surface
15+
advertised on six author-facing surfaces — the union's own describe, the
16+
`fields` describe, the lint entry-shape prose, the reference docs, and
17+
objectui's input description — with ZERO read points, measured at the
18+
2026-08-20 census in every direction: objectui's renderer normalizes the
19+
authored object and carries `icon: f?.icon` into `HeaderHighlight`, whose chip
20+
has no icon slot (its only `icon` occurrence is a button `size="icon"`); the
21+
key is structurally unable to travel `useRegisterHighlightFields`, which
22+
registers `names: string[]`; the Studio block designer publishes the field
23+
list as a `string[]` input, so the key was never designer-publishable; and
24+
every in-tree `record:highlights` producer authors bare string arrays. So an
25+
authored `icon` parsed clean and was drawn by nothing — the #8691
26+
reference-rail-`icon` shape, on the highlight chip.
27+
28+
**What is refused:** `icon` on an object-form highlight field. The arm is
29+
`strictObject`, so the key is deleted from the shape and the unknown-key
30+
rejection carries the retirement prescription via the arm's `guidance` entry
31+
(fully-qualified key, why it was inert, the no-replacement guidance, the
32+
`os migrate meta` pointer) — surfaced through the zod-4 union collapse by
33+
`packages/lint/src/zod-issue-format.ts`'s arm unpacking.
34+
35+
**What stays accepted:** bare-string entries and `{name, label?, type?,
36+
readonly?}` objects parse byte-identically. `readonly` behaviour is untouched
37+
— it is the arm's one enforced key (#5176, HeaderHighlight's inline-edit
38+
gate). There is no replacement for `icon`: the highlight chip renders label
39+
and value only.
40+
41+
The retirement kit:
42+
43+
- strict deletion + `guidance` prescription at the schema
44+
(`packages/spec/src/ui/component.zod.ts`); the two advertising describes
45+
(the union's and `RecordHighlightsProps.fields`') no longer spell the key
46+
- ADR-0087 registration: retired-key entry `ui/RecordHighlightsField:icon` and
47+
the D2 conversion `record-highlights-field-icon-removed` (protocol 18),
48+
wired into the step-18 chain — `os migrate meta --from 17` strips the key
49+
from the object entries of every `record:highlights` `fields[]` (pure
50+
lossless delete; it never had an effect to lose)
51+
- pin tests (`component.test.ts` — the old parse-survival pin respells to the
52+
surviving surface; a refusal pin asserts the named `unrecognized_keys`
53+
rejection and its prescription through the union collapse)
54+
- generated baselines/docs follow the schema (spec-changes, upgrade guide,
55+
reference docs); `packages/lint`'s entry-shape prose corrected
56+
- objectui's plugin-detail input-description advertisement is cross-repo and
57+
follows on its own card
58+
59+
## FROM → TO
60+
61+
```ts
62+
// before — parsed green; the renderer normalized `icon` into a chip with no
63+
// icon slot, so the strip rendered identically with or without it
64+
{
65+
type: 'record:highlights',
66+
properties: {
67+
fields: ['status', { name: 'budget', label: 'Budget', icon: 'dollar-sign' }],
68+
},
69+
}
70+
71+
// after — delete the key; nothing replaces it (the chip renders label and
72+
// value only)
73+
{
74+
type: 'record:highlights',
75+
properties: {
76+
fields: ['status', { name: 'budget', label: 'Budget' }],
77+
},
78+
}
79+
```

‎content/docs/references/ui/component.mdx‎

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -551,7 +551,7 @@ const result = AIChatWindowProps.parse(data);
551551

552552
## RecordHighlightsField
553553

554-
Highlight field: bare name, or `{name,label?,icon?,type?,readonly?}`
554+
Highlight field: bare name, or `{name,label?,type?,readonly?}`
555555

556556
### Union Options
557557

@@ -571,7 +571,6 @@ Type: `string`
571571
| :--- | :--- | :--- | :--- |
572572
| **name** | `string` | ✅ | Field name on the record |
573573
| **label** | `string` | optional | Display label (overrides schema label) |
574-
| **icon** | `string` | optional | Icon name (lucide icon key) |
575574
| **type** | `string` | optional | Override cell renderer type (rare) |
576575
| **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. |
577576

@@ -586,7 +585,7 @@ Type: `string`
586585

587586
| Property | Type | Required | Description |
588587
| :--- | :--- | :--- | :--- |
589-
| **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. |
588+
| **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. |
590589
| **layout** | `Enum<'horizontal' \| 'vertical'>` | optional (default: `"horizontal"`) | Layout orientation for highlight fields |
591590
| **aria** | `{ ariaLabel?: string \| Record<string, string>; ariaDescribedBy?: string; role?: string }` | optional | ARIA accessibility attributes |
592591

‎packages/lint/src/zod-issue-format.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@
2222
* exist to replace. `aggregate.groupBy` is a union (`ChartGroupBySchema` —
2323
* bare field name or `{ field, dateGranularity?, alias? }`) and so is
2424
* `RecordHighlightsProps.fields[]` (`RecordHighlightsField` — bare field
25-
* name or `{ name, label?, icon?, type?, readonly? }`), so this is the
25+
* name or `{ name, label?, type?, readonly? }`), so this is the
2626
* common path on both surfaces. **As of #5583 it is also the only thing
2727
* carrying a STRICT rejection out of a union arm**: `ChartGroupBySchema`'s
2828
* object arm is a `strictObject` now, and the `unrecognized_keys` it raises

‎packages/spec/src/conversions/registry.ts‎

Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7256,6 +7256,150 @@ const metricFiltersRemoved: MetadataConversion = {
72567256
},
72577257
};
72587258

7259+
/**
7260+
* `record:highlights` highlight-field `icon` — a declared, advertised key with
7261+
* zero read points (#10054, ADR-0049 enforce-or-remove; maintainer ruling
7262+
* 2026-08-21, executing the 2026-08-20 census verdict).
7263+
*
7264+
* The dead-end is complete in every direction the census measured: objectui's
7265+
* renderer normalizes the authored object and carries `icon: f?.icon` into
7266+
* `HeaderHighlight`, whose chip has NO icon slot (its only `icon` occurrence
7267+
* is a button `size="icon"`); the key is structurally unable to travel
7268+
* `useRegisterHighlightFields`, which registers `names: string[]`; and the
7269+
* Studio block designer publishes the field list as a `string[]` input, so the
7270+
* key was never designer-publishable either. Meanwhile six author-facing
7271+
* surfaces advertised it — the union's own describe, the `fields` describe,
7272+
* the lint entry-shape prose, the reference docs, and objectui's input
7273+
* description. The exact #8691 reference-rail-`icon` shape, on the highlight
7274+
* chip.
7275+
*
7276+
* **A pure lossless delete.** The key never had an effect to lose: the chip
7277+
* renders label and value only, so there is no rewrite target — deleting the
7278+
* key preserves observed behaviour exactly.
7279+
*
7280+
* ⚠️ Scoped by component `type` AND by entry shape: `icon` is live on half a
7281+
* dozen other components (`element:button`, header actions, …), and the
7282+
* bare-string entries of the same `fields` array are not carriers — only the
7283+
* OBJECT entries of a `record:highlights` `fields[]` are touched. The
7284+
* neighbouring `readonly` key is LIVE (#5176 — HeaderHighlight's inline-edit
7285+
* gate reads it) and survives untouched.
7286+
*/
7287+
const recordHighlightsFieldIconRemoved: MetadataConversion = {
7288+
id: 'record-highlights-field-icon-removed',
7289+
toMajor: 18,
7290+
retiredFromLoadPath: true,
7291+
surface: 'page.component.record:highlights.fields[].icon',
7292+
summary:
7293+
"record:highlights highlight-field key 'icon' removed (#10054, ADR-0049 — no render path: "
7294+
+ 'the highlight chip has no icon slot, the register hook carries field names only, and the '
7295+
+ 'Studio designer publishes the field list as plain strings, so an authored icon was '
7296+
+ 'accepted and drawn by nothing)',
7297+
apply(stack, emit) {
7298+
return mapPageComponents(stack, (component, path) => {
7299+
if (component.type !== 'record:highlights') return component;
7300+
const properties = component.properties;
7301+
if (!isDict(properties)) return component;
7302+
const fields = properties.fields;
7303+
if (!Array.isArray(fields)) return component;
7304+
let touched = false;
7305+
const nextFields = fields.map((entry, i) => {
7306+
// Bare-string entries are not carriers — untouched, same reference.
7307+
if (!isDict(entry)) return entry;
7308+
const stripped = stripKeys(entry, ['icon'], emit, `${path}.properties.fields[${i}]`);
7309+
if (stripped !== entry) touched = true;
7310+
return stripped;
7311+
});
7312+
if (!touched) return component;
7313+
return { ...component, properties: { ...properties, fields: nextFields } };
7314+
});
7315+
},
7316+
fixture: {
7317+
before: {
7318+
pages: [
7319+
{
7320+
name: 'project_detail',
7321+
regions: [
7322+
{
7323+
name: 'main',
7324+
components: [
7325+
{
7326+
type: 'record:highlights',
7327+
properties: {
7328+
fields: [
7329+
// Bare-string entry — the shape every in-tree producer
7330+
// authors; not a carrier, rides through untouched.
7331+
'status',
7332+
// The retired shape: an object entry carrying `icon`.
7333+
{ name: 'budget', label: 'Budget', icon: 'dollar-sign' },
7334+
// The LIVE neighbour keys survive — `readonly` is the
7335+
// #5176 enforced key, `type` the renderer override.
7336+
{ name: 'health', type: 'number', readonly: true },
7337+
],
7338+
},
7339+
},
7340+
// `icon` on a component that is not record:highlights — live
7341+
// there, and not this entry's key.
7342+
{ type: 'element:button', properties: { label: 'Open', icon: 'external-link' } },
7343+
],
7344+
},
7345+
],
7346+
},
7347+
// The named-slot shape (#6776 reach): a highlights strip authored into
7348+
// a slotted record page's `highlights` slot.
7349+
{
7350+
name: 'task_detail',
7351+
kind: 'slotted',
7352+
regions: [],
7353+
slots: {
7354+
highlights: {
7355+
type: 'record:highlights',
7356+
properties: { fields: [{ name: 'priority', icon: 'flag' }] },
7357+
},
7358+
},
7359+
},
7360+
],
7361+
},
7362+
after: {
7363+
pages: [
7364+
{
7365+
name: 'project_detail',
7366+
regions: [
7367+
{
7368+
name: 'main',
7369+
components: [
7370+
{
7371+
type: 'record:highlights',
7372+
properties: {
7373+
fields: [
7374+
'status',
7375+
{ name: 'budget', label: 'Budget' },
7376+
{ name: 'health', type: 'number', readonly: true },
7377+
],
7378+
},
7379+
},
7380+
{ type: 'element:button', properties: { label: 'Open', icon: 'external-link' } },
7381+
],
7382+
},
7383+
],
7384+
},
7385+
{
7386+
name: 'task_detail',
7387+
kind: 'slotted',
7388+
regions: [],
7389+
slots: {
7390+
highlights: {
7391+
type: 'record:highlights',
7392+
properties: { fields: [{ name: 'priority' }] },
7393+
},
7394+
},
7395+
},
7396+
],
7397+
},
7398+
// Two notices: the region-level object entry and the slotted one.
7399+
expectedNotices: 2,
7400+
},
7401+
};
7402+
72597403
export const CONVERSIONS_BY_MAJOR: Readonly<Record<number, readonly MetadataConversion[]>> = {
72607404
11: [flowNodeHttpRename, pageKindJsxToHtml, flowNodeFilterAlias, objectCompactLayoutRename],
72617405
13: [stackRolesToPositions, owdLegacyReadAliases, sharingRecipientRoleToPosition],
@@ -7335,6 +7479,7 @@ export const CONVERSIONS_BY_MAJOR: Readonly<Record<number, readonly MetadataConv
73357479
elementFilterRemoved,
73367480
fieldColumnListsCanonicalized,
73377481
metricFiltersRemoved,
7482+
recordHighlightsFieldIconRemoved,
73387483
],
73397484
};
73407485

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.
2+
3+
// #10054 — ADR-0049 enforce-or-remove (maintainer ruling 2026-08-21, executing
4+
// the 2026-08-20 census verdict). `icon` on the object arm of
5+
// `RecordHighlightsField` was declared, described (`Icon name (lucide icon
6+
// key)`), and advertised on six author-facing surfaces — with ZERO read
7+
// points, measured in every direction: objectui's renderer normalizes the
8+
// authored object and carries `icon: f?.icon` into `HeaderHighlight`, whose
9+
// chip has NO icon slot (its only `icon` occurrence is a button
10+
// `size="icon"`); the key is structurally unable to travel
11+
// `useRegisterHighlightFields`, which registers `names: string[]`; the Studio
12+
// block designer publishes the field list as a `string[]` input, so the key
13+
// was never designer-publishable; and all in-tree `record:highlights`
14+
// producers author bare string arrays. The exact shape #8691 recorded for the
15+
// reference-rail `icon`: declared and normalized, drawn by nothing — an
16+
// authored value parsed clean and cost the author silently. The neighbouring
17+
// `readonly` key is LIVE (#5176, HeaderHighlight's inline-edit gate) and is
18+
// untouched.
19+
//
20+
// Registered under 18, not 17: v17.0.0 was cut before this landed, so the
21+
// removal ships on the 17.x line (launch-window convention: accept-set
22+
// narrowings ride minor releases) and the prescription lives at the major
23+
// boundary where `migrate meta` users look (the #8495 / PR #8666 precedent).
24+
// The object arm is `strictObject`, so the route is strict deletion + a
25+
// `guidance` entry carrying the prescription (no retiredKey tombstone — the
26+
// key is out of the walked shape entirely, and the refusal is the arm's own
27+
// named `unrecognized_keys`, unpacked through the zod-4 union collapse by
28+
// `packages/lint/src/zod-issue-format.ts`; the `data/Metric:filters` route).
29+
// Sources are rewritten by the D2 conversion
30+
// `record-highlights-field-icon-removed`, which strips the key from the object
31+
// entries of every `record:highlights` `fields[]` (pure lossless delete — the
32+
// chip renders label and value only, so the key never had an effect to lose).
33+
export const entry = 'ui/RecordHighlightsField:icon';

‎packages/spec/src/migrations/registry.ts‎

Lines changed: 44 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5111,14 +5111,26 @@ const step18: MigrationStep = {
51115111
'objectui\'s ThemeEngine/ThemeContext and their unit tests are retained. Semantic ' +
51125112
'rather than mechanical: an authored palette has no lossless target (N themes vs ' +
51135113
'M apps is a judgment), so the entry prescribes the hand move instead of deleting ' +
5114-
'authored content silently.',
5114+
'authored content silently. ' +
5115+
'It also retires the `record:highlights` highlight-field `icon` (#10054, ADR-0049 ' +
5116+
'enforce-or-remove; maintainer ruling 2026-08-21, executing the 2026-08-20 census ' +
5117+
'verdict): a declared key with zero read points in any direction — objectui\'s renderer ' +
5118+
'normalized the authored object and carried `icon` into a highlight chip with no icon ' +
5119+
'slot, `useRegisterHighlightFields` registers field NAMES only (structurally unable to ' +
5120+
'carry it), and the Studio designer publishes the field list as plain strings — while ' +
5121+
'six author-facing surfaces advertised the key (the #8691 reference-rail-`icon` shape, ' +
5122+
'on the highlight chip). The mechanical conversion strips the key from the object ' +
5123+
'entries of every `record:highlights` `fields[]` (pure lossless delete — the chip ' +
5124+
'renders label and value only, so it never had an effect to lose); there is no ' +
5125+
'replacement, and the live neighbour `readonly` (#5176) is untouched.',
51155126
conversionIds: [
51165127
'field-malformed-scale-precision-removed',
51175128
'record-chatter-position-vocabulary',
51185129
'element-input-target-variable-removed',
51195130
'element-filter-removed',
51205131
'field-column-lists-canonicalized',
51215132
'metric-filters-removed',
5133+
'record-highlights-field-icon-removed',
51225134
],
51235135
semantic: [
51245136
// One file per entry under `entries/semantic/`, concatenated here sorted by
@@ -6736,6 +6748,37 @@ export const RETIRED_KEYS_BY_MAJOR: Readonly<Record<number, readonly string[]>>
67366748
// `element-input-target-variable-removed` (a page component IS a stack
67376749
// collection member, unlike the `kernel/Manifest:loading` family).
67386750
'ui/ElementTextInputProps:targetVariable',
6751+
// #10054 — ADR-0049 enforce-or-remove (maintainer ruling 2026-08-21, executing
6752+
// the 2026-08-20 census verdict). `icon` on the object arm of
6753+
// `RecordHighlightsField` was declared, described (`Icon name (lucide icon
6754+
// key)`), and advertised on six author-facing surfaces — with ZERO read
6755+
// points, measured in every direction: objectui's renderer normalizes the
6756+
// authored object and carries `icon: f?.icon` into `HeaderHighlight`, whose
6757+
// chip has NO icon slot (its only `icon` occurrence is a button
6758+
// `size="icon"`); the key is structurally unable to travel
6759+
// `useRegisterHighlightFields`, which registers `names: string[]`; the Studio
6760+
// block designer publishes the field list as a `string[]` input, so the key
6761+
// was never designer-publishable; and all in-tree `record:highlights`
6762+
// producers author bare string arrays. The exact shape #8691 recorded for the
6763+
// reference-rail `icon`: declared and normalized, drawn by nothing — an
6764+
// authored value parsed clean and cost the author silently. The neighbouring
6765+
// `readonly` key is LIVE (#5176, HeaderHighlight's inline-edit gate) and is
6766+
// untouched.
6767+
//
6768+
// Registered under 18, not 17: v17.0.0 was cut before this landed, so the
6769+
// removal ships on the 17.x line (launch-window convention: accept-set
6770+
// narrowings ride minor releases) and the prescription lives at the major
6771+
// boundary where `migrate meta` users look (the #8495 / PR #8666 precedent).
6772+
// The object arm is `strictObject`, so the route is strict deletion + a
6773+
// `guidance` entry carrying the prescription (no retiredKey tombstone — the
6774+
// key is out of the walked shape entirely, and the refusal is the arm's own
6775+
// named `unrecognized_keys`, unpacked through the zod-4 union collapse by
6776+
// `packages/lint/src/zod-issue-format.ts`; the `data/Metric:filters` route).
6777+
// Sources are rewritten by the D2 conversion
6778+
// `record-highlights-field-icon-removed`, which strips the key from the object
6779+
// entries of every `record:highlights` `fields[]` (pure lossless delete — the
6780+
// chip renders label and value only, so the key never had an effect to lose).
6781+
'ui/RecordHighlightsField:icon',
67396782
// </os-generated retired-key:18>
67406783
],
67416784
};

0 commit comments

Comments
 (0)