diff --git a/.changeset/liveness-ledger-followthrough-colorvariant-field-widget.md b/.changeset/liveness-ledger-followthrough-colorvariant-field-widget.md new file mode 100644 index 0000000000..aff0fe64ba --- /dev/null +++ b/.changeset/liveness-ledger-followthrough-colorvariant-field-widget.md @@ -0,0 +1,65 @@ +--- +"@objectstack/spec": patch +"@objectstack/lint": patch +--- + +fix(spec): liveness-ledger follow-through — `dashboard.widgets[].colorVariant` is `live`, and `field.widget`'s note names the widget that is actually stamped (#6774, #6773) + +Two ledger records that stopped being answerable to reality after objectui +implementations landed. Both are corrections to the **evidence base**, not new +judgments: the legal-metadata set is byte-identical before and after, and no +schema acceptance test changed. + +## `dashboard.widgets[].colorVariant` — `dead` → `live` (#6774) + +The 2026-08-03 `dead` verdict was right when it was written: every read of +`widget.colorVariant` was an authoring surface, `DashboardRenderer` built the +metric component schema explicitly, and only `options.colorVariant` ever reached +`MetricWidget`. #5010 ruling B then resolved the enforce-or-remove the other way +— keep the declaration, objectui implements it — and objectui#3359 / +PR objectui#3799 (merge `c4c0ac897`) did exactly that: `DatasetWidget` resolves +the declared token through the accent table `MetricWidget` already shared. This +repo absorbed it with the `.objectui-sha` pin `09987b68`, whose ancestry over +that merge is re-verified in the row. + +So the 16 authored sites — 7 in `packages/platform-objects`' `system_overview`, +9 across `examples/app-showcase` — now paint the accent they declare. A widget +that never authored the key, and the enum's own `default`, still resolve to no +class, so their markup is unchanged. + +**What changes for an author.** The row drops `authorWarn`/`authorHint`, so +`os validate` (and any other `@objectstack/lint` consumer) no longer emits +`liveness-dead-property` telling you to move `colorVariant` under `options`. That +advisory would now be wrong twice over: the key works where it is declared, and +`options.colorVariant` is the slot that measured dead. PR #5255's pinned +"`colorVariant` still warns beside the four retired keys" positive contrast is +released with it — its premise was that no renderer reads the key. + +The dashboard ledger now warns on nothing, which is the resolved state +`webhook` and `email_template` already sit in. `dashboard` stays registered in +the lint's `TYPE_COLLECTIONS` so a future regression that re-deadens a widget +key warns on its own, and the block's silence pins gained the anti-vacuity guard +#4651's area gates use — a lint that had stopped loading ledgers returns "no +findings" too. + +## `field.widget` — the note named a widget nobody ever stamped (#6773) + +The note offered `sys_permission_set (capability-multiselect)` as a worked +example of the override in use. That half was never true. ADR-0056 P1 stamps +`permission-facet-link` on all six `sys_permission_set` facets through a single +choke point, and `field:capability-multiselect` was registered only by objectui's +docs-site-only `registerFields()` — never by the live `registerAllFields()` walk +over `fieldWidgetMap` — so authoring it always fell through to the `type` +renderer. objectui#3308 / PR objectui#3793 then retired the name outright under +ADR-0049; at pin `09987b68` it survives only as tombstone comments and a +retirement pin test. + +The verdict is untouched and was never at risk: `widget` is `live` on the +`sys_sharing_rule` trio alone (`object-ref` / `filter-condition` / +`recipient-picker`, all three re-checked in `fieldWidgetMap` at the same pin). +What was wrong was one example — false evidence in the base the next +enforce-or-remove audit reads, which is the #5175 lesson. + +Nothing to migrate in either half: the schemas, the parsed shapes and the +runtime are unchanged — only the classification of what they already do, and the +evidence cited for it. diff --git a/packages/lint/src/lint-liveness-properties.test.ts b/packages/lint/src/lint-liveness-properties.test.ts index 53a5bd1e96..d60b61dfcf 100644 --- a/packages/lint/src/lint-liveness-properties.test.ts +++ b/packages/lint/src/lint-liveness-properties.test.ts @@ -421,29 +421,35 @@ describe('lintLivenessProperties', () => { }], }); - it('warns on `colorVariant`, the key this repo\'s own system dashboard authors 7 times', () => { + // ── #6774: `colorVariant` went LIVE, so this lint must go quiet on it ───── + // + // Until 2026-08-09 this block's first assertion was the opposite — that + // authoring `colorVariant` produced a warning whose hint said "move it under + // `options`". That advisory was correct on the premise it rested on: no + // renderer read the top-level key. #5010 ruling B resolved the + // enforce-or-remove the other way (keep the declaration, objectui + // implements), and objectui#3359 / PR objectui#3799 landed the reader — + // absorbed here by the `.objectui-sha` pin `09987b68`. The ledger row is + // `live` now and carries no `authorWarn`, so the warning is gone and the + // hint would have been telling authors to relocate a key that works. + // + // Kept as a SILENCE pin rather than deleted, the disposition #4651's area + // gates reached above: a half-reverted flip (the ledger row restored to + // `dead`, or the pin rolled back under it) shows up right here. + it('no longer warns on `colorVariant` — the renderer reads it since objectui#3799 (#6774)', () => { const findings = lintLivenessProperties(dash({ colorVariant: 'teal' })); - const hit = findings.find((f) => f.message.includes('widgets.colorVariant')); - expect(hit).toBeDefined(); - // The hint has to name the surviving home, or the author reads it as - // "widgets cannot be coloured". - expect(hit!.hint).toMatch(/options/); + expect(findings.map((f) => f.message).some((m) => m.includes('widgets.colorVariant'))).toBe(false); }); - it('fans out over EVERY widget, not just the first', () => { - const findings = lintLivenessProperties({ - dashboards: [{ - name: 'ops', - widgets: [ - { id: 'a', type: 'metric', dataset: 'd', values: ['v'] }, - { id: 'b', type: 'metric', dataset: 'd', values: ['v'], colorVariant: 'teal' }, - ], - }], - }); - // The dead key is on the SECOND widget — a walk that only looked at - // `widgets[0]` would be silently half-blind on every real dashboard. - expect(findings.map((f) => f.message).some((m) => m.includes('widgets.colorVariant'))).toBe(true); - }); + // ⚠️ What this flip COST, recorded so the next author does not read the + // absence as an oversight: `widgets.colorVariant` was the only warned entry + // in any ledger sitting under an array container, so it was the only subject + // `getNested`'s array fan-out ever had. The assertion that used to live here + // — "fans out over EVERY widget, not just the first" — cannot be written + // against a warn-map that is empty for `dashboard`, and no other type offers + // a dotted warned path today. The fan-out is now untested; filed as #7079 + // rather than replaced with a test that would pass on a lint which never + // walks past `widgets[0]`. // ── #5010: four of these keys are RETIRED, so this lint must go quiet ───── // @@ -465,21 +471,32 @@ describe('lintLivenessProperties', () => { }, ); - it('the retirement silenced only those four — `colorVariant` still warns beside them', () => { - // The negative control for the block above. Without it, a change that - // broke the dashboard walk entirely (or dropped `dashboard` from - // TYPE_COLLECTIONS again) would read as "the retirement worked". - const findings = lintLivenessProperties(dash({ - actionUrl: '/apps/sales/orders', - actionType: 'url', - actionIcon: 'plus', - aria: { ariaLabel: 'Total pipeline' }, - colorVariant: 'teal', - })); + // Anti-vacuity guard for every dashboard silence pin above — the shape + // #4651's area gates use, and the reason those pins are worth keeping at + // all. `lintLivenessProperties` returns [] when it cannot resolve the + // shipped ledgers, so "no dashboard findings" is also what a lint that had + // stopped reading ledgers returns; and since #6774 flipped `colorVariant`, + // `dashboard` is a registered type with an EMPTY warn map, so nothing inside + // the dashboard walk can tell a working walk from one that was dropped from + // TYPE_COLLECTIONS. This authors all five once-warned widget keys and a + // property that IS still `authorWarn` (`object.externalSharingModel`, the + // last one in tree) in the SAME call: same process, same ledger load, one + // warning and not six. + it('the dashboard silence is a real verdict, not a lint that stopped loading ledgers', () => { + const findings = lintLivenessProperties({ + objects: [{ name: 'widget', externalSharingModel: 'read' }], + ...dash({ + actionUrl: '/apps/sales/orders', + actionType: 'url', + actionIcon: 'plus', + aria: { ariaLabel: 'Total pipeline' }, + colorVariant: 'teal', + }), + }); const messages = findings.map((f) => f.message); - expect(messages.some((m) => m.includes('widgets.colorVariant'))).toBe(true); - for (const retired of ['actionUrl', 'actionType', 'actionIcon', 'aria']) { - expect(messages.some((m) => m.includes(`widgets.${retired}`))).toBe(false); + expect(messages.some((m) => m.includes('externalSharingModel'))).toBe(true); + for (const quiet of ['actionUrl', 'actionType', 'actionIcon', 'aria', 'colorVariant']) { + expect(messages.some((m) => m.includes(`widgets.${quiet}`))).toBe(false); } }); diff --git a/packages/lint/src/lint-liveness-properties.ts b/packages/lint/src/lint-liveness-properties.ts index 11dec98c1c..2eb86a3b56 100644 --- a/packages/lint/src/lint-liveness-properties.ts +++ b/packages/lint/src/lint-liveness-properties.ts @@ -205,6 +205,13 @@ const TYPE_COLLECTIONS: Array<{ type: string; key: string }> = [ // checks every widget on the dashboard. Registering it here is not optional // bookkeeping: without it the ledger would be newly correct and newly // silent, which is the shape this lint exists to prevent. + // + // As of #6774 the dashboard ledger warns on NOTHING — four of those five were + // retired in 17.0.0 (#5010) and `colorVariant` went `live` when objectui#3799 + // gave it a renderer. The type STAYS listed, the resolved state `webhook` and + // `email_template` already sit in: a zero-warn entry costs one empty map + // lookup, and it means a future regression that re-deadens a widget key warns + // on its own instead of waiting for someone to notice this list again. { type: 'dashboard', key: 'dashboards' }, ]; diff --git a/packages/spec/liveness/README.md b/packages/spec/liveness/README.md index 4c55f75978..704bd80c97 100644 --- a/packages/spec/liveness/README.md +++ b/packages/spec/liveness/README.md @@ -686,7 +686,7 @@ for t, v in r['types'].items(): | view | 79 | 0 | 4 | – | list/form drilled via `children` (#2998 Track B); list.{responsive,performance} + form.{defaultSort,aria} REMOVED 2026-07-30 (#3896 close-out sweep — list aria/data stay live); **form.data was that sweep's one CORRECTION** — the removal attempt broke the build (`defineForm` writes `data.provider='schema'` onto every metadata form, `metadata-protocol` serves it), so it stands `live` with re-verified evidence; form.{buttons,defaults} live (framework#1894 / #2998); audit-era DEAD lines superseded by re-verification; level-2 dead residue (userActions.buttons, addRecord.mode/formView, tabs[].order) noted on parents — one drill level only | | report | 21 | 0 | 0 | – | dataset-bound (ADR-0021); the aria/performance LEDGER entries were stale — the keys left the schema in the report-liveness close-out; deleted 2026-07-30 as hygiene. Audit-era `chart` DEAD superseded (framework#1890 / #3441) | -| dashboard | 33 | 0 | 8 | – | ADR-0021 dataset widgets (#3251; DashboardWidgetSchema `.strict()`); `aria`/`performance` (and widget `performance` + PerformanceConfigSchema) REMOVED 2026-07-30 (#3896 close-out sweep — no renderer applied any of them); audit-era `globalFilters`/`dateRange` DEAD superseded (framework#2501) | **#4956**: `widgets` DRILLED — the row jumps 20 → 41 classified because all 22 widget-level keys enter the count at once. They had never been classified at all: the entry carried one blanket `live` plus a `note` asserting they were classified "in the DashboardWidgetSchema subtree", and no such subtree existed in any of the 28 ledger files. That gap, not any evidence, is what carried `widgets[].responsive` through the #3896 sweep that removed both its sibling `widgets[].performance` and its literal namesake `view.responsive` — `view` is drilled, so `list.responsive` got asked and went out. New dead 6 = `responsive` (retired #4876/#4995, tombstone keeps the row) + `colorVariant` + `actionUrl`/`actionType`/`actionIcon` + `aria`. The action trio is the sharpest: no renderer draws a per-widget action button at all (every `actionUrl` read in DashboardRenderer is scoped to `header.actions[]`), yet `validate-dashboard-action-refs.ts` enforces reference integrity on it and its docblock calls it "the per-widget button" — a lint guarding an affordance that does not exist. `requiresService` is the counter-example worth remembering: dead by every objectui measurement, and LIVE server-side (`filterDashboardForUser`, ADR-0057 D10) — judging a widget key from the renderer repo alone would have retired an enforced gate. `compareTo` is `live` on ONE path only (inline object-provider charts); on the ADR-0021 dataset path the string arms are dropped and `{ offset }` throws in the executor | +| dashboard | 34 | 0 | 7 | – | ADR-0021 dataset widgets (#3251; DashboardWidgetSchema `.strict()`); `aria`/`performance` (and widget `performance` + PerformanceConfigSchema) REMOVED 2026-07-30 (#3896 close-out sweep — no renderer applied any of them); audit-era `globalFilters`/`dateRange` DEAD superseded (framework#2501) | **#4956**: `widgets` DRILLED — the row jumps 20 → 41 classified because all 22 widget-level keys enter the count at once. They had never been classified at all: the entry carried one blanket `live` plus a `note` asserting they were classified "in the DashboardWidgetSchema subtree", and no such subtree existed in any of the 28 ledger files. That gap, not any evidence, is what carried `widgets[].responsive` through the #3896 sweep that removed both its sibling `widgets[].performance` and its literal namesake `view.responsive` — `view` is drilled, so `list.responsive` got asked and went out. New dead 6 = `responsive` (retired #4876/#4995, tombstone keeps the row) + `colorVariant` + `actionUrl`/`actionType`/`actionIcon` + `aria`. The action trio is the sharpest: no renderer draws a per-widget action button at all (every `actionUrl` read in DashboardRenderer is scoped to `header.actions[]`), yet `validate-dashboard-action-refs.ts` enforces reference integrity on it and its docblock calls it "the per-widget button" — a lint guarding an affordance that does not exist. `requiresService` is the counter-example worth remembering: dead by every objectui measurement, and LIVE server-side (`filterDashboardForUser`, ADR-0057 D10) — judging a widget key from the renderer repo alone would have retired an enforced gate. `compareTo` is `live` on ONE path only (inline object-provider charts); on the ADR-0021 dataset path the string arms are dropped and `{ offset }` throws in the executor. **#6774** moves the row 33/8 → 34/7: `colorVariant` CORRECTED dead → live 2026-08-09, the enforce leg of #5010 ruling B landing from the renderer side (objectui#3359 / PR objectui#3799, absorbed by pin `09987b68`). Worth reading beside `requiresService` above, because it is the same lesson from the other end — that row warns against judging a widget key from the renderer repo alone, and this one is a `dead` verdict that was correct in this repo AND correct in the renderer repo on the day it was measured, and stopped being either when a cross-repo decision was implemented. A ledger row is a claim with a timestamp; `verifiedAt` is what makes the claim re-askable. It also empties the dashboard warn set, so the author-side lint now says nothing about any widget key — `dashboard` stays in the lint's TYPE_COLLECTIONS all the same (the `webhook`/`email_template` resolved state) | | query | 15 | 1 | 5 | 0 | **not a metadata type** — the REQUEST surface (`QuerySchema`: client SDK QueryBuilder output; the `POST /data/:object/query` body), governed via `SPEC_ONLY_SCHEMAS` (#4286). The gate's one-level walk resolves 1 experimental; the 7 marker-experimental search affordances sit one level deeper, below the walk — resolved from `[EXPERIMENTAL — not enforced]` describe markers, not ledger entries (search `fuzzy`/`operator`/`boost`/`minScore`/`language`/`highlight` + `aggregations[].filter` — declared engine affordances no executor receives). The #4286 sweep closed out same-release: `having` ENFORCED 2026-07-31 (engine-side post-aggregation filter, both paths; was finding 1); dead 4 = the tombstoned removals `joins`/`windowFunctions`/`cursor`/`distinct` — REMOVED 2026-07-31 (retiredKey keeps each in the walked shape so the rows stay; protocol-17 semantic migrations; the JoinNode + WindowFunctionNode clusters and the `QueryBuilder.cursor()`/`.distinct()` producers deleted with their keys; `distinct`'s mis-wired REST count suppression deleted too — finding 2). **#6815** adds the 5th dead: `aggregations[].distinct` REMOVED 2026-08-09 (live → dead, `-1` live). It is the one member of this ledger the #4286 sweep could not have caught with the question it asked — that sweep looked for keys NO executor reads, and this one had a reader: the objectql in-memory fallback deduplicated before applying the function while all five other faces (driver-sql, driver-turso, driver-mongodb, driver-memory, service-analytics' `AGGREGATE_SQL`) ignored it, so one query answered two plausible NUMBERS depending on which backend served it. The lesson for the next audit is the question, not the key: a per-key `live` verdict is only as good as the count of faces it was measured across, and this row's 2026-07-31 evidence (`in-memory-aggregation.ts:167,204-206`) was TRUE and still the wrong verdict. `count_distinct` is the surviving spelling (enforce leg, #6409) | | datasource | 30 | 0 | 0 | 0 | seeded 2026-08-01 (#4487) — the **highest dead ratio of any governed type** (20 of 43), and it was ungoverned until now, which is not a coincidence: #4410/#4465/#4481 found six inert keys here by hand, two security-shaped (`schemaMode` left an external DB constructible as `managed` with DDL ungated; `ssl` configured nothing while looking configured). Dead set = `capabilities.*` (all 11 — the engine gates pushdown on the runtime driver's `supports.*` object, a non-overlapping vocabulary), `healthCheck.*` (3 — nothing schedules a datasource probe; the 20 `healthCheck` hits in the repo all belong to the PLUGIN health monitor and other surfaces), `retryPolicy.*` (4 — `retryPolicy` IS enforced on `hook` and `job`, which is what makes this one read alive; the shapes differ), `external.label`, `external.requirePermission`. **`capabilities.readOnly` is the one to know**: it reads as a safety switch, gates nothing, and two shipped prescriptions pointed authors at it until #4487 — `external.allowWrites: false` is the enforced write gate. `config` is a `z.record`, so its per-driver keys sit outside the walk (recorded in the entry's note, not silently skipped) **批 A CLOSED 2026-08-02 (#4583)**: the `capabilities` block — 11 flags, every one dead and authorWarn'd — was REMOVED rather than bridged; pushdown comes from the runtime driver's own `supports.*`, so there was nothing to connect it to. Its rows are deleted (strict-removal route), which is why dead falls 20 → 9. `readOnly` was the reason the audit was worth doing: it read as a safety switch, gated nothing, and had already been MOVED twice toward somewhere it might be enforced (#4410, #4465) — the shipped CRM example called a datasource a read replica on the strength of it while the datasource took writes. Removing it does NOT hand the author a working alternative: `external.allowWrites` only gates FEDERATED datasources, so a managed one has no read-only gate at all (#4584). Remaining 9 = healthCheck ×3 + retryPolicy ×4 + external ×2, batches B/C/D of #4583 **BATCHES B/C/D CLOSED 2026-08-02 — datasource now has ZERO dead properties**, down from the 20 it was seeded with (the highest dead ratio of any governed type). `retryPolicy` ×4 and `healthCheck` ×3 went as whole blocks, `external.label` / `external.requirePermission` as keys. None was bridgeable: each already had a different LIVE mechanism doing the job — the boot policy, the driver handle's on-demand `ping()`/`checkHealth()`, the top-level `label`, and ordinary permission sets + RLS. The `retryPolicy` rejection deliberately refuses to offer a rename: `hook`/`job` retryPolicy ARE enforced but spell the delay `backoffMs`, and that inconsistency is itself the evidence nothing read the datasource one (#4488's sharpest trap) | | webhook | 11 | 0 | 0 | – | **not a registered metadata type** — governed via the gate's spec-only schema override (`SPEC_ONLY_SCHEMAS`), not `getMetadataTypeSchema`; folding it onto the registry is the #3490 reassessment. This row once read 0/1/16 ("the ENTIRE authoring surface is dead", #3461) and both halves of that were CLOSED same-quarter: #3489 built the materializer bridge (authored `webhooks:` entries now land as `sys_webhook` dispatcher rows) and #3494 pruned the aspirational props outright — so the surviving surface is fully live. Kept in the table as the worked example that a dead verdict is a worklist entry, not a tombstone: enforce-or-remove resolved this one by ENFORCING | diff --git a/packages/spec/liveness/dashboard.json b/packages/spec/liveness/dashboard.json index b346342b37..ebbdb73589 100644 --- a/packages/spec/liveness/dashboard.json +++ b/packages/spec/liveness/dashboard.json @@ -53,11 +53,11 @@ "note": "chart presentation config on the dataset-bound chart path — read PER KEY, not forwarded as a bag. Of the 14 keys ChartConfigSchema declares, 9 reach the renderer (above) and 5 do not: xAxis/yAxis/series are derived from the dataset selection, so an authored value would shadow the derived binding; type is answered by the widget's own `type` through CHART_TYPE_MAP; aria has no reader on this path (AdvancedChartImpl has no `aria` prop and the ARIA injection reads the flat ariaLabel/ariaDescribedBy/role). This entry previously read 'chart-config bag forwarded to the chart renderer', which was never measured: at that time exactly one key (showLegend, #3135) was lowered, and the overstatement mis-steered the #5022 measurement for half its length. #5175 measured the real 1-of-14 state, #7016 forwarded the keys the chart block actually delivers, and the narrowing half for the 5 unforwarded keys is still open on #5175 — do not read this row as classifying them." }, "colorVariant": { - "status": "dead", - "authorWarn": true, - "verifiedAt": "2026-08-03", - "authorHint": "No dashboard render path reads the top-level `colorVariant`. The inline metric card takes its accent from `options` (MetricWidget's `colorVariant` prop is fed by the `...options` spread in DashboardRenderer), and the ADR-0021 dataset-bound path (DatasetWidget) has no colour affordance at all — so move it under `options`, or drop it.", - "note": "CALL GRAPH CLOSED BY HAND 2026-08-03 (objectui @91757a7). Every read of `widget.colorVariant` is an AUTHORING surface — WidgetConfigPanel.tsx:407, plugin-designer/DashboardEditor.tsx:266-267, metadata-admin/inspectors/DashboardWidgetInspector.tsx:380, DashboardWithConfig.tsx:113 (config-panel draft) — i.e. the designers write the key back out and no renderer consults it. DashboardRenderer builds the metric/object-metric component schema explicitly (`{ type: 'metric', ...options, label, value }`), so only `options.colorVariant` ever reaches MetricWidget. This repo's own packages/platform-objects/src/apps/dashboards/system_overview.dashboard.ts authors it 7 times, which is the cost of the gap being invisible. ADR-0049 enforce-or-remove tracked in #5010." + "status": "live", + "verifiedAt": "2026-08-09", + "evidenceScope": "cross-repo", + "evidence": "objectui @09987b68: packages/plugin-dashboard/src/DatasetWidget.tsx:641 — metricAccentTextClass(widget?.colorVariant) tints the dataset-bound metric card's value; objectui @09987b68: packages/plugin-dashboard/src/colorVariants.ts:44-53 — VARIANT_TEXT_CLASSES, the eight tokens of WidgetColorVariantSchema; objectui @09987b68: packages/plugin-dashboard/src/__tests__/DatasetWidget.colorVariant.test.tsx — renders every token and derives the vocabulary from the spec enum at runtime; packages/platform-objects/src/apps/dashboards/system_overview.dashboard.ts — 7 of the 16 authored sites this now paints", + "note": "CORRECTED dead → live 2026-08-09 (#6774). #5010 ruling B chose ENFORCE over remove — the declaration stays and objectui implements it — and objectui#3359 / PR objectui#3799 (merge `c4c0ac897`, 2026-08-08) did: DatasetWidget resolves the declared token through the accent table MetricWidget already shared, so the 16 authored sites (7 in packages/platform-objects' system_overview, 9 across examples/app-showcase) paint the accent they declare. Absorbed by this repo's `.objectui-sha` pin `09987b68` — `git merge-base --is-ancestor c4c0ac897 09987b68` re-verified against this branch's base rather than inherited from the unlock comment. No declaration, and the enum's own `default`, still resolve to no class, so every widget that never authored the key renders byte-identically. The 2026-08-03 `dead` verdict was accurate when written and is kept here as history: every read of `widget.colorVariant` was then an AUTHORING surface (WidgetConfigPanel.tsx:407, plugin-designer/DashboardEditor.tsx:266-267, metadata-admin/inspectors/DashboardWidgetInspector.tsx:380, DashboardWithConfig.tsx:113), DashboardRenderer built the metric component schema explicitly, and only `options.colorVariant` ever reached MetricWidget. `authorWarn`/`authorHint` are dropped with the flip — the CLI advisory told authors to relocate a key that now works, and PR #5255's positive contrast pinning that warning is released in the same change." }, "requiresObject": { "status": "live", diff --git a/packages/spec/liveness/field.json b/packages/spec/liveness/field.json index 40569d3d19..8373ccf0a5 100644 --- a/packages/spec/liveness/field.json +++ b/packages/spec/liveness/field.json @@ -106,7 +106,10 @@ }, "widget": { "status": "live", - "note": "objectui generic form widget override. ObjectForm (packages/plugin-form/src/ObjectForm.tsx) threads `field.widget` into the renderer; form.tsx resolves `widget || type` and looks it up as `field:` in the ComponentRegistry (renderFieldComponent). Used to render pick-not-type controls for sys_sharing_rule (object-ref / filter-condition / recipient-picker) and sys_permission_set (capability-multiselect). Degrades to the `type` renderer when the widget is unregistered." + "verifiedAt": "2026-08-09", + "evidenceScope": "cross-repo", + "evidence": "objectui @09987b68: packages/plugin-form/src/ObjectForm.tsx:585-590 — threads the per-field widget override into the renderer, naming permission-facet-link; objectui @09987b68: packages/components/src/renderers/form/form.tsx:230 — resolveWidgetType resolves widget || field.widget || type; objectui @09987b68: packages/plugin-detail/src/index.tsx:563 — ComponentRegistry.register('permission-facet-link', withFieldCarrier(PermissionFacetLink), { namespace: 'field' }); objectui @09987b68: packages/fields/src/index.tsx:2203-2205 — fieldWidgetMap carries object-ref / filter-condition / recipient-picker", + "note": "objectui generic form widget override. ObjectForm (packages/plugin-form/src/ObjectForm.tsx) threads `field.widget` into the renderer; form.tsx resolves `widget || type` and looks it up as `field:` in the ComponentRegistry (renderFieldComponent). Used to render pick-not-type controls for sys_sharing_rule (object-ref / filter-condition / recipient-picker) and sys_permission_set (permission-facet-link). Degrades to the `type` renderer when the widget is unregistered. CORRECTED 2026-08-09 (#6773): this note named `capability-multiselect` as sys_permission_set's widget, and that half was never true. No producer stamped it — ADR-0056 P1 stamps `permission-facet-link` on all six facets through the single `applyFieldWidgetOverrides` choke point (objectui packages/data-objectstack/src/index.ts:2492-2501) — and `field:capability-multiselect` was registered only by the docs-site-only `registerFields()`, never by the live `registerAllFields()` walk over `fieldWidgetMap`, so authoring it always fell through to the `type` renderer. objectui#3308 / PR objectui#3793 then retired the name outright (ADR-0049 enforce-or-remove); it is absent from `packages/*/src` at pin `09987b68` apart from tombstone comments and a retirement pin test. The other three names were re-checked at the same pin and are still in `fieldWidgetMap`. The verdict itself is unchanged and was never at risk — `widget` is live on the sys_sharing_rule trio alone; what was wrong was one worked example, i.e. false evidence in the base the next enforce-or-remove audit reads (the #5175 lesson)." }, "requiredPermissions": { "status": "live", diff --git a/packages/spec/src/conversions/registry.ts b/packages/spec/src/conversions/registry.ts index 136f7e5030..97293f15c7 100644 --- a/packages/spec/src/conversions/registry.ts +++ b/packages/spec/src/conversions/registry.ts @@ -2400,10 +2400,16 @@ const dashboardWidgetResponsiveRemoved: MetadataConversion = { * the two places an upgrading author actually reads. All are `toMajor: 17`, so a * stored dashboard carrying keys from several of them is cleaned in one replay. * - * `colorVariant`, the fifth key #5010 lists, is deliberately NOT here: its - * disposition is unresolved (the rewrite target `options.colorVariant` measured - * dead on the ADR-0021 dataset path too), and 16 authored sites depend on the - * answer. Retiring it later is a new entry, not an edit to this one. + * `colorVariant`, the fifth key #5010 lists, is deliberately NOT here — and the + * reason has since been SETTLED, in the other direction. When this entry was + * written its disposition was open: the rewrite target its triage assumed, + * `options.colorVariant`, measured dead on the ADR-0021 dataset path too, and + * 16 authored sites depended on the answer. #5010 ruling B answered ENFORCE — + * the declaration stays and objectui implements it — which objectui#3359 / + * PR objectui#3799 did, absorbed here by the `.objectui-sha` pin `09987b68`. + * The key is `live` in `liveness/dashboard.json` as of #6774, so nothing is + * owed: no retirement, no later entry. Read this paragraph as "not being + * removed", not as "not removed yet". */ const dashboardWidgetActionAriaRemoved: MetadataConversion = { id: 'dashboard-widget-action-aria-removed',