Skip to content

feat(plugin-kanban,plugin-calendar): declare the seven scalar spec keys both renderers already honour (#8201, slice 1 of 2) - #8309

Merged
os-justin merged 1 commit into
mainfrom
claude/issue-8201-kanban-calendar-undeclared-spec-keys
Sep 7, 2026
Merged

feat(plugin-kanban,plugin-calendar): declare the seven scalar spec keys both renderers already honour (#8201, slice 1 of 2)#8309
os-justin merged 1 commit into
mainfrom
claude/issue-8201-kanban-calendar-undeclared-spec-keys

Conversation

@os-justin

Copy link
Copy Markdown
Collaborator

Part of #8201 — this is slice 1 of 2, so it deliberately does not close the card. See the slice boundary below for what stays open and why.

What this does

Declares seven ComponentPropsMap keys that the two object-* renderers already read and already act on, and deletes the seven matching UNPUBLISHED_EXEMPTIONS entries in the same change.

block key arm read site that changes behaviour
object-kanban groupBy string ObjectKanban.tsx materializes lanes from the field's picklist options; bucketCardsIntoColumns buckets records by its value; a drag between lanes writes the value back
object-kanban cardTitle string effectiveData picks the card-title field (read ahead of titleField)
object-kanban titleField string the legacy spelling of the same choice
object-kanban swimlaneField string becomes effectiveSchema.swimlaneField, which KanbanImpl splits the board on
object-kanban coverImageField string bucketCardsIntoColumns maps it onto each card's coverImage, rendered as the card <img>
object-calendar defaultView enum ObjectCalendar.tsx seeds its view state from schema.defaultView
object-calendar locale string resolveAuthoredLocale(rest.locale) forwards a tag Intl accepts into the component's date/time formatting

Both tags of each block (object-kanban/view:kanban, object-calendar/view:calendar) now spread one shared list rather than two hand-copies. The two tags are the same renderer, so a hand-copy was the only way they could ever disagree.

No renderer behaviour changes. Every one of these keys was already read; what changes is that the html tier stops reporting working metadata as unknown-prop (objectui#6678's shape) and authoring tools can discover them.

Clause ② — this widens the authoring surface, and here are the grounds

Declaring an input widens what an author may write, so the grounds are stated rather than assumed. They are the same grounds #8186 (filter) and #8223 (sort) cleared on: the spec already declares the key and the renderer already honours it, so this restores declared = enforced instead of publishing anything new.

That claim was measured, not assumed, because #8172 is the counter-example: limit is taught by four faces and refused by name by this same strict ComponentPropsMap. Every one of the fifteen keys on the card was probed with a control key on the same safeParse call:

CONTROL __objectui_control_probe_8201__: success=false unrecognized_keys=true
  groupBy          refusedByName=false   arms={"string":["accepts"], ...}
  defaultView      refusedByName=false   arms={"string":["refuses-content"], ...}
  ... (all 15 refusedByName=false)

The control draws unrecognized_keys on the same call shape while none of the fifteen does — so "the spec declares it" is a verdict here, not a vacuous read. The probe is preserved as row 4 of each new pin test.

⚠️ The slice boundary, argued rather than silent

#8212 landed a tightened direction: every array/object-armed input must pin its member shape, so declaring such a key owes three things — the inputs entry, the exemption deletion, and a MEMBER_PINS entry whose shape must be measured at the sink rather than assumed.

The boundary drawn here is the declared arm, and it was chosen because it is mechanical rather than tasteful: structuredInputs in the parity gate is keyed off the declared arm, a scalar arm never enters that population, so these seven owe no MEMBER_PINS entry and MEMBER_PIN_EXEMPTION_CEILING stays at 62. A reviewer can check the boundary instead of agreeing with it.

Eight keys stay behind, and the card stays open:

Correction to the dispatch's expectation: conditionalFormatting was flagged alongside quickAdd as a likely product ruling. It is notKanbanImpl reaches it through getCardStyles(card, conditionalFormatting, …) at every card render, so it is a plain declare-case. It is deferred only because it is array-armed and owes a member pin.

Ledger

The backlog ceiling moves 15 → 8, and both assertions carrying it move together (toBeLessThanOrEqual and toBe, same it()); moving only toBe would leave the other banking headroom the ratchet exists to refuse. Proven below.

Corrected in passing: the prose above that ceiling read "Sixteen … ten on object-kanban, six on object-calendar" while both assertions already read 15#8223 moved the numbers and not the sentence. A stale count there is what makes a reader distrust the assertions instead of the prose.

Evidence

Parity gate assertion count: 198 before, 198 after — no move. Measured by reverting the three tracked files to origin/main on this worktree, running, and restoring by state. No move is expected: the gate's rows are static it()s, and the change moves counts inside arrays those rows read, not the row set.

Every new pin proven able to fail — red BY NAME, per registration, restored BY STATE.

  1. Ablate groupBy from the kanban list and defaultView from the calendar list (one line each, on-disk deletion verified by grep -cF going 1 → 0 and git diff --stat showing the deletions):

    × 'object-kanban'   — the html tier accepts an authored `'groupBy'`
    × 'view:kanban'     — the html tier accepts an authored `'groupBy'`
    × 'object-kanban'   — the registration declares `'groupBy'`
    × 'view:kanban'     — the registration declares `'groupBy'`
    × 'object-calendar' — the html tier accepts an authored `'defaultView'`
    × 'view:calendar'   — the html tier accepts an authored `'defaultView'`
    × 'object-calendar' — the registration declares `'defaultView'`
    × 'view:calendar'   — the registration declares `'defaultView'`
    × 'object-calendar' — the declared `defaultView` members are DERIVED from the spec …
    × 'view:calendar'   — the declared `defaultView` members are DERIVED from the spec …
    × both tags publish ONE shared list, so a hand-copy cannot drift   (×2 files)
    Tests  12 failed | 28 passed (40)
    

    Per-registration redness, not per-file: each tag reddens on its own named row, and 28 rows stayed green.

  2. Ablate the ledger — re-add one deleted exemption while the declaration stands:

    × carries no stale unpublished-key exemption — a published key must lose its entry
        AssertionError: expected [ 'object-kanban.groupBy' ] to deeply equal []
    × the objectui#8176 backlog has a ceiling — it may shrink, never grow
        … the backlog list is shrink-only: expected 9 to be less than or equal to 8
    

    The second row is the proof that both halves of the ceiling moved: it is the toBeLessThanOrEqual assertion firing.

  3. Ablate the honour claim — neutralise the coverImage mapper in bucketCardsIntoColumns:

    × honoured — `coverImageField` becomes the card cover `KanbanImpl` renders
    Tests  1 failed | 25 passed (26)
    

    One row, precisely.

Every restore was verified by state, never by an exit code: git diff HEAD empty, and git hash-object <path> equal to git rev-parse HEAD:<path> for each mutated file.

Suites and gates (all from the repo root — pnpm --filter PKG exec vitest run FILE is refused here by #3378's guard):

run result
the two new pin files 40 passed
apps/console/.../registry-inputs-spec-parity.test.ts 198 passed
@object-ui/plugin-kanban test 28 files, 167 passed
@object-ui/plugin-calendar test 28 files, 181 passed
plugin-kanban + plugin-calendar type-check pass (each runs tsc --noEmit and tsc -p tsconfig.test.json, so the new tests are typechecked)
@object-ui/console type-check pass
check:control-bytes OK (6601 files)
check:sdui-registration-pins OK, 16 registrations (first run returned exit 2, a stated prerequisite — needed the console built; re-run after pnpm --filter @object-ui/console build)
check:element-data-source-declaration OK
check:designer-field-key-parity · check:handler-key-reads · check:spec-symbols · check:unreferenced-sources · check:side-effects-array · check:self-import · check:phantom-deps OK

Repo-wide pnpm lint and the full suite are left to CI.

Found in passing, filed not fixed

#8308ObjectKanban.tsx resolves the cardTitle / titleField pair with || at :403 (card list) and ?? at :1077 (detail drawer). An authored cardTitle: '' therefore titles the cards from titleField while the drawer heading falls through to the record floor: one document, two answers. Not fixed here — this PR declares the two spellings without changing how either resolves — but declaring them makes the divergence more reachable, which is why it is filed.

Reviewer note

⛔ Draft on purpose. The PM flips and lands; auto-merge is not enabled.

🤖 Generated with Claude Code

https://claude.ai/code/session_01YBWFb5YgMU5dw8p2VKj16S


Generated by Claude Code

…ys both renderers already honour

`ComponentPropsMap` declares 13 top-level keys for `object-kanban` and 9 for
`object-calendar`; the registrations published 4 of each. The gap was structural
rather than considered — the console registers both blocks with `registerLazy`
and `getConfig` is loaded-only, so they sat outside the console's reverse-parity
population until objectui#8176 loaded it.

Declares the seven whose arm is SCALAR and whose read site changes behaviour:
`groupBy`, `cardTitle`, `titleField`, `swimlaneField`, `coverImageField` on the
board, and `defaultView` (the spec's three-member enum) + `locale` on the
calendar. Both tags of each block now spread ONE shared list, so the two
published surfaces cannot drift apart by hand-copy.

The seven matching `UNPUBLISHED_EXEMPTIONS` entries are deleted in the same
change and both halves of the shrink-only backlog ceiling move 15 -> 8.

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-OYH27h6g.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) 497.99KB 113.90KB
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) 47.98KB 13.51KB
plugin-charts (index.js) 71.39KB 19.92KB
plugin-chatbot (index.js) 193.54KB 46.04KB
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) 53.72KB 15.08KB
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.74KB 1.41KB
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.

1 participant