Skip to content

feat(types)!: a form view's subforms[].columns entry is the spec's InlineGridColumnSchema, by reference (objectui#11266) - #11618

Merged
objectstack-fleet[bot] merged 4 commits into
mainfrom
claude/issue-11266-subforms-columns-mirror
Oct 4, 2026
Merged

objectstack-fleet[bot] merged 4 commits into
mainfrom
claude/issue-11266-subforms-columns-mirror

Conversation

@objectstack-fleet

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

Copy link
Copy Markdown
Contributor

Fixes #11266
Clause-②: yes (narrowing)

@objectstack/spec 17.6.0 judges FormViewSchema.subforms[].columns with its strict InlineGridColumnSchema (objectstack-ai/objectstack#20927, bee75cebe6). The object-form mirror here still held z.array(z.any()), so objectui validate accepted columns that os validate refuses. This PR makes one column the spec's schema, by reference, on the zod face and on the TypeScript face.

Dispatched under claim 5981407176 (domain:spec, size S). Session: https://claude.ai/code/session_01CPvhwGcirXqBGEdPSb72TZ.

What changes

  • packages/types/src/zod/objectql.zod.ts, the object-form mirror's subforms[] item: columns goes from z.array(z.any()) to z.array(stripImportedDefaults(SpecInlineGridColumnSchema)), imported from @objectstack/spec/data. It crosses the objectui#8317 import boundary like every other spec read. The column schema carries no default, so the crossing is the identity and the member IS the spec's object. No hand-copied column shape.
  • packages/types/src/objectql.ts: ObjectFormSchema's subforms[].columns goes from any[] to the spec's InlineGridColumn (its z.input, the authoring face), by a type-only import. ObjectViewSchema['form'] follows, because it picks from ObjectFormSchema.
  • Docs: content/docs/plugins/plugin-view.mdx and packages/plugin-view/README.md, section "Child records (master-detail)", wrote columns: ['product', 'quantity', 'price']. Bare strings: hydrateColumns reads col.name, the spec refuses them, and with the TypeScript face narrowed that snippet no longer compiles under check:doc-snippets. Both now write { name: ... } objects and say what a column is. ⚠️ These two files are outside the claim's file surface. They are added because this change makes them false: the snippet gate would red on them.
  • .changeset/11266-subforms-columns-mirror.md: @object-ui/types: minor, with a BREAKING banner, FROM / TO migration lines and the Clause-②: yes (narrowing) line.

Premise readings (this worktree, base 2abec3a9, which is origin/main at PR time)

  • pnpm-lock.yaml resolves @objectstack/spec at 17.6.0 only (two entries, both 17.6.0). The package packages/types resolves answers 17.6.0.
  • @objectstack/spec/data exports InlineGridColumnSchema.
  • FormViewSchema.safeParse of a simple form whose subforms[0].columns holds COL:
    • COL { name: 'qty', bogusKey: 1 } is refused: one unrecognized_keys issue at subforms.0.columns.0, keys bogusKey.
    • COL { name: 'qty' } is accepted (the control).
  • Control version: 17.5.0, installed from npm into a scratch directory. Its form view ACCEPTS the bogus key (the z.any() era), so the probe can fire.

The currency + scale finding

  • A column that DECLARES type: 'currency' and carries scale ({ name: 'amount', type: 'currency', scale: 2 }) is refused by the spec's InlineGridColumnSchema itself, at the zod level: one custom issue at path scale. It is pinned. Through the mirror the same column is refused at form.subforms.0.columns.0.scale, and the pin asserts that the message equals the spec schema's own message, read in the same run. No literal text is pinned.
  • An identity-only column carrying scale ({ name: 'amount', scale: 2 }) is ACCEPTED by the spec's column schema, at 17.5.0 and at 17.6.0. Its refusal is defineStack's, which resolves name through childObject to a currency field. The mirror judges a document that does not carry the child object's fields, so it cannot reproduce that check. The pin records the mirror ACCEPTING it, so reaching for it later is a deliberate change. The render-time report from objectui#11238 (reportCurrencyColumnScale in plugin-form's hydrateColumns) stays the backstop. No plugin-form behaviour changes: patch round 1 (b1d035bb) corrects only its prose, see Acceptance note 2.

The @objectstack/spec range

packages/types/package.json already reads ^17.6.0 on origin/main: objectui#11438 (PR #11531, the 17.6.0 lockfile move) raised it. So it does not move here.

Would this change alone need 17.6.0? No. The mirror reads InlineGridColumnSchema only, and 17.5.0 already exports it with the same verdicts on the three probes: bogus key refused with unrecognized_keys, typed currency with scale refused at scale, { name } accepted. What 17.6.0 adds is the spec's own FormViewSchema reading that schema. That is the other door's verdict, not this mirror's. check:spec-floors is green on the built artifact.

The pin

packages/types/src/__tests__/object-form-subforms-columns-11266.test.ts, 17 tests:

  • By reference. The mirror's column schema is toBe stripImportedDefaults(SpecInlineGridColumnSchema), and toBe the spec object itself.
  • On the tolerant face (safeValidateSchema, which objectui validate runs) and on the strict authoring face, through the object-view form slot. That slot is the route an authored document has: subforms is a form-VIEW member, which the object-form row refuses in its properties bag.
    • { name } is accepted: the lit control.
    • A bogus key is refused: one unrecognized_keys issue at the column, keys bogusKey.
    • Typed currency with scale is refused: one custom issue at the column's scale.
    • A bare string is refused: invalid_type at the column.
    • Identity-only with scale is accepted.
  • The flat mirror gives the same codes at the same column.
  • One verdict across the two doors. For each probe column, safeValidateSchema succeeds exactly when the spec's FormViewSchema does, read live.
  • Type level (judged by tsc -p tsconfig.test.json inside type-check): a declared column Equals the spec's InlineGridColumn, and a string column is a @ts-expect-error.

Ablation. The fix was committed first. The ablation used objectstack's scripts/ablation-replace.mjs in wrap mode, inside os-verify-lock.

  • Mutation: anchor columns: z.array(stripImportedDefaults(SpecInlineGridColumnSchema)).optional() became columns: z.array(z.any()).optional(). Anchor count went from 1 to 0, and the blob from 27ae77d7 to d6b4ac83.
  • Result: 11 failed, 6 passed (17). The six that stay green are the two controls, the two identity-only rows, and the two cross-door rows whose expected verdict is accept.
  • Restore: the blob after restore is 27ae77d7, the same as the blob at HEAD, and git diff HEAD is empty. The pin rerun: 17 passed.

Tests moved

  • imported-defaults-8317.test.ts: IMPORTED gains ['InlineGridColumnSchema', SpecInlineGridColumnSchema]. The census "every symbol the mirrors import is covered by the differential above" requires it. The row is measured as the identity (no default, no z.lazy).
  • No zod-mirror-parity KnownDrift or SPEC_DERIVED_PAIRS row moves:
    • ObjectFormSchema was already spec-derived.
    • Its KnownDrift entry names only the five runtime-slot handler keys.
    • The TypeScript and zod faces of columns now carry the same type. Before, it was any[] against z.any(), so the ledger saw nothing either way.
  • object-form-unmirrored-members-6152.test.ts: the subforms row has no columns in either value, keeps its verdicts, and is not edited.

Repo-wide sweep. I grepped for subforms and columns across tests, docs, JSON and scripts. No test anywhere asserts that objectui validate accepts a bogus column or a typed-currency column with scale, so no pin flips. The two doc examples above were the only documents writing a column the narrowed faces refuse.

Gate readings, at head ce55b465

  • pnpm exec vitest run packages/types/ (under the lock): Test Files 355 passed (355), Tests 9492 passed (9492).
  • packages/types type-check (the build program, the examples program and the test program): exit 0.
  • The import side of the narrowed TypeScript face: type-check for @object-ui/plugin-form, @object-ui/plugin-view and @object-ui/app-shell exits 0, and each echoes type-check: Done. They ran against the closure that check:doc-snippets --build-filter names, built with pnpm, not turbo.
  • vitest run packages/cli/ plus the subforms-adjacent consumer tests (deriveMasterDetail.currencyScale-10783, subformHosts, MetadataProvider.merge): 28 files, 390 tests, all passed.
  • eslint --no-inline-config --format json over the four touched TypeScript files: 4 files, 0 errors. The warnings are existing no-explicit-any hits in objectql.ts and objectql.zod.ts; the pin file has 0.
    • This is a narrowing of pnpm lint, and it is declared. Population: eslint.config.js lints **/*.{ts,tsx}, so the two edited .md / .mdx files are outside it. File count: 4, from the JSON output. Invariance: the config sets no parserOptions.project or projectService, so type-aware linting is off, and this diff cannot move the verdict on any untouched file. The full run belongs to CI.
  • check:doc-snippets: 777 of 777 blocks judged, 0 failed.
  • Exit 0 for each of: check:doc-examples, check:doc-types, check:doc-fences, check:doc-example-ids, check:doc-example-readers, docs:check-links.
  • check:new-line-citations: 0 new. check:control-bytes: OK.
  • Changeset checkers, exit 0 for each: check:changeset-claims, check:pending-changeset-literals, changeset:check (fixed and no-major), check-changeset-presence and check-changeset-overwrite.
  • Exit 0 for each of: check:spec-symbols, check:spec-floors, check:installed-pin-claims, check:phantom-deps, check:unused-deps, check:readme-exports, check:test-path-roots, check:component-surface-parity.
    • check:spec-floors was first refused with no-artifact on the unbuilt @object-ui/plugin-tree. I built that package and reran it.
  • check-governed-queue-guard --test over the seven paths: NOT GOVERNED.
  • NOT MEASURED locally, left to CI: the full pnpm test, the tree-wide pnpm lint, and check:published-dist.
  • Patch round 1, at head b1d035bb: @object-ui/plugin-form type-check passes, with its dependency closure built by pnpm, not turbo. vitest run on deriveMasterDetail.currencyScale-10783.test.ts passes, the renamed test among them. eslint over the two touched files reports no errors; its warnings are existing no-explicit-any in code. check:new-line-citations reports no new citation, check:control-bytes passes, and the changeset checkers exit 0. The readings above are at ce55b465; this round touched only the two plugin-form files.

Acceptance notes

  1. The parse output. InlineGridColumnSchema's readonlyWhen and requiredWhen carry the spec's ExpressionInput pipe, so safeValidateSchema returns a column's string predicate as { dialect: 'cel', source }. Measured: readonlyWhen: 'record.locked' comes back as the envelope. The input document is not changed.
    • This is the spec schema's own output, taken by reference.
    • Four other imported crossings already return transformed values. I counted them by walking AnyComponentSchema: a page's slots.header and its visibleWhen, listViews exportOptions, and an action params entry.
    • objectui#8347's Q6 = B ("a string stays a string") was ruled for BaseSchema.visibleWhen. Nothing extends it to imported subtrees.
    • The changeset states it. Flagged for the contract review.
  2. Stale prose, fixed in patch round 1 (b1d035bb). Seat ruling: a PR fixes the sentences it makes false.
    • reportCurrencyColumnScale's docblock in packages/plugin-form/src/deriveMasterDetail.ts, the deriveMasterDetail.currencyScale-10783 test header and one of its test names said a subform's columns is z.array(z.any()) in the object-form mirror and in the spec's FormViewSchema, and that the spec does not judge that path.
    • They now say both validators judge a declared column with InlineGridColumnSchema. They keep the report's reason: neither zod face sees an identity-only { name, scale } column whose child field is a currency (only defineStack does, at publish, feat(spec)!: a form view's subform columns are the inline grid column contract, and an identity-only column is judged as the type it renders (#20901) objectstack#20927), and nothing runs either validator between a stored or code-built form view and the render.
    • Comment and test-name bytes only. The TypeScript printer with removeComments prints deriveMasterDetail.ts identically before and after, and the test file differs only in that one test-name string. The old test name was referenced nowhere else in the tree.
    • Left as written: the released @object-ui/plugin-form and @object-ui/fields CHANGELOG text that says the same, which is history.
  3. Looser local copies. DrawerForm.tsx and ModalForm.tsx in plugin-form declare their own subforms item types with columns as any[]. They receive values from the now-typed face, so nothing breaks, but they are looser than the spec. Carrier: none.

Serial: objectui#11608's PR #11616 is still open. Its files (packages/types/src/index.ts and the objectui#8347 pin) are disjoint from these. origin/main has not moved since this branch was cut (2abec3a9), so the merge before opening was a no-op.


Generated by Claude Code

claude added 2 commits October 4, 2026 15:18
…lineGridColumnSchema, by reference (objectui#11266)

The object-form mirror held z.array(z.any()) for subforms[].columns while
@objectstack/spec 17.6.0 judges FormViewSchema.subforms[].columns with its
closed InlineGridColumnSchema (objectstack#20927), so objectui validate
accepted a column with an undeclared key, or a typed currency column carrying
scale, that os validate refuses.

- zod: columns is z.array(stripImportedDefaults(SpecInlineGridColumnSchema)),
  crossing the import boundary like every other spec read; the 8317 census
  measures the new crossing.
- TS: ObjectFormSchema subforms[].columns is the spec's InlineGridColumn.
- Pin: object-form-subforms-columns-11266 on both faces, with the spec's
  FormViewSchema read live as the other door.
- Docs: the plugin-view master-detail example wrote bare field-name strings
  as columns, which the renderer never read and the spec refuses.

Claude-Session: https://claude.ai/code/session_01CPvhwGcirXqBGEdPSb72TZ
Co-authored-by: Claude <noreply@anthropic.com>
… not an any cast (objectui#11266)

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.9 KB 3330.4 KB
Main entry chunk (gzip) 151.4 KB 350 KB
Entry file index-B9UAsMJC.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

…tors judge a declared subform column now (objectui#11266)

reportCurrencyColumnScale's docblock and the currencyScale-10783 test header
and one test name said a subform's columns is z.array(z.any()) in the
object-form mirror and in the spec's FormViewSchema, and that the spec does
not judge that path. This PR makes the mirror half false, and
@objectstack/spec 17.6.0 already made the spec half false: both now judge a
declared column with InlineGridColumnSchema.

The reason the render-time report exists is restated truthfully: neither zod
face sees an identity-only { name, scale } column whose child field is a
currency, only defineStack does at publish (objectstack#20927), and nothing
runs either validator between a stored or code-built form view and the
render. Comment and test-name bytes only; the TS printer with removeComments
shows no other change.

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.9 KB 3330.4 KB
Main entry chunk (gzip) 151.4 KB 350 KB
Entry file index-B9UAsMJC.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: 72211fd07298d05e4decf8b7844a83a131819f6e
Local-runs: none

Inputs. Card objectui#11266 (its body and every comment: the filing, triage grade 5919698597, hold release 5980854935, retriage answer 5981265987, claim 5981407176, the two os-dev-reports 5981697019 and 5981767889, the seat ruling 5981710565); PR #11618 (body, file list, net diff against main at the head); the check-runs on the head; and, for the premise only, objectstack's tag @objectstack/spec@17.6.0 (commit 617f25f8a4) read with git show, because @objectstack/spec is not installed under this clone's node_modules. Read-only throughout: no worktree, checkout, build, test or gate re-run. The one node invocation was the fleet's readClause2Line reader over the two carriers' text, which is a read.

The head. 72211fd0 is GitHub's update-branch merge of main at 8b14aecb (objectui#11616) into b1d035bb (the dev's build ce55b465 plus patch round 1). Its combined diff (git diff-tree --cc) is empty, so the merge authored nothing. main's side brought packages/types/src/index.ts, an edit to the 8347 pin (base-schema-closed-face-8347), the new 11608 pin and its changeset; this PR touches none of them. The net diff against main is exactly the nine files in the PR's file list: 297 insertions, 18 deletions, matching the file list's per-file counts.

① Derived judgments

1. The tolerant mirror's subforms[].columns, by reference: right. The object-form mirror's subforms[] item now reads columns: z.array(stripImportedDefaults(SpecInlineGridColumnSchema)).optional(), imported from @objectstack/spec/data (the data barrel re-exports field.zod, and the package maps the ./data subpath). At the tag, InlineGridColumnSchema is a lazySchema over the spec's strictObject with the #20045 .superRefine (the currency/scale refusal); no member in its shape carries a .default(), and lazySchema is the spec's memoising Proxy, not a z.lazy, so the objectui#8317 walker's identity property applies: the boundary hands back the spec object itself, which is what the pin's second toBe row asserts. The walker keeps every key, every check and the registry metadata and strips only substitution, so no verdict is lost on the way in. The diff restates no column key anywhere: no local shape, no alias table, no copied message. That is the triage direction ("references, or derives from ... no hand-copied column shape") met in its stronger form. The other door at the tag: FormViewSchema.subforms[].columns is z.array(InlineGridColumnSchema).optional(), so both validators hand a column to one schema object.

  • Reach: the object-view form slot is ObjectFormSchema.omit(type, objectName, mode).partial() under z.lazy, so the narrowing reaches the authored route, and the flat object-form mirror reads the same member. The strict authoring face derives from the same graph; its walker walks a pipe on both sides and reports a transform as opaque rather than dropping it, and the imported column is strict already, so both faces refuse the undeclared key. The pin exercises both faces and CI's Test shards are the verdict on it.

2. The TS face, and what follows through ObjectViewSchema['form']: right. ObjectFormSchema.subforms[].columns goes from any[] to SpecInlineGridColumn[] by a type-only import. At the tag, InlineGridColumn is the schema's z.input (the authoring face; the parsed shape is exported separately as InlineGridColumnParsed), so a string predicate on readonlyWhen / requiredWhen stays authorable on the TS face and the type is the spec's, not a restatement. ObjectFormSlotKey lists subforms, so ObjectViewSchema['form'] (a Partial of a Pick over ObjectFormSchema) follows without an edit. Consequences, both stated in the changeset: a bare string column and an object literal with an undeclared key no longer compile; the pin's @ts-expect-error row holds the first under tsc -p tsconfig.test.json. Import side: the dev measured type-check green for plugin-form, plugin-view and app-shell and left apps/console to CI; CI's Type Check on the head is that verdict. My sweep of the head for subforms authoring with string columns found only the two docs sites the PR fixes (the console record-block-record-reach fixture's columns: ['name', 'amount'] is a record:related_list block sample typed as an open record, not a subform).

3. The parse-output change (a string readonlyWhen / requiredWhen comes back as { dialect: 'cel', source }): acceptable, disclosed, and consistent with this repository's mirror rules, with one policy residual named for the seat. Precisely: the member schema is the spec's EvaluatedExpressionInputSchema (the dev writes "ExpressionInput pipe"; it is the same construction, a union whose string arm refines non-blank and transforms to the envelope). Judged against Q6 = B and against the existing crossings:

  • The rule that governs an imported crossing is the objectui#8317 boundary, and it is explicit: stripImportedDefaults removes the substitution of a default and keeps "every key, every type and every check exactly as the spec declares them"; its pipe arm walks through and keeps a transform. The repository already lives with imported transforms in parse output: the strict face's own docblock counts four pipes reachable on the published face (a preprocessor into an enum under page.interfaceConfig.filterBy, and an object, a string and an array each into a transform), and the dev's four named crossings are those by reference (SpecPageSchema's fields for a page's slots.header and its visibleWhen, SpecViewSchema.shape.listViews, SpecListViewSchema.shape.exportOptions, which the mirror reaches through an explicit unwrapping walk, and the action crossings such as SpecBulkActionDefSchema and SpecUserActionsConfigSchema). Not re-measured here; the class is established in the tree.
  • objectui#8347 Q6 = B was ruled for BaseSchema.visibleWhen, a member this repository authors in its own BaseSchemaCore; base.zod.ts's text and the visible-when-spec-input-8347 pin scope it to that key. Nothing in the card, the triage direction or the claim extends it to an imported subtree, and the direction for this card, confirmed twice and executed by the claim, is by reference with no hand copy. Reproducing Q6 = B here would mean rebuilding the spec's column object with two members swapped, a derived copy of the very shape the direction forbids, and a weaker pin.
  • Reach of the output: the only non-test readers of safeValidateSchema are cli validate (prints type, id, label and the children count from data, never writes it back) and cli check (success plus issues). The renderer's resolveFieldRuleState (packages/core, fieldRules) accepts string | { dialect?, source }, so a consumer that did render the parsed output would evaluate the envelope identically. The authored document is not mutated.
  • Disclosed: the changeset's paragraph "The parse output is the spec schema's too ..." and PR Acceptance note 1; flagged to this review in both dev reports and the seat ruling.
  • Residual, not a defect of this PR: the Q6 = B sentence "this mirror authors nothing into a parsed document" and the by-reference boundary disagree at every imported EvaluatedExpressionInputSchema member, and this PR adds one more such member. If the seat wants "a string stays a string" to hold mirror-wide, the fix is a transform-stripping arm at the 8317 boundary (the reversible shape batch [WIP] Fix action run issue in CI/CD pipeline #90 chose for defaults), ruled on its own card, never a per-crossing hand copy. Also noted, non-blocking: the pin records the identity-only accept so that reaching for it is deliberate, but does not record the envelope output; one row would do the same for this behaviour.

4. The pin object-form-subforms-columns-11266.test.ts: right.

  • By reference: two toBe rows, the boundary's result and the spec object itself.
  • Both faces through the object-view form slot (the authored route, since the object-form properties bag refuses the form-view member subforms): { name } accepted as the control; an undeclared key refused with one unrecognized_keys issue at the column, keys naming bogusKey; a column declaring type: 'currency' with scale refused with one custom issue at the column's scale, its message asserted equal to the spec schema's own message read in the same run (no literal text pinned); a bare string refused with invalid_type at the column; identity-only { name, scale } accepted, with the reason in the test name.
  • The flat mirror: the same codes at the same column paths. (An authored flat object-form node is not a subforms carrier: the spec's object-form props row has no such member, so on that node the tolerant face leaves the undeclared key unjudged as before and the strict face refuses it by name, which the dev's round-1 sweep measured; this PR changes nothing there, and the authored route for subforms is the view's form slot the pin uses.)
  • Cross-door: five rows, safeValidateSchema success compared with the spec's FormViewSchema.safeParse on { type: 'simple', subforms: [...] } read live, one per probe column.
  • Type level: an Equal assertion between the declared column and the spec's input type, and the @ts-expect-error string column.
  • The identity-only row recorded as accepted is right. The spec's own column schema accepts it (the .superRefine reads only the declared type), and the refusal is defineStack's collectHydratedInlineColumnErrors in stack.zod.ts at the tag, which resolves name against the stack's own child object fields on a relationship field's inlineColumns and on a form view's subforms[].columns. A document validator has no child object to resolve against; the render-time report stays the backstop, as the direction says.
  • Ablation is a reading in the report: 11 of 17 red with the member reverted to z.array(z.any()), the six green being exactly the rows whose expected verdict is accept; restore verified by blob equality. The pin imports the source mirror, not dist.
  • Census and ledgers: imported-defaults-8317 gains the InlineGridColumnSchema IMPORTED row (identity: no default, no z.lazy), which the census requires for every imported symbol. No zod-mirror-parity row moves: ObjectFormSchema was already spec-derived, its KnownDrift names the five runtime-slot handler keys only, and any[] against z.any() was invisible to the ledger. The object-form-unmirrored-members-6152 subforms row carries no columns in either value and keeps its verdicts.

5. The two docs fixes (content/docs/plugins/plugin-view.mdx, packages/plugin-view/README.md): right and necessary. hydrateColumns keys on col.name, so columns: ['product', 'quantity', 'price'] never rendered as written; the spec refuses it; and with the TS face narrowed the two ObjectViewSchema snippets would fail check:doc-snippets. Both now write { name } objects and the following prose says what a column is. These two were the only authoring sites with string columns under subforms on the head. Doc Snippet Type Check and Doc Component Type Check are green on the head.

6. The patch-round prose in plugin-form: true at the head. The reportCurrencyColumnScale docblock: the mirror judges a subform's columns with the spec's own InlineGridColumnSchema (true, item 1); the child object's fields are not in the document (true); only defineStack refuses an identity-only currency column, at publish, on inlineColumns and on subforms[].columns, when the stack declares the child object (true at the tag: collectHydratedInlineColumnErrors names both carriers and resolves against the stack's own objects); the declared arm is refused by both validators on subforms[].columns (true: the spec's FormViewSchema from 17.6.0 and the mirror by reference); neither runs between a stored or code-built form view and the function (true: no runtime path reads safeValidateSchema output). The currencyScale-10783 test header's "17.5.0 refuses scale on a column declaring type: 'currency'" stays true: the hold release measured data.InlineGridColumnSchema exported by both versions, and the dev's 17.5.0 control probe measured the same refusal there. The renamed test is cited by content; the dev's grep found no other reference. The comment-only proof is a reading in the report and agrees with the diff: the only code-adjacent change is the it(...) name string.

7. The dependency range: right as read. packages/types/package.json at the head declares @objectstack/spec ^17.6.0; the lockfile resolves @objectstack/spec@17.6.0 only (two entries, both 17.6.0). Nothing moves, and nothing needs to: this mirror's floor is 17.5.0's InlineGridColumnSchema (the same object with the same verdicts, per the dev's control probe); 17.6.0 is what the other door needs, and the card's own premise.

② Semver level

  • .changeset/11266-subforms-columns-mirror.md: frontmatter '@object-ui/types': minor; a BREAKING banner naming the accept-set narrowing and why the bump is minor; three FROM → TO migration lines (string columns to { name } objects, an undeclared key removed, scale removed from a declared currency column); the not-refused case; the parse-output disclosure; and the bold line Clause-②: yes (narrowing) with its one-sentence reason.
  • Both carriers read through the fleet's one reader, readClause2Line: the PR body reads declared / yes / narrowing (its second line, under Fixes #11266); the changeset reads declared / yes / narrowing. Both match the claim 5981407176's Clause-②: yes (narrowing).
  • Under the fixed-group policy (objectui's major follows the @objectstack family major; a breaking change of objectui's own ships as minor with the breaking semantics stated in the body; check-changeset-no-major enforces it), minor with a BREAKING banner and a migration is the right level for a declared narrowing: yes takes at least minor, (narrowing) is breaking, and the FROM → TO is present. Changeset Bump Policy, Changeset Fixed Group Check, Changeset Declaration, Changeset Claim Re-read and Changeset Overwrite Report are green on the head.
  • Coverage of the other touched packages: the plugin-form edits are comment bytes and one test-name string with no published behaviour; the plugin-view README and the content/docs page are documentation outside the presence gate's population (*.md is excluded, and the docs site is not a released package); and the fixed group versions all forty packages together on this one changeset. The presence gate counted six source files of two released packages against one declared changeset and passed. No second changeset is owed, and naming plugin-form or plugin-view in this one would publish a release note for a comment.
  • objectstack's ADR-0087 disposition marker is a rule of objectstack's own check:adr-0087-registration on packages/spec retirements; objectui's tree at the head carries no such gate and this PR retires nothing. Not applicable.

③ Boundary flags

Deviations (both reports).

  1. Two docs files outside the claim's file surface: accepted by the seat ruling 5981710565 (a PR fixes what it makes false); judged right in ①.5.
  2. Commit trailers: all three branch commits end with the model-free pair AGENTS.md prescribes for an agent commit (the session-link trailer and the plain co-author trailer); the harness reminder's model-named line is the exempted alternative, not a requirement. No model identifier appears in the diff, the PR title or body, or the changeset (scanned). Right.

Open questions. None in either report.

Out-of-scope findings.

  1. The parse-output envelope, carried to this review: answered in ①.3. Acceptable and disclosed; the mirror-wide policy residual is named there for the seat to file or not.
  2. The stale plugin-form prose: fixed in patch round 1 and now in the diff; judged true in ①.6.
  3. DrawerForm / ModalForm local subforms item types keep columns?: any[]: left per the seat ruling. They are types, not statements, and they receive the narrower spec type without a break. Acceptable to leave; a follow-up could type them from ObjectFormSchema['subforms']. This review requires no carrier.

Body replacements. The PR body at the head carries round 1's three replacements (the "No plugin-form behaviour changes" sentence, Acceptance note 2 rewritten as fixed, the patch-round gate readings). The body is consistent with the diff; it contains no HTML comment and no model identifier.

The merge. 72211fd0 is GitHub's update-branch merge of main 8b14aecb (#11616); its combined diff is empty, and #11616's files (packages/types/src/index.ts, the edited 8347 pin, the new 11608 pin and its changeset) are disjoint from this PR's nine. Verified with git diff-tree --cc and git diff --stat on the fetched refs.

Check-runs on the head (the gate verdicts), each reading with its time. Read with the REST check-runs endpoint on 72211fd0 only; the cancelled shards on the superseded heads ce55b465 and b1d035bb were not consulted.

  • Reading 1 at 2026-10-04T15:51Z: 42 runs; 23 completed (20 success, 3 skipped), 19 in progress or queued; no failure.
  • Reading 2 at 2026-10-04T15:56Z: 42 runs; 13 not yet completed; every completed run success or skipped.
  • Reading 3 at 2026-10-04T15:57Z: unchanged from reading 2.
  • Reading 4 at 2026-10-04T16:00Z: 10 not yet completed (Test shards 1 to 8, Type Check, Spec Main Shape Gate); no failure.
  • Reading 5 at 2026-10-04T16:05Z: 6 not yet completed (Test shards 1, 2, 3, 4 and 7, Type Check); no failure.
  • Final reading at 2026-10-04T16:07Z: 43 runs, every one completed; 40 success, 3 skipped, no failure, no cancelled, no neutral, nothing in progress. The 43rd run is the aggregate Test, which appears once the eight shards finish; it is success, and the last run completed at 2026-10-04T16:06Z. The three skipped runs are Test (coverage), the coverage shard matrix, and dependabot, skipped by their workflows' own conditions on a pull request, not verdicts on this diff.
  • The gate families the dev left to CI are answered by name: the full test suite by Test shards 1 to 8 and the aggregate Test (success), the tree-wide lint by Lint (success), the published-dist and installed-floor checks by Test (dist pins) and Spec Main Shape Gate (success), and the import side of the narrowed TypeScript face, apps/console included, by Type Check (success). The docs fixes are judged by Doc Snippet Type Check, Doc Component Type Check, Doc Example Id Check, Doc Fence Language Check, Internal Docs Link Check and Build Docs (success). The changeset by Changeset Bump Policy, Changeset Fixed Group Check, Changeset Declaration, Changeset Claim Re-read and Changeset Overwrite Report (success). The citation and byte rules by Line Citation Gate, Control Byte Scan and Shell Escape Residue Scan (success). Governed Surface Queue Guard is success: no governed path is on the file list. Build & E2E, Bundle Analysis (the performance budget comment on the PR reads PASS), README Export Check, Skill Example Check and Live E2E (informational) are success as well.

Implemented-by: claude/issue-11266-subforms-columns-mirror
Reviewed-by: session_01CPvhwGcirXqBGEdPSb72TZ

VERDICT: PASS


Generated by Claude Code

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review October 4, 2026 16:10
@objectstack-fleet
objectstack-fleet Bot enabled auto-merge October 4, 2026 16:10
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Oct 4, 2026
Merged via the queue into main with commit 9db9ff3 Oct 4, 2026
45 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/issue-11266-subforms-columns-mirror branch October 4, 2026 16:27
akarma-synetal pushed a commit to akarma-synetal/objectui that referenced this pull request Oct 7, 2026
… spec rule `deriveInlineGridColumns` (objectui#11345) (objectstack-ai#11623)

Fixes objectstack-ai#11345

Clause-②: no

## What this PR is

`deriveColumns`, the default columns of a master-detail inline grid
whose author listed none, now takes **which** columns it draws, their
order and their `defaultHidden` flag from `@objectstack/spec`'s
`deriveInlineGridColumns`, and its visible budget from the spec's
`DEFAULT_MAX_INLINE_GRID_COLUMNS`. objectui's own per-column builder
stays over the returned names (extracted as `deriveColumn`), so each
column's label, cell type, options, lookup target, conditional rules and
computed expression are built exactly as before, including the plain
text column for a falsy field definition.

Deleted from `packages/plugin-form/src/deriveMasterDetail.ts`:
`curateColumns`, `NAME_LIKE_FIELDS`, `TYPE_FILL_PRIORITY`,
`fillPriority`, `NON_EDITABLE_TYPES`, and the module-level
`DEFAULT_MAX_INLINE_COLUMNS = 6`, which the package entry never
re-exported. `SYSTEM_FIELDS` and `SORT_FIELD_NAMES` stay, because
`deriveFormFields` and the sort-field pick in `deriveDetail` still read
them.

The direction is triage's first grade (comment 5926478449): names and
`defaultHidden` from the spec, objectui's builder over the names, the
budget imported as `DEFAULT_MAX_INLINE_GRID_COLUMNS`, no second `6`.

## The fence

- `deriveColumns` keeps its signature, and its output is unchanged on
every input the evidence below reached.
- No export, prop, type member or i18n key is added to any
`@object-ui/*` package. The one declaration-file change is the
module-level `DEFAULT_MAX_INLINE_COLUMNS` leaving
`dist/deriveMasterDetail.d.ts`. The package entry `src/index.tsx` never
re-exported it, and the package `exports` map exposes only `.`.

## Landing site, and one addition to the claimed file surface

The landing site is the dispatched one:
`packages/plugin-form/src/deriveMasterDetail.ts` and a test beside it.

⚠️ **Outside the claim's declared surface:**
`packages/plugin-form/package.json` (`@objectstack/spec` goes from
`^17.0.0` to `^17.6.0`) and the one matching specifier line in
`pnpm-lock.yaml`. The resolved version stays 17.6.0. They are in commit
`399fc03`, kept separate so it can be judged on its own. The reason: the
published `dist/index.js` now imports `deriveInlineGridColumns` and
`DEFAULT_MAX_INLINE_GRID_COLUMNS` from `@objectstack/spec/data`. The old
floor admitted 17.0.0 to 17.5.x, which carry neither name, so a consumer
that resolves one of those versions could not link the entry. PR objectstack-ai#11552
and PR objectstack-ai#11555 raised floors the same way, with the lockfile specifier.

## Evidence (head `fe06e31`, after merging main `c096f03`)

**The pin.** `deriveMasterDetail.inlineGridColumns-11345.test.ts` holds
one 14-entry corpus to two answers:
1. the names, order and `defaultHidden` equal (`toStrictEqual`) the
spec's `deriveInlineGridColumns` answer;
2. the full output equals (`toStrictEqual`) the arrays the replaced
implementation returned. These were generated from `deriveColumns` at
`6e9090c`.

The corpus covers both sides of the budget edge, `maxColumns` 3, 0 and
-1, `exclude` with and without a relationship field, the filters, more
required columns than the budget, a computed required column, falsy
definitions, and four shapes with no field map. One more test checks
that an omitted `maxColumns` leaves exactly
`DEFAULT_MAX_INLINE_GRID_COLUMNS` columns visible.

**Ablations on committed code (`b879c7c`).** Each ran through
objectstack's `scripts/ablation-replace.mjs`. In each, the anchor went
from 1 hit to 0, the blob changed, and the restore was proven (blob
equals HEAD, `git diff HEAD` empty).
- A: drop the spec's `defaultHidden` carry. Predicted 13 red. Observed
`Tests 13 failed | 16 passed (29)`.
- B: a falsy definition answers a bare `{ name }`, the hydrate route the
card warns about. Predicted 1 red (pin 2, falsy entry). Observed `Tests
1 failed | 28 passed (29)`.
- One earlier spelling of B is not counted. The tool refused it before
any test ran, because its replacement contained the anchor.

**One-off differential.** Not committed; a historical reading at
`fe06e31`. It compares `deriveColumns` at BASE (`6e9090c`) with this
branch over 80,014 cases: the 14 corpus entries, 50,000 random cases and
30,000 budget-heavy ones.
- Inputs: system, sort, name-like and prototype-named field keys; random
types and flags; falsy and primitive definitions; random
`relationshipField`, `exclude` and `maxColumns`, including NaN and
Infinity.
- Comparison: the `util.inspect` rendering (so key order and
undefined-valued keys count) plus deep equality.
- Result: **0 mismatches**.
- Control leg: the rejected route, the spec's names through
`hydrateColumns`, gives 68,522 mismatches on the same cases, so the
instrument can fire.

**Floor.** This is objectui's `scripts/check-spec-range-floors.mjs`
verdict (its `analyze()`), scoped to `@object-ui/plugin-form` over its
built dist:
- at `^17.0.0`: red, `floor-too-low` on exactly
`DEFAULT_MAX_INLINE_GRID_COLUMNS` and `deriveInlineGridColumns`;
- at `^17.5.0` (in memory): red, on the same two names;
- at `^17.6.0`: green.

| Run (from the repo root) | Exit |
|:--|:--|
| `pnpm exec vitest run packages/plugin-form/` | 0: `Test Files 164
passed (164)`, `Tests 1918 passed / 1 skipped (1919)` |
| `pnpm --filter @object-ui/plugin-form type-check`, after `turbo run
build --filter=@object-ui/plugin-form`. Both `tsc` legs ran, and
`--listFiles` shows the test program reads the new pin | 0 |
| `pnpm --filter @object-ui/plugin-form lint` | 0, with 0 errors |
| `check:spec-symbols`, `check:phantom-deps`, `check:unused-deps`,
`check:lockfile-integrity`, `check:lockfile-dedupe`,
`check:new-line-citations`, `check:control-bytes`,
`check:test-path-roots`, `check:unreferenced-sources`,
`check:installed-pin-claims`, `check:pending-changeset-literals`,
`check:changeset-claims`, `check:esm-specifiers`, `check:self-import`,
`check:vi-mock-specifiers` | 0 each |
| `scripts/check-changeset-presence.mjs`,
`scripts/check-changeset-no-major.mjs` | 0 each |
| `scripts/check-governed-queue-guard.mjs --test` over the five changed
paths | NOT GOVERNED |

**NOT MEASURED locally.** These need every published package built, and
CI or the release path runs them:
- the full `check:spec-floors` (the scoped verdict above stands in);
- `check:readme-exports` (refused here with 24 packages unbuilt; this
diff touches no README and no export);
- `check:dist-completeness`, `check:node-esm-load` and
`check:published-dist`;
- the root `pnpm lint` and `pnpm test`.

## Measured answers to the dispatch's open points

- **Spec names.** The installed `@objectstack/spec` 17.6.0 `dist/data`
declares `DEFAULT_MAX_INLINE_GRID_COLUMNS = 6` and exports
`deriveInlineGridColumns`. 17.5.0 lacks both, as the scoped floor
verdict shows.
- **Re-measure on the current base.** The card's 80,004-case reading was
taken against `be5211522412`. The differential above was taken on this
base, after PR objectstack-ai#11333 and PR objectstack-ai#11618.
- **Other readers of the budget.**
- Nothing outside `deriveMasterDetail.ts` read
`DEFAULT_MAX_INLINE_COLUMNS`.
- One other literal `6` exists: the `maxColumns = 6` default of
`RelatedList` in `packages/plugin-detail`. That is a different budget: a
related-list card that slices columns away, not an inline grid that
collapses them into a chooser. It is left as is.
- `GridField`'s doc comment names `deriveColumns` and gives no number.
No docs page states the budget.

## Acceptance notes

- `SYSTEM_FIELDS` and `SORT_FIELD_NAMES` (used by the row form and the
sort-field pick) hold the same names as the spec's module-private
`INLINE_GRID_SYSTEM_FIELDS` and `INLINE_GRID_SORT_FIELDS`, which
`@objectstack/spec/data` does not export. This is an observation only:
the row-form rule is not spec-owned, and nothing here judges it.
Carrier: none.

## Changeset

`.changeset/11345-inline-grid-columns-spec-rule.md` declares
`@object-ui/plugin-form` `patch` and includes the floor sentence.

Session: `https://claude.ai/code/session_015W8GBu6sBiqus2L2xjMsAL`
(os-dev, dispatched by the `domain:ui` seat 1).

---
_Generated by [Claude
Code](https://claude.ai/code/session_015W8GBu6sBiqus2L2xjMsAL)_

---------

Co-authored-by: Claude <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation package: types plugin tests

Projects

None yet

2 participants