Repository navigation
chore(deps): resolve @objectstack/* 17.5.0, and the zod 4.6.5 it requires, in pnpm-lock.yaml (objectui#11073) - #11086
Conversation
…i#11073) Regenerated by pnpm 10.31.0, not by hand: every @objectstack/* specifier was raised to ^17.5.0 for one `pnpm install`, then every manifest was restored to HEAD and a second `pnpm install` reconciled the importers back to their declared ranges. No manifest range moves in this commit. Claude-Session: https://claude.ai/code/session_01TdiauJaVCHuj45EzZGUxHh Co-authored-by: Claude <noreply@anthropic.com>
… @objectstack/spec@17.5.0 uses (objectui#11073)
@objectstack/spec@17.5.0 and @objectstack/core@17.5.0 raise their zod floor
from ^4.4.3 to ^4.6.1. With only the @objectstack/* entries moved, the lock
resolved spec's zod to 4.6.5 while objectui's own direct `zod ^4.4.3`
importers (types, app-shell, components, plugin-timeline, test-support) stayed
on 4.4.3. Zod stamps its minor version into its types
(`_zod.version.minor`), so the two copies are type-incompatible and
`pnpm --filter @object-ui/types build` failed with TS2345 in
src/zod/views.zod.ts ("Type '4' is not assignable to type '6'").
Regenerated by pnpm 10.31.0, not by hand: the five direct `zod` ranges were
raised to ^4.6.1 for one `pnpm install`, then restored to HEAD and reconciled
by a second `pnpm install`. No manifest range moves; ^4.4.3 already admits
4.6.5.
Claude-Session: https://claude.ai/code/session_01TdiauJaVCHuj45EzZGUxHh
Co-authored-by: Claude <noreply@anthropic.com>
|
changeset-claim-re-read
|
❌ 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. Which half objected:
📦 Bundle Size Report
Size Limits
|
…ne zod 4 copy (objectui#11073) `pnpm dedupe --check` named exactly one change after the zod move: fumadocs-mdx's own `zod ^4.4.3` dependency, 4.4.3 -> 4.6.5, which removes the last zod@4.4.3. Without it `node scripts/check-lockfile-integrity.mjs` reports "zod gained a physical copy: 2 -> 3" (the Lockfile Integrity Check). Applied with `pnpm dedupe`, not by hand; the resolution census against the base lock is now the seven @objectstack/* 17.4.0 -> 17.5.0 and zod 4.4.3 -> 4.6.5, nothing else. Claude-Session: https://claude.ai/code/session_01TdiauJaVCHuj45EzZGUxHh 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. Which half objected:
📦 Bundle Size Report
Size Limits
|
…ectstack/spec 17.5.0's zod ^4.6.1 (objectui#11073) - imported-defaults-describe-9034: a ZodDefault now answers `optin: 'defaulted'` (zod 4.4.3 answered 'optional'). The census asks the UNWALKED source node, so "already omissible" counts either rung; the corpus figure (5) is unchanged. - mirror-partial-record-narrowing-8516: zod 4.6 reports a partialRecord's out-of-vocabulary key as a non-aborting `unrecognized_keys`, so the `GridSchema.columns` union returns that arm's issue alone: the key is named at `columns` (in `keys` and the message) instead of inside an `invalid_union`. The pin's intent, that the author is shown the key, holds. Claude-Session: https://claude.ai/code/session_01TdiauJaVCHuj45EzZGUxHh Co-authored-by: Claude <noreply@anthropic.com>
…he published @objectstack/spec 17.5.0 (objectui#11073) Each re-pin follows the spec's own new answer, or a tripwire's written instruction for the pin bump; no objectui accept set is decided here. - detail-view-field-options-10296 + zod-mirror-parity: 17.5.0 requires a non-blank `source` on the `visibleWhen` envelope. Both tripwire rows flip to refusals, and the spec-version gate SpecEnvelopeAdmitsSourceless, its gated WiderThanDeclared entries (DetailViewFieldSchema `options`, DetailViewSchema `fields`/`sections`, DetailViewSectionSchema `fields`) and their WIDER_ARMS rows are deleted, as the gate's docblock instructed; the header figures move 6/7/7 - 5/2/0/0 -> 3/3/3 - 2/1/0/0. - element-number-arm-10872 + imported-defaults-8317 + imported-defaults.ts docblock: ElementDataSourceSchema.filter is the ViewFilterRule array now, which reaches no z.lazy, so REBUILT_CLEAN is empty and the boundary hands back the spec's own object; the filter rows follow the spec's verdicts. - imported-defaults-8317 "no member became REQUIRED": zod 4.6's `'defaulted'` rung; the strip must turn it into `'optional'`. - record-highlights-layout-9187: the contract gained requiredPermissions, redactFields, enforceFieldSecurity; `layout` did not move. - calendar-doc-key-set-8830 + calendar-flat-color-allday-8466 + plugin-calendar.mdx: CalendarConfigSchema declares `allDayField`; the doc fence tracks the SPEC type, so it lists five keys, and the sentences that said the spec refuses `allDayField` are corrected. - spec-object-refinements-7715: checkListViewPageMount is no longer exported; its ListView row entry and measurement leave, as its message instructed. Claude-Session: https://claude.ai/code/session_01TdiauJaVCHuj45EzZGUxHh Co-authored-by: Claude <noreply@anthropic.com>
…onsole patch (objectui#11073) `patch` on @object-ui/console: the only published artifact whose bytes move is the console bundle, which inlines its @objectstack/* and zod devDependencies. No manifest range and no @object-ui/* source API moves; the @object-ui/types edits are re-read test pins and one comment. Claude-Session: https://claude.ai/code/session_01TdiauJaVCHuj45EzZGUxHh 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. Which half objected:
📦 Bundle Size Report
Size Limits
|
…ctui#11073) Six commits, none touching pnpm-lock.yaml or a manifest; merged so the patch round measures the tree the merge queue will build. Claude-Session: https://claude.ai/code/session_01TdiauJaVCHuj45EzZGUxHh 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. Which half objected:
📦 Bundle Size Report
Size Limits
|
…s terminally, restoring the objectui#9256 / #11022 refusals; DashboardWidgetSchema attaches the spec's two new checks (objectui#11073) zod 4.6 (forced by @objectstack/spec 17.5.0's zod ^4.6.1) made a strict object's `unrecognized_keys` non-aborting. A plain `z.union` whose one non-aborted failing arm is that strict object returns ITS issues alone, so the widget-slot `metric-card` lost its by-name content-channel refusal and had its registered `value` reported as unrecognized. `closedObject` / `closeStrictUnionArms` (node-derivation.ts) are the spec's own mechanism (its ZodClosedObject marks `unrecognized_keys` continue:false; not exported, so spelled here), applied only to strict arms of PLAIN unions, as closed twins: the widgets slot (DashboardComponentSchema), the calendar selection union (form.zod.ts) and every plain union on the strict authoring face. Blast radius measured over every objectui union before landing: 29 plain unions with a strict arm, none left open, 1106 probes, 0 accept-set moves, 187 error-shape moves (each a single arm's `unrecognized_keys` becoming zod 4.4's `invalid_union` with every arm). Seat ruling Q3 → A. complex.zod.ts also re-attaches the two object-level checks @objectstack/spec 17.5.0 added to DashboardWidgetSchema (checkDashboardWidgetStageOrder, checkDashboardWidgetMetricMeasureArity), under the objectui#7715 B1 ruling; the census and the parity pin for it land in the next commit with the rest of the test re-reads. Claude-Session: https://claude.ai/code/session_01TdiauJaVCHuj45EzZGUxHh Co-authored-by: Claude <noreply@anthropic.com>
…l refusal can no longer let run (objectui#8355, objectui#10321; objectui#11073) @objectstack/spec 17.5.0 marks a closed object's unrecognized_keys continue:false, so zod skipped ObjectViewSchema's when-guarded superRefine (checkNamedViewCalendarAliases, checkNamedViewKanbanStrayGroupBy) on exactly the documents it existed for. The document is still refused by the protocol at listViews.KEY.calendar / .kanban, naming the key. The two checks and their helper are removed; the list-view route keeps objectui's pointers. The pins re-read the protocol's refusal. Seat ruling Q2 -> A (objectui#8934 principle), no upstream card. Claude-Session: https://claude.ai/code/session_01TdiauJaVCHuj45EzZGUxHh Co-authored-by: Claude <noreply@anthropic.com>
…he retired page view kind, as @objectstack/spec 17.5.0 did (objectui#11073) The spec retired type: 'page', pageName and the list view's own tabs (ADR-0049). NamedListView declared pageName/tabs as 'declared inert' under objectui#8980, whose premise (the protocol declares them) is falsified; both are ?: never tombstones now with the protocol's prescriptions. The derived ViewType faces already follow the spec; core's UNDRAWABLE_VIEW_KINDS page row was deleted as its own note said it would be once the residual pins were converted, and they are (objectui#8429 now pins the closed state). The plugin-list README record drops page. Seat ruling Q4 -> A. Claude-Session: https://claude.ai/code/session_01TdiauJaVCHuj45EzZGUxHh Co-authored-by: Claude <noreply@anthropic.com>
…17.5.0 began exporting under check:spec-symbols (objectui#11073)
Per symbol, measured (seat ruling Q6 -> A):
- core isRefusedTextComparand / textComparandRefusalReason: the spec's are byte-identical ports of core's module, so core re-exports them from @objectstack/spec/data (describeComparand stays local; the spec does not export it). They are 17.5.0-only exports, so @object-ui/core's spec floor rises ^17.3.0 -> ^17.5.0 (core declares no zod).
- components ActionButtonProps / ActionIconProps: the spec's are the AUTHORED props bags; these are the React envelope and were never on the package's published entry, so renamed ActionButtonRendererProps / ActionIconRendererProps (objectui#7265 precedent), reason pinned in a tripwire test.
- plugin-dashboard / plugin-report DatasetTotals: the spec's is the REQUEST side ({ groupings }); these are file-local RESULT groupings, renamed DatasetResultTotals, reason pinned per package.
- plugin-gantt / plugin-map / plugin-timeline / plugin-tree ObjectXProps: React props envelope vs the spec's authored bag, measured unequal, and PUBLISHED exports, so not renamed (a public-name change): ALLOW rows carrying the measured member lists.
@object-ui/types' floors rise to @objectstack/spec ^17.5.0 and zod ^4.6.1 together (Q8): its DashboardWidgetSchema now imports two 17.5.0-only checks. pnpm-lock.yaml moves only those three importer specifiers.
Claude-Session: https://claude.ai/code/session_01TdiauJaVCHuj45EzZGUxHh
Co-authored-by: Claude <noreply@anthropic.com>
…rdWidget checks; the objectui#10916 tripwire is flipped (objectui#11073) - spec-object-refinements-7715: the DashboardWidgetSchema row names checkDashboardWidgetStageOrder and checkDashboardWidgetMetricMeasureArity as attached, with a parity pin that is objectui#9111's executable criterion (a non-funnel widget with options.stageOrder refused at options.stageOrder with the spec's own message; a funnel accepted on both). - imported-defaults-8317: the two checks join the named REFINEMENT_EXCEPTIONS. - report-chart-query-spec-parity: the JoinedReportBlock state pin loses its erased arm and the tripwire row flips to typed, its version row retired, as the tripwire instructed. The published-type burn-down stays on objectui#10940. - .changeset/7715-mirrors-carry-spec-object-checks.md: a dated note (prose only, frontmatter untouched) on the two sentences 17.5.0 made false. Claude-Session: https://claude.ai/code/session_01TdiauJaVCHuj45EzZGUxHh Co-authored-by: Claude <noreply@anthropic.com>
…jectui#4795) Brings in PR #11086 (objectui#11073), which moves the workspace to @objectstack/spec 17.5.0 and zod 4.6.5. The textual merge is clean; the CLI importer's lockfile entry still names the 17.4.0 snapshot main no longer carries, and the next commit regenerates it. Claude-Session: https://claude.ai/code/session_012UwY3ahMixEFkfTUxMVkYm Co-authored-by: Claude <noreply@anthropic.com>
…he 17.5.0 snapshot (objectui#4795) `pnpm install` after merging origin/main: the `packages/cli` importer's `@objectstack/spec` (`^17.1.0`) now resolves the same `17.5.0(ai@7.0.65(zod@4.6.5))` snapshot as the rest of the workspace. The 17.4.0 snapshot it named is gone from main since PR #11086, which is what broke `pnpm install --frozen-lockfile` in the merge group. Generated, not hand-edited; no manifest moved. Claude-Session: https://claude.ai/code/session_012UwY3ahMixEFkfTUxMVkYm Co-authored-by: Claude <noreply@anthropic.com>
…keys Brings in the @objectstack/spec 17.5.0 resolution (objectui#11073, #11086), whose ElementDataSourceSchema.filter takes the ViewFilterRule array, so this branch's pins can be measured on the tree the merge queue builds. No conflicts; six files auto-merged. Refs objectui#11070 Claude-Session: https://claude.ai/code/session_012UwY3ahMixEFkfTUxMVkYm Co-authored-by: Claude <noreply@anthropic.com>
…and fields members (objectui#8071 slice 18) (objectstack-ai#11113) Part of objectstack-ai#8071 Clause-②: no — test pins only; no published contract, key, export or flag changes 「lands after objectui#11086: merge main, then re-measure the three pins on 17.5.0」 > Seat amendment (objectui#8071 `5892096260`): the 「lands after objectui#11086」 constraint is relaxed on evidence. `git merge-tree` against objectstack-ai#11086's head auto-merges the ledger, no pinned read site is in objectstack-ai#11086's file list, and the pins survive 17.5.0 per its tarball. This PR lands on its own green CI; whichever of the two lands second runs these pins on its own tree. ## What this does Slice 18 of objectui#8071. It pins the last three `AWAITING_A_PIN` keys, all on one block: `object-master-detail-form.dataSource`, `.details` and `.fields`. In the same commit it deletes their `MEMBER_PIN_EXEMPTIONS` entries and lowers `MEMBER_PIN_EXEMPTION_CEILING` from **4 to 1**. That closes the block. `AWAITING_A_PIN` had no entries left after this, and `apps/console/tsconfig.json` sets `noUnusedLocals`, so the constant is deleted too. A comment where it stood says why. `record:related_list.actions` (`NO_READ_SITE_TO_PIN`) is **not touched**. It holds the ceiling's last unit. | key | pin file | new or promoted | |---|---|---| | `dataSource` | `packages/plugin-form/src/MasterDetailForm.elementDataSource.test.tsx` | promoted (objectstack#7121 file), read end to end, then **grown by 5 rows** | | `details` | `packages/plugin-form/src/__tests__/masterDetailDetailsMembers-8071.test.tsx` | **new**, 8 rows | | `fields` | `packages/plugin-form/src/__tests__/topLevelFieldsWarnCoverage-8847.test.tsx` | promoted (objectui#8847 file), read end to end, then **grown by 5 rows** | ## Why `Part of`, not a closing keyword The card's own text defines the work as turning **every** listed key into a pin: 「本卡的工作就是把它们一个个变成 pin」. `record:related_list.actions` is on that list. It has no read site, so no pin can meet the card's criterion. Its remedy is the enforce-or-remove decision the spec owns, and until that decision lands the entry stays and the card's population is not closed.⚠️ There is a second reason, measured today. The `NO_READ_SITE_TO_PIN` constant says the entry "belongs to" objectui#7300, "which is open". **objectui#7300 is closed as `not_planned`** (REST read on 2026-09-29). So objectstack-ai#8071 is now the only open card whose ledger text names this last entry, and closing it would leave the entry with no live tracker. This PR does not edit that constant (it is out of scope for this dispatch). The stale sentence is listed under Acceptance notes. ## H2: the real read site for each key⚠️ The read sites are in **`packages/plugin-form`**. None is in `packages/plugin-grid`, which is what the claim's file surface names. See Deviation below. | key | where the renderer reads it | what reads each member | |---|---|---| | `dataSource` | `MasterDetailFormRenderer` (plugin-form `index.tsx`) wraps `ElementDataSourceGate` with **no `mapping`**, so the default applies and only `object` lands, on `objectName`. The gate lives in `packages/react/src/element-data-source/`. | `object` becomes `objectName`, unconditionally. From there `MasterDetailForm` uses it three ways: the parent `ObjectForm`, `deriveDetail`'s FK lookup, and the batch's parent leg. `view` is resolved: an unresolvable view reports, a resolving one contributes nothing. `filter` is not mapped, but beside a `view` it is **merged**, and a malformed one refuses the block. `sort` and `limit` are not read at all. | | `details` | `MasterDetailForm` itself: `rawDetails = schema.details`, the derive effect, `MasterDetailLines`, `sendBatch`, `readLines`. | `title` becomes the section heading. `columns` go to the grid. `sortField`, `minRows`, `maxRows`, `addLabel` and `amountField` reach the grid **renamed**, as `sort_field`, `min_rows`, `max_rows`, `add_label` and `total_field`. `inlineMode` and `formFields` choose the display mode and the row form. `childObject` and `relationshipField` address the batch creates and the edit-mode read. `totalField` receives the rollup on the parent leg. | | `fields` | **Not read by `MasterDetailForm`.** The `parentSchema` memo copies `fields: schema.fields` onto an `object-form`-shaped node rendered through a directly imported `ObjectForm`, which falls through to `SimpleObjectForm`. That is the real read site. | Members are bare parent field names in authored order, and `{ name }` is tolerated. The `FormFieldSchema` object form resolves to no name and draws the route-1 warning. | ## H3: will the pins survive `@objectstack/spec` 17.5.0? Yes, per the tarball I read the 17.5.0 tarball read-only and diffed it against the installed 17.4.0: - `ObjectMasterDetailFormPropsSchema` is **identical** for `fields` and `details`. Both are `z.array(z.unknown())` with the same descriptions, so every member parses on both versions. No pin row parses them anyway, because the read site is the whole member contract. - `dataSource` (`ElementDataSourceSchema`) **does change**: `filter` moves from `FilterConditionSchema` (a MongoDB-style record) to `z.array(ViewFilterRuleSchema)`. No pin row parses the binding. The new rows spell `filter` in the 17.5.0 rule-array form, and the one row that needs a malformed filter uses a rule the **runtime** lowering refuses: an array value on `equals`, objectui#8557. - PR objectstack-ai#11086's file list touches **none** of the read sites these pins exercise: `MasterDetailForm.tsx`, `ObjectForm.tsx`, plugin-form `index.tsx`, `ElementDataSourceGate.tsx`, `element-data-source.ts`, `filter-converter.ts`, `deriveMasterDetail.ts`, `masterDetailTx.ts`. It does touch `GridField.tsx`, but only the currency and percent scale clamp, which these pins do not exercise. - Trial merge with `git merge-tree` of this branch against PR objectstack-ai#11086's head `174c4a7`: the ledger **auto-merges**, because objectstack-ai#11086's hunks and this branch's hunks do not overlap. The only conflict is `packages/fields/src/index.tsx`, which this branch does not touch. It is between objectstack-ai#11086 and `main`. - ⛔ NOT MEASURED: the pins actually **run** on 17.5.0. That happens after objectstack-ai#11086 lands, per the line at the top. ## The ledger, re-derived by TypeScript AST on both trees (not taken from the order) | | base `88fbd79` | head | |---|---:|---:| | `MEMBER_PIN_EXEMPTIONS` | 4 | **1** | | `MEMBER_PINS` | 98 | **101** | | `MEMBER_PIN_EXEMPTION_CEILING` | 4 | **1** | | `NEWLY_JUDGED_UNPINNED_MEMBERS` | `[]` | `[]` | | non-property members in either map | `[]` | `[]` | The sets match exactly. The three keys that left the exemptions are the three that entered the pins. Nothing entered the exemptions and nothing left the pins. What remains is `record:related_list.actions = NO_READ_SITE_TO_PIN`, and 1 equals the ceiling. ## Tests (head `ae719c4`) - The three pin files together with the ledger: 4 files, **223 passed**. - `pnpm exec vitest run packages/plugin-form/`: **144 files, 1719 passed, 1 skipped**. - `pnpm exec vitest run apps/console/`: **131 files, 1475 passed**. The ledger file alone had 199 passed. - Type-check. plugin-form: `turbo run type-check` gave 13/13 successful. `--listFiles` shows the `tsconfig.test.json` leg lists all three pin files, while the main leg lists 0 of them (it excludes tests). Console: `tsc --noEmit && tsc -b tsconfig.node.json` exit 0, and `--listFiles` includes the ledger. - Lint, narrowed to the changed files with the narrowing measured. `eslint --no-inline-config --format json` over the 4 changed test files reports **4 files, 0 errors, 20 warnings**, all `no-explicit-any`. `eslint.config.js` sets no `parserOptions.project` or `projectService`, so the lint is not type-aware and this diff cannot change the verdict on any untouched file. Repo-wide `pnpm lint` is left to CI. - Gates, each exit 0: `check:control-bytes`, `check:new-line-citations` (0 new), `check:test-path-roots`, `check:vi-mock-specifiers`, `check:vi-mock-inherit`, `check:vi-mock-override-shape`, `check:changeset-claims`, `check:pending-changeset-literals`, `check:unreferenced-sources`, `check-changeset-presence` (reads the empty frontmatter as the declaration), `check-changeset-no-major`, `check-changeset-fixed`, `check-changeset-overwrite`. - `check:changeset-claims` names `.changeset/8067-component-input-member-kind.md` because it mentions the ledger file. I re-read the paragraph: it is a past-tense account of the `page:header.actions` drift, and this diff does not falsify it. No correction is owed. ## Ablations Each leg ran on the committed tree through `ablation-replace.mjs`. Before any result was read, the tool proved the mutation on disk (anchor count, blob hash). Each leg restored under the tool's trap, with the restore proven by blob equal to the HEAD blob and an empty `git diff HEAD`. The vitest config aliases every `@object-ui/*` import to `src`, so the mutated source is what ran and no dist preflight applies. - **A, the gate mechanism.** I re-added `object-master-detail-form.details` as an exemption and left the ceiling untouched. Blob `e591038c` became `335d11ab`. Result: **3 failed, 196 passed**. The three are the census partition row, `carries no stale member-pin exemption`, and `only ratchets DOWN` with "expected 2 to be less than or equal to 1", which proves the ceiling moved with the list. - **B, `details`.** I deleted `min_rows: d.minRows,` from the grid object. Result: **2 failed**, rows 2 and 2b, and the other two pin files stayed green in the same run. The first run of this leg reddened row 2 only, because `toEqual` treats a key holding `undefined` as absent. Row 2b now asserts the key set too, and the leg was re-run on `ae719c4`. - **B, `dataSource`.** I changed the gate's `next[objectKey] = composed.object` to the `??=` spelling. Result: **2 failed**, the OUTRANKS row and the detail-half row, with neighbours green. The detail-half row fails at its first wait: the parent form is the old object `order`, whose only field is `code`, so the bound object's field never appears. - **B, `fields`.** I deleted `fields: schema.fields,` from the `parentSchema` memo. Result: **5 failed**, every row in the file that authors a non-default `fields`, including the pre-existing route-1 warning row. The bare-name control and the no-`fields` control stayed green, as did both neighbour files. ## Acceptance notes (not changed here) -⚠️ **Handed back as a finding (class b): the `fields` description versus the parent leg.** The `object-master-detail-form` `fields` registration calls the key the parent pool for "values, create defaults and the submitted set". Measured: a seeded parent field that `fields` does not list is **still written** on the batch's parent leg. With `fields: ['status']` and `initialValues: { status, memo }`, the parent leg carries both. The pin records this as behaviour. Fixing it means changing either the renderer or the declaration, and this card writes pins only. - The `NO_READ_SITE_TO_PIN` text ("objectui#7300 … is open") is stale: objectstack-ai#7300 is closed as `not_planned`. Out of scope for this dispatch. - All four `fields` registration descriptions (`object-form`, `form`, `embeddable-form`, `object-master-detail-form`) still say the `FormFieldSchema` object member "is silently skipped". Since objectui#8738 route 1 it draws a named warning. Wording only; no carrier. - The spec's `details` description names five of the twelve members the renderer reads (`title`, `childObject`, `addLabel`, `columns`, `relationshipField`). That is this card's own subject, now pinned. - `masterDetailFormTypeVocabulary.test.tsx` still carries six cross-file `ObjectForm.tsx` line addresses, which AGENTS.md objectstack-ai#11 bans. This diff does not touch that file, so the opportunistic-repair rule does not apply. With this block closed, no later slice of this card will touch it either. No carrier. - From reading the sources only, not measured at any entry point: the registration declares `details` as `required: true`, the spec declares it `.optional()`, and the renderer tolerates it missing (`schema.details || []`). - I also corrected a stale reason in the promoted `topLevelFieldsWarnCoverage-8847.test.tsx` docblock ("`fields` is documented as ignored once `sections` is given"), which objectui#9884 had already falsified. The change is limited to that docblock paragraph. ## Deviation: the claim's file surface The claim lists `packages/plugin-grid/src/__tests__/` "only if a pin needs a renderer-side fixture there". The dispatch's H2 flagged that entry as inherited and possibly wrong. Measured: this block renders from `packages/plugin-form`, so the three pin files live there. Nothing under `packages/plugin-grid` is touched. The claim's file surface needs amending; that belongs to the seat, and this PR posts no claim. --- _Generated by [Claude Code](https://claude.ai/code/session_01TdiauJaVCHuj45EzZGUxHh)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
…Script twin declared (objectui#6152, round 1) (objectstack-ai#11125) Refs objectstack-ai#6152 Clause-②: yes — mirroring a declared key widens both zod faces; removing an unread declared key narrows a published TypeScript type (stated as a break in a `minor` changeset). The PR waits as a draft for the director seat's contract review. > This PR uses only the mirroring half of the Clause-② line above. It narrows no TypeScript declaration. The removals this round measured belong to the `ObjectGridSchema` pair, which moved to the next round (see **Scope** below). ⛔ Draft for the director seat's contract review. Do not mark it ready, and do not enable auto-merge. ## What changed `UnmirroredDeclared['objectql.zod.ts#ObjectFormSchema']` (in `packages/types/src/__tests__/zod-mirror-parity.test.ts`) recorded 21 keys. The published `ObjectFormSchema` TypeScript type invites these keys, but the zod mirror had never heard of them. So the two faces gave different answers: - the tolerant face (`BaseSchema` is `.passthrough()`) kept any value at those keys without judging it; - the strict authoring face (`StrictAnyComponentSchema`) refused the keys, although `tsc` accepts them. That is objectui#5250's M3 class (iii). This round measured every key in the entry and routed each one: - **19 keys MIRRORED** (route a) in `packages/types/src/zod/objectql.zod.ts`. Each is shaped as the TypeScript twin declares it. `nextText` / `prevText` are the spec's `I18nLabelSchema` by reference, because the twin takes `I18nLabel` from the spec. objectui#10993 bound the form's other five label members the same way. - `sections` gets a module-private entry schema, `ObjectFormSectionEntrySchema`, member for member with `ObjectFormSection`. One exception: a `fields` entry stays `z.any()`, as the mirror's existing `customFields` does. `FormFieldSchema` carries its own `KnownDrift` / `UnmirroredDeclared` rows, and binding it here would import that drift. - The object-view `form` slot (`ObjectFormSchema.omit(...)`) inherits all 19 members. - **2 keys LEFT** in the entry, with their routes open: `open` and `submitHandler`. See the open questions. - Ledger: **12 entries / 84 keys → 12 entries / 65 keys**. The split moves from 3 / 37 spec-derived to 3 / 18. Every docblock figure the file pins moved with it, in the prose convention the file uses: the file header, both ledger docstrings, the split, and the totals line. - `object-form-i18n-label-members-10993.test.ts`: its row "`nextText` and `prevText` are not mirrored" asked to move together with the ledger. It now holds all seven label members. - **New pin file**, `packages/types/src/__tests__/object-form-unmirrored-members-6152.test.ts`. For each mirrored key it pins: - the key is a mirror member; - an authored value parses on both the strict face and the tolerant face; - a wrong-typed value is refused at the key on the tolerant face. The file also pins: - the object-view `form` slot; - a section is closed on the strict face; - the catalog document M3 charged now parses strict; - `open` / `submitHandler` stay out of the shape. - **One file outside the claimed surface**: `packages/app-shell/src/views/metadata-admin/previews/__tests__/block-config-schema-parity-8216.test.ts`. Its ledger row `object-form::formType@node` pointed at objectstack-ai#6152. It went stale the moment `formType` was mirrored, and its own ratchet turned red: "every ledger row still applies". The row is deleted, with a tombstone comment like the one above it. - A live control re-measures the retired verdict. The node-face ledger is now empty, and an empty ledger reads exactly like a broken oracle. - This is an in-place repair. All four conditions hold: same defect class; a mechanical change the ratchet pinned; no other claim holds the file (the only in-flight PR touching it, objectstack-ai#11086, edits the `EXEMPT` block, a separate hunk); same gate family.⚠️ The claim's file surface needs this path added. - Changeset `.changeset/6152-object-form-unmirrored-members.md`: `@object-ui/types` **minor**. It names every mirrored key and states the tolerant face's new refusal of wrongly typed values as the breaking half. ## Per-key table: `objectql.zod.ts#ObjectFormSchema` Two instruments, both scratch-only (not committed): - **Authored:** an AST/JSON census over every tracked `.json`, the JSON fences in `md`/`mdx`, and TS object literals carrying `type: 'object-form'` (plus the object-view `form` slot). - Positive control: the census sees `objectName` / `mode` on the same nodes (catalog 2, fences 3, in-code 12 src). - **Read:** a TypeScript type-checker census. It finds every property read, `in` check and destructure whose receiver resolves to `ObjectFormSchema`, including plugin-form's mapped `LocalizedObjectFormSchema`. - Positive controls: `objectName` 32 reads, `mode` 28. - Its first run read `mode` = 0 and was discarded as a broken instrument: it could not see through the mapped type. The fix resolves property symbols to their declaration. - A cast read is invisible to the checker, so the three cast reads were found by hand and are quoted as such. "spec" is `@objectstack/spec` 17.4.0: `CPM` = `ComponentPropsMap['object-form']`, `FV` = `FormViewSchema`. | key | authored (documents · in-code src) | read site (symbol · quoted expression) | spec | route | | --- | --- | --- | --- | --- | | `formType` | catalog `plugin-form/object-form-tabbed-sections.json`; fences in `api/schema-reference.md` (also its object-view `form` slot) and `plugins/plugin-form.mdx` · 9 hosts | `ObjectForm` · `schema.formType` (the variant fork) | CPM | a · mirrored | | `sections` | same catalog doc + 2 fences · 5 hosts | `ObjectForm` · `schema.sections`; `ObjectView` · `schema.form?.sections` | CPM | a · mirrored | | `defaultTab` | catalog tabbed-sections doc | `ObjectForm` · `defaultTab: schema.defaultTab` | CPM | a · mirrored | | `tabPosition` | none | `ObjectForm` · `tabPosition: schema.tabPosition` | CPM | a · mirrored (read; protocol-authorable) | | `allowSkip` | tests only | `ObjectForm` · `allowSkip: schema.allowSkip` | CPM | a · mirrored (read; protocol-authorable) | | `showStepIndicator` | fence in `plugins/plugin-form.mdx` | `ObjectForm` · `showStepIndicator: schema.showStepIndicator` | CPM | a · mirrored | | `nextText` | none | `ObjectForm` · `nextText: schema.nextText` | CPM (I18nLabel) | a · mirrored by reference | | `prevText` | none | `ObjectForm` · `prevText: schema.prevText` | CPM (I18nLabel) | a · mirrored by reference | | `splitDirection` | none | `ObjectForm` · `splitDirection: schema.splitDirection` | CPM | a · mirrored (read; protocol-authorable) | | `splitSize` | none | `ObjectForm` · `splitSize: schema.splitSize` | CPM | a · mirrored (read; protocol-authorable) | | `splitResizable` | none | `ObjectForm` · `splitResizable: schema.splitResizable` | CPM | a · mirrored (read; protocol-authorable) | | `drawerSide` | fence in `api/schema-reference.md` (object-view `form` slot) · `FieldDesigner` | `ObjectForm` · `drawerSide: schema.drawerSide` | CPM | a · mirrored | | `drawerWidth` | 0 documents · `FieldDesigner` | `ObjectForm` · `drawerWidth: schema.drawerWidth` | CPM | a · mirrored (read; protocol-authorable) | | `modalSize` | 0 documents · `useActionModal`, `ObjectManager` | `ObjectForm` · `modalSize: schema.modalSize` | CPM | a · mirrored (read; protocol-authorable) | | `modalCloseButton` | none | `ObjectForm` · `modalCloseButton: schema.modalCloseButton` | CPM | a · mirrored (read; protocol-authorable) | | `mobile` | none | `ObjectForm` · `const mobileOpts = schema.mobile` | CPM | a · mirrored (read; protocol-authorable) | | `buttons` | form-view metadata relayed: `RecordFormPage` (`formDef.buttons`), `ObjectView` (`schema.form?.buttons`) | `ObjectForm` · `foldFormButtons`: `(schema as { buttons?: … }).buttons` (a cast read) | FV only | a · mirrored; spec half reported below | | `defaults` | relayed the same way (`formDef.defaults`, `schema.form?.defaults`) | `ObjectForm` · `foldFormButtons`: `(schema as { defaults?: … }).defaults` (a cast read) | FV only | a · mirrored; spec half reported below | | `subforms` | relayed: `RecordFormPage` / `ScreenView` (`objectDef.form?.subforms`), `ObjectView` (`schema.form?.subforms`) | `ObjectForm` · `(schema as any).subforms?.length` (a cast read) | FV only | a · mirrored; spec half reported below | | `open` | in-code only: `AppContent`, `useActionModal`, `ObjectManager`, `FieldDesigner`, the BYO example · 0 documents | `ObjectForm` · `open: schema.open` (drawer and modal arms) | neither (CPM refuses it) | b · **not mirrored**; open question 1 | | `submitHandler` | in-code only: `MasterDetailForm` (`submitHandler: submitViaBatch`) · 0 documents | `ObjectForm` · `await schema.submitHandler(writePayload)` | neither (CPM refuses it) | handler · **not mirrored**; open question 1 | "protocol-authorable" is stated because it is a judgment call the director may overrule. Eleven of the mirrored keys have zero document occurrences in this repository (`allowSkip` appears in tests only; `drawerWidth` and `modalSize` are written only by in-code hosts). They are read, and the spec publishes each as an author-facing member of `ComponentPropsMap['object-form']`. - They are not route (b): the protocol models each as authored metadata, and nine of them have no writer at all (not even a synthesising one). - They are not route (c): they are read. - They are not route (d): none is a second spelling. So the objectstack-ai#6170 family rule, "the exported type aligns to the measured authored+read set", keeps them declared, and mirroring is what closes declared ≠ enforced for them. ## Scope: stopped at the `ObjectFormSchema` pair boundary The dispatch allowed this: "If round 1 becomes too large to review as one PR, stop at a pair boundary, with `ObjectFormSchema` first". The two remaining pairs were measured in full and are handed to the next round as a worklist. They were not implemented, for three reasons: - `ObjectGridSchema`'s mirrors include `bulkActionDefs` (a nested `BulkActionDef` / `BulkActionParam` shape whose `visible` indexes the spec's slot type) and `conditionalFormatting` (the two-arm rule union the `ListViewSchema` mirror spells inline). - Its removal half narrows a published TypeScript type. That needs a runtime inertness reading AGENTS.md requires, and a rewrite of `p1-spec-alignment.test.ts`, which writes all three keys. - That is a second contract-review question, not more of this one. ### `objectql.zod.ts#ObjectGridSchema`: measured, not implemented Measured with the same two instruments. "spec" is `ComponentPropsMap['object-grid']`, a strict object. | key | authored | read site | spec | route (proposed) | | --- | --- | --- | --- | --- | | `aggregations` | none | `ObjectGrid` · `aggregations: schema.aggregations` | yes | a | | `bulkActionDefs` | relayed from named views: `ObjectView` · `tableRelay.bulkActionDefs` | `ObjectGrid` · `Array.isArray(schema.bulkActionDefs) ? schema.bulkActionDefs : []` | yes | a (nested mirror) | | `conditionalFormatting` | relayed (`tableRelay.conditionalFormatting`) | `ObjectGrid` · `conditionalFormatting: schema.conditionalFormatting as unknown[] \| undefined` | yes | a | | `grouping` | relayed (`tableRelay.grouping`) | `ObjectGrid` · `schema.grouping?.fields` | yes | a, by reference (the twin takes `GroupingConfig` from the spec) | | `navigation` | tests only | `ObjectGrid` · `navigation: schema.navigation` | yes | a, by reference (`ViewNavigationConfig` is the spec's `NavigationConfig`) | | `operations` | fences in `README.md` and `api/schema-reference.md` | `ObjectGrid` · `'operations' in schema ? schema.operations : undefined`; `ObjectView` · `schema.table?.operations` | yes | a | | `reorderableColumns` | none | `ObjectGrid` · `reorderableColumns: schema.reorderableColumns ?? false` | yes | a | | `rowColor` | relayed (`tableRelay.rowColor`) | `ObjectGrid` · `useRowColor(schema.rowColor)` | yes | a, by reference | | `rowHeight` | relayed (`tableRelay.rowHeight`) | `ObjectGrid` · `resolveRowHeightMode(schema.rowHeight)` | yes | a | | `singleClickEdit` | fence in `plugins/plugin-grid.mdx` | `ObjectGrid` · `singleClickEdit: schema.singleClickEdit ?? true` | yes | a | | `resizableColumns` | fence in `api/schema-reference.md` | `ObjectGrid` · `schema.resizable ?? schema.resizableColumns ?? true` | yes, as "Alternate spelling of `resizable`" | **d** · alias retirement; spec-modelled, so the spec half forks; open question 2 | | `emptyState` | tests only (`p1-spec-alignment.test.ts`, `object-view-table-slot-10976.test.ts`) | **none** on the grid (checker census 0; the `emptyState` reads in `ListView` / `ObjectView` are on other types) | no (strict refusal) | **c** · retire (`?: never` + `retirementTombstone`) | | `rowSpecActions` | tests only | **none** (checker 0, source grep 0) | no | **c** · retire, naming `rowActions` | | `bulkSpecActions` | tests only | **none** (checker 0, source grep 0) | no | **c** · retire, naming `bulkActions` |⚠️ The three route-(c) rows are source/checker readings. AGENTS.md says a zero from a source read does not answer "no renderer reads this key". The round that removes them owes a runtime probe first: a real `SchemaRenderer` and registry, varying only that key. ### `navigation.zod.ts#PaginationSchema`: measured, not implemented | key | authored | read site | spec | route | | --- | --- | --- | --- | --- | | `currentPage` | catalog `components-basic-pagination/basic-pagination.json` and `with-item-count.json`; `apps/site` playground | `pagination` renderer · `schema.currentPage \|\| schema.page \|\| 1` | no pagination component in the spec | **d** · a second spelling of the mirrored `page` (the twin calls `page` the "Legacy page property"); open question 3 | ## Acceptance: objectui#5250 M3 class (iii), re-run This used a scratch port of PR objectstack-ai#11069's `findUndeclaredKeys` (⛔ not committed; none of objectstack-ai#11069's wiring). It ran over the SHIPPED face: `StrictAnyComponentSchema` from a freshly built `packages/types/dist`. The corpora were those of `scripts/measure-strict-authoring-face.mjs` (catalog, docs JSON fences, apps plus `packages/*/examples`), plus `examples/**/*.json`. Class (iii) = membership of the `UnmirroredDeclared` ledger in the same tree, which tsc reconciles against the compiler measurement. | reading | documents judged | strict-refused | strict-only | refusal pairs | class (iii) pairs · occurrences · strict-only | | --- | --: | --: | --: | --: | --- | | before (`fe41dc76a`, base ledger) | 620 | 190 | 121 | 109 | **10 · 15 · 13** | | after (this branch) | 620 | 186 | 117 | 103 | **4 · 5 · 3** | The before reading reproduces M3's published class (iii) exactly: 10 pairs, 15 occurrences, 13 strict-only. It is the positive control for the port. - **The six pairs that left:** `object-form` · `formType`, `sections`, `defaultTab`, `showStepIndicator`; `object-view` · `form.formType`, `form.drawerSide`. - **The four that remain** are the next round's: `object-grid` · `operations`, `singleClickEdit` (route a) and `resizableColumns` (route d); `pagination` · `currentPage` (route d). - **No refusal pair appeared.** The before/after diff of all refusal pairs has deletions only. So closing `sections` surfaced no nested refusal in the corpora. - **Tolerant-face verdicts are unchanged:** 69 tolerant-refused documents on both readings. ## Ablations Predictions were written to a scratch file before each run. Each mutation went through objectstack's `scripts/ablation-replace.mjs` in wrap mode, on committed code (HEAD `78b4eee67`): - the anchor had to hit exactly once; - the blob change is proven on disk; - the restore is proven by blob equality with HEAD and an empty `git diff HEAD`; - the tree was porcelain-clean afterwards. The subject resolves to `src` (relative imports; the root vitest alias maps `@object-ui/types/zod` to `packages/types/src/zod/index.zod.ts`), so no `dist` leg exists. | ablation (route) | predicted | observed | | --- | --- | --- | | **A1**: delete `formType` from the mirror; ledger untouched (route a) | tsc exit 2, `TS2322` at `assertionUnmirroredMatchesLedger` naming the pair; 5 red in the 6152 pin file; 2 red in block-config 8216; parity runtime half green | **as predicted**: tsc exit 2 with `Type '"objectql.zod.ts#ObjectFormSchema"' is not assignable to type 'never'` at `assertionUnmirroredMatchesLedger`; vitest 7 failed / 107 passed. The 7 are the member, both-faces, wrong-typed, object-view slot and catalog rows, plus 8216's "no control writes a name its oracle refuses" and its new node-face control. The parity runtime file stayed green. | | **A2**: put `formType` back into the ledger while the mirror keeps it, a stale ledger key (the ledger half) | tsc exit 2 at the same assertion; parity runtime red on the objectui#8222 / objectstack-ai#8243 / objectstack-ai#7279 figure pins | tsc **as predicted**. Runtime: 2 red, "the split figures and the totals the header writes down equal the ledger" (objectui#7279) and "both ledger docstrings state the key total" (objectui#8243).⚠️ **One pin fewer than predicted:** objectui#8222's pin does not read `UnmirroredDeclared`'s key total. It defers that figure to objectui#7279's pin by design, and objectstack-ai#7279's pin reads the file-header bullet too. | ## Gates (at HEAD `78b4eee67`, exit codes captured before any pipe) | gate | exit | verdict line | | --- | --: | --- | | `pnpm --filter @object-ui/types type-check` (`tsc --noEmit && … tsconfig.examples.json && … tsconfig.test.json`) | 0 | no diagnostics | | `pnpm exec vitest run packages/types/ …/block-config-schema-parity-8216.test.ts` (root form) | 0 | `Test Files 283 passed (283)` · `Tests 6555 passed (6555)` | | readers' suites: `packages/plugin-form/` + the `ObjectView` form-slot tests + `RecordFormPage` tests | 0 | `Test Files 149 passed (149)` · `Tests 1765 passed \| 1 skipped` | | zod-face consumers: `examples/schema-catalog/test/`, app-shell `previews/__tests__/`, `packages/cli/src/__tests__/`, four `apps/console` contract tests | 1 → 0 | first run red only on the stale 8216 row (fixed above); 8216 re-run `Tests 15 passed (15)` | | `pnpm --filter @object-ui/types lint` · `pnpm --filter @object-ui/app-shell lint` | 0 · 0 | `0 errors`; no finding on a changed line (the 7 warnings in touched files sit on untouched lines) | | `check-changeset-presence` · `-no-major` · `-fixed` · `-overwrite` | 0 ×4 | declares `.changeset/6152-object-form-unmirrored-members.md` · no major | | `check:changeset-claims` · `check:pending-changeset-literals` | 0 · 0 | report-only; the self-contradiction reading passes | | `check:control-bytes` · `check:new-line-citations` | 0 · 0 | `OK` · `0 new citation(s)` | | `check:spec-symbols` · `check:test-path-roots` · `check-type-check-coverage` | 0 ×3 | `0 untriaged collisions` · `OK` · `43/43 packages compile their tests` | | `check:handler-key-reads` · `check:element-data-source-declaration` · `check:unreferenced-sources` | 0 ×3 | OK | | `check-governed-queue-guard --test` (the 6 changed paths) · `--self-test` | 0 · 0 | `NOT GOVERNED` · self-test OK | **NOT MEASURED locally:** - **app-shell's `tsconfig.test.json` type-check.** Reason: its dependency closure's `dist` is not built in this worktree. The one edit there calls the file's own `judge(schema: unknown, name: string)` with a string. Declared to CI. - **The repo-wide `pnpm lint`.** CI's. - **`pnpm check`,** the CLI self-check in `lint.yml`. It is advisory (exit 0 unless a JSON file is unreadable), and the M3 re-run shows tolerant verdicts unchanged across the corpora. Declared to CI. - **`check:doc-snippets`.** No docs fence is touched. No TypeScript type was narrowed, so no downstream type-check is owed. ## Open questions (in the round report on objectstack-ai#6152, with options) 1. **`submitHandler` and `open`.** - `submitHandler`: objectui#6182 rules it runtime-only (function arm). But `RuntimeOnlyDeclared`'s shape pin admits `/^on[A-Z]/` spellings only. - `open`: a host-driven boolean. It is not callback-shaped, so it is not that ledger's either. - Recommended: widen the shape pin to a named allow-list by ruling. 2. **`ObjectGridSchema.resizableColumns`**: canonical `resizable` (the spec's own description, the renderer's first read, the twin's `@deprecated`). The spec half forks to objectstack. 3. **`PaginationSchema.currentPage`**: canonical `currentPage`, and `page` retires. Evidence: the renderer's first read, the twin's primary member, and the only authored spelling. ## Remaining rounds (not this PR) - **This card's leftovers:** - `objectql.zod.ts#ObjectGridSchema` (14; worklist above); - `navigation.zod.ts#PaginationSchema` (1; route d); - `objectql.zod.ts#ObjectFormSchema` (`open`, `submitHandler`; open question 1). - **The other pairs:** - `data-display.zod.ts#DataTableSchema` (17) - `views.zod.ts#DetailViewSchema` (11) - `form.zod.ts#FormSchema` (8) - `complex.zod.ts#ChatbotSchema` (3) - `reports.zod.ts#ReportComponentSchema` (3) - `complex.zod.ts#ChatbotFloatingSchema` (2) - `form.zod.ts#FormFieldSchema` (1; objectui#11070 in flight re-derives its half) - `form.zod.ts#LabelSchema` (1) - **Spec-derived, routed to objectstack-ai#2231 and ⛔ not a local edit:** `complex.zod.ts#DashboardWidgetSchema` (2). ## Acceptance notes - **Producer boundary, per objectui#6170's binding check.** - `@objectstack/spec` 17.4.0's `ComponentPropsMap['object-form']` refuses `buttons`, `defaults` and `subforms` with `unrecognized_keys` (measured). Its `FormViewSchema` declares all three. - objectui's `object-form` reads them: `RecordFormPage` relays a form view's values onto the node. - The local half is mirrored here. The spec half forks to the spec lane and is reported to the seat, ⛔ not edited here. - **A `sections[].fields` entry is judged nowhere on this key** (`z.any()`, the `customFields` precedent). It becomes judgeable when `FormFieldSchema`'s own ledger rows close. - **This PR overlaps two in-flight PRs,** objectui#11070 (PR objectstack-ai#11115) and objectui#11073 (PR objectstack-ai#11086), in `objectql.zod.ts` and `zod-mirror-parity.test.ts`. - No hunk touches their keys or ledger rows. - objectstack-ai#11115's docstring edits rewrite the same `UnmirroredDeclared` totals line (`**12 entries / 84 keys** — …`), so whichever PR lands second merges `main` and re-derives that line. - With both applied it reads 12 / 65 with the split 4 / 19 spec-derived, 8 / 46 local. objectstack-ai#11115 moves `FormFieldSchema`'s one key into the spec-derived half. - `origin/main` moved three commits past `fe41dc76a` during this round, none touching this diff's files. No merge was needed. ## Round 2: `main` merged after PR objectstack-ai#11115 (objectui#11070), figures re-derived Written 2026-09-30T08:17Z by the dispatched dev, session `https://claude.ai/code/session_012UwY3ahMixEFkfTUxMVkYm`. Two things opened this round. PR objectstack-ai#11115 landed on `main` as `b0a05dda1`, and the maintainer ruled on this PR's pending-changeset edit: 「改成追加说明 (Recommended)」 (seat comment `5904913872` on objectui#6152). The director's PASS (`5904218639`) was given at `a5884b403`. This round moves the head, so that PASS is spent and `needs:contract-review` is back on this PR for a new record. The director's earlier deferral (`5903664978`) was answered by the base-merge commit `f78dc3602`, the head `5904218639` reviewed. No rebase and no force-push: three commits on top of `a5884b403`, each pushed fast-forward. | commit | what | | --- | --- | | `f3e9d830f` | merge of `main` `b0a05dda1` (parents `a5884b403`, `b0a05dda1`) | | `4ed5adf6f` | the dated note appended to `.changeset/10993-object-form-i18nlabel.md` | | `8f9bb76f6` | the dated note appended to `.changeset/7200-object-form-section-style-keys-undeclared.md` (see **The pending-changeset re-read** below) | ### The merge: two conflict hunks, one file Merging `b0a05dda1` conflicted only in `packages/types/src/__tests__/zod-mirror-parity.test.ts`, in two docblock hunks of the `UnmirroredDeclared` split section. The ledger itself (`interface UnmirroredDeclared`) and `SPEC_DERIVED_PAIRS` merged without a conflict: - PR objectstack-ai#11115 added `form.zod.ts#FormFieldSchema` and `objectql.zod.ts#ObjectKanbanSchema` to `SPEC_DERIVED_PAIRS` and changed no ledger key; - this branch took nineteen keys off the `ObjectFormSchema` entry. 1. **The SPEC-DERIVED split bullet.** - This branch read "(3 entries, 15 keys) — it was 3 / 34 until objectui#6152 round 1 …". - `main` read "(4 entries, 35 keys) — it was 3 / 34 until objectui#11070 …". - Merged: both history sentences kept, newest first, in landing order (objectui#11068, then objectui#11070, then this PR). It now reads "(4 entries, 16 keys) — it was 4 / 35 until objectui#6152 round 1 … It was 3 / 34 until objectui#11070 … It was 3 / 37 until objectui#11068 …". 2. **The totals line and the sentence before it.** - This branch read "… objectui#6152 round 1 then MIRRORED nineteen of that entry's keys … **12 entries / 62 keys** — 3 / 15 spec-derived, 9 / 47 local." - `main` read "… and objectui#11070 moved `FormFieldSchema`'s entry (one key) the same way … **12 entries / 81 keys** — 4 / 35 spec-derived, 8 / 46 local." - Merged: both sentences kept, in landing order. "that entry's" became "`ObjectFormSchema`'s", because `main`'s sentence now stands between it and its antecedent. The line reads **12 entries / 62 keys — 4 / 16 spec-derived, 8 / 46 local**. The figure sites that merged without a conflict were each checked against the same measurement: - the file-header `UnmirroredDeclared` bullet (12 entries / 62 keys, from this branch); - both ledger docstrings (62 keys, from this branch); - the LOCAL split bullet (8 entries, 46 keys, with `main`'s objectui#11070 history sentence). The two other auto-merged files move separate hunks: - `objectql.zod.ts`: `main`'s hunks there (among them `dataSource` on `ObjectFormSchema` and its siblings) do not overlap this branch's; - `block-config-schema-parity-8216.test.ts`: `main` empties the `EXEMPT` table, and this branch's ledger-row hunk is untouched. ### The figures, measured on the merged ledger A scratch reader (⛔ not committed) parsed `interface UnmirroredDeclared` and `SPEC_DERIVED_PAIRS` at four trees. The file's own pin, objectui#7279's "the split figures and the totals the header writes down equal the ledger", re-derives the merged row on every run and is green at `4ed5adf6f`. | tree | ledger | spec-derived | local | | --- | --- | --- | --- | | merge base `c2a8d23c6` | 12 / 81 | 3 / 34 | 9 / 47 | | this branch before the merge, `a5884b403` | 12 / 62 | 3 / 15 | 9 / 47 | | `main`, `b0a05dda1` | 12 / 81 | 4 / 35 | 8 / 46 | | **merged, `4ed5adf6f`** | **12 / 62** | **4 / 16** | **8 / 46** |⚠️ This corrects the prediction in **Acceptance notes** above: "12 / 65 with the split 4 / 19 spec-derived, 8 / 46 local". That prediction was written before the base-merge round brought in objectui#11068, which closed three `ObjectGridSchema` keys (84 → 81 on `main`). So the measured total and the measured spec-derived half are each three keys lower. The paragraph above is left as written; this section supersedes its figures. ### The pending objectui#10993 changeset: restored, then appended Per the ruling, the line this PR rewrote is back to `main`'s bytes, and the correction is an appended, dated note: - the merge commit `f3e9d830f` takes `main`'s blob for `.changeset/10993-object-form-i18nlabel.md` (its diff against `b0a05dda1` on that path is empty); - `4ed5adf6f` appends one blank line and one paragraph, "**Correction, 2026-09-30 (objectui#6152).** …". It says `nextText` and `prevText` are now mirrored (PR objectstack-ai#11125), by the same reference to the spec's `I18nLabelSchema`; - `git diff b0a05dd --numstat` on the path reads 2 added, 0 deleted. The frontmatter is byte-identical, and `main`'s blob is an exact byte prefix of the new one (`cmp`). This PR's own changeset, `.changeset/6152-object-form-unmirrored-members.md`, states no ledger figure, so nothing in it moved with the merge. It is not edited. ### The pending-changeset re-read (the `changeset-claim-re-read` request on this PR, `5894373256`) That comment asked this PR to re-read the 31 pending changesets that name a file it touches, against its diff. This round did, on the net diff against `main`. Each body was searched for the `ObjectFormSchema` / `object-form` members this PR moves (the nineteen mirrored keys, `open`, `submitHandler`, `sections`) and for `UnmirroredDeclared` figures, and every hit was read in its paragraph. - **One claim goes false when this PR lands:** `.changeset/7200-object-form-section-style-keys-undeclared.md`. It gives, as its reason for not using a never-typed tombstone, that "`ObjectFormSchema` in `zod/objectql.zod.ts` does not declare `sections`", "so there is no parse door to refuse at". This PR mirrors `sections` with a closed entry. - Measured with a scratch probe (⛔ not committed) at `4ed5adf6f`: the strict face refuses `{ sections: [{ label, className, fields }] }` as `unrecognized_keys` at `sections.0` naming `className`. The tolerant face accepts it and drops the key. The control without `className` parses on both. - On `main`, `ObjectFormSchema`'s mirror has no `sections` member; its `objectName` member is found by the same read, as the control. - **The remedy follows the same ruling as step 2:** `8f9bb76f6` appends one dated note ("**Correction, 2026-09-30 (objectui#6152).** …"), 2 added / 0 deleted against `b0a05dda1`. The frontmatter and every existing line are byte-identical: `HEAD`'s blob before the append is an exact byte prefix of the new one. Card objectui#7200 is closed (completed 2026-09-02), so no open claim holds that file. -⚠️ This path is outside this card's claimed file surface. It is an in-place repair: same defect class as step 2; a mechanical shape the maintainer's append-only ruling pins; no other claim on the file; the same changeset gate family. The claim's file surface needs this path added. - **The other 30 hold.** 22 carry none of those tokens. The 8 that do describe one of three things: - other pairs: `ObjectGanttSchema`, `ObjectViewSchema`, `ObjectGridSchema.title`, the tree-view and chatbot slots; - `ObjectFormSchema.recordId` or `onStepChange`, members this PR does not move; - a ledger figure as a dated "X to Y" move. objectui#6150's "`RuntimeOnlyDeclared` is now a SUBSET of `UnmirroredDeclared`" still holds at pair level: its three pairs (`DataTableSchema`, `FormSchema`, `DetailViewSchema`) are all `UnmirroredDeclared` entries on the merged ledger. ### Ablation A3: a wrong total in the docblock turns the figure pin red Predictions were written to a scratch file before the run. The mutation went through objectstack's `scripts/ablation-replace.mjs` in wrap mode, on committed code (HEAD `4ed5adf6f`). It put the predicted-but-wrong spec-derived figure back into the totals line: "4 / 16 spec-derived" → "4 / 19 spec-derived". | leg | predicted | observed | | --- | --- | --- | | mutation on disk | anchor 1 → 0, replacement 0 → 1, the blob changes | as predicted; blob `ed320c3c755d` → `1e6b780b577f` | | `tsc -p packages/types/tsconfig.test.json` | exit 0: the type-level assertions read key sets, not prose | exit 0, no diagnostics | | `vitest` on `zod-mirror-parity.test.ts` | exit 1, exactly one red: objectui#7279's "the split figures and the totals the header writes down equal the ledger" | exit 1, `1 failed \| 36 passed (37)`, that test. Its diff: `totalsLine.specDerived.keys` is 16 in the ledger and 19 in the header | | restore, by state | blob equals the HEAD blob; `git diff HEAD` empty | blob `ed320c3c755d` equals HEAD's; `git diff HEAD` 0 bytes; `git status --porcelain` 0 lines | No `dist` leg exists: the parity file reads its own source off disk and compiles against `src`. ### Gates on the merged tree (exit codes captured before any pipe) The head is `8f9bb76f6`. Every row was read there except the readers' suites, which were read at `4ed5adf6f`. The one commit between the two only appends a paragraph to `.changeset/7200-object-form-section-style-keys-undeclared.md`, and no test names that file (`git grep` outside `.changeset/`: 0 hits; `check:pending-changeset-literals` green). | gate | exit | verdict line | | --- | --: | --- | | `pnpm --filter @object-ui/types type-check` (`tsc --noEmit && tsc -p tsconfig.examples.json && tsc -p tsconfig.test.json`) | 0 | no diagnostics | | `pnpm exec vitest run packages/types/` | 0 | `Test Files 285 passed (285)` · `Tests 6602 passed (6602)` | | app-shell `block-config-schema-parity-8216.test.ts` | 0 | `Tests 15 passed (15)` | | readers' suites (at `4ed5adf6f`): `packages/plugin-form/`, the three `ObjectView` form-slot tests, the two `RecordFormPage` tests | 0 | `Test Files 158 passed (158)` · `Tests 1817 passed \| 1 skipped (1818)` | | `check-changeset-presence` · `-no-major` · `-fixed` | 0 ×3 | declares `.changeset/6152-object-form-unmirrored-members.md` · no major · fixed group OK | | `check-changeset-overwrite` | 0 | report-only. It names the two pending changesets this round appends to, `10993-object-form-i18nlabel.md` and `7200-object-form-section-style-keys-undeclared.md`; each declares the same packages at the same levels at base and now | | `check:changeset-claims` · `check:pending-changeset-literals` | 0 · 0 | report-only · `No test source names a pending changeset` | | `check:control-bytes` · `check:new-line-citations` | 0 · 0 | `OK` · `0 new citation(s)` | | `check-governed-queue-guard --test` (the 8 paths of the diff against `main`) | 0 | `NOT GOVERNED` | **NOT MEASURED locally:** - the package `lint` runs and the repo-wide `pnpm lint`. This round edits two docblock comments and appends two changeset paragraphs. CI's. - app-shell's `tsconfig.test.json` type-check. CI's, as in round 1. The director's other answers in `5904218639` ③ go to later rounds on their own PRs, not this one: the `RuntimeOnlyDeclared` named allow-list, `resizableColumns` → `resizable`, and `currentPage` canonical. --- _Generated by [Claude Code](https://claude.ai/code/session_012UwY3ahMixEFkfTUxMVkYm)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
…alled spec (objectui#10940) (objectstack-ai#11288) Fixes objectstack-ai#10940 Clause-②: yes Why yes: the published `@object-ui/types` `JoinedReportBlock` changes shape in both directions. The local-only `objectName` / `groupingsDown` / `groupingsAcross` / `filter` / `chart` and the index signature leave (a narrowing). The spec-only `dataset` / `rows` / `values` / `runtimeFilter` / `order` arrive (a widening). ## What changed - **`packages/types/src/spec-report.ts`.** The hand-written `interface JoinedReportBlock` is gone. `JoinedReportBlock` is now the spec's own `JoinedReportBlock` from `@objectstack/spec/ui`, which is the INPUT shape of `JoinedReportBlockSchema`. It is imported under an alias and re-exported as a type alias, the same idiom the neighbouring `SpecReport` / `SpecReportInput` use. `JoinedSpecReport.blocks` is now `NonNullable OF SpecReport['blocks']`, the spec's own `Report.blocks`, made required. - **`report-chart-query-spec-parity.test.ts`, the `JoinedReportBlock` section.** - The `OnTypedSpec` licence is deleted. It was the last `erased` arm: on an erased spec it answered `true` for every probe. - The local-vs-spec divergence probes are replaced. The new pins cover the derivation (the published block IS the spec block, declared key sets included, and `JoinedSpecReport.blocks` IS the spec's parsed `Report.blocks`) and the member-by-member change, asserted on the published type. - Every member lookup is guarded with `[BlockMember OF …] extends [never] ? false : …`. - The file docblock and the test-time tripwire's comment record the burn-down. - **`scripts/check-spec-symbol-derivation.mjs`.** The `@object-ui/types:JoinedReportBlock` ALLOW row is deleted. The case-2b comment that pointed at that row now records the burn-down. - **`spec-derived-unions.test.ts`.** The batch-8 ledger note that said "STILL erased" now records the burn-down. - **`.changeset/10940-joined-report-block-from-spec.md`.** `@object-ui/types` `minor`, with the member-by-member change. - **`.changeset/3162-types-ledger-batch8-verdicts.md`** (pending). A dated, append-only note: in this same release, its "still erased" verdict for this symbol no longer holds. The frontmatter is byte-identical. ## Member by member (installed spec 17.5.0) | member | before (hand-written) | after (the spec's type) | direction | |---|---|---|---| | `name` | required `string` | required `string` | unchanged | | `type` | optional `'tabular' \| 'summary' \| 'matrix'` | the same; the schema defaults it to `tabular` | unchanged | | `label`, `description` | `string`, or `{ default, translations? }` | `string`, or an inline locale map (every value a string) | narrowed | | `columns` | REQUIRED array of column objects `{ field, label?, aggregate?, … }` | optional `string[]`: dimension names across a matrix | changed concept | | `objectName`, `groupingsDown`, `groupingsAcross`, `filter`, `chart` | declared | absent; the spec's closed schema refuses all five (`chart` by name, objectstack#20161) | removed | | index signature `[k: string]: unknown` | present | absent | removed | | `dataset`, `rows`, `values`, `runtimeFilter`, `order` | absent | declared | added | ## Two tier choices, and why - **`JoinedReportBlock` is the INPUT tier.** The spec exports this name with that meaning. A symbol with the spec's name but a different tier is the planted premise that `check:spec-symbols` exists to stop, and the rc.6 note in `spec-report.ts` records what a silent tier swap costs. The hand-written interface was input-shaped too (`type` optional), so `type` stays optional. - **`JoinedSpecReport.blocks` is on the PARSED tier.** `JoinedSpecReport` is built on `SpecReport`, which is `ReportParsed`, and the spec's `Report.blocks` already carries the typed block on that tier. So `blocks` derives from `Report.blocks` by reference. Writing `blocks: JoinedReportBlock[]` would have intersected the input block with the parsed one that `SpecReport` already carries. Each parsed element can be assigned to `JoinedReportBlock`, but not the reverse; a pin asserts both directions. - **Context.** On `main` today, with 17.5.0 installed, `JoinedSpecReport['blocks'][number]` is the old interface intersected with the spec's parsed block. For example, its `columns` is `{ field, … }[] & string[]`. This change leaves one shape. ## Premises, re-measured on this worktree (base `e420df31`) - **H1 holds.** The instrument is the tripwire's own. `SpecTyping OF SpecJoinedReportBlock` `satisfies 'typed'` compiles on the unmodified tree: the types `type-check` exits 0. The TypeScript-API reading that `installedSpecTyping` takes gives: installed `@objectstack/spec` 17.5.0, `JoinedReportBlock: typed`, and the lit control `ReportSort: typed`. - **H2 holds, with one amendment.** At `e420df31` the interface, the ALLOW row and the "Burnable now, on its own card" docblock were all present. But the bump (PR objectstack-ai#11086, objectui#11073) had already cut the state pin down to `typed` alone. What remained of the `erased` arm was the `OnTypedSpec` licence, and this PR deletes it. - **H3 holds.** A `git grep` for `JoinedReportBlock`, `JoinedSpecReport` and `isJoinedSpecReport` over `packages apps content examples scripts` finds one hit outside `packages/types`: a comment in `plugin-report/src/DatasetReportRenderer.tsx`. That renderer types its blocks with its own local `DatasetReportLike`. Positive control: the grep hits inside `packages/types`. No dependant's program names these symbols, so the downstream `tsc` sweep is a declared narrowing left to CI's `turbo run type-check`. The reverse check below compiles through the published entry. - **H4 holds.** The member table was re-derived against the installed 17.5.0 through the TypeScript API. It matches the card's table exactly. ## The two items the contract review owed (the review comment on objectui#10940) 1. **Each probe goes red by itself.** Every member lookup is guarded `[BlockMember OF …] extends [never] ? false : …`, and `label` and `description` are now separate probes. Ablation B below shows a dropped member turning its own probe red. 2. **The `erased` arm is deleted, not flipped.** `OnTypedSpec` is gone, so no probe is licensed by an erased spec. The state pin admits `typed` alone; that half landed at the bump. ## Verification, at `4806a1c16` | gate | result | |---|---| | `@object-ui/types` `type-check` (`tsc --noEmit`, examples, tests) | exit 0, 0 TS errors | | `vitest run packages/types/` | 298 files, 7465 tests passed | | the gate script's own suite (the 12 `scripts/__tests__` files that name `check-spec-symbol-derivation`) | 12 files, 343 tests passed | | `type-check:scripts` | exit 0 | | `pnpm --filter @object-ui/types build` | exit 0; dist completeness verified (138 files) | | eslint over the four touched lintable files (objectui's invocation, JSON) | 4 files, 0 errors, 0 warnings | | `check:spec-symbols` | exit 0 | | `check:installed-pin-claims`, `check:new-line-citations` (0 new), `check:control-bytes` | exit 0 | | `check:changeset-claims`, `check:pending-changeset-literals` | exit 0 | | `check:test-path-roots`, `type-check:coverage` | exit 0 | | `check-changeset-presence`, `check-changeset-no-major` | exit 0 | | `check-governed-queue-guard --test` over the six paths | NOT GOVERNED | - **The eslint narrowing is a measurement, not a skip.** - The four files are inside the root `eslint.config.js` population: all four came back linted. - The count comes from the JSON output. - The config has no type-aware setting (zero hits for `projectService`, `parserOptions` and `typeChecked`), and no rule under `eslint-rules/` reads the disk. So this diff cannot move any other file's verdict. - With `--no-inline-config`, one error appears, at an untouched and sanctioned `eslint-disable` block in `spec-derived-unions.test.ts`. objectui CI never passes that flag (zero hits in workflows and package scripts; the control is lit). **Ablations.** Each ran once, through objectstack's `scripts/ablation-replace.mjs` in WRAP mode, which verifies the mutation on disk and proves the restore against the HEAD blob. None is kept as a test. - **Control, at HEAD:** `tsc -p tsconfig.test.json` exits 0, and `check:spec-symbols` exits 0. - **A: the hand-written interface restored.** - tsc exits 2, with 8 probes red: `_publishedBlockIsTheSpecBlock`, `_joinedReportBlocksAreParsedTier`, `_legacyBlockKeysAreGone`, `_publishedBlockHasNoIndexSignature`, `_datasetBoundBlockKeysArrived`, `_blockColumnsAreDimensionNames`, `_blockLabelRefusesTheLegacyObjectArm` and `_blockDescriptionRefusesTheLegacyObjectArm`. - `check:spec-symbols` exits 1: "a spec-named symbol is hand-written, not derived: interface `JoinedReportBlock`". - **B: `label` dropped from the published block (an `Omit` of the spec type).** - tsc exits 2, with exactly two probes red. One is `_blockLabelRefusesTheLegacyObjectArm`, on its own line. The other is `_publishedBlockIsTheSpecBlock`, and only through the declared-key-set comparison that `4806a1c16` added: mutual assignability alone stays green when an optional key is dropped. - The description probe stays green. - `check:spec-symbols` exits 0, because an `Omit` of the spec type counts as derived. - **C: the retired ALLOW row re-added over the derived type.** - `check:spec-symbols` exits 1: "`@object-ui/types:JoinedReportBlock` is in ALLOW but no longer collides with a spec export name". So the row's removal is enforced by the gate, not by prose. -⚠️ The first attempt at C was a no-op, and its reading was discarded. Its replacement contained its own anchor, so the tool refused (anchor count 1 before and 1 after) and the child command never ran. It was re-run with an anchor the replacement does not contain. - **Every leg restored.** The blob equals the HEAD blob, `git diff HEAD` is empty, and porcelain is 0. **Reverse check through the built declarations.** This ran after the types build. A probe in `packages/plugin-report/src` imports `JoinedReportBlock` from `@object-ui/types`; that package's tsconfig resolves `@object-ui/types` through `dist`. - A spec-shaped block compiles (exit 0). `--listFiles` shows it read `packages/types/dist/spec-report.d.ts`, not the source. - The same literal with a pasted `objectName` fails with TS2353. - The first run was void: tsc refused file arguments beside a tsconfig (TS5112) before reading anything. It was re-run with `--ignoreConfig`. The probe file is removed. **NOT MEASURED** - `check:spec-floors` and `check:readme-exports`. Both need every package's `dist`, and this fresh worktree built only `@object-ui/types`. The floor gate reported 20 packages `no-artifact`, and readme-exports reported "the population COLLAPSED -- this run proves nothing". Left to CI. - The downstream type-check of `@object-ui/types`' dependants. This is a declared narrowing; see H3. ## Acceptance notes - **File surface.** The claim's surface holds. Two edits sit inside files it names, and both keep text true: the case-2b comment beside the retired ALLOW row, and the tripwire's docblock. One edit sits outside the named paths: the dated note on the pending `3162` changeset. The dispatch's pending-changeset clause sanctions it. - **Docs.** No README or guide names this type (checked with `git grep`). `content/docs/plugins/plugin-report.mdx` already documents joined blocks in the spec's dataset-bound shape. - **Runtime.** Nothing changes at runtime. `isJoinedSpecReport` and every renderer are untouched. Dispatched by the `domain:spec @ objectui` seat. Session: `https://claude.ai/code/session_01VhxTqosz7wn54ahqyxgERT`. --- _Generated by [Claude Code](https://claude.ai/code/session_01VhxTqosz7wn54ahqyxgERT)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
…he metadata designers validate with; the eager-closure ceiling comes down by what that recovered (objectui#11101) (objectstack-ai#11342) Fixes objectstack-ai#11101 Clause-②: no. Load timing only; no contract, export or accept set moves, and validation is unchanged once loaded. The console-side payback of the objectui#11088 decision 1 = A loan (the `@objectstack/*` 17.5.0 ceiling raise of PR objectstack-ai#11086). Implemented by `os-dev` for the `domain:devx` seat 2 claim `5924908810`, session `https://claude.ai/code/session_01TdiauJaVCHuj45EzZGUxHh`. ## Measured first Every `@objectstack/spec` entry the console bundles sits in one chunk, `vendor-objectstack`, and that chunk is a static import of the entry. Read on a console build of `main` at `5262f7d` with a throwaway module-graph dump (not committed). "gzip alone" is each module's rendered code gzipped by itself: an attribution, not additive, and larger than the module's share of the chunk. | spec entry | rendered | gzip alone | static importers in eager chunks | verdict | |---|---:|---:|---|---| | root `@objectstack/spec` | 2,370,467 | 692,269 | `@object-ui/core`, `@object-ui/auth`, `@objectstack/formula`, metadata-admin `view-item-normalize.ts` | runtime; objectstack#20646's | | `/system` | 1,039,623 | 281,625 | `views/FlowRunner.tsx` (runtime), metadata-admin `metadata-form-i18n.ts`, `previews/doc-draft.ts` | runtime | | `/kernel` | 886,545 | 247,041 | `@object-ui/data-objectstack` (runtime), preview and designer modules | runtime | | `/data` | 614,780 | 172,275 | core, fields, grid, detail, form, client, formula | runtime | | `/ui` | 584,593 | 156,024 | shell, layout, components, grid, list, view | runtime | | `/shared` | 368,108 | 81,754 | `preview/DraftChangesPanel.tsx` (through `ConsoleLayout`'s draft preview bar), `inspectors/ObjectFieldInspector.tsx`, `previews/simulator/flow-sim-validate.ts` | admin/designer code, eager through the layout and the registration leaf | | `/api` | 322,818 | 90,817 | core, data-objectstack, client, formula | runtime | | `/automation` | 127,672 | 37,283 | `utils/approverIdentity.ts` (runtime, `DeclaredActionsBar`) plus metadata-admin inspectors | runtime | | `/ai` | 108,241 | 32,100 | **none**: only `clientValidation.ts`, through `await import()` | lazy in source, eager by the chunk group | | `/security` | 58,779 | 18,500 | formula, plugin-grid | runtime | | `/identity` | 26,836 | 9,348 | `inspectors/FlowReferenceField.tsx`, eager through `register-builtins.ts` | admin-only, eager through the registration leaf | | `/integration` | 23,392 | 6,981 | **none**: only `clientValidation.ts`, through `await import()` | lazy in source, eager by the chunk group | | `/contracts` | 1,467 | 639 | none (lazy console pages, the linter) | lazy in source, eager by the chunk group | | `@objectstack/sdui-parser` (not spec) | 19,801 | 5,694 | none (the linter only) | lazy in source, eager by the chunk group | ⇒ The hypothesis held only in part. `clientValidation.ts` already loads every spec entry through `await import()`. What kept `/ai` and `/integration` on the first screen was the `vendor-objectstack` chunk group: a group claims by module id, not by reachability. That is the objectui#5266 mechanism, one package over; the lookaheads in `VENDOR_OBJECTSTACK_TEST` only ever excluded `@objectstack/lint`. Every other entry the validator loads is also imported statically by something on the first screen, so no lazy import in the validator can move it. ## The change 1. `apps/console/vite.config.ts`: the `vendor-objectstack` group gets `tags: ['$initial']`. That is rolldown's built-in tag for "statically reachable from an entry", so the group now claims only the entry's static closure. A vendor module that sits behind an `import()` follows its importer into a lazy chunk. Nothing the first screen executes moves, because `$initial` IS that closure. The lint lookaheads stay. A group-table pin in the budget's unit test records the tag. 2. `scripts/check-eager-closure-budget.mjs`: the ceilings come down by exactly what was recovered (below). The owed `squashMerge` handle for `048e7f6` is back-filled, and the near-ceiling `framework` fixture pair holds the aggregate at `BASELINE` by construction. 3. A patch changeset for `@object-ui/console`, whose published `dist` changes chunk layout. `check-changeset-presence` owes none, because no published source moved. ## Eager closure, before and after Two builds of one tree, one container, one instrument (`CI=true pnpm exec vite build` in `apps/console`, reading `apps/console/dist/eager-closure.json`), in one `os-verify-lock.sh` hold. The trees differ only in the vite.config line; the before leg put `5262f7d`'s blob on disk and the after leg restored `HEAD`'s, each proven by blob hash. | | `5262f7d` (`main`) | `4acbea07` (this PR) | delta | |---|---:|---:|---:| | aggregate eager closure, gzip | 3,671,724 | 3,638,104 | **−33,620** | | `vendor-objectstack`, gzip | 1,704,699 | 1,670,964 | −33,735 | | the other 329 eager chunks, gzip | 1,967,025 | 1,967,140 | +115 (import bookkeeping, 31 chunks, none over 20) | | aggregate eager closure, raw | 12,366,134 | 12,257,433 | −108,701 | | eager / total chunks | 330 / 2447 | 330 / 2450 | three new lazy chunks: `ai`, `integration`, `contracts` | Noise: the instrument is deterministic per tree. The untagged config read 3,671,724 on three builds (the graph-dump build, the canonical before leg, the ablation's mutated leg), and the tagged config read 3,638,104 on three (the graph-dump build, the canonical after leg, the ablation's restored leg). The recovered 33,620 is 0.37x the 89 KiB regression the gate exists to catch. ## The ceiling: lowered by the recovered amount | constant | before | after | |---|---:|---:| | `MAX_EAGER_CLOSURE_GZIP_BYTES` | 3,694,000 | **3,660,380** (−33,620) | | `BASELINE` | 3,648,737 on `048e7f6` | 3,638,104 on `4acbea07` | | `PER_CHUNK_GZIP_CEILINGS['vendor-objectstack']` | 1,723,000 | **1,689,265** (−33,735) | | `PER_CHUNK_BASELINE['vendor-objectstack']` | 1,703,690 on `048e7f6` | 1,670,964 on `4acbea07` | The ceilings are not re-derived as "reading + half a regression". That would absorb `main`'s own drift since `048e7f6` (+22,987 on `5262f7d`) into the ceiling, which is a raise under another name. The live headroom `main` had before the payback is kept to the byte: 22,276 (0.24x) on the aggregate and 18,301 (0.20x) on `vendor-objectstack`. No build that passed before and measures under 3,660,380 fails after. `REGRESSION_THIS_GATE_MUST_CATCH_BYTES` and the other three per-chunk rows did not move. **`squashMerge`:** `048e7f6` landed on `main` as `81f8498` (objectstack-ai#11086, single parent, an ancestor of `origin/main`). That handle is back-filled in the header prose that still cites the `048e7f6` reading. This change re-pins `BASELINE` onto its own reading, so the field is `null` again by its own rule ("the change that re-pins the field above cannot write its own here"). The `4acbea07` → squash back-fill is owed once this merges. ## Ablation: re-eager, and the lowered ceiling goes red `node ../objectstack/scripts/ablation-replace.mjs` dropped `tags: ['$initial']` (anchor 1 → 0, blob `40f6d42c` → `32c403ab`), rebuilt, and ran the gate: - **mutated:** 3,671,724 bytes, `node scripts/check-eager-closure-budget.mjs` **exit 1**. The aggregate line read "3585.7 KB … 11.1 KB over the 3574.6 KB budget", and `vendor-objectstack` read "1664.7 KB / 1649.7 KB ceiling (OVER by 15.1 KB)". - **restored:** blob back to `HEAD` (`40f6d42c`), `git diff HEAD` empty. The rebuild read 3,638,104 and the gate **exit 0**. ## Validation unchanged once a designer opens - **Probe on the BUILT console** (`vite preview`, proxied to a `--fresh` objectstack 17.5.0 showcase backend started by `e2e/live/ci/start-backend.sh`, Playwright + `/opt/pw-browsers/chromium`). The probe ran two builds of the same tree, with and without the tag. - First screen (`/apps/showcase_app`, 331 JS requests): no `ai-*` or `integration-*` chunk is fetched. - `/apps/showcase_app/metadata/agent/new`: opening the designer fetches `ai-rfegJgG7.js`. Typing `Bad Name!` into Name raises "This metadata does not match the spec — 4 validation error(s). Name: Invalid string: must match pattern …". - `/apps/showcase_app/metadata/connector/new`: this designer fetches `integration-DM--okeM.js`, and the 3-error banner shows. - The validator's text is **byte-identical** between the untagged and the tagged build, for both designers. - The only non-2xx was `GET /api/v1/usage/storage` 404 from the backend, present on both builds. No `pageerror`. - **Existing tests, all of `packages/app-shell/src/views/metadata-admin/**` (373 files):** 36 clientValidation / ResourceEditPage / createConformance files plus 1 console file, 398 passed. The other 337 files ran in three chunks: 1066 passed; 1455 passed and 1 skipped; 1462 passed. ## Verification (head `7d24740c`) - Console build + budget, before and after, under the lock: the table above. Checker exit 0 on both. The after build gets exit 0 at the new constants: "3552.8 KB … budget: 3574.6 KB, headroom: 21.8 KB". - `scripts/__tests__`: the 18 files that read `apps/console/vite.config.ts` or the budget script, 1020 passed. That includes `check-eager-closure-budget.test.ts` at 179, plus `vite-objectstack-spec-dist`, `vite-objectstack-client-dist`, `render-budget-comment` and `check-node-esm-load`. - `apps/console` vitest project: 139 files, 1593 passed. - `pnpm --filter @object-ui/console run type-check` exit 0. Reverse check: `tags: ['$bogus']` gives `tsc -b tsconfig.node.json` exit 1 with TS2769 at the group, then restored. - `pnpm --filter @object-ui/app-shell run type-check` exit 0. - Both type-checks ran after `turbo run build --filter='@object-ui/console^...'`. - `pnpm check:control-bytes` OK. - `node scripts/check-changeset-presence.mjs` exit 0. - `pnpm changeset:check` exit 0. - `pnpm check:changeset-claims` exit 0, report-only. It shows two pending bodies naming files touched here, `6681-declared-lazy-marketplace-routes.md` and `7173-ai-pending-actions-inbox-i18n.md`. I read both, and this diff falsifies neither. - `pnpm check:new-line-citations` 0 new. - Narrowed lint: `eslint --no-inline-config --format json` on the 3 changed code files gives 3 files, 0 errors, 0 warnings. - Population: all three are inside `eslint.config.js`'s population; none was reported ignored. - Invariance: the config enables no type-aware linting, so this diff cannot move a verdict on an untouched file. - **NOT MEASURED:** - The full `@object-ui/app-shell` suite outside `metadata-admin/**`, because its ~600 remaining files exceed the foreground window on a box shared by three devs. `packages/app-shell/src` is untouched by this diff, and CI runs the suite. - The repo-wide `pnpm lint`, which CI owns. ## Acceptance notes (observations, not filed) - `/shared` (81,754 gzip alone) and `/identity` (9,348) are reached on the first screen only by designer code. Their routes in are `register-builtins.ts` → `inspectors/index.ts` / `previews/index.ts`, which the package entry bare-imports, and `ConsoleLayout` → `DraftPreviewBar` → `DraftChangesPanel`. Recovering them means lazy-registering the inspector and preview tables, which is an architecture change, not this card. Carrier: none. - `DraftChangesPanel.tsx`'s comment says `@objectstack/core` (inside `@objectstack/client`) has a runtime import of `@objectstack/spec/shared` that keeps that subpath eager. In this 17.5.0 bundle, `/shared`'s importers are the three objectui modules above plus the linter. The comment's premise no longer holds. Carrier: none. - `e2e/live/studio-editor.spec.ts` waits on `getByTestId('metadata-edit-page')`. `git grep` finds that string in no other tracked file, and the agent and connector edit pages rendered without it (this probe's first wait on it timed out). Whether the live suite still runs that spec, and how it passes, is NOT MEASURED. Carrier: none. --- _Generated by [Claude Code](https://claude.ai/code/session_01TdiauJaVCHuj45EzZGUxHh)_ --------- Co-authored-by: Claude <noreply@anthropic.com> Co-authored-by: 黄怡 <huangyi@hotoa.com>
Fixes #11073
Clause-②: no — consumes the already-published
@objectstack17.5.0 contract by version; objectui adds no key, export or flag of its own, and every accept-set movement arriving through spec-by-reference pairs is enumerated in the PR for reviewWhat this PR is, at this head
pnpm-lock.yamlnow resolves@objectstack/*17.5.0 (was 17.4.0), plus thezod4.6.5 that 17.5.0 requires. No manifest range moves in this PR and no objectui packageversionmoves. The PM asked for the draft to open at the first compilable step, so this body records the readings taken on the lockfile-only head. Commits that adapt objectui to the published contract follow on this branch. The final gate readings, commit by commit, go into theos-dev-reportcomment on objectui#11073. Any later change to this body is for the seat to write.It is expected to be RED on this head, for measured reasons (below). ⛔ No red here is waved through or re-run.
H1 — premise (measured)
origin/mainc80236e: the lock resolved@objectstack/{spec,types,sdui-parser,lint,formula,core,client}at 17.4.0, and npmlatestfor all seven is 17.5.0.@objectstack/specat^17.0.0in 23 places,^17.4.0in 6,^17.1.0in 2 and^17.3.0in 1;@objectstack/clientat^17.3.0in 2;formulaandlintat^17.0.0in 2 each.pnpm.overrides(rootpackage.json) andpnpm-workspace.yamlhold no@objectstackentry.H2 — how the lock was regenerated, and everything that moved
Regenerated by pnpm 10.31.0 only. ⛔ Not edited by hand.
pnpm update -r "@objectstack/*"was tried first and REJECTED. It rewrote 30 manifests (every range raised to^17.5.0, and script keys reordered), and it re-resolved unrelated packages:seroval,seroval-plugins,bson,@mongodb-js/saslprep,@microsoft/tsdoc-config,is-core-module,fast-uri,brace-expansion,use-sync-external-store, and a seconddompurify. None of those is@objectstack-caused.@objectstack/*specifier to^17.5.0for ONEpnpm install; restore every manifest byte-for-byte to HEAD; run a secondpnpm install, which reconciles the importers back to their declared ranges. The same raise-and-restore was then applied to the five directzodspecifiers (see below).@objectstack/*at 17.4.0;@objectstack/*at 17.5.0, andzod@4.6.5;specifierline changes.zod@4.6.5(same versions, new snapshot keys):ai@7.0.65,@ai-sdk/gateway,@ai-sdk/mcp,@ai-sdk/provider-utils,@ai-sdk/react, and thefumadocs-core/fumadocs-mdx/fumadocs-uisnapshots ofapps/site.zod@4.4.3stays in the lock ONLY asfumadocs-mdx's own dependency, unchanged from base.@object-ui/sitetype-checks green with it.Why
zodmoves (forced by the published 17.5.0 contract, not drift)@objectstack/spec@17.5.0and@objectstack/core@17.5.0raise theirzodrange from^4.4.3to^4.6.1. With only the@objectstack/*entries moved, the lock resolved spec'szodto 4.6.5 while objectui's own directzod ^4.4.3importers (types,app-shell,components,plugin-timeline,test-support) stayed on 4.4.3. Zod stamps its minor version into its types, so the two copies do not type-check against each other:pnpm --filter @object-ui/types buildfailed with TS2345 insrc/zod/views.zod.ts, bottoming out at "The types of '_zod.version.minor' are incompatible between these types. Type '4' is not assignable to type '6'."zod ^4.4.3to the same 4.6.5 (a range it already admits; no manifest edit), the same build exits 0.Gates read on this head (
b732e17)pnpm install --frozen-lockfilepnpm --filter @object-ui/types buildzodresolution)turbo run build --filter=!@object-ui/site --concurrency=2turbo run type-check --concurrency=2(unfiltered, 45 packages incl.@object-ui/site)vitest run packages/types/c80236e)So every one of the 56 reds is caused by the bump. The full suite is being read in four shards; its result is in the report comment.
H3 / H4 — what the bump moves, by cause
The reds fall into three causes. Each row names the upstream change that forced it.
Cause A — zod 4.4 to 4.6 internals (forced by the spec's
zod ^4.6.1).default()member now reports_zod.optin === 'defaulted', a rung zod 4.4.3 did not have (it answered'optional').imported-defaults-8317.test.ts("no member became REQUIRED", 26 rows) compares the rung by equality, and the 9034 census counts a default-under-default as already optional. The production walker is NOT affected:walk()turns an inner'defaulted'into'optional'beforeisAlreadyOptionalreads it, and the probe row "answers every probe exactly as the spec does" stays green for every imported schema. Adaptation: test-only, re-expressing "omissible" as either rung.unrecognized_keysis nowcontinue: true(non-aborting). In a union where the other arm aborts, the non-aborted arm is returned ALONE (handleUnionResults, whose code is unchanged).GridSchema.columns(8516): the out-of-vocabulary key is now reported directly, asunrecognized_keysnamingxxlatcolumns, instead of inside aninvalid_union. The test's intent (the key is named) holds, and the author's message is better. Mechanical re-pin.content-channel-public-blocks-9256.test.ts;strict-widget-slot-registered-inputs-11022.test.ts):{ type: 'metric-card', value: 42, body: [] }now yields ONEunrecognized_keysnamingvalueandbody. The by-name refusal ("metric-cardreads NEITHER content channel") is GONE, andvalue, a registered input, is misreported as unrecognized. Base yieldsinvalid_unioncarrying both arms. This is a regression in what an author (or an AI) is told, and restoring it is a design choice → left red, taken to the seat as a decision.Cause B — spec 17.5.0 makes an unknown-key refusal terminal
17.5.0's closed objects mark every
unrecognized_keysissuecontinue: false(markUnknownKeyRefusalTerminalin the spec's dist). zod skips even awhen-guarded check once the payload is explicitly aborted, so the twosuperRefinepointers (each carrying awhenthat always answers true) onObjectViewSchemano longer run:checkNamedViewCalendarAliases(objectui#8355) andcheckNamedViewKanbanStrayGroupBy(objectui#10321).listViews.KEY.calendar/.kanban).dateFieldcorrectly ("does not say which end of the range it binds … Write the one you mean").endFieldandkanban.groupByit is a generic unrecognized-key message, so objectui's specific pointer is lost on the named-view route.Cause C — the published contract moved, read by reference (the accept sets the seat's review reads)
DetailViewFieldSchema.options[].visibleWhen(specSelectOptionSchema){ dialect, ast }envelope acceptedelement:numberdataSource.filter(specElementDataSourceSchema)$and/$or) and a CEL envelope acceptedViewFilterRulearray[{ field, operator, value }]calendarblock (specCalendarConfigSchema)allDayFieldrefused by nameallDayFieldacceptedrecord:highlightsprops (specRecordHighlightsProps)aria,fields,layoutrequiredPermissions,redactFields,enforceFieldSecuritylistViews.KEY(specViewSchema)pageName,tabsliveListViewSchemaobject-level checkscheckListViewPageMountpresentDashboardWidgetSchemaobject-level checkscheckDashboardWidgetStageOrder,checkDashboardWidgetMetricMeasureArity.shapemirror does not carry them, so objectui's door is now WIDER than the spec on both rulesJoinedReportBlockSchemaunknown)z.ZodObject)H4 — consumer breaks
waitnode.defaultNodeExtras('wait')seeds{ waitEventConfig: { eventType: 'timer' } }. Spec 17.5.0'sFlowNodeSchemarefuses a timer wait with notimerDuration(its message: a timer wait with no duration "parks the run forever while reporting success"). So a node the designer just created is refused at save. Failing check:flow-canvas-seeds.spec-parse.test.tsx, row "wait: a freshly added node parses as a spec FlowNode" (base: parses). Which default to seed is a product decision → left red.What the follow-up commits on this branch do, and what they leave red
Mechanical, each forced by a named upstream change (Cause A1, A2 for
GridSchema.columns, and the Cause C re-pins that follow the spec by reference or follow a tripwire's own written instruction):optinrung re-expression (8317, 9034);GridSchema.columnsissue-shape re-pin (8516);WIDER_ARMSentries and theSpecEnvelopeAdmitsSourcelessgate;element:numberdataSource.filterrows re-pinned to the spec's verdicts, andREBUILT_CLEANemptied (the spec's schema no longer reaches az.lazy);record:highlightsPREMISE member list;allDayField) and the nested key list;checkListViewPageMount, as its own message instructs.Left RED on purpose, each with options in the report (
needs_decision):pageName/tabsstill declared on objectui's TypeScript named-view face while the protocol now retires them (7779; the objectui#8980 ruling kept them "declared inert" under a protocol that still declared them);waitseed (H4);Floors
This PR raises no floor. No code in it imports a 17.5.0-only export. The
^17.5.0floors are left to the consuming cards, each at the moment it first imports a 17.5.0-only export: objectui#11013, objectui#11021, objectui#10188, objectui#7759, objectui#7347, objectui#8649, objectui#9217, objectui#10107 and objectui#9830. Each is unblocked by this landing; none is concluded by it.Acceptance notes
zodskew. A consumer whose own lock holdszod4.4.x and resolves@objectstack/spec17.5.0 through objectui's^17.xranges gets the same two-copy split measured above. That state already exists for any consumer today; it is not created here. Whether objectui'szodfloor should rise to^4.6.1beside the consuming cards' spec floors is an open question in the report, not an edit here.checkDashboardWidgetStageOrderis still not exported on 17.5.0)". Measured here: it IS exported on 17.5.0 from@objectstack/spec/ui, and not from the root entry. The 7715 census sees it through/ui.Generated by Claude Code