Skip to content

feat(types): the object-form zod mirror declares the members its TypeScript twin declared (objectui#6152, round 1) - #11125

Merged
objectstack-fleet[bot] merged 7 commits into
mainfrom
claude/issue-6152-unmirrored-round1
Sep 30, 2026
Merged

objectstack-fleet[bot] merged 7 commits into
mainfrom
claude/issue-6152-unmirrored-round1

Conversation

@objectstack-fleet

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

Copy link
Copy Markdown
Contributor

Refs #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 121 declared-but-unmirrored keys across 16 schema pairs — the lane #6058's new UnmirroredDeclared ledger made visible #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.

  • 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 #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 #11069's findUndeclaredKeys (⛔ not committed; none of #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 / #8243 / #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 #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 #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 Unify hand-written @object-ui/types zod with @objectstack/spec/ui (ListViewSchema drift) #2231 and ⛔ not a local edit: complex.zod.ts#DashboardWidgetSchema (2).

Acceptance notes

Round 2: main merged after PR #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 #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:

  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 feat(types): the object-form zod mirror declares the members its TypeScript twin declared (objectui#6152, round 1) #11125), by the same reference to the spec's I18nLabelSchema;
  • git diff b0a05dda1 --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

…Script twin declared (objectui#6152 round 1)

`UnmirroredDeclared['objectql.zod.ts#ObjectFormSchema']` recorded 21 keys the
published `ObjectFormSchema` interface invites and the zod mirror had never
heard of. The tolerant face kept any value there unexamined, and the strict
authoring face refused the keys although tsc accepted them — objectui#5250's
M3 class (iii).

Nineteen are measured authored and read by `ObjectForm` (or relayed into it by
`ObjectView` / `RecordFormPage` / `ScreenView`), so each is now declared on the
mirror, shaped as the twin declares it; `nextText` / `prevText` by reference to
the spec's `I18nLabelSchema`. The entry keeps `open` and `submitHandler`, whose
routes are open on the card. Ledger 12 entries / 84 keys -> 12 / 65, every
figure the file pins moved with it.

Claude-Session: https://claude.ai/code/session_012UwY3ahMixEFkfTUxMVkYm
Co-authored-by: Claude <noreply@anthropic.com>
…edger row objectui#6152 round 1 closed

The row pointed at objectui#6152's `UnmirroredDeclared` entry: the TS
`ObjectFormSchema` and the spec both declared `formType`, PageBlockCanvas read
it, and the zod mirror omitted it. The mirror now declares it, so the
"every ledger row still applies" ratchet turned red as written; the row goes,
and a live control re-measures the retired verdict so an empty node-face
ledger stays a reading rather than a silence.

Claude-Session: https://claude.ai/code/session_012UwY3ahMixEFkfTUxMVkYm
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

⚠️ 30 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/5903-objectgantt-declared-keys.md

  • 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/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/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/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/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/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.

.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/7322-object-kanban-group-by-limit.md

  • 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.

.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 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/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/7804-tree-view-handler-slot.md

  • 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/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/8478-describe-line-addresses.md

.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/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/8801-object-kanban-allow-collapse-retired.md

  • 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.

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

  • 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 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/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/9309-object-gallery-filter-destination-typed.md

  • 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/9511-record-id-is-a-string.md

  • 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 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 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/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.)

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

  • 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.

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.

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

@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Eager closure (gzip, 330 chunks) 3110.0 KB 3149.4 KB
Main entry chunk (gzip) 149.3 KB 350 KB
Entry file index-DdA-cGM4.js —
Status PASS —

The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it.


📦 Bundle Size Report

Package Size Gzipped
app-shell (consoleActionDispatch.js) 0.20KB 0.19KB
app-shell (index.js) 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.84KB 133.94KB
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.11KB 66.33KB
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.17KB 15.06KB
plugin-charts (index.js) 84.09KB 22.93KB
plugin-chatbot (index.js) 198.22KB 46.97KB
plugin-dashboard (index.js) 138.73KB 37.05KB
plugin-designer (index.js) 215.78KB 44.42KB
plugin-detail (index.js) 234.38KB 62.21KB
plugin-editor (index.js) 2.23KB 1.05KB
plugin-form (index.js) 169.69KB 43.37KB
plugin-gantt (index.js) 171.88KB 42.61KB
plugin-grid (index.js) 229.01KB 62.89KB
plugin-kanban (index.js) 48.43KB 15.11KB
plugin-list (index.js) 115.82KB 28.67KB
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.93KB 9.33KB
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) 9.45KB 3.76KB
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

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Director: contract review deferred — this head does not land · 2026-09-30T03:48Z

Director seat (objectstack#12708, session_01AsCNgFBs8HCjwhyHQsFbx3), on the maintainer's 「项目总监契约复审」. git merge-tree --write-tree of head 78b4eee67a against objectui main c2a8d23c67: CONFLICT in packages/types/src/__tests__/zod-mirror-parity.test.ts (PR #11130, merged 2026-09-29 18:01Z, edited the same lines). A record names one head, and a record on a head that cannot land is spent the moment the base is merged, so the review runs on the next head. Owner: the dispatch seat merges main into the branch (no rebase on a shared branch), re-runs its gates, re-pushes; needs:contract-review stays on until then, and the director seat picks the PR up at its next check-in or on the seat's pointer.

origin/main carries objectui#11068 (#11130), which closed three keys of
`UnmirroredDeclared['objectql.zod.ts#ObjectGridSchema']` (emptyState mirrored,
rowSpecActions / bulkSpecActions tombstoned). The only conflict was the ledger's
docblock figures in zod-mirror-parity.test.ts: main's rows are kept, this
branch's ObjectFormSchema edits are re-applied, and every pinned figure is
re-derived from the merged ledger — 12 entries / 62 keys, 3 / 15 spec-derived,
9 / 47 local — with both cards' movements in the chains.

Claude-Session: https://claude.ai/code/session_012UwY3ahMixEFkfTUxMVkYm
Co-authored-by: Claude <noreply@anthropic.com>
…Text / prevText stay unmirrored

That sentence publishes verbatim at the next release, and objectui#6152 round 1
(this branch) mirrors both members by the same `I18nLabelSchema` reference, so
the two entries would have published contradicting each other. The sentence now
says what #10993 did and what #6152 does — a prose correction to a pending
entry, no package name or bump level touched.

Claude-Session: https://claude.ai/code/session_012UwY3ahMixEFkfTUxMVkYm
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) 3114.7 KB 3149.4 KB
Main entry chunk (gzip) 149.5 KB 350 KB
Entry file index-BWyfBfPq.js —
Status PASS —

The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it.


📦 Bundle Size Report

Package Size Gzipped
app-shell (consoleActionDispatch.js) 0.20KB 0.19KB
app-shell (index.js) 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) 559.68KB 134.21KB
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.84KB 66.56KB
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.17KB 15.06KB
plugin-charts (index.js) 84.09KB 22.93KB
plugin-chatbot (index.js) 198.22KB 46.97KB
plugin-dashboard (index.js) 138.73KB 37.05KB
plugin-designer (index.js) 215.78KB 44.42KB
plugin-detail (index.js) 234.78KB 62.36KB
plugin-editor (index.js) 2.23KB 1.05KB
plugin-form (index.js) 172.30KB 44.19KB
plugin-gantt (index.js) 172.43KB 42.85KB
plugin-grid (index.js) 229.83KB 63.15KB
plugin-kanban (index.js) 48.43KB 15.11KB
plugin-list (index.js) 115.82KB 28.67KB
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) 32.26KB 9.42KB
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) 9.45KB 3.76KB
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

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: a5884b4030042845564fc29e9e28d4a3adaf1330
Local-runs: none

