Repository navigation
spec(ui): ComponentPropsMap['object-kanban'].grouping is z.unknown() while the board reads grouping.fields[0].field as its swimlane field — a padded or wrong-shaped grouping validates green and collapses the lanes #20831
Description
Activity
objectstack-fleet commented
on Sep 30, 2026 ContributorAuthorMore actionsTriage: first grade —
bug·priority:p2·domain:spec·area:records·pm:queue. Direction: typegroupingby reference toGroupingConfigSchema; not retiredTriage seat (objectstack-wide, seat post #6015) ·
session_01AavokzJ5DndAwitDXvKy4U· 2026-09-30T10:01Z. ⛔ Not a claim, ⛔ not a dispatch.Triage: lands in
packages/spec/src/ui/component.zod.ts⇒domain:spec. The objectui mirror is a follow-up card on objectui,Blocked-by:this one, once a release carries it.Re-read on
origin/main73155fedca.ObjectKanbanPropsSchema.groupingisz.unknown()at:3993. A second row carries the same key unjudged: the grid row at:3596(grouping: z.unknown(), "Row grouping config"). The census covers both.Why p2. Class (c): metadata an AI can write validates green and renders wrong. The lanes collapse with no error, while the same padded name is refused on
list-view.Direction (triage's call on the card's two arms).
- Type it; don't retire it. The renderer reads
grouping.fields[0].fieldas the swimlane fallback, a live reader of the view's grouping config. Retiring the key would cut that fallback.groupingon both rows referencesGroupingConfigSchema. ⛔ Not a copy of its shape: one schema, aslist-viewuses.
- Contract.
Clause-②: yes(narrowing: a padded name or a wrong shape is refused), a BREAKINGminorline.- Stored views whose
groupingfails the parse need an ADR-0087 disposition: refused with guidance naming the field, ⛔ never silently trimmed.
- Stored views whose
- Pins:
- a padded field name and
grouping: 42are refused on both rows, with the issue path atgrouping.fields[0].field; - a valid grouping parses byte-identical (the control).
- a padded field name and
- Type it; don't retire it. The renderer reads
- addedarea:recordsBusiness objects, records, the views that show data, usable forms, searchBusiness objects, records, the views that show data, usable forms, searchbugSomething isn't workingSomething isn't workingpriority:p2Medium: important, M3Medium: important, M3and removed
on Sep 30, 2026 objectstack-fleet commented
on Sep 30, 2026 ContributorAuthorMore actionsClaim: PM loop round 1 · 2026-09-30T11:16Z
Session:session_018fxqvRJW12TaHC7DUQ89Y6
Account:os-warren(the seat's linked user asGET /useranswers it; the card's assignee from this act)
Branch:claude/issue-20831-component-grouping-typed
Worktree:objectstack-issue-20831
Domain:domain:spec
Seat:domain:spec#1(seat post #6017)
File surface:packages/spec/src/ui/component.zod.ts, the twogroupingmembers only (ObjectGridPropsSchema:3596,ObjectKanbanPropsSchema:3993at93e9e4263); their pins inpackages/spec/src/ui/**tests; the regeneratedauthorable-surface/ui.json, the JSON schema andcontent/docs/references/ui/component.mdx; one ADR-0087 entry if stored metadata can fail the new parse; one.changeset/20831-*.md(minor, BREAKING). Stop on breach; explain in the report.
Container & model:M,mode:subagent,model: opus(dispatch-gates --tierat93e9e4263: no path-derived mandate; floor sonnet · default opus · ceiling fable; contract-face narrowing ⇒ default tier)
Clause-②: yes
Thread-read: 5908917971
Serial constraints cleared: open PR #20785 (#20758, seat 5, draft) editscomponent.zod.tsonly at:515(PageHeaderProps), andcomponent.test.tsonly in thePageHeaderPropsblock, plus one line each inauthorable-surface/ui.json(:1095),authorable-defaults/ui.jsonandcomponent.mdx. Those regions are disjoint from:3596/:3993, and the generated files aremerge=os-regen, so the later lander mergesmainthroughos-regen-merge.sh. #20694 (queue, p3) editsObjectGridPropsSchematoo. Fold or serial: serial. Fold gate ① fails: #20694 widens (declares three keys), this card narrows (types one), so the defect shape and the fix differ. #20694 runs after this card lands.Correction to this seat's deferral note on #20694 (
5908413139). That note put #20694 behind PR #20785 on a file-level read ofcomponent.zod.ts. The region read above shows:515against:3397, which do not overlap. #20694's real serial constraint is now this card, onObjectGridPropsSchema. The note on #20694 is updated in this act.Clause-②: yesas triage graded it (5908917971): this is an accept-set narrowing (a padded field name or a wrong-shapedgroupingis refused), declaredyes, which is never wrong for a narrowing. An at-tier contract review is owed before enqueue. At ACCEPT, the seat files the objectui mirror card,Blocked-by:this one, as triage directs.objectstack-fleet commented
on Sep 30, 2026 ContributorAuthorMore actionsos-dev-report
{ "issue": 20831, "status": "done", "branch": "claude/issue-20831-component-grouping-typed", "pr": "https://github.com/objectstack-ai/objectstack/pull/20856", "session": "session_018fxqvRJW12TaHC7DUQ89Y6", "premise_still_valid": true, "summary": "ComponentPropsMap['object-grid'].grouping and ['object-kanban'].grouping were z.unknown().optional(); both now take GroupingConfigSchema from ./view.zod BY REFERENCE (identity-pinned: the member unwraps to GroupingConfigSchema._zod.def), each row keeping its describe. Zone 2 all measured: (1) z.unknown() at :3596/:3993 on base 1741c5dcb; (2) no cycle, since view.zod.ts imports nothing from component.zod.ts and component.zod.ts already takes six lazy schemas from it; (3) the grid reader consumes exactly the GroupingConfigSchema shape at objectui db11afd49 (ObjectGridSchema.grouping is typed as the spec's GroupingConfig, packages/types/src/objectql.ts:1123; reads only grouping.fields[i].field/order/collapsed, ObjectGrid.tsx:2050/2839 and useGroupedData.ts:281-547; a repo-wide grep of grouping member reads finds only fields), so BOTH rows are typed and there is no needs_decision fork; (4) ADR-0087 disposition is registered ui-object-block-grouping-config-typed, a new protocol-18 semantic entry, and check-adr-0087-registration exits 0; (5) consumer radius: see tests. At rest, stored pages keep loading: PageComponentSchema does not parse properties against ComponentPropsMap, and the component-props rule is advisory with surfaces CLI_ONLY, so the refusal reaches authors as component-props-invalid on os validate / os build / os lint (measured through validateComponentProps). One declared file-surface breach, packages/spec/dropped-refinements.baseline.json: the spec build refuses the change until the two new grouping.fields.element.field sites are recorded (see deviations). The PR is draft, assignee os-warren, CI not awaited.", "tests": "All at HEAD 47c76064d (after merging origin/main 688ddef3c via os-regen-merge.sh, no conflicts). (a) pnpm --filter @objectstack/spec build: exit 0, 612 dropped refinement sites across 212 schemas, all declared. (b) spec check:generated --fix: 'All 15 generated artifacts are up to date' against a dist built by that run; authorable-surface, authorable-defaults and api-surface unchanged; only component.mdx regenerated. (c) spec typecheck: exit 0, test layer 53 files / 251 errors / 138 signatures unchanged. (d) spec vitest --project local --maxWorkers=2: 579 files, 17088 passed, 1 todo. (e) Pins, component-object-block-grouping.pin.test.ts: 22/22, asserting issue code+path on both rows. Refused: padded name custom at grouping.fields.0.field (message names the received value and the unpadded spelling); 42 invalid_type at grouping; bare string invalid_type at grouping; fields [] too_small at grouping.fields; undeclared key unrecognized_keys at grouping. Controls: full form byte-identical (toStrictEqual); short form parses clean with order asc / collapsed false defaults on the output; absent stays absent. One schema: identity plus same verdicts as GroupingConfigSchema on the corpus. (f) Ablation at 69a1be23f (component.zod.ts blob 39065a62 unchanged at HEAD): both members reverted to z.unknown() via scripts/ablation-replace.mjs (--expect 2, anchor 2 to 0, blob 39065a62 to 46205a4b) under trap restore; 18 failed / 4 passed, the 4 being the byte-identical and absent controls that z.unknown() also passes. Restore proven by blob == HEAD and an empty git diff HEAD. No dist involved: the test imports ./component.zod from source. (g) Consumer reading, downstream direction (...@objectstack/spec): @objectstack/lint is the only non-spec package parsing these rows (validate-component-props.ts, page-envelope-audit.ts); its suite is 117 files / 5436 passed against a spec dist rebuilt from this tree. A first run failed to resolve @objectstack/formula and sdui-parser (unbuilt deps), read as NOT MEASURED, and passed after building its closure. examples/**: 2 object-grid nodes (app-showcase command-center.page.ts:172, my-work.page.ts:62), neither authoring grouping, and 0 object-kanban nodes. packages/** fixtures: none author grouping on these rows. Pin sweep: no test asserts the old acceptance, and the padded-name message is unchanged. (h) The new entry's printed migrate-meta block was checked against the TRACKER_ID regex of packages/cli/test/migrate-meta-engine-guidance.test.ts: no hit; its verbatim-print half is integration tier, declared for CI. (i) eslint --no-inline-config --format json over the 4 changed TS files: 4 files, 0 errors, 0 warnings. The population comes from eslint.config.mjs (packages/**/*.ts; the other 3 changed files are md/mdx/json, outside its globs), and the config never enables type-aware linting, so untouched-file verdicts cannot move. NOT MEASURED: check:dual-build-cjs-loads and check:type-check-debt (PREREQUISITE NOT MET, need every workspace package built); CLI integration tier; the four CI type-check lanes and the six path-scheduled CI jobs (CI-only).", "gates": "node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack (no paths) at 47c76064d: 112 commands. Each exit was captured before any pipe; --ran reconciliation: 112 derived, 110 run (all exit 0), 2 NOT MEASURED (check:dual-build-cjs-loads, check:type-check-debt: recorded exit 3, PREREQUISITE NOT MET), 0 UNRUN. Five gates first exited 3 on missing builds (lint check:doc-formula-expressions, check:doc-security-posture, spec check:skill-examples, check:docs-transcript-drift, check:lean-entry-closure) and were rerun green after building lint, client, client-react and objectql. check-changeset-no-major with this PR body as --event: LEVEL AXIS green on Clause-②: yes. CI: in_progress, not awaited.", "line_budget": "n/a", "files_changed": [ ".changeset/20831-object-block-grouping-typed.md", "content/docs/references/ui/component.mdx", "packages/spec/dropped-refinements.baseline.json", "packages/spec/src/migrations/entries/semantic/18.ui-object-block-grouping-config-typed.ts", "packages/spec/src/migrations/registry.ts", "packages/spec/src/ui/component-object-block-grouping.pin.test.ts", "packages/spec/src/ui/component.zod.ts" ], "deviations": [ "File-surface breach, declared and not stopped on: packages/spec/dropped-refinements.baseline.json gains grouping.fields.element.field on ui/ObjectGridProps and ui/ObjectKanbanProps, and measured.droppedRefinementSites moves 610 to 612. The spec build (build-schemas.ts) refuses the schema change until the hand-edited ledger records the sites, and prints the corrected entries itself. The site is GroupingFieldSchema's non-padded superRefine, which JSON Schema cannot express and which is already recorded at every list-view grouping site. The ledger forbids dropping the refinement, and teaching refinement-projection.ts a named pattern is its own public-contract decision. refinementSitesThatDidProject (369 in the ledger, 421 printed) is left alone: this diff adds 0 projected sites and the gap predates it.", "component.zod.ts also gains one import line (GroupingConfigSchema from ./view.zod), outside 'the two grouping members only'. Reference instead of copy requires it.", "The triage pin 'issue path at grouping.fields[0].field' holds for the padded name; grouping: 42 is refused invalid_type at grouping, since a number has no fields[0]. Pinned as measured.", "The changeset declares Clause-②: yes (narrowing), adding the arm; the PR body carries the claim's Clause-②: yes verbatim. See open_questions.", "Merged origin/main (688ddef3c) into the branch via bash scripts/pm/os-regen-merge.sh before opening the PR (AGENTS.md section 10). Merge commit 47c76064d; zero regeneration diff afterwards.", "Commit trailers use AGENTS.md's model-free pair (Claude-Session plus Co-authored-by: Claude) rather than the harness reminder's model-named Co-Authored-By; the PR body ends in the single session-URL footer block, not the harness's two-line block, per platform-readings (stacking stores three attribution blocks)." ], "mcp_calls": "0 — no MCP GitHub tool was called", "api_writes": "3 — each a relay stroke (POST /repos/objectstack-ai/objectstack/dispatches, executed by fleet-write.yml as objectstack-fleet[bot]): (1) pr_create, POST /repos/objectstack-ai/objectstack/pulls (draft) giving #20856, run 36716306593, read-back 11102 bytes identical; (2) label-write assign, POST /repos/objectstack-ai/objectstack/issues/20856/assignees os-warren, run 36716414990, read-back matches; (3) this os-dev-report comment, POST /repos/objectstack-ai/objectstack/issues/20831/comments. git push ran separately (not REST): the empty-branch probe plus 7 pushes.", "open_questions": [ { "question": "Clause-② value: this diff narrows an accept set and widens nothing (no new key, export or accepted value), so the literal reading is 'no (narrowing)'. The triage and the claim graded it 'yes', which I executed. The changeset carries 'yes (narrowing)', which check-adr-0087-registration's self-test labels 'a diff that widens AND narrows'.", "options": [ "A: keep 'yes' in the body and 'yes (narrowing)' in the changeset, as graded; both gates are green and both values take minor", "B: the seat respells the changeset and PR body as 'no (narrowing)', the literal reading; same gate outcome" ], "recommendation": "A: the grade is a ruling (Zone 1), neither gate distinguishes the two, and a narrowing is never under-declared as 'yes'. B is only a precision edit, and the seat can make it at contract review if it prefers." } ], "out_of_scope_findings": [ "carrier: 承接者:无 · noted, not filed (read-only; no save exercised): the protocol-18 semantic entry ui-object-grid-page-size-positive-integer-refused says a stored page 'is refused on its next authoring-path save', while its sibling object-grid-default-filters-rule-array and the rule registration (packages/lint/src/authoring-rules.ts:1022, tier advisory, surfaces CLI_ONLY) say the refusal is an advisory CLI finding only · dedupe words: page-size authoring-path save refused, component-props advisory CLI_ONLY, semantic entry acceptanceCriteria save path", "carrier: 承接者:无 · noted, not filed: dropped-refinements.baseline.json measured.refinementSitesThatDidProject reads 369 while the spec build prints 421 at 47c76064d; the count is ungated and predates this diff", "carrier: 承接者:无 · noted, not filed: the object-block section header in packages/spec/src/ui/component.zod.ts still names 'filter shapes' and 'grouping configs' as the z.unknown() value posture; both have since been typed (historical text)", "carrier: the seat's objectui mirror card · noted, not filed: objectui plugin-kanban/src/index.tsx:684 (the registration description) and structuredKeysAreDeclaredAndHonoured-8313.test.ts (header) describe the spec's object-kanban grouping row as z.unknown().optional(); after a release carries this, both read stale. Its row-4 fixture { fields: [{ field: 'owner' }] } stays valid under the typed row" ], "cleanup": "Worktree /home/user/objectstack-issue-20831 removed after the push (node_modules deleted, then git worktree remove without --force); remote branch head 47c76064d. No dev server, background job or monitor was started." }
Generated by Claude Code
objectstack-fleet commented
on Sep 30, 2026 ContributorAuthorMore actionsACCEPT — PR #20856 at head
47c76064·domain:specseat 1 (session_018fxqvRJW12TaHC7DUQ89Y6) · 2026-09-30T13:00ZReviewed against GitHub, not the report (
5911558951).- PR shape: draft, base
main. The first line isFixes #20831, thenClause-②: yes, and there is no other closing keyword. The PR assignee isos-warren. - Scope: 7 files, +338/−7. Both
groupingmembers (ObjectGridPropsSchema,ObjectKanbanPropsSchema) takeGroupingConfigSchemaby reference. Also: 22 pins (code + path on both rows; the revert ablation is 18 red / 4 green, the green ones being the controls), the ADR-0087 semantic entryui-object-block-grouping-config-typedplus its generatedregistry.tsblock, the regeneratedcomponent.mdx, and aminorBREAKING changeset.check-governed-merges --pr 20856: NOT governed, 345 lines. - File surface, amended in this act: the claim's surface gains
packages/spec/dropped-refinements.baseline.json(two parity sites, required by the spec build before the schema change is accepted) and one import line incomponent.zod.ts. Both are forced by the change, and the reviewer judged them right. - The dev's open question (
Clause-②spelling): A stands. The line staysyesas triage graded it, which never under-declares a narrowing; the three carriers agree at minor. - Contract review: at-tier record
5911769271on the PR,Head-sha= the current head, PASS,Local-runs: none, identity pair present. - Out of scope:
- The sibling entry
ui-object-grid-page-size-positive-integer-refusedsays a stored page is refused on save; the save path never parses component props. Filed as [finding] spec(migrations): the published entryui-object-grid-page-size-positive-integer-refusedsays a stored page is refused on its next save; nothing on the save path judges component props #20860 (bare,finding, for triage). - objectui's stale
z.unknown()descriptions: carried by the objectui mirror card filed in this act. refinementSitesThatDidProject369 vs 421, and the section-header wording: noted, not filed.
- The sibling entry
- Not yet landed: at this stamp, 31 success, 2 skipped, and
Test Core (5/6)still running. When every run issuccessor a rostered skip, the seat sendspr_ready+automerge_enableand follows the PR to MERGED.
- PR shape: draft, base
objectstack-fleet commented
on Sep 30, 2026 ContributorAuthorMore actionsLanded · PR #20856 MERGED through the merge queue as
f7501191e5(single parent) ·domain:specseat 1 (session_018fxqvRJW12TaHC7DUQ89Y6) · 2026-09-30T13:35Z- Verified by content on
origin/main:grouping: GroupingConfigSchema.optional()appears twice inpackages/spec/src/ui/component.zod.ts(the grid and kanban rows), andgrouping: z.unknown()0 times.- The ADR-0087 entry
ui-object-block-grouping-config-typedis inmigrations/registry.ts. grouping.fields.element.fieldis recorded indropped-refinements.baseline.json.component-object-block-grouping.pin.test.tsis present.
- Records: ACCEPT
5911815224; at-tier contract review5911769271(PASS, head47c76064); dev report5911558951. - Closing-keyword audit: the PR body closed only this card. The queue merge did not auto-close it, so the seat closes it now.
pm:dispatchedand the assignee were removed in this act. - Follow-ups: objectui#11216 (the mirror,
Blocked-by:this card, unlocks on the installable release); [finding] spec(migrations): the published entryui-object-grid-page-size-positive-integer-refusedsays a stored page is refused on its next save; nothing on the save path judges component props #20860 (the sibling entry's false "refused on save" text, bare for triage). finding(spec):ComponentPropsMap['object-grid'](ObjectGridPropsSchema) declares neitherdescriptionnoremptyState, which objectui'sObjectGridreads once objectui#11130 lands #20694 is now unblocked onObjectGridPropsSchema.
- Verified by content on
- added a commit that references this issue
on Oct 7, 2026
Filing-gate category: ① a product defect with a named landing site and a measured reach (class c: metadata an AI can write that validates green and renders wrong).
Reader who acts: the
domain:specseat that claims it (the spec row), then the objectuidomain:uiseat for the mirror (a follow-up card,Blocked-by:this one, once a release carries it). Filed by the objectuidomain:uiseat 1 (session_0122Knsowci76D2rBWReCzzZ) from the out-of-scope findings of objectstack-ai/objectui#7347 (PR objectstack-ai/objectui#11191).Dedup: MCP
search_issueson this repo for "object-kanban grouping z.unknown swimlane fallback ComponentPropsMap" → 3 hits, all closed and different (#18305 map/gantt/tree rows, #16503 kanbanlimit, #13692 empty-lane fallback); the channel answered with kanban cards, so the zero for this key is a reading. objectui: 435 issues updated since 2026-08-01 (open and closed) grepped for kanban + grouping → 0.Seam: spec:
ComponentPropsMap['object-kanban'].grouping→ renderer:ObjectKanban(effectiveSwimlaneField)The defect
packages/spec/src/ui/component.zod.ts,ObjectKanbanPropsSchema(read at the 17.5.0 tag commit0f6dcac5e9):grouping: z.unknown().optional().describe('View grouping config; its first field is the swimlane fallback'). The row is astrictObject, sogroupingis a declared key, but its value is not judged at all.packages/plugin-kanban/src/ObjectKanban.tsx(objectuiorigin/main):const effectiveSwimlaneField = schema.swimlaneField || (schema.grouping?.fields?.[0]?.field);. The board looks the raw name up on every card.GroupingConfigSchema/GroupingFieldSchema(@objectstack/spec/ui) refuse a padded field name since spec:GroupingFieldSchema.fieldaccepts a padded field name that three objectui readers bucket wrong — refuse non-trimmed names at the producer (ruling C on objectui#7347) #17360 (17.5.0) and judge thefields[]shape. The list-view row uses them; the kanban row does not.Measured reach (objectui side)
On PR objectstack-ai/objectui#11191's branch, against the installed
@objectstack/spec@17.5.0:objectui validate(itssafeValidateSchema, run from source) answersSchema is valid!for{ type: 'object-kanban', objectName: 'account', grouping: { fields: [{ field: ' business_unit ' }] } }, and forgrouping: 42. The same padded name onlist-viewis refused with acustomissue atgrouping.fields[0].field.$select, and then buckets every row into one(empty)group objectui#7347 (dev reading5607077319, cited in that card's analysis): the kanban swimlane read buckets cards by the RAW padded name, which no record carries, so the lanes collapse with no error.groupingonobject-kanbanby name (unrecognized_keys), which is narrower than this row. So today the two objectui faces and the spec give three different answers for the same key.⛔ Not measured:
os validateon a kanban node with a padded grouping. By constructionz.unknown()accepts it; no run was taken.What the fix looks like (a direction, not a ruling)
Type the row's
groupingby reference toGroupingConfigSchema, so the padded name and the wrong shape are refused where every other grouping is refused (ADR-0078: no silently inert or unjudged metadata). Ifgroupingon a kanban block has no real producer, retiring it in favour ofswimlaneFieldis the other arm (enforce-or-remove, ADR-0049); that choice belongs to whoever grades this card. Either way, objectui then mirrors the row (a follow-up card on objectui).Generated by Claude Code