Skip to content

feat(types,fields)!: camelCase the grid widget's eight field-level keys; the snake_case spellings are refused by name (objectui#11610) - #11614

Merged
objectstack-fleet[bot] merged 3 commits into
mainfrom
claude/issue-11610-grid-keys-camelcase
Oct 4, 2026
Merged

objectstack-fleet[bot] merged 3 commits into
mainfrom
claude/issue-11610-grid-keys-camelcase

Conversation

@objectstack-fleet

@objectstack-fleet objectstack-fleet Bot commented Oct 4, 2026 •

Copy link
Copy Markdown
Contributor

Fixes #11610
Clause-②: yes (narrowing)

The grid widget's eight field-level keys become camelCase in one move across the published type, its zod mirror, the form-field face and every reader. The snake_case spellings retire at once and are refused by name on all three faces, each refusal naming the camelCase key. Ground: objectstack-ai/objectstack#21704 fork 2, ruled B (record 5978663135); claim 5979906990 (seat ruling: refuse by name, no load-time conversion, premised on no stored producer). Dispatch session session_01CPvhwGcirXqBGEdPSb72TZ, mode:subagent.

Premise (measured before any edit): no stored producer outside objectui's own fixtures

  • objectstack origin/main 316be321 (read through git grep on a private ref, nothing edited): the eight snake_case words occur only in comments, test strings and one changeset line. packages/spec component.zod.ts names them in three comments (the line-items guidance block, the customFields fork note, the master-detail sortField retirement note); the retirement test for details.sortField carries one as a must-not-match judge string; conversions/registry.ts, migrations/registry.ts and the retired-key entry for details.sortField carry it in comments. No schema declares or emits any of the eight as a field's metadata, and the spec's FieldType enum has no grid member at all, so the spec cannot store a grid field. The seat's reading is confirmed.
  • objectui at base b508ac50 (every tracked file, git grep on that commit), each hit classified:
    • grid field metadata, READERS: GridField.tsx (the eight reads).
    • grid field metadata, PRODUCERS (in-repo, render-time, not stored): MasterDetailForm and LineItemsPanel (both hand the grid an object cast through any), and apps/console DevLookup (a dev harness).
    • grid field metadata, DECLARATIONS: GridFieldMetadata, the form-field face (FormField), its zod mirror (FormFieldSchema).
    • grid field metadata, STORED fixture: examples/schema-catalog fields-grid/line-items-grid.json (the only stored document).
    • docs: content/docs/fields/grid.mdx; the clipboard-paste RFC (states max_rows / allow_add as GridFieldMetadata's API in its proposed wiring); ADR 0001 (reported below, not edited).
    • tests: seven GridField test files in packages/fields (the triage counted five), the master-detail members test and the line-items total test in packages/plugin-form, three packages/types tests (strict face, form-field coverage, mirror parity comment), the console registry pin's prose.
    • SAME NAME, DIFFERENT READER (not renamed): the ## MAX_ROWS vs MIN_ROWS heading-anchor string in plugin-markdown's toc test; the en locale comment (a comment about the grid's label, updated); every CHANGELOG.md (history, untouched). The master-detail details.sortField the spec retired is a different key on a different object and stays untouched.
  • Existing facts not re-measured: the objectstack#21464 census (0 customFields entries) and the triage's hotcrm / cloud reading (0 hits).

Rename map

FROM TO
min_rows minRows
max_rows maxRows
allow_add allowAdd
allow_delete allowDelete
allow_reorder allowReorder
total_field totalField
add_label addLabel
sort_field sortField

One list holds it: GRID_FIELD_RETIRED_KEYS (new export of @object-ui/types, with its key type GridFieldRetiredKey). The zod arms and the widget both read it; a type-level pin holds its keys equal to the never tombstones and its values equal to the live camelCase members.

The retirement form, per face (refused by name, never stripped)

  • TypeScript. GridFieldMetadata and FormField each declare the eight snake_case keys as never tombstones with a @deprecated note naming the replacement. { type: 'grid', name: 'lines', min_rows: 1 } typed GridFieldMetadata does not compile; on the docs face the gate's compile of grid.mdx reports TS2322: Type 'number' is not assignable to type 'undefined' for the same key.
  • zod (@object-ui/types/zod). FormFieldSchema declares each snake_case key as an aliasKeyRefusal arm. On the mirror, the tolerant face (safeValidateSchema, the objectui validate door) and the strict authoring face, fields[0].min_rows: 1 answers exactly one issue, invalid_type at fields.0.min_rows, whose message leads Unrecognized key(s) on this form field: `min_rows`. Did you mean `min_rows` → `minRows`? and goes on to say the value stays the same. Any value is refused (the refusal is by name). The camelCase key with the same value parses and is kept on all three doors.
  • The widget (@object-ui/fields). GridField is now a thin gate over the grid body: a field whose metadata carries any snake_case key (with a defined value) is drawn as RetiredGridFieldKeys instead of the grid, an inline role="alert" (data-testid="grid-field-retired-keys") reading for example [object-ui] Grid field `lines` carries retired snake_case key(s): `min_rows` → `minRows`. …, plus the same text on console.error once per message. This is RetiredFieldTombstone's settled shape in this package: nothing thrown, nothing silently substituted, rows untouched (onChange is never called).

Producers move with it: both plugin-form adapters write the camelCase keys, and their grid object is now satisfies Partial of GridFieldMetadata instead of as any, so a snake_case key written there no longer compiles.

Pins and guards, with ablation readings

All ablations ran through the anchored mutation tool (anchor must hit; blob change and restore proven on disk; restore leg git checkout HEAD, blob after restore equal to the HEAD blob, git diff HEAD empty), from committed state.

  1. The eight camelCase keys draw, each through the real GridField with a lit control without the key (GridField.retiredSnakeKeys-11610.test.tsx): minRows disables every Remove at the minimum; maxRows disables Add and every Duplicate at the maximum; allowAdd: false removes Add and Duplicate; allowDelete: false removes Remove; allowReorder: false removes the drag handles; totalField draws the footer total (30); addLabel labels Add; sortField stamps the new indices after a drag. Through the real form path too: fields-grid-keys-camelcase-11610.test.tsx renders the catalog fixture through SchemaRenderer and the form renderer (label Add line item, total 109.97, two drag handles / Duplicate / Remove, all enabled), with the snake_case respelling as its control.
  2. A snake_case key is answered by the refusal, never dropped: per key, the alert names the key and its replacement, the grid is not drawn, console.error is called once with the alert's text; edges pinned: several keys in one alert in map order, a snake key beside its camelCase twin is still refused, an undefined value is not refused (as on the TS and zod faces), a nameless field reads "This grid field", a second mount logs nothing new.
    • ABLATION (widget gate disabled): 12 refusal tests red, the 10 others (the eight draws, the lit control, the undefined row) green. Restored.
  3. zod refusal by name (grid-field-keys-camelcase-11610.test.ts, three doors × eight keys, plus lit controls and an any-value row).
    • ABLATION (delete the min_rows arm): 8 tests red (the min_rows row and the any-value row on each of the three doors, and both form-field-zod-coverage rows). Restored.
  4. TS tombstones, each with a deletion guard that does not lean on an index signature (same file, read by tsc -p tsconfig.test.json): an Equal row per member on both faces, a keyof membership row on FormField, and the map-equals-tombstones rows.
    • ABLATION A (delete all eight GridFieldMetadata tombstones): the eight Equal rows red, TS2339 naming each key; the two map rows red; the strict-face lit-control row red; the map's satisfies red (TS2561). ⚠️ The eight @ts-expect-error rows over fresh GridFieldMetadata literals stayed SILENT: with the member gone the key is still an excess property and the directive swallows it. That is the objectui#8347 lesson, reproduced; the Equal rows are what catch the deletion.
    • ABLATION A1 (delete total_field alone): exactly its Equal row (TS2339), the map row and the satisfies red.
    • ABLATION B (delete all eight FormField tombstones): the eight Equal rows and the keyof row red, four TS2578 unused @ts-expect-error (the index signature now accepts the key), the strict-face by-reference rows red, and the mirror-parity ratchet red on FormFieldSchema.
    • ABLATION B1 (delete sort_field alone): its Equal row, the keyof row, its TS2578, the strict-face rows and the parity ratchet red, naming sort_field.
    • Every leg restored with blob equal to HEAD; the green tsc -p tsconfig.test.json runs are in the gates below.
  5. Docs face: respelling one key of grid.mdx's snippet snake_case turns pnpm check:doc-snippets red at that line (TS2322), restored green.

Repo-wide pin sweep: every assertion that read a grid key by its snake_case spelling now asserts the camelCase key's substance (the master-detail grid object's exact key set and values, the strict face's accept and wrong-type rows, the coverage key set, the widget draws). The rejection rows for genuinely wrong values (a string minRows, a numeric addLabel, …) stay, respelled; the guarded surface grows by the eight refusals.

Files and landing point

The claim's surface, plus two adjacent files the census named: apps/console DevLookup.tsx (a producer) and its registry pin's prose; packages/i18n en locale (one comment naming add_label); the RFC. The landing point is the claim's list; no producer was found in another package.

ADR and RFC

  • docs/adr/0001-master-detail-subform.md (governed, ⛔ not edited) names the keys in three sentences: "Honors min_rows / max_rows / allow_add / allow_delete from the …", "Drag reorder of lines (allow_reorder), grouping/subtotal rows." and "… drag-to-reorder (a sort_field …". Reported for whoever owns that record.
  • content/docs/rfcs/0001-clipboard-paste.md (status Draft) stated max_rows "from GridFieldMetadata" and wired field.allow_add / field.max_rows as the current API, so it is edited to the camelCase keys.

Serial and overlap

The claim's serial step is done. objectui#11605's PR #11612 (fd060f07) and objectui#8347's PR #11607 (b403bb36) both landed. main at b403bb36 is merged into this branch by merge commit 8b56ef26. Its combined diff is empty, so no hunk was hand-resolved.

The four files this branch shares with them carry main's hunks outside this branch's hunks: packages/i18n/src/locales/en.ts, packages/types/src/form.ts, packages/types/src/index.ts and zod-mirror-parity.test.ts. The contract review on the merged head (5981193754) reads every hunk of this branch as byte-identical to the reviewed head. The gate verdict on the merged head is its own check-runs.

Gates (the dev's local readings, tree 82b400f9; the current head's verdict is its check-runs)

  • type-check, package and test program: @object-ui/types, @object-ui/fields, @object-ui/plugin-form, @object-ui/i18n, @object-ui/example-schema-catalog, @object-ui/console — all exit 0 (the console run after building @object-ui/plugin-tree, which its import graph needs). The fields and plugin-form test programs list the touched test files (--listFilesOnly).
  • vitest, repo-root form: packages/types/ 353 files / 9482 tests passed; packages/fields/ 233 passed + 1 skipped / 3647 passed + 7 skipped; packages/plugin-form/ 163 / 1889 passed + 1 skipped; examples/schema-catalog/ + the console registry pin + packages/i18n/ 121 / 3807 passed + 13 skipped. The types / fields / plugin-form runs measured a working tree byte-identical to 82b400f9 (its two later-committed files, the changeset and the catalog test, were already on disk); the rest ran at 82b400f9.
  • lint (eslint with the root config, as each package's lint runs it) over the 25 touched TS/TSX files, counted from --format json: 0 errors; the one finding on a touched line is the pre-existing as any warning in GridField.test.tsx's fixture. Narrowing statement: the population is every file under the touched packages, read by the one root eslint.config.js; it enables no type-aware linting and no custom rule reads the filesystem, so this diff cannot move a verdict on an untouched file. pnpm lint itself is CI's.
  • check:doc-snippets (777 blocks judged, 0 failed), check:doc-examples, check:readme-exports, check:new-line-citations (0 new), check:control-bytes, check:spec-symbols, check:designer-field-key-parity, check:prompt-keys, check:handler-key-reads, check:unreferenced-sources, check:phantom-deps, check:self-import, check:test-path-roots, check:doc-types, check:doc-example-ids, check:doc-fences, check:doc-example-readers, check:i18n-drift, check:i18n-keys, check:esm-specifiers, check:side-effects-array: all exit 0.
  • changeset checkers: presence, no-major, fixed, overwrite, claims, pending-literals: all exit 0. .changeset/11610-grid-keys-camelcase.md: @object-ui/types and @object-ui/fields minor, breaking banner, FROM → TO for all eight, Clause-②: yes (narrowing).
  • Governed surface: node scripts/check-governed-queue-guard.mjs --test over the diff answers NOT GOVERNED.

Acceptance notes

The dispatching session is https://claude.ai/code/session_01CPvhwGcirXqBGEdPSb72TZ.


Generated by Claude Code

claude added 2 commits October 4, 2026 12:51
…eld-level keys; the snake_case spellings are refused by name (objectui#11610)

`GridFieldMetadata`, the form-field face and its zod mirror now declare
`minRows`, `maxRows`, `allowAdd`, `allowDelete`, `allowReorder`,
`totalField`, `addLabel` and `sortField`. The retired snake_case spellings
stay declared only to refuse: `?: never` tombstones on both TS faces, an
alias refusal naming the camelCase key on the zod mirror, and a named
refusal the `grid` widget draws instead of the grid. One list,
`GRID_FIELD_RETIRED_KEYS`, feeds the zod arms and the widget.

Both plugin-form adapters, the console's dev harness, the catalog
fixture, the grid doc page and the clipboard-paste RFC move to the new
spellings; the adapters' grid objects are now checked against
`GridFieldMetadata` instead of cast through `any`.

Claude-Session: https://claude.ai/code/session_01CPvhwGcirXqBGEdPSb72TZ
Co-authored-by: Claude <noreply@anthropic.com>
…aws its camelCase keys through the real form path; declare the rename (objectui#11610)

Claude-Session: https://claude.ai/code/session_01CPvhwGcirXqBGEdPSb72TZ
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions

github-actions Bot commented Oct 4, 2026

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Eager closure (gzip, 331 chunks) 3313.3 KB 3330.4 KB
Main entry chunk (gzip) 151.4 KB 350 KB
Entry file index-B2giHo4b.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) 17.22KB 6.37KB
app-shell (runtime-config.js) 22.52KB 7.86KB
app-shell (types.js) 0.01KB 0.04KB
app-shell (urlParams.js) 10.11KB 3.87KB
auth (ActiveOrganizationStorage.js) 27.95KB 10.04KB
auth (AuthContext.js) 0.31KB 0.24KB
auth (AuthGuard.js) 2.07KB 1.00KB
auth (AuthProvider.js) 40.22KB 10.61KB
auth (AuthShell.js) 3.49KB 1.40KB
auth (ForgotPasswordForm.js) 12.21KB 3.45KB
auth (LoginForm.js) 18.17KB 5.40KB
auth (PreviewBanner.js) 0.90KB 0.50KB
auth (RegisterForm.js) 6.72KB 2.24KB
auth (SocialSignInButtons.js) 9.70KB 3.93KB
auth (UserMenu.js) 3.39KB 1.21KB
auth (auth-gate-events.js) 1.29KB 0.66KB
auth (authStyles.js) 5.04KB 1.72KB
auth (createAuthClient.js) 40.70KB 10.94KB
auth (createAuthenticatedFetch.js) 8.54KB 3.46KB
auth (index.js) 3.63KB 1.64KB
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) 27.11KB 7.97KB
collaboration (LiveCursors.js) 3.17KB 1.27KB
collaboration (PresenceAvatars.js) 6.28KB 2.60KB
collaboration (PresenceProvider.js) 2.79KB 1.13KB
collaboration (index.js) 1.68KB 0.73KB
collaboration (useCollaborationTranslation.js) 6.50KB 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) 572.97KB 137.34KB
core (index.js) 10.00KB 3.96KB
create-plugin (index.js) 27.94KB 9.51KB
data-objectstack (index.js) 232.57KB 64.51KB
fields (index.js) 262.75KB 66.62KB
i18n (LocalizationContext.js) 1.76KB 0.96KB
i18n (builtinAggregateLabels.js) 0.86KB 0.49KB
i18n (currency.js) 2.59KB 1.22KB
i18n (fallbackInterpolation.js) 6.25KB 2.77KB
i18n (i18n.js) 8.87KB 3.64KB
i18n (index.js) 5.24KB 2.27KB
i18n (pickLocalized.js) 9.86KB 3.95KB
i18n (provider.js) 39.35KB 12.88KB
i18n (translateFn.js) 0.20KB 0.18KB
i18n (useDisplayLocale.js) 3.52KB 1.76KB
i18n (useObjectLabel.js) 35.66KB 9.49KB
i18n (useSafeTranslation.js) 7.14KB 2.92KB
layout (index.js) 39.47KB 11.25KB
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) 6.62KB 2.45KB
mobile (useResponsive.js) 0.72KB 0.42KB
mobile (useSpecGesture.js) 5.52KB 2.10KB
mobile (useTouchTarget.js) 1.01KB 0.54KB
permissions (MePermissionsProvider.js) 13.86KB 5.00KB
permissions (PermissionContext.js) 0.31KB 0.25KB
permissions (PermissionGuard.js) 0.89KB 0.45KB
permissions (PermissionProvider.js) 6.52KB 2.26KB
permissions (discardProofCache.js) 1.04KB 0.55KB
permissions (evaluator.js) 8.33KB 3.07KB
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) 16.04KB 3.92KB
plugin-calendar (index.js) 53.17KB 15.46KB
plugin-charts (index.js) 83.60KB 22.89KB
plugin-chatbot (index.js) 198.22KB 46.97KB
plugin-dashboard (index.js) 142.89KB 38.62KB
plugin-designer (index.js) 231.41KB 48.84KB
plugin-detail (index.js) 245.74KB 64.59KB
plugin-editor (index.js) 2.23KB 1.05KB
plugin-form (index.js) 175.73KB 45.36KB
plugin-gantt (index.js) 179.16KB 45.06KB
plugin-grid (index.js) 235.63KB 64.77KB
plugin-kanban (index.js) 49.59KB 15.58KB
plugin-list (index.js) 116.46KB 28.93KB
plugin-map (index.js) 25.60KB 8.62KB
plugin-markdown (index.js) 13.88KB 4.80KB
plugin-report (index.js) 44.12KB 12.29KB
plugin-timeline (index.js) 38.80KB 11.71KB
plugin-tree (index.js) 14.51KB 5.15KB
plugin-view (index.js) 90.23KB 22.73KB
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.81KB 3.58KB
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) 120.63KB 39.56KB
react (data-invalidation.js) 5.05KB 2.08KB
react (index.js) 4.50KB 2.06KB
react (schema-input.js) 4.31KB 2.07KB
react (spec-input.js) 0.20KB 0.18KB
sdui-parser (body-dialect.js) 4.50KB 1.99KB
sdui-parser (codegen.js) 9.45KB 3.76KB
sdui-parser (dashboard-widget-options.js) 3.08KB 1.30KB
sdui-parser (index.js) 6.06KB 2.68KB
sdui-parser (input-type.js) 2.84KB 1.40KB
sdui-parser (parse.js) 25.28KB 7.80KB
sdui-parser (provenance.js) 3.84KB 1.90KB
sdui-parser (types.js) 0.28KB 0.23KB
sdui-parser (validate.js) 21.42KB 7.05KB
types (ai.js) 4.39KB 2.17KB
types (api-types.js) 0.20KB 0.18KB
types (app.js) 4.12KB 1.61KB
types (authoring-nodes.js) 0.20KB 0.19KB
types (base.js) 0.20KB 0.18KB
types (blocks.js) 0.20KB 0.18KB
types (cloud.js) 0.20KB 0.18KB
types (complex.js) 4.44KB 2.07KB
types (crud.js) 0.20KB 0.18KB
types (dashboard-filter-alias.js) 6.23KB 2.74KB
types (dashboard-widget-layout.js) 2.06KB 0.96KB
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) 1.13KB 0.65KB
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) 5.36KB 2.51KB
types (layout.js) 0.20KB 0.18KB
types (managed-by.js) 0.19KB 0.18KB
types (mobile.js) 5.00KB 2.39KB
types (navigation.js) 0.20KB 0.18KB
types (objectql.js) 0.20KB 0.18KB
types (overlay.js) 0.20KB 0.18KB
types (permissions.js) 2.52KB 1.31KB
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) 4.99KB 1.96KB
types (spec-ui-namespace.js) 0.20KB 0.19KB
types (strict-authoring-face.js) 19.93KB 7.25KB
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

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 82b400f9b0f7911d97b533d96486e32007bbd819
Local-runs: none

