Skip to content

Commit c73cdb5

Browse files
feat(types,plugin-kanban)!: object-kanban's conditionalFormatting takes the spec list view's { condition, style } rule only; the native and flat-colour dialects are refused by name (objectui#11522) (#11532)
Fixes #11522 Clause-②: no (narrowing) `object-kanban`'s `conditionalFormatting` takes one rule dialect, the spec list view's `{ condition, style }`. The native `{ field, operator, value, backgroundColor, borderColor }` rule and the flat CEL rule (a colour written beside `condition` instead of inside `style`) are retired with no alias window and refused by name. This executes triage's ruling `5963861071` (retire, not widen). objectstack-ai/objectstack#21464 can then type the member by reference to its list-view member. ## What changes | Rule on `object-kanban` | Before (BASE `6903eafb`) | After | |---|---|---| | `{ condition, style }` (string, envelope or `''` condition) | accepted on all three zod faces | accepted, unchanged | | native `{ field, operator, value, backgroundColor }` | accepted on all three faces | refused at `field`, `operator`, `value` and `backgroundColor`, each naming the retirement | | flat CEL `{ condition, backgroundColor }` | refused as a bare `invalid_union` at the rule | refused at `backgroundColor` (named) and `style` (required) | | `{ condition, style, backgroundColor }` | tolerant face accepted it (the colour key was stripped from the parse, while the shared resolver paints a top-level colour over `style`, as `listConditional.test.ts` pins); strict face refused it as `invalid_union` | refused at `backgroundColor` on every face | | `{ condition, style, label }` | tolerant face accepted it, strict face refused it | refused on every face, `unrecognized_keys` with the spec rule's own message | The three faces are `ObjectKanbanSchema`, `safeValidateSchema` (tolerant) and `StrictAnyComponentSchema` (strict). The before and after columns were read with a throwaway probe on each tree. The probe was deleted and never committed. - **zod (`@object-ui/types`, `zod/objectql.zod.ts`).** `KanbanConditionalFormattingRuleSchema` is the spec `ListViewSchema.conditionalFormatting` element, taken through `stripImportedDefaults` and `.extend()`-ed. It is no longer a union. It inherits the spec rule's strictness and its `style` record. Two things are layered on top. First, `condition` is `SpecRuleConditionSchema`, the list view's and the grid's own condition arm (objectui#10946), so a string condition is still not canonicalized and `''` is still accepted. Second, six retirement tombstones: `field`, `operator`, `value`, `backgroundColor`, `borderColor`, `textColor`. - **TS (`objectql.ts`).** `KanbanConditionalFormattingRule` is an interface that extends `SpecConditionalFormattingRule` and declares the same six keys `?: never`. `KanbanNativeConditionalFormattingRule` is deleted and dropped from the barrel (TS2305 for an importer). tsc reports a retired key by name (TS2322 at each key). - **`@object-ui/plugin-kanban`.** The registration's `conditionalFormatting` description no longer teaches the native or flat-colour rule. `KanbanImpl`'s comments say what did and did not narrow. No render path changed: the description string is the only non-comment line. - **Docs.** The `object-kanban` row in `content/docs/api/schema-reference.md` documents the one rule and its respelling. - **One changeset** (`11522-kanban-rule-dialect-retired.md`): `types` and `plugin-kanban` `minor`, with BREAKING and FROM/TO spelled out. ## Mechanism hypotheses, measured **H1, the census (writers of each retired dialect).** The census used three instruments, and each one caught something the others missed. (1) Rule objects inside a `conditionalFormatting: [` array. (2) Every object literal carrying a top-level colour key, classified by its other keys. (3) Every rule-shaped object within 400 characters after any `conditionalFormatting` token, in any syntax, which catches tuples and `key:`/`value:` rows. A fourth pass listed every `{ field, operator }` literal in files that mention both `conditionalFormatting` and `kanban`. Each remaining hit was triaged by hand as a filter rule, a grid, list-view or report carrier, or a resolver unit test. | Where | Writers on `object-kanban` at BASE | Disposition | |---|---|---| | `packages/types/src/__tests__/kanban-conditional-formatting.test.ts` | 3 native (2 zod documents, 1 TS literal) | turned around: pins both refusals | | `packages/types/src/__tests__/object-kanban-allow-collapse-retired-8801.test.ts` | 1 native (the "still accepts the live member" row) | respelled `{ condition: "record.status == 'open'", style: { backgroundColor: '#fee2e2' } }` | | `packages/plugin-kanban/src/__tests__/ObjectKanban.structuredMembersReachTheirSinks-8313.test.tsx` | 1 native, 1 flat CEL | respelled `{ condition: "record.owner == 'ann'", style: { backgroundColor: 'rgb(1, 2, 3)' } }` and `{ condition: "record.owner == 'bob'", style: { backgroundColor: 'rgb(4, 5, 6)' } }`, plus one new row for the whole `style` map | | `packages/plugin-kanban/src/__tests__/structuredKeysAreDeclaredAndHonoured-8313.test.ts` | 1 native | respelled `{ condition: "record.owner == 'ann'", style: { backgroundColor: '#eef' } }` | | `packages/plugin-kanban/src/__tests__/objectFieldsIsAPropNotASchemaKey-7742.test.tsx` | 1 native (relation field) | respelled `{ condition: "record.owner == 'u1'", style: { backgroundColor: PAINT } }` | | `packages/plugin-kanban/src/index.tsx` (registration description) | taught both | rewritten | | `content/docs/api/schema-reference.md` | taught native | rewritten | | `apps/console/src/__tests__/registry-inputs-spec-parity.test.ts` (member-pin prose) | described both | rewritten | | objectui `examples/`, `apps/console` non-test code, `skills/` | zero; controls fire (`object-kanban` in 4 `examples/` files, `kanban` in 9 `skills/` files) | none | | objectstack `examples/` at `f9a8eb88` | zero. The one `conditionalFormatting` (app-showcase `field-zoo.view.ts`) is `{ condition, style }` on a list view. Controls fire: `kanban` hits in `app-crm` views, and `{ field, operator, value }` filter literals match the same matcher | none | | `objectstack-ai/hotcrm` | NOT MEASURED: not in this container. Triage measured zero | none | **H2, by reference.** The installed `@objectstack/spec` 17.5.0 exports no named rule schema. The rule is reachable as `ListViewSchema.shape.conditionalFormatting.unwrap().element`, a strict object of `condition` and `style`. The zod twin `.extend()`s that element through `stripImportedDefaults`. It does not override the condition with the bare spec slot, because the spec slot canonicalizes a string condition into an envelope and refuses `''`. The 10946 pins assert both behaviours on `object-kanban`. The TS twin is `SpecConditionalFormattingRule`, which already indexes `ObjectListViewSchema`'s slot by reference. It is extended, not restated. A new pin asserts `shape.style` is the spec element's own `style`, by identity. Its control is that `shape.condition` is not the bare slot. **H3, the refusal.** With one arm, zod reports at the retired key's own path, and no `invalid_union` remains at the rule. The messages are identical on `safeValidateSchema` and `StrictAnyComponentSchema`. They are shown below, with backticks spelled as in the source: - Native rule, at `conditionalFormatting.N.field` (and the same at `.operator` and `.value`, with the key name swapped): > `field` belongs to the native kanban rule dialect `{ field, operator, value, backgroundColor, borderColor }`, which `object-kanban`'s `conditionalFormatting` no longer accepts: RETIRED (objectui#11522), with no alias window. A rule is `{ condition, style }` — a CEL `condition` over `record.*` and a CSS `style` map, the rule `@objectstack/spec`'s `ListViewSchema.conditionalFormatting` declares. Respell `{ field: 'priority', operator: 'equals', value: 'high', backgroundColor: '#fee2e2' }` as `{ condition: "record.priority == 'high'", style: { backgroundColor: '#fee2e2' } }` (`not_equals` is `!=`, `contains` is `.contains(…)`, `in` is `record.f in [ … ]`). - Flat CEL rule, and any top-level colour, at `conditionalFormatting.N.backgroundColor` (`borderColor` and `textColor` are the same; `textColor` says `style: { color }`): > `backgroundColor` is a colour written at the top level of the rule, which `object-kanban`'s `conditionalFormatting` no longer accepts: RETIRED (objectui#11522), with no alias window. A rule is `{ condition, style }` — a CEL `condition` over `record.*` and a CSS `style` map, the rule `@objectstack/spec`'s `ListViewSchema.conditionalFormatting` declares. Move the colour into the rule's CSS map: `style: { backgroundColor }`. - Beside these, a native rule also draws `invalid_union` at `.condition` and `invalid_type` at `.style` (both required), and a flat CEL rule draws `invalid_type` at `.style`. An undeclared key draws the spec rule's own `unrecognized_keys` message. For `expression` that message adds "Did you mean `expression` → `condition`?". The `retirementTombstone` helper is the one used. `aliasKeyRefusal` does not fit: these keys are a retired dialect, not aliases of one canonical key. **H4, the resolver.** Every arm of `resolveConditionalFormatting` still has a live authorable carrier. This was measured on the built `safeValidateSchema` and `ObjectGridSchema`: | Arm | `object-grid` node (`properties` bag) | `ObjectGridSchema` (view `table` slot / renderer props) | `object-view` `table` slot | `list-view` node | `object-kanban` node | |---|---|---|---|---|---| | `condition` + `style` | accepts | accepts | accepts | accepts | accepts | | `expression` | accepts | accepts | accepts | accepts | refuses | | `field` / `operator` / `value` | accepts | accepts | accepts | accepts | refuses | | `backgroundColor` / `textColor` / `borderColor` overrides | accepts | accepts | accepts | accepts | refuses | No arm has a measured zero carrier, so `listConditional.ts` is untouched. **H5, the render.** A throwaway probe drew each rule through the real `SchemaRenderer` → `object-kanban` board, and through `KanbanRenderer` with and without `objectFields`. It ran at BASE and again after the change. The two readings are byte-identical: ```text native (8313): Alpha=[background-color: rgb(1, 2, 3);] Beta=[(none)] native respelled: Alpha=[background-color: rgb(1, 2, 3);] Beta=[(none)] flat CEL (8313): Alpha=[(none)] Beta=[background-color: rgb(4, 5, 6);] flat CEL respelled: Alpha=[(none)] Beta=[background-color: rgb(4, 5, 6);] native (7742) objectFields=true: paint=[rgb(255, 0, 0)] native (7742) objectFields=false: paint=[(none)] native respelled (7742) objectFields=true: paint=[rgb(255, 0, 0)] native respelled (7742) objectFields=false: paint=[(none)] ``` Each respelling paints the card the retired rule painted, with the same style string. The respelled 8313 and 7742 rows pin this permanently through the real board, each with the live non-matching control. ## The pins - `kanban-conditional-formatting.test.ts` (turned around). For each of the three faces, it pins: the `{ condition, style }` control; the native rule refused at each retired key, with the message starting with the key's name and containing `RETIRED (objectui#11522)` and `{ condition, style }`; the accepted rule at index 0 of the same document drawing no issue; the flat CEL rule refused at `backgroundColor` with the `style: { backgroundColor }` respelling and no `invalid_union` at the rule; and all three colour keys refused beside a `style`. It also pins the identity of `style` with the spec element (with its control) and the inherited strictness. On the TS face, `@ts-expect-error` covers each retired key, `keyof` equality holds between the two faces, and `condition` and `style` types are equal. - `spec-expression-wire-slots-10946.test.ts` reads `condition` straight off the rule's shape. There is no union arm left to index. - `zod-mirror-parity.test.ts`: the `KanbanConditionalFormattingRuleSchema` exclusion reason is rewritten. Its old reason ("a union of two rule dialects … the `'kanban'` arm") was false twice over. It stays an exclusion, compared where it is pinned, the way `ExpressionWireSchema` is. ## Pending changesets - **Dated note appended:** `10946-expression-wire-slots-by-reference.md`. Its sentence "the list view's and the kanban board's rule unions share one `condition` schema" describes the kanban union. - **Dated note appended:** `7664-kanban-arm-plugin-dialect.md`. Its sentence "`KanbanConditionalFormattingRuleSchema` is … the rule union the `'object-kanban'` arm already applied" is now false. - **Left alone, still true:** - `7664-plugin-kanban-declared-schema.md`: says nothing about rules. - `8932-retire-kanban-enhanced.md`: only names `KanbanConditionalFormattingRule` as the type to use, which still exists. - `8313-kanban-structured-authoring-keys.md`: the key is still declared and honoured, and its description still states what the board reads. - `7727-conditional-formatting-record-scope.md`: about `record.*` conditions. - `7928-listviews-by-reference-fold.md`, `9242-stray-kanban-groupby-lane-second-route.md`: about named-view kanban config keys. - `8801-object-kanban-allow-collapse-retired.md`: `conditionalFormatting` is still a live control key in `plugin-kanban`. - `7742-kanban-arm-batch70.md`: about `objectFields`. - `7322-object-kanban-group-by-limit.md`: its "it now authors `groupBy`" remark about the kanban test still holds. - `6349`, `8165` and `8261`: they name the one-authority gate, whose behaviour did not change; only its comments did. - The 13 grid and list-view entries that mention conditional formatting: none makes a kanban rule claim. One of them, `10275`, names kanban only as a view binding. - `check:changeset-overwrite` reports the two modified entries with their declarations unchanged (`types: minor`), which is the appended-note case. ## Verification (head `6d3b4da0`) - `pnpm exec vitest run --maxWorkers=2 packages/types/ packages/plugin-kanban/ apps/console/src/__tests__/registry-inputs-spec-parity.test.ts scripts/__tests__/one-authority-per-exported-name-6273.test.ts`: 411 files, 9878 tests passed. `VERDICT command-exit 0`. - `pnpm --filter @object-ui/types run type-check` (src, examples and test configs): exit 0. Before the types pin was turned around, the same check reported TS2322 at the four retired keys of its old native literal. `pnpm --filter @object-ui/plugin-kanban run type-check`: exit 0, after `pnpm --filter '@object-ui/plugin-kanban^...' build`. - The doc gates ran after the scoped `turbo run build` (35/35 tasks): - `check:doc-snippets`: 777 of 777 blocks, 0 failed. - `check:doc-examples`: exit 0. - `check:doc-types`, `check:readme-exports`: OK. - With the console built, `check:sdui-registration-pins` and `check:component-surface-parity` (report-only, no kanban row): both green. - All exit 0: `check:new-line-citations` (0 new), `check:control-bytes`, `check:changeset-claims`, `check:pending-changeset-literals`, `check:test-path-roots`, `check:spec-symbols`, `check:doc-fences`, `check-changeset-no-major`, `check-changeset-presence`, `check-changeset-fixed`, `check-doc-expression-carriage`, `check-type-check-coverage`. - NOT MEASURED: the Spec Main Shape Gate (it needs objectstack `main`'s spec; it belongs to CI). Spec `main` declares the rule with the same `strictObject` helper and no refinements (read at `f9a8eb88`), so `.extend()` is expected to hold there. The repo-wide lint is also left to CI. ## Acceptance notes - **What the board still paints.** The board still paints any rule a relay hands it, because the shared resolver keeps every arm. For example, `ObjectView`'s kanban branch relays a named or active view's `conditionalFormatting`, and objectui's `ListViewSchema` member still declares the native rule ("broader than spec, migration deferred", per its own docblock). The generated `object-kanban` node can therefore carry a rule the authored `object-kanban` contract now refuses. It is never validated, which is the same shape as the `groupField` note in that branch. Carrier: none. - **The grid's native arm.** On objectstack `main`, the object-grid block is typed by reference to the list view (objectstack PR #21463, per the card). objectui's grid `properties` bag and `ListViewSchema` still declare the native arm, and the installed 17.5.0 row is `z.unknown()` for both `object-grid` and `object-kanban`. The grid's native arm meets the same disagreement on a spec bump. Carrier: none named. - **The skills guide.** `skills/objectui/guides/schema-expressions.md` lists "the native `{ field, operator, value }` form … still work" under renderer-side facts for "list/grid/kanban". That is true of the evaluator and no longer of `object-kanban` authoring. `skills/**` is governed, and this PR stays ungoverned, so it is left. Carrier: none. Session: `https://claude.ai/code/session_01XvhGmGAP79ZB8swnkapxPC` --- _Generated by [Claude Code](https://claude.ai/code/session_01XvhGmGAP79ZB8swnkapxPC)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 9d7419b commit c73cdb5

18 files changed

Lines changed: 436 additions & 134 deletions

‎.changeset/10946-expression-wire-slots-by-reference.md‎

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,3 +32,17 @@ So the sentences above that say the bulk def qualifies no record, and that the
3232
test file pins the `ast`-only fault "on both", describe the tree at
3333
objectui#10946, not the code in this release. See objectui#11322's changesets
3434
for `@object-ui/plugin-grid` and `@object-ui/types`.
35+
36+
⚠️ **Dated note, 2026-10-03 — the kanban rule is no longer a union — objectui#11522.**
37+
At this change the kanban board's rule was a union of two dialects (the native
38+
`{ field, operator, value }` comparison and `{ condition, style }`), and the
39+
sentence above that says "the list view's and the kanban board's rule unions
40+
share one `condition` schema" describes that union. Now the kanban rule is ONE
41+
object, the spec list view's `{ condition, style }` rule by reference, and the
42+
native and flat-colour dialects are refused by name on `object-kanban`. It still
43+
reads the same `condition` schema, so everything this entry says about the
44+
condition (the `z.string()` first arm, the envelope, the `''` control, the
45+
reference identity of the spec arm) still holds on `object-kanban`, and
46+
`spec-expression-wire-slots-10946.test.ts` still pins it there, now reading
47+
`condition` straight off the rule's shape. The rest of this entry is kept as
48+
the reading of this change.
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
---
2+
'@object-ui/types': minor
3+
'@object-ui/plugin-kanban': minor
4+
---
5+
6+
**BREAKING for authors of `object-kanban` card rules, released as `minor`.** `object-kanban`'s `conditionalFormatting` takes ONE rule dialect, the spec list view's `{ condition, style }`: a CEL `condition` over the card's `record.*` and a CSS `style` map. The two other dialects it used to take are retired with no alias window and refused by name (objectui#11522).
7+
8+
| Rule on `object-kanban` | Before | Now |
9+
|---|---|---|
10+
| `{ condition, style }` | accepted | accepted, unchanged |
11+
| native `{ field, operator, value, backgroundColor?, borderColor? }` | accepted on every face | refused at `field`, `operator`, `value` and the colour key |
12+
| flat CEL `{ condition, backgroundColor }` (a colour beside `condition`, no `style`) | refused as a bare union failure | refused at the colour key, naming the retirement |
13+
| `{ condition, style, backgroundColor }` | accepted by `safeValidateSchema` (the colour key was stripped from the parse, then painted by the board anyway) | refused at the colour key |
14+
| `{ condition, style, label }` (any other undeclared key) | accepted by `safeValidateSchema` | refused as an unrecognized key, with the spec rule's own message |
15+
16+
Each refusal message names the key, objectui#11522 and the `{ condition, style }` respelling, on `ObjectKanbanSchema`, `safeValidateSchema` and `StrictAnyComponentSchema` alike.
17+
18+
**Respelling.** `{ field: 'priority', operator: 'equals', value: 'high', backgroundColor: '#fee2e2' }` is `{ condition: "record.priority == 'high'", style: { backgroundColor: '#fee2e2' } }`. `not_equals` is `!=`, `contains` is `record.f.contains(…)` and `in` is `record.f in [ … ]`. A top-level colour moves into `style`; `textColor` is `style.color`.
19+
20+
**`@object-ui/types`.**
21+
22+
- `KanbanConditionalFormattingRule` is now an interface that extends `SpecConditionalFormattingRule`, with `field`, `operator`, `value`, `backgroundColor`, `borderColor` and `textColor` declared `?: never`. It used to be the union of `SpecConditionalFormattingRule` and `KanbanNativeConditionalFormattingRule`.
23+
- `KanbanNativeConditionalFormattingRule` is removed. Importing it is a compile error (TS2305).
24+
- `KanbanConditionalFormattingRuleSchema`, the rule schema `ObjectKanbanSchema` applies, is a module export of this package's `src/zod/objectql.zod.ts`. It is not on the `@object-ui/types/zod` barrel or on any other entry of the package's `exports` map, so it is not an import a consumer can name. It is the spec `ListViewSchema.conditionalFormatting` rule taken by reference and extended, not a union. It keeps the spec rule's strictness and its `style` map. Its `condition` is the same schema the list view's and the grid's `{ condition, style }` arm reads, so a string condition is still not canonicalized into an envelope and `''` is still accepted. The six retired keys are retirement tombstones.
25+
26+
**`@object-ui/plugin-kanban`.** The `object-kanban` registration's `conditionalFormatting` input description now describes the one rule. `KanbanRendererProps.schema.conditionalFormatting` and the board's `ConditionalFormattingRule` follow the narrowed type.
27+
28+
**Not changed: what the board paints.** The shared evaluator, `resolveConditionalFormatting` in `@object-ui/core`, keeps every arm, because the grid's and the list view's rule union still declares them. A `{ condition, style }` rule styles a card exactly as before. A rule that a relay hands the board, for example a list view's, is painted as before too. Only the authored `object-kanban` member narrowed.
29+
30+
Pins: `packages/types/src/__tests__/kanban-conditional-formatting.test.ts` (turned around) pins both refusals on all three zod faces, the spec rule's identity and strictness, and the TS face. `ObjectKanban.structuredMembersReachTheirSinks-8313.test.tsx` and `objectFieldsIsAPropNotASchemaKey-7742.test.tsx` in `@object-ui/plugin-kanban` draw the respelled rules through the real board and assert the same cards are painted.

