Skip to content

fix(plugin-list,types): a stray groupBy no longer overrides the kanban lane, and is refused by name - #9236

Merged
os-tesla merged 2 commits into
mainfrom
claude/issue-8365-stray-groupby-refused
Sep 12, 2026
Merged

fix(plugin-list,types): a stray groupBy no longer overrides the kanban lane, and is refused by name#9236
os-tesla merged 2 commits into
mainfrom
claude/issue-8365-stray-groupby-refused

Conversation

@os-tesla

@os-tesla os-tesla commented Sep 12, 2026

Copy link
Copy Markdown
Collaborator

Part of #8365

Implemented by the domain:ui os-dev seat, session session_01UzHd6hDYatoDn17BuwKxnZ (recorded in prose as well as in the footer, because a body edit downgrades the footer's session form).

What this repairs

ListView's kanban branch destructured columns / groupByField / groupField / cardFields / titleField out of the merged kanban config and spread the rest after its own groupBy: laneField. A groupBy surviving in that bag therefore overrode the lane the branch had just resolved.

Measured on the card's distinguishing fixture, not reasoned:

options.kanban = { groupBy: 'LANE_FROM_STRAY_GROUPBY' }
kanban         = { groupByField: 'LANE_FROM_CANONICAL' }
=> node.groupBy was LANE_FROM_STRAY_GROUPBY

It read as latent only because the single producer that fed it wrote both spellings with the same value by construction, so no fixture driven by that producer could tell the two apart.

The ruling this implements — option B, not re-opened

Maintainer ruling of 2026-09-12, decision batch #117 item 5, verbatim 「8365 同意」. Option A (strip the key and re-group in silence) was the fallback for a measured zero of stored views and is not taken. Honouring groupBy as a declared alias was never on the table.

  1. The canonical lane wins. groupBy joins the destructure in packages/plugin-list/src/ListView.tsx, so the stray key can no longer reach the passthrough spread.
  2. The stray key is refused loudly, by name, pointing at groupByField.

Where the refusal is — the part the ruling flagged, answered explicitly

The read door is the view-level KanbanConfig mirror, packages/types/src/zod/objectql.zod.ts. The key is now a declared, unwritable member (aliasKeyRefusal, the helper this repo already uses for the chartType alias), so it is refused instead of riding the object's .passthrough() in silence. The message is the sentence @objectstack/spec's own strictObject({ aliases }) answers with, quoted down to its surface noun:

Unrecognized key(s) on this kanban configuration: `groupBy`. Did you mean `groupBy` → `groupByField`? …

That door is where the author of the view is standing — every channel that validates a view's metadata reaches it:

  • the CLI's os check / os validate (safeValidateSchema),
  • the VS Code extension,
  • tsc at the authoring site: z.never() at a member position makes z.input of that member undefined, so the inferred authoring face carries groupBy?: never and the key is a compile error before anything runs.

⛔ It is deliberately not a console.warn. The ruling names that outcome and refuses it; this repo's two existing warn-only diagnostics (columnSpellingDiagnostics.ts, dataTableBindDiagnostic.ts) both record that a rendered in-view message would need user-facing copy in all ten locale packs — which is the one thing this card must not do, since that is objectui#9169 / objectui#9170's surface and the ruling's note 4 says this card shares no file with them. It does not: see the file list.

Both nestings, and why the second one is load-bearing rather than thorough

ListView merges { ...schema.options?.kanban, ...schema.kanban } before it reads anything, and the producer objectui#8213 retired wrote into options.kanban — so that is where the stored views this ruling is about carry the key. options is declared z.record(z.string(), z.any()) and can declare no member at all, so it takes the same guidance string (read off the declared arm's own .description) as a check.

nesting mechanism issue code path
kanban.groupBy declared aliasKeyRefusal arm invalid_type kanban.groupBy
options.kanban.groupBy check on the legacy bag custom options.kanban.groupBy

Two codes, one message — pinned equal, so the remedy cannot fork into two dialects. Refusing only the declared nesting would have left exactly the affected population silently re-grouped, i.e. option A wearing option B's name.

Scope kept narrow, deliberately

  • The .passthrough() is kept. One named arm was declared; the object was not closed. An undeclared sibling key still rides through — pinned as a control, because a later .strict() would satisfy every other assertion in the file.
  • Nothing else under options is judged. That bag is z.any() for every view kind and stays that way.
  • groupBy on the generated object-kanban node is untouched — that is the live canonical lane key ObjectKanban reads. Only the view-level config spelling is refused.
  • The live legacy alias kanban.groupField still resolves the lane, pinned as a control.

The pin, and the two ablations that prove it bites

packages/plugin-list/src/__tests__/ListView.strayGroupByRefused-8365.test.tsx carries the card's fixture and asserts both halves off the same document — the lane through the renderer (a registry spy on the generated object-kanban node), the refusal through the published read door. 11 tests.

Controls, each with a stated failure it would catch: a dark control (canonical config parses green), a passthrough control (an undeclared sibling key still parses green), two protocol controls (the spec refuses groupBy by name with lit and dark controls firing; the spec still does not declare it), and a lane control (groupBy alone no longer sets the lane — the arm that tells option B apart from "strip it and hope").

packages/types/src/__tests__/kanban-stray-group-by-refusal-8365.test.ts carries the compile-time half, because vitest erases it and a green vitest run says nothing about it. Read through tsc, never vitest.

Protocol re-measurement (the card measured 17.3.0; this tree pins 17.4.0), both controls firing:

DECLARED KEYS: columns, groupByField, summarizeField
lit  control  { groupByField, zzzBogusKey } -> unrecognized_keys names `zzzBogusKey`
dark control  { groupByField }              -> no unrecognized_keys
sibling alias { groupField }  -> "Did you mean `groupField` → `groupByField`?"
target        { groupBy }     -> refused by name, no alias clause (upstream knows nothing about it)

Note the protocol's arrow is U+2192, not the ASCII -> the ruling comment transcribed; this PR matches the protocol's own bytes.

Gate results

Every reading below is from d6edbaecb, i.e. after origin/main was merged in — the merge moved the head, so every family was re-run on it rather than quoted from the pre-merge run. Each verdict is the gate's own printed line; exit codes were captured by redirect-then-capture, never through a pipe.

Typecheck (and the build closure it waits on)pnpm exec turbo run type-check --filter=@object-ui/types --filter=@object-ui/plugin-list --filter=@object-ui/app-shell --concurrency=2

Tasks:    32 successful, 32 total

⭐ This is the gate that reads the compile-time pin: @object-ui/types' type-check is tsc --noEmit && tsc -p tsconfig.examples.json && tsc -p tsconfig.test.json, and the third pass is the only thing in this repo that compiles src/__tests__/**.

Testspnpm exec vitest run packages/types/ packages/plugin-list/ plus the two app-shell kanban pins and core's normalize-list-view (repo-root invocation, per AGENTS.md — not pnpm --filter, which changes process.cwd())

Test Files  258 passed (258)
     Tests  4502 passed (4502)

Ablations — three legs, each read through the gate that actually enforces its claim. Every leg: mutate, prove the mutation landed on disk by anchor count, run, restore, prove the restore by blob hash against the HEAD blob.

leg mutation gate result
1 drop the declared groupBy arm from KanbanConfig tsc -p tsconfig.test.json exit 2, error TS2578: Unused '@ts-expect-error' directive. at the pin
2 the same mutation vitest exit 1 — 1 failed, 10 passed: only the declared-nesting arm
3 drop groupBy from ListView's destructure vitest exit 1 — 3 failed, 8 passed: the three lane arms, naming the stray lane

The asymmetry is the point: leg 3 reddens the lane arms and leaves every refusal arm green, leg 2 reddens a refusal arm and leaves every lane arm green. Neither half is carrying the other.

⚠️ Leg 2 is reported as it measured, not as the template predicts. Dropping the member arm reddens the declared-nesting assertion only; the legacy-options check stays green, because it reads the guidance off the arm's .description const, which the mutation left standing. That is an honest reading of two independent mechanisms sharing one string, and it is why leg 1 exists: the member arm's own enforcement is tsc, and vitest can only see half of it.

Ablation legs read source, not dist: the root vitest config aliases every package specifier to its src, and packages/types/tsconfig.test.json reaches the mirror by relative import. So there is no dist rung in the resolution path to go stale, and none was built for the ablation.

Repo gates — each exit 0:

check:control-bytes  check:spec-symbols  check:published-tsconfig-exclude
check:test-path-roots  check:unreferenced-sources  check:changeset-claims
check:new-line-citations  check:self-import  check:phantom-deps
scripts/check-changeset-no-major.mjs

eslint — the full root union was run, so this is a measurement rather than a declared narrowing: pnpm exec eslint --no-inline-config --format json . at d6edbaecb resolved 4863 files from eslint's own config and reported 95 errors / 12984 warnings. 0 of those errors are in any of the 7 files this PR touches (they sit in 79 unrelated files and are pre-existing). ⚠️ Stated precisely: this root-level invocation is a superset of what CI's pnpm lint (turbo run lint, i.e. per-package eslint .) resolves, not the same invocation.

NOT MEASURED, named rather than implied:

  • CI convergence on this PR — belongs to the PM, not reported here as a result.
  • pnpm lint in CI's own per-package form, and the repo-wide check:* farm in ci.yml — CI owns those runs.

Acceptance notes

Reported rather than migrated, as the dispatch asked. Real authored usages of a view-level kanban.groupBy found by a structural scan of every tracked file (bare groupBy inside a kanban object literal; lit control groupByField in the same position returned 51 sites, so the small reading is a reading):

  1. packages/data-objectstack/src/updateView.draft.test.ts:38 — a stale metadata payload, kanban: { columns: [...], groupBy: 'status' }, in a test about draft ADDRESSING that never validates the body. Not migrated here: it is another package's fixture and nothing gates it. It is the one the objectui#8213 census already named.
  2. packages/plugin-list/src/__tests__/ListView.kanbanOptionsBagCanonical-8193.test.tsx:121,160 — the two arms that pin the capability gate's behaviour for a bag carrying the key. Unaffected: that gate never read groupBy, and both arms stay green.

Zero in docs, examples, seed data or any runtime producer.

noted, not filed:

  • packages/plugin-view/src/ObjectView.tsx (its generateViewSchema kanban branch, around line 1361) has the same defect shape: it destructures columns / groupByField / groupField / titleField / conditionalFormatting and spreads ...restKanban after its own groupBy, so a stray groupBy in viewOptions.kanban overrides the lane there too. That is the SECOND route to ObjectKanban (the authored object-view element, which never passes through ListView). Left alone here because it is a different package and a different renderer, i.e. new validation surface outside this card's fence — but the contract half of this PR now refuses the key for both routes, so the residual is a behaviour gap, not a contract gap. Handing it to the PM to file rather than filing it unsearched: the dedup sweep for it was not run.
  • ListViewSchema.options is z.record(z.string(), z.any()), so no per-view-type config refusal reaches options.KIND for any kind — kanban is covered here only because this PR adds a key-scoped check. Every other view kind's config is unvalidated in that nesting. Observation, not a defect on this card.
  • The dispatch note said @objectstack/spec does not resolve from an objectui checkout. It does: node_modules/@objectstack/spec is present at 17.4.0 and every measurement above was taken from this worktree.

Generated by Claude Code

…ban lane, and is refused by name

`ListView`'s kanban branch destructured
`columns`/`groupByField`/`groupField`/`cardFields`/`titleField` out of the merged
kanban config and spread the REST *after* its own `groupBy: laneField`, so a
`groupBy` surviving in the bag OVERRODE the lane the branch had just resolved.

Measured on the card's distinguishing fixture, not reasoned:
`options.kanban = { groupBy: 'LANE_FROM_STRAY_GROUPBY' }` against
`kanban = { groupByField: 'LANE_FROM_CANONICAL' }` produced
`node.groupBy === 'LANE_FROM_STRAY_GROUPBY'`. It read as latent only because the
one producer that fed it wrote both spellings with the same value by
construction, so no fixture driven by that producer could tell them apart.

Maintainer ruling of 2026-09-12 (decision batch #117 item 5, verbatim
「8365 同意」) — option B. Option A (strip and re-group in silence) was the
fallback for a measured zero of stored views and is NOT taken. Two halves:

1. `groupBy` joins the destructure, so the CANONICAL lane wins.
2. The key is refused LOUDLY at the read door of the view. This repo's
   `.passthrough()` `KanbanConfig` mirror declares it as a named alias refusal
   (`aliasKeyRefusal`) pointing at `groupByField`, in the sentence shape the
   protocol already answers the sibling alias with. The refusal lands wherever a
   view's metadata is validated — `os check` / `os validate`, the VS Code
   extension — and on the type face, where the inferred authoring surface now
   carries `groupBy?: never` and `tsc` refuses the key at the authoring site.

Both nestings are covered, and the second is load-bearing rather than thorough:
`ListView` merges `{ ...options.kanban, ...kanban }`, and the producer
objectui#8213 retired wrote into `options.kanban`, so that is where the stored
views this ruling is about carry the key. `options` is
`z.record(z.string(), z.any())` and can declare no member, so it takes the
declared arm's guidance as a check (`custom` at `options.kanban.groupBy`) while
the declared slot reports the arm itself (`invalid_type` at `kanban.groupBy`).
Two codes, ONE message — read off the arm's own `.description`, and pinned equal.

Honouring `groupBy` as a declared alias was never available: `@objectstack/spec`'s
`KanbanConfigSchema` is a strict object of `columns` / `groupByField` /
`summarizeField` and refuses it by name. Re-measured on the version this tree
pins (17.4.0; the card measured 17.3.0) with both controls firing — a lit control
(`zzzBogusKey`) is named, a dark control (`groupByField` alone) draws no
`unrecognized_keys`. Legalising it is a spec change on its own objectstack card,
never a renderer-side widening (AGENTS.md #0.1).

`groupBy` on the generated `object-kanban` NODE is untouched: that is the live
canonical lane key `ObjectKanban` reads. The `.passthrough()` is kept — an
undeclared sibling key still rides through, pinned as a control.

Two now-false prose claims in `app-shell` are corrected in the same commit:
`ObjectView.tsx` and `ObjectView.kanbanGroupByRetired-8213.test.tsx` both stated
that the override "is untouched" and "is a `plugin-list` change on its own card".
Comment-only; no behaviour in that package changes.

Part of objectui#8365

Claude-Session: https://claude.ai/code/session_01UzHd6hDYatoDn17BuwKxnZ

Co-authored-by: Claude <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Eager closure (gzip, 51 chunks) 3112.9 KB 3134.8 KB
Main entry chunk (gzip) 144.2 KB 350 KB
Entry file index--gnpvdPp.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) 16.69KB 6.21KB
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) 11.08KB 4.58KB
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) 500.84KB 114.81KB
core (index.js) 8.52KB 3.41KB
create-plugin (index.js) 27.94KB 9.51KB
data-objectstack (index.js) 209.38KB 57.92KB
fields (index.js) 247.41KB 62.43KB
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) 8.87KB 3.64KB
i18n (index.js) 5.21KB 2.26KB
i18n (pickLocalized.js) 7.62KB 3.26KB
i18n (provider.js) 32.15KB 10.49KB
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.95KB
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) 13.52KB 4.88KB
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) 8.39KB 3.10KB
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) 14.81KB 3.63KB
plugin-calendar (index.js) 49.03KB 13.93KB
plugin-charts (index.js) 71.52KB 19.98KB
plugin-chatbot (index.js) 195.32KB 46.51KB
plugin-dashboard (index.js) 131.27KB 34.62KB
plugin-designer (index.js) 215.81KB 44.32KB
plugin-detail (index.js) 253.38KB 65.87KB
plugin-editor (index.js) 2.23KB 1.05KB
plugin-form (index.js) 136.79KB 34.19KB
plugin-gantt (index.js) 166.94KB 41.04KB
plugin-grid (index.js) 211.67KB 57.51KB
plugin-kanban (index.js) 46.10KB 14.33KB
plugin-list (index.js) 112.59KB 27.66KB
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.54KB 3.31KB
plugin-view (index.js) 84.42KB 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) 94.03KB 31.02KB
react (data-invalidation.js) 5.05KB 2.08KB
react (index.js) 4.63KB 2.18KB
react (schema-input.js) 4.25KB 2.04KB
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.66KB 2.50KB
sdui-parser (input-type.js) 2.84KB 1.40KB
sdui-parser (kanban-quick-add.js) 3.89KB 1.87KB
sdui-parser (parse.js) 25.28KB 7.80KB
sdui-parser (provenance.js) 3.66KB 1.82KB
sdui-parser (types.js) 0.28KB 0.23KB
sdui-parser (validate.js) 14.82KB 4.99KB
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 (strict-authoring-face.js) 14.27KB 5.47KB
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

