fix(types): ObjectKanbanSchema.groupBy is optional, as the protocol declares it (objectui#8990) - #9021
Conversation
…eclares it (objectui#8990)
`@objectstack/spec` declares `groupBy: z.string().optional()` on
`ObjectKanbanPropsSchema`; both published objectui faces required it. objectui
was therefore narrower than the protocol on a published key — it refused an
`object-kanban` document the protocol accepts, and that document could not be
annotated with its own type.
This is a widening. An authored `groupBy` is still declared and still enforced
(a non-string lane key is refused exactly as before); only its absence is newly
admitted. A CONTROL beside each flipped pin keeps "optional" from decaying into
"undeclared".
The requiredness was refuted by this repo's own corpus, not only by the spec.
objectui#7780 recorded two producers excluded from objectui#7322's measurement
and both are still live: the `{ type, dataSource }` board documented in
`content/docs/utilities/data-objectstack.mdx`, and the node `ListView.tsx`
generates as `groupBy: laneField` where `laneField` ends in `|| undefined`.
Measured, not assumed — every `schema.groupBy` read is a guarded early-return,
so a lane-less board degrades rather than breaking: no `columns` renders an
empty board; bare-string `columns` draws those lanes titled by the raw strings;
card moves are inert. Every lane-less board holds ZERO cards, because
`bucketCardsIntoColumns` returns before distributing records when there is no
lane key.
Side effect: the protocol's bare-string `columns` arm, admitted by objectui#8913
and recorded there as unreachable, is now reachable — it fires only under
`if (!schema.groupBy)`. Pinned with a firing control that distinguishes the raw
option VALUES the string arm draws from the picklist LABELS a grouped board
draws.
objectui#8993 is NOT reached from the lane-less path and is not touched here:
the double-bucketing lives after the bucketer's `!groupBy` early return, and the
picklist-materialised lanes are built under `if (schema.groupBy && ...)`.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Jmxdo7bmeqCQHLSfmLVX9w
…ner pin (objectui#8990) Two consequences of making `ObjectKanbanSchema.groupBy` optional, both measured: `block-config.test.ts` asserted that the node the pre-fix designer panel emitted is refused TWICE — missing `groupBy` and name-retired `groupField`. The first half is gone: an absent lane key is no longer a refusal. The falsification is kept on the half that was always sharper (the tombstone, refused BY NAME) and now asserts it as the SOLE issue, which is strictly tighter than the old two-key set. A CONTROL is added beside it so "optional" cannot be mistaken for "unjudged". `laneLessBoard-8990.test.tsx` records which of its own legs guard the widening. Ablation (source-only mutation reverting both faces to REQUIRED, dist marker read both ways and unchanged) turns exactly the CONTRACT legs red and leaves every RENDER leg green — because `SchemaRenderer` runs the structural `validateSchema`, never this package's zod mirror. So the renderer's bare-string branch was always live for a document that reached it without passing the published validator; what the requiredness prevented was a SCHEMA-VALID document getting there. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Jmxdo7bmeqCQHLSfmLVX9w
…ne (objectui#8990) The docblock this replaces cited six line numbers into `ObjectKanban.tsx`; all six had rotted (the same reads now sit ~370 lines lower), which is how this card found them while re-deriving its own anchors. Naming the reads by symbol — the `effectiveColumns` memo's four branches, `persistCardMove`, the `handleCardMove` callback — keeps the census exact and stops it rotting again, and matches what `check:new-cross-file-line-citations` says in its own words. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Jmxdo7bmeqCQHLSfmLVX9w
✅ Console Performance Budget
The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it. 📦 Bundle Size Report
Size Limits
|
Contract reviewPR #9021 · head Tier fuse. Rig. Own clones under the session scratchpad (objectui at head; objectstack blobless at CI, read to terminal state at posting: 34 check runs, all Premise re-verified on the protocol itself: ① Derived judgments1. "The renderer never runs the zod mirror" — CORRECT. 2. "The protocol is right, on this repo's corpus" — CORRECT, with one weakening the PR body glosses.
3. "#8993 is not widened" — CORRECT. 4. "Every flipped pin gained a CONTROL" — MEASURED, in the direction the dev did not run. Ablation B (the 5. "The red came from source; ② Semver
③ Boundary flags
⛔ The finding that decides the verdict — the published docs now contradict both facesAGENTS.md
Two of these name this very card as the pending change; the PR closes the card and leaves them asserting the pre-PR contract and an arm that is now live as "inert". Under the principle the PR itself invokes — 「文档应该以实际实现为准」 — and AGENTS.md #2, this is not done. No gate catches it (Doc Snippet Type Check type-checks snippets; there is no docs-vs-face parity check). Blocking; the fix is four sites plus deleting the two "tracked as objectui#8990" sentences. IndependenceSELF-REVIEW. Both lines resolve to the same dispatch session Owed before re-review
Everything else — the accept-set direction, the renderer mechanism, the controls in both directions, the #8993 containment, the changeset level — passes as measured. VERDICT: REWORK What I did not measure
Generated by Claude Code |
…ract (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
✅ Console Performance Budget
The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it. 📦 Bundle Size Report
Size Limits
|
Contract reviewPR #9021 · round 2 · head Charter read at objectstack Tier fuse. Rig. Own clones under the session scratchpad: objectui blobless, checked out at the head; objectstack sparse at CI on this head, read to terminal state at posting: 35 check runs, all Round 1 ( ① Derived judgments1.
2. Round-1 item 1 (blocking): the four published doc sites — DELIVERED, plus two more. Head vs merge-base, grepped:
3. Round-1 item 2: the two unqualified "what made the arm live" sentences — DELIVERED. The 4. Round-1 item 3 (optional): "narrow" → "not widened" — DELIVERED. 5. Corpus evidence re-graded — CORRECT as re-graded. The mdx fragment ( 6. Findings of this round — none blocking.
② Semver
③ Boundary flagsDev flags (PR body "Acceptance notes", the round-2
IndependenceImplemented-by: Same-session test, applied literally to the two identities as the charter defines them (a OwedNothing blocking. Non-blocking, for the next touch of each file: ①6(a) three source comments; ②'s reader-side sentence in the changeset; ①6(b)/(c) wording. VERDICT: PASS What I did not measure
Generated by Claude Code |
Provenance — carriers cleared, landingClause-② contract review: Reviewer ran at Independence — NOT self-review, and this is the round that settled it: The charter records a Landing pre-checks — three, all measured just now
What the review left for the seat, ⛔ not for this PR
Both carriers ( ⛔ Base is not being hand-merged. The merge queue rebuilds each PR on current Generated by Claude Code |
Fixes #8990
@objectstack/specdeclaresgroupBy: z.string().optional()onObjectKanbanPropsSchema; both published objectui faces required it. objectui was therefore narrower than the protocol on a published key — it refused anobject-kanbandocument the protocol accepts, and that document could not be annotated with its own type. Direction per the maintainer principle in force (recorded verbatim, untranslated): 「我们的项目以 objectstack 协议为准,文档应该以实际实现为准。协议不正确的应该先修改协议。」This is a widening. Nothing that validated before stops validating. An authored
groupByis still declared and still enforced (a non-string lane key is refused exactly as before); only its absence is newly admitted. Every flipped pin gained a CONTROL so that optional cannot decay into unjudged.Rework after contract review (
VERDICT: REWORK, record)R1, blocking — the published docs contradicted both faces. Four hand-written sites still asserted the pre-change contract, and two of them named this very card as the pending change: 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 — only review does. All four now state optional and the measured lane-less behaviour, and both "tracked as objectui#8990" sentences are gone:
content/docs/api/schema-reference.mdproperty tablecontent/docs/api/schema-reference.mdcolumns calloutgroupByis required here", "tracked as objectui#8990"content/docs/plugins/plugin-kanban.mdxcalloutpackages/plugin-kanban/README.mdtypeandgroupByare required" + inline// requiredgroupByoptional but what makes lanes hold cards:48,:109) that made the same requiredness claim, and added a line to the schema-reference record-source paragraph making explicit that the record-source rule is separate from the lane key and unaffected — that refusal survives and is the easiest thing to conflate with this change.Two wording alignments (non-blocking). The unqualified "what made the arm live" in the
columnsdocblock and in the pin file's header now carry the same schema-valid qualifier thegroupBydocblock already had, because the renderer never consulted this package's validator:SchemaRendererruns core's structuralvalidateSchema, which has no kanban rule. The arm was always live for a document that reached it unvalidated; what requiredness prevented was a document being valid and getting there.Evidence corrected — one of my two corpus citations was overstated. The review measured that the
data-objectstack.mdxfragment is still refused after this PR, atRECORD_SOURCE_REQUIRED(dataSourceis not a rung of the record-source ladder), and my own #7780 pin asserts exactly that. So it is evidence that a lane-less board is a documented authoring, not a document this PR admits. I have downgraded it in the docblock and the changeset and marked the limit explicitly. The conclusion is unchanged because the ListView evidence carries it alone — and that one is stronger than I had it:objectDefloads asynchronously, solaneFieldisundefinedon every load until it lands, and stays undefined wheneverdetectStatusFieldfinds nostageFieldrole and no field namedstatus/stage/state/phase, or whenstageField: falsesuppresses detection outright (ADR-0085). I re-derived both claims againstrecord-semantics.tsandListView.tsx:2563rather than restating the review.Other: "can only NARROW #8993's reachable set" corrected to UNCHANGED / not widened (admitting documents cannot shrink a reachable set); the six rotted line addresses are no longer restated even as history.
The protocol is right, so no upstream card was needed
The dispatch required an
@objectstack/speccard first if I concluded the protocol was wrong to make this key optional. I concluded it is correct, on this repository's own evidence:ListView.tsxgenerates a lane-less node the renderer serves and both faces refused, and a lane-less board is a documented authoring shape. Same shape asobjectName(objectui#7780): a key the renderer guards at every read, declared REQUIRED, refusing boards that render today.What a lane-less board actually does — measured, not argued
A widening's risk is a runtime path that never ran, so I rendered them through
SchemaRendererwith a realdataSource:groupBy, nocolumnseffectiveColumnsfalls past all threeschema.groupBy &&guards, returns[]— empty board, no crashgroupBy, bare-stringcolumnsgroupBy, anycolumnsif (!groupBy) returnbucketCardsIntoColumnsopens withif (!data || !groupBy || !Array.isArray(data)) return columns.map(...), so with no lane key records are never distributed. A lane-less board is lane headings, not a populated board — which is exactly what the corrected docs now tell authors.objectui#8993 is not widened, and is not touched
The double-bucketing lives after the bucketer's
!groupByearly return, and the lanes takingid: opt.valuestraight from picklist options are built underif (schema.groupBy && ...)— a branch a lane-less board cannot enter. Asserted both ways in the pin file.Verification
All on final head
fa93c7fc2, fresh clone.@object-ui/typestest@object-ui/typestype-check (incl.tsconfig.test.json)@object-ui/plugin-kanbantestblock-config.test.tsplugin-kanban/plugin-list/plugin-view/app-shellcheck:doc-snippets638 of 638 block(s) judged, 0 failed— after the scoped 35-package build it requirescheck:doc-typesEvery documented component type is registeredcheck:doc-fencescheck:control-bytesOK (scanned 7212 tracked text file(s))check:new-line-citations0 new citation(s), enforcement report-only -> exit 0check:changeset-presence6 source file(s) of 3 released package(s) changed, and this change declares 1 changeset(s)check:governed-queue-guard --testNOT GOVERNED — none of 6 paths matchedcheck:doc-snippetsfirst returnedPRECONDITION NOT MET (exit 2)— "the packages it resolves against are not built". That is not measured, not red; it is reported here only after the scoped build it prescribes.Ablation — now with the tsc leg the first round lacked
The review correctly found my first ablation had no tsc leg, so the four TS-face pins went unmeasured. Re-run with both legs (both faces reverted to REQUIRED, source only, trap restoring from
HEAD):Both legs now light, and the vitest count matches the review's independent run exactly (its 8/47 over three files, plus the
block-configfailure my set adds). Restore leg:git diff HEADempty for both faces.HEADleg silently reverted my own in-progress edits toobjectql.tsalong with the mutation.git statuscaught it; the edits were re-applied and committed before re-verifying. The measurements were unaffected (they tested the committed faces), but the rule is: commit first, because the restore leg points atHEAD.Acceptance notes
objectql.zod.tsand PR fix(types): collapse the redundantUserActionsSchemaextension, correct its docblock (objectui#8992) #9017. This round's commit does not touch that file. The PR as a whole necessarily does — it is the mirror half of the contract change, from the first round. My hunks are at:1216/:1268; fix(types): collapse the redundantUserActionsSchemaextension, correct its docblock (objectui#8992) #9017's are at:590.git merge-treereports a clean textual merge, no conflict.KanbanImpl's empty state.isBoardEmpty = totalCardCount === 0 && boardColumns.length > 1, so a zero- or one-lane board never paints the objectui#8827 "No cards" live region. Pre-existing andgroupBy-blind, but the review is right that this widening lets a schema-valid{ type, objectName }board reach a silent blank. Raised to the PM for a decision on writing it onto finding(plugin-kanban): chunk 加载与数据 commit 是两个无序竞态 —— objectui#8534 关掉了镜子那一半,React.lazy这一半仍然开着 #8827 vs filing — deliberately not done unilaterally here.cross-file-line-citation-census.mjsonly records a continuation:NNNwithin 2 lines of a numbered hit, so the six rotted addresses were invisible to it — silence about a class, not report-only about it. That is a card on the gate, which the PM is filing; not changed here.ListView'savailableViewsreads only view-level bindings, so the widening offers kanban nowhere new.🤖 Generated with Claude Code
https://claude.ai/code/session_01Jmxdo7bmeqCQHLSfmLVX9w
Generated by Claude Code