‎.changeset/7664-kanban-arm-plugin-dialect.md‎

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,3 +88,20 @@ This is a breaking change shipped as `minor`: this repository's
8888
version-alignment rule keeps objectui's major pinned to `@objectstack`'s and
8989
ships objectui's own breaking changes as `minor` with the break spelled out in
9090
the changeset body, which is what the bullets above are.
91+
92+
⚠️ **Dated note, 2026-10-03 — `KanbanConditionalFormattingRuleSchema` is no longer a union — objectui#11522.**
93+
At this change `KanbanConditionalFormattingRuleSchema` was "the rule union the
94+
`'object-kanban'` arm already applied": the native `{ field, operator, value }`
95+
comparison or `{ condition, style }`. Now it is one object, the spec list view's
96+
`{ condition, style }` rule by reference, and the native rule and a top-level
97+
colour key (`backgroundColor`, `borderColor`, `textColor`) are refused by name;
98+
the `'kanban'` arm it was shared with has itself retired in this release
99+
(objectui#8802). It keeps its name, and it is a module export of
100+
`src/zod/objectql.zod.ts` inside `@object-ui/types`, NOT an export of the
101+
`@object-ui/types/zod` barrel. Measured at objectui#11522's change: that barrel
102+
re-exports `KanbanCardSchema`, `KanbanColumnSchema` and `ObjectKanbanSchema` from
103+
the kanban family and not this schema, and no entry of the package's `exports`
104+
map carries it. So the bullet above that calls it "newly exported from
105+
`@object-ui/types/zod`" does not hold in this release either; whether it held at
106+
objectui#7664's own commit was not measured. The rest of this entry is kept as
107+
the reading of this change.

‎apps/console/src/__tests__/registry-inputs-spec-parity.test.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2878,7 +2878,7 @@ const MEMBER_PINS: Record<string, MemberPin> = {
28782878
},
28792879
'object-kanban.conditionalFormatting': {
28802880
file: 'packages/plugin-kanban/src/__tests__/ObjectKanban.structuredMembersReachTheirSinks-8313.test.tsx',
2881-
pins: 'Members are card STYLE RULES in two accepted dialects, and BOTH reach the sink: the native `{ field, operator, value, backgroundColor }` and the spec CEL `{ condition, backgroundColor }` each colour the matching card and only it, with the sibling card in the same render as the live non-matching control so a green cannot come from two unstyled cards agreeing. ⛔ NOT an identity pin and not a wire pin: `ObjectKanban.tsx` never names this key at all (measured zero, against nine for `cardFields` in the same file) — it rides the `{ ...schema }` spread into `KanbanRenderer`, which forwards it to `KanbanImpl`\'s `getCardStyles`. That makes the pin load-bearing in a way the others are not: an edit replacing that spread with an explicit key list drops the key silently and nothing else in the repo would notice. The spec row is `z.unknown()`, so the read site is the whole member contract (objectui#8313).',
2881+
pins: 'Members are card STYLE RULES in ONE dialect since objectui#11522, the spec list view\'s `{ condition, style }`, and it reaches the sink: a rule colours the matching card and only it, a second rule aimed at the other card does the same, and the whole `style` map arrives, with the sibling card in the same render as the live non-matching control so a green cannot come from two unstyled cards agreeing. The retired native `{ field, operator, value }` and flat-colour dialects are refused by name on the types side (`packages/types/src/__tests__/kanban-conditional-formatting.test.ts`); these rows are their respellings, painting the same cards. ⛔ NOT an identity pin and not a wire pin: `ObjectKanban.tsx` never names this key at all (measured zero, against nine for `cardFields` in the same file) — it rides the `{ ...schema }` spread into `KanbanRenderer`, which forwards it to `KanbanImpl`\'s `getCardStyles`. That makes the pin load-bearing in a way the others are not: an edit replacing that spread with an explicit key list drops the key silently and nothing else in the repo would notice. The spec row is `z.unknown()`, so the read site is the whole member contract (objectui#8313).',
28822882
},
28832883
'object-kanban.data': {
28842884
file: 'packages/plugin-kanban/src/__tests__/ObjectKanban.structuredMembersReachTheirSinks-8313.test.tsx',

‎content/docs/api/schema-reference.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1012,7 +1012,7 @@ A drag-and-drop Kanban board. The `object-kanban` type key validates the shape t
10121012
| `filter` | `any[]` | Query filter, forwarded verbatim as `$filter`. |
10131013
| `limit` | `number` | Fetch window for the board (default 100). |
10141014
| `coverImageField` | `string` | Field whose URL renders as the card cover image. |
1015-
| `conditionalFormatting` | `KanbanConditionalFormattingRule[]` | Card colouring rules — native `{ field, operator, value }` or spec `{ condition, style }`. |
1015+
| `conditionalFormatting` | `KanbanConditionalFormattingRule[]` | Card colouring rules, each `{ condition, style }` — a CEL `condition` over the card's `record.*` and a CSS `style` map, the rule a list view declares; the first matching rule styles the card. The native `{ field, operator, value }` rule and a colour written beside `condition` (rather than inside `style`) are retired and refused by name (objectui#11522): `{ field: 'priority', operator: 'equals', value: 'high', backgroundColor: '#fee2e2' }` is `{ condition: "record.priority == 'high'", style: { backgroundColor: '#fee2e2' } }`. |
10161016
| `navigation` | `ViewNavigationConfig` | What a card click opens — the spec's `NavigationConfig` by reference, the type `ObjectGridSchema.navigation` uses: `mode` (`page`, `drawer`, `modal`, `split`, `popover`, `new_window` or `none`) with `size`, `openNewTab` and `preventNavigation`. With the key absent a click opens the record in a drawer. `page` — and a block written without `mode`, which takes the spec's `page` default — opens the record page through the record navigator the host publishes (objectui#11293); the console publishes one on its custom pages, record pages and list views, and under a host that publishes none the click opens nothing. |
10171017

10181018
> `groupField` is refused by name (objectui#7322): the renderer reads `groupBy`.

‎packages/plugin-kanban/src/KanbanImpl.tsx‎

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -127,8 +127,8 @@ const SWIMLANE_AXIS_X_PADDING = 'px-2 pl-36 sm:pl-44'
127127
// was for any importer. A re-export is not a second declaration.
128128
export type { KanbanCard, KanbanColumn } from './types'
129129

130-
// Card formatting accepts the native `{ field, operator, value }` shape and the
131-
// spec `{ condition, style }` CEL shape (issue #1584) — see @object-ui/types.
130+
// Card formatting is the spec `{ condition, style }` CEL rule — the one dialect
131+
// `object-kanban` declares since objectui#11522 — see @object-ui/types.
132132
export type ConditionalFormattingRule = KanbanConditionalFormattingRule
133133

134134
export interface KanbanBoardProps {
@@ -157,10 +157,12 @@ export interface KanbanBoardProps {
157157
* Evaluate conditional formatting rules for a card.
158158
* Returns CSS style overrides for backgroundColor and borderColor.
159159
*/
160-
// Card conditional formatting now delegates to the shared CEL evaluator
160+
// Card conditional formatting delegates to the shared CEL evaluator
161161
// (issue #1584 / ADR-0058) so kanban cards, list rows, and grid rows reach the
162-
// identical verdict. Beyond the native `{ field, operator, value }` rules the
163-
// kanban schema declares, this also accepts spec `{ condition, style }` rules.
162+
// identical verdict. The kanban schema declares the spec `{ condition, style }`
163+
// rule only (objectui#11522); the evaluator is shared with the grid and the list
164+
// view, whose rule union still declares the native and colour-key arms, so it
165+
// still reads them — a rule a relay hands this board is painted as it was.
164166
// The host predicate scope is bound alongside the card so `features.*` /
165167
// `current_user.*` conditions resolve here exactly as they do on grid rows.
166168
function getCardStyles(

‎packages/plugin-kanban/src/__tests__/ObjectKanban.structuredMembersReachTheirSinks-8313.test.tsx‎

Lines changed: 36 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -45,8 +45,9 @@
4545
* - `grouping` — WHICH SINGLE NESTED POSITION is read, and under what
4646
* precedence. One position (`fields[0].field`), one role (the fallback for
4747
* `swimlaneField`), everything else inert.
48-
* - `conditionalFormatting` — WHICH MEMBER DIALECTS are evaluated, and
49-
* against what. Two dialects, per card, on the card's own record.
48+
* - `conditionalFormatting` — WHICH MEMBER DIALECT is evaluated, and
49+
* against what. One dialect since objectui#11522, `{ condition, style }`,
50+
* per card, on the card's own record.
5051
*
5152
* ## The spec supplies none of it
5253
*
@@ -447,21 +448,29 @@ describe('objectui#8313 — `object-kanban`.`grouping`: one nested position and
447448
});
448449

449450
/* -------------------------------------------------------------------------- */
450-
/* `conditionalFormatting` — two member dialects, per card. */
451+
/* `conditionalFormatting` — the `{ condition, style }` member, per card. */
451452
/* -------------------------------------------------------------------------- */
452453

453-
describe('objectui#8313 — `object-kanban`.`conditionalFormatting`: two member dialects', () => {
454+
describe('objectui#8313 — `object-kanban`.`conditionalFormatting`: the `{ condition, style }` member', () => {
454455
const ROWS = [
455456
{ id: 'a', name: 'Alpha deal', status: 'open', owner: 'ann' },
456457
{ id: 'b', name: 'Beta deal', status: 'open', owner: 'bob' },
457458
];
458459

459-
it('the NATIVE `{ field, operator, value }` member colours the matching card, and only it', async () => {
460+
// ⚠️ RESPELLED, not rewritten (objectui#11522). The two rows below used to
461+
// author the two dialects the member then accepted: the native
462+
// `{ field: 'owner', operator: 'equals', value: 'ann', backgroundColor }` and
463+
// the flat CEL `{ condition: "record.owner == 'bob'", backgroundColor }`.
464+
// Both are retired and refused by name on every zod face; each row now writes
465+
// the SAME predicate and the SAME paint as `{ condition, style }`, and asserts
466+
// the SAME card is painted and its neighbour is not — so a respelling that
467+
// changed which card the rule reaches would turn the row red.
468+
it('a `{ condition, style }` member colours the matching card, and only it', async () => {
460469
const adapter = makeAdapter();
461470
const { container } = renderBoard(adapter, {
462471
data: ROWS,
463472
conditionalFormatting: [
464-
{ field: 'owner', operator: 'equals', value: 'ann', backgroundColor: 'rgb(1, 2, 3)' },
473+
{ condition: "record.owner == 'ann'", style: { backgroundColor: 'rgb(1, 2, 3)' } },
465474
],
466475
});
467476

@@ -474,14 +483,14 @@ describe('objectui#8313 — `object-kanban`.`conditionalFormatting`: two member
474483
expect(stylesOnCard(container, 'Beta deal')).not.toContain('background-color');
475484
});
476485

477-
it('the SPEC CEL `{ condition }` member does the same, against the card’s own record', async () => {
478-
// The second dialect (#1584 / ADR-0058). Aimed at the OTHER card on
479-
// purpose, so a shared fixture cannot make the two dialects look alike.
486+
it('a second rule aimed at the OTHER card does the same, against that card’s own record', async () => {
487+
// Aimed at the other card on purpose, so one shared fixture cannot make two
488+
// rules look alike (#1584 / ADR-0058).
480489
const adapter = makeAdapter();
481490
const { container } = renderBoard(adapter, {
482491
data: ROWS,
483492
conditionalFormatting: [
484-
{ condition: "record.owner == 'bob'", backgroundColor: 'rgb(4, 5, 6)' },
493+
{ condition: "record.owner == 'bob'", style: { backgroundColor: 'rgb(4, 5, 6)' } },
485494
],
486495
});
487496

@@ -491,6 +500,23 @@ describe('objectui#8313 — `object-kanban`.`conditionalFormatting`: two member
491500
expect(stylesOnCard(container, 'Alpha deal')).not.toContain('background-color');
492501
});
493502

503+
it('the WHOLE `style` map reaches the card — `style` is the only colour channel now', async () => {
504+
// With the top-level colour keys retired (objectui#11522), every paint
505+
// rides `style`, so the map must arrive whole and not as one picked key.
506+
const adapter = makeAdapter();
507+
const { container } = renderBoard(adapter, {
508+
data: ROWS,
509+
conditionalFormatting: [
510+
{ condition: "record.owner == 'ann'", style: { backgroundColor: 'rgb(1, 2, 3)', borderColor: 'rgb(7, 8, 9)' } },
511+
],
512+
});
513+
514+
await waitFor(() => expect(container.textContent).toContain('Alpha deal'));
515+
expect(stylesOnCard(container, 'Alpha deal')).toContain('background-color: rgb(1, 2, 3)');
516+
expect(stylesOnCard(container, 'Alpha deal')).toContain('border-color: rgb(7, 8, 9)');
517+
expect(stylesOnCard(container, 'Beta deal')).not.toContain('border-color');
518+
});
519+
494520
it('⚠️ `ObjectKanban.tsx` never NAMES this key — it rides the `{ ...schema }` spread', () => {
495521
// The structural fact that makes the two rows above load-bearing, made
496522
// mechanical instead of left in prose. `conditionalFormatting` is the only

‎packages/plugin-kanban/src/__tests__/objectFieldsIsAPropNotASchemaKey-7742.test.tsx‎

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,10 @@ import '../KanbanImpl';
7676
/** The paint the matching rule applies — a colour no other element uses. */
7777
const PAINT = 'rgb(255, 0, 0)';
7878

79-
const RULE = [{ field: 'owner', operator: 'equals', value: 'u1', backgroundColor: PAINT }];
79+
// `{ condition, style }`: RESPELLED (objectui#11522) from the retired native
80+
// `{ field: 'owner', operator: 'equals', value: 'u1', backgroundColor }` — the
81+
// same comparison on the same relation field, the same paint.
82+
const RULE = [{ condition: "record.owner == 'u1'", style: { backgroundColor: PAINT } }];
8083

8184
/** `owner` arrives EXPANDED, the way the board's own `$expand` delivers it. */
8285
const CARD = { id: 'c1', title: 'Painted card', owner: { _id: 'u1', name: 'Ann' } };

‎packages/plugin-kanban/src/__tests__/structuredKeysAreDeclaredAndHonoured-8313.test.ts‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,9 @@ const DECLARED_STRUCTURED_KEYS = [
104104
{
105105
key: 'conditionalFormatting',
106106
arm: 'array',
107-
value: [{ field: 'owner', operator: 'equals', value: 'ann', backgroundColor: '#eef' }],
107+
// `{ condition, style }`: respelled (objectui#11522) from the retired native
108+
// `{ field: 'owner', operator: 'equals', value: 'ann', backgroundColor }`.
109+
value: [{ condition: "record.owner == 'ann'", style: { backgroundColor: '#eef' } }],
108110
},
109111
] as const;
110112

0 commit comments

Comments
 (0)