@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Eager closure (gzip, 51 chunks) 3113.1 KB 3134.8 KB
Main entry chunk (gzip) 144.3 KB 350 KB
Entry file index-DPflE4El.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) 16.69KB 6.21KB
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) 11.08KB 4.58KB
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) 500.84KB 114.81KB
core (index.js) 8.52KB 3.41KB
create-plugin (index.js) 27.94KB 9.51KB
data-objectstack (index.js) 209.38KB 57.92KB
fields (index.js) 247.88KB 62.48KB
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) 8.87KB 3.64KB
i18n (index.js) 5.21KB 2.26KB
i18n (pickLocalized.js) 7.62KB 3.26KB
i18n (provider.js) 32.15KB 10.49KB
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.95KB
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) 13.52KB 4.88KB
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) 8.39KB 3.10KB
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) 14.81KB 3.63KB
plugin-calendar (index.js) 49.03KB 13.93KB
plugin-charts (index.js) 71.52KB 19.98KB
plugin-chatbot (index.js) 195.35KB 46.52KB
plugin-dashboard (index.js) 131.27KB 34.62KB
plugin-designer (index.js) 215.81KB 44.32KB
plugin-detail (index.js) 253.38KB 65.87KB
plugin-editor (index.js) 2.23KB 1.05KB
plugin-form (index.js) 136.79KB 34.19KB
plugin-gantt (index.js) 166.94KB 41.04KB
plugin-grid (index.js) 211.67KB 57.51KB
plugin-kanban (index.js) 46.10KB 14.33KB
plugin-list (index.js) 112.59KB 27.66KB
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.54KB 3.31KB
plugin-view (index.js) 84.42KB 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) 94.03KB 31.02KB
react (data-invalidation.js) 5.05KB 2.08KB
react (index.js) 4.63KB 2.18KB
react (schema-input.js) 4.25KB 2.04KB
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.66KB 2.50KB
sdui-parser (input-type.js) 2.84KB 1.40KB
sdui-parser (kanban-quick-add.js) 3.89KB 1.87KB
sdui-parser (parse.js) 25.28KB 7.80KB
sdui-parser (provenance.js) 3.66KB 1.82KB
sdui-parser (types.js) 0.28KB 0.23KB
sdui-parser (validate.js) 14.82KB 4.99KB
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 (strict-authoring-face.js) 14.27KB 5.47KB
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

