Skip to content

chore(deps): resolve @objectstack/* 17.5.0, and the zod 4.6.5 it requires, in pnpm-lock.yaml (objectui#11073) - #11086

Merged
os-zhuang merged 29 commits into
mainfrom
claude/issue-11073-objectstack-17-5-bump
Sep 30, 2026
Merged

os-zhuang merged 29 commits into
mainfrom
claude/issue-11073-objectstack-17-5-bump

Conversation

@objectstack-fleet

@objectstack-fleet objectstack-fleet Bot commented Sep 29, 2026 •

Copy link
Copy Markdown
Contributor

Fixes #11073

Clause-②: no — consumes the already-published @objectstack 17.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 review

⏸ Current state: seat amendment (domain:devx seat 2, session_01TdiauJaVCHuj45EzZGUxHh, 2026-09-30T02:59Z; head c385748)

The body below was written at round 1, on the lockfile-only head b732e17. This section is current; where the two differ, this section governs.

Draft, awaiting a human merge. The diff is 5,370 changed lines, over the 5,000-line human-merge line; no governed path is touched.

  • It lands through the merge queue after an APPROVED review from os-zhuang or hotlong.
  • ⛔ No seat flips it ready or arms auto-merge before that approval.
  • Contract review at CONTRACT_REVIEW_TIER on this head: PASS (5903181734).
  • CI on this head: all green, Live E2E included. 42 success; 3 skipped by design (coverage matrix, dependabot).

No expected reds.

Rounds and commits:

  • Round 1 (cb3b89a…174c4a7): seat rulings Q2/Q3/Q4/Q6/Q8 and the woken cards' re-reads.
    • Q3 blast radius: 29 plain unions, 1106 probes, 0 accept-set moves, 187 error-shape moves.
    • Tripwire-text deletions: the percent-scale clamp (81de0ff), the quickAdd carve-out, and two block-config exemptions (8053108).
  • Round 2 (base 174c4a7):
    • 569e923: the flow designer moves to 17.5.0 (objectui#11088 decision 2 = A). A new wait node is seeded PT1H, visible and editable. The decision form offers mode, the screen form states its create default, and preview script nodes use function.
    • 2abb653: prose-only corrections to seven pending changesets this bump made false (Q12).
    • 048e7f6: changeset lines.
    • 92ac539: the eager-closure re-baseline (decision 1 = A).
  • Round 3 (283c97c, Q13):
    • @object-ui/data-objectstack and @object-ui/app-shell raise @objectstack/spec to ^17.5.0, and app-shell raises zod to ^4.6.1. The lockfile moves only those three specifiers; the resolution census is unchanged.
    • check:spec-floors --cross-check over a full build of 283c97c reads 0 findings.
    • ⚠️ Round 2's 「spec-range-floors exit 0」 is withdrawn: it read a data-objectstack dist built before 865a81d.
  • Round 4 (119f4d3, sync with main 8a5ae3d, merged as a merge commit):
    • One textual conflict: the number-field scale block in @object-ui/fields. Both intents are stacked: this branch's unclamped scale (81de0ff), then main's objectui#11026 useGrouping read and its hand-off to formatDisplayNumber.
    • The lockfile did not move; the resolution census is identical.
    • The flow start node takes objectui#11067's platform-event drop. The PT1H wait seed, the decision mode and the screen create default are intact.
  • Round 5 (cdd84c7, Q14 = A): the Q3 pin reads StrictAnyComponentSchema through the zod/index.zod barrel. objectui#8345's guard (the barrel is the only entry into the deep module's cycle) is green again. Test-only; no changeset.
  • Round 6 (objectui#11111 ruled, record 5902351047; base cdd84c7):

Eager-closure budget re-baseline (objectui#11088, decision 1 = A).

  • Measured with CI=true vite build of the console on this branch, against control a5841be35 built the same way (330 of 2446 chunks each):
    • vendor-objectstack 1,237,912 → 1,703,690 (+465,778);
    • aggregate 3,183,235 → 3,648,737 gzip bytes (+465,502). All of the growth is vendor-objectstack; the other rows net −276 bytes.
    • On the merged head 119f4d3: aggregate 3,650,586 (+1,849 from main's 21 commits), vendor-objectstack unchanged at 1,703,690. CI Bundle Analysis reads 3565.6 KB against 3607.4 KB: PASS.
    • The ruling's 413.8 KB is the overage above the retired ceiling (3563.2 KB against 3149.4 KB).
  • What it buys: the console's client-side validation answers as a 17.5.0 server does, with the same refusals (retired keys by name, the terminal unknown-key refusal, the new object-level checks) and the same schemas. It also unblocks the twenty cards waiting on this bump. It is not a new console capability.
  • Paid back at the source by spec: the root entry stops carrying the migration and conversion registries (about 1 MB of prose), so a console first screen no longer downloads os migrate text (the upstream payback of objectui#11088 D1) objectstack#20646 (the spec root entry sheds the migration and conversion registries) and objectui#11101 (metadata-admin-only spec validation leaves the first screen). The ceiling and BASELINE come down with each.
  • New constants: MAX_EAGER_CLOSURE_GZIP_BYTES 3,694,000 over BASELINE 3,648,737 (headroom 0.50x the regression constant); vendor-objectstack 1,723,000 over 1,703,690 (0.21x). REGRESSION_THIS_GATE_MUST_CATCH_BYTES is not moved.

Changeset levels: @object-ui/types, core and app-shell minor; fields, components, plugin-list, console and data-objectstack patch. objectui marks breaking changes minor with the semantics in the body (AGENTS.md §9).

Superseded statements in the body below (this section governs):

  • H2's "zod@4.4.3 stays in the lock ONLY as fumadocs-mdx's own dependency": it is gone from the lock since 32b7f52.
  • Round 1's "No manifest range moves in this PR": four manifests move (@object-ui/types, core, data-objectstack and app-shell; Q8 and Q13).

Owed after merge: the BASELINE.squashMerge back-fill in scripts/check-eager-closure-budget.mjs (the field's own instruction). The ceiling and BASELINE come down with each payback.

Cards named here are cited as rulings, payback cards or holders, never beside a closing keyword; Fixes #11073 is the only one.

What this PR is, at this head

pnpm-lock.yaml now resolves @objectstack/* 17.5.0 (was 17.4.0), plus the zod 4.6.5 that 17.5.0 requires. No manifest range moves in this PR and no objectui package version moves. 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 the os-dev-report comment 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)

  • Base origin/main c80236e: the lock resolved @objectstack/{spec,types,sdui-parser,lint,formula,core,client} at 17.4.0, and npm latest for all seven is 17.5.0.
  • Manifests declare caret ranges only: @objectstack/spec at ^17.0.0 in 23 places, ^17.4.0 in 6, ^17.1.0 in 2 and ^17.3.0 in 1; @objectstack/client at ^17.3.0 in 2; formula and lint at ^17.0.0 in 2 each.
  • objectui's pnpm.overrides (root package.json) and pnpm-workspace.yaml hold no @objectstack entry.
  • ⇒ every range already admits 17.5.0, so a within-range regeneration moves the resolution without touching a range.

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 second dompurify. None of those is @objectstack-caused.
  • The route used instead: raise each @objectstack/* specifier to ^17.5.0 for ONE pnpm install; restore every manifest byte-for-byte to HEAD; run a second pnpm install, which reconciles the importers back to their declared ranges. The same raise-and-restore was then applied to the five direct zod specifiers (see below).
  • Resolution census against the base lock (package@version keys, both directions):
    • removed: the seven @objectstack/* at 17.4.0;
    • added: the seven @objectstack/* at 17.5.0, and zod@4.6.5;
    • nothing else. No importer specifier line changes.
  • Peer-suffix re-keys that follow from 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 the fumadocs-core / fumadocs-mdx / fumadocs-ui snapshots of apps/site.
  • zod@4.4.3 stays in the lock ONLY as fumadocs-mdx's own dependency, unchanged from base. @object-ui/site type-checks green with it.

Why zod moves (forced by the published 17.5.0 contract, not drift)

@objectstack/spec@17.5.0 and @objectstack/core@17.5.0 raise their zod range 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, so the two copies do not type-check against each other:

  • pnpm --filter @object-ui/types build failed with TS2345 in src/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'."
  • After the lock resolves objectui's zod ^4.4.3 to the same 4.6.5 (a range it already admits; no manifest edit), the same build exits 0.

Gates read on this head (b732e17)

gate result
pnpm install --frozen-lockfile exit 0, "Lockfile is up to date"
pnpm --filter @object-ui/types build exit 0 (was TS2345 before the zod resolution)
turbo run build --filter=!@object-ui/site --concurrency=2 43/43 tasks successful
turbo run type-check --concurrency=2 (unfiltered, 45 packages incl. @object-ui/site) 81/81 tasks successful
vitest run packages/types/ RED: 56 failed in 15 files, 6376 passed
control: the same on the base tree (17.4.0 installed, detached worktree at c80236e) 6432/6432 passed, 279/279 files

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)

  1. A .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' before isAlreadyOptional reads 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.
  2. zod's own strict-object unrecognized_keys is now continue: 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, as unrecognized_keys naming xxl at columns, instead of inside an invalid_union. The test's intent (the key is named) holds, and the author's message is better. Mechanical re-pin.
    • ⚠️ The dashboard widget slot (objectui#9256 in content-channel-public-blocks-9256.test.ts; strict-widget-slot-registered-inputs-11022.test.ts): { type: 'metric-card', value: 42, body: [] } now yields ONE unrecognized_keys naming value and body. The by-name refusal ("metric-card reads NEITHER content channel") is GONE, and value, a registered input, is misreported as unrecognized. Base yields invalid_union carrying 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_keys issue continue: false (markUnknownKeyRefusalTerminal in the spec's dist). zod skips even a when-guarded check once the payload is explicitly aborted, so the two superRefine pointers (each carrying a when that always answers true) on ObjectViewSchema no longer run: checkNamedViewCalendarAliases (objectui#8355) and checkNamedViewKanbanStrayGroupBy (objectui#10321).

  • The document is still REFUSED (by the spec, at listViews.KEY.calendar / .kanban).
  • The spec's own message now guides dateField correctly ("does not say which end of the range it binds … Write the one you mean").
  • For endField and kanban.groupBy it is a generic unrecognized-key message, so objectui's specific pointer is lost on the named-view route.
  • Keep, rebuild or retire the pointer is a decision → left red.

Cause C — the published contract moved, read by reference (the accept sets the seat's review reads)

objectui surface (mirror by reference) 17.4.0 17.5.0 direction
DetailViewFieldSchema.options[].visibleWhen (spec SelectOptionSchema) a whitespace-only string and a source-less { dialect, ast } envelope accepted both refused narrower
element:number dataSource.filter (spec ElementDataSourceSchema) the MongoDB-style record form (incl. $and/$or) and a CEL envelope accepted only the ViewFilterRule array [{ field, operator, value }] shape change, narrower for the record form
nested calendar block (spec CalendarConfigSchema) allDayField refused by name allDayField accepted wider
record:highlights props (spec RecordHighlightsProps) aria, fields, layout adds requiredPermissions, redactFields, enforceFieldSecurity wider
named view listViews.KEY (spec ViewSchema) pageName, tabs live both refused by name as retirement tombstones narrower
spec ListViewSchema object-level checks checkListViewPageMount present gone no objectui accept-set change (objectui never attached it)
spec DashboardWidgetSchema object-level checks none checkDashboardWidgetStageOrder, checkDashboardWidgetMetricMeasureArity objectui's .shape mirror does not carry them, so objectui's door is now WIDER than the spec on both rules
spec JoinedReportBlockSchema erased (unknown) typed (z.ZodObject) the objectui#10916 tripwire fires; the divergence pins compile green on 17.5.0

H4 — consumer breaks

  • Workspace build and type-check: no break (43/43, 81/81).
  • Runtime (a test, not a type-check): ⚠️ the flow designer's fresh wait node. defaultNodeExtras('wait') seeds { waitEventConfig: { eventType: 'timer' } }. Spec 17.5.0's FlowNodeSchema refuses a timer wait with no timerDuration (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):

  • the optin rung re-expression (8317, 9034);
  • the GridSchema.columns issue-shape re-pin (8516);
  • the objectui#10296 tripwires, done as their docblocks instruct at the pin bump: flip both rows to refusals, and delete the gated WIDER rows, their WIDER_ARMS entries and the SpecEnvelopeAdmitsSourceless gate;
  • the element:number dataSource.filter rows re-pinned to the spec's verdicts, and REBUILT_CLEAN emptied (the spec's schema no longer reaches a z.lazy);
  • the record:highlights PREMISE member list;
  • the calendar spec-type fence (the doc fence tracks the SPEC type, so it gains allDayField) and the nested key list;
  • the 7715 ListView row losing checkListViewPageMount, as its own message instructs.

Left RED on purpose, each with options in the report (needs_decision):

  • Cause A2 widget-slot message regression (9256, 11022);
  • Cause B named-view pointers (8355, 10321);
  • pageName / tabs still 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);
  • the flow wait seed (H4);
  • two sites that are consuming-card scope:
    • the 7715 DashboardWidget census row is objectui#9111's criterion;
    • the objectui#10916 tripwire is objectui#10940's burn-down, whose card says it "should land WITH that bump" — a published-type change this claim's surface excludes.

Floors

This PR raises no floor. No code in it imports a 17.5.0-only export. The ^17.5.0 floors 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

  • Consumer zod skew. A consumer whose own lock holds zod 4.4.x and resolves @objectstack/spec 17.5.0 through objectui's ^17.x ranges gets the same two-copy split measured above. That state already exists for any consumer today; it is not created here. Whether objectui's zod floor should rise to ^4.6.1 beside the consuming cards' spec floors is an open question in the report, not an edit here.
  • The 11-card addendum's objectui#9111 reading. The addendum on objectui#11073 reads "Not met: objectui#9111 (checkDashboardWidgetStageOrder is 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.
  • The probes behind these readings were untracked scratch files, deleted before any commit; none is part of the diff.

Generated by Claude Code

…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>
@github-actions

github-actions Bot commented Sep 29, 2026 •

Copy link
Copy Markdown
Contributor

changeset-claim-re-read

⚠️ 113 pending changeset(s) describe a file this change touches

Their bodies publish verbatim into the CHANGELOG at the next release, so this is a request to re-read them against your diff — addressed here because you are the one seat that can answer it without re-deriving anything.

⛔ Nothing here blocks, and nothing here is a verdict on your change. This gate exits 0, is not a required context, and judges name resolution, never meaning: it asked whether a pending body names a file you touched. "Is this sentence still true?" is the one question it will not answer, and the one you are being asked to answer.

.changeset/10864-app-shell-spec-floor.md

  • names packages/app-shell/src/views/metadata-admin/previews/simulator/flow-sim-validate.ts → packages/app-shell/src/views/metadata-admin/previews/simulator/flow-sim-validate.ts — edited by this change

    packages/app-shell/src/views/metadata-admin/previews/screen-spec.ts imports predicateSlotRefusal from @objectstack/spec/automation; packages/app-shell/src/views/metadata-admin/previews/simulator/flow-sim-validate.ts imports structuralConditionRefusal from the same subpath plus EVALUATED_EXPRESSION_SOURCE_REQUIRED and EvaluatedExpressionSchema from @objectstack/spec/shared. @objectstack/spec was declared as ^17.3.0, so any resolution landing on 17.3.0 (which the range allows) gets a module that names exports its own declared spec dependency does not have.

.changeset/5793-spec-range-floors.md

  • names pnpm-lock.yaml → pnpm-lock.yaml — edited by this change

    Nothing a consumer installs today changes: normal resolution already picks the newest 17.x, and pnpm-lock.yaml still resolves 17.2.0 on this edge after the bump. The change is to the declared floor only, which is why it is scored patch rather than minor — the same reasoning objectui#5753 used for the other direction on this dependency.

.changeset/5903-objectgantt-declared-keys.md

  • names packages/types/src/objectql.ts → packages/types/src/objectql.ts — edited by this change

    Both halves move together. The TS declaration (packages/types/src/objectql.ts) and its zod mirror (src/zod/objectql.zod.ts) gain the same ten keys at the same requiredness — all optional — and no KnownDrift entry is added. navigation is taken from @objectstack/spec's NavigationConfigSchema by reference rather than restated, matching ObjectGridSchema.navigation.

  • names src/zod/objectql.zod.ts → packages/types/src/zod/objectql.zod.ts — edited by this change

    Both halves move together. The TS declaration (packages/types/src/objectql.ts) and its zod mirror (src/zod/objectql.zod.ts) gain the same ten keys at the same requiredness — all optional — and no KnownDrift entry is added. navigation is taken from @objectstack/spec's NavigationConfigSchema by reference rather than restated, matching ObjectGridSchema.navigation.

  • names packages/types/src/__tests__/zod-mirror-parity.test.ts → packages/types/src/__tests__/zod-mirror-parity.test.ts — edited by this change

    ⚠️ The JUSTIFICATION for that parity clause was retired (objectui#9743); the clause itself stands and did not move. As first written it credited the zod-mirror-parity ratchet with a zero-drift reading for this pair — but at that time the ratchet measured three directions and was structurally blind to the MIRRORED-but-undeclared one (objectui#9711), so a zero from it recorded that it had not looked in that direction, not that nothing was there. objectui#9725 landed the fourth direction, and it covers this pair BY NAME: packages/types/src/__tests__/zod-mirror-parity.test.ts registers objectql.zod.ts#ObjectGanttSchema in both its mirror map and its declaration map; assertionMirroredUndeclaredMatchesLedger requires every registered pair's mirrored-but-undeclared key set to equal that pair's MirroredUndeclared ledger entry — never for a pair the ledger does not name — and assertionNoVacuousMirroredUndeclaredMeasurement refuses a measurement that has degenerated to any. ⛔ Read this pair's verdict off that reconciliation, which re-derives it on every run, rather than off any figure written here; when this paragraph was authored, on 2026-09-18, it required no MirroredUndeclared entry for the pair.

.changeset/5905-componentinput-inputtype-tombstone.md

  • names zod/form.zod.ts → packages/types/src/zod/form.zod.ts — edited by this change

    The write was measured as a no-op before it was deleted, and re-measured on this branch's base rather than inherited from the card. A structural census over every inputs: array in the repository (211 regions, all tracked TS/TSX/JS sources) scores inputType at exactly ONE authoring site — the plugin-markdown registration — against name 953, type 969, label 966, description 194, enum 119, required 86 and binding 4 in the same pass over the same regions, so the instrument was not blind. The other 192 in-repo inputType hits are a DIFFERENT face: FormField.inputType (zod/form.zod.ts), the text-input renderer's prop, and SchemaBuilder.inputType, none of which sit on a ComponentInput. The publication path is unchanged and was re-confirmed: packages/sdui-parser/src/index.ts forwards exactly seven keys per input — name, type, of, required, enum, binding, description — so an authored inputType could not reach the published sdui.manifest.json even in principle.

.changeset/5920-flow-runner-translation-overlay.md

  • names flow-sim-validate.ts → packages/app-shell/src/views/metadata-admin/previews/simulator/flow-sim-validate.ts — edited by this change

    ⚠️ Dated note, 2026-09-28 — the declared floor has since moved again — objectui#10864. At this change @object-ui/app-shell declared ^17.3.0, and the paragraph above is kept as the reading of that change: 17.3.0 was in fact the first release exporting the resolver family this fix uses. Later in this same release the floor was raised again, to ^17.4.0, because two other shipped app-shell sources (screen-spec.ts, flow-sim-validate.ts) import four symbols (predicateSlotRefusal, structuralConditionRefusal, EVALUATED_EXPRESSION_SOURCE_REQUIRED, EvaluatedExpressionSchema) that 17.3.0 does not export — a defect this changeset's own range did not introduce and did not need to guard against. @object-ui/app-shell now declares ^17.4.0.

.changeset/5928-classname-style-props-rename.md

  • names zod-mirror-parity.test.ts → packages/types/src/__tests__/zod-mirror-parity.test.ts — edited by this change

    Where the non-pair is recorded now. zod-mirror-parity.test.ts keys its existing EXCLUSIONS entry — the mechanism that accounts for every exported const with no TypeScript declaration to mirror, each with its stated reason — to ClassNameStylePropsSchema. Named for its own two keys, the const leaves no like-named declaration for a name-derived pairing to reach for.

.changeset/6050-gantt-navigation-basepath.md

  • names src/readme-navigation-example.test.ts → packages/plugin-gantt/src/readme-navigation-example.test.ts — edited by this change

    No gate in this repo could have caught the original defect, and that is why the fix ships with a measurement rather than a re-reading: check-doc-snippet-types compiles ts/tsx fences and check-doc-component-types reads type literals, and both are structurally blind to a metadata key in a README — the former's own header records schema-key validity as "a different question … left unruled on purpose". src/readme-navigation-example.test.ts closes that hole for this example by EXTRACTING the fence from the README on every run and parsing it against NavigationConfigSchema, with a control asserting the same parse still rejects an undeclared key by name, so the green cannot come from a schema that accepts everything.

.changeset/6051-gantt-flat-config-declared-keys.md

  • names packages/types/src/__tests__/zod-mirror-parity.test.ts → packages/types/src/__tests__/zod-mirror-parity.test.ts — edited by this change

    ⚠️ The JUSTIFICATION for that parity clause was retired (objectui#9743) — the same retirement objectui#5903's entry carries, for the same clause and the same reason; the clause itself stands and did not move. As first written it credited the zod-mirror-parity ratchet with a zero-drift reading for this pair, at a time when that ratchet measured three directions and was structurally blind to the MIRRORED-but-undeclared one (objectui#9711), so a zero from it recorded that it had not looked in that direction. objectui#9725 landed the fourth direction, and it covers this pair BY NAME: packages/types/src/__tests__/zod-mirror-parity.test.ts registers objectql.zod.ts#ObjectGanttSchema in both its mirror map and its declaration map; assertionMirroredUndeclaredMatchesLedger reconciles every registered pair's mirrored-but-undeclared key set against that pair's MirroredUndeclared ledger entry — never for a pair the ledger does not name — and assertionNoVacuousMirroredUndeclaredMeasurement refuses a measurement that has degenerated to any. ⛔ Read this pair's verdict off that reconciliation, which re-derives it on every run, rather than off any figure written here; when this paragraph was authored, on 2026-09-18, it required no MirroredUndeclared entry for the pair.

.changeset/6150-undeclared-but-consumed-keys.md

  • names zod-mirror-parity.test.ts → packages/types/src/__tests__/zod-mirror-parity.test.ts — edited by this change

    ⭐ AMENDED, and the amendment ships in this same release. The TreeViewSchema slice (604476d97) gave the key a zod arm after all — a NAMED REFUSAL (handlerKeyRefusal(key, 'runtime-slot', label)), never a shape — because "no mirror entry" is not neutral under BaseSchema.passthrough(): it meant an authored { "type": "tree-view", "onNodeClick": { "action": "toast" } } parsed GREEN, survived the parse, and reached a call site that expects a function. ⇒ the three clauses this bullet used to carry are no longer true of the code shipping beside it. The key is now a MEMBER of TreeViewSchema.shape and an authored value is refused BY NAME at path onNodeClick; it has LEFT zod-mirror-parity.test.ts's RuntimeOnlyDeclared for that file's KnownDrift; and it is no longer "the first pair to sit there without also sitting in UnmirroredDeclared" — draining it emptied that difference, so RuntimeOnlyDeclared is now a SUBSET of UnmirroredDeclared and the union of the two equals UnmirroredDeclared itself. ⛔ objectui#6152's ruling is untouched by any of this: what the key still does not have, and never will, is a z.function() shape — no serialized document could satisfy one.

.changeset/6175-column-state-persistence.md

  • names zod-mirror-parity.test.ts → packages/types/src/__tests__/zod-mirror-parity.test.ts — edited by this change

    Nothing is retired. Both spellings remain declared on DataTableSchema; onColumnReorder stays declared and stays unwired, exactly as the RuntimeOnlyDeclared ledger in zod-mirror-parity.test.ts records it. Which of the two survives is a declared-surface ruling that stays open and is deliberately not settled here.

.changeset/6286-explain-batch-cap-from-spec.md

  • names scripts/check-spec-symbol-derivation.mjs → scripts/check-spec-symbol-derivation.mjs — edited by this change

    No value changes and no behaviour changes. The spec exports 200, which is what the local copy said, verified by resolving the installed package and reading the export — both statically (dist/security/index.d.mts) and at runtime through the same specifier the source now uses. What changes is reference identity: if the server relaxes or tightens the cap and the spec follows, the client follows too, instead of paginating at the old boundary with no signal anywhere. The cap's whole point is that an over-cap request is refused with 400 VALIDATION_FAILED rather than truncated, so a client that silently disagrees with it is exactly the drift scripts/check-spec-symbol-derivation.mjs argues about — and could not catch here: when this change landed, both of its scanners skipped non-exported declarations, and this const was module-local (objectui#5899). objectui#6291 has since removed that filter from both scanners.

.changeset/6306-action-icon-type-resolution.md

  • names action-button.tsx → packages/components/src/renderers/action/action-button.tsx — edited by this change

    Scope is this one renderer. type: schema appears in exactly two files under renderers/action/ — action-button.tsx (already correct) and action-icon.tsx; action:group and action:menu compose their members differently and are untouched.

  • names action-icon.tsx → packages/components/src/renderers/action/action-icon.tsx — edited by this change

    Scope is this one renderer. type: schema appears in exactly two files under renderers/action/ — action-button.tsx (already correct) and action-icon.tsx; action:group and action:menu compose their members differently and are untouched.

.changeset/6349-name-authority-batch-3.md

  • names form.zod.ts → packages/types/src/zod/form.zod.ts — edited by this change

    @object-ui/components — ComboboxOption now IS @object-ui/types' declaration. The component declared its own { value, label }, a strict subset of the ComboboxOption that @object-ui/types declares for ComboboxSchema.options and mirrors in form.zod.ts ({ value, label, disabled? }). The component now re-exports the types declaration (through the @object-ui/types/form subpath — the root barrel does not publish the name), so the name ComboboxOption exported from @object-ui/components gains the optional disabled?: boolean member. Every value that type-checked before still does — nothing narrows and no key changes type; the one thing that moves is keyof ComboboxOption, so a consumer that EXHAUSTS the type (a Record over its keys) will need the new key. Note that the Combobox component itself does not read option.disabled — that member was already declared on the @object-ui/types face and is now visible on this one too; it is recorded as a separate finding, not changed here.

.changeset/6349-types-internal-name-collisions-batch-1.md

  • names ui-action.ts → packages/types/src/ui-action.ts — edited by this change

    ActionSchema — renamed, because the two shapes are unrelated. crud.ts and ui-action.ts both declared it. Measured member-by-member they share 9 keys out of 28 each: crud.ts extends BaseSchema and pins type: 'action' (a UI node — a button in a component tree), ui-action.ts extends nothing and types type as ActionType (a spec-v2 action definition, with name, locations, params, target). Re-pointing either at the other would silently hand a consumer a different type, so this took the rename branch (objectui#5044 is the precedent for choosing the surviving name). ui-action.ts's declaration is now spelled UIActionSchema — the name src/index.ts has always PUBLISHED it under, via export type { ActionSchema as UIActionSchema }, which is now a plain re-export. The package's public surface is unchanged: ActionSchema still means crud.ts's legacy shape and UIActionSchema still means ui-action.ts's, exactly as before. Nothing outside ui-action.ts imported the old spelling — there is no ./ui-action subpath in exports, so the old name was never reachable from outside the package.

.changeset/6361-spec-floor-17-2-0.md

  • names pnpm-lock.yaml → pnpm-lock.yaml — edited by this change

    Nothing a consumer installs today changes: normal resolution already picks the newest 17.x, and pnpm-lock.yaml still resolves 17.2.0 on both edges after the bump — only the recorded specifier: moves. No source and no behaviour changes, which is why this is scored patch, on the reasoning 111741454 used for the same remediation on @object-ui/plugin-detail.

.changeset/6396-previous-values-dom-leak.md

  • names packages/types/src/zod/form.zod.ts → packages/types/src/zod/form.zod.ts — edited by this change

    Scope is the runtime leak only. The declared key stays exactly as declared (packages/types/src/form.ts, packages/types/src/zod/form.zod.ts are untouched): it has a live consumer, so there is nothing here for the enforce-or-remove channel.

.changeset/6488-carry-over-field-keys.md

  • names MetadataService.fieldKeyCarryOver.test.ts → packages/app-shell/src/services/MetadataService.fieldKeyCarryOver.test.ts — edited by this change

    Two properties keep the fix from becoming its own mirror image, both pinned in MetadataService.fieldKeyCarryOver.test.ts:

.changeset/6499-inactive-values-retained.md

  • names flow-node-config.ts → packages/app-shell/src/views/metadata-admin/inspectors/flow-node-config.ts — edited by this change

    • New inactiveRetainedKind(field, node, fields) in flow-node-config.ts — a pure read that reports a field rendered ONLY because the stored-value re-show rule fired. It distinguishes 'controller-off' (a real toggle the author can switch back on) from 'no-controller' (the __legacy__ render-only keys, where no such toggle exists and saying otherwise would be a fresh lie). - FlowNodeConfigField renders the notice beside the affected control, with a "Clear value" action so the author can discard the residue deliberately. Read-only inspectors show the notice without the action. - Coverage is every showWhen group in the inspectors, pinned mechanically: all 33 gated fields across the descriptor tables, plus the two runtime producers (an engine-published configSchema and a connector input schema) that mint groups no source file contains.

.changeset/6519-retired-field-key-strip.md

  • names plugin-designer/src/MetadataFieldsPage.tsx → packages/plugin-designer/src/MetadataFieldsPage.tsx — edited by this change

    Unifying the three retired-key lists on this seam is deliberately not part of this change: it spans plugin-designer/src/MetadataFieldsPage.tsx, which objectui#6489 owns in flight.

.changeset/6594-headercolor-mirror-enum.md

  • names packages/types/src/views.ts → packages/types/src/views.ts — edited by this change

    The nearest precedent is fc62bb490 (.changeset/5853-tablecolumn-type-canonical-union.md), which narrowed TableColumn.type on the same three-ends pattern and exported a TABLE_COLUMN_TYPES tuple for the zod mirror to build its enum from. That shape is not available here and the difference is structural, not a preference: packages/types/src/views.ts is a type-only module, so a tuple there would add a runtime export to the package barrel (a value export cannot ride the barrel's export type block) and a runtime import edge from the zod entry into views.js. fc62bb490 had a second reason to export — producers needed its normalizeTableColumnType() at their emit seam — and headerColor has no producer that needs a runtime value. The literals are therefore written on each half and the anti-drift guarantee is carried by the pin above, which also covers the third end a shared tuple could not reach: the renderer, in a package @object-ui/types must not depend on.

.changeset/6620-escalation-enabled-default.md

  • names flow-node-config.spec-reconciliation.test.ts → packages/app-shell/src/views/metadata-admin/inspectors/flow-node-config.spec-reconciliation.test.ts — edited by this change

    The reconciliation assertion in flow-node-config.spec-reconciliation.test.ts now covers the whole escalation block rather than notifySubmitter alone, and derives every expected value from the installed ApprovalEscalationSchema instead of pinning literals. This is the substance of the fix as much as the flip is: the previous tripwire for this key read only the hand-written table, so a spec bump could never redden it and the divergence went live unnoticed. A vacuity guard fails if the spec stops materialising defaults for the block, so the ledger cannot quietly become a comparison against nothing.

.changeset/6625-retire-fieldmeta-decimals.md

  • names GridField.tsx → packages/fields/src/widgets/GridField.tsx — edited by this change

    buildFieldMeta computed decimals: overrides.decimals ?? meta?.decimals ?? meta?.scale on every call and the value reached nothing. Re-measured on this branch's base (efdc6c62): zero .decimals member reads across @object-ui/fields, @object-ui/i18n, @object-ui/components, @object-ui/core and plugin-dashboard itself — the only non-comment occurrence was the write being removed here. The positive control in the same query shape fires: .scale member reads hit NumberField.tsx, GridField.tsx and fields/src/index.tsx. So the zero is a finding, not a broken query. The overrides.decimals ?? head of that chain had already lost its only feeder when objectui#6425's ruling removed the authored read from ObjectDataTable.enrich(); RecordDetailDrawer, the only other buildFieldMeta caller, passes no overrides at all. Both halves retire together, so the key leaves in one move.

  • names fields/src/index.tsx → packages/fields/src/index.tsx — edited by this change

    buildFieldMeta computed decimals: overrides.decimals ?? meta?.decimals ?? meta?.scale on every call and the value reached nothing. Re-measured on this branch's base (efdc6c62): zero .decimals member reads across @object-ui/fields, @object-ui/i18n, @object-ui/components, @object-ui/core and plugin-dashboard itself — the only non-comment occurrence was the write being removed here. The positive control in the same query shape fires: .scale member reads hit NumberField.tsx, GridField.tsx and fields/src/index.tsx. So the zero is a finding, not a broken query. The overrides.decimals ?? head of that chain had already lost its only feeder when objectui#6425's ruling removed the authored read from ObjectDataTable.enrich(); RecordDetailDrawer, the only other buildFieldMeta caller, passes no overrides at all. Both halves retire together, so the key leaves in one move.

.changeset/6639-objectgrid-title-mirrored.md

  • names zod-mirror-parity.test.ts → packages/types/src/__tests__/zod-mirror-parity.test.ts — edited by this change

    The gain is the typed refusal: the mirror's .passthrough() base was already admitting any title unexamined, and it now enforces the declared string. zod-mirror-parity.test.ts's UnmirroredDeclared ledger records the key as worked off — the ledger's first shrink by repair (97 + 1 mirrored + 23 reclassified is what the seeded "121" now means).

.changeset/6694-dashboard-lookup-reference-meta.md

  • names packages/fields/src/index.tsx → packages/fields/src/index.tsx — edited by this change

    ⚠️ The copy set is three keys where ObjectGrid's RELATIONAL_META_KEYS is nine, and the difference is measured per key, not preferred. The grid's cells are EDITABLE, so its extra keys drive the inline picker's query (LookupField / UserField read id_field, description_field, lookup_filters, lookupFilters); these two widgets are read-only and their render path ends at a cell renderer. packages/fields/src/index.tsx reads exactly reference_to, reference and display_field off a cell's field prop; titleFormat is never read off a field meta at all (its readers take it off the object schema, which arrives here through useRefObjectSchema(reference_to)), and reference_to_field has zero member reads anywhere in the repo. Copying the other six would mint six members written on every call and read by nothing — precisely what objectui#6625 (decimals) and objectui#6597 (referenceTo) retired from this same file.

.changeset/6726-find-envelope-records-arms.md

  • names plugin-view/src/ObjectView.tsx → packages/plugin-view/src/ObjectView.tsx — edited by this change

    | module | what it does | | --- | --- | | components/src/hooks/related-count-store.ts | related-list tab badge count | | components/src/renderers/basic/data-list.tsx | element:repeater rows | | components/src/renderers/basic/elements.tsx | element:number client-side aggregate | | components/src/renderers/basic/record-picker.tsx | element:record_picker options | | plugin-detail/src/renderers/record-activity.tsx | record:activity self-fetch | | plugin-detail/src/renderers/record-history.tsx | record:history self-fetch | | plugin-view/src/ObjectView.tsx | non-grid (kanban / calendar / gallery / timeline) fetch |

.changeset/6776-metadata-admin-lazy-registration.md

  • names packages/app-shell/package.json → packages/app-shell/package.json — edited by this change

    • packages/app-shell/package.json's sideEffects array now names views/metadata-admin/register-builtins (the new leaf that performs the five registrations) instead of views/metadata-admin/index. The five registrations still run at package load, bare-imported by the package entry, so nothing a consumer could observe changes — but the array is a contract every consumer's bundler reads, so the swap is stated here rather than left to a diff. - The package barrel's 25 metadata-admin runtime re-exports (and 11 type-only ones) now point at their leaf modules. Same names, same types. They are unreachable from outside the package by any other path — exports is root-only — so no import an out-of-package consumer can write is affected.

.changeset/6794-notify-submitter-default.md

  • names flow-node-config.spec-reconciliation.test.ts → packages/app-shell/src/views/metadata-admin/inspectors/flow-node-config.spec-reconciliation.test.ts — edited by this change

    A reconciliation assertion in flow-node-config.spec-reconciliation.test.ts keeps the two sides pinned. It reads the expected value out of the installed spec rather than pinning the literal 'true': objectui is the consumer of that contract and must not become a second source of truth for it. A vacuity guard alongside it fails if the spec ever stops materialising the key at all, so the comparison can never quietly become a comparison against undefined.

.changeset/6837-reference-to-arm-deletion.md

  • names fields/src/index.tsx → packages/fields/src/index.tsx — edited by this change

    Three readers were deliberately left alone. LookupCellRenderer (fields/src/index.tsx), LookupField and UserField read FieldMetadata — ObjectUI's OWN contract, whose LookupFieldMetadata declares reference_to and never declares reference. They are fed by the emitters above and by published example schemas (examples/schema-catalog/src/schemas/fields-lookup/*.json), so narrowing them would break in-repo producers, and plugin-grid's relationalMetaCopySet.derivation.test.ts re-derives its read set from exactly those three sources — where reference_to is recorded with verdict adapter-stamped. DetailViewFieldSchema is likewise untouched.

.changeset/6938-checkbox-wrapper-class.md

  • names zod/form.zod.ts → packages/types/src/zod/form.zod.ts — edited by this change

    packages/components/src/renderers/form/checkbox.tsx:36 reads cn("flex items-center space-x-2", schema.wrapperClass) — classes on the wrapper div around the box and its label — and neither the TypeScript interface in packages/types/src/form.ts nor the zod mirror in zod/form.zod.ts declared the key. It compiled through BaseSchema's index signature and parsed through .passthrough(), admitted unexamined. The same key, on the same class of read, is declared on FileUploadSchema and FilterBuilderSchema (objectui#6150); the checkbox was left out only because its doc page's schema block is a six-line summary.

.changeset/6939-kanban-column-cards.md

  • names complex.zod.ts → packages/types/src/zod/complex.zod.ts — edited by this change

    Breaking, deliberately. KanbanColumn declared its card list as items in complex.ts and in the zod mirror complex.zod.ts. Every board reads cards. Measured on origin/main 78a3cc238: KanbanImpl.tsx reads .cards on 12 lines, KanbanEnhanced.tsx on 8, and bucketCardsIntoColumns twice more as col.cards || []; .items had zero read sites in either board (a same-shaped .title control on the same two files returns 8 and 3, so those zeros are readings and not a mis-shaped probe). Both catalog entries, the plugin docs and content/docs/api/schema-reference.md all author cards.

.changeset/6940-rowactions-boolean-mirror.md

  • names zod/objectql.zod.ts → packages/types/src/zod/objectql.zod.ts — edited by this change

    The list view's same-named rowActions in zod/objectql.zod.ts — z.array(z.string()), the legacy bare-name action list on ObjectGridSchema — is a different key that is correct as it stands, is in parity with its own TS twin (rowActions?: string[]), and is not touched.

.changeset/7070-no-invented-gantt-date-fields.md

  • names plugin-view/src/ObjectView.tsx → packages/plugin-view/src/ObjectView.tsx — edited by this change

    • app-shell/src/views/ObjectView.tsx — the console object page. The inline branch becomes ganttViewOptions, the sibling of calendarViewOptions and timelineViewOptions: the declared block spread whole, title floored at 'name', no date field invented. - plugin-list/src/ListView.tsx — the render branch AND the capability gate. - plugin-view/src/ObjectView.tsx — generateViewSchema, the authored object-view element route, which bypasses ListView entirely.

.changeset/7113-chart-data-model.md

  • names objectql.zod.ts → packages/types/src/zod/objectql.zod.ts — edited by this change

    .extend() with a NEW key still works and preserves the fold and the refinement; .optional(), z.discriminatedUnion, z.toJSONSchema and safeValidateSchema are all unaffected. Nothing in this repository calls the throwing combinators on either const, and the published surface already ships refined mirrors (objectql.zod.ts, complex.zod.ts, form.zod.ts, app.zod.ts), so the class is not new — but it is a real behaviour change on a published export and it belongs in the release note rather than in a reviewer's file.

  • names complex.zod.ts → packages/types/src/zod/complex.zod.ts — edited by this change

    .extend() with a NEW key still works and preserves the fold and the refinement; .optional(), z.discriminatedUnion, z.toJSONSchema and safeValidateSchema are all unaffected. Nothing in this repository calls the throwing combinators on either const, and the published surface already ships refined mirrors (objectql.zod.ts, complex.zod.ts, form.zod.ts, app.zod.ts), so the class is not new — but it is a real behaviour change on a published export and it belongs in the release note rather than in a reviewer's file.

  • names form.zod.ts → packages/types/src/zod/form.zod.ts — edited by this change

    .extend() with a NEW key still works and preserves the fold and the refinement; .optional(), z.discriminatedUnion, z.toJSONSchema and safeValidateSchema are all unaffected. Nothing in this repository calls the throwing combinators on either const, and the published surface already ships refined mirrors (objectql.zod.ts, complex.zod.ts, form.zod.ts, app.zod.ts), so the class is not new — but it is a real behaviour change on a published export and it belongs in the release note rather than in a reviewer's file.

.changeset/7122-objectstack-family-17-3-0.md

  • names pnpm-lock.yaml → pnpm-lock.yaml — edited by this change

    @objectstack/client, core, formula and lint each pin @objectstack/spec EXACTLY, so resolving the spec alone to 17.3.0 left the console bundling TWO copies of it. Moving the family with it in pnpm-lock.yaml collapses the duplicate; every declared range already admitted 17.3.0, so no manifest moved.

.changeset/7125-dashboard-empty-state-keys-retired.md

  • names DatasetWidget.tsx → packages/plugin-dashboard/src/DatasetWidget.tsx — edited by this change

    Not touched: table.noRows ('No rows to display') and engine.form.noRows (packages/app-shell/src/views/metadata-admin/i18n.ts, read at widgets.tsx) — two different, same-named keys in different namespaces. Nor the comments in WidgetEmptyState.tsx, DatasetWidget.tsx, ObjectDataTable.tsx and PivotTable.tsx that record WHY three widgets with three strings became one shared empty state; the packs' own comment keeps that rationale and now names the retirement instead of a row that is gone.

.changeset/7166-retire-inert-fieldmeta-copies.md

  • names packages/fields/src/index.tsx → packages/fields/src/index.tsx — edited by this change

    applyRelationalMeta writes the copy set onto the fieldMeta that generateColumns hands to ANGLE-BRACKETS(CellRenderer) as the field prop — six JSX passes across the three column-building paths, and nowhere else. For a relational column that resolves to LookupCellRenderer, which reads exactly reference_to, reference, display_field, displayField, reference_field and options; a user column resolves to UserCellRenderer, which destructures { value } and reads no field meta at all. Measured by receiver rather than by count: packages/fields/src/index.tsx, the file holding every cell renderer, contains zero occurrences of the three retired keys, against a control of 22 occurrences of the display_field / displayField / reference_to spellings the cell does read.

.changeset/7173-ai-pending-actions-inbox-i18n.md

  • names scripts/check-eager-closure-budget.mjs → scripts/check-eager-closure-budget.mjs — edited by this change

    The ten pack blocks are locale DATA, and locale data lands in the console's eager framework chunk, so scripts/check-eager-closure-budget.mjs raises that chunk's ceiling from 512,000 to 524,000 gzipped bytes and re-pins its baseline onto a fresh measurement (502,405 to 514,863). Attributed by three console builds: the merge parent reads 510,192, this branch with the ten aiApprovals blocks cut reads 510,192 again, and this branch reads 514,863 — so the whole 4,671-byte delta is the pack data and nothing else. Headroom is kept at the line's own convention (9,137 bytes, 0.10x the regression the gate must catch) rather than widened; most of the overage was pre-existing drift, with the merge parent already at 510,192 of the 512,000 allowed.

.changeset/7200-object-form-section-style-keys-undeclared.md

  • names zod/objectql.zod.ts → packages/types/src/zod/objectql.zod.ts — edited by this change

    The authored-metadata type now agrees with @objectstack/spec, whose FormSectionSchema is a strict object declaring neither key, and with the ruling's rationale (maintainer 2026-09-01, verbatim): "retire the reads … Declaring the keys was weighed and not adopted: it would formally invite free Tailwind strings into authored metadata, the exact class the boundary exists to keep out." A ?: never tombstone was not used: ObjectFormSection has no zod mirror (ObjectFormSchema in zod/objectql.zod.ts does not declare sections), so there is no parse door to refuse at, and a tombstone is still a declaration in completion and in the published .d.ts.

.changeset/7265-types-user-filter-field-derives.md

  • names zod/objectql.zod.ts → packages/types/src/zod/objectql.zod.ts — edited by this change

    zod/objectql.zod.ts declared two schemas under names @objectstack/spec/ui already exports. They were triaged separately, by reading their sites, and went different ways.

.changeset/7287-resolve-title-field-shared-ladder.md

  • names views.ts → packages/types/src/views.ts — edited by this change

    def.primaryField — a DetailViewSchema key (@object-ui/types views.ts), read off an OBJECT def and ranked ABOVE the canonical nameField ADR-0079 Phase 2 made the pointer (AGENTS.md Commandment #0.1). No producer can put it there: @objectstack/spec's object schema is a strictObject that answers unrecognized_keys: ['primaryField'], and ObjectSchema.create() throws — which is why objectstack#6326 deleted the identical read from two lint rules. A census across both repos found zero object payloads carrying it (the only writers are three test fixtures), and primaryField appears in zero files of the shipped @objectstack/spec@17.2.0 dist against 68 for nameField. Same shape as the undeclared objectDef.titleField read objectui#6531 measured and Five more consumer-side reads of the undeclared object-level titleField survive #6531 — in ObjectView view-config fallbacks and a useRecordSearch memo key #6557 removed. DetailViewSchema.primaryField is untouched and still honoured by DetailView's own header — it is a view key, and on a view it is legitimate.

.changeset/7313-object-calendar-record-source.md

  • names plugin-calendar/src/ObjectCalendar.tsx → packages/plugin-calendar/src/ObjectCalendar.tsx — edited by this change

    ObjectCalendar resolves its records through the shared ladder (resolveRecordSourceConfig in @object-ui/core, called from plugin-calendar/src/ObjectCalendar.tsx): data first, then staticData, then objectName. The published TypeScript interface REQUIRED objectName and declared neither data nor staticData; the published Zod mirror did the same. So an object-calendar node authored on staticData — the route the plugin page documents twice — rendered correctly and was refused by safeValidateSchema, and could not be annotated with its own type (TS2741: Property 'objectName' is missing).

  • names content/docs/plugins/plugin-calendar.mdx → content/docs/plugins/plugin-calendar.mdx — edited by this change

    A widening. A node authoring staticData or data without objectName now validates (it always rendered — the read is resolveRecordSourceConfig(schema), keyed on the three). Every document that validated before still validates: objectName alone still parses, an empty one included, because presence is !== undefined. The one shape the refinement refuses (none of the three) was refused before too, at objectName. The two static-data examples in content/docs/plugins/plugin-calendar.mdx are now annotated ObjectCalendarSchema and compile under the doc-snippet gate.

.changeset/7322-object-kanban-group-by-limit.md

  • names objectql.ts → packages/types/src/objectql.ts — edited by this change

    Breaking for authored metadata: ObjectKanbanSchema.groupField is RETIRED (objectui#7322, ADR-0049 enforce-or-remove), and the two keys the object-kanban renderer actually reads — groupBy and limit — are now DECLARED and validated on both published faces: the TypeScript interface in objectql.ts and the Zod mirror in zod/objectql.zod.ts.

  • names zod/objectql.zod.ts → packages/types/src/zod/objectql.zod.ts — edited by this change

    Breaking for authored metadata: ObjectKanbanSchema.groupField is RETIRED (objectui#7322, ADR-0049 enforce-or-remove), and the two keys the object-kanban renderer actually reads — groupBy and limit — are now DECLARED and validated on both published faces: the TypeScript interface in objectql.ts and the Zod mirror in zod/objectql.zod.ts.

  • names packages/core/src/utils/normalize-list-view.ts → packages/core/src/utils/normalize-list-view.ts — edited by this change

    Who is NOT affected. The VIEW-LEVEL kanban config is untouched: kanban.groupField there is a live legacy alias of the spec's groupByField (packages/core/src/utils/normalize-list-view.ts maps it; plugin-list's ListView and plugin-view's ObjectView still read it). groupField is dead only on the object-kanban NODE. The declarative kanban node (KanbanSchema) is untouched, BaseSchema's unknown-key policy is byte-identical (an undeclared key still passes through), and the renderer is unchanged — boards authored the documented way rendered before and render now. One in-repo fixture authored groupField on this node (packages/types/src/__tests__/kanban-conditional-formatting.test.ts); it now authors groupBy. No doc snippet, catalog entry, skill or app in this repository authored groupField on an object-kanban node.

.changeset/7344-handler-string-any-mirrors.md

.changeset/7352-drill-down-config-mirror.md

  • names zod/objectql.zod.ts → packages/types/src/zod/objectql.zod.ts — edited by this change

    DrillDownConfigSchema is the zod mirror of DrillDownConfig, and both declarations that carry drillDown reference it — ChartSchema (zod/data-display.zod.ts) and ObjectDataTableSchema (zod/objectql.zod.ts) — so the published validator under @object-ui/types/zod reads the key for the first time (objectui#7352).

.changeset/7363-objectql-union-arms.md

  • names objectql.ts → packages/types/src/objectql.ts — edited by this change

    ObjectGallerySchema and ObjectDataTableSchema are members of ObjectQLComponentSchema on both faces — the TS union in objectql.ts and the zod union in zod/objectql.zod.ts — so AnyComponentSchema, and with it validateSchema / safeValidateSchema / objectui validate, has an arm for object-gallery and object-data-table nodes (objectui#7363).

  • names zod/objectql.zod.ts → packages/types/src/zod/objectql.zod.ts — edited by this change

    ObjectGallerySchema and ObjectDataTableSchema are members of ObjectQLComponentSchema on both faces — the TS union in objectql.ts and the zod union in zod/objectql.zod.ts — so AnyComponentSchema, and with it validateSchema / safeValidateSchema / objectui validate, has an arm for object-gallery and object-data-table nodes (objectui#7363).

.changeset/7499-gantt-non-axis-floors-omitted.md

  • names plugin-view/src/ObjectView.tsx → packages/plugin-view/src/ObjectView.tsx — edited by this change

    • plugin-list/src/ListView.tsx — the object-gantt render branch. - plugin-view/src/ObjectView.tsx — generateViewSchema, the authored object-view element route, which bypasses ListView entirely.

.changeset/7530-predicate-envelope-declared.md

  • names zod/form.zod.ts → packages/types/src/zod/form.zod.ts — edited by this change

    • ExpressionWire (type, main entry) — the TypeScript wire union, in packages/types/src/expression.ts. - ExpressionWireSchema (@object-ui/types/zod) — its runtime twin, hoisted out of zod/form.zod.ts (where it was module-private) into zod/expression.zod.ts and imported by both base.zod.ts and form.zod.ts. One envelope type, reused by reference; no second spelling.
  • names form.zod.ts → packages/types/src/zod/form.zod.ts — edited by this change

    • ExpressionWire (type, main entry) — the TypeScript wire union, in packages/types/src/expression.ts. - ExpressionWireSchema (@object-ui/types/zod) — its runtime twin, hoisted out of zod/form.zod.ts (where it was module-private) into zod/expression.zod.ts and imported by both base.zod.ts and form.zod.ts. One envelope type, reused by reference; no second spelling.

.changeset/7586-primary-field-consumer-reads.md

  • names views.ts → packages/types/src/views.ts — edited by this change

    primaryField is a DetailViewSchema key (@object-ui/types views.ts) — a view key, which DetailView.resolveDisplayTitle reads off schema and is welcome to. Read off an object def it is undeclared: @objectstack/spec's object schema is a strictObject answering unrecognized_keys: ['primaryField'], and ObjectSchema.create() throws. primaryField appears in zero files of the shipped @objectstack/spec@17.2.0 dist, against 68 for the canonical nameField. objectstack#6326 removed the identical read from two lint rules; objectui#7287 / PR fix(plugin-detail): resolveTitleField delegates to the shared ADR-0079 ladder #7585 removed it from resolveTitleField. These two survived it — and three of this repo's own changelogs already called the probe "not a spec property — always undefined" while the code kept honouring it.

.changeset/7632-shared-record-source-config.md

  • names packages/types/src/objectql.ts → packages/types/src/objectql.ts — edited by this change

    That ladder is published contract on both faces — packages/types/src/objectql.ts and its zod mirror both ship .describe() strings naming getDataConfig's order (77cb489b4, maintainer ruling 2026-09-02), pinned by objectql-record-source-refinement-6939.test.ts — and it was hand-copied into five plugin components with no gate holding them together. A change to the ruled order had five edit sites and nothing that noticed a missed one; that is the AGENTS.md #0.1 drift class.

.changeset/7635-spec-173-comment-repairs.md

  • names scripts/check-installed-spec-pin-claims.mjs → scripts/check-installed-spec-pin-claims.mjs — edited by this change

    Two of the five sites are now watched. They are written as single-line claims about the installed pin, which moves them out of scripts/check-installed-spec-pin-claims.mjs's ledger of known-stale debt and into the population that gate re-derives at every bump; their ledger entries are deleted in the same change, as that gate's both-direction ratchet requires. The remaining three sit on facts no instrument reads, and say so rather than reading as live.

.changeset/7654-floating-chatbot-trigger-icon-tombstone.md

  • names zod-mirror-parity.test.ts → packages/types/src/__tests__/zod-mirror-parity.test.ts — edited by this change

    Every other tombstone in this package pairs ?: never with a retirementTombstone() refusal on the Zod twin. There is no twin here to carry one: FloatingChatbotConfig has no Zod mirror at all, and floatingConfig sits in the UnmirroredDeclared ledger (zod-mirror-parity.test.ts, complex.zod.ts#ChatbotSchema). BaseSchema is .passthrough(), so the whole floatingConfig object rides through unvalidated — before this change and after it. Minting a mirror to host a refusal would be the declared-but-UNMIRRORED axis (objectui#6152), a different defect: a key can be mirrored and inert, or unmirrored and live, and fixing one says nothing about the other. This change does not widen into it.

.changeset/7703-chatbot-dark-keys-retired.md

  • names packages/types/src/zod/complex.zod.ts → packages/types/src/zod/complex.zod.ts — edited by this change

    Each member goes to ?: never on packages/types/src/complex.ts and to retirementTombstone(...) on packages/types/src/zod/complex.zod.ts — both halves, in lockstep, the convention MarkdownSchema.sanitize (objectui#6972), TimelineSchema.timeScale (objectui#6355) and ObjectViewSchema.viewTabBar (objectui#7779) already carry. Each refusal names the key, says why it is retired, and points at what to write instead; one string feeds both the parse-time message and the .describe() metadata, so the two cannot drift.

.changeset/7712-kanban-calendar-filter-input.md

  • names ObjectCalendar.tsx → packages/plugin-calendar/src/ObjectCalendar.tsx — edited by this change

    ObjectKanban.tsx sends the authored key to the query as $filter: schema.filter and ObjectCalendar.tsx does the same, and @objectstack/spec's ComponentPropsMap declares filter on both blocks (measured: safeParse accepts it, and refuses an undeclared key by name on the same call). But none of the four registrations that publish those two renderers listed filter in inputs, and sdui-parser's validateTree reports unknown-prop for every key no inputs entry claims. So an author writing the one spelling that WORKS was told it was unknown — objectui#6678's shape, where a correct write draws the same diagnostic as a write that does nothing. That is worse than an inert key: it actively punishes the correct behaviour, and the honest response to it is to delete working metadata.

.changeset/7722-wrapper-class-five-more.md

  • names zod/form.zod.ts → packages/types/src/zod/form.zod.ts — edited by this change

    Each of renderers/form/switch.tsx, textarea.tsx, date-picker.tsx, select.tsx and renderers/data-display/list.tsx reads schema.wrapperClass onto its wrapper element, and neither the TypeScript interface (form.ts, data-display.ts) nor the zod mirror (zod/form.zod.ts, zod/data-display.zod.ts) declared the key. The reads compiled through BaseSchema's index signature (objectui#5155) and the values parsed through .passthrough(), admitted unexamined. The same key, on the same class of read, is declared on CheckboxSchema (b74a8598d), FileUploadSchema and FilterBuilderSchema (objectui#6150); these five were left out only because their doc pages never listed it.

.changeset/7735-zod-mirrors-stop-authoring-defaults.md

  • names form.zod.ts → packages/types/src/zod/form.zod.ts — edited by this change

    What changed. All 41 .default() call sites under packages/types/src/zod/ are removed — layout.zod.ts 22, crud.zod.ts 11, form.zod.ts 5, views.zod.ts 2, app.zod.ts 1. @object-ui/components reconciles the third face a separate finding found: flex's registration defaultProps.align seeded 'center', the value its own renderer never applies, so a designer-made node laid out differently from a hand-authored one; it now seeds 'start'.

.changeset/7772-page-block-kanban-group-by-control.md

  • names core/src/utils/normalize-list-view.ts → packages/core/src/utils/normalize-list-view.ts — edited by this change

    The view-level kanban.groupField alias (core/src/utils/normalize-list-view.ts) is live and untouched — node-level retirement and the view-level alias are different things.

.changeset/7773-kanban-adapter-groupfield-write.md

  • names normalize-list-view.ts → packages/core/src/utils/normalize-list-view.ts — edited by this change

    Who is NOT affected — the boundary is node-local. Every VIEW-LEVEL groupField read is untouched and still live: it is a legacy alias of the spec's groupByField on the kanban view config, mapped by normalize-list-view.ts, and both adapters still resolve lanes through it (ObjectView.tsx's kanbanCfg.groupField ||, ListView.tsx's groupByField || groupField). Authoring options.kanban.groupField on a list-view or object-view keeps working exactly as documented in packages/plugin-list/README.md. groupField is dead only on the generated object-kanban NODE.

  • names packages/plugin-list/README.md → packages/plugin-list/README.md — edited by this change

    Who is NOT affected — the boundary is node-local. Every VIEW-LEVEL groupField read is untouched and still live: it is a legacy alias of the spec's groupByField on the kanban view config, mapped by normalize-list-view.ts, and both adapters still resolve lanes through it (ObjectView.tsx's kanbanCfg.groupField ||, ListView.tsx's groupByField || groupField). Authoring options.kanban.groupField on a list-view or object-view keeps working exactly as documented in packages/plugin-list/README.md. groupField is dead only on the generated object-kanban NODE.

.changeset/7804-objectql-handler-key-arms.md

  • names objectql.ts → packages/types/src/objectql.ts — edited by this change

    The four plain objectql.ts node faces declare the nine handler keys their registered renderers read (the objectql.ts slice): ObjectFormSchema.onCancel / .onError / .onOpenChange / .onStepChange / .onSuccess, ObjectGallerySchema.onCardClick / .onRowClick, ObjectGridSchema.onNavigate and ObjectViewSchema.onNavigate.

.changeset/7804-tree-view-handler-slot.md

  • names objectql.ts → packages/types/src/objectql.ts — edited by this change

    'runtime-slot' and not 'retired', measured at this key's own channel. 'retired' publishes "no renderer reads this key, so nothing could ever run it" — true of the two siblings already tombstoned on this arm (onSelectChange, onExpandChange) and flatly false here, since the read is live and INVOKED. ⚠️ No in-repo host builds a tree-view node carrying the key: the channel is wired end to end and only the supplier is absent, which is the same shape as ObjectFormSchema.onStepChange in this card's objectql.ts slice and is not evidence of a dead read. The TypeScript declaration is unchanged and still callable, so a programmatic host supplies it exactly as before.

  • names zod-mirror-parity.test.ts → packages/types/src/__tests__/zod-mirror-parity.test.ts — edited by this change

    The pair moves from zod-mirror-parity.test.ts's RuntimeOnlyDeclared to its KnownDrift, which empties the former of the one entry the latter did not also hold — so the two unmirrored ledgers are now in a containment relation, and the cross-ledger figure that recorded their difference states the containment instead.

.changeset/7917-export-breadcrumb-object-tree-zod-schemas.md

  • names objectql.zod.ts → packages/types/src/zod/objectql.zod.ts — edited by this change

    AnyComponentSchema declares 107 node component types. 105 of them could be named on the ./zod barrel — ButtonSchema.safeParse(node), which is what a designer, a form builder or a targeted test needs. The arms declaring type: 'breadcrumb' (navigation.zod.ts) and type: 'object-tree' (objectql.zod.ts) could not: both were already export const in their own module, but index.zod.ts — the package's only zod entry point — did not re-export them, so the schemas existed, were maintained, and were applied by the union while no consumer could name them.

.changeset/7963-alert-dialog-footer-keys-retired.md

  • names objectql.ts → packages/types/src/objectql.ts — edited by this change

    Nothing else moves. These spellings are overloaded across the tree and every other owner is a live key on a different declaration — FormSchema.cancelLabel, objectql.ts's confirmLabel, plugin-designer's ConfirmDialog React props, plugin-grid's def.confirmLabel, and plugin-form's ModalForm / DrawerForm, which build a local cancelLabel from schema.cancelText. None is an AlertDialogSchema; none is touched, and a pin asserts it. No fixture, catalog schema, example app or doc fence authored any of the three on an alert-dialog node, so no shipped document is stranded.

.changeset/8067-component-input-member-kind.md

  • names apps/console/src/__tests__/registry-inputs-spec-parity.test.ts → apps/console/src/__tests__/registry-inputs-spec-parity.test.ts — edited by this change

    A registration's type: 'array' said a value was a list and stopped there, so a member that drifted from @objectstack/spec was invisible to every layer that reads a declaration. page:header.actions is the measured cost: the contract declares z.array(z.string()) ("Action IDs"), the renderer read the members as ActionDef objects, and the repo-wide parity gate in apps/console/src/__tests__/registry-inputs-spec-parity.test.ts stayed green for the whole life of the drift because both sides carried the key and neither could say what was inside it. What settled it was a maintainer ruling, not a test — and even after the fix, "these are ids" survived only as English in the registration's description.

.changeset/8171-calendar-sort-input.md

  • names ObjectCalendar.tsx → packages/plugin-calendar/src/ObjectCalendar.tsx — edited by this change

    objectui#7712's defect, one key over. ObjectCalendar.tsx lowers the authored key onto its own query as $orderby: convertSortToQueryParams(schema.sort), and @objectstack/spec's ComponentPropsMap['object-calendar'] declares sort (measured on 17.2.0: safeParse({ objectName, sort }) returns success: true, while the same strict schema on the same call refuses bogusProp by name — that control is what makes the acceptance a verdict). But neither of the two registrations that publish this renderer — plugin-calendar:object-calendar and view:calendar — listed sort in inputs, and sdui-parser's validateTree reports unknown-prop for every key no inputs entry claims. So an author writing the one spelling that WORKS was told it was unknown — objectui#6678's shape, where a correct write draws the same diagnostic as a write that does nothing.

.changeset/8174-kanban-calendar-filter-sort.md

  • names ObjectCalendar.tsx → packages/plugin-calendar/src/ObjectCalendar.tsx — edited by this change

    filter had four declaration faces and only three of them named it: @objectstack/spec declares it (ComponentPropsMap['object-kanban'] and ['object-calendar']), both plugins' registration inputs publish it, and both renderers read it — ObjectKanban.tsx lowers schema.filter onto $filter, ObjectCalendar.tsx lowers schema.filter onto $filter and schema.sort onto $orderby through convertSortToQueryParams. This package's own published faces (the TypeScript interface and its zod mirror) named none of them, so an authored value reached the renderer only through BaseSchema's index signature and the mirror's .passthrough() — admitted, never examined. That is the same reasoning finding(types,plugin-kanban): ObjectKanbanSchema requires groupField (zero read sites) and declares neither groupBy nor limit — no working object-kanban node is assignable to any declared type #7322 used to move groupBy and limit into this same interface.

.changeset/8317-strip-imported-defaults.md

  • names zod/imported-defaults.ts → packages/types/src/zod/imported-defaults.ts — edited by this change

    Decision batch Redesign examples based on new JSON project specification #69 (objectui#7735) ruled a principle: a validator validates; it does not write values into an author's document. PR fix(types,components): the zod mirrors stop authoring defaults #8299 delivered it for the 41 .default() call sites written in this package's own mirrors. Measured afterwards, 57 ZodDefault nodes were still reachable from the published @object-ui/types/zod barrel, every one inside a subschema imported by reference from @objectstack/spec — so safeValidateSchema went on substituting on those keys, with 41 stripped and 57 not and no way to tell which was which from the document. Batch [WIP] Fix action run issue in CI/CD pipeline #90 ruled that the principle holds for every key the validator answers, and those 57 are now stripped where the spec enters this package (.removeDefault(), the established local pattern, applied through zod/imported-defaults.ts).

.changeset/8338-retire-toast-action.md

  • names zod-mirror-parity.test.ts → packages/types/src/__tests__/zod-mirror-parity.test.ts — edited by this change

    The parity ledgers drain with it, every figure re-derived by zod-mirror-parity.test.ts's own AST and mirror instruments rather than stepped by hand: KnownDrift 42 entries / 64 keys → 41 / 63 (the entry's whole content, so the entry went too — the ledger's first loss by RETIRING a key rather than by moving either face toward the other), and WiderThanDeclared 23 / 36 / 47 arms, split 6 / 30 / 0 / 11 → 22 / 35 / 45, split 6 / 29 / 0 / 10. The pair itself stays registered, so EXPECTED_MIRROR_PAIRS does not move.

.changeset/8355-retire-calendar-date-aliases.md

  • names calendar-date-alias-refusal-8355.test.ts → packages/types/src/__tests__/calendar-date-alias-refusal-8355.test.ts — edited by this change

    ⚠️ Dated note, 2026-09-28 — a named view's options.calendar nesting is no longer judged inside — objectui#7928. Later in this same release ObjectViewSchema.listViews became the protocol's strict ObjectListViewSchema record, by reference, and that record refuses a named view's options bag whole (unrecognized_keys at listViews.KEY, naming options). So an alias written under listViews.KEY.options.calendar no longer draws an issue naming the key or the remedy. The named view's calendar block still does: the protocol refuses the alias there as an unrecognized key, and this entry's check adds the pointer at the key beside it. A stored body that @object-ui/app-shell's ViewPreview relays has its options.calendar folded onto the calendar block first (foldStoredListOptions). The list view's two nestings and both object-calendar surfaces are unchanged; calendar-date-alias-refusal-8355.test.ts in @object-ui/types re-derives each surface. .changeset/7928-listviews-by-reference-fold.md (PR objectui#10821) states what ships; the text above is kept as the reading of this change.

  • names ObjectView.calendarAliasRefused-8355.test.tsx → packages/plugin-view/src/__tests__/ObjectView.calendarAliasRefused-8355.test.tsx — edited by this change

    Pinned by calendar-date-alias-refusal-8355.test.ts (@object-ui/types, all five surfaces, the per-surface consequence split, the compile-time face, and the structural guard that listViews stays out of the object's shape), ListView.calendarAliasRefused-8355.test.tsx (@object-ui/plugin-list) and ObjectView.calendarAliasRefused-8355.test.tsx (@object-ui/plugin-view) — both reading the node their producer really emits through a spy registration, which is the only census that can see a key arriving through a spread — and the inverted ledger rows in calendarUnionReads-8651.test.tsx (@object-ui/plugin-calendar).

  • names calendarUnionReads-8651.test.tsx → packages/plugin-calendar/src/__tests__/calendarUnionReads-8651.test.tsx — edited by this change

    Pinned by calendar-date-alias-refusal-8355.test.ts (@object-ui/types, all five surfaces, the per-surface consequence split, the compile-time face, and the structural guard that listViews stays out of the object's shape), ListView.calendarAliasRefused-8355.test.tsx (@object-ui/plugin-list) and ObjectView.calendarAliasRefused-8355.test.tsx (@object-ui/plugin-view) — both reading the node their producer really emits through a spy registration, which is the only census that can see a key arriving through a spread — and the inverted ledger rows in calendarUnionReads-8651.test.tsx (@object-ui/plugin-calendar).

.changeset/8415-filter-builder-condition-id.md

  • names zod/complex.zod.ts → packages/types/src/zod/complex.zod.ts — edited by this change

    Breaking for authored metadata: a filter-builder CONDITION must now declare id (objectui#8415). It is declared on both published faces — the TypeScript interface FilterBuilderCondition in complex.ts and the Zod mirror FilterBuilderConditionSchema in zod/complex.zod.ts — so a key the renderer has always required is finally validated instead of silently discarded.

.changeset/8466-calendar-color-allday-fields.md

  • names ObjectCalendar.tsx → packages/plugin-calendar/src/ObjectCalendar.tsx — edited by this change

    ObjectCalendar.tsx's getCalendarConfig reads FIVE flat keys off the node, and packages/plugin-calendar/README.md teaches all five in one sentence — "point titleField / startDateField / endDateField / allDayField / colorField at your own fields when they differ." Only three of the five were declared. The other two reached the renderer through BaseSchema's [key: string]: any on the TypeScript face and its .passthrough() on the zod mirror: admitted, never examined. A misspelling therefore left the calendar silently colourless while every published gate passed.

.changeset/8478-describe-line-addresses.md

.changeset/8478-zod-pins-complex.md

.changeset/8478-zod-pins-form-layout.md

.changeset/8499-node-slot-registered-arms.md

  • names zod/form.zod.ts → packages/types/src/zod/form.zod.ts — edited by this change

    • SemanticElementSchema (zod/layout.zod.ts) — the seven HTML sectioning tags renderers/layout/semantic.tsx registers: aside main header nav footer section article. - HtmlElementSchema (zod/layout.zod.ts) — the 37 safe flow/inline tags renderers/basic/html-elements.tsx registers (h1…h6, p, a, ul, img, …), plus the per-tag keys that module forwards to the DOM (href, target, rel, title, src, alt, width, height, dateTime, cite). ⚠️ Dated note, 2026-09-27 — that set has since gained code — objectui#10756. At this change TAGS and this arm both named 37 tags; both now name 38, and the parity pin counts 38. The rest of this entry is kept as the reading of this change. - InputShorthandSchema (zod/form.zod.ts) — email / password, the two aliases renderers/form/input.tsx registers onto the input renderer with inputType pinned. inputType is deliberately NOT declared on this arm: the wrapper spreads its own value last, so an authored one is overwritten. ⚠️ Dated note, 2026-09-28 — inputType is now declared on this arm, as a refusal — objectui#8762. Later in this same release the arm declares inputType on both faces and refuses it by name (?: never on the TypeScript face, a retirementTombstone on the zod mirror, at path inputType), with guidance pointing at { "type": "input", "inputType": "email" }. So "inputType is deliberately NOT declared on this arm" no longer holds; the reason does, since the wrapper still spreads its own value last. The rest of this entry is kept as the reading of this change. - UiCalendarSchema (zod/form.zod.ts) — ui:calendar, the date-picker primitive renderers/form/calendar.tsx registers under exactly that key (skipFallback, because bare calendar belongs to the plugin-calendar view).

.changeset/8516-8556-mirror-partial-record-narrowing.md

  • names __tests__/mirror-partial-record-narrowing-8516.test.ts → packages/types/src/__tests__/mirror-partial-record-narrowing-8516.test.ts — edited by this change

    The spelling is z.partialRecord, and that is load-bearing. ⛔ Not z.record(z.enum([…]), …): measured on zod 4.4.3, the plain record over an enum key REQUIRES every member, so { md: 2 } stops parsing — it would trade this divergence for its exact opposite, and on exportConfigs it would re-impose the total-Record authoring face objectui#6121's maintainer ruling removed. That measurement is pinned executably in __tests__/mirror-partial-record-narrowing-8516.test.ts, at compile time (the inferred map is Partial<RecordANGLE-BRACKETS(…)>, not RecordANGLE-BRACKETS(…)) and at run time (one accepting row per member), so it cannot rot into folklore.

.changeset/8572-chatbot-body-retired.md

  • names zod-mirror-parity.test.ts → packages/types/src/__tests__/zod-mirror-parity.test.ts — edited by this change

    Why this key and not another. It was the ONE place in this vocabulary where body did not mean "what goes inside this component": zod-mirror-parity.test.ts carried the pair under KnownDrift as "two different meanings of one key", and the same collision was the whole reason chatbot was the single arm of the component union whose output was not assignable to SchemaNode. Both ledger rows move with this change, and the two pins that recorded the old state are INVERTED rather than deleted (see below).

.changeset/8649-detail-renderer-undeclared-keys.md

  • names packages/plugin-detail/src/renderers/__tests__/detailRendererUndeclaredKeys-8649.test.ts → packages/plugin-detail/src/renderers/__tests__/detailRendererUndeclaredKeys-8649.test.ts — edited by this change

    This is an alignment, not a widening: the accept set of this face moves to the contract's and never past it. The pin that says so re-derives the contract side on every run rather than restating it (packages/plugin-detail/src/renderers/__tests__/detailRendererUndeclaredKeys-8649.test.ts).

.changeset/8653-listview-title-retired-rowactiondefs-pinned.md

  • names packages/plugin-view/src/ObjectView.tsx → packages/plugin-view/src/ObjectView.tsx — edited by this change

    title — retired. ListView resolved its export filename through schema.label || (schema as any).title. @objectstack/spec/ui's ListViewSchema refuses title by name (unrecognized_keys: ['title']) while ObjectGridPropsSchema accepts it; packages/types mirrors the platform contract rather than ruling over it, so declaring title on ListViewSchema would have made this repo accept what the platform save gate rejects. That asymmetry is also why objectui#6639 could take the declare branch for ObjectGridSchema.title one package over and this site could not. A parse-based census of apps/ examples/ content/ and packages/ found zero list-view nodes authoring title, so the retirement costs no author a filename. Over that same corpus the instrument reports three object-grid nodes carrying the key: two authored ones, both in content/docs/api/schema-reference.md, plus one that is not authored at all — packages/plugin-view/src/ObjectView.tsx composes title: schema.table?.title onto a grid node it builds, so it is a producer writing the key rather than an author declaring it. ObjectGrid's own title reads are untouched — they remain declared, ruled and read.

.changeset/8655-object-tree-props-schema-typed.md

  • names ObjectTree.schemaTyped-8655.test.ts → packages/plugin-tree/src/ObjectTree.schemaTyped-8655.test.ts — edited by this change

    What the re-measurement then surfaced, reported rather than tidied: six more reads became answerable, five declared and one — filter — not. Its verdict is declare-by-mirror-alignment and it is ⛔ NOT executed here, because the two files that would carry it are held by an in-flight branch; the ledger row in ObjectTree.schemaTyped-8655.test.ts reddens the day it lands. tree is declared by the protocol on the VIEW and on zero element faces, so declaring it on the node would fork rather than mirror the contract, and retiring the read needs a producer census a text search cannot make. navigation belongs to a blocked card and is ledgered, ⛔ not ruled.

.changeset/8735-objectql-mirror-docblocks-not-defaulted.md

  • names zod/objectql.zod.ts → packages/types/src/zod/objectql.zod.ts — edited by this change

    Correct four zod/objectql.zod.ts docblocks that described the behaviour objectui#8317 removed. Since that change the zod mirrors strip imported @objectstack/spec defaults at this package's import boundary, but the docblocks on HttpRequestSchema, ListColumnSchema, SelectionConfigSchema and PaginationConfigSchema still said, in the present tense, that method, prefix.type, type and pageSize are defaulted on parse — the opposite of what each export does. Each now says the key is declared and accepted but NOT defaulted on parse.

.changeset/8755-bulk-lookup-depends-on.md

  • names plugin-grid/src/__tests__/bulkLookupDependsOnReach-8755.test.tsx → packages/plugin-grid/src/__tests__/bulkLookupDependsOnReach-8755.test.tsx — edited by this change

    ⚠️ The bulk param schema does not license the key, and must not be cited as if it did. @objectstack/spec's BulkActionParamSchema "accepts" dependsOn — and accepts a nonsense key in the same run, because it is not strict; the strict sibling ActionParamSchema refuses both. That accept is a null reading, pinned with both controls in plugin-grid/src/__tests__/bulkLookupDependsOnReach-8755.test.tsx leg B. Whether the key should become authorable by contract here — by closing that schema, or by giving the bulk surface the field-backed param route resolveActionParams gives the single-record dialog — is upstream of this repository and stays open.

.changeset/8767-object-grid-refuses-string-sort.md

  • names packages/types/src/objectql.ts → packages/types/src/objectql.ts — edited by this change

    Migration. Write the array: sort: [{ field: 'name', order: 'desc' }]. Both keys are required. SortConfig.order carries no ? in @object-ui/types (packages/types/src/objectql.ts) and no .optional() in its zod mirror, and the protocol's own reusable SortItemSchema requires order as well — measured: that schema refuses [{ field: 'name' }] with invalid_value at 0.order. Do not omit it: this block's array arm interpolates whatever is present, so an omitted order lowers to $orderby: 'name undefined' today. That is pre-existing behaviour on the arm this change does not touch, and it is filed as a successor card rather than widened into here.

.changeset/8801-object-kanban-allow-collapse-retired.md

  • names packages/types/src/objectql.ts → packages/types/src/objectql.ts — edited by this change

    • the declarations retired here — packages/types/src/objectql.ts and its mirror packages/types/src/zod/objectql.zod.ts; - the pins that assert the retirement — object-kanban-allow-collapse-retired-8801.test.ts and bare-kanban-node-key-retired-8802.test.ts; - a comment in packages/types/src/zod/complex.zod.ts, recording that the deleted retiredZeroReadKanbanKey helper once carried this spelling on the SIBLING arm; - one row of content/docs/api/schema-reference.md; - the .changeset/ release notes that discuss it — this one, the two historical entries covering the sibling arm's own spelling, and objectui#9629's note recording the correction to this paragraph.
  • names packages/types/src/zod/objectql.zod.ts → packages/types/src/zod/objectql.zod.ts — edited by this change

    • the declarations retired here — packages/types/src/objectql.ts and its mirror packages/types/src/zod/objectql.zod.ts; - the pins that assert the retirement — object-kanban-allow-collapse-retired-8801.test.ts and bare-kanban-node-key-retired-8802.test.ts; - a comment in packages/types/src/zod/complex.zod.ts, recording that the deleted retiredZeroReadKanbanKey helper once carried this spelling on the SIBLING arm; - one row of content/docs/api/schema-reference.md; - the .changeset/ release notes that discuss it — this one, the two historical entries covering the sibling arm's own spelling, and objectui#9629's note recording the correction to this paragraph.
  • names packages/types/src/zod/complex.zod.ts → packages/types/src/zod/complex.zod.ts — edited by this change

    • the declarations retired here — packages/types/src/objectql.ts and its mirror packages/types/src/zod/objectql.zod.ts; - the pins that assert the retirement — object-kanban-allow-collapse-retired-8801.test.ts and bare-kanban-node-key-retired-8802.test.ts; - a comment in packages/types/src/zod/complex.zod.ts, recording that the deleted retiredZeroReadKanbanKey helper once carried this spelling on the SIBLING arm; - one row of content/docs/api/schema-reference.md; - the .changeset/ release notes that discuss it — this one, the two historical entries covering the sibling arm's own spelling, and objectui#9629's note recording the correction to this paragraph.

.changeset/8841-tree-view-config-derive.md

  • names scripts/check-spec-symbol-derivation.mjs → scripts/check-spec-symbol-derivation.mjs — edited by this change

    What was wrong. @object-ui/types published TreeViewConfig as a hand-written interface — a copy of the protocol's ListView.tree block under a second name — and the copy declared a fifth key, titleField. @objectstack/spec@17.4.0 refuses that key there by name: TreeConfigSchema is a strictObject since spec objectstack#15469 closed the .passthrough() window 17.3.0 left open. So this package's published face accepted what the contract rejects, and an author who followed @object-ui/types was refused at publish with Unrecognized key(s) on this tree configuration: 'titleField'. The copy was invisible to scripts/check-spec-symbol-derivation.mjs, which matches spec symbols BY NAME — a hand copy renamed away from the spec's symbol has nothing for its rule 1 to match (objectui#4592's recorded blind spot).

.changeset/8885-object-chart-drilldown-title-compareto.md

  • names packages/types/src/objectql.ts → packages/types/src/objectql.ts — edited by this change

    ObjectChart.tsx reads all three off schema, and until now neither published copy declared any of them: not the TS interface (packages/types/src/objectql.ts) and not the zod mirror (packages/types/src/zod/objectql.zod.ts). They rode BaseSchema's index signature / .passthrough() and arrived unvalidated. drillDown was the sharpest case — this component's registry inputs advertise it to the designer palette, and @objectstack/spec publishes ChartDrillDownSchema for exactly this carrier, so an author was offered a key that neither published shape mentioned.

  • names packages/types/src/zod/objectql.zod.ts → packages/types/src/zod/objectql.zod.ts — edited by this change

    ObjectChart.tsx reads all three off schema, and until now neither published copy declared any of them: not the TS interface (packages/types/src/objectql.ts) and not the zod mirror (packages/types/src/zod/objectql.zod.ts). They rode BaseSchema's index signature / .passthrough() and arrived unvalidated. drillDown was the sharpest case — this component's registry inputs advertise it to the designer palette, and @objectstack/spec publishes ChartDrillDownSchema for exactly this carrier, so an author was offered a key that neither published shape mentioned.

.changeset/8913-object-kanban-columns-declared.md

  • names objectql.ts → packages/types/src/objectql.ts — edited by this change

    What moved. ObjectKanbanSchema gains columns on both halves that move together — the TypeScript interface (objectql.ts) and its Zod mirror (zod/objectql.zod.ts). Retiring the bare kanban node type key (objectui#8802) removed the only face that judged a lane, and object-kanban had never declared the key, so it rode BaseSchema's [key: string]: any / .passthrough(): read by the renderer at three sites, named by no published face.

  • names zod/objectql.zod.ts → packages/types/src/zod/objectql.zod.ts — edited by this change

    What moved. ObjectKanbanSchema gains columns on both halves that move together — the TypeScript interface (objectql.ts) and its Zod mirror (zod/objectql.zod.ts). Retiring the bare kanban node type key (objectui#8802) removed the only face that judged a lane, and object-kanban had never declared the key, so it rode BaseSchema's [key: string]: any / .passthrough(): read by the renderer at three sites, named by no published face.

.changeset/8990-object-kanban-groupby-optional.md

  • names packages/types/src/objectql.ts → packages/types/src/objectql.ts — edited by this change

    @objectstack/spec declares the key optional — groupBy: z.string().optional() on ObjectKanbanPropsSchema — while this package required it on the TypeScript declaration (packages/types/src/objectql.ts) and on the Zod mirror (packages/types/src/zod/objectql.zod.ts). objectui was therefore narrower than the protocol on a published key: ObjectKanbanSchema.safeParse and safeValidateSchema refused an object-kanban node the protocol accepts, and such a node could not be annotated with its own type.

  • names packages/types/src/zod/objectql.zod.ts → packages/types/src/zod/objectql.zod.ts — edited by this change

    @objectstack/spec declares the key optional — groupBy: z.string().optional() on ObjectKanbanPropsSchema — while this package required it on the TypeScript declaration (packages/types/src/objectql.ts) and on the Zod mirror (packages/types/src/zod/objectql.zod.ts). objectui was therefore narrower than the protocol on a published key: ObjectKanbanSchema.safeParse and safeValidateSchema refused an object-kanban node the protocol accepts, and such a node could not be annotated with its own type.

.changeset/8992-user-actions-collapse-and-docblock.md

  • names objectql.zod.ts → packages/types/src/zod/objectql.zod.ts — edited by this change

    objectql.zod.ts's UserActionsSchema read stripImportedDefaults(Spec).extend({ group, hideFields, rowColor }), an extension that existed only because @objectstack/spec did not declare those three keys while normalizeListViewSchema folded objectui's legacy showGroup / showHideFields / showColor onto them. The protocol adopted all three in 17.3.0 (objectui#5435's ruling), so the extension is now a second local copy of a protocol declaration — the shape two faces start drifting from — and it collapses into the plain by-reference re-export its own note always said it would become.

.changeset/9012-core-spec-floor.md

  • names normalize-list-view.declaredSpecFloor-9012.test.ts → packages/core/src/utils/__tests__/normalize-list-view.declaredSpecFloor-9012.test.ts — edited by this change

    scripts/check-spec-range-floors.mjs was green before and after, and would be green at any floor here: its criterion is symbol PRESENCE, and UserActionsConfigSchema is exported by every version above. The floor is held instead by normalize-list-view.declaredSpecFloor-9012.test.ts, which carries firing controls proving its comparator can redden.

.changeset/9040-retire-record-details-layout.md

  • names packages/types/src/__tests__/record-highlights-layout-9187.test.ts → packages/types/src/__tests__/record-highlights-layout-9187.test.ts — edited by this change

    • packages/types/src/__tests__/p1-spec-alignment.test.ts wrote layout: 'stacked' on this interface and read it back. Both lines are gone, with the reason stated at the site and a live-key read-back put in their place so the leg is not quietly shrunk. Its RecordHighlightsComponentProps neighbour keeps its own layout — a different key on a different face. - packages/types/src/__tests__/record-details-top-level-9040.test.ts ledgered the divergence as OPEN. Those legs become the retirement's own: a keyof absence assertion (with a declared key read through the identical form as its control), a @ts-expect-error literal, and a new source-text leg. - packages/types/src/__tests__/record-highlights-layout-9187.test.ts read this declaration as TEXT, using its three-value layout as the lit control for a two-member assertion about the sibling. That control MOVED to RecordChatterComponentProps.position — a three-value union the contract genuinely declares — as that file's own instruction required. It was never to be deleted outright, and it is not.

.changeset/9067-zod-barrel-named-arms.md

  • names zod/form.zod.ts → packages/types/src/zod/form.zod.ts — edited by this change

    • InputShorthandSchema (zod/form.zod.ts) — the email / password shorthand arm. - UiCalendarSchema (zod/form.zod.ts) — ui:calendar, the date-picker primitive renderers/form/calendar.tsx registers, a different component from the calendar plugin view that owns the bare literal.

.changeset/9092-inline-locale-declared-face.md

  • names objectql.ts → packages/types/src/objectql.ts — edited by this change

    • AppComponentSchema.label (app.ts) - ObjectGridSchema.label and .description (objectql.ts) - PageNodeSchema.aria.ariaLabel (layout.ts)

.changeset/9109-flow-node-unbacked-defaults.md

  • names flow-node-config.spec-reconciliation.test.ts → packages/app-shell/src/views/metadata-admin/inspectors/flow-node-config.spec-reconciliation.test.ts — edited by this change

    flow-node-config.spec-reconciliation.test.ts's unbacked register is now empty, and its re-measurement was rewritten to walk the scope table rather than the register's own rows — written the old way, an emptied register contributes no case, runs nothing and still reports green.

.changeset/9187-record-highlights-layout-two-values.md

  • names record-highlights-layout-9187.test.ts → packages/types/src/__tests__/record-highlights-layout-9187.test.ts — edited by this change

    ⚠️ The layout one interface up, on RecordDetailsComponentProps, is a different divergence and was not touched by this change: the contract refuses that key by name, and its removal was objectui#9040's. Copying either declaration onto the other is refused at publish. The union landed here is pinned against the installed spec, in both directions, in record-highlights-layout-9187.test.ts.

.changeset/9278-end-outcome-select.md

  • names packages/app-shell/src/views/metadata-admin/inspectors/flow-node-config.ts → packages/app-shell/src/views/metadata-admin/inspectors/flow-node-config.ts — edited by this change

    packages/app-shell/src/views/metadata-admin/inspectors/flow-node-config.ts declared the end group's outcome key as a free-text box with placeholder: 'success · failure'. EndConfigSchema.outcome is a closed enum of completed | refused defaulting to completed, and FlowNodeSchema discriminates an end node's config through it — so both printed words are refused at parse, not ignored at run time. Measured against the installed @objectstack/spec (17.4.0), with the accepted row in the same output so the refusals are a reading rather than a dead probe:

  • names FlowNodeInspector.declaredDefault.test.tsx → packages/app-shell/src/views/metadata-admin/inspectors/FlowNodeInspector.declaredDefault.test.tsx — edited by this change

    The control is now a select whose options are exactly the spec's enum, declaring defaultValue: 'completed' so an unset key states on the trigger what the runtime applies to it, and the invented placeholder is gone. Both are derived from the installed spec, as FlowConfigField.defaultValue's doc comment requires of a declaration outside the escalation ledger, and both are reconciled against EndConfigSchema in FlowNodeInspector.declaredDefault.test.tsx — through zod's public toJSONSchema rather than a respelled literal, so the claim cannot quietly rot at the next spec bump. The zh-CN overlay gains the two option labels and the help line.

.changeset/9280-record-highlights-entry-icon-retired.md

  • names packages/types/src/record-components.ts → packages/types/src/record-components.ts — edited by this change

    • packages/types/src/record-components.ts — RecordHighlightsComponentProps.fields[]'s object arm: { name; label?; icon?; type?; readonly? } → { name; label?; type?; readonly? }. The key is removed, not tombstoned: the contract's arm is $strict, so the refusal an author needs already exists upstream and arrives named (invalid_union at the entry). A ?: never tombstone buys nothing here — it is the remedy for a non-strict mirror that would otherwise strip in silence, which is not this arm. - packages/plugin-detail/src/renderers/record-highlights.tsx — the entry normalizer stops copying icon: f?.icon into the normalized entry. That read was unreachable, not merely unused: no author could feed it past the $strict arm, and HeaderHighlight renders no .icon on the far side either, so the copy had no consumer in either direction. - packages/plugin-detail/src/index.tsx — the registry manifest's fields input description sketched the entry as {name,label?,icon?,type?,readonly?} → {name,label?,type?,readonly?}. The inputs ARE the published contract (gen-manifest.ts serializes them into sdui.manifest.json and sdui-intrinsics.d.ts), so leaving the sketch standing would have gone on teaching AI and human authors a key that gets the whole document refused at publish.
  • names packages/plugin-detail/src/index.tsx → packages/plugin-detail/src/index.tsx — edited by this change

    • packages/types/src/record-components.ts — RecordHighlightsComponentProps.fields[]'s object arm: { name; label?; icon?; type?; readonly? } → { name; label?; type?; readonly? }. The key is removed, not tombstoned: the contract's arm is $strict, so the refusal an author needs already exists upstream and arrives named (invalid_union at the entry). A ?: never tombstone buys nothing here — it is the remedy for a non-strict mirror that would otherwise strip in silence, which is not this arm. - packages/plugin-detail/src/renderers/record-highlights.tsx — the entry normalizer stops copying icon: f?.icon into the normalized entry. That read was unreachable, not merely unused: no author could feed it past the $strict arm, and HeaderHighlight renders no .icon on the far side either, so the copy had no consumer in either direction. - packages/plugin-detail/src/index.tsx — the registry manifest's fields input description sketched the entry as {name,label?,icon?,type?,readonly?} → {name,label?,type?,readonly?}. The inputs ARE the published contract (gen-manifest.ts serializes them into sdui.manifest.json and sdui-intrinsics.d.ts), so leaving the sketch standing would have gone on teaching AI and human authors a key that gets the whole document refused at publish.

.changeset/9309-object-gallery-filter-destination-typed.md

  • names objectql.ts → packages/types/src/objectql.ts — edited by this change

    ObjectGallerySchema.filter is typed as the destination its own docblock names — QueryParams['$filter'] — on both faces, the TS interface in objectql.ts and the zod mirror in zod/objectql.zod.ts (objectui#9309).

  • names zod/objectql.zod.ts → packages/types/src/zod/objectql.zod.ts — edited by this change

    ObjectGallerySchema.filter is typed as the destination its own docblock names — QueryParams['$filter'] — on both faces, the TS interface in objectql.ts and the zod mirror in zod/objectql.zod.ts (objectui#9309).

.changeset/9491-walkabledef-rest-null.md

  • names packages/types/src/zod/node-derivation.ts → packages/types/src/zod/node-derivation.ts — edited by this change

    packages/types/src/zod/node-derivation.ts declares the def member set both zod walkers in this package read. It declared rest?: z.ZodType — i.e. z.ZodType | undefined — while zod 4 spells "this tuple has no rest element" as an OWN rest key holding null, minted by const rest = hasRest ? _paramsOrRest : null in its tuple factory.

  • names zod/imported-defaults.ts → packages/types/src/zod/imported-defaults.ts — edited by this change

    This is the declaration, not a behaviour change. Nothing here changes what either walker does with the value; the accept set of every exported schema is untouched. objectui#9088 already repaired the one arm the inaccurate type misled — the tuple arm in zod/imported-defaults.ts, which normalised the absent case to undefined because the declared type said that was the absent case, and so rebuilt every rest-less tuple through a === comparison that could never match. This change corrects the type that licensed it, so the next arm written against it is told the truth and tsc agrees with the truth instead of with the mistake.

.changeset/9511-record-id-is-a-string.md

  • names objectql.ts → packages/types/src/objectql.ts — edited by this change

    The three authorable keys, each on BOTH faces. ObjectFormSchema.recordId (objectql.ts + zod/objectql.zod.ts), DetailViewSchema.resourceId (views.ts + zod/views.zod.ts) and DetailSchema.resourceId (crud.ts + zod/crud.zod.ts). ⚠️ The crud pair is DetailSchema, not DetailViewSchema, and it reaches the same renderer — not by symbol but by data flow: plugin-detail registers the 'detail' node type onto DetailView. A read that follows TypeScript symbols alone finds two keys and is incomplete.

  • names zod/objectql.zod.ts → packages/types/src/zod/objectql.zod.ts — edited by this change

    The three authorable keys, each on BOTH faces. ObjectFormSchema.recordId (objectql.ts + zod/objectql.zod.ts), DetailViewSchema.resourceId (views.ts + zod/views.zod.ts) and DetailSchema.resourceId (crud.ts + zod/crud.zod.ts). ⚠️ The crud pair is DetailSchema, not DetailViewSchema, and it reaches the same renderer — not by symbol but by data flow: plugin-detail registers the 'detail' node type onto DetailView. A read that follows TypeScript symbols alone finds two keys and is incomplete.

  • names views.ts → packages/types/src/views.ts — edited by this change

    The three authorable keys, each on BOTH faces. ObjectFormSchema.recordId (objectql.ts + zod/objectql.zod.ts), DetailViewSchema.resourceId (views.ts + zod/views.zod.ts) and DetailSchema.resourceId (crud.ts + zod/crud.zod.ts). ⚠️ The crud pair is DetailSchema, not DetailViewSchema, and it reaches the same renderer — not by symbol but by data flow: plugin-detail registers the 'detail' node type onto DetailView. A read that follows TypeScript symbols alone finds two keys and is incomplete.

  • names zod-mirror-parity.test.ts → packages/types/src/__tests__/zod-mirror-parity.test.ts — edited by this change

    ⭐ The declaration alone would not have been enough, and this is the reusable part. A TypeScript declaration does not run at parse time. The hand-written zod mirror is the only face in this repository that can refuse an authored number, so narrowing the declaration without the mirror would have shipped declared !== enforced on a published surface — and zod-mirror-parity.test.ts would have stayed GREEN through it, because that instrument asserts a mirror accepts everything its declaration declares and a mirror left WIDER passes. Both faces moved together for that reason.

.changeset/9549-tree-filter-declared.md

  • names objectql.ts → packages/types/src/objectql.ts — edited by this change

    ObjectTreeSchema.filter is declared on both faces, in the shape objectui#9309 settled for ObjectGallerySchema.filter: QueryParams['$filter'] by indexed access on the TS interface in objectql.ts, and the same two-arm union (array first) on the zod mirror in zod/objectql.zod.ts (objectui#9549).

  • names zod/objectql.zod.ts → packages/types/src/zod/objectql.zod.ts — edited by this change

    ObjectTreeSchema.filter is declared on both faces, in the shape objectui#9309 settled for ObjectGallerySchema.filter: QueryParams['$filter'] by indexed access on the TS interface in objectql.ts, and the same two-arm union (array first) on the zod mirror in zod/objectql.zod.ts (objectui#9549).

.changeset/9550-object-tree-root-barrel.md

  • names objectql.ts → packages/types/src/objectql.ts — edited by this change

    ObjectQLComponentSchema declares the node types an ObjectQL block may be. Every one of its arms was a named export of this package's root barrel except ObjectTreeSchema, which was declared in objectql.ts, applied by the union, and re-exported by the ./zod barrel (objectui#7917) — while no TypeScript consumer could name it. There is no ./objectql subpath to reach around the barrel: the package's exports map is pinned by packages/types/src/__tests__/package-exports-manifest.test.ts, and the root barrel was the only route to this type.

.changeset/9550-plugin-tree-import-object-tree-schema.md

  • names ObjectTree.schemaTyped-8655.test.ts → packages/plugin-tree/src/ObjectTree.schemaTyped-8655.test.ts — edited by this change

    Nothing about the type changes — the imported name and the narrowing resolve to the same declaration, which ObjectTree.schemaTyped-8655.test.ts asserts invariantly and independently through ObjectTreeProps['schema']. What changes is that the renderer no longer carries a derived restatement of a published type. A type nobody can import mints one of those per consumer, and each one is correct on the day it is written — that is the second-authority shape objectui#6349 is burning down, and this was one of its instances rather than a tidy-up.

.changeset/9606-object-kanban-card-title.md

  • names zod/objectql.zod.ts → packages/types/src/zod/objectql.zod.ts — edited by this change

    Both published faces of the object-kanban arm now name the key: the zod mirror ObjectKanbanSchema in zod/objectql.zod.ts and its TypeScript twin, the ObjectKanbanSchema interface in objectql.ts. Both declare it OPTIONAL, at the same requiredness the other face uses, so the two faces accept and refuse the same documents. (Located and cited by SYMBOL: line addresses in zod/objectql.zod.ts have drifted before, and this change is itself about a drifted mirror.)

  • names objectql.ts → packages/types/src/objectql.ts — edited by this change

    Both published faces of the object-kanban arm now name the key: the zod mirror ObjectKanbanSchema in zod/objectql.zod.ts and its TypeScript twin, the ObjectKanbanSchema interface in objectql.ts. Both declare it OPTIONAL, at the same requiredness the other face uses, so the two faces accept and refuse the same documents. (Located and cited by SYMBOL: line addresses in zod/objectql.zod.ts have drifted before, and this change is itself about a drifted mirror.)

.changeset/9628-kanban-column-collapsed-honoured.md

  • names objectql.ts → packages/types/src/objectql.ts — edited by this change

    The key was declared on both published faces of the object-kanban arm — the lane element of ObjectKanbanSchema (objectql.ts and its Zod mirror) and the runtime lane KanbanColumn (complex.ts and its mirror) — and read by KanbanEnhanced alone, a module no production source imports. An authored { "id": "todo", "title": "To Do", "collapsed": true } therefore parsed green on both faces and reached a board that did nothing with it: KanbanImpl's only collapse is the SWIMLANE row's, held in viewer state under objectui:kanban-collapsed:ANGLE-BRACKETS(swimlaneField) and never keyed to a lane's declared value. That is the ADR-0049 declared-but-unhonoured shape.

.changeset/9880-undrawable-view-kinds-both-legs.md

  • names normalize-list-view.ts → packages/core/src/utils/normalize-list-view.ts — edited by this change

    Compiled both ways rather than argued. With the published pin @objectstack/spec@17.4.0 installed, turbo run type-check --filter=@object-ui/core exits 0 before and after this change. With a spec built from objectstack main injected in its place, the same command reported the TS2353 in normalize-list-view.ts before and does not report it after; the diagnostics that remain on that leg come from other files, are reproduced against the pristine source, and are not this change's.

  • names normalize-list-view.undrawableBothLegs-9880.test.ts → packages/core/src/utils/__tests__/normalize-list-view.undrawableBothLegs-9880.test.ts — edited by this change

    normalize-list-view.undrawableBothLegs-9880.test.ts pins all of it over a SIMULATED vocabulary, because any single run installs only one spec: the retirement direction, the addition direction, and the old spelling's TS2353 kept as a firing control.

.changeset/action-callback-retired-7068.md

  • names ui-action.ts → packages/types/src/ui-action.ts — edited by this change

    Where the live meaning lives. Post-success navigation is the spec's onSuccess block, { navigate, openIn }, declared on UIActionSchema (ui-action.ts) and forwarded to the runner (objectui#5934). A success or failure notice is successMessage / errorMessage — adjacent keys on the same legacy ActionSchema, NOT retired, and still accepted on both faces.

.changeset/console-form-container-specs-one-declaration-5596.md

  • names scripts/check-spec-symbol-derivation.mjs → scripts/check-spec-symbol-derivation.mjs — edited by this change

    Both types are therefore derived from the spec's own FormSection / FormView with named narrowings — the repo's sanctioned form for a spec-shaped local type (scripts/check-spec-symbol-derivation.mjs) — rather than restated. Every key the two layers agree on comes from the spec and cannot fall behind it; the four positions where this layer is deliberately narrower are each named in an Omit list and restated once next to its reason: fields keeps the converged 26-key leaf (deriving it would silently re-open [finding] A THIRD inline copy of the form-field authoring contract lives in apps/console FormPage.tsx — objectui#5040 converged only the app-shell two #5542), and label / description / visibleWhen / visibleOn keep the shapes this repo's renderers and evaluators actually consume rather than the spec's I18nLabel and ExpressionInput. apps/console's submitBehavior union — previously hand-written under the comment "Mirrors the spec FormView.submitBehavior union" — is now read back off the shared type, making the mirror structural. @object-ui/app-shell re-exports both names from its package root (type-only, erased at build — nothing is added to the bundle), because a type that cannot be imported is a type that gets retyped.

.changeset/host-dispatch-action-keys-5611.md

  • names actions/actionKeys.ts → packages/core/src/actions/actionKeys.ts — edited by this change

    Adds an exported HOST_DISPATCH_ACTION_KEYS (sole member overrideNotice) to actions/actionKeys.ts and unions it into KNOWN_ACTION_KEYS, which is the fourth input to that set and the first one that is not an authored-surface mirror. Measured before and after on the exact dispatch the bar composes: the warning went from one call naming overrideNotice to none, KNOWN_ACTION_KEYS grew by exactly one member, and an action carrying a real typo alongside it still warns — naming targt only.

.changeset/i18nlabel-inverted-pin-5612.md

  • names ui-action.ts → packages/types/src/ui-action.ts — edited by this change

    The I18nLabel "inverted pin" now watches the premise it claims to watch, and ui-action.ts no longer imports a symbol it never uses (objectui#5612, objectui#5613).

.changeset/listview-comment-pair-4559.md

  • names scripts/check-spec-symbol-derivation.mjs → scripts/check-spec-symbol-derivation.mjs — edited by this change

    objectui#4966 — formatActionLabel's docblock now sits above formatActionLabel. It had drifted two declarations up, so the exported parseSortConfig carried two stacked leading comments and the helper carried none. This one was not cosmetic: because parseSortConfig is exported, vite-plugin-dts copied the misattributed block into dist/ListView.d.ts, so every consumer's editor hover and TypeDoc introduced the sort parser with a sentence about action labels. Moving the block removes it from the .d.ts; formatActionLabel is module-private, so its now-correct docblock does not appear there. It also matters to scripts/check-spec-symbol-derivation.mjs, whose rule 2 reads the comment block attached to a declaration — a misattributed docblock is the mechanism by which a claim gets scored against the wrong symbol. This block carries no spec-alignment phrase, so nothing fired today.

.changeset/object-view-unmirrored-keys-7779.md

  • names packages/plugin-view/src/ObjectView.tsx → packages/plugin-view/src/ObjectView.tsx — edited by this change

    What was measured. Every reading was taken on the object-view node renderer (packages/plugin-view/src/ObjectView.tsx, registered by plugin-view/src/index.tsx) with schema.objectName / schema.layout as the positive controls of the same schema.KEY query, so each zero is a reading; the repo-wide census of viewTabBar finds the key in no source file outside @object-ui/types (two doc tables listed it as authorable and are corrected here). The spec side was read through the installed pin (@objectstack/spec@17.2.0, ui entry, 117 exported object schemas walked; control keys objectName / columns / navigation / listViews hit): the three spec-modelled keys are optional slots on ListViewSchema and ObjectListViewSchema; the six local keys have no spec slot anywhere.

  • names object-view-unmirrored-keys-7779.test.ts → packages/types/src/__tests__/object-view-unmirrored-keys-7779.test.ts — edited by this change

    ⚠️ Dated note, 2026-09-28 — the value type has since been decided, and listViews is mirrored by reference — objectui#7928. Later in this same release the maintainer's ruling A made ObjectViewSchema.listViews the protocol's own named-view record, by reference: on both faces its value is @objectstack/spec's strict ObjectListViewSchema, and a NamedListView is not assignable to it. The docs have taught that shape since objectui#8255. So "listViews stays unmirrored", "The declaration's value is the local NamedListView", the key "stays in the parity ledger … until the maintainer decides its value type" and the table's "still unmirrored" above no longer hold: the ObjectViewSchema entry has left UnmirroredDeclared. NamedListView stays exported with its 64 declared members. The renderer reads some of them off a named view and leaves the rest unread, and the split is no longer the 21 and 43 this entry counted: objectui#7928 stopped reading the options bag, which the record refuses, and objectui#10758 made the host delegation read a named view's remaining protocol members off the named view. The census in object-view-unmirrored-keys-7779.test.ts pins the current figures. .changeset/7928-listviews-by-reference-fold.md (PR objectui#10821) states what ships; the text above is kept as the reading of this change.

  • names zod-mirror-parity.test.ts → packages/types/src/__tests__/zod-mirror-parity.test.ts — edited by this change

    Who is NOT affected: every correctly typed document, and every document that never wrote these keys — absent stays valid on all nine. No renderer changed. The parity ledger (zod-mirror-parity.test.ts) records the move: UnmirroredDeclared 14 entries / 96 keys to 14 / 87, the ObjectViewSchema entry re-derived into the SPEC-DERIVED half because the mirror now references the spec in code.

.changeset/param-dialog-title-fallback-4282.md

  • names ui-action.ts → packages/types/src/ui-action.ts — edited by this change

    title is declared on no action surface in the ecosystem: it is absent from @objectstack/spec's ActionSchema (44 keys walked at spec 17.0.0), from @object-ui/core's ActionDef and its pinned ACTION_DEF_KEYS / SPEC_ACTION_KEYS inventories, and from @object-ui/types' renderer view (ui-action.ts) and crud.ts ActionSchema / BaseSchema. None of the four action renderers — action:button, action:icon, action:group, action:menu — forwards it either. So the right-hand side of that || could not be reached by authored metadata: a fallback that cannot fire, which is the "declared is not enforced" shape objectstack#4075 exists to reduce. Nothing a user hits changes; the line now reads exactly one key, matching the description line directly below it.

.changeset/record-alert-cta-label-i18n-4998.md

.changeset/record-detail-param-dialog-title-5610.md

  • names ui-action.ts → packages/types/src/ui-action.ts — edited by this change

    title is declared on no action surface in the ecosystem: it is absent from @objectstack/spec's ActionSchema (44 keys walked at spec 17.0.0), from @object-ui/core's ActionDef and its pinned ACTION_DEF_KEYS / SPEC_ACTION_KEYS inventories, and from @object-ui/types' renderer view (ui-action.ts) and crud.ts ActionSchema / BaseSchema. None of the four action renderers — action:button, action:icon, action:group, action:menu — forwards it either. So the right-hand side of that || could not be reached by authored metadata: a fallback that cannot fire, which is the "declared is not enforced" shape objectstack#4075 exists to reduce. Nothing a user hits changes; the line now reads exactly one key, matching the description line directly below it.

Read the paragraph, not the line: both false halves of the objectui#8617 claim sat in one paragraph, and correcting either alone would have left it asserting the same wrong thing.

If a claim did go false, correct the body. That is precedented and prose-only, frontmatter untouched; check-changeset-overwrite.mjs will report the correction as its own case 2 ("correcting a declaration on purpose … legitimate"), which is the intended shape — one gate asks for the read, the other records the write.

Not covered, stated so nobody reads this as more: a born-false claim that spells no line address at all (objectui#9495 coordinated one by ORDINAL — "a grep finds that member first" — and deciding that means reading what the sentence means), a claim spelled as a symbol or a package rather than a backticked file name, and a file named ambiguously.

Angle-bracketed names in the quoted prose above are rewritten as ANGLE-BRACKETS(name): GitHub deletes tag-shaped fragments from a stored body, and a quote that silently loses the identifier it is about is worse than a visible repair.

Compared the checked-out tree with c2a8d23c6 (merge-base with origin/main): 114 file(s) changed outside .changeset/, read against 1776 pending declaration(s) that publish a body (2382 pending in total). · run

@github-actions

Copy link
Copy Markdown
Contributor

❌ Console Performance Budget

Metric Value Budget
Eager closure (gzip, 330 chunks) 3563.2 KB 3149.4 KB
Main entry chunk (gzip) 149.3 KB 350 KB
Entry file index-CUM63uDC.js —
Status FAIL —

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:

Eager-closure half Verdict
Aggregate closure ceiling ❌ over its ceiling
Per-chunk ceilings ❌ over its ceiling
Per-chunk membership (declared packages) ✅ pass
Ceiling sensitivity (headroom) ✅ pass
Ceiling freshness (checkout vs. base branch) ✅ pass

📦 Bundle Size Report

Package Size Gzipped
app-shell (consoleActionDispatch.js) 0.20KB 0.19KB
app-shell (index.js) 16.88KB 6.25KB
app-shell (runtime-config.js) 20.68KB 7.36KB
app-shell (types.js) 0.01KB 0.04KB
app-shell (urlParams.js) 10.11KB 3.87KB
auth (ActiveOrganizationStorage.js) 27.95KB 10.04KB
auth (AuthContext.js) 0.31KB 0.24KB
auth (AuthGuard.js) 2.07KB 1.00KB
auth (AuthProvider.js) 40.22KB 10.61KB
auth (AuthShell.js) 3.49KB 1.40KB
auth (ForgotPasswordForm.js) 12.21KB 3.45KB
auth (LoginForm.js) 18.17KB 5.40KB
auth (PreviewBanner.js) 0.90KB 0.50KB
auth (RegisterForm.js) 6.72KB 2.24KB
auth (SocialSignInButtons.js) 9.70KB 3.93KB
auth (UserMenu.js) 3.39KB 1.21KB
auth (auth-gate-events.js) 1.29KB 0.66KB
auth (authStyles.js) 5.04KB 1.72KB
auth (createAuthClient.js) 40.70KB 10.94KB
auth (createAuthenticatedFetch.js) 8.54KB 3.46KB
auth (index.js) 3.63KB 1.64KB
auth (invitation-status.js) 1.22KB 0.70KB
auth (org-roles.js) 6.66KB 2.78KB
auth (phone-identifier.js) 1.11KB 0.66KB
auth (types.js) 0.59KB 0.35KB
auth (useAuth.js) 5.30KB 1.02KB
auth (useWorkspaceAdminStatus.js) 11.08KB 4.58KB
collaboration (CommentThread.js) 27.13KB 7.95KB
collaboration (LiveCursors.js) 3.17KB 1.27KB
collaboration (PresenceAvatars.js) 6.49KB 2.64KB
collaboration (PresenceProvider.js) 2.79KB 1.13KB
collaboration (index.js) 1.68KB 0.73KB
collaboration (useCollaborationTranslation.js) 6.05KB 2.52KB
collaboration (useCommentSearch.js) 1.98KB 0.88KB
collaboration (useConflictResolution.js) 7.75KB 1.86KB
collaboration (useMentionNotifications.js) 1.81KB 0.68KB
collaboration (usePresence.js) 6.33KB 1.84KB
collaboration (useRealtimeSubscription.js) 7.91KB 2.01KB
components (index.js) 558.78KB 133.92KB
core (index.js) 9.94KB 3.94KB
create-plugin (index.js) 27.94KB 9.51KB
data-objectstack (index.js) 227.99KB 63.22KB
fields (index.js) 261.01KB 66.28KB
i18n (LocalizationContext.js) 1.76KB 0.96KB
i18n (builtinAggregateLabels.js) 0.86KB 0.49KB
i18n (currency.js) 2.59KB 1.22KB
i18n (fallbackInterpolation.js) 6.25KB 2.77KB
i18n (i18n.js) 8.87KB 3.64KB
i18n (index.js) 5.24KB 2.27KB
i18n (pickLocalized.js) 9.86KB 3.95KB
i18n (provider.js) 39.40KB 12.91KB
i18n (translateFn.js) 0.20KB 0.18KB
i18n (useDisplayLocale.js) 3.52KB 1.76KB
i18n (useObjectLabel.js) 34.35KB 9.18KB
i18n (useSafeTranslation.js) 5.60KB 2.33KB
layout (index.js) 39.32KB 11.09KB
mobile (MobileProvider.js) 0.92KB 0.49KB
mobile (ResponsiveContainer.js) 0.94KB 0.38KB
mobile (breakpoints.js) 1.51KB 0.70KB
mobile (createOfflineDataSource.js) 5.61KB 1.75KB
mobile (index.js) 1.99KB 0.87KB
mobile (offlineQueue.js) 3.91KB 1.35KB
mobile (pwa.js) 0.97KB 0.49KB
mobile (serviceWorker.js) 1.48KB 0.62KB
mobile (serviceWorkerSource.js) 3.41KB 1.48KB
mobile (useBreakpoint.js) 1.54KB 0.65KB
mobile (useGesture.js) 6.96KB 1.98KB
mobile (useOfflineSync.js) 1.99KB 0.72KB
mobile (usePullToRefresh.js) 6.62KB 2.45KB
mobile (useResponsive.js) 0.72KB 0.42KB
mobile (useSpecGesture.js) 5.52KB 2.10KB
mobile (useTouchTarget.js) 1.01KB 0.54KB
permissions (MePermissionsProvider.js) 13.58KB 4.90KB
permissions (PermissionContext.js) 0.31KB 0.25KB
permissions (PermissionGuard.js) 0.89KB 0.45KB
permissions (PermissionProvider.js) 6.25KB 2.17KB
permissions (discardProofCache.js) 1.04KB 0.55KB
permissions (evaluator.js) 8.33KB 3.07KB
permissions (index.js) 0.93KB 0.41KB
permissions (store.js) 0.91KB 0.42KB
permissions (useFieldPermissions.js) 1.28KB 0.53KB
permissions (usePermissions.js) 4.83KB 2.27KB
plugin-ai (index.js) 16.01KB 3.93KB
plugin-calendar (index.js) 52.33KB 15.01KB
plugin-charts (index.js) 84.09KB 22.93KB
plugin-chatbot (index.js) 198.22KB 46.97KB
plugin-dashboard (index.js) 137.68KB 36.71KB
plugin-designer (index.js) 215.78KB 44.42KB
plugin-detail (index.js) 233.54KB 61.80KB
plugin-editor (index.js) 2.23KB 1.05KB
plugin-form (index.js) 169.60KB 43.32KB
plugin-gantt (index.js) 171.88KB 42.61KB
plugin-grid (index.js) 228.33KB 62.59KB
plugin-kanban (index.js) 48.43KB 15.11KB
plugin-list (index.js) 115.86KB 28.64KB
plugin-map (index.js) 22.90KB 7.62KB
plugin-markdown (index.js) 13.88KB 4.80KB
plugin-report (index.js) 44.04KB 12.21KB
plugin-timeline (index.js) 31.07KB 9.15KB
plugin-tree (index.js) 11.21KB 3.89KB
plugin-view (index.js) 90.43KB 22.76KB
providers (DataSourceProvider.js) 0.75KB 0.39KB
providers (MetadataProvider.js) 1.37KB 0.59KB
providers (ThemeProvider.js) 1.90KB 0.85KB
providers (UploadProvider.js) 11.81KB 3.58KB
providers (index.js) 0.45KB 0.23KB
providers (types.js) 0.01KB 0.04KB
react-runtime (index.js) 5.62KB 2.34KB
react (LazyPluginLoader.js) 4.47KB 1.63KB
react (SchemaRenderer.js) 119.55KB 39.23KB
react (data-invalidation.js) 5.05KB 2.08KB
react (index.js) 4.50KB 2.06KB
react (schema-input.js) 4.25KB 2.04KB
react (spec-input.js) 0.20KB 0.18KB
sdui-parser (body-dialect.js) 4.78KB 2.09KB
sdui-parser (codegen.js) 8.61KB 3.45KB
sdui-parser (dashboard-widget-options.js) 3.08KB 1.30KB
sdui-parser (index.js) 6.17KB 2.73KB
sdui-parser (input-type.js) 2.84KB 1.40KB
sdui-parser (kanban-quick-add.js) 3.89KB 1.87KB
sdui-parser (parse.js) 25.28KB 7.80KB
sdui-parser (provenance.js) 3.84KB 1.90KB
sdui-parser (types.js) 0.28KB 0.23KB
sdui-parser (validate.js) 22.61KB 7.40KB
types (ai.js) 4.39KB 2.17KB
types (api-types.js) 0.20KB 0.18KB
types (app.js) 3.83KB 1.49KB
types (base.js) 0.20KB 0.18KB
types (blocks.js) 0.20KB 0.18KB
types (complex.js) 3.19KB 1.62KB
types (crud.js) 0.20KB 0.18KB
types (dashboard-filter-alias.js) 6.23KB 2.74KB
types (data-display.js) 3.75KB 1.85KB
types (data-protocol.js) 0.20KB 0.19KB
types (data.js) 0.20KB 0.18KB
types (designer.js) 1.85KB 0.85KB
types (disclosure.js) 0.20KB 0.18KB
types (error-code.js) 1.54KB 0.88KB
types (expression.js) 0.20KB 0.18KB
types (feedback.js) 0.20KB 0.18KB
types (field-types.js) 0.20KB 0.18KB
types (form.js) 0.20KB 0.18KB
types (http-inflight.js) 8.87KB 3.73KB
types (http-retry.js) 4.32KB 2.02KB
types (icon-key-migration.js) 4.26KB 1.63KB
types (index.js) 4.74KB 2.26KB
types (layout.js) 0.20KB 0.18KB
types (managed-by.js) 0.19KB 0.18KB
types (mobile.js) 5.00KB 2.39KB
types (navigation.js) 0.20KB 0.18KB
types (objectql.js) 0.20KB 0.18KB
types (overlay.js) 0.20KB 0.18KB
types (permissions.js) 2.52KB 1.31KB
types (plugin-scope.js) 0.20KB 0.18KB
types (record-components.js) 0.20KB 0.19KB
types (record-semantics.js) 1.28KB 0.67KB
types (registry.js) 0.20KB 0.18KB
types (reports.js) 0.20KB 0.18KB
types (select-option.js) 0.20KB 0.19KB
types (spec-report.js) 5.05KB 1.93KB
types (spec-ui-namespace.js) 0.20KB 0.19KB
types (strict-authoring-face.js) 19.30KB 6.99KB
types (system-fields.js) 3.33KB 1.54KB
types (theme.js) 6.28KB 2.87KB
types (ui-action.js) 8.11KB 3.32KB
types (views.js) 0.20KB 0.18KB
types (widget.js) 0.20KB 0.18KB

Size Limits

  • ✅ Core packages should be < 50KB gzipped
  • ✅ Component packages should be < 100KB gzipped
  • ⚠️ Plugin packages should be < 150KB gzipped

…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>
@github-actions

Copy link
Copy Markdown
Contributor

❌ Console Performance Budget

Metric Value Budget
Eager closure (gzip, 330 chunks) 3563.5 KB 3149.4 KB
Main entry chunk (gzip) 149.3 KB 350 KB
Entry file index-kinSCXlb.js —
Status FAIL —

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:

Eager-closure half Verdict
Aggregate closure ceiling ❌ over its ceiling
Per-chunk ceilings ❌ over its ceiling
Per-chunk membership (declared packages) ✅ pass
Ceiling sensitivity (headroom) ✅ pass
Ceiling freshness (checkout vs. base branch) ✅ pass

📦 Bundle Size Report

Package Size Gzipped
app-shell (consoleActionDispatch.js) 0.20KB 0.19KB
app-shell (index.js) 16.88KB 6.25KB
app-shell (runtime-config.js) 20.68KB 7.36KB
app-shell (types.js) 0.01KB 0.04KB
app-shell (urlParams.js) 10.11KB 3.87KB
auth (ActiveOrganizationStorage.js) 27.95KB 10.04KB
auth (AuthContext.js) 0.31KB 0.24KB
auth (AuthGuard.js) 2.07KB 1.00KB
auth (AuthProvider.js) 40.22KB 10.61KB
auth (AuthShell.js) 3.49KB 1.40KB
auth (ForgotPasswordForm.js) 12.21KB 3.45KB
auth (LoginForm.js) 18.17KB 5.40KB
auth (PreviewBanner.js) 0.90KB 0.50KB
auth (RegisterForm.js) 6.72KB 2.24KB
auth (SocialSignInButtons.js) 9.70KB 3.93KB
auth (UserMenu.js) 3.39KB 1.21KB
auth (auth-gate-events.js) 1.29KB 0.66KB
auth (authStyles.js) 5.04KB 1.72KB
auth (createAuthClient.js) 40.70KB 10.94KB
auth (createAuthenticatedFetch.js) 8.54KB 3.46KB
auth (index.js) 3.63KB 1.64KB
auth (invitation-status.js) 1.22KB 0.70KB
auth (org-roles.js) 6.66KB 2.78KB
auth (phone-identifier.js) 1.11KB 0.66KB
auth (types.js) 0.59KB 0.35KB
auth (useAuth.js) 5.30KB 1.02KB
auth (useWorkspaceAdminStatus.js) 11.08KB 4.58KB
collaboration (CommentThread.js) 27.13KB 7.95KB
collaboration (LiveCursors.js) 3.17KB 1.27KB
collaboration (PresenceAvatars.js) 6.49KB 2.64KB
collaboration (PresenceProvider.js) 2.79KB 1.13KB
collaboration (index.js) 1.68KB 0.73KB
collaboration (useCollaborationTranslation.js) 6.05KB 2.52KB
collaboration (useCommentSearch.js) 1.98KB 0.88KB
collaboration (useConflictResolution.js) 7.75KB 1.86KB
collaboration (useMentionNotifications.js) 1.81KB 0.68KB
collaboration (usePresence.js) 6.33KB 1.84KB
collaboration (useRealtimeSubscription.js) 7.91KB 2.01KB
components (index.js) 558.78KB 133.92KB
core (index.js) 9.94KB 3.94KB
create-plugin (index.js) 27.94KB 9.51KB
data-objectstack (index.js) 227.99KB 63.22KB
fields (index.js) 261.01KB 66.28KB
i18n (LocalizationContext.js) 1.76KB 0.96KB
i18n (builtinAggregateLabels.js) 0.86KB 0.49KB
i18n (currency.js) 2.59KB 1.22KB
i18n (fallbackInterpolation.js) 6.25KB 2.77KB
i18n (i18n.js) 8.87KB 3.64KB
i18n (index.js) 5.24KB 2.27KB
i18n (pickLocalized.js) 9.86KB 3.95KB
i18n (provider.js) 39.40KB 12.91KB
i18n (translateFn.js) 0.20KB 0.18KB
i18n (useDisplayLocale.js) 3.52KB 1.76KB
i18n (useObjectLabel.js) 34.35KB 9.18KB
i18n (useSafeTranslation.js) 5.60KB 2.33KB
layout (index.js) 39.32KB 11.09KB
mobile (MobileProvider.js) 0.92KB 0.49KB
mobile (ResponsiveContainer.js) 0.94KB 0.38KB
mobile (breakpoints.js) 1.51KB 0.70KB
mobile (createOfflineDataSource.js) 5.61KB 1.75KB
mobile (index.js) 1.99KB 0.87KB
mobile (offlineQueue.js) 3.91KB 1.35KB
mobile (pwa.js) 0.97KB 0.49KB
mobile (serviceWorker.js) 1.48KB 0.62KB
mobile (serviceWorkerSource.js) 3.41KB 1.48KB
mobile (useBreakpoint.js) 1.54KB 0.65KB
mobile (useGesture.js) 6.96KB 1.98KB
mobile (useOfflineSync.js) 1.99KB 0.72KB
mobile (usePullToRefresh.js) 6.62KB 2.45KB
mobile (useResponsive.js) 0.72KB 0.42KB
mobile (useSpecGesture.js) 5.52KB 2.10KB
mobile (useTouchTarget.js) 1.01KB 0.54KB
permissions (MePermissionsProvider.js) 13.58KB 4.90KB
permissions (PermissionContext.js) 0.31KB 0.25KB
permissions (PermissionGuard.js) 0.89KB 0.45KB
permissions (PermissionProvider.js) 6.25KB 2.17KB
permissions (discardProofCache.js) 1.04KB 0.55KB
permissions (evaluator.js) 8.33KB 3.07KB
permissions (index.js) 0.93KB 0.41KB
permissions (store.js) 0.91KB 0.42KB
permissions (useFieldPermissions.js) 1.28KB 0.53KB
permissions (usePermissions.js) 4.83KB 2.27KB
plugin-ai (index.js) 16.01KB 3.93KB
plugin-calendar (index.js) 52.33KB 15.01KB
plugin-charts (index.js) 84.09KB 22.93KB
plugin-chatbot (index.js) 198.22KB 46.97KB
plugin-dashboard (index.js) 137.68KB 36.71KB
plugin-designer (index.js) 215.78KB 44.42KB
plugin-detail (index.js) 233.54KB 61.80KB
plugin-editor (index.js) 2.23KB 1.05KB
plugin-form (index.js) 169.60KB 43.32KB
plugin-gantt (index.js) 171.88KB 42.61KB
plugin-grid (index.js) 228.33KB 62.59KB
plugin-kanban (index.js) 48.43KB 15.11KB
plugin-list (index.js) 115.86KB 28.64KB
plugin-map (index.js) 22.90KB 7.62KB
plugin-markdown (index.js) 13.88KB 4.80KB
plugin-report (index.js) 44.04KB 12.21KB
plugin-timeline (index.js) 31.07KB 9.15KB
plugin-tree (index.js) 11.21KB 3.89KB
plugin-view (index.js) 90.43KB 22.76KB
providers (DataSourceProvider.js) 0.75KB 0.39KB
providers (MetadataProvider.js) 1.37KB 0.59KB
providers (ThemeProvider.js) 1.90KB 0.85KB
providers (UploadProvider.js) 11.81KB 3.58KB
providers (index.js) 0.45KB 0.23KB
providers (types.js) 0.01KB 0.04KB
react-runtime (index.js) 5.62KB 2.34KB
react (LazyPluginLoader.js) 4.47KB 1.63KB
react (SchemaRenderer.js) 119.55KB 39.23KB
react (data-invalidation.js) 5.05KB 2.08KB
react (index.js) 4.50KB 2.06KB
react (schema-input.js) 4.25KB 2.04KB
react (spec-input.js) 0.20KB 0.18KB
sdui-parser (body-dialect.js) 4.78KB 2.09KB
sdui-parser (codegen.js) 8.61KB 3.45KB
sdui-parser (dashboard-widget-options.js) 3.08KB 1.30KB
sdui-parser (index.js) 6.17KB 2.73KB
sdui-parser (input-type.js) 2.84KB 1.40KB
sdui-parser (kanban-quick-add.js) 3.89KB 1.87KB
sdui-parser (parse.js) 25.28KB 7.80KB
sdui-parser (provenance.js) 3.84KB 1.90KB
sdui-parser (types.js) 0.28KB 0.23KB
sdui-parser (validate.js) 22.61KB 7.40KB
types (ai.js) 4.39KB 2.17KB
types (api-types.js) 0.20KB 0.18KB
types (app.js) 3.83KB 1.49KB
types (base.js) 0.20KB 0.18KB
types (blocks.js) 0.20KB 0.18KB
types (complex.js) 3.19KB 1.62KB
types (crud.js) 0.20KB 0.18KB
types (dashboard-filter-alias.js) 6.23KB 2.74KB
types (data-display.js) 3.75KB 1.85KB
types (data-protocol.js) 0.20KB 0.19KB
types (data.js) 0.20KB 0.18KB
types (designer.js) 1.85KB 0.85KB
types (disclosure.js) 0.20KB 0.18KB
types (error-code.js) 1.54KB 0.88KB
types (expression.js) 0.20KB 0.18KB
types (feedback.js) 0.20KB 0.18KB
types (field-types.js) 0.20KB 0.18KB
types (form.js) 0.20KB 0.18KB
types (http-inflight.js) 8.87KB 3.73KB
types (http-retry.js) 4.32KB 2.02KB
types (icon-key-migration.js) 4.26KB 1.63KB
types (index.js) 4.74KB 2.26KB
types (layout.js) 0.20KB 0.18KB
types (managed-by.js) 0.19KB 0.18KB
types (mobile.js) 5.00KB 2.39KB
types (navigation.js) 0.20KB 0.18KB
types (objectql.js) 0.20KB 0.18KB
types (overlay.js) 0.20KB 0.18KB
types (permissions.js) 2.52KB 1.31KB
types (plugin-scope.js) 0.20KB 0.18KB
types (record-components.js) 0.20KB 0.19KB
types (record-semantics.js) 1.28KB 0.67KB
types (registry.js) 0.20KB 0.18KB
types (reports.js) 0.20KB 0.18KB
types (select-option.js) 0.20KB 0.19KB
types (spec-report.js) 5.05KB 1.93KB
types (spec-ui-namespace.js) 0.20KB 0.19KB
types (strict-authoring-face.js) 19.30KB 6.99KB
types (system-fields.js) 3.33KB 1.54KB
types (theme.js) 6.28KB 2.87KB
types (ui-action.js) 8.11KB 3.32KB
types (views.js) 0.20KB 0.18KB
types (widget.js) 0.20KB 0.18KB

Size Limits

  • ✅ Core packages should be < 50KB gzipped
  • ✅ Component packages should be < 100KB gzipped
  • ⚠️ Plugin packages should be < 150KB gzipped

…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>
@github-actions github-actions Bot added documentation Improvements or additions to documentation package: types tests labels Sep 29, 2026
…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>
@github-actions

Copy link
Copy Markdown
Contributor

❌ Console Performance Budget

Metric Value Budget
Eager closure (gzip, 330 chunks) 3563.5 KB 3149.4 KB
Main entry chunk (gzip) 149.3 KB 350 KB
Entry file index-kinSCXlb.js —
Status FAIL —

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:

Eager-closure half Verdict
Aggregate closure ceiling ❌ over its ceiling
Per-chunk ceilings ❌ over its ceiling
Per-chunk membership (declared packages) ✅ pass
Ceiling sensitivity (headroom) ✅ pass
Ceiling freshness (checkout vs. base branch) ✅ pass

📦 Bundle Size Report

Package Size Gzipped
app-shell (consoleActionDispatch.js) 0.20KB 0.19KB
app-shell (index.js) 16.88KB 6.25KB
app-shell (runtime-config.js) 20.68KB 7.36KB
app-shell (types.js) 0.01KB 0.04KB
app-shell (urlParams.js) 10.11KB 3.87KB
auth (ActiveOrganizationStorage.js) 27.95KB 10.04KB
auth (AuthContext.js) 0.31KB 0.24KB
auth (AuthGuard.js) 2.07KB 1.00KB
auth (AuthProvider.js) 40.22KB 10.61KB
auth (AuthShell.js) 3.49KB 1.40KB
auth (ForgotPasswordForm.js) 12.21KB 3.45KB
auth (LoginForm.js) 18.17KB 5.40KB
auth (PreviewBanner.js) 0.90KB 0.50KB
auth (RegisterForm.js) 6.72KB 2.24KB
auth (SocialSignInButtons.js) 9.70KB 3.93KB
auth (UserMenu.js) 3.39KB 1.21KB
auth (auth-gate-events.js) 1.29KB 0.66KB
auth (authStyles.js) 5.04KB 1.72KB
auth (createAuthClient.js) 40.70KB 10.94KB
auth (createAuthenticatedFetch.js) 8.54KB 3.46KB
auth (index.js) 3.63KB 1.64KB
auth (invitation-status.js) 1.22KB 0.70KB
auth (org-roles.js) 6.66KB 2.78KB
auth (phone-identifier.js) 1.11KB 0.66KB
auth (types.js) 0.59KB 0.35KB
auth (useAuth.js) 5.30KB 1.02KB
auth (useWorkspaceAdminStatus.js) 11.08KB 4.58KB
collaboration (CommentThread.js) 27.13KB 7.95KB
collaboration (LiveCursors.js) 3.17KB 1.27KB
collaboration (PresenceAvatars.js) 6.49KB 2.64KB
collaboration (PresenceProvider.js) 2.79KB 1.13KB
collaboration (index.js) 1.68KB 0.73KB
collaboration (useCollaborationTranslation.js) 6.05KB 2.52KB
collaboration (useCommentSearch.js) 1.98KB 0.88KB
collaboration (useConflictResolution.js) 7.75KB 1.86KB
collaboration (useMentionNotifications.js) 1.81KB 0.68KB
collaboration (usePresence.js) 6.33KB 1.84KB
collaboration (useRealtimeSubscription.js) 7.91KB 2.01KB
components (index.js) 558.78KB 133.92KB
core (index.js) 9.94KB 3.94KB
create-plugin (index.js) 27.94KB 9.51KB
data-objectstack (index.js) 227.99KB 63.22KB
fields (index.js) 261.01KB 66.28KB
i18n (LocalizationContext.js) 1.76KB 0.96KB
i18n (builtinAggregateLabels.js) 0.86KB 0.49KB
i18n (currency.js) 2.59KB 1.22KB
i18n (fallbackInterpolation.js) 6.25KB 2.77KB
i18n (i18n.js) 8.87KB 3.64KB
i18n (index.js) 5.24KB 2.27KB
i18n (pickLocalized.js) 9.86KB 3.95KB
i18n (provider.js) 39.40KB 12.91KB
i18n (translateFn.js) 0.20KB 0.18KB
i18n (useDisplayLocale.js) 3.52KB 1.76KB
i18n (useObjectLabel.js) 34.35KB 9.18KB
i18n (useSafeTranslation.js) 5.60KB 2.33KB
layout (index.js) 39.32KB 11.09KB
mobile (MobileProvider.js) 0.92KB 0.49KB
mobile (ResponsiveContainer.js) 0.94KB 0.38KB
mobile (breakpoints.js) 1.51KB 0.70KB
mobile (createOfflineDataSource.js) 5.61KB 1.75KB
mobile (index.js) 1.99KB 0.87KB
mobile (offlineQueue.js) 3.91KB 1.35KB
mobile (pwa.js) 0.97KB 0.49KB
mobile (serviceWorker.js) 1.48KB 0.62KB
mobile (serviceWorkerSource.js) 3.41KB 1.48KB
mobile (useBreakpoint.js) 1.54KB 0.65KB
mobile (useGesture.js) 6.96KB 1.98KB
mobile (useOfflineSync.js) 1.99KB 0.72KB
mobile (usePullToRefresh.js) 6.62KB 2.45KB
mobile (useResponsive.js) 0.72KB 0.42KB
mobile (useSpecGesture.js) 5.52KB 2.10KB
mobile (useTouchTarget.js) 1.01KB 0.54KB
permissions (MePermissionsProvider.js) 13.58KB 4.90KB
permissions (PermissionContext.js) 0.31KB 0.25KB
permissions (PermissionGuard.js) 0.89KB 0.45KB
permissions (PermissionProvider.js) 6.25KB 2.17KB
permissions (discardProofCache.js) 1.04KB 0.55KB
permissions (evaluator.js) 8.33KB 3.07KB
permissions (index.js) 0.93KB 0.41KB
permissions (store.js) 0.91KB 0.42KB
permissions (useFieldPermissions.js) 1.28KB 0.53KB
permissions (usePermissions.js) 4.83KB 2.27KB
plugin-ai (index.js) 16.01KB 3.93KB
plugin-calendar (index.js) 52.33KB 15.01KB
plugin-charts (index.js) 84.09KB 22.93KB
plugin-chatbot (index.js) 198.22KB 46.97KB
plugin-dashboard (index.js) 137.68KB 36.71KB
plugin-designer (index.js) 215.78KB 44.42KB
plugin-detail (index.js) 233.54KB 61.80KB
plugin-editor (index.js) 2.23KB 1.05KB
plugin-form (index.js) 169.60KB 43.32KB
plugin-gantt (index.js) 171.88KB 42.61KB
plugin-grid (index.js) 228.33KB 62.59KB
plugin-kanban (index.js) 48.43KB 15.11KB
plugin-list (index.js) 115.86KB 28.64KB
plugin-map (index.js) 22.90KB 7.62KB
plugin-markdown (index.js) 13.88KB 4.80KB
plugin-report (index.js) 44.04KB 12.21KB
plugin-timeline (index.js) 31.07KB 9.15KB
plugin-tree (index.js) 11.21KB 3.89KB
plugin-view (index.js) 90.43KB 22.76KB
providers (DataSourceProvider.js) 0.75KB 0.39KB
providers (MetadataProvider.js) 1.37KB 0.59KB
providers (ThemeProvider.js) 1.90KB 0.85KB
providers (UploadProvider.js) 11.81KB 3.58KB
providers (index.js) 0.45KB 0.23KB
providers (types.js) 0.01KB 0.04KB
react-runtime (index.js) 5.62KB 2.34KB
react (LazyPluginLoader.js) 4.47KB 1.63KB
react (SchemaRenderer.js) 119.55KB 39.23KB
react (data-invalidation.js) 5.05KB 2.08KB
react (index.js) 4.50KB 2.06KB
react (schema-input.js) 4.25KB 2.04KB
react (spec-input.js) 0.20KB 0.18KB
sdui-parser (body-dialect.js) 4.78KB 2.09KB
sdui-parser (codegen.js) 8.61KB 3.45KB
sdui-parser (dashboard-widget-options.js) 3.08KB 1.30KB
sdui-parser (index.js) 6.17KB 2.73KB
sdui-parser (input-type.js) 2.84KB 1.40KB
sdui-parser (kanban-quick-add.js) 3.89KB 1.87KB
sdui-parser (parse.js) 25.28KB 7.80KB
sdui-parser (provenance.js) 3.84KB 1.90KB
sdui-parser (types.js) 0.28KB 0.23KB
sdui-parser (validate.js) 22.61KB 7.40KB
types (ai.js) 4.39KB 2.17KB
types (api-types.js) 0.20KB 0.18KB
types (app.js) 3.83KB 1.49KB
types (base.js) 0.20KB 0.18KB
types (blocks.js) 0.20KB 0.18KB
types (complex.js) 3.19KB 1.62KB
types (crud.js) 0.20KB 0.18KB
types (dashboard-filter-alias.js) 6.23KB 2.74KB
types (data-display.js) 3.75KB 1.85KB
types (data-protocol.js) 0.20KB 0.19KB
types (data.js) 0.20KB 0.18KB
types (designer.js) 1.85KB 0.85KB
types (disclosure.js) 0.20KB 0.18KB
types (error-code.js) 1.54KB 0.88KB
types (expression.js) 0.20KB 0.18KB
types (feedback.js) 0.20KB 0.18KB
types (field-types.js) 0.20KB 0.18KB
types (form.js) 0.20KB 0.18KB
types (http-inflight.js) 8.87KB 3.73KB
types (http-retry.js) 4.32KB 2.02KB
types (icon-key-migration.js) 4.26KB 1.63KB
types (index.js) 4.74KB 2.26KB
types (layout.js) 0.20KB 0.18KB
types (managed-by.js) 0.19KB 0.18KB
types (mobile.js) 5.00KB 2.39KB
types (navigation.js) 0.20KB 0.18KB
types (objectql.js) 0.20KB 0.18KB
types (overlay.js) 0.20KB 0.18KB
types (permissions.js) 2.52KB 1.31KB
types (plugin-scope.js) 0.20KB 0.18KB
types (record-components.js) 0.20KB 0.19KB
types (record-semantics.js) 1.28KB 0.67KB
types (registry.js) 0.20KB 0.18KB
types (reports.js) 0.20KB 0.18KB
types (select-option.js) 0.20KB 0.19KB
types (spec-report.js) 5.05KB 1.93KB
types (spec-ui-namespace.js) 0.20KB 0.19KB
types (strict-authoring-face.js) 19.30KB 6.99KB
types (system-fields.js) 3.33KB 1.54KB
types (theme.js) 6.28KB 2.87KB
types (ui-action.js) 8.11KB 3.32KB
types (views.js) 0.20KB 0.18KB
types (widget.js) 0.20KB 0.18KB

Size Limits

  • ✅ Core packages should be < 50KB gzipped
  • ✅ Component packages should be < 100KB gzipped
  • ⚠️ Plugin packages should be < 150KB gzipped

…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>
@github-actions

Copy link
Copy Markdown
Contributor

❌ Console Performance Budget

Metric Value Budget
Eager closure (gzip, 330 chunks) 3563.5 KB 3149.4 KB
Main entry chunk (gzip) 149.3 KB 350 KB
Entry file index-kinSCXlb.js —
Status FAIL —

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:

Eager-closure half Verdict
Aggregate closure ceiling ❌ over its ceiling
Per-chunk ceilings ❌ over its ceiling
Per-chunk membership (declared packages) ✅ pass
Ceiling sensitivity (headroom) ✅ pass
Ceiling freshness (checkout vs. base branch) ✅ pass

📦 Bundle Size Report

Package Size Gzipped
app-shell (consoleActionDispatch.js) 0.20KB 0.19KB
app-shell (index.js) 16.88KB 6.25KB
app-shell (runtime-config.js) 20.68KB 7.36KB
app-shell (types.js) 0.01KB 0.04KB
app-shell (urlParams.js) 10.11KB 3.87KB
auth (ActiveOrganizationStorage.js) 27.95KB 10.04KB
auth (AuthContext.js) 0.31KB 0.24KB
auth (AuthGuard.js) 2.07KB 1.00KB
auth (AuthProvider.js) 40.22KB 10.61KB
auth (AuthShell.js) 3.49KB 1.40KB
auth (ForgotPasswordForm.js) 12.21KB 3.45KB
auth (LoginForm.js) 18.17KB 5.40KB
auth (PreviewBanner.js) 0.90KB 0.50KB
auth (RegisterForm.js) 6.72KB 2.24KB
auth (SocialSignInButtons.js) 9.70KB 3.93KB
auth (UserMenu.js) 3.39KB 1.21KB
auth (auth-gate-events.js) 1.29KB 0.66KB
auth (authStyles.js) 5.04KB 1.72KB
auth (createAuthClient.js) 40.70KB 10.94KB
auth (createAuthenticatedFetch.js) 8.54KB 3.46KB
auth (index.js) 3.63KB 1.64KB
auth (invitation-status.js) 1.22KB 0.70KB
auth (org-roles.js) 6.66KB 2.78KB
auth (phone-identifier.js) 1.11KB 0.66KB
auth (types.js) 0.59KB 0.35KB
auth (useAuth.js) 5.30KB 1.02KB
auth (useWorkspaceAdminStatus.js) 11.08KB 4.58KB
collaboration (CommentThread.js) 27.13KB 7.95KB
collaboration (LiveCursors.js) 3.17KB 1.27KB
collaboration (PresenceAvatars.js) 6.49KB 2.64KB
collaboration (PresenceProvider.js) 2.79KB 1.13KB
collaboration (index.js) 1.68KB 0.73KB
collaboration (useCollaborationTranslation.js) 6.05KB 2.52KB
collaboration (useCommentSearch.js) 1.98KB 0.88KB
collaboration (useConflictResolution.js) 7.75KB 1.86KB
collaboration (useMentionNotifications.js) 1.81KB 0.68KB
collaboration (usePresence.js) 6.33KB 1.84KB
collaboration (useRealtimeSubscription.js) 7.91KB 2.01KB
components (index.js) 558.78KB 133.92KB
core (index.js) 9.94KB 3.94KB
create-plugin (index.js) 27.94KB 9.51KB
data-objectstack (index.js) 227.99KB 63.22KB
fields (index.js) 261.01KB 66.28KB
i18n (LocalizationContext.js) 1.76KB 0.96KB
i18n (builtinAggregateLabels.js) 0.86KB 0.49KB
i18n (currency.js) 2.59KB 1.22KB
i18n (fallbackInterpolation.js) 6.25KB 2.77KB
i18n (i18n.js) 8.87KB 3.64KB
i18n (index.js) 5.24KB 2.27KB
i18n (pickLocalized.js) 9.86KB 3.95KB
i18n (provider.js) 39.40KB 12.91KB
i18n (translateFn.js) 0.20KB 0.18KB
i18n (useDisplayLocale.js) 3.52KB 1.76KB
i18n (useObjectLabel.js) 34.35KB 9.18KB
i18n (useSafeTranslation.js) 5.60KB 2.33KB
layout (index.js) 39.32KB 11.09KB
mobile (MobileProvider.js) 0.92KB 0.49KB
mobile (ResponsiveContainer.js) 0.94KB 0.38KB
mobile (breakpoints.js) 1.51KB 0.70KB
mobile (createOfflineDataSource.js) 5.61KB 1.75KB
mobile (index.js) 1.99KB 0.87KB
mobile (offlineQueue.js) 3.91KB 1.35KB
mobile (pwa.js) 0.97KB 0.49KB
mobile (serviceWorker.js) 1.48KB 0.62KB
mobile (serviceWorkerSource.js) 3.41KB 1.48KB
mobile (useBreakpoint.js) 1.54KB 0.65KB
mobile (useGesture.js) 6.96KB 1.98KB
mobile (useOfflineSync.js) 1.99KB 0.72KB
mobile (usePullToRefresh.js) 6.62KB 2.45KB
mobile (useResponsive.js) 0.72KB 0.42KB
mobile (useSpecGesture.js) 5.52KB 2.10KB
mobile (useTouchTarget.js) 1.01KB 0.54KB
permissions (MePermissionsProvider.js) 13.58KB 4.90KB
permissions (PermissionContext.js) 0.31KB 0.25KB
permissions (PermissionGuard.js) 0.89KB 0.45KB
permissions (PermissionProvider.js) 6.25KB 2.17KB
permissions (discardProofCache.js) 1.04KB 0.55KB
permissions (evaluator.js) 8.33KB 3.07KB
permissions (index.js) 0.93KB 0.41KB
permissions (store.js) 0.91KB 0.42KB
permissions (useFieldPermissions.js) 1.28KB 0.53KB
permissions (usePermissions.js) 4.83KB 2.27KB
plugin-ai (index.js) 16.01KB 3.93KB
plugin-calendar (index.js) 52.33KB 15.01KB
plugin-charts (index.js) 84.09KB 22.93KB
plugin-chatbot (index.js) 198.22KB 46.97KB
plugin-dashboard (index.js) 137.68KB 36.71KB
plugin-designer (index.js) 215.78KB 44.42KB
plugin-detail (index.js) 233.54KB 61.80KB
plugin-editor (index.js) 2.23KB 1.05KB
plugin-form (index.js) 169.60KB 43.32KB
plugin-gantt (index.js) 171.88KB 42.61KB
plugin-grid (index.js) 228.33KB 62.59KB
plugin-kanban (index.js) 48.43KB 15.11KB
plugin-list (index.js) 115.86KB 28.64KB
plugin-map (index.js) 22.90KB 7.62KB
plugin-markdown (index.js) 13.88KB 4.80KB
plugin-report (index.js) 44.04KB 12.21KB
plugin-timeline (index.js) 31.07KB 9.15KB
plugin-tree (index.js) 11.21KB 3.89KB
plugin-view (index.js) 90.43KB 22.76KB
providers (DataSourceProvider.js) 0.75KB 0.39KB
providers (MetadataProvider.js) 1.37KB 0.59KB
providers (ThemeProvider.js) 1.90KB 0.85KB
providers (UploadProvider.js) 11.81KB 3.58KB
providers (index.js) 0.45KB 0.23KB
providers (types.js) 0.01KB 0.04KB
react-runtime (index.js) 5.62KB 2.34KB
react (LazyPluginLoader.js) 4.47KB 1.63KB
react (SchemaRenderer.js) 119.55KB 39.23KB
react (data-invalidation.js) 5.05KB 2.08KB
react (index.js) 4.50KB 2.06KB
react (schema-input.js) 4.25KB 2.04KB
react (spec-input.js) 0.20KB 0.18KB
sdui-parser (body-dialect.js) 4.78KB 2.09KB
sdui-parser (codegen.js) 8.61KB 3.45KB
sdui-parser (dashboard-widget-options.js) 3.08KB 1.30KB
sdui-parser (index.js) 6.17KB 2.73KB
sdui-parser (input-type.js) 2.84KB 1.40KB
sdui-parser (kanban-quick-add.js) 3.89KB 1.87KB
sdui-parser (parse.js) 25.28KB 7.80KB
sdui-parser (provenance.js) 3.84KB 1.90KB
sdui-parser (types.js) 0.28KB 0.23KB
sdui-parser (validate.js) 22.61KB 7.40KB
types (ai.js) 4.39KB 2.17KB
types (api-types.js) 0.20KB 0.18KB
types (app.js) 3.83KB 1.49KB
types (base.js) 0.20KB 0.18KB
types (blocks.js) 0.20KB 0.18KB
types (complex.js) 3.19KB 1.62KB
types (crud.js) 0.20KB 0.18KB
types (dashboard-filter-alias.js) 6.23KB 2.74KB
types (data-display.js) 3.75KB 1.85KB
types (data-protocol.js) 0.20KB 0.19KB
types (data.js) 0.20KB 0.18KB
types (designer.js) 1.85KB 0.85KB
types (disclosure.js) 0.20KB 0.18KB
types (error-code.js) 1.54KB 0.88KB
types (expression.js) 0.20KB 0.18KB
types (feedback.js) 0.20KB 0.18KB
types (field-types.js) 0.20KB 0.18KB
types (form.js) 0.20KB 0.18KB
types (http-inflight.js) 8.87KB 3.73KB
types (http-retry.js) 4.32KB 2.02KB
types (icon-key-migration.js) 4.26KB 1.63KB
types (index.js) 4.74KB 2.26KB
types (layout.js) 0.20KB 0.18KB
types (managed-by.js) 0.19KB 0.18KB
types (mobile.js) 5.00KB 2.39KB
types (navigation.js) 0.20KB 0.18KB
types (objectql.js) 0.20KB 0.18KB
types (overlay.js) 0.20KB 0.18KB
types (permissions.js) 2.52KB 1.31KB
types (plugin-scope.js) 0.20KB 0.18KB
types (record-components.js) 0.20KB 0.19KB
types (record-semantics.js) 1.28KB 0.67KB
types (registry.js) 0.20KB 0.18KB
types (reports.js) 0.20KB 0.18KB
types (select-option.js) 0.20KB 0.19KB
types (spec-report.js) 5.05KB 1.93KB
types (spec-ui-namespace.js) 0.20KB 0.19KB
types (strict-authoring-face.js) 19.30KB 6.99KB
types (system-fields.js) 3.33KB 1.54KB
types (theme.js) 6.28KB 2.87KB
types (ui-action.js) 8.11KB 3.32KB
types (views.js) 0.20KB 0.18KB
types (widget.js) 0.20KB 0.18KB

Size Limits

  • ✅ Core packages should be < 50KB gzipped
  • ✅ Component packages should be < 100KB gzipped
  • ⚠️ Plugin packages should be < 150KB gzipped

…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>
Merged via the queue into main with commit 81f8498 Sep 30, 2026
46 of 47 checks passed
@os-zhuang
os-zhuang deleted the claude/issue-11073-objectstack-17-5-bump branch September 30, 2026 04:00
huangyiirene pushed a commit that referenced this pull request Sep 30, 2026
…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>
huangyiirene pushed a commit that referenced this pull request Sep 30, 2026
…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>
This was referenced Sep 30, 2026
huangyiirene pushed a commit that referenced this pull request Sep 30, 2026
…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>
akarma-synetal pushed a commit to akarma-synetal/objectui that referenced this pull request Oct 7, 2026
…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>
akarma-synetal pushed a commit to akarma-synetal/objectui that referenced this pull request Oct 7, 2026
…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>
akarma-synetal pushed a commit to akarma-synetal/objectui that referenced this pull request Oct 7, 2026
…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>
akarma-synetal pushed a commit to akarma-synetal/objectui that referenced this pull request Oct 7, 2026
…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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

deps: bump objectui's @objectstack/* to 17.5.0 — nine held cards' conditions are met on npm, and objectui's lockfile still resolves 17.4.0

3 participants