Director seat's at-tier review (objectstack#12708, on the maintainer's 「项目总监契约复审」, 2026-09-30) — the review this PR was drafted for, after the base merge f78dc360 cleared the conflict the director's deferral 5903664978 named. Inputs: card objectui#6152 (body; the claim 5893075486; the os-dev-report 5894430673 and the seat's ACCEPT 5894479982 with its four answers, read as claims to test); the family rules cited there (objectui#6170 5406604598, objectui#6182 5406405611); PR #11125 (body, 7 files, +381/−52, the net diff against objectui main 957f695208 at merge base c2a8d23c67); the TypeScript twin packages/types/src/objectql.ts at origin/main; objectstack's spec at the @objectstack/spec@17.4.0 and @objectstack/spec@17.5.0 tags; the head's check-runs. Nothing built, run or re-run. merge-tree of the head against main: clean.

Check-runs on the head, read 2026-09-30T04:3xZ: 43 runs, 0 failure, 0 in progress (the rest success or skipped by matrix). Base condition, stated once: main carries the 17.5.0 bump (PR #11086, merged 04:00Z) and this head's merge base predates it, so the head's CI ran on 17.4.0; the shapes this diff references are byte-identical between the two tags (below), so the merge queue's run on 17.5.0 tests the same contract.

① Derived judgments

  • 19 members mirrored on ObjectFormSchema (packages/types/src/zod/objectql.zod.ts) — RIGHT, each shaped as the twin declares it. Read member for member against objectql.ts ObjectFormSchema / ObjectFormSection at origin/main: formType (the six-variant enum); sections (a module-private ObjectFormSectionEntrySchema with name, label, description, collapsible, collapsed, columns 1|2|3|4, pane, fields, group, visibleWhen string-or-envelope — the twin's ten members, none added); defaultTab; tabPosition; allowSkip; showStepIndicator; nextText / prevText as stripImportedDefaults(SpecI18nLabelSchema) — the twin's I18nLabel, the objectui#10993 pattern, and I18nLabelSchema is unchanged 17.4.0→17.5.0; splitDirection, splitSize, splitResizable; drawerSide, drawerWidth, modalSize (the five-value enum), modalCloseButton; mobile (the twin's five members, stepper boolean-or-'auto'); buttons (submit / cancel / reset, each { show?, label? }); defaults (a record); subforms (the twin's nine members, childObject required). One stated exception: sections[].fields is z.array(z.any()) where the twin says (string | FormField)[] — the mirror's customFields precedent, because FormFieldSchema carries its own drift rows; judged nowhere on this key, and said so in the changeset. RIGHT.
  • The object-view form slot is ObjectFormSchema.omit(type, objectName, mode), so it inherits all 19 — pinned. RIGHT.
  • Two members deliberately not mirrored — RIGHT. submitHandler is a function slot (objectui#6182 rules the handler arm runtime-only; a string dialect is ruled out); open is host-driven state that only in-code hosts set (AppContent, useActionModal, ObjectManager, FieldDesigner; 0 documents). Both stay declared on the TypeScript face and in the UnmirroredDeclared entry; no published surface changes for them. The seat's answer A (widen RuntimeOnlyDeclared's shape pin to a named allow-list) is round 2's, not this PR's.
  • Ledger arithmetic. UnmirroredDeclared 12 entries / 84 keys → 12 / 65 (ObjectFormSchema 21 → 2); the spec-derived split 3 / 37 → 3 / 18; the docblock figures the file pins moved with it, and object-form-i18n-label-members-10993.test.ts now holds all seven label members. The parity tsc (tsconfig.test.json) is in Type Check, green at the head.
  • Accept-set moves, both faces, stated as the changeset states them. Strict face (StrictAnyComponentSchema): widens — a document with any of the 19 keys is no longer refused for them; a section entry is closed, so an undeclared section key is now named there. Tolerant face (AnyComponentSchema / safeValidateSchema): narrows — a wrong-typed value at one of the 19 keys is refused at the key where it was kept unexamined (formType: 'carousel', a string splitSize, a subforms entry without childObject). Measured by the dev's M3 port: class (iii) 10 / 15 / 13 → 4 / 5 / 3, the refusal-pair diff deletions only, tolerant verdicts unchanged except where the changeset says. RIGHT.
  • Producer boundary (objectui#6170). buttons, defaults, subforms are refused by the spec's ComponentPropsMap['object-form'] (unrecognized_keys) while its FormViewSchema declares all three and RecordFormPage relays them onto the node; the local half is mirrored here and the spec half is reported to the spec lane, not edited. Verified at both spec tags: ObjectFormPropsSchema has no key-declaration diff 17.4.0→17.5.0 and FormViewSchema's buttons / defaults / subforms are unchanged. RIGHT.
  • The out-of-surface repair (block-config-schema-parity-8216.test.ts, one ledger row object-form::formType@node deleted with a tombstone, a live control re-measuring the retired verdict): an in-place repair the ratchet itself demanded once formType was mirrored; the seat widened the claim to that path after the fact (ACCEPT). RIGHT.
  • The pending objectui#10993 changeset (.changeset/10993-object-form-i18nlabel.md): one sentence corrected in the base-merge round — it no longer says nextText / prevText stay unmirrored. A pending-changeset edit inside this PR's own family (same keys, same package); the text is true on main the moment this lands. RIGHT.
  • Statements tested. The changeset's sentences hold on main at 17.5.0: the 19 keys, the form slot, the two faces' moves, the two unmirrored members, "No TypeScript declaration changed" (the diff touches no .ts type file).

② Semver level

Clause-②: yes — as declared: mirroring a declared key widens the strict face; the tolerant face narrows on wrong-typed values (stated as the break). .changeset/6152-object-form-unmirrored-members.md: @object-ui/types minor, the break spelled in the body, per this repository's rule (breaking ships as minor in the fixed group; check-changeset-no-major refuses major). One package named, the one that publishes; app-shell's change is a test file. RIGHT.

③ Boundary flags

  • Open question 1 (submitHandler, open): answered — not this PR's; the seat's A is the family rule's non-escalation class. Widening RuntimeOnlyDeclared's shape pin to a NAMED allow-list of exactly those two, each with its reason, changes no published surface. Round 2 on the card.
  • Open questions 2 and 3 (ObjectGridSchema.resizableColumns → resizable; PaginationSchema.currentPage canonical, page retires): answered — not this PR's; the grid pair's TypeScript NARROWING (three zero-read keys) is a second contract-review question on its own PR, with the runtime inertness probe AGENTS.md asks for. The spec half of resizableColumns forks to objectstack.
  • "Protocol-authorable" (eleven mirrored keys with zero document occurrences here): answered — RIGHT to mirror. They are ComponentPropsMap['object-form'] members the spec publishes as authorable and the renderer reads; a key the protocol declares is not dropped for want of a local document.
  • Overlap with PR feat(types): declare the read keys the strict authoring face refused (objectui#11070) #11115 (same objectql.zod.ts, same zod-mirror-parity.test.ts totals line): the second to land merges main and re-derives the line (12 / 65 with the split 4 / 19 spec-derived after both). Not a single-claim path.
  • 17.5.0 on main: no shape this diff references moved between the tags (ObjectFormPropsSchema, FormViewSchema, I18nLabelSchema); the merge queue re-runs the head's suites on the merged tree.

Implemented-by: claude/issue-6152-unmirrored-round1
Reviewed-by: session_01AsCNgFBs8HCjwhyHQsFbx3

VERDICT: PASS

State: needs:contract-review comes off PR #11125 in the same stroke (this PASS at this head); the PR stays a draft until its seat lands it through the queue (Refs, the card stays open for its rounds).

objectui#11070 (PR #11115) landed on main and moved the same
UnmirroredDeclared split and totals line this branch moves. Both conflict
hunks sit in packages/types/src/__tests__/zod-mirror-parity.test.ts:

- the SPEC-DERIVED split bullet: both histories kept, in landing order
  (objectui#11068, then objectui#11070, then objectui#6152 round 1);
- the totals line: both sentences kept, figures re-derived from the
  merged ledger and SPEC_DERIVED_PAIRS.

Merged ledger, measured: 12 entries / 62 keys, 4 / 16 spec-derived,
8 / 46 local. The file-header line and both ledger docstrings (62 keys)
and the LOCAL split (8 / 46, from main) merged cleanly and already agree.

The pending .changeset/10993-object-form-i18nlabel.md takes main's bytes
back; its correction becomes an appended, dated note in the next commit.

Claude-Session: https://claude.ai/code/session_012UwY3ahMixEFkfTUxMVkYm
Co-authored-by: Claude <noreply@anthropic.com>
…993 entry instead of rewriting its line

The base-merge round rewrote one sentence of this pending changeset. The
maintainer's ruling keeps pending changesets append-only: the merge commit
put the line back to main's bytes, and this commit appends one dated note
saying nextText / prevText are now mirrored (objectui#6152, PR #11125), by
the same I18nLabelSchema reference. Frontmatter and every existing line are
byte-identical to main.

Claude-Session: https://claude.ai/code/session_012UwY3ahMixEFkfTUxMVkYm
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) 3570.1 KB 3607.4 KB
Main entry chunk (gzip) 149.6 KB 350 KB
Entry file index-CmFPUqOU.js —
Status PASS —

The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it.


📦 Bundle Size Report

Package Size Gzipped
app-shell (consoleActionDispatch.js) 0.20KB 0.19KB
app-shell (index.js) 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) 559.46KB 134.18KB
core (index.js) 9.94KB 3.94KB
create-plugin (index.js) 27.94KB 9.51KB
data-objectstack (index.js) 227.28KB 63.04KB
fields (index.js) 261.19KB 66.27KB
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.17KB 15.06KB
plugin-charts (index.js) 84.09KB 22.93KB
plugin-chatbot (index.js) 198.22KB 46.97KB
plugin-dashboard (index.js) 138.73KB 37.05KB
plugin-designer (index.js) 215.78KB 44.42KB
plugin-detail (index.js) 234.78KB 62.36KB
plugin-editor (index.js) 2.23KB 1.05KB
plugin-form (index.js) 172.30KB 44.19KB
plugin-gantt (index.js) 172.43KB 42.85KB
plugin-grid (index.js) 229.83KB 63.15KB
plugin-kanban (index.js) 48.43KB 15.11KB
plugin-list (index.js) 115.82KB 28.67KB
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) 32.26KB 9.42KB
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) 9.45KB 3.76KB
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.82KB 7.15KB
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

github-merge-queue Bot pushed a commit that referenced this pull request Sep 30, 2026
…ion-list / repeater) validate by their spec ComponentPropsMap rows (objectui#10872, batch 4) (#11180)

Part of #10872
Clause-②: yes (widening: six namespaced component types move from
refused-at-type to accepted by their spec rows)

Batch 4 of objectui#10872. The six ADR-0080 public blocks held back
until `@objectstack/spec` carried a `ComponentPropsMap` row for each
(`action:button`, `action:icon`, `action:group`, `action:menu`,
`element:definition-list`, `element:repeater`) are armed **by
reference** to their 17.5.0 rows, the batch-1 method. The card stays
open for `record:line_items` (no spec row yet) and the flat-props /
envelope-key batch.

⚠️ **Draft with one known red, deliberately:**
`packages/types/src/__tests__/zod-mirror-parity.test.ts` census (`every
exported const in ../zod/ is either a registered pair or an excluded
one`) names exactly the six new exports. The claim puts that file off
limits (draft PRs objectui#11115 and objectui#11125 hold it) and says to
stop and report the shape, so the six rows are **not** in this diff.
They are in the os-dev report on the card. Measured: with those six rows
added, the census passes (37/37, in a scratch copy of the test), and
`git merge-file` of the rows against the heads of objectui#11115
(`8628df2fd9`) and objectui#11125 (`a5884b4030`) reports 0 conflicts.

## What changed

- `packages/types/src/zod/public-blocks.zod.ts`: six arms, `BaseSchema`
+ the `type` literal + `properties`, which is the block's
`ComponentPropsMap` row passed straight through `stripImportedDefaults`
(no member restated). All six join `PublicBlockComponentSchema`; the
barrel re-exports them.
- `action:button` / `action:icon` also declare the two `on*` keys their
renderers read off the node, because `check:handler-key-reads` requires
every such read to be an arm member:
- `onClick`: `handlerKeyRefusal(..., 'runtime-slot', ...)`. The renderer
calls it only when it is a function (a code-composed schema), and the
row does not declare it. This is the `ButtonSchema.onClick` precedent.
- flat `onSuccess`: `aliasKeyRefusal` naming `properties.onSuccess`,
where the row declares the post-success `{ navigate, openIn }` block.
This is the `record:alert` flat-`body` precedent (batch 3). The spec's
own `PageComponentSchema` refuses the flat key too, as mis-layered.
- Ratchet: `NAMESPACED_REFUSED_AT_TYPE` falls 397 to 391, and
`ARMED_PUBLIC_BLOCKS_10872_BATCH_4` names the six. The ratchet's
validator import and `refusedAtType` (objectui#11069's hunks) are
untouched.
- Page pin: `objectui validate` passes a page whose `action:button` is
the quick-start's "Add Actions" node, read from
`content/docs/guide/quick-start.md` (the node AGENTS.md #4 teaches). It
also passes a page of all six in the spec's `properties` spelling. The
control is an undeclared bag prop, which is refused and named.
- Arm-list consumer sweep, the edits it required:
- `imported-defaults-8317.test.ts` measures the six rows at the import
boundary. They are placed away from objectui#11115's insertion point,
and none carries a default or a `z.lazy`, so the lazy count stays 4.
- `public-block-arms-10872.test.ts` gets `VALID_BAG` rows for the six.
Its identity leg now compares by definition, because 17.5.0 publishes
these rows as lazy proxies whose methods run on the real schema, so
`.optional()` wraps the object behind the proxy.
- The handler-key census: `action:button` and `action:icon` leave the
objectui#9573 alias population, which is now `view:form`, `view:grid`,
`view:list`. Their four reads are judged on their own arms. Both the
gate docblock and the gate test are amended.
- Changeset `10872-held-public-block-arms.md` (`'@object-ui/types':
minor`). A dated note on the pending `10872-public-block-zod-arms.md`,
whose "Not armed in this release" bullet this falsifies. The zod README
lists the six.

## The spec round's registration notes, re-measured at `main`
`0ffc423b1`

The arm follows the row, and the row follows the read points. Items 1–3
and 5 are pinned at the validator in
`held-public-block-arms-10872.test.ts`; item 4 is a renderer reading.

1. `action:group` publishes `name`, which is read nowhere. Its `size`
enum carries `md`, which inline mode hands raw to the primitive. **Still
true.** The arm refuses both, with the row's prescription.
2. `element:definition-list` publishes `columns` as the strings `'1'` /
`'2'`, and the renderer compares the number `2`. **Still true.** The arm
refuses `'2'` and accepts `2`.
3. `element:repeater` advertises `fields[].label` (its TS interface and
its registration description), which is never rendered. **Still true.**
The arm refuses it.
4. `action:menu` spreads `...rest` after `disabled={loading}`.
**Reproduced** with a probe through the real `SchemaRenderer` and
registry: an in-flight action (an `autoTrigger` member whose handler
never settles) shows the spinner on both mount channels, but the trigger
is `disabled: true` only on a direct registry mount and `disabled:
false` through `SchemaRenderer`, whose forwarded `disabled: undefined`
wins. It is reported on the card as a class-(a) finding; it is not fixed
here.
5. `objectName`: the 17.5.0 rows declare it on `action:button` and
`action:icon`. On `action:group` / `action:menu` it rides each member
(`actions[]` members are open records) and is refused at container
level. Pinned.

**The registrations did not move in this PR.** Items 1–3 sit in
`apps/console/src/__tests__/registry-inputs-spec-parity.test.ts`'s
objectui#11111 ledger, which the maintainer's decision 3 = B routes to
objectui#11168 ("nothing else may own an entry"). objectui#11168's slice
1 (the `action:*` family, including `renderers/action/`) was claimed at
2026-09-30T05:39Z, before this card's claim. So the registration moves,
and the item-4 renderer fix, would duplicate in-flight work. They are
reported on the card instead. This PR does not touch
`registry-inputs-spec-parity.test.ts` or any renderer.

## Tests and gates (all after the final commit, `c1ea153ad`)

- `pnpm exec vitest run packages/types/ packages/cli/src/__tests__/
packages/components/src/renderers/action/
packages/components/src/renderers/basic/
apps/console/src/__tests__/registry-inputs-spec-parity.test.ts`: 351
files, **7678 passed, 1 failed**. The failure is the zod-mirror-parity
census above, and nothing else.
- Arm-list consumer sweep: 54 files, 2878 passed, 0 failed. It covered:
- app-shell `block-config-schema-parity-8216`, `block-config`,
`definition-list-item-keys-8279` and
`PageBlockInspector.retiredBlockProps`;
  - `examples/schema-catalog/test/`;
- console `public-contract`, `html-tier-manifest`,
`component-input-union-specimens` and `unfulfilled-chart-stubs-8760`;
- plugin-dashboard `metricCardRegisteredInputsStrictFace-11022` and
plugin-map `bareMapKeyRetired-10393`;
  - the scripts tests that read the zod directory.
- Every test that names `check-handler-key-read-sites`: 5 files, 132
passed.
- `type-check` for `@object-ui/types` (three programs;
`tsconfig.test.json` lists the three changed test files) and
`@object-ui/cli`, after `pnpm --filter '@object-ui/cli^...' build`: exit
0. `@object-ui/components` is not in the diff.
- eslint on the 8 changed code files: 0 errors, 0 warnings, 0 ignored
(`--format json`). `eslint.config.js` configures no type-aware parsing,
so this diff cannot move a verdict on an untouched file.
- These gates exit 0: `check-changeset-presence`, `check:control-bytes`,
`check:new-line-citations` (0 new), `check:spec-symbols`,
`check:handler-key-reads`, `check:changeset-claims` (report-only),
`check:pending-changeset-literals`, `check-changeset-no-major`.
- NOT MEASURED: `check:readme-exports`. Reason: it needs every package's
`dist` and reported "the population COLLAPSED" on this unbuilt tree. CI
runs it.
- Ablation, both legs through `ablation-replace.mjs`: the anchor hit
once, the write was verified on disk, and the restore was proven (blob
equals HEAD, `git diff HEAD` empty).
- Leg A drops the six from the union: 60 of 179 tests go red across the
three pin files.
- Leg B deletes `action:button`'s `onClick` / `onSuccess` members:
`check:handler-key-reads` exits 1 naming both reads, and 2 held-arm
tests go red.

## Overlap

`git merge-tree` of this head with objectui#11069, #11115 and #11125: my
files merge cleanly with all three. objectui#11069 conflicts in
`packages/cli/src/commands/check.ts`, a file this PR does not touch; the
same conflict appears against the base alone.

## Acceptance notes

- **The taught `action:button` node is flat, and the strict face refuses
it.** AGENTS.md #4, the quick-start, the layout guide and the other
guides write `label` / `actionType` / `target` on the node. The tolerant
face `objectui validate` runs today passes that node (which is what the
page pin reads). `StrictAnyComponentSchema` refuses exactly `actionType`
and `target`, as `PageComponentSchema` does, and this is pinned as a
reading. objectui#11069 moves `objectui validate` onto the strict face.
When it next merges `main`, this PR's taught-node page pin turns red
there. That is the flat-props question for `action:*`, which this card
holds, and it is raised on the card.
- **Content channels are not narrowed.** The six arms carry no
objectui#9256 `children` / `body` refusal: that measurement never
covered them. They keep `BaseSchema`'s channels, and the module docblock
and README say so.
- **Inference, not reproduced, not filed:** inline `action:group` also
spreads `...rest` onto its wrapping div. This is the item-4 shape on a
div.

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

---------

Co-authored-by: Claude <noreply@anthropic.com>
…00 entry

The pending objectui#7200 changeset gives, as its reason for not using a
never-typed tombstone, that ObjectFormSchema's zod mirror does not declare
`sections`. objectui#6152 round 1 (this PR) mirrors `sections` with a
closed section entry, so that parenthetical goes false when this lands.
The changeset-claim re-read on PR #11125 asked for exactly this read.

Per the maintainer's append-only ruling for pending changesets, one dated
note is appended; the frontmatter and every existing line are byte-identical
to main. Measured with a scratch probe at 4ed5adf: the strict authoring
face refuses a section `className` as unrecognized_keys at sections.0, and
the tolerant face accepts the document and drops the key.

Claude-Session: https://claude.ai/code/session_012UwY3ahMixEFkfTUxMVkYm
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) 3570.1 KB 3607.4 KB
Main entry chunk (gzip) 149.6 KB 350 KB
Entry file index-DsxdmOp_.js —
Status PASS —

The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it.


📦 Bundle Size Report

Package Size Gzipped
app-shell (consoleActionDispatch.js) 0.20KB 0.19KB
app-shell (index.js) 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) 559.46KB 134.18KB
core (index.js) 9.94KB 3.94KB
create-plugin (index.js) 27.94KB 9.51KB
data-objectstack (index.js) 227.28KB 63.04KB
fields (index.js) 261.19KB 66.27KB
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.17KB 15.06KB
plugin-charts (index.js) 84.09KB 22.93KB
plugin-chatbot (index.js) 198.22KB 46.97KB
plugin-dashboard (index.js) 138.73KB 37.05KB
plugin-designer (index.js) 215.78KB 44.42KB
plugin-detail (index.js) 234.78KB 62.36KB
plugin-editor (index.js) 2.23KB 1.05KB
plugin-form (index.js) 172.30KB 44.19KB
plugin-gantt (index.js) 172.43KB 42.85KB
plugin-grid (index.js) 230.25KB 63.22KB
plugin-kanban (index.js) 48.43KB 15.11KB
plugin-list (index.js) 115.82KB 28.67KB
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) 32.26KB 9.42KB
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) 9.45KB 3.76KB
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.82KB 7.15KB
types (system-fields.js) 3.33KB 1.54KB
types (theme.js) 6.28KB 2.87KB
types (ui-action.js) 8.11KB 3.32KB
types (views.js) 0.20KB 0.18KB
types (widget.js) 0.20KB 0.18KB

Size Limits

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

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 8f9bb76f6c5ceb93420fafbbf7c8041eae11e68c
Local-runs: none

Director seat (objectstack#12708, summon #30 续 2). The head moved past this seat's PASS 5904218639 (a5884b4030…): a base merge of main at b0a05dda (f3e9d830, carrying objectui#11115, #11175, #11177, #11172, #11171 and the earlier landings) and two commits of the seat's own that append dated corrections to pending changesets (4ed5adf6 for the objectui#10993 entry, 8f9bb76f for the objectui#7200 entry). That record no longer names the head; this one replaces it. Inputs: the previous record and its inputs, the net diff against main e978ed5e at this head (8 files, +384 / −51), the three .changeset faces in that net diff read in full, and the check-runs on the head. git merge-tree --write-tree origin/main against the head is clean.

① Derived judgments

The PR's own change is what the previous record judged. The net diff against main holds the same non-changeset files as before — packages/types/src/zod/objectql.zod.ts and its four test files — and the base merge introduced no main commit that touches objectql.zod.ts or the object-form mirror, so every judgment of 5904218639 stands: the mirror declares the members the TypeScript twin declares, shaped as the twin shapes them, by reference to the spec's I18nLabelSchema where the twin is I18nLabel; submitHandler and open deliberately unmirrored; Clause-②: yes (widening) on the strict face and a refusal of wrong-typed values on the tolerant face, stated as breaking-for-invalid-documents.

The three changeset faces at this head:

  1. .changeset/6152-object-form-unmirrored-members.md (this PR's entry, @object-ui/types: minor) — unchanged in substance from the previous record, plus a dated correction at its end: the reason it gave for using no ?: never tombstone ("ObjectFormSchema in zod/objectql.zod.ts does not declare sections") is no longer true because this same PR mirrors sections, judged member by member against ObjectFormSection; a strict-face section refuses an undeclared key by name (unrecognized_keys at the section's path) and the tolerant face drops it. Read against objectql.zod.ts at the head: sections is mirrored as described. Right, and honest about its own earlier sentence.
  2. .changeset/10993-object-form-i18nlabel.md (pending, from the objectui#10993 landing) gains one dated paragraph: its Types paragraph said nextText and prevText stay unmirrored; PR feat(types): the object-form zod mirror declares the members its TypeScript twin declared (objectui#6152, round 1) #11125 mirrors both by the same I18nLabelSchema reference, so safeValidateSchema judges them like the other five label members (a locale map accepted, a number refused at the member). The mirror at the head declares both that way. Right. The correction is appended, dated and attributed; the original line is left as written, which is the form the repository's other corrections this week take (objectui#11073, feat(types,plugin-detail): declare the field-security triple on record:details / record:highlights / record:related_list and read it un-cast (objectui#8649) #11184).
  3. .changeset/7200-object-form-section-style-keys-undeclared.md (pending) gains the twin paragraph about sections: a section now has a parse door, so a section className (or any undeclared section key) is refused by name on the strict face and dropped on the tolerant face. Consistent with 1 and with the mirror. Right.

Nothing else in the net diff is a review face.

② Semver level

Unchanged: Clause-②: yes (widening); minor on @object-ui/types, with the tolerant-face refusal of wrong-typed values named in the body as the break (objectui's fixed group never declares major). The two pending entries corrected keep their own levels.

③ Boundary flags

Check-runs on the head, read 2026-09-30T09:28Z — none in_progress, none red.

Implemented-by: claude/issue-6152-unmirrored-round1
Reviewed-by: session_01AsCNgFBs8HCjwhyHQsFbx3

VERDICT: PASS

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Correction to record 5908321649 · director seat 2026-09-30T09:29Z

One sentence in ① is wrong and is withdrawn: "the base merge introduced no main commit that touches objectql.zod.ts". It did — objectui#11115 (b0a05dda, merged 2026-09-30T07:22Z), which declares the read keys the strict authoring face refused (objectui#11070), edits packages/types/src/zod/objectql.zod.ts too, on other members (form.showSubmit, the form.fields[] keys, dataSource on the data blocks). The merge f3e9d830 resolved cleanly and the net diff of this PR against main e978ed5e still holds only this PR's object-form mirror additions, so the judgments stand as written; the provenance sentence should have said "no main commit touches the object-form members this PR mirrors". The verdict is unchanged: PASS at 8f9bb76f6c5ceb93420fafbbf7c8041eae11e68c.

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.

2 participants