Copy link
Copy Markdown
Collaborator Author

Contract review — head d6edbaecb6PASS

Reviewed-by: domain:ui PM seat (os-tesla), R16, 2026-09-12T03:2xZ.

Clause-② yes--pair 9236 reads , both carriers agree. ⚠️ The seat reported it at exit 4 and was right at the time: the defect was in my claim comment, not in this PR, and it was a batch defect across all four cards I claimed this round (CLAIM_COMMENT_MARKER needs a line beginning Claimed:; I wrote "Claimed for an os-dev seat"). All four patched. ⭐ The seat reported it and ⛔ did not repair it — the claim comment is the PM's carrier, and a dev seat writing its own would be 自查放行 in miniature. Correct call, made twice this round by two different seats.

CI: 36 checks, 0 pending, 0 failing.


The ruling, discharged in both halves

Batch #117 item 5, 「8365 同意」 → option B. ⛔ Not re-opened.

  1. groupBy joins the destructure in ListView's kanban branch, so the stray key can no longer ride ...restKanban past the branch's own groupBy: laneField. The canonical lane wins.
  2. The refusal sits at the contract read door, and the report says exactly where: KanbanConfig in packages/types/src/zod/objectql.zod.ts declares groupBy as a named alias-refusal arm via aliasKeyRefusal — the helper this repo already uses for the chartType alias. That door is reached by os check / os validate (safeValidateSchema), the VS Code extension, and tsc at the authoring site (z.input is undefined, so the authoring face carries groupBy?: never).

