Skip to content

fix(app-shell): stop emitting the third kanban lane spelling groupBy - #8356

Merged
os-justin merged 1 commit into
mainfrom
claude/issue-8213-objectview-groupby-third-lane
Sep 7, 2026
Merged

os-justin merged 1 commit into
mainfrom
claude/issue-8213-objectview-groupby-third-lane

Conversation

@os-justin

Copy link
Copy Markdown
Collaborator

Fixes #8213

ObjectView.kanbanViewOptions wrote { groupBy: lane, groupByField: lane } into the view-level kanban config it hands to list-view. groupBy is a third spelling of one concept, and @objectstack/spec's strict KanbanConfigSchema refuses it by name. This drops the write. PM ruling on the card's two options: A.

The four probes, re-run on this branch's own base

Measured against the installed @objectstack/spec 17.3.0 (the card measured 17.2.0; objectui#7685 has since moved the pin, so the whole measurement was re-taken rather than quoted).

declared keys: groupByField, summarizeField, columns

SUBJECT  { groupByField, groupBy }     -> unrecognized_keys: ["groupBy"]
SUBJECT  { groupBy } alone             -> unrecognized_keys: ["groupBy"]
CONTROL  { groupByField, zzzBogusKey } -> unrecognized_keys: ["zzzBogusKey"]   (fires)
CONTROL2 { groupByField } alone        -> unrecognized_keys: []                (accepts)
BONUS    { groupField } alone          -> "Unrecognized key(s) ... `groupField`.
                                          Did you mean `groupField` -> `groupByField`?"

CONTROL fires and CONTROL2 accepts on the same call shape, so the refusal of groupBy carries information. Key recognition is read off unrecognized_keys only; the missing-required-columns invalid_type is present in every arm including CONTROL2 and is not a verdict. Upstream names the other alias by name and knows nothing about groupBy.

The producer census (the card's binding precondition), sites printed

Instrument: a structural scan of all 6,628 tracked text files — balanced-brace extract of every kanban/calendar/gallery/timeline/gantt object literal, searched for a bare groupBy (negative lookahead, so groupByField and groupBy2 do not match). Not a count: every hit printed and classified.

Lit control on the same instrument — groupByField in the same position: 58 sites. The empty reading below is therefore a reading.

Real view-level kanban bag hits, all of them:

site what it is
packages/app-shell/src/views/ObjectView.tsx:407 the subject — the only runtime producer
packages/plugin-list/src/__tests__/ListView.kanbanOptionsBagCanonical-8193.test.tsx:107,146 fixtures pinning this very bag
packages/data-objectstack/src/updateView.draft.test.ts:38 a stale metadata payload in an addressing test; never reaches the collectors

Everything else the raw grep returns is node-level object-kanban.groupBy (its own declared, required key), ObjectQL chart aggregates (aggregate.groupBy), the grid shorthand schema.groupBy, or i18n labels. The sibling producer defaultKanbanFromObject emits { groupByField } alone and always has; CreateViewDialog emits kanban: { groupByField }.

Verdict: the ruling survives the census. No producer emits groupBy and nothing else, so the v.groupBy rung in ListView's projection/expand collectors is covering nothing this repo writes — and the same expression writes the identical value under groupByField, which those collectors read first. The capability gate resolves groupByField || groupField on both nestings and never read it; the render branch resolves groupByField || groupField || detectStatusField(...) and never read it either.

What the pin asserts, and why it is not a source-text snapshot

packages/app-shell/src/views/ObjectView.kanbanGroupByRetired-8213.test.tsx. Every arm runs the producer and hands the returned object to KanbanConfigSchema. Nothing greps ObjectView.tsx, so a reformat or a rename of the local lane cannot make it lie in either direction.

⚠️ The claim had to be narrowed, and this is a correction against the brief. The brief asked for a pin asserting "the emitted config carries no key the spec refuses". That assertion is unreachable, and would be red after this change too: the producer also emits titleField and cardFields, and KanbanConfigSchema — which declares exactly groupByField / summarizeField / columns — refuses both of those by name as well:

producer output BEFORE -> unrecognized_keys: ["groupBy", "titleField", "cardFields"]
producer output AFTER  -> unrecognized_keys: ["titleField", "cardFields"]

So the pin asserts the honest form: the refused set is EXACTLY the two known residuals, plus, separately, that the lane keys present on the bag are exactly ['groupByField']. That reddens for groupBy and for any fourth undeclared key, and it stays green for a key that becomes properly declared. The two residuals are a different question and stay: cardFields is a declared deprecated alias of the spec's columns in this repo's own KanbanConfig mirror; titleField is live and undeclared anywhere (filed separately — see below).

The ...restKanban override: measured, and NOT pinned from this producer

The brief warned that a naive override pin cannot fail here. It is worse than that — no override pin can be driven from this producer: every bag kanbanViewOptions can build has groupBy and groupByField holding the same value by construction, so the two spellings are indistinguishable from this side in every world.

A distinguishing fixture exists only one package over, and I built and ran it rather than reasoning about it. options.kanban = { groupBy: 'LANE_FROM_STRAY_GROUPBY' } against kanban = { groupByField: 'LANE_FROM_CANONICAL' }, captured off the generated object-kanban node:

MEASURED node.groupBy = "LANE_FROM_STRAY_GROUPBY"
AssertionError: expected 'LANE_FROM_STRAY_GROUPBY' to be 'LANE_FROM_CANONICAL'

The override is real and active, not merely latent — it is latent only in the sense that nothing was feeding it a differing value. ⚠️ So one line of the ruling needs correcting: dropping this write removes the only producer that fed the override; it does not remove the override, which stays reachable from author-written kanban.groupBy riding this repo's .passthrough() mirror. Filed as a plugin-list card. The probe file was deleted; nothing temporary is committed.

Ablation — the read site, by state

Restored { groupBy: lane, groupByField: lane }, proved it reached disk, ran, restored by state. Script carried trap '<restore>' EXIT INT TERM with absolute paths, from the committed implementation.

HEAD blob         : 5d66a9b5d638d6a5559a8f0e3f7482cf344d68ff
post-mutation hash: a03bdab5e92f87e8551c54cd6c3e5e547a4321a7   (differs -> mutation on disk)
fixed-text grep -c : 1 -> 0        mutated-text grep -c : 1

Six rows red, 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

AssertionError: expected [ 'groupBy', 'groupByField' ] to deeply equal [ 'groupByField' ]
AssertionError: expected [ 'cardFields', 'groupBy', …(1) ] to deeply equal [ 'cardFields', 'titleField' ]

Every CONTROL stayed green in both worlds (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 proved by state, not by exit code:

restored hash : 5d66a9b5d638d6a5559a8f0e3f7482cf344d68ff   (== HEAD blob)
git diff HEAD --name-only: []

Fixture triage, not a spelling sweep

  • ObjectView.kanbanLane-8193.test.tsx — the MEASURED, NOT FOLDED arm asserted the producer still wrote groupBy. Replaced, not re-spelled: the spec-refusal half is still true and stays; the producer half moved to the new pin. Its anti-fork arm carried .filter((k) => k !== 'groupBy') before comparing the two producers — removed, which makes that arm stronger (it is one of the six rows the ablation reddens).
  • ListView.kanbanOptionsBagCanonical-8193.test.tsx — its fixture called itself "the producer's real output" with groupBy in it. That claim is now false. The producer claim moved to the real shape and the groupBy-bearing bag became an explicit CONTROL (stored metadata written before this change still carries the key, and the mirror is .passthrough()). Split into two it blocks — a second kanbanOffered in one test renders twice into the shared screen and makes every queryByRole ambiguous; that is a real red I hit and fixed, not a theoretical one.

Verification (all at 8c8ffb467, clean tree)

run verdict line, quoted from the runner
vitest run packages/app-shell/ Test Files 648 passed (648) / `Tests 6246 passed
vitest run packages/plugin-list/ Test Files 71 passed (71) / Tests 884 passed (884)
type-check (both packages, incl. tsconfig.test.json) packages/app-shell type-check: Done / packages/plugin-list type-check: Done, exit 0
lint (package-scoped eslint ., both packages) ✖ 2923 problems (0 errors, 2923 warnings), exit 0 — warnings are the pre-existing no-explicit-any baseline; the two on the new file match the pattern the sibling pin already uses
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 <diff paths> ✅ NOT GOVERNED — 5 path(s) checked against 5 governed surface(s); none matched.

Dependency closures were built first (pnpm --filter '@object-ui/app-shell^...' --filter '@object-ui/plugin-list^...' build, exit 0), so the type-check reading is not the stale-dist signature.

Changeset

minor (this repo never declares major; a breaking change ships minor). The changeset argues the removal explicitly: a consumer that did read options.kanban.groupBy loses the key, loses nothing by it — the identical value is written by the same expression under groupByField, no gate or render branch ever read it — and anyone who authored the key was already off-spec, since the platform refuses it at authoring and publish time.

Out of scope, filed separately

  1. plugin-list: the restKanban spread overrides the lane the kanban branch just resolved — measured above with a distinguishing fixture. groupBy is the one key the destructure does not strip.
  2. The view-level titleField is undeclared on both faces — refused by the spec's KanbanConfigSchema, accepted only by this repo's .passthrough() mirror, and live (ListView forwards it onto the generated node). Unlike cardFields it is not declared as a deprecated alias anywhere. Either promote it upstream or declare it in the mirror the way CalendarConfig.defaultView is.
  3. data-objectstack/src/updateView.draft.test.ts:38 authors kanban: { groupBy: 'status' } in a draft-view payload fixture — off-spec metadata, inert for that test's subject, and no longer what any producer writes.

⚠️ GitHub's API was returning API rate limit already exceeded for issue search while this ran, so the dedup search these three need could not be completed. They are described in full in the report for the PM to file rather than filed unsearched — filing without the dedup read and filing nothing are both forbidden.

Scope fence held

View-level KanbanConfig only. The node-level ObjectKanbanSchema tombstones (objectui#7322 / #8172 / #7742 / #7772 / #8174) are untouched, and the live view-level groupField alias objectui#8193 deliberately left in this same expression is untouched — its read and its write both stay. Branch cut from current main (614d85b19); nothing was carried over from a card's numbers.

🤖 Generated with Claude Code

https://claude.ai/code/session_01YBWFb5YgMU5dw8p2VKj16S


Generated by Claude Code

`ObjectView.kanbanViewOptions` wrote `{ groupBy: lane, groupByField: lane }`
into the view-level kanban config it hands to `list-view`. Measured against
`@objectstack/spec` 17.3.0, the strict `KanbanConfigSchema` declares
`groupByField` / `summarizeField` / `columns` and refuses `groupBy` BY NAME,
with a control that fires on the same call and a control that accepts — so the
refusal carries information. Upstream knows the other legacy spelling by name
("Did you mean `groupField` to `groupByField`?") and knows nothing about
`groupBy`: this repo was producing a third dialect for one concept.

A producer census over every tracked file — a bare `groupBy` inside a
view-level kanban/calendar/gallery/timeline/gantt object literal, lit control
`groupByField` in the same position returning 58 sites — found no other runtime
writer. The only reader that listed it, `ListView`'s projection/expand
collectors, offers `groupByField` first and gets the identical value; the
capability gate and the kanban render branch never read it at all.

Also removes the only producer feeding a latent override: `ListView` spreads
the rest of the merged config after its own `groupBy: laneField`, so a
surviving `groupBy` won over the lane just resolved. The override itself is a
`plugin-list` change and is untouched here.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YBWFb5YgMU5dw8p2VKj16S
@github-actions

github-actions Bot commented Sep 7, 2026

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Eager closure (gzip, 50 chunks) 3472.2 KB 3512.7 KB
Main entry chunk (gzip) 143.9 KB 350 KB
Entry file index-PEeJMOpU.js —
Status PASS —

The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it.


📦 Bundle Size Report

Package Size Gzipped
app-shell (consoleActionDispatch.js) 0.20KB 0.19KB
app-shell (index.js) 15.67KB 5.75KB
app-shell (runtime-config.js) 20.68KB 7.36KB
app-shell (types.js) 0.01KB 0.04KB
app-shell (urlParams.js) 10.06KB 3.86KB
auth (ActiveOrganizationStorage.js) 25.05KB 9.16KB
auth (AuthContext.js) 0.31KB 0.24KB
auth (AuthGuard.js) 2.07KB 1.00KB
auth (AuthProvider.js) 40.18KB 10.59KB
auth (AuthShell.js) 3.49KB 1.40KB
auth (ForgotPasswordForm.js) 12.21KB 3.45KB
auth (LoginForm.js) 18.15KB 5.39KB
auth (PreviewBanner.js) 0.90KB 0.50KB
auth (RegisterForm.js) 6.65KB 2.22KB
auth (SocialSignInButtons.js) 9.61KB 3.89KB
auth (UserMenu.js) 3.41KB 1.23KB
auth (auth-gate-events.js) 1.29KB 0.66KB
auth (authStyles.js) 5.04KB 1.72KB
auth (createAuthClient.js) 40.21KB 10.80KB
auth (createAuthenticatedFetch.js) 8.46KB 3.43KB
auth (index.js) 3.19KB 1.44KB
auth (invitation-status.js) 1.22KB 0.70KB
auth (org-roles.js) 6.66KB 2.78KB
auth (phone-identifier.js) 1.11KB 0.66KB
auth (types.js) 0.59KB 0.35KB
auth (useAuth.js) 5.30KB 1.02KB
auth (useWorkspaceAdminStatus.js) 5.13KB 2.35KB
collaboration (CommentThread.js) 26.08KB 7.56KB
collaboration (LiveCursors.js) 3.17KB 1.27KB
collaboration (PresenceAvatars.js) 6.49KB 2.64KB
collaboration (PresenceProvider.js) 2.79KB 1.13KB
collaboration (index.js) 1.68KB 0.73KB
collaboration (useCollaborationTranslation.js) 6.05KB 2.52KB
collaboration (useCommentSearch.js) 1.98KB 0.88KB
collaboration (useConflictResolution.js) 7.75KB 1.86KB
collaboration (useMentionNotifications.js) 1.81KB 0.68KB
collaboration (usePresence.js) 6.33KB 1.84KB
collaboration (useRealtimeSubscription.js) 7.91KB 2.01KB
components (index.js) 498.55KB 114.03KB
core (index.js) 7.48KB 2.96KB
create-plugin (index.js) 10.08KB 3.26KB
data-objectstack (index.js) 189.15KB 52.56KB
fields (index.js) 243.15KB 61.40KB
i18n (LocalizationContext.js) 1.76KB 0.96KB
i18n (builtinAggregateLabels.js) 0.86KB 0.49KB
i18n (currency.js) 1.22KB 0.64KB
i18n (fallbackInterpolation.js) 6.25KB 2.77KB
i18n (i18n.js) 6.57KB 2.76KB
i18n (index.js) 3.65KB 1.47KB
i18n (pickLocalized.js) 7.62KB 3.26KB
i18n (provider.js) 26.89KB 9.04KB
i18n (useDisplayLocale.js) 2.85KB 1.45KB
i18n (useObjectLabel.js) 34.34KB 9.17KB
i18n (useSafeTranslation.js) 5.60KB 2.33KB
layout (index.js) 38.84KB 10.94KB
mobile (MobileProvider.js) 0.92KB 0.49KB
mobile (ResponsiveContainer.js) 0.94KB 0.38KB
mobile (breakpoints.js) 1.51KB 0.70KB
mobile (createOfflineDataSource.js) 5.61KB 1.75KB
mobile (index.js) 1.99KB 0.87KB
mobile (offlineQueue.js) 3.91KB 1.35KB
mobile (pwa.js) 0.97KB 0.49KB
mobile (serviceWorker.js) 1.48KB 0.62KB
mobile (serviceWorkerSource.js) 3.41KB 1.48KB
mobile (useBreakpoint.js) 1.54KB 0.65KB
mobile (useGesture.js) 6.96KB 1.98KB
mobile (useOfflineSync.js) 1.99KB 0.72KB
mobile (usePullToRefresh.js) 2.53KB 0.85KB
mobile (useResponsive.js) 0.72KB 0.42KB
mobile (useSpecGesture.js) 4.39KB 1.66KB
mobile (useTouchTarget.js) 1.01KB 0.54KB
permissions (MePermissionsProvider.js) 11.71KB 4.29KB
permissions (PermissionContext.js) 0.31KB 0.25KB
permissions (PermissionGuard.js) 0.89KB 0.45KB
permissions (PermissionProvider.js) 6.24KB 2.16KB
permissions (discardProofCache.js) 1.04KB 0.55KB
permissions (evaluator.js) 5.12KB 1.74KB
permissions (index.js) 0.93KB 0.41KB
permissions (store.js) 0.91KB 0.42KB
permissions (useFieldPermissions.js) 1.28KB 0.53KB
permissions (usePermissions.js) 4.83KB 2.27KB
plugin-ai (index.js) 15.16KB 3.68KB
plugin-calendar (index.js) 49.00KB 13.91KB
plugin-charts (index.js) 71.39KB 19.92KB
plugin-chatbot (index.js) 193.79KB 46.09KB
plugin-dashboard (index.js) 131.48KB 34.45KB
plugin-designer (index.js) 213.21KB 43.63KB
plugin-detail (index.js) 248.56KB 63.89KB
plugin-editor (index.js) 2.23KB 1.05KB
plugin-form (index.js) 131.01KB 32.32KB
plugin-gantt (index.js) 167.16KB 40.99KB
plugin-grid (index.js) 208.58KB 56.63KB
plugin-kanban (index.js) 55.38KB 15.72KB
plugin-list (index.js) 113.38KB 27.73KB
plugin-map (index.js) 20.49KB 6.83KB
plugin-markdown (index.js) 13.88KB 4.80KB
plugin-report (index.js) 43.42KB 11.92KB
plugin-timeline (index.js) 30.10KB 8.74KB
plugin-tree (index.js) 9.33KB 3.25KB
plugin-view (index.js) 84.46KB 20.80KB
providers (DataSourceProvider.js) 0.75KB 0.39KB
providers (MetadataProvider.js) 1.37KB 0.59KB
providers (ThemeProvider.js) 1.90KB 0.85KB
providers (UploadProvider.js) 11.66KB 3.50KB
providers (index.js) 0.45KB 0.23KB
providers (types.js) 0.01KB 0.04KB
react-runtime (index.js) 5.62KB 2.34KB
react (LazyPluginLoader.js) 4.47KB 1.63KB
react (SchemaRenderer.js) 81.07KB 26.86KB
react (data-invalidation.js) 5.05KB 2.08KB
react (index.js) 4.63KB 2.18KB
react (schema-input.js) 2.32KB 1.24KB
react (spec-input.js) 0.20KB 0.18KB
sdui-parser (codegen.js) 6.58KB 2.74KB
sdui-parser (dashboard-widget-options.js) 3.08KB 1.30KB
sdui-parser (index.js) 5.55KB 2.45KB
sdui-parser (input-type.js) 2.84KB 1.40KB
sdui-parser (parse.js) 20.57KB 5.88KB
sdui-parser (provenance.js) 3.66KB 1.82KB
sdui-parser (types.js) 0.28KB 0.23KB
sdui-parser (validate.js) 13.64KB 4.59KB
types (ai.js) 0.20KB 0.17KB
types (api-types.js) 0.20KB 0.18KB
types (app.js) 2.87KB 1.00KB
types (base.js) 0.20KB 0.18KB
types (blocks.js) 0.20KB 0.18KB
types (complex.js) 2.93KB 1.49KB
types (crud.js) 0.20KB 0.18KB
types (dashboard-filter-alias.js) 6.23KB 2.74KB
types (data-display.js) 3.75KB 1.85KB
types (data-protocol.js) 0.20KB 0.19KB
types (data.js) 0.20KB 0.18KB
types (designer.js) 1.85KB 0.85KB
types (disclosure.js) 0.20KB 0.18KB
types (error-code.js) 1.54KB 0.88KB
types (expression.js) 0.20KB 0.18KB
types (feedback.js) 0.20KB 0.18KB
types (field-types.js) 0.20KB 0.18KB
types (form.js) 0.20KB 0.18KB
types (http-inflight.js) 8.87KB 3.73KB
types (http-retry.js) 4.32KB 2.02KB
types (icon-key-migration.js) 4.26KB 1.63KB
types (index.js) 4.74KB 2.25KB
types (layout.js) 0.20KB 0.18KB
types (managed-by.js) 0.19KB 0.18KB
types (mobile.js) 4.73KB 2.28KB
types (navigation.js) 0.20KB 0.18KB
types (objectql.js) 0.20KB 0.18KB
types (overlay.js) 0.20KB 0.18KB
types (permissions.js) 0.20KB 0.18KB
types (plugin-scope.js) 0.20KB 0.18KB
types (record-components.js) 0.20KB 0.19KB
types (record-semantics.js) 1.28KB 0.67KB
types (registry.js) 0.20KB 0.18KB
types (reports.js) 0.20KB 0.18KB
types (select-option.js) 0.20KB 0.19KB
types (spec-report.js) 5.05KB 1.93KB
types (spec-ui-namespace.js) 0.20KB 0.19KB
types (system-fields.js) 3.33KB 1.54KB
types (theme.js) 6.28KB 2.87KB
types (ui-action.js) 8.11KB 3.32KB
types (views.js) 0.20KB 0.18KB
types (widget.js) 0.20KB 0.18KB

Size Limits

  • ✅ Core packages should be < 50KB gzipped
  • ✅ Component packages should be < 100KB gzipped
  • ⚠️ Plugin packages should be < 150KB gzipped

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

finding(app-shell): the view-level groupBy ObjectView writes into options.kanban is a third lane spelling the spec's KanbanConfigSchema refuses by name

2 participants