Repository navigation
A grouping field name with surrounding whitespace passes validation, is trimmed for $select, and then buckets every row into one (empty) group #7347
Description
Activity
- addedbugSomething isn't workingSomething isn't workingdomain:uiobjectui ui stream: fix lands on the published library or apps — objectui execution seatobjectui ui stream: fix lands on the published library or apps — objectui execution seat
on Sep 5, 2026 分诊 —
domain:ui/priority:p2/pm:queue/bug锚定 (anchoring):最便宜且不被上游挡住的修法(路线 1,在读侧归一化)落在
packages/plugin-grid/src/useGroupedData.ts⇒domain:ui。⚠️ 路线 2(在@objectstack/spec的GroupingFieldSchema.field上加.trim())是上游动作,见下。在
origin/maina472b07上复核 —— 两个读者的分歧成立packages/core/src/utils/grouping-fields.ts:59 export function collectGroupingFieldRefs(grouping: unknown): string[] { packages/core/src/utils/grouping-fields.ts:73 const trimmed = name.trim(); ← 收割侧 trim,喂 $select packages/plugin-grid/src/useGroupedData.ts:97 function buildSegmentKey(row: Record<string, any>, field: string): string { packages/plugin-grid/src/useGroupedData.ts:320 const segment = buildSegmentKey(row, f.field); ← 分桶侧用 RAW f.field packages/plugin-grid/src/useGroupedData.ts:114 function buildSegmentLabel(:320传的是f.field原值,没有任何归一化 —— 与:73的name.trim()正面冲突。卡片的因果链(服务端按business_unit返回、表格去查' business_unit '、每行undefined、buildSegmentLabel一律答(empty))在代码上是闭合的。⭐ 一条卡片没提、但很有说服力的证据
packages/core/src/utils/grouping-fields.ts:29—— 收割器自己的 docblock 就描述了这个失效模式:grouping-fields.ts:29 * so `useGroupedData` reads `undefined` on every row and `buildSegmentLabel`也就是说"读到
undefined→ 全部落进一个桶"这条链路,在本仓已经被写下来过(是 #7179 那次修复的语境)。这张卡记录的是同一个失效模式的另一条入口,而这条入口穿过了作者时校验。这让卡片"strictly more reachable through the validated path, and quieter"的判断更硬。定级理由
priority:p2:- 静默错答,且是最难察觉的那种形态:一个
(empty)桶装下全部记录,读起来像是关于数据的一句真话("这些记录都没填这个字段"),而不是视图的 bug。卡片这句我复述并同意。 - 穿过了校验:
GroupingFieldSchema.field是裸z.ZodString,没有.trim()也没有 pattern,所以带空格的字段名是合法的已授权元数据。对比 A null entry ingrouping.fields[]throws a TypeError that takes down the whole grid, before any projection is built #7217 守的那个 null entry —— 那个是被GroupingConfigSchema拒掉的。所以这条路更可达。 - 卡片给了实测三行(spec 接受、收割器产
["business_unit"]、表格按 raw key 分桶),不是推断。
不上 p1:需要作者写出带空格的字段名才触发(不是默认路径),且无数据损坏、无权限面。
定型:Bug,不踩 manual floor —— 但两条路的型不同
路线 落点 型 是否需上游 1. 读侧归一化(表格 entry normalizer 里 trim,让两个读者按构造一致) packages/plugin-grid恢复 declared = enforced ⇒ Bug 否 2. producer 拒绝( @objectstack/spec的GroupingFieldSchema.field加.trim()或非填充 pattern)上游 spec 收窄已发布校验器的 accept set 是,且需 maintainer 3. 两者都做 二者之和 是 ⛔ 本席不裁定走哪条,但把一件事说清楚,因为它影响状态:路线 1 单独就能消除静默错答,且完全在本仓、不被任何东西挡住 —— 所以这张卡是
pm:queue,不是pm:blocked。卡片援引 AGENTS.md #0.1("fix the producer, do not teach the renderer to tolerate")主张路线 2 是契约优先方向,这个论证是正当的;但请注意路线 2 收窄了一个已发布校验器的 accept set,那意味着今天已存的、带空格的元数据会开始被拒,这是一次有迁移成本的动作,需要 maintainer 而不只是"更正确"。⛔ 给执行席的边界
- 先做卡片指定的那次横向检查,再选路线。 卡片写得对:"Worth checking whether the same trim asymmetry exists for the sibling view kinds'
groupByField(kanban / gantt / timeline) before choosing, since they union their grouping field into the projection through their own path."⚠️ 如果三个兄弟视图都有同样的不对称,那就是四处读侧修复 vs 一处 producer 修复,路线 2 的性价比会翻转 —— 这次测量是选路的输入,⛔ 不要跳过它先动手。 - 若走路线 1,⛔ 不要只在
:320那一处 trim。groupValueFormattermemo 也按 raw key 走(卡片点名),两处必须一致,否则会造出第三种口径。 - 加钉子必须用带空格的字段名。任何用干净字段名的测试今天也通过,抓不住它。
- ⛔ 不要改 A null entry in
grouping.fields[]throws a TypeError that takes down the whole grid, before any projection is built #7217 那个守卫的准入规则来顺带解决这个。卡片解释了为什么它没修:"a padded name satisfies it" —— 那个准入规则刻意与收割器对齐,动它会让"表格能按投影忽略掉的 entry 分组"重新可能。这是两个缺陷,不是一个。
⛔ 分诊席不认领、不派单、不写码、不裁定路线。
Generated by Claude Code
- 静默错答,且是最难察觉的那种形态:一个
Claim: session
session_01611D6ZaRaMmwTNQmSbk8MH· branchclaude/issue-7347-grouping-field-trimPM dispatch(
domain:ui席)。assignee 与本评论由 PM 代设,dev 继承二者、⛔ 不写 assignee 字段、⛔ 不再发第二条 claim。⭐ 第三档派发:裁方向 + 挂在一个可证伪的前提上 + 明令不许硬推。 三段分开看。
一、⛔ 裁决 —— 路线 1 归本车道,路线 2 不归
路线 落点 型 归属 1. 读侧归一化(表格 entry normalizer 里 trim,让两个读者按构造一致) packages/plugin-grid恢复 declared = enforced ⇒ Bug ✅ 本车道,能落地 2. producer 拒绝( @objectstack/spec的GroupingFieldSchema.field加.trim()/pattern)上游 spec 收窄一个已发布校验器的 accept set ⛔ 上游 + 需 maintainer 3. 两者都做 二者之和 ⛔ 含路线 2 ⇒ 你实现的是路线 1。 ⛔ 不许碰
@objectstack/spec,⛔ 不许在本 PR 里替路线 2 做决定。⚠️ 路线 2 不只是「更正确」:它会让今天已存的、带空格的元数据开始被拒,那是一次有迁移成本的动作。二、
⚠️ 前提 —— 先做横向测量再动手,⛔ 不许跳过卡片和分诊席都点名了同一件事:kanban / gantt / timeline 的
groupByField是否存在同一个 trim 不对称? 它们各自把分组字段并进投影,走自己的路径。⭐ 这不是背景知识,是选路的输入:若三个兄弟视图都有同样的不对称,那就是四处读侧修复 vs 一处 producer 修复,路线 2 的性价比会翻转。
⇒ ⛔ 若横向测量显示兄弟视图共享这个不对称:停下并回报,不要动手做四处读侧修复,也不要伸手上游。 那时这张卡要重新定路线,那是裁决,不是实现。
⇒ 若只有 grid 一处:按路线 1 做完。
三、⛔ 三条硬边界(分诊席定的,本席背书)
- ⛔ 不要只在
useGroupedData.ts:320那一处 trim。groupValueFormatter的 memo 也按 raw key 走(卡片点名)。两处必须一致,否则你造出第三种口径 —— 而本卡的缺陷正是「两个读者对同一个名字不一致」。 - ⭐ 钉子必须用带空格的字段名。 任何用干净字段名的测试今天也通过,抓不住它。⇒ 一根不用
' business_unit '这种形状的钉子,是一根不会红的钉子。 - ⛔ 不要改 objectui#7217 那个守卫的准入规则来顺带解决这个。 那条规则刻意与收割器对齐;动它会让「表格按投影忽略掉的 entry 分组」重新可能。这是两个缺陷,不是一个。
四、已经替你核过的坐标(⛔ 不是让你继承,是省掉一轮)
packages/core/src/utils/grouping-fields.ts:73 const trimmed = name.trim(); ← 收割侧 trim,喂 $select packages/plugin-grid/src/useGroupedData.ts:320 const segment = buildSegmentKey(row, f.field); ← 分桶侧用 RAW packages/core/src/utils/grouping-fields.ts:29 ← ⭐ 收割器自己的 docblock 就描述了这个失效模式⚠️ 按符号重新定位,⛔ 不要相信这些行号 —— 本班刚为位置漂移付过一次(PR #8867 被一行只指着行号的台账钉红,已立卡 objectui#8875)。五、验收 —— 类判据
- 判据是「一个带空格的字段名,收割侧与分桶侧读到同一个名字」,⇒ 行落进它真正的组,⛔ 不是「某处调用了
.trim()」。 - 消融:从已提交树出发,把归一化拿掉,先在盘上证明改动落了(marker 计数 和 blob hash),再跑,看它按用例名变红,然后按状态还原(
git diff HEAD为空 且 blob 回原值)。⛔ 只有退出码不算。 - 控制要会动:一个用干净字段名的现有分组测试必须在两个世界里都绿 —— 它证明你没有把正常路径弄坏;而带空格那根必须只在修好后才绿。⛔ 一个「主体坏了它也红」的控制不是控制。
六、
⚠️ Clause-② —— 由你判机械下限:新导出符号、或已发布载荷上的新键,恒
yes;拿不准 ⇒yes。⛔ 不要继承本简报任何一句作为结论。路线 1 是纯读侧行为修复,⚠️ 但那是我的预期、不是你的读数。⚠️ 载体问题(⛔ 不是你的欠账):固定拼写行必须落在这条 claim 评论里,本席没有编辑已有评论的工具(objectstack#17213)。⛔ 不要另发第二条 claim 评论 —— 那会变成misplaced,同样 exit 4 且看起来像已经答了。结论写进回报与 PR 正文。⚠️ 契约评审档位不可用(连续 16 次 429)。yes会停队,⛔ 不要因此改形状或自审。⚠️ scripts/pm/check-widening-tells.mjs在 objectui 里不存在,它住在 objectstack(/home/user/objectstack/scripts/pm/)。而且它的裸 CLI 对 objectui 判定是瞎的 —— 无--repo,main()不传,恒以 objectstack 判定 ⇒ 对 objectui diff 恒返回 clean(已立卡 objectstack#17217)。⛔ 那不是证据。七、⭐ 枚举,两条轴
⛔ 证明一个门看得见你的改动,不等于枚举了看得见你改动的门。①内容轴:谁读了你改动说的东西。②位置轴:谁按
file:line指着你改的文件,以及我的改动移动了什么行。八、⛔ 红线
⛔ 绝不编辑共享主检出(用专属 worktree,
/home/user/objectui的 HEAD 会在你脚下动)· ⛔ 绝不git stash· ⛔ 绝不按名杀进程,只杀自己记下的 PID · ⛔ 不碰content/docs/releases/· ⛔ 不碰 governed surface · ⛔ 不扩范围:测到的相关事实进验收备注并点名后继。⚠️ 工具链(本班实测两次假绿):pnpm --filter PKG exec vitest run FILE被 objectui#3378 的守卫拒绝,会静默跑 console 的 22 个文件报绿;pnpm --filter PKG test在没有test脚本的包上匹配零个脚本 exit 0。⛔ 都不是读数。从仓根:pnpm exec vitest run packages/PKG/src/FILE。⚠️ 共享身份os-zhuang的 GitHub API 间歇性限流 —— 能用git对origin/*回答的走 git;⛔ 永不在循环里重试限流。
Generated by Claude Code
- ⛔ 不要只在
os-dev-report
{
"issue": 7347,
"status": "blocked",
"branch": "claude/issue-7347-grouping-field-trim",
"pr": null,
"premise_still_valid": true,
"summary": "Stopped at the dispatch's own lateral precondition, before writing any fix. Zero files changed; branch pushed empty at BASE 7fb22a1, working tree clean (git diff HEAD empty). The card's premise is CONFIRMED, measured: spec accepts a padded field name, collectGroupingFieldRefs trims it, usableGroupingFields keeps it raw. The lateral measurement answers the card's NAMED question NO — a padded groupByField on kanban/gantt/timeline reaches $select neither trimmed nor raw, because ListView's collectViewFields feeds addSpeculative the RAW name and the knownObjectFields gate drops it; both sides use the raw name, so those three siblings are symmetric and carry NO trim asymmetry. But the answer is also not 'only the grid': THREE renderers of the same grouping.fields[] block bucket by the raw field while that same block's projection is harvested TRIMMED — plugin-grid (the card), plugin-list's ObjectGallery (measured: one '(empty)' group holding every row), and plugin-kanban's swimlaneField fallback (measured: one 'Uncategorized' lane). Route 1 as scoped repairs 1 of 3 and leaves the identical silent wrong answer live in two more packages, so per the dispatch's stop rule the route ruling has to be re-taken by the PM.",
"tests": "No implementation, so no acceptance suite and no ablation — nothing was changed to ablate. What was run is the PRECONDITION MEASUREMENT, from the repo root per the toolchain note, in worktree /home/user/objectui-issue-7347 at 7fb22a1: (1) pnpm exec vitest run packages/plugin-list/src/tests/zz-measure-7347.test.tsx yields EXIT=0, 'Test Files 1 passed (1) / Tests 9 passed (9)'; (2) pnpm exec vitest run packages/plugin-kanban/src/tests/zz-measure-7347-kanban.test.tsx yields EXIT=0, 'Test Files 1 passed (1) / Tests 2 passed (2)'. Exit codes captured by redirect-then-read, never through a pipe. Readings (console is silenced by the root config, so the instruments appended to a file): M1 spec accepts padded grouping field: true | M2 collectGroupingFieldRefs yields: ["business_unit"] | M3 usableGroupingFields[0].field = " business_unit " | M4 $select for padded grouping.fields[]: ["id","subject","business_unit"] (TRIMMED name on the wire) | M5 $select for padded gantt.groupByField: ["id","subject"] — contains trimmed? false, contains padded? false | M6 timeline: ["id","subject","status"] — trimmed? false, padded? false | M7 kanban.groupByField: ["id","subject"] — trimmed? false, padded? false | M8 GALLERY padded yields '(empty)' headers: 1, real-value headers: 0 | M9 GALLERY CONTROL clean name yields '(empty)': 0, real-value headers: 2 | M10 KANBAN padded yields Uncategorized lanes: 1, Retail: 0, Wholesale: 0 | M11 KANBAN CONTROL clean name yields Uncategorized: 0, Retail: 1, Wholesale: 1. M9 and M11 are the live controls: the clean-name path is green in the same run, so M8/M10 are readings about the padded name and not about a broken harness. Both instruments were deleted after the reading; removal verified BY STATE — git status empty and git diff HEAD empty, not by an exit code.",
"mcp_calls": "1 — one issue_read(get) on objectui#7347. Card body, triage comment 5548478232 and claim comment 5606940450 were read over REST with curl (HTTP 200); the report comment is posted over REST too.",
"open_questions": [
{
"question": "The dispatch's precondition has fired: the trim asymmetry is NOT confined to the grid, and NOT on the groupByField axis the card named. Three readers of one grouping.fields[] block bucket by the raw field name while the projection for all three is harvested trimmed by collectGroupingFieldRefs: packages/plugin-grid/src/useGroupedData.ts usableGroupingFields (feeding both grid readers), packages/plugin-list/src/ObjectGallery.tsx groupedItems memo, packages/plugin-kanban/src/ObjectKanban.tsx effectiveSwimlaneField. Which route now? NOTE ON FORM: this dispatch carried no four-axis decision frame, and per the standing clause I do not author one — options and costs below are factual, the recommendation is flagged as mine.",
"options": [
"A. Route 1 as ruled, grid only — trim inside usableGroupingFields. Cost: genuinely one edit; ObjectGrid's groupValueFormatter memo and useGroupedData ALREADY share that normalizer (ObjectGrid.tsx calls usableGroupingFields(schema.grouping?.fields)), so hard boundary 1 is satisfied by construction, no third dialect. Repairs 1 of 3 sites. Leaves the measured gallery and kanban failures live and now knowingly so. Clause-2: no new exported symbol, no new key on a published payload — my read is 'no', but see the caveat below.",
"B. Route 1-prime, not enumerated in the dispatch — one canonical entry normalizer in @object-ui/core (next to collectGroupingFieldRefs, which already owns the trimming rule), consumed by all three readers. One behaviour rule, three call sites, three packages. Covers all measured sites. Cost: it ADDS A NEW EXPORTED SYMBOL to a published package, so Clause-2 is 'yes' by the mechanical floor, and it spans three packages — well past this card's declared scope.",
"C. Route 2 at the producer — .trim() or a non-padded pattern on @objectstack/spec GroupingFieldSchema.field. One fix covers all three readers and any future one. Cost is unchanged and still real: it narrows a published validator's accept set, so already-stored padded metadata starts being REJECTED — a migration cost, and it is upstream plus maintainer-gated. What the measurement changes is only the other side of the ledger: the count it is weighed against is 3 reader sites, not 1.",
"D. Split: land A now as the card's declared scope, and file the gallery and kanban sites as their own cards under a shared route ruling. Cost: three separate silent-wrong-answer defects of one class, fixed at three times and possibly by three different routes — which is the shape that produced this card's own dialect split."
],
"recommendation": "MINE, NOT INHERITED, and offered only as input to your ruling: C or B, not A alone. The reason is the count the precondition was written to surface — the trade is now 3 reader-side sites against 1 producer fix, and the dispatch itself said that inverts route 2's cost-benefit. Between them: C is contract-first per AGENTS.md #0.1 and is the only one that also covers a fourth reader written next year, but it is upstream and carries the migration cost you already named; B stays in this repo and covers everything measured today, at the price of a new exported symbol on a published package (Clause-2 yes) and a three-package diff. If the migration cost of C is the blocker, B is the cheapest thing that leaves no measured site broken. A alone is defensible ONLY as a deliberate partial with the other two sites filed — it should not be taken as 'the card is closed'."
},
{
"question": "Clause-2 for this round.",
"options": [
"There is no diff: zero files changed, no new exported symbol, no new key on any published payload."
],
"recommendation": "Clause-2: n/a this round — no diff was produced, so there is nothing to review. Flagging forward, since the ruling decides it: route B is 'yes' by the mechanical floor (new exported symbol on a published package) and route C is 'yes' outright (it moves a published validator's accept set). Route A is the only one I read as 'no', and that read is mine, not inherited from the brief. Note the carrier problem you named is unchanged — the fixed-spelling line still has no home on the claim comment, and I have posted no second claim comment."
}
],
"out_of_scope_findings": [
"noted, not filed: packages/plugin-list/src/ObjectGallery.tsx groupedItems memo buckets by String(item[f.field]) with the RAW authored name and labels the bucket '(empty)' — byte-for-byte this card's failure, in a second package, measured (M8: one '(empty)' group holding every row; M9 control green).",
"noted, not filed: packages/plugin-kanban/src/ObjectKanban.tsx effectiveSwimlaneField falls back to schema.grouping?.fields?.[0]?.field RAW, and KanbanImpl then reads card[swimlaneField] — same failure wearing the label 'Uncategorized', measured (M10: one lane; M11 control green). The package documents this read as contract in its own index.tsx prop description, so it is intended behaviour reading an un-normalized name.",
"NOT FILED ON PURPOSE, and this is the one item I am asking you to take rather than absorb: both findings above are class-(a) reproducible defects with named repro, so they are ordinarily fileable by me. I am handing them to you instead of filing because filing them now would PRE-EMPT the ruling this report requests — if route C is taken they are one producer fix and three cards are noise, and if route B is taken they are one PR and three cards are noise. Successor named: whichever PR carries the re-routed objectui#7347. If you rule route A, they become two real cards and I will file them on the next dispatch.",
"noted, not filed: the sibling groupByField path is quiet for a different reason worth knowing — a padded name is dropped by addSpeculative's knownObjectFields gate, so it never reaches $select at all (M5/M6/M7). Those views still show every record ungrouped for a padded name; that is a producer-side acceptance problem (route C territory), NOT a two-readers-disagree defect, and it is NOT the asymmetry the card asked about. Recording the boundary so the next reader does not widen the class. Successor: none needed unless route C is declined.",
"noted, not filed: the harvester's own docblock (packages/core/src/utils/grouping-fields.ts, the 'THE FAILURE THIS CLOSES' section) already describes this exact failure mode, as the triage seat observed. Re-derived by symbol, not by the line numbers in the brief — the trim now sits at line 73 and the docblock passage at line 29 in this checkout, matching the brief, but they were located by grep on collectGroupingFieldRefs and usableGroupingFields.",
"process note, no card: scripts/pm/check-widening-tells.mjs was NOT run. It does not exist in objectui, and the brief records its bare CLI as blind to this repo. With a zero-byte diff there is nothing for it to judge, so no control could have been made to fire — I am recording it as not-measured rather than as clean."
]
}
Generated by Claude Code
前置测量已跑,停手条件命中 —— 转
needs-user-decision(domain:ui席,⛔ 本席不裁)派发时的硬停手:若横向测量显示不对称不止 grid 一处 ⇒ 停手回报,那是裁决不是实现。dev 照做了:零文件改动,分支在 base
7fb22a12c空推,工作树干净(git diff HEAD为空)。⛔ 没有写任何修复。pm:dispatched→needs-user-decision,assignee 清空。
⚠️ 一、卡片点名的那个横向问题,答案是 NO —— 但结论不是「只有 grid」卡片问的:kanban / gantt / timeline 的
groupByField是否有同样的 trim 不对称?
实测答案:没有。 那三个是对称的 ——ListView的collectViewFields把 RAW 名字喂给addSpeculative,而knownObjectFields闸门把它丢掉,两侧都用 raw。M5 gantt.groupByField 带空格 → $select ["id","subject"] 含 trimmed? false 含 padded? false M6 timeline 同形 → 含 trimmed? false 含 padded? false M7 kanban.groupByField 同形 → 含 trimmed? false 含 padded? false⇒ 一个带空格的名字在那三条路上根本到不了
$select。它们仍然会「所有记录不分组地显示」,⚠️ 但那是生产者侧的接受问题(路线 2 的地盘),⛔ 不是「两个读者不一致」那个缺陷。dev 明确记下这条边界,免得下一个读者把类扩宽。⭐⭐ 二、真正的发现:同一个
grouping.fields[]块有三个读者,全都按 raw 分桶而这个块的投影,三者共用、都由
collectGroupingFieldRefs按 trimmed 收割:包 读点 实测症状 plugin-griduseGroupedData的usableGroupingFields卡片本身 plugin-listObjectGallery的groupedItemsmemoM8:一个 (empty)组装下全部行plugin-kanbanObjectKanban的effectiveSwimlaneField兜底M10:一条 Uncategorized泳道两条都带同一次运行里的活控制:
M9干净字段名 →(empty)0 个、真值表头 2 个;M11干净名 →Uncategorized0、Retail1、Wholesale1。⇒ M8/M10 是关于带空格名字的读数,⛔ 不是探针坏了的读数。⇒ 路线 1 按原定范围只修 3 处中的 1 处,把同一个静默错答明知故留在另外两个包里。
⭐ 附带确认(按符号重新定位,⛔ 不按简报里的行号):收割器自己 docblock 的 "THE FAILURE THIS CLOSES" 一段已经描述了这个失效模式,与分诊席的观察一致。
三、⛔ 请裁:四选一(⛔ 本席不推荐;dev 的推荐标明是 dev 的)
路线 覆盖 代价 Clause-② A 路线 1 原范围,只修 grid(在 usableGroupingFields里 trim)1 / 3 确实只有一处编辑,且 ObjectGrid的groupValueFormatter已经共用那个 normalizer,⇒ 硬边界 1「不许造第三种口径」按构造满足。⚠️ 但 gallery 与 kanban 的已测失效明知故留dev 读作 noB 在 @object-ui/core立一个规范 entry normalizer(挨着已经拥有 trim 规则的collectGroupingFieldRefs),三个读者共用3 / 3(今天已测的全部) 新增一个已发布包的导出符号;跨三个包,远超本卡声明范围 yes(机械下限)C 路线 2,生产者侧(spec GroupingFieldSchema.field加.trim()/pattern)3 / 3 + 未来任何新读者 代价不变且真实:已存的带空格元数据开始被拒(迁移成本),上游 + 需 maintainer yesD 拆:A 先落地,gallery 与 kanban 各立卡 1 now + 2 later ⚠️ 一类三个静默错答,分三次修、可能走三条不同路线 —— dev 指出那正是造出本卡这次口径分裂的形状— ⭐ 测量改变的只有账本的一边:路线 2 现在要对比的是 3 个读者侧站点,而不是 1 个 —— 而本席派发单自己写过「若兄弟视图共享,性价比会翻转」。
dev 的推荐(⛔ 是 dev 的,不是本席的):C 或 B,⛔ 不要单独的 A。理由原话要点:C 契约优先(AGENTS.md #0.1)且是唯一能覆盖「明年才写出来的第四个读者」的,但上游 + 迁移成本;B 留在本仓、覆盖今天所有已测站点,代价是已发布包新增导出符号(Clause-②
yes)与三包 diff。A 单独只有在「明确作为部分修复、另两处立卡」时才成立,⛔ 不能当作「本卡已关闭」。⭐⭐ 四、dev 刻意没有立那两张卡 —— 这个判断本席背书
gallery 与 kanban 两处都是有复现路径的 class-(a) 缺陷,按常规 dev 可以自行立卡。它没有,理由原话:立了会预判本报告所请求的裁决 —— 若裁 C,它们是一次生产者修复,三张卡是噪音;若裁 B,它们是一个 PR,三张卡同样是噪音。
⇒ 它把两处移交而不是吸收。⭐ 这是对的第三条路:既不盲目立卡,也不静默丢弃。⛔ 本席也不在裁决前拆卡 —— 两处的完整读数记在本条评论里,裁决落下后由承接的 PR 或卡带走。若裁 A,它们即刻成为两张真卡。
五、
⚠️ 本席派发单的一处,值得记下来我写的停手条件是「若兄弟视图共享这个不对称」—— 我是照卡片点名的那条轴(
groupByField)写的。实测那条轴是对称的,而不对称出现在另一条轴(grouping.fields[]的三个读者)。⇒ 停手条件的字面没被满足,精神被满足了,而 dev 按精神停了手。⭐ 这正是第三档派发要的效果:前提可证伪,而它以我没预料的方式被证伪。⛔ 一个只按字面执行的席位会在这里直接做完 A。
六、本轮的 Clause-② 与载体
n/a —— 零字节 diff,无可评审。
⚠️ 前瞻:B 与 C 都是yes(B 新增已发布导出符号,C 移动已发布校验器的 accept set),A 是 dev 读作no的唯一一条。⚠️ 本卡 claim 评论的固定拼写行仍无归处,dev 没有另发第二条 claim 评论(正确 —— 那会变成misplaced)。⭐ 载体缺口本班已解(objectstack#17213:由做出判断的 dev 在 PM 授权下经 REST 追加),但本轮没有判断可写,所以不适用。⚠️ 测量用的两个临时探针已删除,按状态验证(git status空 +git diff HEAD空),⛔ 不是按退出码。
Generated by Claude Code
决策分析 —— 总监席第 21 场 · 批 #110 · 5/5(
session_01QVMnxyWBx8cAQMsV6akDV9,2026-09-10T08:2xZ)Governing text:AGENTS.md #0.1(修生产者,不教渲染器容忍);
@objectstack/specGroupingFieldSchema.field= 裸z.string()(无.trim()无 pattern);packages/core/src/utils/grouping-fields.ts:73收割侧已 trim。协议声明:字段名无「不得带空格」约束。C 改协议(给已发布校验器加约束 ⇒ 若裁 C,本席在 objectstack 立协议卡,spec 席走Clause-②: yes);B 不改协议。
前提(dev 5607077319 实测):三个读者按 raw 名分桶(grid / gallerygroupedItems/ kanbaneffectiveSwimlaneField),投影三者共用、按 trimmed 收割;兄弟视图的groupByField轴对称(带空格的名到不了$select),不是本缺陷。一句话问题:字段名两边带空格的分组配置能通过校验;取数时按去掉空格的名字要数据,分桶时按带空格的名字找——每一行都找不到,全部落进一个「(empty)」组,看起来像数据本来就是空的。三处视图同病。
选项 × 真实代价
做什么 客户可感知的后果 A 只修 grid 3 处修 1 处,gallery / kanban 明知故留 B @object-ui/core立一个规范 entry normalizer,三读者共用新导出符号( Clause-②: yes),三包 diff;作者永远不知道自己写了空格C spec 侧 GroupingFieldSchema.field.trim()/ 非填充 pattern一处修三读者 + 未来读者;存量带空格的元数据开始被拒(响亮);跨仓、等发版可安装 D 拆三卡 一类三处三次修、可能三条路线 业务含义直译:A=「只修一个窗口」;B=「三个窗口共用一把尺」;C=「进门就不让带空格」。
四轴:① C 契约先行、一处、覆盖明年才写的第四个读者;B 是教三个渲染器容忍同一种脏输入。② 静默错答、穿过校验;无用户报告但三处实测。③ C 响亮(拒绝);B 静默改正。④ C 收窄一个已发布校验器——按「不渐进」立即收窄、无窗口;迁移成本 = 存量带空格的字段名(本席判断极少:字段名带空格是手误不是意图,且今天它本来就渲染错)。
推荐:C(
.trim()不够——它会让' a '与'a'静默等价;用非填充 pattern 拒绝),并在同一轮把collectGroupingFieldRefs的 trim 保留为防御;回退 B。
置信缺口:cloud / 客户存量元数据中带空格字段名的数量未测;useGroupedData的 formatter memo 是否已共用 normalizer(dev 称已共用,本席未核)。裁后执行:裁 C ⇒ 本席在 objectstack 立协议卡(spec 席,
packages/spec/src收窄 + 拒绝 pin + tombstone 不适用),本卡pm:blocked+Blocked-by: objectstack#<n>至发版可安装;gallery / kanban 两处不立卡(一次生产者修复覆盖)。裁 B ⇒ ui 席pm:queue,Clause-②: yes。os-decision-facets
- ① 项目长远合理性:C 契约先行、一处覆盖所有读者。
- ② 实际业务拉动:静默错答穿过校验,三处实测。
- ③ 防 AI 犯错:C 响亮拒绝;B 静默改正。
- ④ 创业阶段不扩散:立即收窄、无窗口;迁移成本判极少。
推荐 C;置信缺口:存量带空格字段名数量未测。
Generated by Claude Code
11 remaining items
objectstack-fleet commented
on Sep 30, 2026 ContributorMore actionsUnlock scan:
pm:blocked→pm:queue. The install-face condition is met, because objectuimainnow resolves@objectstack/*17.5.0 (PR objectui#11086, merged as81f849852a, closing objectui#11073)Triage seat (objectstack-wide, seat post objectstack-ai/objectstack#6015) ·
session_01AavokzJ5DndAwitDXvKy4U· 2026-09-30T04:14Z. ⛔ Not a claim, ⛔ not a dispatch. The grade, route and ruling are unchanged.- The card's condition: the INSTALLED
GroupingFieldSchemarefuses{ field: ' business_unit ' }while{ field: 'business_unit' }still parses. - The probe, run against the published
@objectstack/spec@17.5.0from npm (the version objectui'spnpm-lock.yamlnow resolves; its tag commit is objectstack0f6dcac5e9): the padded field is refused (acustomissue atfield), and the control parses. - Next. The card goes to
pm:queue. The dispatching seat re-reads the body against objectuimainat claim. The probe above licenses the work; it does not replace that read.
- The card's condition: the INSTALLED
objectstack-fleet commented
on Sep 30, 2026 ContributorMore actionsClaim: PM loop round 1
Session:session_0122Knsowci76D2rBWReCzzZ
Account:os-warren(the seat's linked user as MCPget_meanswers it; the card's assignee)
Branch:claude/issue-7347-padded-grouping-refused-pin
Worktree:objectui-issue-7347
Domain:domain:ui
Seat:domain:ui#1
File surface: one new regression pin underpackages/types/src/__tests__/(or the package whose validation face judgesgrouping, if the dev measures it elsewhere), the stale docblock sentence inpackages/core/src/utils/grouping-fields.ts(comment only;collectGroupingFieldRefs's trim stays as ruled), one.changeset/7347-*.md. ⛔ No reader-side normalization in grid / gallery / kanban (ruled out by ruling C). The dev stops on a breach and explains it in the report.
Container & model:S,mode:subagent,model: opus(default judgment tier;dispatch-gates.mjs --tier --repo objectstack-ai/objectuianswers no path-derived mandate)
Clause-②: no
Thread-read: 5903907265
Serial constraints cleared: open PR objectui#10278 (objectui#9853) editspackages/types/src/zod/objectql.zod.tsandplugin-grid, notgrouping-fields.tsand not a grouping pin; PR objectui#11125 editsobjectql.zod.tsobject-formrows only. No in-flight claim onpackages/core/src/utils/grouping-fields.ts(git log origin/main -- that path: last touch5b2ea17).Scope, from ruling C (
5615810454, batch #110 item 5) and the hold comment5818806428: the objectui action after the unlock is the regression pin "a padded name is refused before it reaches any renderer", on the objectui validation face(s) that re-export the spec'sGroupingConfigSchema(objectql.zod.tsimports it asSpecGroupingConfigSchema), now thatpnpm-lock.yamlresolves@objectstack/spec@17.5.0.Clause-②: no— why: no objectui accept set or public surface changes in this diff; the refusal arrives through the dependency bump objectui#11086 already landed.domain:uiseat 1 · claim · 2026-09-30T09:04Zobjectstack-fleet commented
on Sep 30, 2026 ContributorMore actionsos-dev-report
{
"issue": 7347,
"status": "done",
"branch": "claude/issue-7347-padded-grouping-refused-pin",
"pr": "#11191",
"session": "session_0122Knsowci76D2rBWReCzzZ",
"premise_still_valid": true,
"summary": "Ruling C's objectui half is landed as a draft PR: a regression pin (packages/types/src/tests/grouping-field-padded-refused-7347.test.ts) holding that on every face that declaresgrouping(list-view, object-gallery, object-view listViews.*), through all four doors (tolerant AnyComponentSchema, safeValidateSchema = whatobjectui validateruns, strict StrictAnyComponentSchema, the exported mirror), the unpadded control parses and each padded spelling fails with exactly onecustomissue at that entry'sfield; a census derives the declaring arms from the union so a new declarer is covered or goes red. All four PM mechanism assumptions measured: (1) installed @objectstack/spec 17.5.0 refuses; (2) holds for every declaring face — stripImportedDefaults rebuilds the enclosing GroupingConfig/GroupingField objects but carries thefieldstring node by reference with its one check, so no schema fix was needed and no objectui schema source changed; (3) the docblock sentence was false and is corrected, the harvester's trim untouched; (4) groupByField not touched. Boundary measured and reported, not fixed:object-grid/object-kanbannodes do not declaregroupingon their mirrors, so the tolerant face (andobjectui validate) passes a padded grouping there unjudged — a Clause-② change, outside this card.",
"tests": "All at HEAD 4d99273 unless noted; exit codes captured by redirect before any pipe; heavy runs via os-verify-lock (VERDICT command-exit 0 each). (1) pnpm exec vitest run packages/types/ → EXIT 0, 'Test Files 286 passed (286)' / 'Tests 6673 passed (6673)'; pin is in the population (vitest list --filesOnly names it). (2) pin alone (identical content, run pre-commit) → 'Test Files 1 passed (1)' / 'Tests 62 passed (62)' = 1 producer + 3 arms x 4 doors x 5 cases + 1 census. (3) pnpm exec vitest run packages/core/src/utils/tests/grouping-fields.test.ts → EXIT 0, 'Tests 14 passed (14)'. (4) pnpm --filter @object-ui/types type-check → EXIT 0 (echoes 'tsc --noEmit && tsc -p tsconfig.examples.json && tsc -p tsconfig.test.json'; tsc -p tsconfig.test.json --listFilesOnly lists the pin 1x); pnpm --filter @object-ui/core type-check → EXIT 0. (5) pnpm --filter '@object-ui/types^...' build → EXIT 1 ERR_PNPM_RECURSIVE_RUN_NO_SCRIPT (empty closure, nothing to build — not a red gate); pnpm --filter '@object-ui/core^...' build (= @object-ui/types build) → EXIT 0, 'dist completeness: 1 package(s) complete (136 emitted files verified)'; pnpm --filter @object-ui/types build → EXIT 0 (pre-edit, and again inside the core closure at 4d99273). (6) node scripts/check-changeset-presence.mjs → EXIT 0 '2 source file(s) of 2 released package(s) changed, and this change declares 1 changeset(s)' (EMPTY frontmatter); pnpm check:control-bytes → EXIT 0 'OK (scanned 9549 tracked text file(s); skipped 85 binary)'; check:changeset-claims, check:pending-changeset-literals, check:test-path-roots, check:shell-escape-residue, check:vi-mock-specifiers, type-check:coverage → EXIT 0 each; check:new-line-citations → 'VERDICT new-cross-file-line-citations: 0 new citation(s)'; self byte scan grep -naP over the 3 files → exit 1 (none). (7) ESLint narrowed to the 2 touched source files, proven to exclude nothing: eslint --print-config returns a config for both (118 / 116 rules, i.e. inside the population); --format json → 2 files, 0 errors, 0 warnings; parserOptions.project and projectService both null ⇒ not type-aware ⇒ this diff cannot move an untouched file's verdict. Repo-wide pnpm lint is CI's. (8) Premise probes (scratch, deleted with the worktree): spec GroupingFieldSchema padded → [{code:custom,path:[field]}], clean → success; 4 doors x 3 declaring arms → padded custom at grouping.fields.0.field / listViews.all.grouping.fields.0.field, clean OK; census walk over 145 flattened arms and 249 barrel zod exports → exactly list-view, object-gallery, object-view. objectui validate end to end (validate.ts from source via tsx, process.exit intercepted): clean list-view → exit(0) 'Schema is valid!'; padded list-view → exit(1) 'Code: custom', 'Path: grouping → fields → 0 → field'; padded object-view → exit(1) at listViews → all → grouping → fields → 0 → field; padded object-grid → exit(0) 'Schema is valid!' (the boundary). (9) Ablation, fix committed first, each leg via objectstack scripts/ablation-replace.mjs (anchor must hit; blob before/after) inside a script with an EXIT INT TERM restore trap; subject imported by relative path from source, spec from node_modules untouched, so no dist preflight applies. Leg A (imported-defaults.ts leaf arm rebuilds every imported string leaf as a fresh z.string(), dropping checks): anchor 1→0, marker 0→1, blob 0fa9694db112→ac546120b72e; pre-registered 'all padded red, all controls green'; observed 'Tests 48 failed | 14 passed (62)' (48 = 4 padded case names x 12 arm-door pairs; 14 = producer + 12 controls + census); restored blob == HEAD 0fa9694db112, git diff HEAD empty. Leg B (object-grid mirror gains grouping): first attempt refused by the tool as a no-op (replacement contained the anchor, count 1→1, nothing ran), re-anchored; observed 'Tests 1 failed | 61 passed (62)' — the census, found set gained object-grid; restored blob == HEAD 091582208cc9, git diff HEAD empty.",
"gates": {
"vitest packages/types/": "EXIT 0 — 286 files / 6673 tests passed @4d99273",
"vitest grouping-field-padded-refused-7347 (pin)": "62 passed",
"vitest packages/core/src/utils/tests/grouping-fields.test.ts": "EXIT 0 — 14 passed",
"pnpm --filter '@object-ui/types^...' build": "EXIT 1 ERR_PNPM_RECURSIVE_RUN_NO_SCRIPT — empty closure, nothing built (not a red gate)",
"pnpm --filter '@object-ui/core^...' build": "EXIT 0 @4d99273",
"pnpm --filter @object-ui/types build": "EXIT 0",
"pnpm --filter @object-ui/types type-check": "EXIT 0",
"pnpm --filter @object-ui/core type-check": "EXIT 0",
"node scripts/check-changeset-presence.mjs": "EXIT 0",
"pnpm check:control-bytes": "EXIT 0",
"pnpm check:changeset-claims": "EXIT 0",
"pnpm check:pending-changeset-literals": "EXIT 0",
"pnpm check:new-line-citations": "EXIT 0 — 0 new citation(s)",
"pnpm check:test-path-roots": "EXIT 0",
"pnpm type-check:coverage": "EXIT 0",
"pnpm check:shell-escape-residue": "EXIT 0",
"pnpm check:vi-mock-specifiers": "EXIT 0",
"eslint (2 touched files, narrowed + proven)": "EXIT 0 — 0 errors, 0 warnings",
"@object-ui/cli suite": "NOT RUN — no validate case added (e2e validate reading was a one-off probe)",
"CI": "in_progress — not awaited (PM owns convergence)",
"governed surface": "NOT GOVERNED (check-governed-queue-guard --test, 3 paths)"
},
"files_changed": [
"packages/types/src/tests/grouping-field-padded-refused-7347.test.ts (new, +244)",
"packages/core/src/utils/grouping-fields.ts (comment only, +4 -1)",
".changeset/7347-padded-grouping-refused-pin.md (new, empty frontmatter, +14)"
],
"line_budget": "n/a — no skills/** in the diff",
"deviations": [
"pnpm --filter '@object-ui/types^...' buildfrom the order's gate list answers ERR_PNPM_RECURSIVE_RUN_NO_SCRIPT (exit 1): the closure has no package with a build script. Recorded as an empty closure; the touched core package's closure build ran instead (exit 0).",
"objectui validateend to end was measured by running packages/cli/src/commands/validate.ts from source with tsx (process.exit intercepted), not the built CLI (dist not built; tsup dts needs the components/react closure). One-off; no validate test added, so the @object-ui/cli suite was not owed.",
"Docblock: the corrected sentence also names the pin and says the harvester's trim stays as defence (the order said 'correct that sentence only'); no other sentence in the paragraph changed.",
"Ablation leg B first attempt was a tool-refused no-op (replacement contained the anchor text; anchor count stayed 1; the test command never ran). Re-anchored and rerun; reported rather than silently retried.",
"Attribution: the commit carries AGENTS.md's model-free trailer pair (Claude-Session + 'Co-authored-by: Claude'), not the harness reminder's model-named Co-Authored-By line; the PR body ends with the definition's session-URL footer, not the harness's 'Generated with' line. Chosen by the stated precedence of the repo/agent rules over the harness reminder.",
"The pin-alone 62-passed run preceded the commit on byte-identical content; at 4d99273 the pin ran inside the full types suite."
],
"mcp_calls": "0 — no MCP GitHub tool used (reads were REST GETs via curl: issue 7347, its comments, issue 6152, PR 11191, the pulls list)",
"api_writes": "3 — all through the fleet relay (each one POST /repos/objectstack-ai/objectstack/dispatches, landing as objectstack-fleet[bot]): (1) pr_create → POST /repos/objectstack-ai/objectui/pulls (draft #11191; request fw-20260930T092910Z-385a7e, run 36696417719 success); (2) label-write --assign os-warren → POST /repos//issues/11191/assignees (request fw-20260930T093006Z-f09534, run 36696516334 success; read-back MATCHES, labels untouched); (3) this os-dev-report comment → POST /repos//issues/7347/comments via post-stamped. Plus 2 git pushes (not REST). Card assignee and labels untouched; no second claim.",
"open_questions": [],
"out_of_scope_findings": [
"class: b · family: objectui#6152 (UnmirroredDeclared worklist) — MERGE as new evidence on that card, not a new card · reach: public door —objectui validate(safeValidateSchema) answers exit 0 'Schema is valid!' for {type:'object-grid', objectName:'account', grouping:{fields:[{field:' business_unit '}]}}, a value @objectstack/spec's GroupingConfigSchema refuses; it also accepts grouping: 42; the strict face refusesgroupingon object-grid by name (unrecognized_keys) whatever the value · contract quoted: the TS ObjectGridSchema membergrouping?: GroupingConfigis documented 'Aligned with @objectstack/spec GroupingConfigSchema', while the zod mirror does not declare it (the parity ledger's UnmirroredDeclared entry for ObjectGridSchema lists grouping) · Seam: spec:GroupingConfigSchema (GroupingFieldSchema.field) → runtime:safeValidateSchema object-grid arm | renderer:ObjectGrid (reads schema.grouping) · not fixed here: declaring it moves a published face's accept set (Clause-② no on this card) · dedupe words: object-grid grouping unmirrored; ObjectGridSchema grouping mirror; padded grouping object-grid validate",
"class: c · reach: public door —objectui validateanswers valid for anobject-kanbannode carrying grouping:{fields:[{field:' business_unit '}]} (and for grouping: 42), because neither the TS ObjectKanbanSchema nor its zod mirror declaresgrouping, while ObjectKanban's effectiveSwimlaneField reads schema.grouping?.fields?.[0]?.field as the swimlane fallback (the earlier dev report on this card measured one 'Uncategorized' lane for a padded name on that read); the strict face refuses the key by name · producer: the list-view path (ListView forwards its already-validated grouping onto the kanban node) is covered by the list-view refusal; the unguarded door is a directly authored object-kanban node · family candidate for the seat to confirm: the undeclared-index-signature reads that ObjectKanban.tsx's docblock names (it points at objectui#9726) · dedupe words: object-kanban grouping undeclared; swimlane fallback grouping validate; ObjectKanbanSchema grouping",
"carrier: 承接者:无 · noted in PR Acceptance notes only, not filed: packages/plugin-list/src/ListView.tsx's grouping-projection comment still calls GroupingFieldSchema.field 'a bare string'; its load-bearing point (no field-existence check) still holds."
]
}
Generated by Claude Code
objectstack-fleet commented
on Sep 30, 2026 ContributorMore actions✅ ACCEPT: PR objectui#11191 at
4d99273e0; ready and auto-merge follow this ACCEPTdomain:uiseat 1 ·session_0122Knsowci76D2rBWReCzzZ· 2026-09-30T09:49Z. Checked against the diff and the head's check-runs, not against the report's own account. Claim5907894556; dev report5908411423.- Contract review at
CONTRACT_REVIEW_TIER: PASS on this head, which is the landing head (5908577520).Implemented-by: claude/issue-7347-padded-grouping-refused-pin,Reviewed-by: session_0122Knsowci76D2rBWReCzzZ(the seat adopting its isolated at-tier subagent).
item reading shape draft → main; first body lineFixes #7347,Clause-②: noat line start; the body's only closing keyword is that one (scan of every#Nnear fix/close/resolve)scope 3 files, +262 / −1: the new pin packages/types/src/__tests__/grouping-field-padded-refused-7347.test.ts, one docblock sentence inpackages/core/src/utils/grouping-fields.ts(no code token moved; the harvester's trim stays, as ruling C5615810454says),.changeset/7347-padded-grouping-refused-pin.md(empty frontmatter: nothing ships)ruling C honoured: no reader-side normalization in grid / gallery / kanban; the refusal comes from the spec's GroupingFieldSchemathroughSpecGroupingConfigSchemaassertions each padded spelling asserts exactly [{ code: 'custom', path }]on four doors per declaring arm, with an unpadded control per door; a census derived from the union fails when a new arm declaresgrouping(the dev's ablation leg B showed it firing)CI head 4d99273e0: 43 check runs, 40 success, 3 expected skips (dependabot, the two coverage jobs), 0 failure;Lint,Type Check,Test (shard 1/8 … 8/8),Build & E2E,Build Docs,Changeset Declarationallsuccessgoverned / size check-governed-merges.mjs --pr objectstack-ai/objectui#11191: 0 of 3 paths governed; 263 changed linesDeviations, accepted as reported: the
@object-ui/types^...build is an empty closure (pnpm answersERR_PNPM_RECURSIVE_RUN_NO_SCRIPT), so nothing ran there, and it is not a red gate. The corrected docblock sentence also names the pin, which is one clause more than the order asked for.Out of scope, one line each:
object-griddoes not mirrorgrouping, soobjectui validateaccepts a padded name andgrouping: 42there: merged as evidence on objectui#6152, theUnmirroredDeclaredworklist that already lists that row (5908681663).object-kanbanreadsgrouping.fields[0].fieldas its swimlane fallback, and the spec row typesgroupingasz.unknown(): filed spec(ui):ComponentPropsMap['object-kanban'].groupingisz.unknown()while the board readsgrouping.fields[0].fieldas its swimlane field — a padded or wrong-shaped grouping validates green and collapses the lanes objectstack#20831 (the spec row first; the objectui mirror follows it).packages/plugin-list/src/ListView.tsx's comment still calls the field "a bare string": Acceptance notes, carrier: none.
domain:uiseat 1 · ACCEPT · 2026-09-30T09:49Z
Generated by Claude Code
- Contract review at
objectstack-fleet commented
on Sep 30, 2026 ContributorMore actionsLanded: PR objectui#11191 merged as
574d82fdf; closedcompleteddomain:uiseat 1 ·session_0122Knsowci76D2rBWReCzzZ· 2026-09-30T10:08Z.- PR objectui#11191 merged through the merge queue at 2026-09-30T10:06:54Z as
574d82fdf, which is onorigin/main. - Verified by content, not by sha: its three files on
origin/mainare blob-identical to the reviewed head4d99273e0(.changeset/7347-padded-grouping-refused-pin.md,packages/core/src/utils/grouping-fields.ts,packages/types/src/__tests__/grouping-field-padded-refused-7347.test.ts). - The merge commit carries
Fixes #7347, but the platform did not close the card on the queue merge, so the seat closes it here and stripspm:dispatchedin the same act. The assignee is cleared with it: the claim5907894556is fulfilled. - Ruling C's objectui half is done: the pin bump (PR objectui#11086) plus this regression pin. Follow-ups from the ACCEPT
5908698924: spec(ui):ComponentPropsMap['object-kanban'].groupingisz.unknown()while the board readsgrouping.fields[0].fieldas its swimlane field — a padded or wrong-shaped grouping validates green and collapses the lanes objectstack#20831 (theobject-kanbangrouping row) and the evidence note on objectui#6152 (5908681663).
domain:uiseat 1 · landed · 2026-09-30T10:08Z- PR objectui#11191 merged through the merge queue at 2026-09-30T10:06:54Z as
- added a commit that references this issue
on Oct 7, 2026
Blocked-by: objectstack-ai/objectstack#17360
Surfaced while implementing objectui#7217 (the null-entry crash guard), by the measurement that defined that guard's admission rule. Filed unassigned, not fixed there — it is a different defect class from that card's loud crash, and it is deliberately left alone by objectui#7217's guard rather than ridden.
The claim
GroupingFieldSchema.fieldis a barez.ZodStringwith no.trim()and no pattern, sogrouping: { fields: [{ field: ' business_unit ' }] }is valid authored metadata. The two readers of that name then disagree:collectGroupingFieldRefs(packages/core/src/utils/grouping-fields.ts:71-76) trims — it harvestsbusiness_unit, and that is what reaches$select.ObjectGrid'sgroupValueFormattermemo anduseGroupedData'sbuildSegmentKeybucket rows by the raw, untrimmed key, i.e.row[' business_unit '].So the server is asked for
business_unitand returns it under that name, while the grid looks up a key that no row carries. Every row readsundefined,buildSegmentLabelanswers(empty)for all of them, and the view shows ONE group holding every record — no error, no warning, no empty state.Measured, not inferred
Run in the objectui#7217 worktree against
@objectstack/spec/uiand@object-ui/core's builtdist:Why this is worth a card of its own
It is the exact silent-wrong-answer failure objectui#7179 closed — one
(empty)bucket holding every row, which reads as a true statement about the data rather than a bug in the view — but arriving by a different route, and it survives author-time validation. The null entry that objectui#7217 guards is refused byGroupingConfigSchema; a padded name is accepted by it. That makes this one strictly more reachable through the validated path, and quieter.Why objectui#7217 did not fix it
That card's guard admits an entry when it is an object carrying a non-empty string
field— the harvester's own admission rule, chosen so the grid can never group by an entry the projection ignored. A padded name satisfies it, so the entry is admitted and grouping proceeds on the raw key, exactly as it does onmaintoday. Changing the value as well as the admission set would have been a behaviour change beyond that card's crash, on a hot file, so it was left for triage here.Fix directions, not a ruling
GroupingFieldSchema.fielda.trim()or a non-padded pattern in@objectstack/spec, so a padded name never becomes valid metadata. This is the contract-first direction (AGENTS.md #0.1: fix the producer, do not teach the renderer to tolerate), but it moves a published validator's accept set and is a spec change, so it needs the maintainer.Worth checking whether the same trim asymmetry exists for the sibling view kinds'
groupByField(kanban / gantt / timeline) before choosing, since they union their grouping field into the projection through their own path.Related: objectui#7179 (the projection fix whose harvester does the trimming), objectui#7217 (the null-entry crash guard this came out of).
Generated by Claude Code