⛔ Deliberately not a console.warn — the ruling names that outcome and refuses it. And the seat gave the reason the stronger in-view form was not attempted: the repo's two existing warn-only diagnostics both record that a rendered in-view message needs copy in all ten locale packs, which is objectui#9169/#9170's surface. ⇒ it stayed off that pair entirely, as the ruling's note 4 requires.

⭐ The load-bearing half I did not specify and would have missed

ListView merges { ...options.kanban, ...kanban }, and the producer objectui#8213 retired wrote into options.kanban — so that is where the stored views this ruling is about actually carry the key. Covering only the declared nesting would have left exactly that population silently re-grouped: option A wearing option B's name. The seat found that and covered both nestings.

options is z.record(z.string(), z.any()) and can declare no member, so it takes the same guidance string — read off the arm's own .description — as a check: custom at options.kanban.groupBy vs invalid_type at kanban.groupBy, two codes, one message, pinned equal.

Scope kept where it was drawn

.passthrough() is kept (an undeclared sibling still rides through, pinned as a control); nothing else under options is judged; the live legacy alias kanban.groupField still resolves the lane; groupBy on the generated object-kanban node is untouched.


Two things measured better than they were specified

The protocol's arrow is U+2192, not the ASCII -> my ruling comment transcribed. The PR matches the protocol's bytes rather than my transcription of them. ⇒ my quotation was the lossy copy; the contract is the reference.

