Skip to content

Commit ced3e1a

Browse files
feat(spec)!: object-kanban conditionalFormatting takes the list view's own rules, and its hold exits (#21464, S-kanban-cf) (#21711)
Part of #21464 Clause-②: yes (narrowing) ## What this does The S-kanban-cf stage of the `ComponentPropsMap` `z.unknown()` close-out: the exit of the `object-kanban` `conditionalFormatting` hold, per the claim `5977940136`, the landing note `5977935364`, triage's direction `5961300594` and the seat answer `5963787404` (item 2). The census ran first. It found no working writer that the list view's member refuses, and the reader's declared dialect is exactly the list view's rule. So the member is typed by reference (def identity) to `ListViewSchema.shape.conditionalFormatting`, as `object-grid`'s is. The enumeration pin's `held-for-decision` line leaves the ledger, and so does the emptied `held-for-decision` stage. | member | was | now | read point at the `.objectui-sha` pin `ab1879721595` | |:--|:--|:--|:--| | `object-kanban` `conditionalFormatting` | `z.unknown()` | `ListViewSchema.shape.conditionalFormatting`, by reference: `[{ condition, style }]`, a non-blank CEL `condition` (string or `{ dialect, source }` envelope) and a `style` map of strings | `KanbanBoardCore.tsx:114` hands `schema.conditionalFormatting` to the board; `getCardStyles` (`KanbanImpl.tsx:182`) evaluates it through `resolveConditionalFormatting` (`core/src/evaluator/listConditional.ts:532`), the evaluator the grid's rows use (`ObjectGrid.tsx:2970`) | No new export and no new schema: the row reads the list view's member, so one rule is judged the same way on the list view, `object-grid` and `object-kanban`. ## The hold's exit - **Why it was held** (`5963787404` item 2): objectui's kanban authored a second rule dialect, the native `{ field, operator, value, backgroundColor }` rule and the flat colour rule, which the list view's member refuses. - **What changed in objectui**: objectui#11522's state is `completed`, landed by objectui#11532 (`c73cdb5695`). `object-kanban`'s `conditionalFormatting` takes the spec list view's `{ condition, style }` rule only, and the native and flat-colour dialects are refused by name. `git merge-base --is-ancestor c73cdb5695 ab1879721595` exits 0, and so does the pin against objectui `main` `2e818d0b51`. Exit 0 proves itself in any checkout, so no control leg is owed. - **The reader's declaration at the pin**: `KanbanConditionalFormattingRuleSchema` extends the spec list view's rule element, read by reference (`types/src/zod/objectql.zod.ts:337`, `:3074`), with tombstones for `field`, `operator`, `value`, `backgroundColor`, `borderColor` and `textColor`. Its TS twin `KanbanConditionalFormattingRule` extends `SpecConditionalFormattingRule` (`types/src/objectql.ts:5166`). The registration's description reads the same (`plugin-kanban/src/index.tsx:676`, recorded in this repo's `sdui.manifest.json`). - **Two differences remain. Neither is a working rule:** 1. The shared evaluator still reads the native, `expression` and top-level colour arms, for a rule a relay hands the board in a stored dialect (`KanbanImpl.tsx:160-166`, `listConditional.ts:495-499`). It is the grid's evaluator, unchanged. No authored `object-kanban` writer uses those arms (census below), and objectui's own faces refuse them. 2. objectui's `condition` takes a string first, so its mirror still admits a blank `condition` (`objectql.zod.ts:363-366`, pinned by objectui's `spec-expression-wire-slots-10946.test.ts:151-155`). The evaluator answers a blank predicate with the caller's fallback (`listConditional.ts:252-260`, single-eval route `:339-340`), and the board passes `fallback: false` (`:546`), so such a rule paints no card. It is refused here, as on the list view and on `object-grid`. This was read, not run. - **Pin to objectui `main` `2e818d0b51`**: `KanbanBoardCore.tsx`, `KanbanImpl.tsx`, `ObjectKanban.tsx`, `plugin-kanban/src/index.tsx`, `listConditional.ts` and `ObjectGrid.tsx` are byte-identical (`git diff --quiet`, exit 0 each). The two `@object-ui/types` files changed only around `grouping`: zero changed lines mention `conditional`, and the rule schema's line `3074` did not move. ## The census A writer is a value written on the block: a page-component node (an object literal naming `object-kanban`, flat or in its `properties` bag, or a literal asserted as one), a direct parse through the row, the block's React component inside its `schema` prop, or the argument of a same-file helper that mounts one. Values resolve through same-file constants and spreads, and parameters resolve at every same-file call site and `it.each` table. Instrument 1 is a TypeScript-AST walk over every tracked file naming `conditionalFormatting` (code, `.json`, and fenced code in `.md` / `.mdx`). It records each `conditionalFormatting` member with its static value and block context, and parses the value through the list view's member. Instrument 2 parses every rule-shaped object within 400 characters after a `conditionalFormatting` token, in any syntax (tuples, `key:` / `value:` rows), as a one-rule list. Each hit outside a block context was read by hand. Controls: instrument 1 finds the showcase's list-view writer and the `object-grid` pin's writers in this repo. | corpus | `object-kanban` writers | parse | refused | |:--|:--|:--|:--| | objectstack `16d241a6af` (every tracked file; 33 name `conditionalFormatting`) | 1 | 0 | 1 · `packages/spec/src/ui/component.test.ts:3947` | | objectui pin `ab1879721595` (94 files name the member, 61 of them also `kanban`) | 9 working, 10 probe | 9 | 10 · all probes | | objectui `main` `2e818d0b51` | the same 19 | 9 | 10 | | hotcrm `4054ec2680` | 0 | | | | cloud `2205b53010` | 0 | | | - **objectstack's one writer** is this package's own test that the key survived the `quickAdd` retirement: `[{ field: 'priority', value: 'high' }]`. It has no `operator`, so the evaluator built no predicate from it (`listConditional.ts:474`) and painted nothing, and objectui's own faces refuse it. Its assertion is about the key, not the value, so it is respelled to `{ condition: "record.priority == 'high'", style: { backgroundColor: '#fee2e2' } }` and still asserts `success: true`. - **objectui's nine working writers**, every one a test fixture, all `{ condition, style }`: the board test's three rules through its mount helper (`ObjectKanban.structuredMembersReachTheirSinks-8313.test.tsx:472`, `:492`, `:509`), the asserted node in `objectFieldsIsAPropNotASchemaKey-7742.test.tsx:97`, the declared-keys row in `structuredKeysAreDeclaredAndHonoured-8313.test.ts:109` (which parses through this very row on the installed spec), the live-member row in `object-kanban-allow-collapse-retired-8801.test.ts:198`, the dialect test's control (`kanban-conditional-formatting.test.ts:48`), and the wire-slot test's string and envelope conditions (`spec-expression-wire-slots-10946.test.ts:123`). - **objectui's ten refused values are refusal probes.** Nine are refused by objectui's own faces too: the native rule (`kanban-conditional-formatting.test.ts:88`), the flat colour rule (`:103`), a colour beside `style` (`:118`, three keys), an undeclared `label` (`:139`), and three malformed conditions (`spec-expression-wire-slots-10946.test.ts:158`). The tenth is that file's probe that its mirror still admits a blank `condition` (`:152`), which the board answers with no paint (above). - **Not writers, and all of them parse:** eight rules mount the runtime `KanbanBoard` directly (`cardPredicateScope.test.tsx`). Nine are view-face relays of a list view's rules (`ObjectView.kanbanConditionalFormatting.test.tsx`, `objectViewHostSurface.test.tsx`). The other members in those 61 files are `object-grid` or list-view writers, which the list view's member already judges. The non-test hits are run-time hand-offs and the schema declarations. objectui `main` adds one non-writer (`InterfaceListPage.relayCensus-11572.test.ts`, a relay census record). - **hotcrm and cloud** have no `conditionalFormatting` at all, case-insensitive and snake case included. Controls: `kanban` in 48 and 35 files, and `objectName` in 115 and 145. ## Changes - `packages/spec/src/ui/component.zod.ts`: the `object-kanban` row's `conditionalFormatting` is `ListViewSchema.shape.conditionalFormatting` with a card-level `.describe()`, and its docblock records the read points, objectui's declaration, the two differences and the census. The import comment names the kanban member beside the grid's. - `packages/spec/src/ui/component-props-unknown-members.pin.test.ts`: the `held-for-decision` line and stage leave, along with the now-unused `staged()` helper. The kanban rule's `conditionalFormatting[].condition.ast` joins the expression-AST lines, exactly as the grid's does. The new §5 pins the def identity, three accepted values parsing to what the list view's member answers, eight refusals by `code` and path, the absent case, and the D3 id. The header records the exit. - `packages/spec/src/ui/component.test.ts`: the one census writer, respelled (above). This file is outside the claim's file list. It is the member's own fixture, and the change reddens it otherwise. - `packages/spec/src/migrations/entries/semantic/18.ui-object-kanban-conditional-formatting-typed.ts` (new) and `packages/spec/src/migrations/registry.ts`: one step-18 D3 entry (the semantic region regenerated by `gen:migration-registry`), and one rationale fragment at **order 77**, the next free order on `main` at the base (73 to 76 are taken, 74 twice), inserted where its id sorts. #21702 also adds a fragment, so whichever lands later re-reads the order and merges `origin/main` through `scripts/pm/os-regen-merge.sh`. No D2 conversion and no `RETIRED_KEYS_BY_MAJOR` row: a page component's `properties` is not parsed on the save or load path, and the refused values are nested member values. - `content/docs/references/ui/component.mdx`: regenerated by `check:generated --fix`, which proved only `check:docs` stale. - `.changeset/21464-component-props-kanban-conditional-formatting-typed.md`: `@objectstack/spec` `minor`, BREAKING banner, the `Clause-②` line, FROM → TO, the census, and the ADR-0087 marker registering `ui-object-kanban-conditional-formatting-typed`. ## Measurements Head `011e53fa2a` (this branch merged with `main` `7e0066af7a`, a comment-only re-anchor of spec test citations that touches none of these files). Base `16d241a6af`. Heavy runs went through `scripts/pm/os-verify-lock.sh`, and every exit code was captured before any pipe. - **Red first, on the published `@objectstack/spec@17.6.0`** (the npm tarball, `ComponentPropsMap['object-kanban'].safeParse`) and on `main` `16d241a6af` (built): each of these is ACCEPTED on both: `conditionalFormatting: 42`, `'red'`, the native rule, the flat colour rule, an `expression` rule, a rule with no `style`, a blank `condition`, and a `style` value of `5`. Control: `navigation: 42` is REFUSED there (`invalid_type` at `navigation`). Each junk value is refused on this branch, pinned in §5 by `code` and path. - **`pnpm --filter @objectstack/spec test`** at `011e53fa2a`: exit 0, Test Files 612 passed (612), Tests 18199 passed, 1 todo, nothing skipped. The same at `27a0e12a09`, before the merge. - **`pnpm --filter @objectstack/spec typecheck`** at `011e53fa2a`: exit 0, `check:test-typecheck: OK` (52 files / 246 errors / 135 signatures held). `tsc -p tsconfig.test.json --listFilesOnly` names both touched tests and the new D3 entry. - **Build and generated artifacts**: the spec build is green. `check:generated` proved only `check:docs` stale, `--fix` regenerated that one, and the re-check at `011e53fa2a` reads "All 15 generated artifacts are up to date". `check:api-surface` reads "public API surface + factory signatures unchanged". `turbo run build --filter=!@objectstack/docs --concurrency=2` built 72 of 72 tasks. - **Consumers**: `pnpm --filter @objectstack/lint test` (after building lint's dependency closure, `--filter '@objectstack/lint^...'`): 119 files passed, 5622 tests passed, 5 skipped. The 5 read lint's own `dist`; after `pnpm --filter @objectstack/lint build`, `validate-component-props`, `lazy-deps` and `runtime-lazy-deps` passed 61 of 61, nothing skipped. The first lint run before that closure build failed 56 files on `Failed to resolve entry for package "@objectstack/formula"` / `"@objectstack/sdui-parser"`: unbuilt dependencies, not a measurement. - **The public door, both ways** (`validateComponentProps` from lint `src` over the built spec, one `object-kanban` node per page). These report nothing: a `{ condition, style }` rule, and an envelope condition. These are reported: `42` as `component-props-invalid` at `properties.conditionalFormatting`; a rule with no `style` as `component-props-invalid` at `…conditionalFormatting.0.style`; the native rule as `component-props-invalid` at `.0.condition` and `.0.style` plus `component-props-unknown-key` at `.0.field`, `.0.operator`, `.0.value` and `.0.backgroundColor`; an `expression` rule at `.0.condition` plus `component-props-unknown-key` at `.0.expression`; and a blank condition as `component-props-invalid` at `.0.condition`. - **Ablation**, one leg, on the committed tree (`27a0e12a09`; the merge did not touch these files). The driver used `node scripts/ablation-replace.mjs` in wrap mode, inside its own `EXIT INT TERM` restore trap on the absolute path, with an empty hash read as failure, against HEAD blob `830a02f619`. The kanban member went back to `z.unknown().optional()`: anchor x1 → x0, replacement x0 → x1, blob → `4b67209318`, re-counted in the leg (`ListViewSchema.shape.conditionalFormatting` 2 → 1). **Red: 12 failed, 421 passed.** §1 received exactly `[ "object-kanban conditionalFormatting" ]` as unlisted, and `[ "object-kanban conditionalFormatting[].condition.ast" ]` as an outliving line. §5 failed its identity, the string-condition parse and all eight refusals. Still green, as expected under `z.unknown()`: the envelope and empty-list parses, the absent case, the D3 id, and §1's size control (one member in, one out). Restore: blob equals HEAD (`830a02f619`), `git diff HEAD` is empty, and `git status` is clean. The pins import `./component.zod` from source, so no build or `dist` preflight sits between mutation and run. - **Derived gates** at `011e53fa2a`: `node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands` (no paths; merge base `7e0066af7`, 7 paths, 276 changed lines) derived 114 commands, all exit 0. `--ran` printed "114 derived, 114 run, 0 NOT-MEASURED, 0 UNRUN". At `27a0e12a09` the first pass recorded six exit-3 `PREREQUISITE NOT MET` refusals for unbuilt packages. After the builds above they re-ran exit 0, and the pass at `011e53fa2a` ran all 114 clean. - **`check-widening-tells`** over the branch diff: `--declaration no` exits 0. That is no tell: the row takes an existing member by reference and adds no key, arm or export. `--declaration yes` exits 0. - **Narrowed lint**: `eslint --no-inline-config --format json` over the 5 changed TypeScript files reports 5 files, 0 errors and 0 warnings. The population comes from eslint's own `--print-config`: those 5 resolve a config, and the changeset and `component.mdx` resolve none. Invariance: `eslint.config.mjs` never enables type-aware linting (its own text, about line 327), so this diff cannot move a verdict on an untouched file. The full `pnpm lint` is CI's. - **NOT MEASURED**: the Console Pin Gate, the Dogfood Regression Gate and the full `pnpm lint` are CI-owned. objectui was read at the pin and at `main`, not built against this spec. The board's paint for a blank `condition` was read, not run. The showcase `validate` was not run: no example in this repository authors an `object-kanban` node. CI on this PR was not waited on. - **Rationale order**: #21702's branch (`c4e3ab9631`) also takes order 77 for `ui-report-joined-block-dataset-required`. That is not refused, because `joinRationale` breaks a tie by id. Whichever lands later re-reads the next free order and merges `origin/main` through `scripts/pm/os-regen-merge.sh`. ## Acceptance notes - **objectui's next `@objectstack/spec` bump** reads this row in one place only, `structuredKeysAreDeclaredAndHonoured-8313.test.ts`, whose value parses. objectui's `object-kanban` node is its own flat `ObjectKanbanSchema` and takes no `propsBag` over this row. The console's member-pin prose for `object-kanban.conditionalFormatting` (`apps/console/src/__tests__/registry-inputs-spec-parity.test.ts:3003`) says "The spec row is `z.unknown()`", which goes stale with that bump. Carrier: objectui's next spec bump. Noted, not filed. - **The blank condition** is the one value objectui's mirror admits and this row refuses. The same split already exists on the list view and on `object-grid`, and the board paints nothing for it. Noted, not filed. - **The lint door's nested unknown-key message** reads "`field` is not a prop `object-kanban` declares" for a key inside a rule. The path (`properties.conditionalFormatting.0.field`) and the zod text ("on this conditional formatting rule") are right. The wording predates this change and reads the same on `object-grid`. Noted, not filed. --- _Generated by [Claude Code](https://claude.ai/code/session_016tKoy8NJa35Yih1FdzrVmn)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 100f68b commit ced3e1a