Reviewed from the card (#11610: body, triage grade 5979675762, claim 5979906990, dev report 5980586583, seat answer 5980610524), the PR (body, 29-file list, net diff against main at the base b508ac50), the check-runs on the head, and read-only git grep / git show on objectui origin/... refs and on @objectstack/spec at objectstack origin/main 316be321 (the same commit the dev read). Nothing was built, run or re-run. The dispatch order and the dispatching seat's own conclusions were not inputs; where this record agrees with them it is because the diff does.

① Derived judgments

1. The rename of the eight GridFieldMetadata keys, and the by-reference members on FormField and FormFieldSchema — right. GridFieldMetadata renames all eight (minRows, maxRows, allowAdd, allowDelete, allowReorder, totalField, addLabel, sortField), each keeping its value type. FormField takes each one by reference (GridFieldMetadata['minRows'] and so on), so the form-field face cannot drift from the grid's type; the strict-face test's Equal rows pin that per member on both faces. FormFieldSchema renames the eight zod members with the same value schemas as before. No ninth key appears; no key's type moves. The Spec Main Shape Gate, Type Check, Lint and the eight Test shards are green on this head.

2. The new GRID_FIELD_RETIRED_KEYS export — right. One as const map from each snake_case spelling to its camelCase member, declared in field-types.ts beside the type it describes, re-exported from the root entry of @object-ui/types with its key type GridFieldRetiredKey. Its satisfies clause holds every key and value to GridFieldMetadata's own members, and the new types test holds the keys equal to the never tombstones and the values equal to the live camelCase members (an Equal row per pair, a lit control that the tombstone set is not empty). It is read by the zod arms and by the widget, so the three faces cannot name different replacements. This is a public-surface widening and the changeset names it. Placement note: the existing ./internal/retired-field-keys subpath is a registry of spec FieldSchema keys for the designer seam's strip sites, a different thing; a root export is the plainer home for a map @object-ui/fields must import, and nothing in the package's exports map argues otherwise.

3. The retirement form on each face — right, and it is a refusal by name on all three, never a strip, never a conversion.

  • TypeScript: both GridFieldMetadata and FormField declare each snake_case key ?: never with an @deprecated note naming the replacement. On FormField this is load-bearing because the interface carries a string index signature typed any: a declared never member outranks it, so { name, type: 'grid', min_rows: 1 } fails to compile there too. Deletion guards do not lean on @ts-expect-error over a fresh literal (which stays silent once the member is gone, the objectui#8347 lesson the dev reproduced under ablation A): an Equal row per member on both faces, a keyof membership row on FormField, and the map-equals-tombstones rows. These type-level rows are in a *.test.ts under src/__tests__/, which the package type-check script reaches through tsc -p tsconfig.test.json (its include is src/**/*.test.ts and src/**/*.test.tsx), so they are compiled, not phantom.
  • zod: each snake_case key is an aliasKeyRefusal arm on FormFieldSchema (retiredGridFieldKey(alias) reads the canonical key from the map). The helper is z.never with the guidance as its error, .optional(), .describe()d with the same string, so the issue is invalid_type at the key's own path, undefined is accepted (the TS twin of a ?: never member, as the mirror-parity test expects), and any other value is refused whatever its type. The tolerant mirror (safeValidateSchema, the objectui validate door) and the strict authoring face (StrictAnyComponentSchema) inherit the arms from the one declaration; the new types test drives all three doors × eight keys with lit controls (the camelCase key with the same value parses and is kept) and an any-value row, and the catalog test's control shows the strict face answering exactly the eight fields.0.KEY paths. The strict-face test records, correctly, that these eight are no longer in its unrecognized_keys list: they stay DECLARED so the strict face answers invalid_type with a prescription rather than a bare unknown-key refusal.
  • Widget: GridField is now a thin gate over GridFieldBody. retiredGridFieldKeysOf(field) collects the map's keys whose value is defined, and when any is present the widget draws RetiredGridFieldKeys (an inline role="alert", data-testid="grid-field-retired-keys", data-retired-keys listing them in map order) instead of the grid, and logs the same sentence once per message on console.error. Nothing is thrown, onChange is never called, and a snake key beside its camelCase twin is still refused (no precedence picks a winner). LineItemsField is the same function (export const LineItemsField = GridField), so the plugin-form path is gated too. The shape follows this package's RetiredFieldTombstone / reportRetiredFieldType discipline. The undefined-is-not-refused edge is drawn in the same place on all three faces, which is the right place: no JSON document carries one.

4. No dual read — right. Every cfg. read in GridField.tsx at the head is camelCase (addLabel twice, allowAdd, allowDelete, allowReorder, columns, maxRows, minRows, sortField, totalField; cfg is field cast to Partial of GridFieldMetadata). A git grep of the whole tree at the head for the eight snake_case words, CHANGELOG.md excluded, finds them only where a retirement must spell them: the tombstones and the map in field-types.ts, the tombstones in form.ts, the eight refusal arms in form.zod.ts, the new and updated tests (as refusal subjects, controls and comments), one comment in GridField.tsx, the retired-keys prose of grid.mdx, the changeset's FROM → TO rows, ADR 0001 (item ③.5), and one unrelated ## MAX_ROWS vs MIN_ROWS heading-anchor string in plugin-markdown's toc test. No reader, no producer, no stored document. The only read of a snake_case key that remains is the widget's gate, and it reads them to refuse.

5. The stored fixture migration — right. examples/schema-catalog fields-grid/line-items-grid.json moves all eight keys with values unchanged. The new catalog test renders the fixture through the real SchemaRenderer and form renderer (Add line item, footer total 109.97, two drag handles / Duplicate / Remove, all enabled, no refusal drawn), validates it on the strict face, and uses the snake_case respelling of the same document as its control (refused at the eight paths; the form draws the grid's refusal instead of the grid). That is the fixture proven through the path a stored document takes, not just rewritten.

6. The totalField meaning (the CHILD column summed, unlike the spec's record:line_items PARENT field) — stated on each face the seat's answer names.

  • TS declaration GridFieldMetadata.totalField: states the CHILD column summed, that it carries the spec's amountField, and the ⚠️ same-name-different-meaning contrast with the spec's PARENT totalField. FormField.totalField says "The CHILD column summed into the footer total" and links to that declaration.
  • zod mirror: the .describe() reads "Name of the CHILD column a grid field sums into its footer total; read only by the grid widget". The meaning is stated; the homonym contrast is not spelled on this face (it is one .describe() string, and the strings on this schema are one line each). This meets the seat's bar — the meaning, on the face — and I note it rather than fail it; the spec seat's report on [Decision] spec(ui): the five ComponentPropsMap members #21464's last stage stopped on — object-metric drillDown.report, object-form customFields, form sections, object-timeline items, action:group / action:menu members objectstack#21704 is where the contrast must be carried in words (the seat's answer already says so).
  • grid.mdx: states both the meaning and the contrast.
  • Changeset: states both, under Migration.
  • Also both producers (LineItemsPanel, MasterDetailForm) carry the contrast as a comment at the hand-off, and the console registry pin's prose was corrected to say that the detail's own totalField is not forwarded.

7. The premise (no stored producer outside objectui's fixtures) — holds, on what I re-measured. @objectstack/spec at objectstack origin/main 316be321: the eight words occur nine times in five files, every one a comment or a test string (component.zod.ts three comments; conversions/registry.ts, migrations/registry.ts and the details.sortField retired-key entry, one comment each; the master-detail detail-sort-field retirement test, prose and one must-not-match judge string). No schema declares or emits any of them, and the spec's FieldType enum has no grid member ('grid' appears in field.zod.ts only as a value of inlineEdit), so the spec cannot store a grid field's metadata under these keys. objectui at the base b508ac50, tests / docs / changelogs excluded: the only carriers were the widget's reads, the two plugin-form adapters, DevLookup, the three declarations and the catalog fixture — exactly the dev's census, and every one moves in this diff. Not re-measured here: the objectstack#21464 census (0 customFields entries), the triage's hotcrm / cloud reading (0 hits), and objectstack paths outside packages/spec (the dev's whole-tree reading: comments, test strings, one changeset line).

Gate verdicts (the check-runs on this head, read three times):

  • 2026-10-04T13:42:39Z — 42 runs, 32 completed (29 success, 3 skipped), 10 in progress (Type Check, Spec Main Shape Gate, Test shards 1–8).
  • 2026-10-04T13:44:34Z — 42 runs, 34 completed (31 success, 3 skipped), 8 in progress (Type Check, Test shards 1–7).
  • 2026-10-04T13:50:14Z — 43 runs, 43 completed: 40 success, 3 skipped (Test (coverage), its shard matrix, dependabot), 0 failure, 0 cancelled. Among the successes: Lint, Type Check, Test and Test (shard 1/8) … Test (shard 8/8), Test (dist pins), Build & E2E, Build Docs, Doc Snippet Type Check, Doc Component Type Check, Spec Main Shape Gate, Changeset Declaration, Changeset Bump Policy, Changeset Fixed Group Check, Changeset Overwrite Report, Changeset Claim Re-read, README Export Check, Line Citation Gate, Governed Surface Queue Guard, Bundle Analysis (the console performance budget comment on the PR reads PASS, 3313.3 KB of 3330.4 KB).
    Every run names head 82b400f9. The verdict below rests on the third reading.

② Semver level

Level — right for this repository. The changeset declares @object-ui/types: minor and @object-ui/fields: minor. objectui's version policy (AGENTS.md, the version-alignment section) keeps the fixed group's major equal to the @objectstack family's and ships objectui's own breaking changes as minor with the breaking semantics stated in the body; scripts/check-changeset-no-major.mjs refuses a major. The body opens with a BREAKING banner naming both packages, says in words why the bump is minor under this model, carries the FROM → TO row for all eight keys and the one-line fix ("Rename each key; its value stays the same"), and states per face what a snake_case key now meets. Changeset Bump Policy and Changeset Fixed Group Check are green on the head. The PR title carries the !.

Clause-② on both carriers — right and consistent. Read with the fleet's one reader (readClause2Line from scripts/pm/clause2-line.mjs): the PR body line Clause-②: yes (narrowing) reads declared · yes · narrowing; the changeset line **Clause-②: yes (narrowing)**: the camelCase spellings widen each face, and the snake_case spellings narrow it. reads declared · yes · narrowing. The arm matches the diff: the accept set narrows (eight spellings refused on the tolerant and strict zod faces, on both TS faces, and in the widget) and the public surface widens (eight camelCase keys accepted with the old value types; GRID_FIELD_RETIRED_KEYS and GridFieldRetiredKey exported), so yes is the truthful value and (narrowing) the breaking arm. Both carriers agree with each other and with the BREAKING banner. The pending 11564 changeset on main uses the same bold inline form, so the spelling is this repo's.

The changeset names @object-ui/types and @object-ui/fields only, while plugin-form source changed — judged right. The adapters' change is the hand-off to LineItemsField: the same five (LineItemsPanel) and five (MasterDetailForm) keys, now camelCase and typed satisfies Partial of GridFieldMetadata instead of cast through any, so a retired spelling or an undeclared key there fails to compile. Nothing a consumer of @object-ui/plugin-form authors or reads moves: no node type, prop, schema member or export changes, and what the panel and the master-detail form draw is pinned unchanged (the members test's sorted key set and values, respelled; the line-items total test). Three facts make the omission the correct declaration rather than a gap: (a) objectui's presence rule asks for ONE declaration when a fixed-group package's src/ changes and admits a purely internal change declaring nothing (empty front matter), so naming only the packages whose surface moves is the rule's own form, and Changeset Declaration is green; (b) the fixed group versions plugin-form in lockstep with types and fields, so no published pairing of an old adapter with the new widget, or the reverse, can arise from leaving it unnamed — the coupling the camelCase hand-off introduces is carried by the group, not by a CHANGELOG line; (c) the body still tells the reader of the types / fields CHANGELOG that the adapters moved. The same reasoning covers @object-ui/console (DevLookup, a dev harness; one key) and @object-ui/i18n (one comment). No ADR-0087 disposition marker is owed on this carrier: that gate reads objectstack's spec changesets, none of objectui's six changeset checks reads such an arm, and the repo's pending narrowing changeset carries none.

③ Boundary flags

Dev deviations:

  1. Surface beyond the claim's list (console registry pin prose, en.ts comment, index.ts export line, the new catalog test, the Draft RFC) — accepted. Each is a census hit under the claim's own clause ("any other reader … the claim's census finds"); the export line is what makes the map public; the catalog test is the fixture's real-path pin; the RFC sits under content/docs/rfcs/, not a governed path, is status Draft, and stated max_rows / allow_add as GridFieldMetadata's current API, so leaving it would have taught a refused spelling.
  2. Changeset omits plugin-form — accepted, judged in ②.
  3. objectstack read through a private fetched ref in the shared checkout, no file edited — accepted. That is the read shape AGENTS.md prescribes ("fetch into a ref you own and read that"); the commit the dev read, 316be321, is objectstack origin/main as I read it.

Dev open_questions:
4. The totalField homonym — answered on the card (seat answer 5980610524): A, keep totalField; the eight names are fixed by the maintainer's ruling and are not reopened; the meaning is to be stated on each face. Verified in ①.6: the meaning is on all four faces the answer names; the contrast is on the TS declaration, grid.mdx, the changeset, both adapters and the console pin prose, and not on the zod .describe() string. Carried forward, per the answer, to the seat's report on objectstack-ai/objectstack#21704 so the spec declares the grid key as the child column and not as the record:line_items sibling.

Flags from this review:
5. ⚠️ main moved before this PR opened, and the claim's serial step is still owed — escalated to the owning seat, not a defect in the contract. The claim says: if this card touches en.ts or packages/plugin-form, it merges main after #11605's PR #11612 lands. It touches both. #11612 merged as fd060f07 at 2026-10-04T13:26:24Z; this PR opened at 2026-10-04T13:34:35Z on base b508ac50, and its body says main had not moved past the base when it was opened. That sentence is contradicted by the commit time; the dev's fetch must have predated the landing. The exposure is small: the only path both change is packages/i18n/src/locales/en.ts, where this PR rewrites one comment word near the grid chrome keys and #11612 adds keys some seven hundred lines below; packages/plugin-form files #11612 touched are none of the four this PR touches; GitHub reads the PR mergeable: true (mergeable_state: behind); and ci.yml / lint.yml check out with actions/checkout at its default ref, which on pull_request is the merge commit, so the green gates above most likely already ran on the head merged with current main. Still, the claim's step is explicit: before arming, merge origin/main (now fd060f07) into the branch; the queue's merge_group build then re-validates on the rebuilt generation. That merge produces a new head, and this record names 82b400f9 only — a record for the new head is the lane's call. #8347's PR #11607 has not landed, so the claim's "merge again after #11607 lands" is not yet due.
6. ADR 0001 names six of the snake_case keys in three sentences (docs/adr/0001-master-detail-subform.md, a Tier H governed path). The dev reported it and did not edit it — the right call under Prime Directive #14. Escalated: whether the dated record gets an amendment note is the maintainer's; it does not block this landing, and a stale sentence in a dated decision record is not a contract defect.
7. Three packages/spec comments in objectstack still name the grid's snake_case keys (the line-items guidance block, the customFields fork note, the master-detail sortField retirement note). Carrier: the spec seat's stage on objectstack-ai/objectstack#21704, which also adds the camelCase keys to the runtime form field. Correctly not touched here.
8. DevLookup.tsx keeps its as any field — a console dev harness; only the key moved. Noted, no action.
9. Governed guard — NOT GOVERNED, confirmed from the file list: the 29 paths are under .changeset/, apps/console/src/, content/docs/, examples/schema-catalog/, packages/fields/src/, packages/i18n/src/, packages/plugin-form/src/, packages/types/src/; none under docs/adr/, docs/NORTH-STAR.md, .claude/, skills/, AGENTS.md or CLAUDE.md. Governed Surface Queue Guard is green. The PR is same-repo (head and base both objectstack-ai/objectui), 1,477 changed lines, draft, no auto-merge armed; this record changes none of that.

Implemented-by: claude/issue-11610-grid-keys-camelcase
Reviewed-by: session_01CPvhwGcirXqBGEdPSb72TZ

VERDICT: PASS


Generated by Claude Code

@github-actions

github-actions Bot commented Oct 4, 2026

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Eager closure (gzip, 331 chunks) 3313.9 KB 3330.4 KB
Main entry chunk (gzip) 151.4 KB 350 KB
Entry file index-BJvlwxTW.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) 17.22KB 6.37KB
app-shell (runtime-config.js) 22.52KB 7.86KB
app-shell (types.js) 0.01KB 0.04KB
app-shell (urlParams.js) 10.11KB 3.87KB
auth (ActiveOrganizationStorage.js) 27.95KB 10.04KB
auth (AuthContext.js) 0.31KB 0.24KB
auth (AuthGuard.js) 2.07KB 1.00KB
auth (AuthProvider.js) 40.22KB 10.61KB
auth (AuthShell.js) 3.49KB 1.40KB
auth (ForgotPasswordForm.js) 12.21KB 3.45KB
auth (LoginForm.js) 18.17KB 5.40KB
auth (PreviewBanner.js) 0.90KB 0.50KB
auth (RegisterForm.js) 6.72KB 2.24KB
auth (SocialSignInButtons.js) 9.70KB 3.93KB
auth (UserMenu.js) 3.39KB 1.21KB
auth (auth-gate-events.js) 1.29KB 0.66KB
auth (authStyles.js) 5.04KB 1.72KB
auth (createAuthClient.js) 40.70KB 10.94KB
auth (createAuthenticatedFetch.js) 8.54KB 3.46KB
auth (index.js) 3.63KB 1.64KB
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) 27.11KB 7.97KB
collaboration (LiveCursors.js) 3.17KB 1.27KB
collaboration (PresenceAvatars.js) 6.28KB 2.60KB
collaboration (PresenceProvider.js) 2.79KB 1.13KB
collaboration (index.js) 1.68KB 0.73KB
collaboration (useCollaborationTranslation.js) 6.50KB 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) 572.97KB 137.34KB
core (index.js) 10.00KB 3.96KB
create-plugin (index.js) 27.94KB 9.51KB
data-objectstack (index.js) 232.57KB 64.51KB
fields (index.js) 262.75KB 66.62KB
i18n (LocalizationContext.js) 1.76KB 0.96KB
i18n (builtinAggregateLabels.js) 0.86KB 0.49KB
i18n (currency.js) 2.59KB 1.22KB
i18n (fallbackInterpolation.js) 6.25KB 2.77KB
i18n (i18n.js) 8.87KB 3.64KB
i18n (index.js) 5.24KB 2.27KB
i18n (pickLocalized.js) 9.86KB 3.95KB
i18n (provider.js) 39.35KB 12.88KB
i18n (translateFn.js) 0.20KB 0.18KB
i18n (useDisplayLocale.js) 3.52KB 1.76KB
i18n (useObjectLabel.js) 35.66KB 9.49KB
i18n (useSafeTranslation.js) 7.14KB 2.92KB
layout (index.js) 39.47KB 11.25KB
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) 6.62KB 2.45KB
mobile (useResponsive.js) 0.72KB 0.42KB
mobile (useSpecGesture.js) 5.52KB 2.10KB
mobile (useTouchTarget.js) 1.01KB 0.54KB
permissions (MePermissionsProvider.js) 13.86KB 5.00KB
permissions (PermissionContext.js) 0.31KB 0.25KB
permissions (PermissionGuard.js) 0.89KB 0.45KB
permissions (PermissionProvider.js) 6.52KB 2.26KB
permissions (discardProofCache.js) 1.04KB 0.55KB
permissions (evaluator.js) 8.33KB 3.07KB
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) 16.04KB 3.92KB
plugin-calendar (index.js) 53.17KB 15.46KB
plugin-charts (index.js) 84.26KB 23.05KB
plugin-chatbot (index.js) 198.22KB 46.97KB
plugin-dashboard (index.js) 143.54KB 38.79KB
plugin-designer (index.js) 231.41KB 48.84KB
plugin-detail (index.js) 245.74KB 64.59KB
plugin-editor (index.js) 2.23KB 1.05KB
plugin-form (index.js) 176.65KB 45.50KB
plugin-gantt (index.js) 179.16KB 45.06KB
plugin-grid (index.js) 235.92KB 64.87KB
plugin-kanban (index.js) 50.06KB 15.74KB
plugin-list (index.js) 116.72KB 29.10KB
plugin-map (index.js) 25.60KB 8.62KB
plugin-markdown (index.js) 13.88KB 4.80KB
plugin-report (index.js) 44.12KB 12.29KB
plugin-timeline (index.js) 38.80KB 11.71KB
plugin-tree (index.js) 14.51KB 5.15KB
plugin-view (index.js) 90.23KB 22.73KB
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.81KB 3.58KB
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) 120.63KB 39.56KB
react (data-invalidation.js) 5.05KB 2.08KB
react (index.js) 4.50KB 2.06KB
react (schema-input.js) 4.31KB 2.07KB
react (spec-input.js) 0.20KB 0.18KB
sdui-parser (body-dialect.js) 4.50KB 1.99KB
sdui-parser (codegen.js) 9.45KB 3.76KB
sdui-parser (dashboard-widget-options.js) 3.08KB 1.30KB
sdui-parser (index.js) 6.06KB 2.68KB
sdui-parser (input-type.js) 2.84KB 1.40KB
sdui-parser (parse.js) 25.28KB 7.80KB
sdui-parser (provenance.js) 3.84KB 1.90KB
sdui-parser (types.js) 0.28KB 0.23KB
sdui-parser (validate.js) 21.42KB 7.05KB
types (ai.js) 4.39KB 2.17KB
types (api-types.js) 0.20KB 0.18KB
types (app.js) 4.12KB 1.61KB
types (authoring-nodes.js) 0.20KB 0.19KB
types (base.js) 0.20KB 0.18KB
types (blocks.js) 0.20KB 0.18KB
types (cloud.js) 0.20KB 0.18KB
types (complex.js) 4.44KB 2.07KB
types (crud.js) 0.20KB 0.18KB
types (dashboard-filter-alias.js) 6.23KB 2.74KB
types (dashboard-widget-layout.js) 2.06KB 0.96KB
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) 1.13KB 0.65KB
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) 5.36KB 2.51KB
types (layout.js) 0.20KB 0.18KB
types (managed-by.js) 0.19KB 0.18KB
types (mobile.js) 5.00KB 2.39KB
types (navigation.js) 0.20KB 0.18KB
types (objectql.js) 0.20KB 0.18KB
types (overlay.js) 0.20KB 0.18KB
types (permissions.js) 2.52KB 1.31KB
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) 4.99KB 1.96KB
types (spec-ui-namespace.js) 0.20KB 0.19KB
types (strict-authoring-face.js) 19.93KB 7.25KB
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

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 8b56ef2638e30772f5c758f79d17b8115e3ba627
Local-runs: none

