fix(spec)!: refuse a padded groupByField on kanban, gantt and timeline instead of handing the renderer a lookup that always misses - #18695
Conversation
WIP checkpoint before the verification run. Claude-Session: https://claude.ai/code/session_01JbZnqu8bt6YqfJsr9vaFb3 Co-authored-by: Claude <noreply@anthropic.com>
LIT (every in-tree spelling still parses, `owner.name` included), DARK (every sibling string key on the same schema still accepts a padded value), the no-trim discriminator and the required-key double door. Claude-Session: https://claude.ai/code/session_01JbZnqu8bt6YqfJsr9vaFb3 Co-authored-by: Claude <noreply@anthropic.com>
…padded `groupByField` Generated projection of the three `describe()` changes. `gen:schema` + `gen:docs`; no hand edit. Claude-Session: https://claude.ai/code/session_01JbZnqu8bt6YqfJsr9vaFb3 Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JbZnqu8bt6YqfJsr9vaFb3 Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift CheckThis PR changes 1 package(s): ⛔ 2 release-owned page(s) name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 136 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 4c7701dfc8fb1a1e58e96bbc89770c4a37ccd575 && git checkout 4c7701dfc8fb1a1e58e96bbc89770c4a37ccd575
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 1bc22b3dcddc8a30b4826da8625e7787d5518a8f ca0dd6ce0ef3ba7cc351629c557a13cb8a24d5c4 && git checkout -B drift-repro 1bc22b3dcddc8a30b4826da8625e7787d5518a8f && git merge --no-ff ca0dd6ce0ef3ba7cc351629c557a13cb8a24d5c4
node scripts/docs-audit/affected-docs.mjs --json 1bc22b3dcddc8a30b4826da8625e7787d5518a8f
|
…pByField` refusal `ui-list-view-groupbyfield-padded-refused` under protocol major 18 — the disposition the changeset's marker already claimed. One new file under `entries/semantic/` plus its regeneration lap; nothing hand-edited inside `registry.ts`'s generated markers. `spec-changes.json` and `docs/protocol-upgrade-guide.md` were regenerated and are byte-identical: both project majors up to `PROTOCOL_VERSION` (17.0.0), and this entry registers under 18. The landed sibling `f8e5790593` is the control — it touched neither file either. Claude-Session: https://claude.ai/code/session_01JbZnqu8bt6YqfJsr9vaFb3 Co-authored-by: Claude <noreply@anthropic.com>
…asure — narrow `DashboardWidgetSchema.values` for the metric/kpi/gauge/solid-gauge/bullet family (objectui#8894 ruling D) (objectstack-ai#18720) Fixes objectstack-ai#17779 Clause-②: yes (narrowing) Executes maintainer ruling **D** on objectui#8894 (decision batch objectstack-ai#119 item 4, 2026-09-12 「同意」) under the standing rule 「协议不正确的应该先修改协议。」 — judge the protocol wrong: a metric-family widget takes exactly one measure. The direction was not re-opened here. ## What changed `DashboardWidgetSchema.values` was `z.array(z.string()).min(1)` with **no upper bound on any widget type**, so a `metric` tile could declare three measures; the dataset query selected and computed all three and the tile rendered `values[0]`. The other two were queried and dropped on the floor (objectui#7293 defect 1). objectui#8887's sub-caption made the tile honest about dropping them; it did not make the document legal. - `checkDashboardWidgetMetricMeasureArity` — a new exported object-level check, chained onto the same door by identifier, refusing more than one measure on `metric` / `kpi` / `gauge` / `solid-gauge` / `bullet` and on a widget that declares no `type` (it defaults to `metric`, and the message says so rather than claiming the author wrote it). One `custom` issue at `values`, naming the widget's `id`, the count, and the authored `type`, and prescribing one measure per tile — "make N tiles for N measures" — plus the visuals that DO render several numbers. - **Exactly one is a conjunction**: the field's own `.min(1)` still owns the empty array (`too_small`, unchanged, and the new check deliberately adds no second issue there); the new check owns the upper bound. - `.changeset/17779-...` — `minor`, **BREAKING** banner, ADR-0087 disposition `registered dashboard-widget-metric-family-multi-measure-refused`. - `packages/spec/src/migrations/entries/semantic/18.dashboard-widget-metric-family-multi-measure-refused.ts` — one new entry file, plus the `gen:migration-registry` lap. No other file in that directory was touched and nothing was hand-edited inside the generated regions of `registry.ts`. - The `values` doc string now states the arity rule it enforces, so the generated reference page stops saying only "at least one". ## The three questions the dispatch asked, answered by measurement ### 1. `superRefine`, not a per-type union arm — because a union destroys every other diagnostic on this door Eight widget bodies through `z.union([metricArm, otherArm])` versus one more `.superRefine` on the strict object, measured on this tree: | body | union arms | the spelling shipped | |---|---|---| | `bogusProp` on a widget | `(root) invalid_union: Invalid input` | the strict-object refusal, naming the key + the history sentence | | `categoryField` / `valueField` | `(root) invalid_union: Invalid input` | the `WIDGET_GUIDANCE_SETS` ADR-0021 prescription | | `titel` | `(root) invalid_union: Invalid input` | "Did you mean `titel` → `title`?" | | `type: 'ziggurat'` | `(root) invalid_union: Invalid input` | `invalid_value` at `type`, listing all twenty | | `metric` + 3 measures | `too_big` at `values` | the curated `custom` refusal at `values` | Four of eight bodies lose their whole diagnostic to one bare `Invalid input`. That is not a new observation on this file: the `compareTo` docblock already records it for the same reason (objectstack-ai#5014 — "a union collapses into one bare `Invalid input` on the wire … A plain strict object's errors reach the author"), and `view-union-diagnostics.test.ts` is the entire apparatus objectui needed **because** `ViewMetadataSchema` is a union. A second union here would commission that apparatus again to buy a refusal the object-level form gives for free. Second datum, measured: zod 4.4.3 throws `Cannot overwrite keys on object schemas containing refinements. Use .safeExtend() instead` on a plain `.extend()` that redeclares a key, so the arms cannot even be built from the existing door without `.safeExtend()` or a duplicated declaration. ### 2. `major` does collide with `check-changeset-no-major` — so the changeset is `minor` The guard is **armed**: there is no `.changeset/pre.json`, so the RC exemption does not apply, and the only other route is the `allow-major` PR label whose own error text says "a whole-stack major release is genuinely intended" — false for this PR. Its header states the convention: every publishable package is in the Changesets `fixed` group, so one `major` promotes all ~70 packages; during the launch window a breaking change ships `minor` and **breaking-ness is carried by the BREAKING banner plus the ADR-0087 disposition, not by the bump level**. `pr-automation.yml`'s "WHICH LEVEL" prose says the same in the place the author reads it. So the card's "major changeset" is satisfied as `minor` + `**BREAKING**` + `registered ...`, and `check-adr-0087-registration --base origin/main` reads the changeset back as `[BREAKING+bang+clause-②-narrowing] registered dashboard-widget-metric-family-multi-measure-refused`. ### 3. The migration entry's acceptance criteria, re-derived from what the code refuses Not a restatement of the card. Two things the card's wording implies that the machinery does **not** do, both measured and both written into the entry: - **The TODO cannot name your dropped measures.** `applyMetaMigrations` maps `step.semantic` straight onto the result (`chain.ts`) with no per-document interpolation and no filtering by whether the stack even carries the shape, and `SemanticMigration` has only static string fields. `os migrate meta` therefore prints the entry's prose, not a list. The **refusal** is what names them, per widget, on the re-parse — so the entry tells the author to drive the fix off `os build`, not off the migrate output. - **Splitting into N tiles is not attempted**, as the card says — and the entry states why in the registry's own terms: N tiles need N ids and N boxes on a 12-column grid, which is a layout fact about a dashboard the registry has never seen. The rest of `acceptanceCriteria` is the measured accept/refuse matrix: which door refuses (publish, not objectui's `.shape`-mirror editor), the empty-array carve-out, the aborting `invalid_value` on an unknown `type`, the un-reachable "does this measure exist in the dataset", and the fact that `.omit()` / `.pick()` / `.partial()` already threw before this change. ## Controls **LIT** — a legal single-measure metric tile parses **identically before and after**, and the non-metric families are untouched. Sixteen bodies through `DashboardWidgetSchema.safeParse`, before and after the change: | body | before | after | |---|---|---| | `metric` + 1 measure | ACCEPT, `values: ["amount_sum"]` | ACCEPT, `values: ["amount_sum"]` | | `metric` / `kpi` / `gauge` / `solid-gauge` / `bullet` + 2–3 measures | ACCEPT (all five) | REFUSE `values:custom` (all five) | | no `type` + 3 measures | ACCEPT, `type: "metric"` | REFUSE `values:custom` | | `bar` / `line` / `table` / `pivot` / `funnel` + 3 measures | ACCEPT | ACCEPT (unchanged) | | `metric` + `values: []` | REFUSE `values:too_small` | REFUSE `values:too_small` (one issue, not two) | | `type: 'ziggurat'` + 3 measures | REFUSE `type:invalid_value` | REFUSE `type:invalid_value` (alone) | | `metric` + 3 measures + `bogusProp` | REFUSE `unrecognized_keys` | REFUSE `unrecognized_keys` | The whole taxonomy is covered by a pin that asserts the metric family plus the fifteen others **is** `ChartTypeSchema.options`, so a new chart type cannot land uncovered by either list. **DARK** — things that must read **0**, with paths and counts: - `.min(1)` **array** keys in `packages/spec/src/ui/dashboard.zod.ts` other than `values`: **0**. The file has exactly two `.min(1)` code sites at the branch point — `values` (line 706) and `dashboard.columns` (line 1151, `z.number().int().min(1).max(24)`, a number bound, not an array). The latter is byte-identical after the change; every other new `.min(1)` occurrence in the file is inside a docblock. - `ReportSchema.values` (`packages/spec/src/ui/report.zod.ts`, lines 237 and 314) is a separate declaration, `optional()`, with no `.min(1)` and no arity check, and its `type` enum (`tabular` / `summary` / `matrix` / `joined`) contains **0** metric-family members. Untouched, and not the same defect. - Fleet census over every tracked `.ts` / `.tsx` / `.json` / `.mdx` / `.md` / `.yaml` **at the branch point** `72dd95fa5a`: **187** brace-local literals carrying a `values: [...]`, **39** of them on a metric-family `type` (both lit controls), and **0** of those carrying more than one measure. Nothing in the monorepo moves. On this branch the same scan reads 205 / 49 / **7**, and all seven are the fixtures this PR added. - `check:authorable-surface` is green with no regeneration: **0** authorable keys move. `check:api-surface` reports `0 breaking (removed/narrowed), 1 added` — the new exported check. ## Verification Red before green, with the mutation proved on disk and the restore hash-verified: ``` HEAD blob : 30c6d78 worktree blob : 30c6d78 (at HEAD before the mutation) anchor occurrences BEFORE: 1 AFTER: 0 injected line: 1 mutated blob : 90548649227c3971f16b7dc85b02e1bab8155f96 (differs -> the edit really landed) RED vitest exit=1 17 failed | 205 passed (222) restored blob : 30c6d78 git diff HEAD on the path: empty GREEN vitest exit=0 222 passed (222) ``` The mutation removed only the `.superRefine(checkDashboardWidgetMetricMeasureArity)` attachment, leaving the function declared — so the 17 reds are the door's behaviour, not a compile failure. The script carried a `trap ... EXIT INT TERM` restore against an absolute `git rev-parse --show-toplevel` path, restored with `git checkout HEAD -- path` (never a bare `git checkout --`), and proved the restore by blob hash **and** an empty `git diff HEAD`. - `pnpm --filter @objectstack/spec test` — **486 files / 13933 tests passed**, exit 0. - `pnpm --filter @objectstack/spec typecheck` — exit 0 (`check:scripts-typecheck` and `check:test-typecheck` included; the test-layer ledger held at 54 files / 259 errors / 144 pinned signatures, shrink-only). - `pnpm --filter @objectstack/spec check:generated` — **all 15 generated artifacts up to date**, exit 0, after regenerating exactly the three it proved stale (`api-surface/`, `export-origins/`, `content/docs/references/**`). - Changeset gates: `check-adr-0087-registration --base origin/main` exit 0 (+ `--self-test`, 384 assertions), `check-changeset-no-major --base origin/main` exit 0, `check-empty-changeset --base origin/main` exit 0. - `pnpm check:nul-bytes` exit 0 (8812 text files, no raw control bytes), plus `check:widget-option-census`, `check:liveness`, `check:exported-any`, `check:dual-source-exports`, `check:entry-nameability`, `check:empty-state`, `check:cross-package-test-inputs`, `check:test-source-alias`, `check:type-check-coverage`, `check:merge-driver`, `check:pm-widening-tells`, `check:spec-docblock-symbol-anchors`, `check:dts-closure`, `check:published-files`, `check:spec-parsed-alias`, `check:page-declaration-shape`, `check:corpus-claim-drift`, `check:skill-examples`, `check:docs-transcript-drift`, `check:doc-formula-expressions`, `check:variant-docs`, `check:llms-txt`, `check:yaml-examples`, `check:objectui-pin-citations`, and the ten doc gates the regenerated `.mdx` newly derives — every one exit 0. - **Repo-wide lint, not a narrowing**: `node --stack-size=4000 node_modules/eslint/bin/eslint.js . --no-inline-config --format json` at `ea17ab8491`, 81s — **6822 files linted, 0 errors, 0 warnings**, exit 0. ## Migration-entry adjacency — checked, not assumed `packages/spec/src/migrations/entries/` is one file per entry and the entries README records the measured objectstack-ai#8344 table: two in-flight registrations merge clean **unless** their ids are adjacent in sort order or both are the first entry of a new major. Enumerated the `18.*` semantic directory and every open PR's file list on 2026-09-17: - In-flight ADDED semantic registrations: `ui-list-view-groupbyfield-padded-refused` (objectstack-ai#18695), `structured-region-body-pause-and-end-refused` (objectstack-ai#18688), `evaluated-expression-slots-source-required` (objectstack-ai#18638), `manifest-id-reverse-domain-required` (objectstack-ai#18319). (objectstack-ai#18420 modifies an existing entry, which is not an insertion.) - This entry's immediate neighbours in the sorted set are `dashboard-header-modal-target-page-only` and `dashboard-widget-stage-order-non-funnel-refused` — **both already landed on `main`**, neither in flight — and it is not the first entry of major 18. Neither ejection row applies. The seat's expectation about objectstack-ai#18695 held, and was verified rather than assumed. The two projections the README names came back **byte-identical, and that is correct rather than a skipped step**: `build-spec-changes.ts` and `build-upgrade-guide.ts` both loop `for (major = MIGRATION_SUPPORT_FLOOR + 1; major <= PROTOCOL_MAJOR; major++)`, and `PROTOCOL_MAJOR` is **17** while this entry registers under **18**. Both were regenerated anyway and `check:spec-changes` / `check:upgrade-guide` are green. ## Acceptance notes Noted, not filed — neither is a reproducible defect, a contract violation, or a metadata-authoring trap: - **zod 4.4.3 refuses `.extend()` that overwrites a key on a refined object** ("Use `.safeExtend()` instead"), measured here while probing the union spelling. It is a trap for the next author who mirrors or re-arms this door — recorded in the new check's docblock and in the migration entry, which is where that author looks. Successor: whoever lands objectui#8894's half, which must re-attach this export onto a `.shape` mirror. - **An ADR-0087 semantic entry cannot name per-document values.** `applyMetaMigrations` emits `step.semantic` unconditionally and `SemanticMigration` carries only static strings, so a card instruction of the form "emit a structured TODO naming X" is unsatisfiable as literally written — the refusal message is the only per-document channel. Recorded in this entry's `acceptanceCriteria`. Successor: the next card that writes that instruction. ## Downstream, not in this PR Card item 3 (objectui's contract twins gain the refusal pin; the runtime warning becomes the door refusal) is the objectui half and objectui#8894 is `pm:blocked` on this card. Nothing in `../objectui` was touched. Until that package imports and chains `checkDashboardWidgetMetricMeasureArity`, its `.shape`-mirror editor keeps accepting three measures on a `metric` and the author meets this refusal at publish — stated in the check's docblock and in the migration entry rather than left implied. --- _Generated by [Claude Code](https://claude.ai/code/session_01JbZnqu8bt6YqfJsr9vaFb3)_ --- > ⏱️ **席位代改正文(dev 只写一次,⛔ 不 PATCH 正文;事后要改的由本席代写)。** 两处: > > - **`applyMigrationChain` → `applyMetaMigrations`(2 处)** —— 前者在树上**不存在**; > 真函数是 `packages/spec/src/migrations/chain.ts:68`,CLI 调它,根 api-surface 导出它。 > 同一处错名也写进了 ADR-0087 语义条目、并经 `gen:migration-registry` 复制进 > `registry.ts:6765` —— 那段文本会被 `os migrate meta` 在协议 18 打印出来, > 所以读者照着 grep 会一无所获。已随 `3b15ca1254` 修正(条目 + 重生成,⛔ 未手改 registry.ts)。 > ⭐ 这一条由**达档隔离契约复核**判出(记录见下方 PASS/FAIL 评论),⛔ 不是本席自己看出来的。 > - **`Clause-②: no (narrowing)` → `yes (widening)`** —— 该行**只定路由**,⛔ 非终审: > 章程原文「只定是否必过席内契约复核的保守方向」,机械地板「新导出符号…恒 `yes`」。 > 本 diff 在 `api-surface/ui.json` 上**净增一个导出符号** > (`checkDashboardWidgetMetricMeasureArity`,+1 / 移除 0,本席对着 merge-base `72dd95fa5a` 实测), > ⇒ 地板落在 `yes`。认领侧早已是 `yes (widening)`,正文与 changeset 两个载体**落后于它**; > changeset 已随 `881db1280d` 对齐,并在行内写明两条轴(接受集**收窄**、公开面**扩大**), > 免得 CHANGELOG 读成「本改动放宽了行为」。 > >⚠️ **破坏性未受影响**:`check-adr-0087-registration` 仍读作 breaking, > 经 `**BREAKING**` 横幅与摘要里的 `!`;它失去的 `clause-②-narrowing` 信号从来不是唯一载体 > (实测 `[BREAKING+bang]`,exit 0)。 > > ⏱️ **再正一次(席位):`yes (widening)` → `yes (narrowing)`。** 上一版本席以为「收窄行为 + 扩大公开面」在这套两态词表里没有正确拼法,于是取了 `widening` 并写了一段话解释「它不是那个意思」。**那个前提是错的**:`readClause2Line` 认 `yes (narrowing)`,而`check-adr-0087-registration` 的自测逐字命名了这个形状 ——「the `narrowing` arm beside a `yes` value — a diff that **widens AND narrows**」。⇒ 值仍是 `yes`(机械地板:新导出符号),但**臂**改回 `narrowing`,`clause-②-narrowing` 信号随之回到 ADR-0087 门禁(实测 `[BREAKING+bang+clause-②-narrowing]`,exit 0)。⭐ 这一条由第二次达档复核在 ③ 里作为**边界旗标**提出,⛔ 不是 FAIL;本席自己验过词表才动手。⚠️ 顺带一提 `no (widening)` 读作 **malformed** —— 臂不是自由的:`no` 只配 `narrowing`,`yes` 两者皆可。 --- _Generated by [Claude Code](https://claude.ai/code)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
Fixes #17499
Clause-②: no
What changed
KanbanConfigSchema.groupByField(required),GanttConfigSchema.groupByFieldandTimelineConfigSchema.groupByFieldwere barez.string(), so' stage'was valid authored metadata. All three now carry the non-padded pattern this repo already chose for the same defect on the sibling axis —grouping.fields[].field, landed asf8e5790593(#17360 / PR #17498), which scoped this axis out by name.The refusal is addressed to the offending key (
kanban.groupByField,gantt.groupByField,timeline.groupByField), quotes the offending spelling so the invisible whitespace is visible, and carries the name to write instead.Reusing the precedent, and the three places it does not fit
GROUPING_FIELD_NON_PADDED_PATTERNtoNON_PADDED_FIELD_NAME_PATTERNand now serves both axes. No exported symbol moves (check:api-surfaceis clean with no regeneration)..trim()producer would be the author's only feedback channel and it would say nothing. The refusal is sharper here, not milder..trim()— the precedent's stated reason, re-checked and unchanged: a trimming schema makes' stage'and'stage'silently equivalent, the consumer-tolerance direction AGENTS.md #0.1 refuses. None of these three rows honestly wants the other answer, and the required key is the strongest case against it.grouping.fields[].fieldand explicitly not about this axis, so quoting it here would attribute a decision nobody made. The code comment says so.ADR-0087 disposition:
registered, and the entry is in this PRThe changeset carries an
adr-0087: registered ui-list-view-groupbyfield-padded-refusedmarker (the HTML-comment form the gate reads), andpackages/spec/src/migrations/entries/semantic/18.ui-list-view-groupbyfield-padded-refused.tsregisters it under protocol major 18, with the regeneration lap committed beside it.Why
registeredand not anot-requiredcategory, measured against the gate's closed vocabulary:unpublished@objectstack/specpublishes to npm.already-registeredgrouping.fields[].fieldand says in its own text that thegroupByFieldaxis is not touched by it.no-migration-prescriptionruntime-interface-onlytype-surface-onlyThe same-defect precedent took
registeredwith a new semantic entry, for the same stored-metadata reason.The regeneration lap, and why two of its three files show no diff.
pnpm --filter @objectstack/spec gen:migration-registryinserted the entry intoregistry.ts's generated region (56 lines, insertion-only, comment run carried through; nothing hand-edited inside the markers).gen:spec-changesandgen:upgrade-guideboth ran and wrote byte-identical files: each projects majors up toPROTOCOL_VERSION, which is17.0.0, and this entry registers under 18. The control is the landed siblingf8e5790593— its file list touched neither projection either.check:migration-registry,check:spec-changesandcheck:upgrade-guideare all green.Evidence
Red before green — ablation, on-disk proof, hash-verified restore. The fix was committed first; the mutation removed the three
superRefinecalls frompackages/spec/src/ui/view.zod.ts(the subject resolves through the same-package relative import./view.zod, so there is nodistleg to prove — the test never resolves throughexports).The direction was predicted before the run and observed as predicted: turning red, 24 of the 64 arms. The 40 that stayed green are the LIT and no-trim arms, which is what makes the 24 a reading rather than a restatement.
⭐ LIT — the narrowing does not over-reach. Every distinct
groupByFieldspelling this repo carries still parses, on all three schemas: harvested across every.ts/.tsx/.mdx/.json/.mjsoutsidenode_modules, 14 distinct literals, zero of them padded.owner.nameis the load-bearing member and is pinned — these keys hold a field reference, not a machine name, which is why the snake_case grammar/^[a-z_][a-z0-9_]*$/is the wrong vocabulary (packages/lint'svalidate-list-view-field-refs.test.tscarrieskanban: { groupByField: 'owner.name' }in a case asserting no findings; that suite is green here, 137 tests). Three of the 14 —'warning','error', a bracketedselect_or_status_fieldplaceholder — are a severity-map value and prose in a completeness hint, so they are not authored names and are not pinned.⭐ DARK — three readings that must be 0, before and after.
z.string()keys inview.zod.tsreached by this changegroupByFielddeclarations;git diffshows 3 changed property sites and no other schema key. A per-schema test probes every sibling string key (summarizeField,titleField,startDateField,endDateField,progressField,colorField) with a padded value and requires it to still parse.authorable-surface/rows movedcheck:authorable-surfacegreen with no regeneration; a value-level refusal adds no key.api-surface/entries movedcheck:api-surfacegreen with no regeneration; the published type surface is byte-unchanged.Which gate can actually go red on this — measured, not guessed.
check:docsreads these threedescribe()strings: it exited 1 namingcontent/docs/references/ui/view.mdxandcontent/docs/references/ui/component.mdxbeforegen:schema && gen:docs, and 0 after.check:adr-0087-registrationreads the changeset and exited 1 before the ledger entry landed, naming exactly the missing id, and 0 after it — red before green on that gate too. The remaining 13 of the 15 spec artifact gates were green throughout — they structurally cannot see a value-level refine, and are recorded as DARK rather than claimed as coverage.Commands run. The schema and test blobs have not moved since the first lap (
view.zod.tsis6ff7ba320a8fa445fa34e421b39716359194bc1bat both heads and on disk,view.test.tsis162772c6…), so the ablation above stands unrepeated; every other reading below was re-taken atca0dd6ce0eafter the ledger entry landed:pnpm --filter @objectstack/spec testpnpm --filter @objectstack/spec typecheckpnpm --filter @objectstack/spec check:generatedpnpm --filter @objectstack/lint exec vitest run src/validate-list-view-field-refs.test.ts src/runtime-gate.view-writes.test.tspnpm exec eslint . --no-inline-configca0dd6ce0e, no narrowing claimed; both files this lap added or changed are provably in the runpnpm check:nul-bytespnpm check:empty-changeset,check-changeset-no-major.mjscheck-adr-0087-registration.mjs1 declared-breaking changeset(s), each carrying an ADR-0087 disposition … registered ui-list-view-groupbyfield-padded-refused (new here)Acceptance notes
''would be a second, undeclared narrowing riding on it. On the required kanban key a blank name is a blank board, refused one layer down by the field-reference rules, not by this pattern.packages/i18ncarriesgroupByField: 'Group by field'/'分组字段'. Those are i18n label entries keyed by the same name, not values of this spec key, so they are neither affected nor evidence about accepted values. Recorded because a reader harvesting the sibling repo will hit them.packages/spec/src/kernel/functional-completeness.tsdocuments the kanban fallback chain asgroupBy = groupByField || groupField || ..., andgroupFieldis a live legacy alias declared in objectui, not in this repo. Nothing here is wrong; it is the context a future reader of this key needs. Carrier: whoever next touches the kanban completeness hint.Authored by the
domain:specexecution seat, sessionsession_01JbZnqu8bt6YqfJsr9vaFb3.Generated by Claude Code