Skip to content
This repository was archived by the owner on Oct 26, 2026. It is now read-only.

Commit fa93c7f

Browse files
committed
docs(kanban): make the published docs state the optional groupBy contract (objectui#8990)
Contract review R1 (blocking): four hand-written doc sites still asserted the pre-change contract, and two of them named THIS card as the pending fix — so the PR closed the card while leaving the docs telling readers it was still open. AGENTS.md #2 ("Not done until docs reflect the code") is binding and no CI gate catches this class. - content/docs/api/schema-reference.md — the property table said "Required."; the columns callout said the bare-string arm is "inert on this block" because "groupBy is required here", and closed with "tracked as objectui#8990". - content/docs/plugins/plugin-kanban.mdx — same claim, same tracking sentence. - packages/plugin-kanban/README.md — "type and groupBy are required" and the inline "// required" comment, plus two further spots the review did not name that made the same claim. All now state optional AND the measured lane-less behaviour: lanes drawn from bare strings and titled by the RAW values (a grouped board shows picklist LABELS), zero cards, moves inert. Both "tracked as objectui#8990" sentences are deleted. The record-source rule is called out as separate and unaffected, since that refusal survives and is easy to conflate with the lane key. Also from the review: - Two unqualified sentences ("what made the arm live") now carry the schema-valid qualifier the groupBy docblock already had. The renderer never consulted this package's validator, so the arm was always live for a document that reached it unvalidated; what requiredness prevented was a document being valid AND getting there. - The mdx corpus evidence is DOWNGRADED where it was overstated: that fragment is still refused after this change, at RECORD_SOURCE_REQUIRED, so it shows a lane-less board is a documented authoring, not a document this change admits. The ListView evidence carries the argument alone and is strengthened to what was actually measured: objectDef loads async, so laneField is undefined on every load until it lands, and stays undefined when detectStatusField finds no stageField role and no status/stage/state/phase field, or when stageField is false (ADR-0085 suppression). - "can only NARROW #8993's reachable set" corrected to UNCHANGED / not widened. - The six rotted line addresses are no longer restated even as history. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Jmxdo7bmeqCQHLSfmLVX9w
1 parent fe330a7 commit fa93c7f

6 files changed

Lines changed: 96 additions & 46 deletions

File tree

‎.changeset/8990-object-kanban-groupby-optional.md‎

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,11 +21,17 @@ objectui#7322 justified it as "every documented and tested `object-kanban` node
2121
this key", and objectui#7780 recorded two producers excluded from that count; both are
2222
still live:
2323

24-
- `content/docs/utilities/data-objectstack.mdx` documents an `object-kanban` node that
25-
is exactly `{ type, dataSource }`, with no `groupBy`;
2624
- `packages/plugin-list/src/ListView.tsx` **generates** the node as
27-
`groupBy: laneField`, where `laneField` ends in an explicit `|| undefined`, so a view
28-
that declares no lane field emits `groupBy: undefined` at runtime.
25+
`groupBy: laneField`. `objectDef` loads asynchronously, so `laneField` is `undefined`
26+
on every load until it lands, and stays `undefined` whenever the object offers no
27+
`stageField` hint and none of `status` / `stage` / `state` / `phase`. The renderer
28+
serves that node; both published faces refused it.
29+
- `content/docs/utilities/data-objectstack.mdx` documents an `object-kanban` node that
30+
is exactly `{ type, dataSource }`, with no `groupBy`. ⚠️ This one is weaker and is
31+
cited for what it is: that fragment is **still** refused after this change, at
32+
`RECORD_SOURCE_REQUIRED`, because `dataSource` is not a rung of the record-source
33+
ladder. It shows a lane-less board is a documented authoring; it is not a document
34+
this change admits.
2935

3036
**What a lane-less board does, measured rather than assumed.** Every `schema.groupBy`
3137
read in `ObjectKanban.tsx` is a guarded early-return, so the board degrades instead of

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

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -932,7 +932,7 @@ A drag-and-drop Kanban board. The `object-kanban` type key validates the shape t
932932
| Property | Type | Description |
933933
|----------|------|-------------|
934934
| `objectName` | `string` | Object to fetch records from. |
935-
| `groupBy` | `string` | **Required.** Field whose values become the lanes (maps to column ids). |
935+
| `groupBy` | `string` | Field whose values become the lanes (maps to column ids). **Optional** since objectui#8990, matching `@objectstack/spec`. A board that omits it draws whatever lanes `columns` declares — and holds **no cards**, because records are only distributed once a lane key exists. |
936936
| `columns` | `string[] \| KanbanLane[]` | Swimlane definitions — an array of `{ id, title }` lanes (one per `groupBy` value), **or** an array of bare value strings; never a mix. **Not** a field projection (that is `cardFields`). A lane's `cards` is optional: an object-bound board buckets records into the lane by `groupBy`, and only a static board writes a lane's cards itself. |
937937
| `titleField` | `string` | Field used as the card title. |
938938
| `cardFields` | `string[]` | Fields rendered on each card. |
@@ -947,11 +947,11 @@ A drag-and-drop Kanban board. The `object-kanban` type key validates the shape t
947947
948948
> `columns` is declared on this face since objectui#8913, as the pair of array shapes `@objectstack/spec` declares — an array of `{ id, title }` lanes, **or** an array of bare value strings. A **mixed** array is refused: the renderer decides which shape it has from the first element alone, so a mix yields a blank lane and mis-bucketed cards. A lane accepts `id`, `title`, `cards`, `limit`, `className` and `collapsed`, which are the members the board implementations read; `id` is a **string** — a non-string lane id makes the board render every card twice, once in its lane and once in "Uncategorized" (objectui#8993). When a lane carries `cards`, each card is judged — a card with no `title` is refused. An undeclared lane key is accepted and dropped, not refused, which is this tolerant face's posture; the strict authoring face refuses it by name.
949949
>
950-
> ⚠️ The **bare-string array is accepted but inert on this block.** It is declared so this package does not refuse an authoring the protocol allows. The renderer reads a bare-string lane list only when a board has no `groupBy`, and `groupBy` is required here — so on `object-kanban` the strings are always ignored and the lanes come from the group field's picklist options or from the data. Write the `{ id, title }` array to control the lanes. The requiredness of `groupBy` is tracked as objectui#8990.
950+
> ⚠️ The **bare-string array applies only to a board with no `groupBy`.** It is declared so this package does not refuse an authoring the protocol allows. The renderer reads a bare-string lane list only when a board has no `groupBy` — so on a board that *does* declare one the strings are ignored and the lanes come from the group field's picklist options or from the data. Since objectui#8990 made `groupBy` optional, a lane-less board is a valid authoring and this arm is live on it: the lanes are drawn, titled by the **raw strings** (a grouped board titles its lanes with the picklist *labels* instead). ⚠️ Such a board holds **no cards** — with no lane key the records are never distributed — and dragging a card writes nothing back. It is lane headings, not a populated board; to control the lanes of a working board, declare `groupBy` and write the `{ id, title }` array.
951951
>
952952
> The other keys the retired `kanban` arm alone declared — `cardTitle`, `swimlaneField`, `grouping` and `navigation` — are still undeclared on this face. The renderer reads them, so a board may carry them; they are simply not judged. The board's React host supplies `onCardMove` / `onCardClick` / `onQuickAdd` as props; none of the three is authorable in JSON.
953953
954-
> `data` and `bind` are [`BaseSchema`](#baseschema) members, not narrowed here, but this face requires **one of** `bind`, `data`, `objectName` — the renderer's own record-source ladder (an external `data` prop → `bind` via `useDataScope` → this schema's own `data` → a fetch keyed by `objectName`). A purely static board (lanes carrying their own cards, no record source) authors `"groupBy"` and `"data": []`.
954+
> `data` and `bind` are [`BaseSchema`](#baseschema) members, not narrowed here, but this face requires **one of** `bind`, `data`, `objectName` — the renderer's own record-source ladder (an external `data` prop → `bind` via `useDataScope` → this schema's own `data` → a fetch keyed by `objectName`). A purely static board (lanes carrying their own cards, no record source) authors `"groupBy"` and `"data": []`. ⚠️ The record-source rule is **separate** from the lane key and is unaffected by objectui#8990: omitting `groupBy` is fine, omitting all of `bind` / `data` / `objectName` is still refused, at the refinement rather than at `groupBy`.
955955
956956
**Related:** [ObjectViewSchema](#objectviewschema), [ObjectGridSchema](#objectgridschema)
957957

‎content/docs/plugins/plugin-kanban.mdx‎

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -94,13 +94,19 @@ and only a static board writes a lane's cards itself. Both faces of
9494
validated: a card with no `title` is refused.
9595

9696
<Callout type="warn">
97-
**The bare-string array is accepted but has no effect on `object-kanban`.** It is
97+
**The bare-string array applies only to a board with no `groupBy`.** It is
9898
declared so this package does not refuse an authoring `@objectstack/spec` allows.
99-
The renderer only reads a bare-string lane list when a board has **no** `groupBy`
100-
— and `groupBy` is required on `object-kanban`, so on this block the strings are
101-
always ignored and the lanes come from the group field's picklist options or from
102-
the data. Write the `{ id, title }` array to control the lanes. The requiredness of
103-
`groupBy` is tracked as objectui#8990.
99+
The renderer only reads a bare-string lane list when a board has **no** `groupBy`,
100+
so on a board that declares one the strings are ignored and the lanes come from
101+
the group field's picklist options or from the data.
102+
103+
Since objectui#8990 made `groupBy` **optional** (matching `@objectstack/spec`), a
104+
lane-less board is a valid authoring and this arm is live on it: the lanes are
105+
drawn, titled by the **raw strings** — where a grouped board titles its lanes with
106+
the picklist **labels**. ⚠️ A lane-less board holds **no cards**: with no lane key
107+
the records are never distributed into lanes, and dragging a card writes nothing
108+
back. It is lane headings, not a populated board. To control the lanes of a
109+
working board, declare `groupBy` and write the `{ id, title }` array.
104110
</Callout>
105111

106112
```plaintext

‎packages/plugin-kanban/README.md‎

Lines changed: 16 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -45,9 +45,10 @@ import '@object-ui/plugin-kanban';
4545
// `object-kanban`. (The STORED `NamedListView.type` value `"kanban"` is a
4646
// different layer and is unaffected — do not rewrite saved views.)
4747
//
48-
// `groupBy` and ONE record source (`data`, `bind` or `objectName`) are what the
49-
// surviving face requires of every board; the shape below is the one the
50-
// catalog fixture `plugin-kanban/basic-kanban-board.json` carries.
48+
// ONE record source (`data`, `bind` or `objectName`) is what the surviving face
49+
// requires of every board; `groupBy` is OPTIONAL since objectui#8990 but is what
50+
// makes the lanes hold cards, so every working board authors it. The shape below
51+
// is the one the catalog fixture `plugin-kanban/basic-kanban-board.json` carries.
5152
const schema = {
5253
type: 'object-kanban',
5354
groupBy: 'status',
@@ -106,8 +107,9 @@ const column: KanbanColumn = {
106107
};
107108

108109
// ⚠️ `object-kanban`: the bare `kanban` node type key and its `KanbanSchema`
109-
// arm RETIRED in objectui#8802. `groupBy` and one of `bind` / `data` /
110-
// `objectName` are what the surviving face requires of every board.
110+
// arm RETIRED in objectui#8802. One of `bind` / `data` / `objectName` is what the
111+
// surviving face requires of every board; `groupBy` is optional since
112+
// objectui#8990, and authored here because a board without it holds no cards.
111113
const schema: ObjectKanbanSchema = {
112114
type: 'object-kanban',
113115
groupBy: 'status',
@@ -123,9 +125,14 @@ import type { ObjectKanbanSchema } from '@object-ui/types';
123125

124126
declare const columns: KanbanColumn[];
125127

126-
// The board document. `type` and `groupBy` are required, and so is ONE record
127-
// source — `bind`, `data` or `objectName`. `columns` and `className` are
128-
// optional.
128+
// The board document. `type` is required, and so is ONE record source — `bind`,
129+
// `data` or `objectName`. `groupBy`, `columns` and `className` are optional.
130+
//
131+
// `groupBy` is OPTIONAL since objectui#8990, matching `@objectstack/spec`. It is
132+
// still the key that makes the board work: with no lane key the records are never
133+
// distributed, so a lane-less board draws whatever lanes `columns` declares and
134+
// holds NO cards, and dragging a card writes nothing back. Omitting it is valid,
135+
// not useful — author it on any board meant to group records.
129136
//
130137
// ⚠️ `onCardMove` is NOT a document key: it is a React prop the host supplies
131138
// (JSON has no function value), which is why it is spelled with explicit
@@ -136,7 +143,7 @@ declare const columns: KanbanColumn[];
136143
// not what catches a misspelt board key.
137144
const board: ObjectKanbanSchema = {
138145
type: 'object-kanban',
139-
groupBy: 'status', // required — the field that makes the lanes
146+
groupBy: 'status', // optional, but the field that makes the lanes
140147
data: [], // one record source is required
141148
columns, // Array of columns
142149
className: 'h-full', // Tailwind classes

‎packages/plugin-kanban/src/__tests__/laneLessBoard-8990.test.tsx‎

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,11 @@
2828
* `effectiveColumns` memo). While `groupBy` was REQUIRED, no schema-valid
2929
* document could reach that branch: objectui#8913 admitted the arm anyway
3030
* (refusing it would have been a second narrowing) and recorded it as
31-
* unreachable. Making `groupBy` optional is what makes it live.
31+
* unreachable. Making `groupBy` optional is what makes it reachable BY A
32+
* SCHEMA-VALID DOCUMENT — the qualifier matters, and the section headed "WHICH
33+
* legs guard the widening" below is where it is measured: the RENDERER never
34+
* consulted this package's validator, so the branch was always live for a
35+
* document that reached it unvalidated. ⛔ Not "dead code that came alive".
3236
*
3337
* ⚠️ "The arm fires" is not observable from the lane COUNT alone — a grouped
3438
* board draws two lanes too. It is observable from the lane TITLES, and that is
@@ -77,9 +81,10 @@
7781
* lane below it. A lane-less board returns before reaching any of it, and the
7882
* picklist-materialised lanes whose ids come straight from `opt.value` are
7983
* built under `if (schema.groupBy && ...)`, a branch a lane-less board cannot
80-
* enter. ⇒ This card can only NARROW objectui#8993's reachable set on the
81-
* documents it newly admits, never widen it. ⛔ Nothing here fixes #8993; it is
82-
* a renderer behaviour change with its own card.
84+
* enter. ⇒ objectui#8993's reachable set is UNCHANGED by this card: admitting
85+
* documents cannot shrink it, and none of the documents newly admitted can reach
86+
* the defect. ⛔ Not "narrowed" — the claim is that nothing was widened. ⛔ And
87+
* nothing here fixes #8993; it is a renderer behaviour change with its own card.
8388
*/
8489

8590
import React from 'react';

‎packages/types/src/objectql.ts‎

Lines changed: 46 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -2928,11 +2928,12 @@ export interface ObjectKanbanSchema extends BaseSchema {
29282928
* from-data branch), `persistCardMove`, and the `handleCardMove` callback —
29292929
* plus their effect deps.
29302930
*
2931-
* ⚠️ Anchored by SYMBOL, deliberately. The docblock this replaces cited
2932-
* `:601` / `:613` / `:625` / `:640` / `:747` / `:865`; every one of those had
2933-
* rotted by the time this card re-derived them (the same reads now sit ~370
2934-
* lines lower). `check:new-cross-file-line-citations` says the same thing in
2935-
* its own words — shifting an already-false address by a hunk delta just
2931+
* ⚠️ Anchored by SYMBOL, deliberately. The docblock this replaces carried six
2932+
* bare line addresses into that file and EVERY ONE had rotted by the time this
2933+
* card re-derived them — the same reads now sit some 370 lines lower. The
2934+
* numbers are not restated here even as history: a literal address in prose is
2935+
* the thing that rots, and `check:new-cross-file-line-citations` says the same
2936+
* in its own words — shifting an already-false address by a hunk delta only
29362937
* moves a wrong pointer somewhere else. Undeclared here until
29372938
* objectui#7322, so an authored value reached the renderer only through
29382939
* {@link BaseSchema}'s `[key: string]: any` — admitted, never examined.
@@ -2954,17 +2955,31 @@ export interface ObjectKanbanSchema extends BaseSchema {
29542955
*
29552956
* 1. `content/docs/utilities/data-objectstack.mdx` documents an
29562957
* `object-kanban` node that is exactly `{ type, dataSource }` — no
2957-
* `groupBy`. The published validator refused this repository's own
2958-
* documented example.
2958+
* `groupBy`. ⚠️ WEAKER THAN IT LOOKS, and the limit is worth stating: that
2959+
* fragment is STILL refused after this card, at `RECORD_SOURCE_REQUIRED`
2960+
* — `dataSource` is not a rung of this ladder — so it is evidence that a
2961+
* lane-less board is a DOCUMENTED AUTHORING, not a document this card
2962+
* admits. ⛔ Do not cite it as "objectui refuses its own documented
2963+
* example" without that qualifier; the record-source half of the refusal
2964+
* is deliberate and survives.
29592965
* 2. `packages/plugin-list/src/ListView.tsx` GENERATES the node with
29602966
* `groupBy: laneField`, where
2961-
* `laneField = groupByField || groupField || detectStatusField(objectDef) || undefined`
2962-
* — an explicit `|| undefined`. A view that declares no lane field and
2963-
* whose object has no detectable status field emits `groupBy: undefined`
2964-
* at runtime, which the renderer serves and both faces refused.
2965-
*
2966-
* Same shape as {@link objectName} (objectui#7780): a key the renderer guards
2967-
* at every read, declared REQUIRED, refusing boards that render today.
2967+
* `laneField = groupByField || groupField || detectStatusField(objectDef) || undefined`.
2968+
* ⭐ This is the load-bearing one, and it is stronger than the explicit
2969+
* `|| undefined` alone suggests: `objectDef` loads ASYNCHRONOUSLY, so
2970+
* `laneField` is `undefined` on EVERY load until it lands — transiently
2971+
* for every list-view kanban — and PERSISTENTLY in two cases from
2972+
* `detectStatusField` (`packages/types/src/record-semantics.ts`): the
2973+
* object declares no `stageField` role AND carries no field named
2974+
* `status` / `stage` / `state` / `phase` and none typed `status`/`stage`;
2975+
* or it sets `stageField: false`, which suppresses detection outright
2976+
* (ADR-0085 — a status-shaped field that is not a linear flow). The
2977+
* renderer serves that node and both published faces refused it.
2978+
*
2979+
* ⇒ Evidence 2 carries this on its own; evidence 1 supports the authoring
2980+
* shape, not the accept set. Same shape as {@link objectName} (objectui#7780):
2981+
* a key the renderer guards at every read, declared REQUIRED, refusing boards
2982+
* that render today.
29682983
*
29692984
* ## What a lane-less board actually does — MEASURED, not argued
29702985
*
@@ -3001,9 +3016,11 @@ export interface ObjectKanbanSchema extends BaseSchema {
30013016
* `__uncolumned__` lane below it). A lane-less board returns before reaching
30023017
* it, and the picklist-materialised lanes whose ids come straight from
30033018
* `opt.value` are built under `if (schema.groupBy && ...)` — a branch a
3004-
* lane-less board cannot enter. So this card can only NARROW objectui#8993's
3005-
* reachable set on the documents it newly admits, never widen it. Measured
3006-
* both ways in the pin file above.
3019+
* lane-less board cannot enter. So objectui#8993's reachable set is UNCHANGED
3020+
* by this card — admitting documents cannot shrink it, and none of the
3021+
* documents newly admitted can reach the defect. ⛔ Not "narrowed": the
3022+
* containment is that nothing was widened. Measured both ways in the pin file
3023+
* above.
30073024
*/
30083025
groupBy?: string;
30093026
/**
@@ -3117,9 +3134,18 @@ export interface ObjectKanbanSchema extends BaseSchema {
31173134
* {@link groupBy} was REQUIRED, no document that passed this schema could
31183135
* reach that branch, and objectui#8913 admitted the arm anyway — refusing it
31193136
* would have made objectui narrower than the protocol — recording it as
3120-
* inert. objectui#8990 made {@link groupBy} OPTIONAL, which is what made the
3121-
* arm live: a `groupBy`-less board with `columns: ['todo', 'doing']` now
3122-
* parses AND draws those two lanes, titled by the raw strings.
3137+
* inert. objectui#8990 made {@link groupBy} OPTIONAL, which is what makes the
3138+
* arm reachable BY A SCHEMA-VALID DOCUMENT: a `groupBy`-less board with
3139+
* `columns: ['todo', 'doing']` now parses AND draws those two lanes, titled by
3140+
* the raw strings.
3141+
*
3142+
* ⚠️ The qualifier is load-bearing, and matches {@link groupBy}'s. The RENDERER
3143+
* never consulted this package's validator — `SchemaRenderer` runs core's
3144+
* structural `validateSchema`, which carries no kanban rule — so the string
3145+
* branch was always live for a document that reached it WITHOUT passing the
3146+
* published faces (`ListView.tsx`'s generated node; any host not running
3147+
* `os check`). What the requiredness prevented was a document being valid AND
3148+
* getting there. ⛔ Do not restate this as "the arm was dead code".
31233149
*
31243150
* ⚠️ RAW strings, not localized labels, and the difference is the arm's
31253151
* signature. The string branch returns `{ id: val, title: val }` and never

0 commit comments

Comments
 (0)