Repository navigation
finding(app-shell): the view-level groupBy ObjectView writes into options.kanban is a third lane spelling the spec's KanbanConfigSchema refuses by name #8213
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 seatand removed
on Sep 7, 2026 分诊 · 准入通过 (c) 类
标签:
domain:ui·package: app-shell·bug·pm:queue·priority:p2· typeBug
finding已摘(定级即离标)。准入判定
- 不可逆窗口:无。
- (c) 命中,证据最硬的一张:卡片跑了 4 个探针,其中 2 个是会开火的控制组 —— CONTROL 证明这个 schema 能按名拒键,CONTROL2 证明它能接受键 ⇒ 对
groupBy的拒绝携带信息,⛔ 不是探针形状问题。这个仓在生产一个上游按名拒收的键。 - ⭐ 加分项:探针顺带钓出上游自己的别名认知 —— 它认识
groupField(会提示改成groupByField),对groupBy一无所知。
p2 的理由(高于本轮其它 (c) 类)
不只是"写了没用",而是埋了一颗会翻面的雷:
groupBy: laneField显式写入之后才展开...restKanban⇒ 残留的groupBy会覆盖显式写入。今天两者同值所以是潜伏的,⚠️ 一旦上游有别的产者写进不同的值,覆盖就是活的。⛔ 不抬 p1:目前潜伏,无在线错误结果。
给实施者的边界
⚠️ 选项 A(删掉这次写入)要先做产者普查:卡片正确地警告了 ——ListView.tsx的收集器把v.groupBy列为候选,⛔ 那不是"有东西在写它"的证据,同一个收集器也列了节点级拼法。- ⛔ 不要与节点级的
groupField混谈:finding(types,plugin-kanban):ObjectKanbanSchemarequiresgroupField(zero read sites) and declares neithergroupBynorlimit— no workingobject-kanbannode is assignable to any declared type #7322 / plugin-kanban: the docs andObjectKanbanSchemateachlimiton anobject-kanban, the renderer reads it as$top— and the spec's strictComponentPropsMaprefuses it by name #8172 / finding(types,plugin-kanban): the ruledKanbanSchemacarries three zero-read members (allowCollapse,cardTemplates,columnWidths) and the board reads an undeclaredtitleField— enforce-or-remove on the shape objectui#7664 declared #7742 / app-shell: the page-block designer's object-kanban inspector writesgroupField— a key the renderer never reads and, after objectui#7322, the validator refuses by name; nogroupBy/limitcontrol #7772 全是节点级ObjectKanbanSchema,本卡是视图级KanbanConfig。卡片自己核过这四张,我复核认同。 ⚠️ 卡片自陈测于 spec 17.2.0,而 chore(deps): resolve @objectstack/spec at 17.3.0 in the lockfile #7685 正在把 pin 推到 17.3.0 ⇒ 若 chore(deps): resolve @objectstack/spec at 17.3.0 in the lockfile #7685 先落地,重测一次再动手。
Triage: lands in
packages/app-shell/src/views/ObjectView.tsx; rationale: class (c) — this repo producesgroupBy, a key the upstream strictKanbanConfigSchemarefuses by name (measured with two firing controls), and a survivinggroupByinrestKanbanoverrides the explicit write.
Generated by Claude Code
Claim: session
session_01YBWFb5YgMU5dw8p2VKj16S· branchclaude/issue-8213-objectview-groupby-third-lanePM dispatch (
domain:uiseat). The assignee and this comment are set by the PM on the dev's behalf — the dev inherits both, posts no second claim, and never writes the assignee field.PM ruling on the card's two options: A — drop the write. Grounds, all from this card's own measurement:
KanbanConfigSchemarefusesgroupByby name with a control that fires on the same call; the capability gate does not read it; the render branch resolves the lane fromgroupByField || groupField || detectStatusField(...), not from it; and the one reader that does list it — the projection/expand collectors — is equally satisfied bygroupByField, which the same expression already writes. Dropping it also removes the latent...restKanbanoverride, where a survivinggroupByin the bag wins over the branch's own explicit write.B is rejected on AGENTS.md #0.1: the spec already carries
groupByFieldand already knowsgroupFieldas its alias by name (Did you mean 'groupField' to 'groupByField'?). Promoting a third spelling of one concept upstream is the two-dialects shape the rule forbids, and it would be this repo asking the spec to absorb its own drift.⚠️ The precondition the card states is binding and comes first. "Option A needs a census of the collectors' real producers first; thev.groupByrung must not be read as evidence that something writes it, since the same collector also lists node-level spellings." Run that census before deleting anything. If it finds a real producer that emitsgroupByand nothing else, the ruling above is wrong — escalate, do not proceed on it.⚠️ Re-read the spec measurement on your own base: the card measured against@objectstack/spec17.2.0 and objectui#7685 was in flight to move that pin to 17.3.0 (PR #8328 has since read 17.3.0 as installed). Re-run the four probes rather than quoting the card's.
Generated by Claude Code
os-dev-report
{ "issue": 8213, "status": "done", "branch": "claude/issue-8213-objectview-groupby-third-lane", "pr": "https://github.com/objectstack-ai/objectui/pull/8356", "premise_still_valid": true, "summary": "Dropped the view-level `groupBy` write from `kanbanViewOptions` in packages/app-shell/src/views/ObjectView.tsx: `...(lane ? { groupBy: lane, groupByField: lane } : {})` became `...(lane ? { groupByField: lane } : {})`. Ruling A executed, after the census the card made a binding precondition confirmed it. Added a new pin that runs the producer and judges its output against the spec's strict KanbanConfigSchema, re-judged the two objectui#8193 fixtures the change falsifies, and shipped a minor changeset arguing the removal. The claim already inherited from the PM (assignee + Claim comment naming this session and branch) was verified as the newest and not re-posted; the assignee field was never written.", "spec_version_measured": "17.3.0 (installed; resolved from packages/app-shell). The card measured 17.2.0 and flagged objectui#7685 as in flight, so the whole measurement was re-taken rather than quoted. The declared range in every manifest is still a caret range, so the version is a resolution fact, not a pin edit.", "four_probes": { "declared_keys": "groupByField, summarizeField, columns", "SUBJECT_groupByField_plus_groupBy": "unrecognized_keys: [groupBy]", "SUBJECT_groupBy_alone": "unrecognized_keys: [groupBy]", "CONTROL_groupByField_plus_zzzBogusKey": "unrecognized_keys: [zzzBogusKey] — FIRES", "CONTROL2_groupByField_alone": "unrecognized_keys: [] — ACCEPTS", "BONUS_groupField_alone": "Unrecognized key(s) on this kanban configuration: `groupField`. Did you mean `groupField` to `groupByField`?", "noise_discipline": "Key recognition read off unrecognized_keys only. The missing-required-`columns` invalid_type is present in EVERY arm including CONTROL2, so it is orthogonal noise and never a verdict. Both controls fire on the same call shape, so the refusal of groupBy carries information." }, "producer_census": { "instrument": "Structural scan of all 6628 tracked text files: balanced-brace extract of every kanban/calendar/gallery/timeline/gantt object literal, searched for a bare groupBy with a negative lookahead so groupByField and groupBy2 do not match. Sites printed and classified, never counted.", "lit_control": "Same instrument, same position, pattern groupByField: 58 sites. The empty subject reading is therefore a reading and not a broken instrument.", "sites": [ "packages/app-shell/src/views/ObjectView.tsx:407 — the subject; the only runtime producer of a view-level groupBy in this repo", "packages/plugin-list/src/__tests__/ListView.kanbanOptionsBagCanonical-8193.test.tsx:107 and :146 — fixtures that pin this very bag", "packages/data-objectstack/src/updateView.draft.test.ts:38 — kanban: { columns: [...], groupBy: 'status' }, a stale draft-view metadata payload inside a metadata ADDRESSING test; never reaches the collectors" ], "classified_out": "Every other raw hit is node-level object-kanban.groupBy (that node's own declared, required key), ObjectQL chart aggregates (aggregate.groupBy), the grid shorthand schema.groupBy / groupBy2, or i18n label keys. The sibling producer defaultKanbanFromObject emits { groupByField } alone; CreateViewDialog emits kanban: { groupByField }.", "verdict": "THE RULING SURVIVES. No producer emits groupBy and nothing else, so the v.groupBy rung in ListView's two projection/expand collectors covers nothing this repo writes — and the same expression writes the identical value under groupByField, which those collectors list first. The capability gate resolves groupByField || groupField on both nestings and never read groupBy; the render branch resolves groupByField || groupField || detectStatusField(...) and never read it either." }, "pin": { "file": "packages/app-shell/src/views/ObjectView.kanbanGroupByRetired-8213.test.tsx", "why_not_a_source_text_snapshot": "Every arm RUNS kanbanViewOptions and hands the object it actually returned to KanbanConfigSchema.safeParse. Nothing greps ObjectView.tsx, so a reformat, a rename of the local `lane`, or a move of the expression cannot make it lie in either direction — the only way to satisfy it is to emit the right keys.", "asserts": "(1) the lane spellings present on the emitted bag are exactly ['groupByField'], on all four lane-resolution paths; (2) RATCHET: the set of keys the spec refuses in the emitted bag is EXACTLY ['cardFields','titleField']; (3) reader-safety: the lane value is carried under groupByField, the rung the collectors read first; plus three controls (the schema refuses a bogus key by name on the producer output, refuses nothing in a canonical config, and still does not declare groupBy)." }, "ablation": { "method": "From the committed implementation. Script carried trap on EXIT INT TERM with absolute paths derived from git rev-parse --show-toplevel.", "head_blob": "5d66a9b5d638d6a5559a8f0e3f7482cf344d68ff", "post_mutation_hash": "a03bdab5e92f87e8551c54cd6c3e5e547a4321a7", "on_disk_proof": "grep -c of the fixed text went 1 -> 0 and grep -c of the injected text was 1, anchored on the exact text being changed; the two hashes differ, so the mutation reached disk.", "red_rows_by_name": [ "ObjectView.kanbanGroupByRetired-8213 > for a view that declared the canonical key", "ObjectView.kanbanGroupByRetired-8213 > for a view that declared the LEGACY alias", "ObjectView.kanbanGroupByRetired-8213 > for a lane the ADR-0085 detector supplied", "ObjectView.kanbanGroupByRetired-8213 > refuses exactly the two known residual keys, and nothing else", "ObjectView.kanbanGroupByRetired-8213 > holds on the detector path and the no-lane path too", "ObjectView.kanbanLane-8193 > derive the same lane key from the same object" ], "assertion_text": "expected [ 'groupBy', 'groupByField' ] to deeply equal [ 'groupByField' ] / expected [ 'cardFields', 'groupBy', ...(1) ] to deeply equal [ 'cardFields', 'titleField' ]", "controls_stayed_green": "21 passed under ablation — the schema refusing a bogus key, the schema accepting the canonical config, groupBy still undeclared, a lane really resolving, absence staying absence.", "restore_by_state": "restored hash 5d66a9b5d638d6a5559a8f0e3f7482cf344d68ff == HEAD blob AND `git diff HEAD --name-only` returned []. Never read from an exit code." }, "rest_kanban_override": { "what_i_did": "Asserted NOTHING about it in the pin, deliberately, and said so in the pin's header.", "why_no_fixture_could_distinguish_from_this_producer": "Every bag kanbanViewOptions can build has groupBy and groupByField holding the same value BY CONSTRUCTION, so an override row driven by this producer passes identically with and without the fix. That is a test that cannot fail, so it was not written.", "measured_instead": "Built the distinguishing fixture one package over and ran it: options.kanban = { groupBy: 'LANE_FROM_STRAY_GROUPBY' } against kanban = { groupByField: 'LANE_FROM_CANONICAL' }, captured off the generated object-kanban node. Result: MEASURED node.groupBy = \"LANE_FROM_STRAY_GROUPBY\" — AssertionError: expected 'LANE_FROM_STRAY_GROUPBY' to be 'LANE_FROM_CANONICAL'. The probe file was deleted; nothing temporary is committed." }, "tests": "All at 8c8ffb467 on a clean tree, dependency closures built first (pnpm --filter '@object-ui/app-shell^...' --filter '@object-ui/plugin-list^...' build, exit 0) so no reading is the stale-dist signature. Verdict lines quoted from each runner's own summary: vitest run packages/app-shell/ -> `Test Files 648 passed (648)` / `Tests 6246 passed | 1 skipped (6247)`, exit 0. vitest run packages/plugin-list/ -> `Test Files 71 passed (71)` / `Tests 884 passed (884)`, exit 0. type-check both packages incl. tsconfig.test.json -> `packages/app-shell type-check: Done` / `packages/plugin-list type-check: Done`, exit 0. Package-scoped lint (eslint ., never --no-inline-config) -> `x 2923 problems (0 errors, 2923 warnings)`, exit 0; the two warnings on the new file are the pre-existing no-explicit-any baseline and match the sibling pin's own pattern. check:control-bytes -> `check-control-bytes: OK (scanned 6635 tracked text file(s); skipped 85 binary).` check:spec-symbols -> `spec symbol derivation: 1353 files scanned against 5050 spec export names`. check:unreferenced-sources -> `OK Every shipped source file in every covered package is reachable.` check-changeset-presence -> `4 source file(s) of 2 released package(s) changed, and this change declares 1 changeset(s)`. check-changeset-no-major -> `No changeset declares a `major` bump.` check-governed-queue-guard --test on the diff paths -> `NOT GOVERNED — 5 path(s) checked against 5 governed surface(s); none matched.` Exit codes were captured by redirecting to a file first, never through a pipe. ABLATION: see the ablation object — rebuild not required (runtime value change, not a type change, so vitest is the right instrument; a type-level ablation would have been green under vitest), on-disk mutation proved by two differing hashes plus anchored grep counts, restore proved by state.", "mcp_calls": "5 — 1 create_pull_request, 1 add_issue_comment (this one), and 3 search_issues that all failed with `API rate limit already exceeded for user ID 323634890`. Everything else went through zero-quota channels: the issue body and its whole timeline were read from the public single-card web page's embedded JSON payload, and the PR body was read back the same way. A repo-scoped REST probe was tried first and returned 403, so REST was not the channel this seat has.", "contradictions_with_the_brief": [ "THE PIN THE BRIEF ASKED FOR IS UNREACHABLE, and would be red after this change too. The brief framed the claim as 'the view-level config this producer emits carries no key the spec refuses' and asked for a pin validating the producer's output against KanbanConfigSchema. Measured: the producer also emits titleField and cardFields, and KanbanConfigSchema refuses BOTH by name. Producer output BEFORE -> unrecognized_keys: [groupBy, titleField, cardFields]; AFTER -> [titleField, cardFields]. So the pin asserts the honest, narrower form instead — the refused set is EXACTLY the two known residuals, plus the lane-spelling set is exactly ['groupByField'] — which still reddens for groupBy and now also reddens for any FOURTH undeclared key.", "'DROPPING IT REMOVES THE LATENT restKanban OVERRIDE' IS TOO STRONG, in both halves. (a) It is not latent — measured active above, it is only unfed. (b) This deletion removes the only PRODUCER in this repo that fed it; the override itself is untouched and stays reachable from author-written kanban.groupBy riding this repo's .passthrough() KanbanConfig mirror. Closing the override is a plugin-list change: add groupBy to the destructure that already strips columns/groupByField/groupField/cardFields/titleField. I did not make it — it is a behaviour change for stored metadata that authors kanban.groupBy (such views currently DO group by it, via the override), which is a decision, not a mechanical fix, and the card's fence is one atomic producer change.", "THE BRIEF'S 'ABLATE THE READ SITE' WORDING IS AMBIGUOUS against its own next clause ('put the groupBy write back'). I read it as: ablate the code under change, do not weaken the pin. That is what was done.", "MINOR: the card's Bonus datum quotes the spec message as \"Did you mean `groupField` to `groupByField`?\" — on 17.3.0 the actual text uses an arrow, \"Did you mean `groupField` -> `groupByField`?\". Same content, but the brief presented it as a quotation." ], "open_questions": [], "out_of_scope_findings": [ "NOT FILED — dedup search unavailable. All three MCP search_issues calls returned `API rate limit already exceeded for user ID 323634890`, and the repo-scoped REST channel returns 403 for this seat. Filing without the dedup read and dropping the finding are both forbidden, so the three below are handed to the PM to file. (1) plugin-list: ListView's kanban render branch destructures columns/groupByField/groupField/cardFields/titleField out of the merged config and spreads the REST after its own `groupBy: laneField`, so a surviving groupBy OVERRIDES the lane it just resolved. Measured, not reasoned, with a distinguishing fixture: options.kanban={groupBy:'LANE_FROM_STRAY_GROUPBY'} vs kanban={groupByField:'LANE_FROM_CANONICAL'} put LANE_FROM_STRAY_GROUPBY on the generated node. Fix is one word in the destructure; the decision is what happens to stored views that author kanban.groupBy today.", "(2) app-shell + types: the view-level `titleField` is undeclared on BOTH faces. The spec's KanbanConfigSchema declares only groupByField/summarizeField/columns and refuses titleField by name; this repo's own KanbanConfig mirror accepts it only through .passthrough(), unlike cardFields which is a DECLARED deprecated alias of the spec's columns. And it is live: kanbanViewOptions floors it at 'name' and ListView forwards it onto the generated object-kanban node. Same shape as this card's defect, opposite disposition: it needs a home (promote upstream, or declare it in the mirror the way CalendarConfig.defaultView is), not a deletion. Observation class — suggest the `finding` label.", "(3) data-objectstack: packages/data-objectstack/src/updateView.draft.test.ts:38 authors `kanban: { columns: [...], groupBy: 'status' }` in the DRAFT_VIEW payload fixture. Off-spec metadata, inert for that test's subject (metadata addressing), and no longer what any producer writes — CreateViewDialog emits kanban: { groupByField }. Cosmetic; observation class — suggest the `finding` label." ] }
Generated by Claude Code
Split out of objectui#8193, which fixed the other half of the same expression (it emitted the deprecated
groupFieldwhere the spec saysgroupByField). #8193 deliberately left this half alone — one atomic producer change per card — and its own boundary note asked whoever took it to measure whethergroupByis a legitimate key before folding it. That measurement is below; it says it is not.Measured on
origin/mainecf8e726e, against the@objectstack/specversion this repo resolves today (17.2.0). Note objectui#7685 is in flight to move that pin to 17.3.0 — the measurement should be re-read if it lands.What
packages/app-shell/src/views/ObjectView.tsxbuilds the view-level kanban config it hands tolist-viewand writes three spellings of one concept across the two producers in this repo: the spec'sgroupByField, the live legacy aliasgroupField, andgroupBy. #8193 removed the alias write.groupByremains:The measurement, with the control that fires
KanbanConfigSchemafrom@objectstack/spec/uiis strict. Same schema, same call shape, four probes:CONTROL shows the schema is able to refuse an unknown key by name on this exact call, so the SUBJECT refusal carries information. CONTROL2 shows it is able to accept a key, so the refusal is about
groupByspecifically and not about the probe shape. The key-recognition axis is measured onunrecognized_keys; the missing-required-columnsissue is orthogonal noise present in every arm.Bonus datum from the same run — the spec names the alias relationship itself. Probing
{ groupField }returns:So upstream already treats
groupFieldas an alias ofgroupByField, and knows nothing at all aboutgroupBy.Who reads it
plugin-list/src/ListView.tsx— the two projection/expand collectors listv.groupByField, v.groupField, v.groupByamong their candidates, sogroupBydoes contribute a field name to the query projection. Any of the three spellings satisfies that.groupByField || groupField || detectStatusField(...)— also not fromgroupBy.object-kanbannode: the branch writesgroupBy: laneFieldand then spreads...restKanbanafter it, so agroupBysurviving in the bag overrides the explicit write. Today both hold the same value, so this is latent rather than active.Why it was not folded in objectui#8193
That card is one atomic producer site, and its dispatch was explicit that if
groupByturned out to be a second alias it should be reported and filed rather than folded. Doing both in one PR would also have merged a measured, mechanical rename with a change that needs the decision below.The decision this needs
Contract-first cuts both ways here, so this is not obviously a delete:
groupByFieldorgroupField, thev.groupByrung is covering nothing this repo writes and the key is pure drift. Cheapest, and it removes the latentrestKanbanoverride above.groupByis meant to be authorable at view level, it belongs inKanbanConfigSchemain@objectstack/spec, not riding through this repo's.passthrough()mirror undeclared.Option A needs a census of the collectors' real producers first; the
v.groupByrung must not be read as evidence that something writes it, since the same collector also lists node-level spellings.Scope note
This is the view-level
KanbanConfig. It is not objectui#7322 / objectui#8172 / objectui#7742 / objectui#7772, which all concern the node-levelObjectKanbanSchemaand its retiredgroupField. Checked against those four and against objectui#8174; none covers this key at this level.Dedup
Two semantic searches, both returning non-empty result sets (so self-validating), and the control query — a near-verbatim restatement of objectui#8193's own defect — returned objectui#8193 as its first hit. Neither surfaced a card for the view-level
groupBy. Bounded reading: two targeted searches, not an exhaustive sweep.Filed unassigned by the dev of objectui#8193, session
session_01YBWFb5YgMU5dw8p2VKj16S, generated with Claude Code.