Repository navigation
feat(types)!: a form view's subforms[].columns entry is the spec's InlineGridColumnSchema, by reference (objectui#11266) - #11618
Conversation
…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>
✅ Console Performance Budget
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
Size Limits
|
…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>
✅ Console Performance Budget
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
Size Limits
|
Contract reviewServed-tier: Inputs. Card objectui#11266 (its body and every comment: the filing, triage grade The head. ① Derived judgments1. The tolerant mirror's
2. The TS face, and what follows through 3. The parse-output change (a string
4. The pin
5. The two docs fixes ( 6. The patch-round prose in 7. The dependency range: right as read. ② Semver level
③ Boundary flagsDeviations (both reports).
Open questions. None in either report. Out-of-scope findings.
Body replacements. The PR body at the head carries round 1's three replacements (the "No The merge. Check-runs on the head (the gate verdicts), each reading with its time. Read with the REST check-runs endpoint on
Implemented-by: VERDICT: PASS Generated by Claude Code |
… 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>
Fixes #11266
Clause-②: yes (narrowing)
@objectstack/spec17.6.0 judgesFormViewSchema.subforms[].columnswith its strictInlineGridColumnSchema(objectstack-ai/objectstack#20927,bee75cebe6). Theobject-formmirror here still heldz.array(z.any()), soobjectui validateaccepted columns thatos validaterefuses. 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, theobject-formmirror'ssubforms[]item:columnsgoes fromz.array(z.any())toz.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'ssubforms[].columnsgoes fromany[]to the spec'sInlineGridColumn(itsz.input, the authoring face), by a type-only import.ObjectViewSchema['form']follows, because it picks fromObjectFormSchema.content/docs/plugins/plugin-view.mdxandpackages/plugin-view/README.md, section "Child records (master-detail)", wrotecolumns: ['product', 'quantity', 'price']. Bare strings:hydrateColumnsreadscol.name, the spec refuses them, and with the TypeScript face narrowed that snippet no longer compiles undercheck:doc-snippets. Both now write{ name: ... }objects and say what a column is..changeset/11266-subforms-columns-mirror.md:@object-ui/types: minor, with a BREAKING banner, FROM / TO migration lines and theClause-②: yes (narrowing)line.Premise readings (this worktree, base
2abec3a9, which isorigin/mainat PR time)pnpm-lock.yamlresolves@objectstack/specat 17.6.0 only (two entries, both 17.6.0). The packagepackages/typesresolves answers17.6.0.@objectstack/spec/dataexportsInlineGridColumnSchema.FormViewSchema.safeParseof a simple form whosesubforms[0].columnsholds COL:{ name: 'qty', bogusKey: 1 }is refused: oneunrecognized_keysissue atsubforms.0.columns.0, keysbogusKey.{ name: 'qty' }is accepted (the control).z.any()era), so the probe can fire.The
currency+scalefindingtype: 'currency'and carriesscale({ name: 'amount', type: 'currency', scale: 2 }) is refused by the spec'sInlineGridColumnSchemaitself, at the zod level: onecustomissue at pathscale. It is pinned. Through the mirror the same column is refused atform.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.scale({ name: 'amount', scale: 2 }) is ACCEPTED by the spec's column schema, at 17.5.0 and at 17.6.0. Its refusal isdefineStack's, which resolvesnamethroughchildObjectto acurrencyfield. 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 (reportCurrencyColumnScaleinplugin-form'shydrateColumns) stays the backstop. Noplugin-formbehaviour changes: patch round 1 (b1d035bb) corrects only its prose, see Acceptance note 2.The
@objectstack/specrangepackages/types/package.jsonalready reads^17.6.0onorigin/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
InlineGridColumnSchemaonly, and 17.5.0 already exports it with the same verdicts on the three probes: bogus key refused withunrecognized_keys, typed currency withscalerefused atscale,{ name }accepted. What 17.6.0 adds is the spec's ownFormViewSchemareading that schema. That is the other door's verdict, not this mirror's.check:spec-floorsis green on the built artifact.The pin
packages/types/src/__tests__/object-form-subforms-columns-11266.test.ts, 17 tests:toBestripImportedDefaults(SpecInlineGridColumnSchema), andtoBethe spec object itself.safeValidateSchema, whichobjectui validateruns) and on the strict authoring face, through the object-viewformslot. That slot is the route an authored document has:subformsis a form-VIEW member, which theobject-formrow refuses in itspropertiesbag.{ name }is accepted: the lit control.unrecognized_keysissue at the column, keysbogusKey.scaleis refused: onecustomissue at the column'sscale.invalid_typeat the column.scaleis accepted.safeValidateSchemasucceeds exactly when the spec'sFormViewSchemadoes, read live.tsc -p tsconfig.test.jsoninsidetype-check): a declared columnEquals the spec'sInlineGridColumn, and a string column is a@ts-expect-error.Ablation. The fix was committed first. The ablation used objectstack's
scripts/ablation-replace.mjsin wrap mode, insideos-verify-lock.columns: z.array(stripImportedDefaults(SpecInlineGridColumnSchema)).optional()becamecolumns: z.array(z.any()).optional(). Anchor count went from 1 to 0, and the blob from27ae77d7tod6b4ac83.27ae77d7, the same as the blob at HEAD, andgit diff HEADis empty. The pin rerun: 17 passed.Tests moved
imported-defaults-8317.test.ts:IMPORTEDgains['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, noz.lazy).zod-mirror-parityKnownDriftorSPEC_DERIVED_PAIRSrow moves:ObjectFormSchemawas already spec-derived.KnownDriftentry names only the five runtime-slot handler keys.columnsnow carry the same type. Before, it wasany[]againstz.any(), so the ledger saw nothing either way.object-form-unmirrored-members-6152.test.ts: thesubformsrow has no columns in either value, keeps its verdicts, and is not edited.Repo-wide sweep. I grepped for
subformsandcolumnsacross tests, docs, JSON and scripts. No test anywhere asserts thatobjectui validateaccepts a bogus column or a typed-currency column withscale, so no pin flips. The two doc examples above were the only documents writing a column the narrowed faces refuse.Gate readings, at head
ce55b465pnpm exec vitest run packages/types/(under the lock):Test Files 355 passed (355),Tests 9492 passed (9492).packages/typestype-check(the build program, the examples program and the test program): exit 0.type-checkfor@object-ui/plugin-form,@object-ui/plugin-viewand@object-ui/app-shellexits 0, and each echoestype-check: Done. They ran against the closure thatcheck:doc-snippets --build-filternames, built with pnpm, not turbo.vitest run packages/cli/plus thesubforms-adjacent consumer tests (deriveMasterDetail.currencyScale-10783,subformHosts,MetadataProvider.merge): 28 files, 390 tests, all passed.eslint --no-inline-config --format jsonover the four touched TypeScript files: 4 files, 0 errors. The warnings are existingno-explicit-anyhits inobjectql.tsandobjectql.zod.ts; the pin file has 0.pnpm lint, and it is declared. Population:eslint.config.jslints**/*.{ts,tsx}, so the two edited.md/.mdxfiles are outside it. File count: 4, from the JSON output. Invariance: the config sets noparserOptions.projectorprojectService, 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.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.check:changeset-claims,check:pending-changeset-literals,changeset:check(fixed and no-major),check-changeset-presenceandcheck-changeset-overwrite.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-floorswas first refused withno-artifacton the unbuilt@object-ui/plugin-tree. I built that package and reran it.check-governed-queue-guard --testover the seven paths: NOT GOVERNED.pnpm test, the tree-widepnpm lint, andcheck:published-dist.b1d035bb:@object-ui/plugin-formtype-checkpasses, with its dependency closure built by pnpm, not turbo.vitest runonderiveMasterDetail.currencyScale-10783.test.tspasses, the renamed test among them. eslint over the two touched files reports no errors; its warnings are existingno-explicit-anyin code.check:new-line-citationsreports no new citation,check:control-bytespasses, and the changeset checkers exit 0. The readings above are atce55b465; this round touched only the twoplugin-formfiles.Acceptance notes
InlineGridColumnSchema'sreadonlyWhenandrequiredWhencarry the spec's ExpressionInput pipe, sosafeValidateSchemareturns a column's string predicate as{ dialect: 'cel', source }. Measured:readonlyWhen: 'record.locked'comes back as the envelope. The input document is not changed.AnyComponentSchema: a page'sslots.headerand itsvisibleWhen,listViewsexportOptions, and an actionparamsentry.BaseSchema.visibleWhen. Nothing extends it to imported subtrees.b1d035bb). Seat ruling: a PR fixes the sentences it makes false.reportCurrencyColumnScale's docblock inpackages/plugin-form/src/deriveMasterDetail.ts, thederiveMasterDetail.currencyScale-10783test header and one of its test names said a subform'scolumnsisz.array(z.any())in theobject-formmirror and in the spec'sFormViewSchema, and that the spec does not judge that path.InlineGridColumnSchema. They keep the report's reason: neither zod face sees an identity-only{ name, scale }column whose child field is a currency (onlydefineStackdoes, 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.removeCommentsprintsderiveMasterDetail.tsidentically 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.@object-ui/plugin-formand@object-ui/fieldsCHANGELOG text that says the same, which is history.DrawerForm.tsxandModalForm.tsxinplugin-formdeclare their ownsubformsitem types withcolumnsasany[]. 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.tsand the objectui#8347 pin) are disjoint from these.origin/mainhas not moved since this branch was cut (2abec3a9), so the merge before opening was a no-op.Generated by Claude Code