Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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.
85 changes: 51 additions & 34 deletions packages/lint/src/lint-liveness-properties.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 ─────
//
Expand All @@ -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);
}
});

Expand Down
7 changes: 7 additions & 0 deletions packages/lint/src/lint-liveness-properties.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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' },
];

Expand Down
Loading
Loading