Repository navigation
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) - #20927
Conversation
… contract, and an identity-only column is judged as the type it renders FormViewSchema.subforms[].columns references InlineGridColumnSchema instead of z.array(z.any()). defineStack's cross-reference check re-parses an identity-only inline grid column over a currency child field as a currency column, on both carriers, so scale is refused there with the column schema's own message. Claude-Session: https://claude.ai/code/session_01Sfe5YjBLwB9J3y8fvm2xq1 Co-authored-by: Claude <noreply@anthropic.com>
…rency column; regenerate the references and record the carrier's dropped refinement Claude-Session: https://claude.ai/code/session_01Sfe5YjBLwB9J3y8fvm2xq1 Co-authored-by: Claude <noreply@anthropic.com>
…ine grid column carriers The generated registry regions are not regenerated in this commit: that waits for a main that carries the two in-flight registry changes. Claude-Session: https://claude.ai/code/session_01Sfe5YjBLwB9J3y8fvm2xq1 Co-authored-by: Claude <noreply@anthropic.com>
…-typed build helper Claude-Session: https://claude.ai/code/session_01Sfe5YjBLwB9J3y8fvm2xq1 Co-authored-by: Claude <noreply@anthropic.com>
… five new sites Claude-Session: https://claude.ai/code/session_01Sfe5YjBLwB9J3y8fvm2xq1 Co-authored-by: Claude <noreply@anthropic.com>
…line-grid-column-carriers
…id column D3 entries Written after merging a main that carries both in-flight registry changes. Claude-Session: https://claude.ai/code/session_01Sfe5YjBLwB9J3y8fvm2xq1 Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift CheckThis PR changes 1 package(s): 3 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
⛔ 1 release-owned page(s) also name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 137 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 4b1361d239dab4945749ce809f6cfd74b0223ee0 && git checkout 4b1361d239dab4945749ce809f6cfd74b0223ee0
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 67c1b11a20f78d6bf37efbd45d896cd22b7fb4d1 feba1a99bddefae5bd1f9271848add43864aa9db && git checkout -B drift-repro 67c1b11a20f78d6bf37efbd45d896cd22b7fb4d1 && git merge --no-ff feba1a99bddefae5bd1f9271848add43864aa9db
node scripts/docs-audit/affected-docs.mjs --json 67c1b11a20f78d6bf37efbd45d896cd22b7fb4d1
|
Contract reviewServed-tier: ① Derived judgmentsInputs read: card #20901 (body; comments Accept-set and public-surface changes the diff implies
Author-shown and AI-facing text, tested sentence by sentence (true unless noted)
② Semver levelClause-②: yes (narrowing)
③ Boundary flagsDev report
Check-runs on Implemented-by: VERDICT: PASS Adopted and posted by
Generated by Claude Code |
…`InlineGridColumnSchema`, by reference (objectui#11266) (objectstack-ai#11618) Fixes objectstack-ai#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 objectstack-ai#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 `Equal`s 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, objectstack-ai/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 objectstack-ai#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](https://claude.ai/code/session_01CPvhwGcirXqBGEdPSb72TZ)_ --------- Co-authored-by: Claude <noreply@anthropic.com> Co-authored-by: Leehom <pm@objectstack.ai>
…columns as its own ADR-0087 step, and two step-18 entries read true (objectstack-ai#20901) (objectstack-ai#20953) Part of objectstack-ai#20901 Clause-②: no The follow-up the contract review of PR objectstack-ai#20927 owes (review record `5919009567`, items 5, 6 and 9, and the ① text findings), dispatched by landing record `5919493523`. objectstack-ai#20901 stays open for the seat to close by hand. ## What changed 1. **Review item 5, the step-18 entry `inline-grid-column-currency-scale-refused`.** Its surface said a column that declares no `type` "is untouched", and its acceptance said such columns "keep their `scale`". Since `bee75cebe6` that is false for an identity-only column over a `currency` child field, which `defineStack` refuses. The surface, the reach sentence of the reason and the acceptance now say so, and each names the sibling entry `inline-grid-column-identity-only-currency-scale-refused`. The registry was regenerated with `gen:migration-registry`. 2. **Review item 6, ruled WIDEN: the `field` → `name` respelling is now an ADR-0087 chain step on the form-view carrier.** New D2 conversion `form-view-subform-columns-canonicalized` (protocol 18, `retiredFromLoadPath: true`, `retiredAfter: '17.5.0'`). It walks every FORM payload `mapViewPayloads` reaches, which is `form`, each `formViews` entry, a form view item's `config` and a flattened form overlay (the stored-row seam wraps a `view` row as `{ views: [row] }` in any of those spellings). It also walks the assembled `viewItems` channel. It uses the same respelling rule as `field-column-lists-canonicalized`, now one shared function (`respellInlineGridColumns`) called by both entries. An entry already spelled `name` is left alone, and so is one carrying both keys, exactly as the existing walk behaves. The D3 entry `form-view-subform-columns-closed` said "NOT mechanically converted". It now names this conversion, and its comment no longer says a stored row is "refused": a stored row is diagnosed at rehydration. 3. **Review item 9.** A pin in `inline-grid-column-carriers.test.ts` probes `InlineGridColumnSchema` for every (type, key, value) that parses with the key alone and with the type alone but not with both. Today it finds exactly `currency` + `scale` (at `0` and `2`), and it asserts that `defineStack` refuses each such rule on an identity-only column over a field of that type. A new type-conditional rule without a `HYDRATED_INLINE_COLUMN_TYPE` row goes red, and the failure message names the table. `stack.zod.ts` itself is unchanged apart from comments. 4. **The ① text findings, in the files touched above.** `stack.zod.ts`: the objectui `hydrateColumns` / `fieldTypeToColumnType` claim is now a historical pin citation (`.objectui-sha` pin `db11afd4967c`, re-read at that sha: `hydrateColumns` leaves a typed column alone, and `fieldTypeToColumnType`'s only `currency` arm is `case 'currency'`), and `check:objectui-pin-citations` lists it. The over-broad "like the view data-source check" is cut. The instruction "adds its row here" now names the pin that holds it. ## Route change on item 6: its own entry, not a wider walk inside `field-column-lists-canonicalized` The ruling's intent is carried out exactly: a lossless respelling on the new carrier, landed as a `retiredFromLoadPath` chain step in the same release. The vehicle differs from the ruling's wording, for one ledger fact the gates cannot see. `retiredAfter` is one value per entry: "the last published `@objectstack/spec` whose authoring surface still accepted the old shape" (ADR-0087 amendment of 2026-09-30, maintainer ruling A on objectstack-ai#20390). `field-column-lists-canonicalized` is published with `17.0.0`, and `retired-after.census.test.ts` pins that value, while the form-view carrier accepted `field` through 17.5.0. The artifact-ingestion door opens its window per entry by that value. Measured at this head, with `applyArtifactForwardConversions`, an artifact declaring `engines.protocol: ^17.5.0`, and runtime label `17.5.0`: - verdict `converted-retired-after`; - the subform column `{ field: 'quantity' }` became `{ name: 'quantity' }` through the new entry (listed in `replayedRetirements` with `retiredAfter` `17.5.0`); - the same `{ field: 'quantity' }` on a relationship field's `inlineColumns` stayed as authored, because `field-column-lists-canonicalized` (`17.0.0`) stays closed at that floor. That is correct for that carrier, and it is what a folded-in subform walk would have done to the new carrier: the refusal instead of the rewrite. A second, smaller reason: the per-release section of `spec-changes.json` reports conversion ids that are new in a release, so a widened, already-published id would be invisible there. If the seat still wants one id, the fold-in is mechanical: move the `mapViewPayloads` walk into `field-column-lists-canonicalized` and delete the sibling. The cost is the artifact-door gap above. ## Verification (at `06f079864a`, which carries `origin/main` `3fbf3ca617`) - **Pins** `packages/spec/src/inline-grid-column-carriers.test.ts`: 21 passed (13 existing + 8 new). - **Ablation 1** (`scripts/ablation-replace.mjs`, `registry.ts`: the new entry's `respellInlineGridColumns(subform.columns, …)` call replaced by `subform.columns`; anchor x1 → x0; blob `909148b74725` → `3b07d9f2d145`), run over the pins plus `conversions.test.ts`: `Tests 4 failed | 251 passed (255)`. The red ones are the fixture pair of `form-view-subform-columns-canonicalized`, the stored-row test in all three `view` spellings, the `os migrate meta` chain test, and the two-carriers-one-rule test. The controls (an entry spelled `name`, one carrying both keys, and the authoring funnel's refusal) stayed green. Restored: blob == HEAD, `git diff HEAD` empty. - **Ablation 2** (`stack.zod.ts`: the `currency` row deleted from `HYDRATED_INLINE_COLUMN_TYPE`; blob `f93870ec6a88` → `d3aca2ec1d38`): `Tests 4 failed | 17 passed (21)`. The new table pin is red with "…add the row to HYDRATED_INLINE_COLUMN_TYPE in stack.zod.ts", and so are the three existing hydrated refusals. Restored: blob == HEAD. - Both ablations ran from committed state (`ca5ad202a9`). The subject resolves to `src` through relative imports, so no dist leg was owed. - **Spec suite** `vitest run --project local`: `Test Files 584 passed (584) / Tests 17200 passed | 1 todo`. **Typecheck** `pnpm --filter @objectstack/spec typecheck`: exit 0 (`check:test-typecheck: OK`; `tsconfig.test.json --listFiles` includes the pin file). **Repo-project subset** (major-18 merge, step-18 rationale merge, retired-key migrate sentence, two view retirement pins): 82 passed. The full `test:repo` project is left to CI. - **`check:generated`**: all 15 artifacts up to date after a fresh `pnpm --filter @objectstack/spec build`. - **Derived gates** `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack`: 90 derived, 90 run, all exit 0. `--ran` reconciliation reports "0 NOT-MEASURED (a DERIVED zero — all 90 recorded an exit code)". Four first exited 3 (PREREQUISITE NOT MET: `lint check:doc-formula-expressions`, `check:dual-build-cjs-loads`, `check:lean-entry-closure`, `check:type-check-debt`) and were green on a re-run after `turbo run build` over `./packages/*` and `./packages/*/*`. - **ESLint** (a narrowed run, stated as such): `eslint --no-inline-config --format json` over the 7 changed `.ts` files reports files 7, errors 0, warnings 0. The population is `eslint.config.mjs`'s `**/*.{ts,tsx,mts,cts,js,jsx,mjs,cjs}`. That config never enables type-aware linting (its own comment: no `parserOptions.project`, no typed rules), so this diff cannot move the verdict on any untouched file. ## One test touched outside the dispatched files `packages/spec/scripts/conversions-major18-merge.test.ts` went red on this diff. It builds its synthetic retirement pair beside the entry at the middle of `MAJOR_18_CONVERSIONS`. At the base that neighbour was `hookTimeoutToTimeoutMs`, which predates the placement rule. With this entry added, the neighbour is `formViewSubformColumnsCanonicalized`, which is placed by the rule. The "defined after every other conversion" variant then reports the neighbour too, because its entry is now followed by the synthetic one. The expectation now derives that neighbour finding when, and only when, the neighbour is not exempt. At the base it reduces to the old single-element list. 12/12 pass. ## Changeset `@objectstack/spec: patch`. The act adds no exported symbol (`check:api-surface` green) and no accepted key or value on an authoring surface: authored sources are refused exactly as before. It rewrites stored and assembled data that the contract already names (`name`), so under the "WHICH LEVEL" rule it is a fix and stays `patch`. `Clause-②: no`, per the gate's definition (a new key on a published payload): the conversion adds no key, and the step-18 edits are prose. It is not a narrowing either, since the data-at-rest seams now accept strictly more. ## Acceptance notes - `retiredAfter: '17.5.0'` is the package label, as `retired-after.census.test.ts` requires for an unpublished entry. The census is refreshed after the next publish. - Step 18's `rationale` gains no fragment. PR objectstack-ai#20927 added none for its two D3 entries either, and no gate requires one per entry. - Not changed here: the PR objectstack-ai#20927 body sentence "No check read `subforms[].childObject`", recorded by the adoption for its next touch; `packages/spec/src/data/field.zod.ts`, which the dispatch keeps out of scope; objectstack-ai#20928 and objectstack-ai#20929; and objectui#11266. --- _Generated by [Claude Code](https://claude.ai/code/session_01Sfe5YjBLwB9J3y8fvm2xq1)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
…ui#11611, objectstack-ai#11614 and objectstack-ai#11619) (objectstack-ai#21800) Fixes objectstack-ai#21772 Clause-②: no This moves the bundled Console's objectui pin from `2e818d0b51ec` (objectstack-ai#21710) to objectui `main` as of the write time, `9dfaca654311cddd81714153c4f82c241d7cdc54`, which is past `c096f03`. The Console now carries objectui#11611 (the `ref:dataset` spec-form widget), objectui#11614 (the grid widget's camelCase keys) and objectui#11619 (the record chrome's picture). Those are the three merges objectstack-ai#21714, objectstack-ai#21768 and objectstack-ai#21765 wait on. ⛔ This is not a release act. Version PR objectstack-ai#21352 is untouched, and no tag, publish or Release was made. The card's "release first" hold is lifted by the maintainer's order recorded in the claim: 「浏览器测试已经在运行,还有很多问题在处理,服务端开发不应该被阻塞」. ## Range - objectui `git ls-remote origin refs/heads/main` read `9dfaca654311cddd81714153c4f82c241d7cdc54` at 2026-10-05T00:51:29Z, just before the bump, and the same sha again before this PR was opened. - `git merge-base --is-ancestor` exits 0 against `9dfaca654311` for all three carried merges: `b508ac50d9` (objectui#11611), `2abec3a96c` (objectui#11614) and `c096f03279` (objectui#11619). The objectui clone is shallow, but exit 0 proves itself there. - `2e818d0b51ec..9dfaca654311` has 17 commits and 0 merges. - objectui declared 24 changesets over the range, and all 24 release. There are 0 release-nothing changesets and 0 commits without a changeset. None declares `major`. Four carry the author's breaking annotation, and the highest declared level is `minor`, so the console changeset is `minor`. These counts come from `scripts/objectui-range.mjs --from 2e818d0b51ec --to 9dfaca654311 --json` and from the bump's own digest. They were measured, not copied from the card. - Four commit subjects carry `!`. `git log --format='%h %s' 2e818d0b5..9dfaca654 | grep -E '^[0-9a-f]+ [a-z]+(\([^)]*\))?!:'` gives `9db9ff3f9`, `8b14aecbd`, `2abec3a96` and `b403bb36f`. | objectui commit | landing | changesets | declared disposition | |---|---|---|---| | `9dfaca654` | objectui#11615: the default `simple` form draws a self-describing inline section entry; `ObjectFormSection.fields` gains the form view's `{ field }` arm | 1, minor, **BREAKING** (for TypeScript readers) | releasing; declared breaking, answered below | | `15f67025b` | objectui#11345: the inline grid's default columns derive through the spec rule `deriveInlineGridColumns` | 1, patch | releasing | | `4c127cdef` | objectui#11613: `record:related_list` stops requiring `columns` | 1, minor | releasing; moves the manifest | | `c096f0327` | objectui#11383 (PR objectui#11619): the record chrome draws the record's picture from the object's `imageField` | 1, minor | releasing; smoke below; unlocks objectstack-ai#21765 | | `6e9090c26` | objectui#11336: an object document's served listViews count as served views | 1, patch | releasing | | `9db9ff3f9` `!` | objectui#11266: a form view's `subforms[].columns` entry is the spec's `InlineGridColumnSchema`, by reference | 1, minor, **BREAKING** | releasing; declared breaking, answered below | | `7c9a6b194` | objectui#11340: the docs portal renders the book resolver's answer | 1, patch | releasing | | `8b14aecbd` `!` | objectui#11608: `PartialSchema` is retired from `@object-ui/types` | 1, minor, **BREAKING** | releasing; declared breaking, answered below | | `2abec3a96` `!` | objectui#11610 (PR objectui#11614): the grid widget's eight field-level keys are camelCase; the snake_case spellings are refused by name | 1, minor, **BREAKING** | releasing; declared breaking, answered below; unlocks objectstack-ai#21768 | | `b403bb36f` `!` | objectui#8347: `BaseSchema` loses its index signature | 1, minor (`Clause-②: yes (narrowing)`, no BREAKING word) | releasing; declared breaking by its `!`, answered below | | `fd060f076` | objectui#11605: bound registrations stop requiring `objectName`, and a node with neither shows a no-object hint | 8, minor | releasing; moves the manifest | | `b508ac50d` | objectui#11601 (PR objectui#11611): the `ref:dataset` spec-form widget, fed by the report inspector's dataset catalog | 1, minor | releasing; unlocks objectstack-ai#21714 | | `d2e859936` | objectui#11002: the console asks `GET /usage/storage` only when the runtime serves `features.storageUsage` | 1, minor | releasing; smoke below | | `b92329c89` | objectui#11591: the Organization flows page copy | 1, patch | releasing | | `902ebab63` | objectui#11569: `record:line_items` stops requiring `childObject` | 1, minor | releasing; moves the manifest | | `278d2444e` | objectui#11546: a node click on a read-only flow canvas opens the inspector read-only | 1, patch | releasing | | `b61c116b2` | objectui#11577: `record:details` read mode keeps a textarea's line breaks | 1, patch | releasing | ## Declared-breaking changes, and how this repo answers each The ADR-0087 disposition is `not-required (no-migration-prescription)`. It replaces the bump's `adr-0087: TODO` placeholder in `.changeset/console-9dfaca654311.md`, and it covers all five entries below. `check-adr-0087-registration --base origin/main` gives "1 declared-breaking changeset(s), each carrying an ADR-0087 disposition". `check-changeset-no-major --base origin/main` gives "This diff introduces no `major` bump". 1. **objectui#11610 / PR objectui#11614 (`2abec3a96`): the grid widget's eight keys.** `min_rows`, `max_rows`, `allow_add`, `allow_delete`, `allow_reorder`, `total_field`, `add_label` and `sort_field` become `minRows` … `sortField`. objectui has no dual read: its zod faces refuse a snake_case key by name, and its `GridField` draws an inline alert instead of the grid. - **This repo, today.** `@objectstack/spec`'s runtime form field (`buildObjectFormRuntimeField`, `packages/spec/src/ui/component.zod.ts`) already refuses the eight snake_case keys by name, with guidance that names objectui#11610. The camelCase keys are not declared there yet. - **The interim, stated plainly.** Between this PR and objectstack-ai#21768, an ObjectStack-authored `grid` form field can spell the keys neither way. Measured on this head's built spec: `object-form` with `customFields: [{ name: 'items', type: 'grid', min_rows: 1 }]` is refused with `unrecognized_keys` at `customFields.0` and the grid-key guidance. The same field with `minRows: 1` is refused with `unrecognized_keys` at `customFields.0`, as an undeclared key. So a `grid` field takes its `columns` and the widget's own defaults. The refusal's prescription says these keys "come in once the widget reads a camelCase spelling". At this pin the widget does, and objectstack-ai#21768 declares the keys and retargets that prescription. - **Nothing here carries the retired spelling.** `git grep` over `examples/` and `packages/` finds no authored snake_case grid key. The only hits are the spec's own refusal list and its pin test, migration prose, and one historical note about objectui's internal master-detail adapter. 2. **objectui#11615 (`9dfaca654`): the `simple` form's inline section entries.** For TypeScript readers of `ObjectFormSection.fields`, the type gains the spec's `FormFieldInput` arm. No code in this repo imports `@object-ui/types`; the only `@object-ui/*` import is `scripts/gen-sdui-manifest-node.mjs` reading `@object-ui/core` from the built tree. The behaviour change is that the default `simple` form now draws an inline `{ name, … }` entry, as the other five form types already did. The showcase's `object-form` nodes are `wizard`, `drawer` and `modal` forms whose sections list field names, so none of them draws differently. 3. **objectui#11266 (`9db9ff3f9`): `subforms[].columns`.** objectui's validator now judges a column by `@objectstack/spec`'s own `InlineGridColumnSchema`. This repo has enforced that closed shape since objectstack-ai#20927. objectui's verdict now matches `os validate`, and the ObjectStack accept set does not move. 4. **objectui#11608 (`8b14aecbd`): `PartialSchema`.** It leaves `@object-ui/types`. Nothing in this repo names it (`git grep PartialSchema`: 0). 5. **objectui#8347 (`b403bb36f`): `BaseSchema` loses its index signature.** This narrows the TypeScript face of objectui's node types, and nothing here compiles against them. objectui's zod faces keep their accept sets for every key except `visibleWhen`. That key widens to the `{ dialect, source }` envelope this repo's own parse writes. ## The three cards this unblocks - **objectstack-ai#21714**: objectui#11601's `ref:dataset` widget (PR objectui#11611, `b508ac50d`) is now in the pinned build, so `report.form.ts`'s joined-block `dataset` row can declare `widget: 'ref:dataset'`. - **objectstack-ai#21768**: objectui#11610's camelCase keys (PR objectui#11614, `2abec3a96`) are now what the pinned widget reads, so the spec's runtime form field can declare them. - **objectstack-ai#21765**: objectui#11383's record picture (PR objectui#11619, `c096f0327`) is now in the pinned build, so `object.imageField`'s liveness row can move to `live`. The smoke below observes that read in the browser. The reader is `packages/components/src/renderers/layout/containers.tsx`, in the `page:header` record chrome. ## What changed here - **`.objectui-sha` and `.changeset/console-9dfaca654311.md`.** `scripts/bump-objectui.sh 9dfaca654311cddd81714153c4f82c241d7cdc54 --no-commit` wrote both, with `OBJECTUI_ROOT` set to the container's objectui clone after `git fetch origin main`. The range walked completely without a deepen, and the level was auto-set to `minor`. - **`sdui.manifest.json` and `scripts/sdui-manifest.record.json`.** `node scripts/gen-sdui-manifest-node.mjs` regenerated them over the tree that `pnpm objectui:build` built at the pin. There are 107 components at both pins, and the sha256 moves from `0ead67c1111d…` to `6f921896ffac…`. **This time the manifest moves**: - Eleven inputs lose `required: true` and gain a description. Nine are `objectName`, on `object-grid`, `list-view`, `object-form`, `embeddable-form`, `object-master-detail-form`, `object-kanban`, `object-metric`, `object-chart` and `object-pivot` (objectui#11605). The other two are `record:related_list` `columns` (objectui#11613) and `record:line_items` `childObject` (objectui#11569). - `object-master-detail-form`'s `fields` description is rewritten for objectui#11615's inline entries. - Where the spec has a `ComponentPropsMap` row, these inputs are already optional there. Measured on the built spec: `object-grid`, `object-form`, `object-kanban`, `object-metric` and `object-master-detail-form` `objectName`, `record:related_list` `columns` and `record:line_items` `childObject`. So the manifest now agrees with the spec rows. The JSX page compile reads the manifest, and it stops refusing a node whose `dataSource` binding names the object. - The record moves its pin and `modulesRoot`. objectui's workspace version stays 17.7.0. - **`packages/sdui-parser/objectui-lockstep.json`.** `pnpm gen:sdui-lockstep` re-recorded it against `OBJECTUI_ROOT=.cache/objectui-9dfaca654311`. It records 214 grammar lines (blob `0131f27cf86d`), 25 codes and containment predicate `76c18fb95d1f`. All are unchanged, and objectui's `packages/sdui-parser` has no diff over the range, so no port is owed. - **The 54 asserting pin citations in `packages/spec/src`.** They were re-measured at the new pin, not restamped: - Each asserting record's anchors were resolved in objectui at `2e818d0b5`, mapped through `git diff -U0 2e818d0b5 9dfaca654`, and re-read at the new pin. Each record gains a dated 2026-10-05 hop sentence and keeps its earlier history. - In every cited file that changed, the cited lines moved with their text byte-identical: - objectui#8347 re-worded docblocks in `ObjectGrid.tsx`, `ObjectTree.tsx`, `ObjectGantt.tsx`, `ObjectCalendar.tsx`, `SchemaRenderer.tsx`, `plugin-view/src/ObjectView.tsx`, `plugin-map/src/index.tsx` and `plugin-gantt/src/index.tsx`. - In `ObjectKanban.tsx`, objectui#8347 added a private `GateBoundKanbanSchema` read type, so its fetch, navigation reads and spread moved +30. - objectui#11605 touched `plugin-kanban/src/index.tsx`, `plugin-dashboard/src/index.tsx` and `ElementDataSourceGate.tsx`. The `object-metric` icon input moved `281` → `299` and is still `{ name: 'icon', type: 'string' }`. - objectui#11619 moved the `page:tabs` and `page:accordion` icon anchors in `containers.tsx` by +3. - objectui#11615, objectui#11266 and objectui#8347 moved `ObjectKanbanSchema.limit`, `ObjectMapConfigSchema` and `LIST_VIEW_LOCAL_OVERRIDES` in `objectql.ts` and `objectql.zod.ts`. - objectui#11605 added `view.noObject` to the `en`, `zh` and `de` locale packs. Their cited `calendar.configRequired` strings did not change or move. - One cited line changed content. `ObjectKanban.tsx:10`, the type import, gained `SortConfig` beside the `ObjectKanbanSchema` the record cites. - Two counts were re-taken by their records' own methods, and both read the same: the `keyboardNavigation` hit lines (15, against the `schema.editable` control's 3) and the `ElementDataSourceGate` occurrences in five `src/index.tsx` shells (0, 3, 3, 3 and 4). - The three quoted anchors in `ui/view.zod.ts`'s map record redded at this pin, and each was re-read and re-pointed: `case 'map':` moved `2299` → `2300`, `ObjectMapConfigSchema` `2370` → `2388`, and `LIST_VIEW_LOCAL_OVERRIDES` `1419` → `1437`. - **The six migration entries' corpus counts** were re-taken with `git grep -o -F`. That method first reproduced every `2e818d0b5` number: 7579 files, `objectstack` 17227, `@objectstack/spec` 7134, `timeout` 1351, `useState` 2477, `TTL` 182, `tenant` 1317 and `Span` 505. - The new numbers are 7632 files, 17313, 7186, 1360, 2477, 182, 1318 and 508. The other controls read `RuntimeConfig` 276 → 293, `resourceLimits` 2 → 2, `window` 4175 → 4193, `period` 238, `interval` 195 and `metrics` 374 → 401. - Every zero is still zero: 98 tokens were checked, the export lists of the three cited spec files plus every named key. The only non-zero tokens are the expected `Span` (508) and `SpanSchema` (57). The three new `Span` hits are `colSpan` in objectui's `object-form-section-field-entry-11615.test.ts`. Both `resourceLimits` hits are still prose in `packages/app-shell`. - `packages/spec/src/migrations/registry.ts` was regenerated with `gen:migration-registry`. - **`.changeset/objectui-pin-citations-9dfaca654311.md`** is a `@objectstack/spec` patch, because the `FormField.span` describe and six migration descriptions name the pin. `content/docs/references/ui/view.mdx` was regenerated by `check:generated --fix`. No example, test or gate needed adapting, and no code changed outside generated records, citations and changesets. ## Console build and canaries `build-console.sh` ran locally under the verify lock, after `turbo run build --filter=@objectstack/client...`, the same two steps the `Console Pin Gate` job runs. Exit 0, 9m02s on a shared four-core box. The build log reports: - "Bundle canary 'import/jobs' present". - "Single-zod canary: exactly one zod version literal {major:4,minor:6,patch:5}". - "Console bundle carries THIS tree's @objectstack/spec, and only it". - "@objectstack/console dist ready (64192 KB) from objectui@9dfaca654311". `check:console-sha` and `check:console-injection` exit 0 against that dist. ## Browser smoke: `examples/app-showcase` with the Console built at `9dfaca654311` The server ran `pnpm dev -- --fresh --ui --no-watch --compile -p 41877` with `OS_PORT=41877`, on its own ephemeral DB with the seeded admin. Headless Chromium (`/opt/pw-browsers/chromium`) drove it, signing in through the console's own login form, with console messages, page errors and every 4xx/5xx response captured. Only the PIDs this run started were stopped. **No showcase object declares `imageField`, so the record picture needs one to exist.** `git grep imageField examples` gives 0 hits. To exercise objectui#11619's read, the smoke made a temporary local edit: - It added `imageField: 'f_image'` to `showcase_field_zoo`, through `node scripts/ablation-replace.mjs --hold`. - It compiled and booted the server. - It restored the file at once with `--restore`. The tool reports the blob back to HEAD's `4130858b8f8b` and `git diff HEAD` empty, and `git status --porcelain` is empty. - After the run, the showcase `dist/` was restored from turbo's cache for the clean source: sha256 `cc1034dd9d23…`, as before the smoke. The stray runtime bundle of the edited compile was deleted. Nothing of the edit is in this diff. A real `sys_file` was uploaded through `/api/v1/storage/upload/presigned` → `PUT` → `/upload/complete` (a 64×64 red PNG) and written to "Specimen — Full"'s `f_image`. | record page | `[data-record-picture]` | image | |---|---|---| | Field Zoo "Specimen — Full" (`f_image` set) | 1, beside the title in the record header, `rounded-md` (an `image` field, not `avatar`) | `src="/api/v1/storage/files/b1f4e384-…"`, `alt=""`, loaded, `naturalWidth` 64 | | Field Zoo "Specimen — Minimal" (`f_image` empty) | 0 | none, as objectui#11619 specifies for an empty value | **Verdict:** at this pin the record header draws the record's picture from the object's `imageField`, drawing the stored file through `/api/v1/storage/files/:id`, and draws nothing when the value is empty. `showcase_task`'s record page is the custom `task-detail` page, which has no `page:header`, so it draws no record chrome to put a picture in. That page was smoked too: it renders its path bar, highlights and sections with 0 page errors. **Console messages across the run:** 0 page errors. The only failed loads are a 401 on `GET /api/v1/auth/get-session` (the pre-login probe) and a 404 for `/favicon.ico`. There is no `/api/v1/usage/storage` 404. objectstack-ai#21625's and objectstack-ai#21710's smokes both recorded one, and objectui#11002 (`d2e859936`) stopped the request on a runtime that does not serve it. ## Gates and tests (head `4b9519ea23`) - `node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands` derived 127 commands from the 22-path diff, and all were run at `4b9519ea23`. `--ran` reports "127 derived, 127 run, 0 NOT-MEASURED, 0 UNRUN", with every exit code recorded. - Two exited 3 (`PREREQUISITE NOT MET`) on the first pass: `check:skill-examples` (no `client-react` dist) and `check:dual-build-cjs-loads` (no dist for 34 packages). - Both exited 0 after `turbo run build --filter=!@objectstack/docs` (71 of 72 tasks were cache hits), and those are the codes recorded. - Also exit 0: - `check:objectui-pin-citations --verify-anchors` with objectui at the pin: 54 asserting citations match `9dfaca654`, and 7 anchor content assertions are verified. - `check:objectui-bump` (20 assertions across 5 cases), `check:sdui-lockstep`, `check-sdui-manifest`, `check:console-sha`, `check:console-injection`, `check:migration-registry`. - `@objectstack/spec check:generated`: all 15 artifacts current after the `--fix`. - `pnpm --filter @objectstack/spec exec vitest run` (both projects, `local` and `repo`): 668 files, 19259 passed, 1 todo. `pnpm --filter @objectstack/spec typecheck`: exit 0. - `@objectstack/sdui-parser` test (14 files, 225 passed) and typecheck: exit 0. - These suites read the regenerated manifest: - `@objectstack/lint` test: 119 files, 5627 passed. It declares `sdui.manifest.json` as a test input. - `@objectstack/metadata-protocol` `src/protocol.runtime-authoring-gate.test.ts`: 70 passed. - `@objectstack/cli` unit tier `src/utils/sdui-manifest.test.ts` and `test/validate-build-gate-parity.test.ts`: 2 files, 79 passed. - `test/jsx-gate-manifest-notice.e2e.test.ts` belongs to neither CLI tier; it runs nightly, so it is NOT MEASURED here and is left to CI. - `eslint --no-inline-config --format json` on the 15 changed TS files: 15 files, 0 errors and 0 warnings. The lint population is `**/*.{ts,tsx,mts,cts,js,jsx,mjs,cjs}` (`eslint.config.mjs:971`). The config enables no type-aware linting (`:327-328`), so this diff cannot move a verdict on an untouched file. Repo-wide `pnpm lint` is left to CI. ## Acceptance notes - `main` moved two commits past this branch's base (objectstack-ai#21783, objectstack-ai#21780). Neither touches a path this diff touches, so no merge was made. The merge queue rebuilds on the current `main`. - Anchors in records that cite no asserting sha are outside the pin-citation gate's population and were not re-measured. Several point into files that changed here: `containers.tsx` anchors in `ui/component.zod.ts` docblocks with no sha, and `component.test.ts`'s `plugin-dashboard/src/index.tsx:204` (the `ObjectMetricPropsSchema icon liveness` test, already reading an unrelated line at `ab1879721595`, as objectstack-ai#21710 noted). - After this pin, the spec's snake_case grid-key prescription ("these come in once the widget reads a camelCase spelling") describes a condition that now holds. objectstack-ai#21768 retargets it when it declares the camelCase keys. It is left as is here, because the pin bump changes no accept set. - Writes: one draft PR through the relay and the report comment. No label, no PR assignee, no ready flag and no auto-merge were written, because this dispatch's write budget names none of them. --- _Generated by [Claude Code](https://claude.ai/code/session_016tKoy8NJa35Yih1FdzrVmn)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
Closes #20901
Clause-②: yes (narrowing)
FormViewSchema.subforms[].columnsreferencesInlineGridColumnSchema(wasz.array(z.any())): the card's typedcurrency+scalecolumn and itszzz_not_a_keycolumn are refused at the view parse.defineStack's cross-reference check (packages/spec/src/stack.zod.ts#collectHydratedInlineColumnErrors, called fromvalidateCrossReferences) re-parses a column that declares notypeas the type it renders (currencyover acurrencychild field) throughInlineGridColumnSchema, on both carriers. The card's identity-only column withscaleis refused there with the column schema's own message; there is no secondscalerule.form-view-subform-columns-closedandinline-grid-column-identity-only-currency-scale-refused; the registry was regenerated after merging amainthat carries feat(spec)!: connector-attached sync leaves the connector — syncConfig / fieldMappings retired, mapping gains the connectorSource pull binding (#20281 stage 1) #20903 and fix(objectql,spec)!: a groupBy on a multi-value field and a count_distinct on a JSON-stored field are refused INVALID_FIELD / 400 at the engine aggregate door, on every driver (#20808) #20911.Evidence at
feba1a99bd: pinspackages/spec/src/inline-grid-column-carriers.test.ts13/13;@objectstack/specsuite 584 files / 17179 tests green;@objectstack/spectypecheck green; 114/114 derived gates exit 0. Ablations, each restored withgit diff HEADempty: reference removed, 5 red; cross-reference call removed, 3 red.os validateon a probe stack: identity-only column, exit 1STACK_CROSS_REFERENCE_INVALID; typed and bogus columns, exit 1STACK_SCHEMA_INVALID; valid columns, exit 0.Acceptance notes
subforms[].childObjectbefore this change (validateCrossReferencesread onlyform.data.object), so the child-object lookup is new here.defineStackonly. A view saved through the metadata door, or a subform whose child object lives in another package, gets the schema half alone (NOT MEASURED at the save door). objectui's@object-ui/typesmirror is stillz.any(), and the render-time warning stays the backstop there.field.zod.ts'sscaledescribe still names only the declared-type refusal. PR docs(spec): field.useGrouping is live at the objectui pin, and its docblock describes the heuristic the renderer uses #20908 holds that file.Generated by Claude Code