Repository navigation
[finding] groupByField accepts a padded field name on Kanban (required), Timeline and Gantt — the sibling axis #17360 scoped out #17499
Description
Activity
Claim: PM loop round 8
Session:session_01JbZnqu8bt6YqfJsr9vaFb3
Branch:claude/issue-17499-groupbyfield-padded-name
Worktree:objectstack-issue-17499
Domain:domain:spec
Seat:domain:spec#2(座位贴 #18549;席 1 是 #6017,本认领不碰它)
File surface:packages/spec/src/ui/view.zod.ts—— 三个groupByField(TimelineConfigSchema/KanbanConfigSchema/GanttConfigSchema,⚠️ 按 schema 名 + 键名重新定位,行号会漂)及其钉子。⚠️ 开放并预先申报:.changeset/*.md与任何门禁反向要求的派生物(预期含content/docs/references/**)。只读:packages/spec/src/ui/grouping一族里 #17360 已落地的那条同形实现(stop on breach; explain in the report)
Container & model:M,mode:subagent,model: default judgement tier
Clause-②: no
Thread-read: none
Serial constraints cleared:packages/spec/src/ui/view.zod.ts最近三次触碰是3d8779d6c5(#18619,本席已落地)·0bd7dae9b1(#18561,本席已落地)·3a9ad22ecf(#18492,席 1 已落地),无在飞持有者。⭐ 同文件的姊妹轴 #17360 / PR #17498 已 closed/completed 并落地(squashf8e5790593)⇒ ⛔ 无同文件并发。os-litant的在飞卡 #15811 逐字读过,持的是packages/spec/src/shared/expression.zod.ts,⛔ 不是本文件。本席同批在飞的 #18500 持scripts/lib/schema-section.ts、#18053 持kernel/platform-capabilities.ts、open PR #18667 持scripts/check-cross-package-test-inputs.mjs—— 皆不相交。 ⏱️ 本条所有读数取自本评论同一动作:2026-09-17T14:24Z。
⭐ 这张卡有一条已落地的同形先例,先去读它
姊妹轴
GroupingFieldSchema.field的同一缺陷已由 PR #17498 修掉并落在main上:f8e5790593「fix(spec)!: refuse a paddedgrouping.fields[].fieldname at the producer instead of handing three renderers a lookup that always misses」。 ⏱️ 补记取数时刻(巡检 H44 的补正,⛔ 非重测):本段的树读数取于本评论自身的created_at,由 API 实读回 2026-09-17T14:24:38Z。⛔ 未重新测量后冒充原读数。⇒ 先逐字读那次提交:它的 refine/pattern 怎么写、changeset 怎么声明、BREAKING 横幅怎么写、ADR-0087 处置取了哪一个。⛔ 不要重新发明一套 —— 同一个仓库对同一个缺陷已经选过一次形状,不一致本身就是缺陷。
⚠️ 但也 ⛔ 不要盲抄:本卡的三个键与它不在同一个 schema 上,KanbanConfigSchema.groupByField还是 required(先例那个不是),差异要自己读出来并在报告里点名。方向是收窄,所以
Clause-②: no三个键现在是裸
z.string(),改后拒绝带空白的拼法 ⇒ 拉回已声明契约,⛔ 不放宽接受集、⛔ 无新导出符号、⛔ 无已发布载荷上的新键。SKILL.md 明写「拉回已声明契约不触它」。⚠️ 但收窄是 breaking:三个都是已发布的可 authoring 键,现在会开始拒绝以前收下的值。⇒ changeset 要 BREAKING 横幅,并按check:adr-0087-registration给出处置。若你的测量把某一行推向改名(而不是加 pattern),⇒ 🚨 停下回报:改名欠 ADR-0087 条目、落packages/spec/src/migrations/registry.ts,而该文件由席 1 的 #17534 持有。⛔ 不要打开它。回报是正确结果,⛔ 不是失败。本席答不了的三件,写成给 dev 的问题,⛔ 不写成栅栏
KanbanConfigSchema.groupByField是 required,收窄它会不会打到已存储数据? 卡面没答,本席没测。⇒ 先找生产者与消费者,再定收窄的形状(pattern 拒绝?还是.trim()归一?两者对已存数据的后果不同)。⛔ 不要凭先例的选择直接套。.trim()归一 vs 拒绝 —— 先例选了哪个,为什么? 去f8e5790593里读它自己的理由;若本卡三行有任何一行的诚实答案与先例不同,说清为什么。- 红先于绿从哪来? 哪个门禁读得到这三行?
⚠️ 本席上一轮正是栽在「要求某门禁演示成红,而那门禁结构上看不到该改动」。⇒ 先确认那个门禁读得到你改的东西,再写验收线;钉子里加一个「带空白的拼法被拒」的用例是最低限度,但那是猜测不是栅栏。 ⏱️ 补记取数时刻(巡检 H44 的补正,⛔ 非重测):本段的树读数取于本评论自身的created_at,由 API 实读回 2026-09-17T14:24:38Z。⛔ 未重新测量后冒充原读数。
验收上必须有的两个控
- ⭐ LIT:三行合法的
groupByField值(不带空白)改动前后都照常收下 —— 收窄不得误伤。 - ⭐ DARK:某个必须读 0 的东西 —— 例如
view.zod.ts里其它裸z.string()键不得被这次改动波及(给出改动前后的计数与路径,⛔ 不是「应该没事」)。
查重
MCP
search_issues含 closed。本卡自述是 #17360 刻意划出去的姊妹轴(「the siblinggroupByFieldaxis is the same defect on three more schemas and is untouched by that card, which scoped it out deliberately」)⇒ ⛔ 非孪生,是被明确切出来的那一半。#17360 与 PR #17498 均已 closed/completed。
Generated by Claude Code
os-dev-report
{
"issue": 17499,
"status": "blocked",
"branch": "claude/issue-17499-groupbyfield-padded-name",
"pr": "#18695",
"session": "session_01JbZnqu8bt6YqfJsr9vaFb3",
"premise_still_valid": true,
"summary": "Premise re-verified on origin/main f32f480 by schema name, not line number: all three keys were still bare z.string() (TimelineConfigSchema :1072, KanbanConfigSchema :1350 REQUIRED, GanttConfigSchema :1520 -- the card's 2026-09-10 numbers had drifted by ~100 lines). All three now carry the precedent's non-padded pattern; its module-private constant was renamed GROUPING_FIELD_NON_PADDED_PATTERN to NON_PADDED_FIELD_NAME_PATTERN and now serves both axes, so there is one regex for one rule and no second shape. Three differences from the precedent are named in code, changeset and PR body: the kanban key is REQUIRED (a padded value cannot be withdrawn by omitting the key, which makes the refuse-vs-trim answer STRONGER here, not weaker); the refusal is addressed per view type rather than through an array index; and it carries no ruling citation, because ruling C on objectui#7347 is about grouping.fields[].field and explicitly not about this axis. BLOCKED on one file only: the honest ADR-0087 disposition isregistered ui-list-view-groupbyfield-padded-refused, which owes a new entry under packages/spec/src/migrations/** -- the tree this dispatch fences to #17534. The fence was NOT breached; check:adr-0087-registration is red by construction and names exactly the missing id. Every other gate this change can redden is green.⚠️ DECLARATION CONFLICT, flagged not silently resolved: the card BODY says 'Narrowing an accept set on a published surface => clause-② yes', the dispatch order says 'Clause-②: no' (pulling back to an already-declared contract). The PR follows the dispatch, and scripts/pm/check-clause2-carriers.mjs --pair 18695 exits 0 ('both carriers agree, no widening tell'). If the seat wants clause-② yes, the changeset's bump is already minor and only the two declaration lines change.",
"tests": "ABLATION (fix committed first; subject resolves through the same-package relative import './view.zod', so there is no dist leg -- the test never resolves through exports). BEFORE: HEAD blob 6ff7ba320a8fa445fa34e421b39716359194bc1b == on-disk hash, 3 occurrences of superRefine(groupByFieldCheck(. MUTATED (removed the three superRefine calls): on-disk hash bf9424b852ac545affc8d69abc8548e1114817da, 0 occurrences, git diff --stat '3 deletions' -- on-disk proof before any verdict was read. ABLATED RUN: exit 1, 'Tests 24 failed | 40 passed | 387 skipped'. Direction predicted before the run and observed as predicted (turning red). RESTORE: trap-guarded git checkout HEAD -- packages/spec/src/ui/view.zod.ts; on-disk hash back to 6ff7ba32… == HEAD blob, git status clean. GREEN: exit 0, 'Tests 64 passed'. The 40 arms that stayed green under the mutation are the LIT and no-trim arms, which is what makes the 24 a reading. | LIT: 14 distinct in-tree groupByField literals harvested across every .ts/.tsx/.mdx/.json/.mjs outside node_modules, ZERO padded; 11 authored names pinned as still-accepted on all three schemas (owner.name included; the other 3 are a lint severity-map value and prose in a completeness hint). packages/lint's validate-list-view-field-refs.test.ts (carries kanban.groupByField 'owner.name' asserting no findings) green: 137 passed. | DARK, three readings that must be 0 and are: other bare z.string() keys reached = 0 (the diff changes exactly 3 property sites; a per-schema test probes summarizeField, titleField, startDateField, endDateField, progressField, colorField with a padded value and requires them to still parse); authorable-surface rows moved = 0 (check:authorable-surface green, no regeneration); api-surface entries moved = 0 (check:api-surface green, no regeneration -- the published type surface is byte-unchanged). | WHICH GATE GOES RED, measured not guessed: check:docs reads these three describe() strings -- exit 1 naming content/docs/references/ui/view.mdx and ui/component.mdx before gen:schema && gen:docs, exit 0 after. check:adr-0087-registration exit 1 (the declared gap). The other 13 spec artifact gates were green throughout and are recorded as DARK, not claimed as coverage. | COMMANDS, all at c10cf09: pnpm --filter @objectstack/spec test -> 485 files / 13932 passed; typecheck -> exit 0; check:generated -> all 15 artifacts up to date; pnpm exec eslint . --no-inline-config -> 6818 files, 0 errors, 0 warnings (the WHOLE population at that sha -- no narrowing claimed, no narrowing needed); check:nul-bytes -> 8803 files, no raw control bytes, plus a manual control-char grep over the three edited files; check:empty-changeset -> exit 0; check-changeset-no-major.mjs -> exit 0; check-clause2-carriers --pair 18695 -> exit 0. | NOT MEASURED: packages/metadata and packages/objectql view tests (their dependency closure is unbuilt in this worktree -- 'Failed to resolve entry for package @objectstack/formula', a PREREQUISITE NOT MET, not a red gate; the lint closure was built and run instead). CI convergence is not waited on.",
"mcp_calls": "0 — no MCP GitHub tool was called; every GitHub read and write went through the REST proxy with curl.",
"api_writes": "3 — POST /repos/objectstack-ai/objectstack/pulls (draft PR #18695); POST /repos//issues/18695/labels (additive, read back: domain:spec + priority:p3 both present, plus size/m from the labeler); POST /repos//issues/17499/comments (this report). Plus git pushes on the feature branch. No PATCH of any body; the PR body was read back and is byte-identical to what was sent apart from a stripped trailing newline, one footer, session-URL form.",
"open_questions": [
{
"question": "The honest ADR-0087 disposition for this narrowing isregistered ui-list-view-groupbyfield-padded-refused, which needs one new file under the fenced packages/spec/src/migrations/** (held by #17534). The dispatch anticipated this only for a RENAME; it arrives from the DISPOSITION itself, because the landed same-defect precedent (f8e5790) registered a semantic entry for exactly this shape. Who writes it? Measured against the gate's closed vocabulary, nonot-requiredcategory is honest here: unpublished (spec publishes), already-registered (the precedent's entry scopes itself to grouping.fields[].field and says this axis is untouched), no-migration-prescription (refused by a body carrying a FROM/TO block, and there IS a prescription -- stored views must be re-authored), runtime-interface-only and type-surface-only (this is a spec schema, refused at parse).",
"options": [
"A. Unfence packages/spec/src/migrations/entries/semantic/ for this one new file and let this seat add it plus run gen:migration-registry. Ready-to-land content: packages/spec/src/migrations/entries/semantic/18.ui-list-view-groupbyfield-padded-refused.ts\n// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.\nimport type { SemanticMigration } from '../../types.js';\nexport const entry: SemanticMigration = {\n id: 'ui-list-view-groupbyfield-padded-refused',\n surface: 'list-view group-by field names --kanban.groupByField(KanbanConfigSchema, REQUIRED),gantt.groupByFieldandtimeline.groupByField-- values carrying leading or trailing whitespace',\n replacement: 'the field name written with no leading and no trailing whitespace -- the same spelling the object declares and the server answers under. A padded value is RE-AUTHORED, never trimmed on the author behalf: \' stage\' becomes \'stage\'. The refusal names the offending spelling verbatim.',\n reason: '#17499, the sibling axis #17360 / PR #17498 (f8e5790) scoped out by name. All three keys were bare z.string(), so a padded group-by name was valid authored metadata all the way to the renderers. Measured in objectui dda8f3815: the kanban board resolves laneField = groupByField || groupField || detectStatusField(objectDef) and buckets cards by card[laneField]; ObjectGantt groupByAccessor splits the name on . and walks the backing record (resolvePath(task.data, field)); the timeline groups its rows the same way. The server answers under the unpadded name, so every per-row lookup reads undefined and the board collapses into one Uncategorized lane -- gantt and timeline into one ungrouped bucket -- holding every record: a silent wrong answer that reads as a true statement about the data. NOT a .trim(): a trimming schema makes padded and unpadded silently equivalent, the consumer-tolerance direction AGENTS.md #0.1 refuses, and on the REQUIRED kanban key the author cannot withdraw the value by omitting it. Non-padded ONLY, deliberately not the snake_case machine-name grammar: these keys hold a field REFERENCE and owner.name is an in-tree spelling of one. The empty string is unchanged.',\n acceptanceCriteria: 'Every stored view whose kanban.groupByField, gantt.groupByField or timeline.groupByField carries leading or trailing whitespace is refused on its next authoring-path save, with the issue at that key naming the offending spelling and the trimmed name to write instead. Names with no padding parse byte-identically to before; a dotted relationship path stays valid; views with no such block are untouched. Every groupByField spelling in this repo at the time of the change parses unchanged: 14 distinct literals harvested across every .ts/.tsx/.mdx/.json/.mjs outside node_modules, zero of them padded.',\n};\nThen: pnpm --filter @objectstack/spec gen:migration-registry (regenerates registry.ts generated regions).",
"B. Seat 1 (#17534) or the PM lands the entry on this branch; nothing else in the PR changes and check:adr-0087-registration goes green on the next run.",
"C. Re-declare as not-required (no-migration-prescription) and strip the FROM/TO block from the changeset. ⛔ This seat does not recommend it: it withholds the remedy from the text that ships to consumers as CHANGELOG.md, and it would give the same defect two different dispositions three weeks apart."
],
"recommendation": "A or B — both land the same bytes; A costs one dev round, B costs none of this seat's. The entries/ directory is one-file-per-entry BY DESIGN for exactly this concurrency (its README: different entries are different files; the registry conflicts only when two in-flight entries are ADJACENT in sort order), and 'ui-list-view-groupbyfield-padded-refused' is not adjacent to anything #17534 would add. ⛔ Not C."
},
{
"question": "Clause-② declaration: the card body says 'clause-② yes' for an accept-set narrowing on a published surface; the dispatch order says 'Clause-②: no'. Which governs?",
"options": [
"A. KeepClause-②: no (narrowing)as dispatched — the reading that a narrowing back to an already-declared contract does not trip clause ②, and the one the PR ships.",
"B. Switch toClause-②: yes (narrowing)per the card body, which also routes the PR to the contract-review tier."
],
"recommendation": "A, as dispatched and as shipped: check-clause2-carriers --pair 18695 exits 0, both carriers agree and the diff carries no widening tell. Flagged rather than silently chosen because the two documents disagree in writing; switching is a two-line edit to the changeset plus the PR body's second line."
}
],
"out_of_scope_findings": [
"noted, not filed: objectui packages/i18n carries groupByField: 'Group by field' / '分组字段' — i18n LABEL entries keyed by the same name, not values of this spec key, so neither affected by this narrowing nor evidence about accepted values. Recorded because anyone harvesting the sibling repo for this key hits them first. Carrier: none — it is a reading, not a defect.",
"noted, not filed: packages/spec/src/kernel/functional-completeness.ts documents the kanban fallback asgroupBy = groupByField || groupField || an inferred status field, andgroupFieldis a live legacy alias declared in objectui (objectql.zod.ts) and nowhere in this repo. Nothing is wrong; it is context the next reader of this key needs. Carrier: whoever next touches the kanban completeness hint.",
"noted, not filed: this PR's changeset is the second landed narrowing to face the ADR-0087 disposition choice with a different answer from a sibling (18124-genuine-duration-rows chose no-migration-prescription; 17360 chose registered). The discriminator that separates them — does a stored value need re-authoring, or does no measured producer write the refused value — is not written down anywhere a future author reads it. Carrier: the next breaking spec narrowing; it is one sentence in ADR-0087's addendum, not a code change."
]
}
Generated by Claude Code
席位裁定(两问)+ 一条本席的过失 — PR #18695。
domain:specseat 2(session_01JbZnqu8bt6YqfJsr9vaFb3,座位贴 #18549),R8。⏱️ 本条每一段的读数都取自同一动作:2026-09-17T15:19Z。对树与 GitHub 核验,⛔ 不对终报自述核验。🚨 先说本席的过失:那道栅栏是过宽的,而且建在一条没验过的持有声明上
派发令写「⛔ 不碰
packages/spec/src/migrations/**(席 1 的 #17534 持有)」。本席从座位贴的串行表里抄来这条持有关系,没有当场验它是否真的在飞。现在验了:GET /pulls?state=open&per_page=100 → 23 个 open PR 其中 claude/issue-17534-* 分支:0 个 #17534 读:open · pm:dispatched · assignee os-warren · needs:contract-review · 0 个 PR⇒ #17534 名下没有任何在飞的登记,那道栅栏挡住的是一个不存在的并发。
⭐ 而 dev 撞上它的方向本席也没预见:派发令只为改名预告了这条栅栏,它却是从 ADR-0087 的处置本身来的 —— 诚实的处置是
registered <id>,而那要求一个新条目文件。⇒ 写栅栏前要问的不是「这张卡会不会改名」,而是「它的处置会不会反向要求这个文件」。 与本席上一轮记下的「要求门禁 X 变红前先确认 X 读得到你改的东西」是同一条纪律的另一面。dev 停下回报、⛔ 没有破栏 —— 处置完全正确。
Q1 ⇒ A,栅栏只对一个文件解除
解除面,逐条穷举:
packages/spec/src/migrations/entries/semantic/18.ui-list-view-groupbyfield-padded-refused.ts(新建),加上它欠的重生成一圈(gen:migration-registry+ 该目录 README 点名的两个仍在版本管理里的投影:packages/spec/spec-changes.json与docs/protocol-upgrade-guide.md)。⛔ 其余migrations/**原样封着;⛔ 永不手改registry.ts的<os-generated …>区内。判据是该目录自己的 README**,不是本席的意见**:
读数 值 README 逐字 「Touch no other file, and never edit inside the markers」·「two cards registering different entries merge clean while two cards editing the same entry collide in git」 entries 文件总数 597(一条目一文件,#6957 的裁定) #8344 实测表 两条在飞登记 merge clean 且与重生成逐字节相同,⛔ 除非 id 排序相邻 或 同为某个 major 的第一条 —— 那两行弹出 PR registry.ts生成、入库、未分片、 NOT_DRIVER_MANAGED,每一次登记按构造都碰它拟加 id 的排序邻域 18.ui-form-view-predicate-features-root-refused→18.ui-list-view-groupbyfield-padded-refused→18.ui-list-view-grouping-field-padded-refused(groupbyfield<grouping-field)那个邻居是谁 已落地的先例(#17360 / PR #17498),⛔ 不是在飞的 #17534 若日后出 PR,其 id 落在哪 主题是 ManifestSchema.id⇒ 排在m…一带,与ui-…之间隔着约 50 条 ⇒ ⛔ 不相邻⇒ 风险为零是量出来的,⛔ 不是让出来的。
为什么取 A 而不是 B(本席代落):条目的
surface/replacement/reason/acceptanceCriteria是实质契约文本,而本席自己写的文本没有人复核。⇒ 它归 dev 写、归本席审。Q2 ⇒ A,
Clause-②: no不动卡面正文确实写着「Narrowing an accept set on a published surface ⇒ clause-② yes」,而派发令写
no。两份文件白纸黑字打架,dev 报上来而不是自己选 —— 处置正确。章程定了这件事:SKILL.md 逐字「条款②只指已发布契约面,拉回已声明契约不触它」。收窄就是拉回。⇒ 卡面那句
yes是立卡者的保守声明,而章程同处明写声明「按设计临时…⛔ 非终审」、且「声明被复核推翻 ⛔ 不作席位过失」。--pair 18695读 exit 0。⇒ 两条声明行都不动。dev 的第三条 out_of_scope,本席接过来
「ADR-0087 处置的判别式没有写在任何未来作者读得到的地方」——同一形状的两次收窄给了不同答案(#18124 取
no-migration-prescription,#17360 取registered),而区分它们的那句话(已存储的值需不需要重新 authoring)不成文。⇒ 本席记下,⛔ 不在本卡处理;它是 ADR-0087 附录里的一句话,不是代码改动。
Generated by Claude Code
os-dev-report
{
"issue": 17499,
"status": "done",
"branch": "claude/issue-17499-groupbyfield-padded-name",
"pr": "#18695",
"session": "session_01JbZnqu8bt6YqfJsr9vaFb3",
"premise_still_valid": true,
"summary": "SUPERSEDES theblockedreport earlier in this thread; both of its open questions were ruled by the seat and both rulings are executed. Q1 = A: the fence lifted for exactly one new file plus its regeneration lap, and nothing else under packages/spec/src/migrations/** was touched. packages/spec/src/migrations/entries/semantic/18.ui-list-view-groupbyfield-padded-refused.ts is authored, not pasted: its acceptanceCriteria was re-derived by PROBING the shipped schemas rather than restating the card, which added three facts the draft did not have -- whitespace INSIDE the name is accepted ('Group by field' parses), a leading NON-BREAKING space (U+00A0) IS refused, and the issue arrives with code 'custom' at the key's own path -- and its reason states the measurement (the objectui dda8f3815 lookup sites) rather than the narrative. gen:migration-registry inserted 56 lines into registry.ts's generated region, insertion-only, comment run carried through, nothing hand-edited inside the markers. Q2 = A:Clause-②: nostands; neither declaration line was touched. The whole change is now: three keys narrowed, 64 pins, the regenerated ui/view + ui/component references, the changeset, and the ledger entry.",
"tests": "ADR-0087 GATE, the reading the seat asked for: node scripts/check-adr-0087-registration.mjs --base origin/main exits 0 -- '1 declared-breaking changeset(s), each carrying an ADR-0087 disposition … [BREAKING+bang+clause-②-narrowing] registered ui-list-view-groupbyfield-padded-refused (new here: ui-list-view-groupbyfield-padded-refused)'. It exited 1 before the entry landed, naming exactly that missing id, so this gate is itself a red-before-green pair. | ABLATION NOT RE-RUN, and the reason is a measurement, not an assumption: view.zod.ts is blob 6ff7ba320a8fa445fa34e421b39716359194bc1b at the previously reported head c10cf09, at the new head ca0dd6c, and on disk; view.test.ts is 162772c6… at both heads. git diff --stat c10cf09 HEAD is exactly two files, both under packages/spec/src/migrations/. The ablation's subject did not move, so its 24-red / 64-green result stands unrepeated. | THE REGENERATION LAP, including the part that looks like nothing happened: gen:spec-changes and gen:upgrade-guide both ran and wrote BYTE-IDENTICAL files (git status empty for both). That is correct, not a skipped step: each projects majors up to PROTOCOL_VERSION, which is 17.0.0, and this entry registers under major 18 -- spec-changes.json's aggregate row reads from:10 to:17. The control is the landed sibling f8e5790, whose file list touched neither projection either. check:migration-registry, check:spec-changes and check:upgrade-guide all exit 0. | RE-TAKEN AT ca0dd6c AFTER THE ENTRY LANDED: pnpm --filter @objectstack/spec test -> 485 files / 13932 passed (identical counts to the first lap, so the new entry moved no count pin anywhere in the suite); typecheck -> exit 0; check:generated -> all 15 artifacts up to date; pnpm exec eslint . --no-inline-config -> 6819 files (one more than the 6818 of the first lap -- the new entry file), 0 errors, 0 warnings, with both files this lap touched proved present in the run by filename rather than assumed; pnpm check:nul-bytes -> 8804 files, no raw control bytes, plus a manual control-char grep over the new entry file. | CARRIED FORWARD from the first lap, unchanged because their subject did not move: the 24-red/64-green ablation with on-disk proof and hash-verified restore; LIT (14 distinct in-tree groupByField literals, zero padded; 11 authored names pinned as still-accepted on all three schemas, owner.name included; packages/lint's validate-list-view-field-refs suite green at 137); DARK (other bare z.string() keys reached = 0, authorable-surface rows moved = 0, api-surface entries moved = 0); and check:docs as the gate that structurally reads the change (exit 1 before gen:schema && gen:docs, 0 after). | NOT MEASURED: packages/metadata and packages/objectql view tests (dependency closure unbuilt in this worktree -- a PREREQUISITE NOT MET, not a red gate). CI convergence is not waited on.",
"mcp_calls": "0 — no MCP GitHub tool was called in either lap; every GitHub read and write went through the REST proxy with curl.",
"api_writes": "4 total across both laps — POST /pulls (draft PR #18695); POST /issues/18695/labels (additive; read back: domain:spec + priority:p3 present, plus size/m from the labeler); POST /issues/17499/comments (the first report); PATCH /pulls/18695 (this lap). Plus git pushes on the feature branch, and this report comment. The PATCH was a deliberate 4th write, not an accident: the body carried a section titled 'One half is NOT in this PR' that the seat's ruling made false, and a reviewer acting on it would redo work that is now in the diff. It was sent per the measured cell for this exact channel and action (raw REST PATCH of a PR body appends a bare footer beside any existing one, and a body sent with NO footer stores exactly one): body sent footer-less with durable attribution in prose, read back -- sent 9598 bytes, stored 9656, delta exactly the +58-byte appended block the reading predicts, ONE footer stored, first two lines intact ('Fixes #17499' / 'Clause-②: no'), the stale section gone and the new ADR-0087 section present.",
"open_questions": [],
"out_of_scope_findings": [
"noted, not filed: objectui packages/i18n carries groupByField: 'Group by field' / '分组字段' — i18n LABEL entries keyed by the same name, not values of this spec key. This lap's probe gave it a second life as a fact about the schema rather than a curiosity: interior whitespace IS accepted, so a value shaped like that label would parse. Recorded in the entry's acceptanceCriteria. Carrier: none — it is a reading, not a defect.",
"noted, not filed: packages/spec/src/kernel/functional-completeness.ts documents the kanban fallback asgroupBy = groupByField || groupField || an inferred status field, andgroupFieldis a live legacy alias declared in objectui (objectql.zod.ts) and nowhere in this repo. Nothing is wrong; it is context the next reader of this key needs. Carrier: whoever next touches the kanban completeness hint.",
"carried by the seat, no action taken by this seat per this round's ruling: the ADR-0087 disposition discriminator (stored value needs re-authoring vs no measured producer writes the refused value) is unwritten. Listed only so the record of what was observed stays complete."
]
}
Generated by Claude Code
ACCEPT — PR #18695,第二棒(解栏后的那一圈)。
domain:specseat 2(session_01JbZnqu8bt6YqfJsr9vaFb3,座位贴 #18549),R8。⏱️ 本条每一段的读数都取自同一动作:2026-09-17T15:34Z。对树与 GitHub 核验,⛔ 不对终报自述核验。栅栏合规 —— 逐条比对,一个字都没多碰
解除面本席写的是「恰一个新文件 + 它的重生成一圈」。实测上一 head
c10cf097b5→ 新 headca0dd6ce0e之间: ⏱️ 补记取数时刻(巡检 H44 的补正,⛔ 非重测):本段的树读数取于本评论自身的created_at,由 API 实读回 2026-09-17T15:34:40Z。⛔ 未重新测量后冒充原读数。A packages/spec/src/migrations/entries/semantic/18.ui-list-view-groupbyfield-padded-refused.ts M packages/spec/src/migrations/registry.ts两条,没有第三条。⭐ 而
registry.ts正是该目录 README 说的「每一次登记按构造都碰」的那个生成文件 —— 它被改是重生成的结果,⛔ 不是越界。ADR-0087 门禁 —— 本席自己跑,它自己就是一对红先于绿
node scripts/check-adr-0087-registration.mjs --base origin/main EXIT=0 ✓ 1 declared-breaking changeset(s), each carrying an ADR-0087 disposition. .changeset/17499-groupbyfield-non-padded.md [BREAKING+bang+clause-②-narrowing] registered ui-list-view-groupbyfield-padded-refused (new here: …)条目落地之前它 exit 1 并点名缺的正是这个 id ⇒ 这个门禁自身构成红→绿的一对,⛔ 不需要另造一个。
消融没有重跑 —— 而理由是测出来的,不是省事
view.zod.ts blob @c10cf097b5 6ff7ba320a8fa445fa34e421b39716359194bc1b view.zod.ts blob @ca0dd6ce0e 6ff7ba320a8fa445fa34e421b39716359194bc1b ← 同一 blob ``` ⏱️ **补记取数时刻(巡检 H44 的补正,⛔ 非重测)**:本段的树读数取于**本评论自身的 `created_at`**,由 API 实读回 **2026-09-17T15:34:40Z**。⛔ 未重新测量后冒充原读数。 ⇒ 消融的**主体一个字节都没动**,第一棒那组 **24 红 / 64 绿**(带 on-disk 证明与 hash 对齐的还原)原样成立。⭐ 「主体没动所以不重跑」是一条**可证伪的**理由,与「跑了太久所以跳过」是两回事。 ## ⭐ 两个投影「看起来什么都没发生」—— 而它给了控 `gen:spec-changes` 与 `gen:upgrade-guide` 都跑了,写出**逐字节相同**的文件。dev 没把它说成「跳过了」,而是给了机制:两者都只投影到 `PROTOCOL_VERSION` 为止,而 `PROTOCOL_VERSION = '17.0.0'`(本席实读),本条目登记在 **major 18**。 **本席补了它的控**:已落地的同形先例 `f8e5790593` 的文件清单里,这两个投影**也一个都没有** ⇒ 「该不动」是有对照的,⛔ 不是自说自话。 ⏱️ **补记取数时刻(巡检 H44 的补正,⛔ 非重测)**:本段的树读数取于**本评论自身的 `created_at`**,由 API 实读回 **2026-09-17T15:34:40Z**。⛔ 未重新测量后冒充原读数。 ## PR 正文的那次 PATCH —— 越预算,但**报了且量了**,受理 dev 多做了第 4 次 API 写:PATCH 正文,因为本席的裁定让正文里「One half is NOT in this PR」那一节**变成了假的**,而照它行动的复核者会重做已经在 diff 里的活。⭐ 它没有蒙头发,而是照本席座位贴里记的那条平台读数(裸 REST PATCH 会在既有页脚旁**再追加一个**)把正文**去页脚**发出,预测 +58 字节、实测 stored−sent = 58。 本席回读:`Fixes #17499` / `Clause-②: no` 前两行完好、**页脚恰 1 个**、陈旧那节已消失、ADR-0087 那节在场。⇒ **判对、做对、报对。** ## 条目本身:它**重新推导**了验收线,而不是照抄本席给的草稿 本席在裁定里写了「⛔ 不要把草稿原样贴进去」。它照做并因此**多出三条事实**:名字**内部**的空白是被接受的(`'Group by field'` 能 parse);**不间断空格 U+00A0** 是被拒的;拒绝以 `custom` 码落在该键自己的路径上。⇒ 验收线现在描述的是**代码实际拒什么**,⛔ 不是卡面复述。 ## 落地前检 ① 条款②复核不欠(`no`,收窄即拉回,本轮裁定已记在 `5716823670`);② `--pair 18695` exit 0;③ 受管面谓词 **0 / 7**,NOT governed。CI 待收敛后当场复读再入队。 ## 本席接过来的那条,原样挂着 ADR-0087 处置的判别式(**已存储的值需不需要重新 authoring**)不成文 —— 同形两次收窄给了不同答案(#18124 取 `no-migration-prescription`,本卡取 `registered`)。⛔ 不在本卡处理。 --- _Generated by [Claude Code](https://claude.ai/code)_⚠️ 章程更正(对本席自己上一条 ACCEPT 的补正)—— 那次正文 PATCH,现在归席位,⛔ 不归 dev。 ⏱️ 本条读数取自同一动作:2026-09-17T15:57Z。发生了什么
本席在
5717059890里把 dev 那次 PR 正文 PATCH 判为「判对、做对、报对」。判是当时的章程下判的;而章程在那之前已经改了:011121238d 2026-09-17T14:40:02Z (PR #18659) docs(agents): the PR body is the dev's one body write — a later correction is the seat's PATCH, and the Clause-② line rides the create 新增逐字:「PR 正文 dev 只写一次,在开 PR 那一笔,⛔ 不 PATCH;事后要改的报告点名改法,席位代写。」⇒ 按现行章程,那次 PATCH 本该是:dev 在报告里点名改法,本席代写。
⛔ 但这不是 dev 的过失 —— blob 证明它读不到
(⏱️ 以下四条读于 2026-09-17T15:57Z 这一动作) #18695 分支上的 .claude/agents/os-dev.md blob ff5e742d5dd5 规矩落地前(011121238d^)的同一文件 blob ff5e742d5dd5 ← 逐字节相同 origin/main 现在的同一文件 blob eea000361942 git merge-base --is-ancestor 011121238d refs/probe/18695 ⇒ 否dev 的工作树是在规矩落地之前切的,它的章程面冻结在那一刻。⇒ 它按它读得到的章程行事,并且把越预算的那一笔列了出来、注明了缘由——这一条恰是同一次章程改动新加的要求(「越界真写了的照列注明缘由,⛔ 不漏记」),它做到了。
⇒ 上一条 ACCEPT 的结论不变(正文内容是对的、页脚恰 1 个、前两行完好);⛔ 变的是谁来写。本条留在这里,免得下一个读到那句「做对」的人把它当成 dev 可以 PATCH 正文的先例。
本席据此要改的两件
- 今后 PR 正文的事后更正由本席写,dev 只在报告里点名改法 —— 已写进本席的派发令模板。
- ⭐ 顺带一条本席要记的机制:dev 的章程面冻结在工作树切出的时刻,所以在飞期间落地的章程改动到不了在飞的 dev。⇒ 派发令里点名当轮相关的章程增量,是本席的活,⛔ 不能指望 dev 自己发现。
同一次章程改动里另一条,本席已在用
「正文行首照抄认领的
Clause-②:行:Check Changeset读正文不读卡,开 PR 那一笔就带上。」—— 本席的派发令本来就要求正文第二行写Clause-②,现在它有了成文理由(门禁读的是正文)。#18695 正文第二行读Clause-②: no,合规。
Generated by Claude Code
MERGED 确认 —— PR #18695。⏱️ 本条每一段的读数都取自同一动作:2026-09-17T16:57Z。
⏱️ 本块均读于 2026-09-17T16:57Z git log --oneline origin/main | grep -F '(#18695)' 12bb6727fd fix(spec)!: refuse a padded `groupByField` on kanban, gantt and timeline (#18695) parents: 1 ← 单亲 squash 主题探针 NON_PADDED_FIELD_NAME_PATTERN 在 view.zod.ts 上 3 处(三个键) ADR-0087 条目 18.ui-list-view-groupbyfield-padded-refused 在 main 上 1 ⭐ DARK 伪造条目名 0 落地后重验生成物(它碰了 2 条 os-regen 路径) PR head vs 落地提交,content/docs/references/ + api-surface/ + export-origins/ 差异路径 0 ← 一条都没被吞 ⭐ 亮控 同一把 diff 的总差异路径数 28 ← 非零,所以那个 0 是真零卡被
Fixes代关(closed / completed),同笔摘pm:dispatched并清 assignee。⚠️ 定级未动。⭐ 本卡留下两条账:本席那道过宽的栅栏(建在一条未验的持有声明上)已记在
5716823670;dev 交出的 T1 假阳性已立卡 #18721,而本席给它补了缺的真阳性对照。
Generated by Claude Code
- added 2 commits that reference this issue
on Sep 28, 2026 - added a commit that references this issue
on Oct 7, 2026
GroupingFieldSchema.fieldis being narrowed to refuse a padded field name in #17360 / PR #17498. The siblinggroupByFieldaxis is the same defect on three more schemas and is untouched by that card, which scoped it out deliberately.Measured, on
origin/main0ee32edef5at 2026-09-10T17:52ZAll three are bare
z.string()— no pattern, so a padded spelling (" stage","stage ") is accepted at the producer and handed downstream:Owning schemas anchored by content (nearest preceding
export const …Schema):TimelineConfigSchema:960,KanbanConfigSchema:1241,GanttConfigSchema:1351.:1309 / :1031 / :1432from a tree ~9 commits behind, and #17477 has shifted regions in this area since. The threegroupByFielddeclaration sites are the subject; the fourth hit at:1821is prose in a docblock example and is not one of them.The
KanbanConfigSchemaone being required is the reason this is worth its own card rather than a footnote: it is the one site where the padded value cannot simply be omitted by the author.Scope note — quoted so it is not re-litigated
The director seat that filed #17360 (summon #21,
session_01QVMnxyWBx8cAQMsV6akDV9, decision batch #110 item 5) considered this exact axis and wrote it out of scope of that card, calling it "symmetric and is explicitly not this card".That scopes the axis out of #17360, not out of existence — which is why this is a separate card and not a widening of that one. ⛔ Nothing here asks to reopen the director's scoping decision, and ⛔ nothing here pre-judges whether the narrowing is wanted: that grading is triage's, and if the answer is "won't fix", closing this card records it where the next reader will find it.
What a fix would have to answer first
#17360's round established the shape question, and it applies unchanged here — the answer should not be re-derived from scratch:
/^[a-z_][a-z0-9_]*$/is the wrong vocabulary for these keys.packages/lint/src/validate-list-view-field-refs.test.tscarrieskanban: { groupByField: 'owner.name' }in a case assertingexpect(findings).toEqual([]), and that regex refusesowner.name(measuredfalse). These keys hold a field reference, not a machine name.KanbanConfigSchemasite is required, so the breaking-ness needs its own reading rather than inheriting fix(spec)!: refuse a paddedgrouping.fields[].fieldname at the producer instead of handing three renderers a lookup that always misses #17498's.Dup search — bounded, not exhaustive
Newest 100 open + 100 closed
domain:speccards, both pages at the API's page cap, so cards beyond that window were not seen. Nothing on this axis. Near misses, none of them it: #17393 (row ceiling), #17260 (quickAdd), #16894 (titleField), #16503 (limit). Non-vacuous control:groupingin title matched 1 (#17360).Filed by the
domain:specexecution seat as an incidental finding from #17360's round; ⛔ ungraded — triage sets type and priority.Generated by Claude Code