Second record on this PR, for the head that merged main. Reviewed from the card (#11610: body, triage grade 5979675762, claim 5979906990, dev report 5980586583, seat answer 5980610524), the PR (body, 29-file list, net diff against main at the new merge base b403bb36, the earlier record 5980764277 on head 82b400f9), the check-runs on this head, and read-only git diff / git show / git grep on objectui origin/... refs. Nothing was built, run or re-run. The dispatch order and the dispatching seat's conclusions were not inputs.

What moved. One commit, 8b56ef26, a merge of main at b403bb36 into the branch (GitHub's update-branch). Its combined diff (git diff-tree --cc) is empty: no hunk was hand-resolved. main brought #11612 (fd060f07, bound registrations stop requiring objectName) and #11607 (b403bb36, BaseSchema loses [key: string]: any). The branch's net diff against b403bb36 is the same 29 files, 1226 added and 251 removed, as the PR file list.

Byte-identity, per file. I normalised the hunk headers and the index lines of the branch's diff against the old base (b508ac50..82b400f9) and against the new base (b403bb36..8b56ef26) and compared them file by file: all 29 files are IDENTICAL, the four files main also changed included (packages/i18n/src/locales/en.ts, packages/types/src/form.ts, packages/types/src/index.ts, packages/types/src/__tests__/zod-mirror-parity.test.ts). The main-side hunks in those four sit outside this branch's hunks and outside their context lines. So the earlier record's readings of every hunk (its ① items 1 through 7, its ② and its ③) are adopted here for all 29 files, and this record spends its own words on what the merge changes about their MEANING: the semantic coupling with #11607, and whether the main-side hunks of the four shared files touch anything this branch relies on.

① Derived judgments

1. The rename, the by-reference members, the map export, the refusal on each face, the no-dual-read reading, the fixture migration, the totalField meaning, the premise: adopted from the earlier record, byte-identical hunks. Re-read on this head, not only compared: GridFieldMetadata declares the eight camelCase members and eight ?: never tombstones; FormField takes each camelCase member by reference and declares the same eight tombstones; FormFieldSchema declares the eight camelCase arms and eight aliasKeyRefusal arms built from GRID_FIELD_RETIRED_KEYS; GridField is a gate over GridFieldBody that draws RetiredGridFieldKeys when any map key holds a defined value, and every cfg. read in the body is camelCase; the catalog fixture carries the eight camelCase keys and its test drives it through the real SchemaRenderer with the snake_case respelling as control.

2. The never tombstones and their deletion guards still mean what the earlier record said, because neither type ever carried BaseSchema's signature.

3. No test or fixture this branch adds authors a snake_case or undeclared key on a fresh literal whose type #11607 closed. Inventory of every fresh literal in the diff that carries a snake_case or undeclared key, with the type it is written against:

  • grid-field-keys-camelcase-11610.test.ts: eight @ts-expect-error rows typed GridFieldMetadata (no index signature before or after feat(types)!: BaseSchema loses its index signature, so a typed node literal refuses an undeclared key; the compile-fail pin, both directions (objectui#8347) #11607) and four typed FormField (its own signature, untouched). The zod rows build untyped objects (gridEntry, formWith) and hand them to safeParse.
  • GridField.retiredSnakeKeys-11610.test.tsx: withRetired spreads a Record of unknown into a grid and asserts as GridFieldMetadata; the nameless-field row uses as never. Neither is a fresh literal checked against a node type.
  • GridField.fieldKeys-11070.test.tsx: the five pre-existing @ts-expect-error rows over GridFieldMetadata literals (reorderable, amount_field, amountField, allow_duplicate, show_line_numbers) are excess-property refusals on that same signature-less type; this branch changes only their comment text.
  • The i18n chrome tests and GridField.test.tsx: as never / the pre-existing as any, keys renamed only.
  • masterDetailDetailsMembers-8071.test.tsx: mount takes a Record of unknown and spreads it into MasterDetailForm's schema behind as any; the off-list allowAdd: false row is data to the block, not a typed literal.
  • strict-face-read-keys-11070.test.ts: form and GRID_ENTRY are untyped objects fed to the zod doors; the type-level rows range over FormField and GridFieldMetadata only.
  • form-field-zod-coverage.test.ts: computed-key zod inputs.
  • The catalog test: a local FormNode interface (its own [key: string]: unknown) and Entry alias, the fixture parsed from JSON, schema={... as never}. Nothing from @object-ui/types types the literal.
  • The plugin-form adapters: satisfies Partial of GridFieldMetadata, then as GridFieldMetadata. satisfies already excess-checks a fresh literal against a signature-less type; every key there is a declared camelCase member.
  • grid.mdx: the snippet is typed GridFieldMetadata and camelCase; Doc Snippet Type Check is green on this head.
    Also the widget's field prop is FieldMetadata (the field-types union), so GridFieldProps never reaches BaseSchema. Conclusion: every refusal this branch pins is produced by the tombstone, the zod arm or the widget gate, on the same types and for the same reason as before the merge; feat(types)!: BaseSchema loses its index signature, so a typed node literal refuses an undeclared key; the compile-fail pin, both directions (objectui#8347) #11607 adds no second reason to any of them and removes none. The one place where feat(types)!: BaseSchema loses its index signature, so a typed node literal refuses an undeclared key; the compile-fail pin, both directions (objectui#8347) #11607 would have mattered, a FormSchema-typed literal carrying a snake_case grid key at the TOP level of the node, does not occur in this diff.

4. GRID_FIELD_RETIRED_KEYS sits correctly beside #11607's index.ts edits. #11607 touched index.ts in two places: the comment above the authoring-node type exports (near the top of the file) and the PartialSchema docblock (near the bottom, 67 lines removed, a dated reading replaced). This branch's hunk is the field-types.js export block in the middle: GridFieldRetiredKey added to the existing export type { ... } from './field-types.js' list, and a separate value export export { GRID_FIELD_RETIRED_KEYS } from './field-types.js' with its comment. Disjoint regions, merged byte-identically. At this head index.ts imports from field-types.js on exactly those two statements (no export * that would make the line redundant), the value export is the only one of its name tree-wide, and the symbol appears only in the nine files that read it (the two declarations, the zod mirror, the widget, four tests and the changeset). The type export staying in the export type block and the const in a value export is the right split for a runtime map. README Export Check is green on this head, and #11607's edit to packages/types/README.md did not touch the field-types section this export would list under.

5. No comment this branch wrote describes BaseSchema's index signature as live. Every added line of the diff was grepped for index signature, [key: string], BaseSchema, passthrough and 8347. Five hits, none about BaseSchema: the new types test's header (the deletion guards "do not lean on an index signature", and name "the index signature's any once it is gone from FormField"), its FormField deletion-guard docblock ("FormField, which carries an index signature"), its sentence naming "the objectui#8347 lesson" (a directive swallowing an excess-property error), and the form.ts tombstone comment on FormField ("a declared member, so it outranks the index signature above"). Each names FormField's own signature, which is live at this head. The catalog test's [key: string]: unknown is on its local FormNode type. No added line mentions BaseSchema at all. There is nothing to name and nothing to repair.

6. The main-side hunks of the four shared files touch nothing this branch relies on.

7. The AGENTS.md rule #11607 added (#0.1, the TypeScript authoring face is part of the contract; never make a refused key compile with as any or a cast to a Record of unknown) is met by this diff where it applies. The branch removes two as any casts (both plugin-form adapters, now satisfies Partial of GridFieldMetadata) and adds none; the casts on added lines are the renamed pre-existing as any in GridField.test.tsx's fixture and in DevLookup.tsx (a console dev harness, flagged below as before), plus test-only as never / as GridFieldMetadata used to deliver a refused key to the runtime refusal on purpose, which is the shape that rule leaves to the strict zod face and the widget.

Gate verdicts (the check-runs on this head, every run naming 8b56ef26):

  • 2026-10-04T14:33Z: 42 runs, 32 completed (29 success, 3 skipped), 10 in progress (Type Check, Spec Main Shape Gate, Test shards 1 through 8).
  • 2026-10-04T14:35Z: 42 runs, 33 completed (30 success, 3 skipped), 9 in progress.
  • 2026-10-04T14:37Z: 42 runs, 36 completed (33 success, 3 skipped), 6 in progress (Type Check, Test shards 1, 4, 6, 7, 8). Spec Main Shape Gate and shards 2, 3, 5 completed success.
  • 2026-10-04T14:41Z: 43 runs, 43 completed: 40 success, 3 skipped (Test (coverage), its shard matrix, dependabot), 0 failure, 0 cancelled. Among the successes, with their completion times: Type Check (14:39:44Z), Test (14:40:55Z, the aggregate that appeared once the shards finished), Test (shard 1/8) through Test (shard 8/8) (the last at 14:40:42Z), Test (dist pins), Lint, Spec Main Shape Gate, Build & E2E, Build Docs, Doc Snippet Type Check, Doc Component Type Check, Changeset Declaration, Changeset Bump Policy, Changeset Fixed Group Check, Changeset Overwrite Report, Changeset Claim Re-read, README Export Check, Line Citation Gate, Governed Surface Queue Guard, Bundle Analysis (the console performance budget comment for this head, 5981086101, reads PASS, 3313.9 KB of 3330.4 KB).
    Type Check and the Test shards are the gates that see the semantic coupling with feat(types)!: BaseSchema loses its index signature, so a typed node literal refuses an undeclared key; the compile-fail pin, both directions (objectui#8347) #11607 and fix(plugins,react,i18n): bound registrations stop requiring objectName, and a node with neither shows a no-object hint (objectui#11605) #11612 on the merged tree, and they are green on this head. The verdict below rests on the fourth reading.

② Semver level

Unchanged and still right. The changeset .changeset/11610-grid-keys-camelcase.md is byte-identical to the one the earlier record judged: @object-ui/types: minor, @object-ui/fields: minor, the BREAKING banner, the in-words reason the bump is minor under this repository's version-alignment rule, the FROM to TO row for all eight keys, the one-line fix, the per-face statement, the GRID_FIELD_RETIRED_KEYS export named. The earlier record's reasoning on leaving plugin-form, console and i18n unnamed (the fixed group versions them in lockstep; nothing a consumer authors or reads there moves) is adopted.

Clause-② on both carriers, re-read on this head with readClause2Line from the fleet's scripts/pm/clause2-line.mjs: the PR body line Clause-②: yes (narrowing) reads declared · yes · narrowing; the changeset line (the bold inline form) reads declared · yes · narrowing. The arm still matches the diff: the accept set narrows (eight spellings refused on both zod faces, both TS faces and the widget) and the public surface widens (eight camelCase keys, the map and its key type).

What the merge adds on this axis: main now also carries #11607's 8347-baseschema-closed-face.md (@object-ui/types: minor, breaking, Clause-②: yes (narrowing)) and the eight #11605 changesets. Same fixed group, two minor entries on @object-ui/types that compile into one bump; no conflict and no second declaration owed from this PR. Changeset Declaration, Changeset Bump Policy, Changeset Fixed Group Check, Changeset Overwrite Report and Changeset Claim Re-read are green on this head. The PR title keeps the !.

③ Boundary flags

Dev deviations (byte-identical diff, the earlier record's readings adopted):

  1. Surface beyond the claim's list (console registry pin prose, the en.ts comment, the index.ts export line, the new catalog test, the Draft RFC): accepted, each a census hit under the claim's own clause.
  2. The changeset omits plugin-form: accepted, judged in ②.
  3. objectstack read through a private fetched ref in the shared checkout, no file edited: accepted, the read shape AGENTS.md prescribes.

Dev open_questions:
4. The totalField homonym: answered on the card (seat answer 5980610524, option A, keep totalField, state the meaning on each face). Verified on this head as before: the meaning is on the TS declaration, the FormField member, the zod .describe(), grid.mdx and the changeset; the parent-versus-child contrast is on all of those but the one-line zod .describe(). Carried, per the answer, to the seat's report on objectstack-ai/objectstack#21704.

The earlier record's escalations:
5. The claim's serial step (merge main after #11612 and after #11607 land): discharged by this head. Both landed (fd060f07 at 2026-10-04T13:26Z, b403bb36 at 2026-10-04T14:07Z) and 8b56ef26 merges main at b403bb36, cleanly, with no hand-resolved hunk. What remains is prose: the PR body's "Serial and overlap" section still says main had not moved past b508ac50 when the PR opened, that #11607 is an open draft and #11612 is open. That sentence was already contradicted at the earlier record and is more so now. A body paragraph, not a contract defect; the owning seat may refresh it. ⚠️ Said per #9: the merged tree has been measured only by CI on this head, since the dev's local gates ran at 82b400f9 and the merge is GitHub's; the verdict below therefore rests on Type Check and the Test shards for this head, and the queue's merge_group build re-validates on the rebuilt generation.
6. ADR 0001 (docs/adr/0001-master-detail-subform.md, Tier H) names six of the snake_case keys in three sentences; not edited, correctly. Still escalated to the maintainer for an amendment note; it does not block this landing.
7. Three packages/spec comments in objectstack still name the grid's snake_case keys. Carrier: the spec seat's stage on objectstack-ai/objectstack#21704. Correctly not touched here.
8. DevLookup.tsx keeps its pre-existing as any on the field it hands the widget (a console dev harness; one key renamed). #11607's new #0.1 clause names that shape for node literals on the authoring face; this is a field object in a dev page, out of this card's scope, noted for whoever owns the harness. No action here.
9. Governed guard: NOT GOVERNED, confirmed from the unchanged 29-path file list (.changeset/, apps/console/src/, content/docs/, examples/schema-catalog/, packages/fields/src/, packages/i18n/src/, packages/plugin-form/src/, packages/types/src/; nothing under docs/adr/, docs/NORTH-STAR.md, .claude/, skills/, AGENTS.md, CLAUDE.md). The merge commit brings main's own AGENTS.md and skills/** edits into the branch's history, but the PR's diff against its base does not touch them, which is what the guard and the rule judge. Governed Surface Queue Guard is green on this head. The PR is same-repo, 1,477 changed lines, draft, no auto-merge armed; this record changes none of that.

Raised by the merge:
10. The earlier record names 82b400f9 only; this record names 8b56ef26 and supersedes it for landing purposes. Any further push produces a new head and owes a record of its own.
11. The PR body's gates paragraph is headed "tree 82b400f9". It describes the dev's local readings on that tree, which remain true of that tree; the gate verdict on the current head is its own check-runs, read above.

Implemented-by: claude/issue-11610-grid-keys-camelcase
Reviewed-by: session_01CPvhwGcirXqBGEdPSb72TZ

VERDICT: PASS


Generated by Claude Code

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

Projects

None yet

2 participants