feat(spec): warn when a grid view rowColor declares no colors map (view/row-color-without-colors) - #15101
Conversation
`RowColorConfigSchema` requires `field` and leaves `colors` optional, so
`rowColor: { field: 'status' }` parses, publishes and colours nothing: the
only renderer that reads the block bails unless both are present. Every key
involved is declared and live, so neither unknown-key rejection nor the
liveness ledger can see it — the ADR-0078 silent half.
`checkViewCompleteness` now emits `view/row-color-without-colors` (warning,
path `rowColor.colors`) when a grid list view binds a non-empty
`rowColor.field` and declares no usable `colors` map. Both spellings of "no
map" are flagged: `colors` absent (the resolver's own guard returns early)
and `colors: {}` (truthy, so it passes that guard, then matches no value).
The message names the view, the bound field and the runtime line; the fix
prescribes the map.
Scope is measured from the renderer, not inferred from the schema: the rule
fires only on the view type whose adapter forwards `rowColor` at all. On the
other list view types the block is inert for a different reason, and
prescribing a colours map would not fix it — recorded in the module as a
non-rule rather than enforced.
`RowColorConfigSchema.field`'s describe is rewritten so it no longer reads as
if a colour is derived without a map. The accept set does not move: `colors`
stays optional and no member is added, removed or narrowed.
Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0174WZTU6XcFcS7g2kykC53i
📓 Docs Drift CheckThis PR changes 1 package(s): ⛔ 1 release-owned page(s) name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 128 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin cb84c7f9cbceead2ce7bb6d8c50fcf7f7e91207c && git checkout cb84c7f9cbceead2ce7bb6d8c50fcf7f7e91207c
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 7ad3d4e8df5cc5aa0748764e15c876c09c40b6e7 67c64885c692a42488e902b334cd73564d969e6d && git checkout -B drift-repro 7ad3d4e8df5cc5aa0748764e15c876c09c40b6e7 && git merge --no-ff 67c64885c692a42488e902b334cd73564d969e6d
node scripts/docs-audit/affected-docs.mjs --json 7ad3d4e8df5cc5aa0748764e15c876c09c40b6e7
|
|
Landing provenance (PM seat
Generated by Claude Code |
Fixes #14111
Clause ②: no — path limb fires (
packages/spec/src/**), content limb no: no accept-set or surface change.What was wrong
RowColorConfigSchema(packages/spec/src/ui/view.zod.ts) requiresfieldand leavescolorsoptional, but the only renderer that reads the block needs both. objectuiplugin-grid'suseRowColor.tsopens with:So
rowColor: { field: 'status' }parses, publishes and colours no row, while every authoring surface reports success. Every key involved is a declared, live one —rowColoritself islivein the liveness ledger — so neither the unknown-key rejection nor the ledger can see it. That is the ADR-0078 silent half, in the familyvalidateFunctionalCompletenessalready gates (asummarywith nosummaryOperations, aselectwith nooptions, acalendarwith nocalendarblock).Triage ruling 5502967476 (2026-09-02): route A only. Route B (a renderer fallback to the bound select's
options[].color) is out of scope, is not filed, and nothing here touches objectui.What changed
1.
packages/spec/src/kernel/functional-completeness.tsVIEW_ROW_COLOR_WITHOUT_COLORS = 'view/row-color-without-colors', beside the family's others and in the pinnedFUNCTIONAL_COMPLETENESS_RULESlist.checkViewCompletenessemits awarningat pathrowColor.colorswhen a grid list view'srowColorbinds a non-emptyfieldand declares no usablecolorsmap. The finding follows the family's shape exactly (rule/severity/path/message/fix), the message carries the runtime line that makes it true, and thefixis machine-pastable and echoes the authored field back.rowColor.2.
packages/spec/src/kernel/functional-completeness.test.ts— a new describe block beside the family's other view pins: the positive fixture (rowColor: { field: 'status' }⇒ exactly one warning, assertingrule,severity,path, the first sentence ofmessageand the cited runtime line), the negative fixture (field+colors⇒ silent), thecolors: {}fixture, the non-grid NON-rule pin, and the junk/absent-field controls. The rule-id enumeration pin and the "every finding carries a fix" pin are extended.3.
packages/spec/src/ui/view.zod.ts— theRowColorConfigSchema.fielddescribe rewritten so it no longer reads as if a colour is derived without a map.colorsstays optional; no other member moves.4. Generated artifacts —
check:generated --fixregenerated the three it proved stale (api-surface/,export-origins/,content/docs/references/ui/view.mdx); a second run reports "All 15 generated artifacts are up to date."5.
.changeset/row-color-without-colors.md—@objectstack/specpatch. A new author-time warning plus describe text; no accept-set movement.Two decisions worth reviewing, both measured
How
colors: {}is treated — flagged, same as absentThe dispatch asked this to be measured against the renderer rather than assumed, and the two spellings reach the same dead end by different routes:
colorsabsent — the resolver's own guard (!config.colors) returns before it reads a record. Nothing is coloured.colors: {}— an empty object is truthy, so it passes that guard; the lookup one line down (hasOwnProperty.call(config.colors, value)) then matches no value,if (!color) return undefined, and nothing is coloured either.So mirroring the guard expression alone would have blessed
{}; the rule mirrors the outcome the guard produces, which is identical. An empty map is also not an "I meant it" marker the way an action'slocations: []is — turning row colouring off has its own spellings (omit the block, or leave theuserActions.rowColortoolbar toggle off) — so{}is the same dead shape spelled out. Same reasoning the family already applies totriggers: []incheckWebhookCompleteness. Any othercolorsshape (a string, an array) is left alone: the schema refuses it at parse, and this module is not a second parser.Why the rule fires on
gridonlyrowColoris declared on every list view, but objectui's ListView adapter forwards it in exactly one branch of its per-type props switch —case 'grid'.kanban/gallery/calendar/timeline/gantt/map/tree/charteach build their own props and never carry the key;pagemounts a published page through a different renderer. On a kanban board arowColorblock is inert too, but for a different reason, and this rule's prescription ("declare acolorsmap") would not fix it — warning there would be a false prescription, the exact failure the module doc, ADR-0078 §6 and this file's ADR anchor exist to prevent. That non-grid inertness is recorded in the module, not enforced, the same dispositiongalleryalready has inVIEW_BINDING_BLOCKS. It costs no reach the predicate had:checkViewCompletenessalready returns early on a view with no stringtype, so a view relying on the schema'stypedefault was never judged by any rule in this family.Path spelling: the finding sits at
rowColor.colors, not barerowColor—CompletenessFinding.pathis documented as "path of the omitted config", and when a block is present and a sub-key is missing the family already spells it that way (tree.parentField,map.locationField). Through the lint walk it readsviews[0].list.rowColor.colors, the same granularity the siblingvalidate-list-view-field-refsuses forrowColor.field.Verification record — all at head
67c64885Run in a dedicated worktree off
origin/main6665c5c9. Every exit code captured by redirecting first, never through a pipe; each gate quoted by its own verdict line.pnpm --filter @objectstack/spec buildcommand-exit 0;34/34 declared declaration file(s) presentpnpm --filter @objectstack/spec exec vitest run --maxWorkers=2Test Files 466 passed (466)·Tests 12434 passed (12434)pnpm --filter @objectstack/lint exec vitest run --maxWorkers=2(downstream consumer of the predicate)Test Files 93 passed (93)·Tests 2873 passed+ 5 skipped, 2878 totalpnpm --filter @objectstack/spec typecheckcheck:test-typecheck: OK — @objectstack/spec's test layer compiles under packages/spec/tsconfig.test.jsontsc -p tsconfig.test.json --listFileslists both edited files (functional-completeness.ts,functional-completeness.test.ts) — measured, so "typecheck clean" actually covers thempnpm --filter @objectstack/spec check:generated✓ All 15 generated artifacts are up to date.(second run, after--fix)check:authorable-surface,check:docs,check:api-surface,check:export-origins,check:liveness,check:exported-any,check:dual-source-exports,check:entry-nameability,check:browser-reachable-entries,check:skill-examples,check:empty-state,check:variant-docs,check:yaml-examples,check:llms-txt,check:skill-refs,check:strictness-ledgernode scripts/check-system-context-census.mjsOK — 106 elevation read sites in 20 packages across 45 files, all anchored; 140 anchors resolve, 27 declared non-read.No anchor rotted, so no--fixpnpm lint(eslint . --no-inline-config, repo-wide, not narrowed)pnpm check:nul-bytesplus a manual control-byte sweep over the edited filesnode scripts/pm/dispatch-gates.mjs --commandsunion at this headThe 4 that are NOT MEASURED, listed as such rather than as green, each by its own verdict text — all four refuse because this worktree has only the spec / lint / client closures built, none because of anything in this diff:
node scripts/check-dev-prereqs.mjs—The workspace is not built — 1 unmet precondition, not a list of problems.node scripts/check-test-completeness.mjs— exit 3,PREREQUISITE NOT MET — this gate grades a saved turbo run test log, and no log was named(its own text: record as NOT MEASURED).pnpm check:type-check-debt— exit 3,PREREQUISITE NOT MET … ⛔ This is NOT a pass and NOT a finding: nothing was measured.pnpm check:dual-build-cjs-loads— exit 3,PREREQUISITE NOT MET — this gate reads built output, and some package has no dist/. … ⛔ This is NOT a pass: nothing was measured.Reverse verification — one leg, from the committed state
The fix was committed first, so the restore leg has a real reference. The rule was then unhooked at its call site (
if (ROW_COLOR_VIEW_TYPES.has(type)becomesif (false && ROW_COLOR_VIEW_TYPES.has(type)), under atrap … EXIT INT TERMwith absolute paths.Rebuild: none needed, and that is a measured claim, not an omission — the test imports
./functional-completenessby relative source path, so nodistsits between the mutation and the assertion. The mutation script fails loudly on a zero-hit anchor rather than proceeding.Mutation confirmed on disk, anchored on the exact text being changed, both directions:
Observed direction — red, as predicted, 3 failures out of 47:
The negative fixture stayed green throughout, as it must: with the rule unhooked, silence is silence — it is a control on the positive pin, not a second discriminator.
Restore proven by state, not by exit code:
git hash-objecton the restored file returnsa8201feeb13ba978dc1545fe639d94b1c78c9497, byte-equal to the HEAD blob (a non-empty hash, checked; an empty one would have been read as failure), and bothgit diff HEADandgit status --porcelaincame back empty.packages/specwas then rebuilt and the file re-run:Test Files 1 passed (1)·Tests 47 passed (47), so the final readings are on the restored tree. The whole gate union above was run after that rebuild, at this head, on a clean tree.Out-of-scope finding, filed not fixed
Filed as #15100 (unassigned, unlabelled):
examples/app-showcase/src/ui/views/task.view.ts:231shipsrowColor: { field: 'priority' }— the exact shape this diagnostic exists to name, soos validatewill now report it as an advisory on the showcase. It is not fixed here: choosing a colour perpriorityvalue is a design decision about the demo app, not a mechanical fix pinned by existing evidence, and it lands in a different tree.os buildprints warnings as advisories and exits 0, so no CI context reds because of it.packages/lint/src/showcase-shape.fixtures.tscarries the same line and is deliberately excluded — its header records the 2026-08-13 ruling that the frozen snapshot must not be re-synced.Not done, deliberately
No
needs:contract-review(Clause ② no). Left as a draft — not flipped to ready, not queued, not auto-merged.🤖 Generated with Claude Code
Generated by Claude Code
Generated by Claude Code