Noticed while deciding how to colour list rows consistently with a status field (objectstack-ai/duly#12). Not blocking anything — we chose not to author rowColor at all — but it is an ADR-0078 functional-completeness shape that the existing rule family does not cover.
The shape
RowColorConfigSchema (spec/src/ui/view.zod.ts):
field: z.string().describe('Field to derive color from (typically a select/status field)'),colors: z.record(z.string(),z.string()).optional().describe('Map of field value to color (hex/token)'),field required, colors optional. The .describe() on field — "derive color from" — reads as though the derivation happens without a map, which is what a select field's own options[].color would let it do.
The grid renderer disagrees (plugin-grid, 17.2.0):
functionqt(e){returnuseCallback(t=>{if(!e?.field||!e.colors)return;// ← both, or nothingletn=String(t[e.field]??``),r=e.colors[n];if(r)returnKt(r);},[e?.field,e?.colors])}So rowColor: { field: 'status' } parses, publishes, and colours nothing. No error, no warning, no console message.
Why it belongs with the existing rules
This is the same family validateFunctionalCompleteness already gates: a summary with no summaryOperations, a lookup with no reference, a select with no options, a calendar with no calendar block. Every key is known, so #4001's unknown-key rejection cannot see it and the liveness ledger cannot either — which is precisely the gap that rule exists to fill.
Two ways to close it, and they are not equivalent
- A — gate it. Add
view/row-color-without-colors (warning) to checkViewCompleteness: a rowColor with a field and no colors never colours a row. Cheap, matches the existing rules exactly. - B — make the description true. Let the renderer fall back to the bound field's
options[].color when colors is absent, the way plugin-timeline already resolves its colorField. This is the better outcome for the case the key is obviously for — colouring by a status select — because it removes the need to hand-copy the option colours into a second map that then drifts from the object. An author who copies them has two sources of truth for one palette.
If B, A is still worth keeping for the non-select case (a text or number field with no options has nothing to derive from).
Noticed while deciding how to colour list rows consistently with a status field (
objectstack-ai/duly#12). Not blocking anything — we chose not to authorrowColorat all — but it is an ADR-0078 functional-completeness shape that the existing rule family does not cover.The shape
RowColorConfigSchema(spec/src/ui/view.zod.ts):fieldrequired,colorsoptional. The.describe()onfield— "derive color from" — reads as though the derivation happens without a map, which is what a select field's ownoptions[].colorwould let it do.The grid renderer disagrees (
plugin-grid, 17.2.0):So
rowColor: { field: 'status' }parses, publishes, and colours nothing. No error, no warning, no console message.Why it belongs with the existing rules
This is the same family
validateFunctionalCompletenessalready gates: asummarywith nosummaryOperations, alookupwith noreference, aselectwith nooptions, acalendarwith nocalendarblock. Every key is known, so #4001's unknown-key rejection cannot see it and the liveness ledger cannot either — which is precisely the gap that rule exists to fill.Two ways to close it, and they are not equivalent
view/row-color-without-colors(warning) tocheckViewCompleteness: arowColorwith afieldand nocolorsnever colours a row. Cheap, matches the existing rules exactly.options[].colorwhencolorsis absent, the wayplugin-timelinealready resolves itscolorField. This is the better outcome for the case the key is obviously for — colouring by a status select — because it removes the need to hand-copy the option colours into a second map that then drifts from the object. An author who copies them has two sources of truth for one palette.If B, A is still worth keeping for the non-select case (a text or number field with no options has nothing to derive from).