7 files changed

Lines changed: 259 additions & 17 deletions

File tree

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
---
2+
'@objectstack/spec': minor
3+
---
4+
5+
feat(spec)!: an `object-kanban` page block's `conditionalFormatting` takes the list view's own `[{ condition, style }]` rules instead of any value (#21464)
6+
7+
Clause-②: yes (narrowing)
8+
9+
<!-- adr-0087: registered ui-object-kanban-conditional-formatting-typed -->
10+
11+
**BREAKING** — an accept-set narrowing on a published authoring surface, shipped as `minor` under the repo's launch-window convention for accept-set narrowings. What reads the row: the component-props gate on `objectstack validate`, `objectstack build` and `objectstack lint`, which reports a refused value as an advisory `component-props-invalid` / `component-props-unknown-key` finding. A stored page still saves and loads, because a page component's `properties` is not parsed on the metadata save or load path.
12+
13+
**`@objectstack/spec`**
14+
15+
- **`object-kanban` `conditionalFormatting` is the list view's own member, by reference, as on `object-grid`.** It was `z.unknown()`, held while objectui's kanban also authored a native `{ field, operator, value, backgroundColor }` rule the list view refuses. objectui has since made the list view's `{ condition, style }` rule the member's only authoring dialect, and the board evaluates it with the evaluator the grid's rows use. So `42`, a bare string, a single rule outside a list or a rule with no `style` — values that passed and painted no card — are refused, and so are a blank `condition`, a non-string `style` value, the native rule, an `expression` rule and a colour written beside `condition` or `style`.
16+
- **`ObjectKanbanProps`** carries the list view's rule type on `conditionalFormatting` instead of `unknown`. The member's string `condition` parses to the `{ dialect: 'cel', source }` envelope, exactly as it does on a list view and on `object-grid`.
17+
18+
## FROM → TO
19+
20+
| you wrote | write instead |
21+
|:--|:--|
22+
| `conditionalFormatting: [{ field: 'priority', operator: 'equals', value: 'high', backgroundColor: '#fee2e2' }]` | `conditionalFormatting: [{ condition: "record.priority == 'high'", style: { backgroundColor: '#fee2e2' } }]` (`not_equals` is `!=`, `contains` is `.contains(…)`, `in` is `record.FIELD in [ … ]`) |
23+
| `conditionalFormatting: [{ condition: "record.priority == 'high'", backgroundColor: '#fee2e2' }]` | `[{ condition: "record.priority == 'high'", style: { backgroundColor: '#fee2e2' } }]` — every colour goes in `style` |
24+
| `conditionalFormatting: [{ expression: "record.priority == 'high'", style: { color: 'red' } }]` | `[{ condition: "record.priority == 'high'", style: { color: 'red' } }]` |
25+
| `conditionalFormatting: { condition, style }` (one rule, no list) | `conditionalFormatting: [{ condition, style }]` |
26+
| `conditionalFormatting: [{ condition: '', style }]` | delete the rule — a blank condition matches no card |
27+
28+
The one-line fix: write each rule as `{ condition, style }`, a CEL `condition` over the card's `record.*` and a CSS `style` map, the rule a list view declares. No conversion is registered: nothing on the load path refuses the shape, and the census below found no working rule to respell — the D3 entry `ui-object-kanban-conditional-formatting-typed` carries that judgment.
29+
30+
## Who is affected, measured
31+
32+
A writer is a value written on the block: a page-component node (an object literal naming `object-kanban`, flat or in its `properties` bag, or a literal asserted as one), a direct parse through the row, the block's React component inside `schema={{…}}`, or the argument of a local test helper that mounts one. Values resolve through same-file constants and spreads, and parameters at every same-file call site. Each static value was parsed through the list view's member; a second pass parsed every rule-shaped object within 400 characters after a `conditionalFormatting` token, in any syntax, and each remaining hit was read by hand.
33+
34+
- **objectstack** at `16d241a6af`, every tracked file: one writer, this package's own test that the key survived the `quickAdd` retirement, `[{ field: 'priority', value: 'high' }]` — no `operator`, so the board's evaluator built no predicate from it and painted nothing. Respelled to a `{ condition, style }` rule in the same change.
35+
- **objectui** at the `.objectui-sha` pin `ab1879721595` and at `main` `2e818d0b51` (the readers are byte-identical between the two), every value a test fixture: nine `{ condition, style }` writers on the block (three through the board test's mount helper, one asserted node, one declared-keys row parsed through this very row, one live-member row, the dialect test's control, and the wire-slot test's string and envelope conditions), all parse. The ten refused values are refusal probes. Nine are refused by objectui's own faces too: the native rule, the flat colour rule, a colour beside `style` (three keys), an undeclared `label` and three malformed conditions. The tenth is objectui's probe that its mirror still admits a blank `condition`, which the board answers with no paint. Eight more rules mount the runtime `KanbanBoard` directly rather than the block, and the view-face relays carry a list view's rules; all of them parse.
36+
- **hotcrm** at `4054ec2680` and **cloud** at `2205b53010`: no `conditionalFormatting` at all (controls: `kanban` in 48 and 35 files).
37+
- **Deployed metadata** was not measured.

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

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -944,7 +944,7 @@ Sort field and direction pair
944944
| **grouping** | `{ fields: object[] }` | optional | View grouping config; its first field is the swimlane fallback |
945945
| **quickAdd** | `never` | optional | [REMOVED] `object-kanban` property `quickAdd` was removed in @objectstack/spec 17 (ADR-0049) — the board forwarded it, but the per-column affordance is gated on both `quickAdd` and `onQuickAdd`, and `onQuickAdd` is a host-supplied function JSON cannot carry and no producer ever put on an `object-kanban` node, so authoring it was a parse-clean no-op. Delete the key; `object-kanban` offers no quick-add control. Run `os migrate meta --from 17` to list the mechanical edits for existing sources; apply them by hand. |
946946
| **coverImageField** | `string` | optional | Image field rendered as the card cover |
947-
| **conditionalFormatting** | `any` | optional | Card conditional formatting rules |
947+
| **conditionalFormatting** | `{ condition: string \| object; style: Record<string, string> }[]` | optional | Card conditional formatting rules — `[{ condition, style }]`, the same rules a list view declares: the first rule whose CEL `condition` holds applies its CSS `style` map to that card |
948948
| **navigation** | `{ mode?: Enum<'page' \| 'drawer' \| 'modal' \| 'split' \| 'popover' \| 'new_window' \| 'none'>; preventNavigation?: boolean; openNewTab?: boolean; size?: Enum<'auto' \| 'sm' \| 'md' \| 'lg' \| 'xl' \| 'full'>; … }` | optional | Card-click navigation config — the same block `ListViewSchema.navigation` declares (`{ mode, size, openNewTab, preventNavigation }`). The renderer's own default is `{ mode: 'drawer' }` when the key is absent; it is documented rather than declared, so a parsed board carries the key only when the author wrote it |
949949

950950
### Nested Shape: `ObjectKanbanProps.columns[number]`
@@ -974,6 +974,13 @@ View filter rule
974974
| :--- | :--- | :--- | :--- |
975975
| **fields** | `{ field: string; order?: Enum<'asc' \| 'desc'>; collapsed?: boolean }[]` | ✅ | Fields to group by, in nesting order — the first entry is the outermost group and each later entry nests one level deeper (at least one field); the same order as the group header query's `groupBy` |
976976

977+
### Nested Shape: `ObjectKanbanProps.conditionalFormatting[number]`
978+
979+
| Property | Type | Required | Description |
980+
| :--- | :--- | :--- | :--- |
981+
| **condition** | `string \| { dialect: Enum<'cel' \| 'cron' \| 'template'>; source: string; ast?: any; meta?: object }` | ✅ | Predicate (CEL) to evaluate. |
982+
| **style** | `Record<string, string>` | ✅ | CSS styles to apply when condition is true |
983+
977984
### Nested Shape: `ObjectKanbanProps.navigation`
978985

979986
| Property | Type | Required | Description |
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.
2+
3+
import type { SemanticMigration } from '../../types.js';
4+
5+
// #21464 — the `object-kanban` page block's `conditionalFormatting` was
6+
// `z.unknown()`, held while objectui's kanban authored a second rule dialect
7+
// the list view's member refuses. objectstack-ai/objectui#11522 retired that
8+
// dialect from objectui's authoring faces, so the row now takes the list view's
9+
// own member, by reference, as `object-grid` does. D3 only: page-component
10+
// `properties` is not parsed on the metadata save or load path, so a stored page
11+
// is never refused; and the authored census found no working rule to respell —
12+
// the refused values are objectui's own refusal probes and one no-predicate
13+
// fixture in this package, respelled in the same change.
14+
export const entry: SemanticMigration = {
15+
id: 'ui-object-kanban-conditional-formatting-typed',
16+
surface: 'page `object-kanban` components — `properties.conditionalFormatting` (which used to accept any '
17+
+ 'value)',
18+
replacement: 'the list view\'s own rules, `[{ condition, style }]`: a non-blank CEL `condition` over the '
19+
+ 'card\'s `record.*` and a CSS `style` map of string values. Rewrite a native rule `{ field, operator, '
20+
+ 'value, backgroundColor }` as `{ condition: "record.FIELD == VALUE", style: { backgroundColor } }`, an '
21+
+ '`expression` as `condition`, and move a colour written beside `condition` into `style`.',
22+
reason: 'The board reads `conditionalFormatting` as an ordered list of `{ condition, style }` rules, through '
23+
+ 'the evaluator the grid\'s rows use, and paints a card with the `style` of the first rule whose condition '
24+
+ 'holds; objectui declares exactly the list view\'s rule as the member\'s only dialect. The page-component '
25+
+ 'row declared it `z.unknown()`, so `42`, a bare string or a rule with no `style` passed the '
26+
+ 'component-props gate and the board painted no card for it. The row now takes the list view\'s own '
27+
+ 'member, by reference, as `object-grid` does, so one rule is judged the same way on every door. It is '
28+
+ 'read where every page component\'s props are: the component-props gate reports a refused value as an '
29+
+ 'advisory `component-props-invalid` / `component-props-unknown-key` finding on `objectstack validate`, '
30+
+ '`objectstack build` and `objectstack lint`, and a stored page still saves and loads, because a page '
31+
+ 'component\'s `properties` is not parsed on the metadata save or load path. No conversion is '
32+
+ 'registered: nothing on the load path refuses the shape, and the authored census found no working rule '
33+
+ 'to respell. Deployed metadata NOT MEASURED.',
34+
acceptanceCriteria: 'Every `object-kanban` node validates: `objectstack validate` reports no '
35+
+ '`component-props-invalid` / `component-props-unknown-key` finding under '
36+
+ '`properties.conditionalFormatting`. Each board that sets rules paints the card each rule names with '
37+
+ 'its `style`.',
38+
};

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

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6318,6 +6318,19 @@ const STEP18_RATIONALE: readonly RationaleFragment[] = [
63186318
+ 'the component-props gate (advisory); a stored page still saves and loads, so no conversion '
63196319
+ 'is registered. Its D3 record is the semantic entry `ui-object-grid-row-members-typed`.',
63206320
},
6321+
{
6322+
id: 'ui-object-kanban-conditional-formatting-typed',
6323+
order: 77,
6324+
text:
6325+
'It also types the `object-kanban` page block\'s `conditionalFormatting`, the one member the '
6326+
+ '`ComponentPropsMap` `z.unknown()` close-out held for a ruling: it was `z.unknown()` while objectui\'s '
6327+
+ 'kanban also authored a native rule dialect the list view refuses, so `42` or a rule with no `style` '
6328+
+ 'passed every door and the board painted no card for it. objectui has since made the list view\'s '
6329+
+ '`{ condition, style }` rule the member\'s only authoring dialect, and the board evaluates it with the '
6330+
+ 'grid\'s evaluator, so the row takes the list view\'s own member by reference, as `object-grid` does. '
6331+
+ 'Read by the component-props gate (advisory); a stored page still saves and loads, so no conversion is '
6332+
+ 'registered. Its D3 record is the semantic entry `ui-object-kanban-conditional-formatting-typed`.',
6333+
},
63216334
{
63226335
id: 'ui-object-map-gantt-tree-navigation-typed',
63236336
order: 64,
@@ -20195,6 +20208,40 @@ const step18: MigrationStep = {
2019520208
+ 'its `colors` map names, the navigation mode on a row click, the conditional styles, the bulk '
2019620209
+ 'actions, the group-header numbers and the affordances `operations` names.',
2019720210
},
20211+
// #21464 — the `object-kanban` page block's `conditionalFormatting` was
20212+
// `z.unknown()`, held while objectui's kanban authored a second rule dialect
20213+
// the list view's member refuses. objectstack-ai/objectui#11522 retired that
20214+
// dialect from objectui's authoring faces, so the row now takes the list view's
20215+
// own member, by reference, as `object-grid` does. D3 only: page-component
20216+
// `properties` is not parsed on the metadata save or load path, so a stored page
20217+
// is never refused; and the authored census found no working rule to respell —
20218+
// the refused values are objectui's own refusal probes and one no-predicate
20219+
// fixture in this package, respelled in the same change.
20220+
{
20221+
id: 'ui-object-kanban-conditional-formatting-typed',
20222+
surface: 'page `object-kanban` components — `properties.conditionalFormatting` (which used to accept any '
20223+
+ 'value)',
20224+
replacement: 'the list view\'s own rules, `[{ condition, style }]`: a non-blank CEL `condition` over the '
20225+
+ 'card\'s `record.*` and a CSS `style` map of string values. Rewrite a native rule `{ field, operator, '
20226+
+ 'value, backgroundColor }` as `{ condition: "record.FIELD == VALUE", style: { backgroundColor } }`, an '
20227+
+ '`expression` as `condition`, and move a colour written beside `condition` into `style`.',
20228+
reason: 'The board reads `conditionalFormatting` as an ordered list of `{ condition, style }` rules, through '
20229+
+ 'the evaluator the grid\'s rows use, and paints a card with the `style` of the first rule whose condition '
20230+
+ 'holds; objectui declares exactly the list view\'s rule as the member\'s only dialect. The page-component '
20231+
+ 'row declared it `z.unknown()`, so `42`, a bare string or a rule with no `style` passed the '
20232+
+ 'component-props gate and the board painted no card for it. The row now takes the list view\'s own '
20233+
+ 'member, by reference, as `object-grid` does, so one rule is judged the same way on every door. It is '
20234+
+ 'read where every page component\'s props are: the component-props gate reports a refused value as an '
20235+
+ 'advisory `component-props-invalid` / `component-props-unknown-key` finding on `objectstack validate`, '
20236+
+ '`objectstack build` and `objectstack lint`, and a stored page still saves and loads, because a page '
20237+
+ 'component\'s `properties` is not parsed on the metadata save or load path. No conversion is '
20238+
+ 'registered: nothing on the load path refuses the shape, and the authored census found no working rule '
20239+
+ 'to respell. Deployed metadata NOT MEASURED.',
20240+
acceptanceCriteria: 'Every `object-kanban` node validates: `objectstack validate` reports no '
20241+
+ '`component-props-invalid` / `component-props-unknown-key` finding under '
20242+
+ '`properties.conditionalFormatting`. Each board that sets rules paints the card each rule names with '
20243+
+ 'its `style`.',
20244+
},
2019820245
// #21464 — `navigation` on the `object-map`, `object-gantt` and `object-tree`
2019920246
// page blocks was `z.unknown()` although each renderer hands it to the shared
2020020247
// navigation hook, which reads `.mode` and types its mode union as the list

0 commit comments

Comments
 (0)