Re-measured on 17.4.0 — the version this tree pins — where the card measured 17.3.0, with both controls firing: lit control {groupByField, zzzBogusKey} draws unrecognized_keys naming the bogus key; dark control {groupByField} draws none; {groupField} answers the alias sentence; {groupBy} is refused by name. ⇒ the ruling's premise re-derived on the pinned version rather than inherited.

The ablation, and what it confirms about ablations

Three legs, each mutation proved on disk and each restore proved by blob hash against HEAD. LEG 1 (drop the declared arm, read through tsc) → exit 2, TS2578: Unused '@ts-expect-error' directive. LEG 2 (same mutation, read through vitest) → 1 failed | 10 passed. LEG 3 (drop the destructure fix) → 3 failed | 8 passed, the three lane arms.

⚠️ And the honest line: LEG 2 reddens the declared-nesting arm only — the legacy-options check stays green, because it reads its guidance off the .description const the mutation left standing. Two independent mechanisms sharing one string. ⭐ That is exactly why LEG 1 exists, and it is the second independent confirmation this round of the rule objectui#9229 bought: an ablation must be read through the gate that actually enforces the claim. Reported as measured, ⛔ not as the template predicts.

Authored usage — reported, ⛔ not migrated

A structural scan of every tracked file (bare groupBy inside a kanban object literal; lit control groupByField in the same position = 51 sites, so the small reading is a reading) found two real usages, both in tests, zero in docs, examples, seed data or any runtime producer. ⇒ the ruling's measured-zero-of-stored-views premise holds. Neither was silently changed, as the dispatch required.

Filed

objectui#9242plugin-view's generateViewSchema kanban branch carries the same spread-order defect, and it is the second route to ObjectKanban (the authored object-view element never passes through ListView), so this PR's destructure fix does not reach it. ⚠️ The contract half does cover both routes, so what remains is a behaviour gap, not a contract gap — a distinction worth keeping when that card is graded.

⛔ A false premise in my dispatch note, corrected by the seat

I wrote that @objectstack/spec does not resolve from an objectui checkout and to read it from /home/user/objectstack or via npm pack. That is false. Verified here: node_modules/@objectstack/spec is present at 17.4.0 in a worktree that has run pnpm install; the bare checkout simply has no node_modules, and I generalised "absent in the bare checkout" into "does not resolve". A dark probe turned into a standing instruction, carried in two dispatches this round. Corrected in my brief. ⇒ every protocol measurement above was taken from the pinned package, which is the better reading anyway.

Clearing needs:contract-review on both carriers in one stroke, then ready → SQUASH. ⛔ Card objectui#8365 is not closed by this PR.


Generated by Claude Code

@os-tesla
os-tesla marked this pull request as ready for review September 12, 2026 03:34
@os-tesla
os-tesla added this pull request to the merge queue Sep 12, 2026
Merged via the queue into main with commit 704e695 Sep 12, 2026
38 checks passed
@os-tesla
os-tesla deleted the claude/issue-8365-stray-groupby-refused branch September 12, 2026 03:53
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.

2 participants