Skip to content

fix(plugin-form): MasterDetailForm resolves its I18nLabel title, submitText and cancelText (objectui#10935) - #10986

Merged
objectstack-fleet[bot] merged 5 commits into
mainfrom
claude/issue-10935-master-detail-i18n-labels
Sep 28, 2026
Merged

objectstack-fleet[bot] merged 5 commits into
mainfrom
claude/issue-10935-master-detail-i18n-labels

Conversation

@objectstack-fleet

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

Copy link
Copy Markdown
Contributor

Fixes #10935
Clause-②: yes

MasterDetailForm resolves its three I18nLabel members, title, submitText and cancelText, with pickLocalized against the active UI language, the way ObjectMetricWidget does (triage comment 5867953691). MasterDetailFormSchema types the three as I18nLabel, as @objectstack/spec's ComponentPropsMap['object-master-detail-form'] does; the zod arm is not narrowed. Draft, head c357b233a (the repair round; the first round was dbb004b16): a dispatched dev's PR (claim comment 5869374008), so ready and landing belong to the seat.

What changed

  • packages/plugin-form/src/MasterDetailForm.tsx
    • MasterDetailFormSchema.title, .submitText and .cancelText go from string to I18nLabel (imported as a type from @object-ui/types).
    • MasterDetailForm reads useObjectTranslation().language once, the source ObjectMetricWidget (and the MetricWidget it forwards to) resolves its I18nLabel members against. title resolves once and feeds the built-in edit-save toast and the parent ObjectForm's title (the string that ObjectFormSchema.title declares). submitText and cancelText resolve where the two Buttons read them.
    • Defaults: with nothing authored the text is unchanged ('Create' or 'Save', 'Cancel', 'Created' or 'Saved'). The button fallback is now || where it was ??, so an authored empty string, or a map with no string entry, shows the English default. An authored empty string used to render an empty button; a map with no string entry used to throw as a React child.
  • packages/plugin-form/src/MasterDetailForm.i18nLabels.test.tsx (new): the pins, below.
  • packages/plugin-form/src/MasterDetailForm.test.tsx: the existing host-string cancelText test was named "i18n is the host's job" and commented "the plugin is locale-agnostic". This change makes both false, so the name and comment are reworded. The assertion is unchanged.
  • packages/types/src/zod/objectql.zod.ts: comment only. The ObjectMasterDetailFormBlockSchema docblock said MasterDetailFormSchema types the three labels "as a plain string", which this change makes false. It now says I18nLabel and names the type pin.
  • packages/plugin-form/src/index.tsx: the object-master-detail-form inputs title, submitText and cancelText declare type: ['string', 'object'], with descriptions that teach the per-locale map (repair round, c357b233a).
  • apps/console/src/__tests__/: three MEMBER_PINS rows in registry-inputs-spec-parity.test.ts, and the new public-door pin masterDetailFormI18nLabelManifest.test.ts (repair round, c357b233a).
  • .changeset/10935-master-detail-i18n-labels.md: '@object-ui/plugin-form': minor, with the Clause-②: yes line.

Premise checks, on origin/main 328abeb55

  • The three raw reads are where the card puts them. They are schema.cancelText ?? 'Cancel' and submitText (schema.submitText ?? …) as Button children, and the ${schema.title} saved toast in handleSaved. title has one more read on this node: the parentSchema memo forwards it to the parent ObjectForm. That forward now gets the resolved string, which is the same one-line repair.
  • Locale source. ObjectMetricWidget resolves label / title with pickLocalized(…, language), where language comes from useObjectTranslation(), the UI language. This file already imported useDisplayLocale(), but that is the NUMBER locale: it puts the tenant's localization before the UI language. So it is not the source for label text, and it is not used for it.
  • Spec-row census. The row has exactly three members that are I18nLabel: title, submitText and cancelText. details, sections and fields are z.array(z.unknown()), so no nested label is typed there. The census came from enumerating the installed spec row (17.4.0) at runtime, not from reading the source. Nothing else rides this PR.
  • Published or not. src/index.tsx exports MasterDetailFormSchema from the package entry (exports['.'] is dist/index.d.ts). After the build, dist/MasterDetailForm.d.ts reads I18nLabel for all three. So the type widening is published: minor, and Clause-②: yes stands.
  • Consumers of the widened type. git grep MasterDetailFormSchema in *.ts / *.tsx outside plugin-form finds only two comment or string mentions in @object-ui/types, and no code that reads the type (control: the same grep hits plugin-form/src/index.tsx). ObjectForm writes the three members from its own string fields, which a widening keeps compiling.

Evidence

All at dbb004b16 unless a line says otherwise. The repair round's readings at c357b233a are in the next bullet. The runs went through the shared verify lock, on a shared box.

  • Repair round, at c357b233a (contract review 5871765302): the public-door pin with index.tsx at dbb004b16 gave 6 failed | 7 passed (13) (the three arm rows and three map rows); at the head, 13 passed. Registry-reading suites: Test Files 10 passed (10) / Tests 309 passed (309). The cli ratchet and the schema-catalog gallery: 2 passed / 601 passed. plugin-form and apps/console type-check exit 0. Gates exit 0, including check-changeset-presence and check:new-line-citations. CI: 43 check-runs, 40 success, 3 skipped, 0 failed, Spec Main Shape Gate included.

Pins, through the real SchemaRenderer and the registry. Each node is authored in the { type, properties } form under an I18nProvider set to zh. Every map lists en first, so falling back to en or to the first entry fails the row.

  • The locale-map documents the rows mount pass safeValidateSchema.
  • A map submitText renders 保存订单 on the Save button.
  • A map cancelText renders 返回 on the Cancel button. onCancel sits beside the document as a host key: the arm refuses it as a runtime slot, and the button renders only when it is present.
  • A map title toasts 采购单 saved on an edit save.
  • Control: plain strings render as authored (Save PO, Discard, PO saved).
  • Control: with nothing authored, the defaults are unchanged (Create, Cancel, Save, Saved).
  • Type pin: assertionLabelMembersAreI18nLabel, an Expect/Equal tuple that holds all three members equal to I18nLabel | undefined.

Red on base, then green (ablation). MasterDetailForm.tsx was committed first, then replaced on disk by its 328abeb55 blob, under a trap-restore. The hash matched the base blob and the pickLocalized count went from 5 to 0.

  • vitest run packages/plugin-form/src/MasterDetailForm.i18nLabels.test.tsx gave Tests 3 failed | 3 passed (6). The submitText and cancelText rows failed on "Objects are not valid as a React child (found: object with keys {en, zh-CN})". The title row failed on expected [ '[object Object] saved' ] to deeply equal [ '采购单 saved' ]. The validator row and the two controls passed.
  • tsc -p tsconfig.test.json failed on the three Expect lines (TS2344: Type 'false' does not satisfy the constraint 'true').
  • Restored: the hash equals the HEAD blob, and git diff HEAD is empty. At HEAD the same file gives Tests 6 passed (6), and with MasterDetailForm.test.tsx Tests 16 passed (16).

Consumer probe of the built .d.ts. A one-shot file, outside the repo, imports MasterDetailFormSchema through a paths entry pointing at packages/plugin-form/dist/index.d.ts. Assigning locale maps to title and submitText compiles. With the @ts-expect-error dropped from a submitText: 42 line, it fails with TS2322 … not assignable to type 'string | (Record … ) | undefined', which proves the rebuilt declaration is read and is not any.

Suites, type-check and lint.

  • vitest run packages/plugin-form/, run together with a one-off manifest probe file (since deleted), gave Test Files 138 passed (138), Tests 1600 passed | 1 skipped, at c4d7c74f6. That is 137 plugin-form files plus the probe. The two later commits change test files, a type-only pin and one comment in @object-ui/types. The two changed plugin-form test files were re-run at dbb004b16, as above.
  • vitest run packages/types/ gave Test Files 268 passed (268), Tests 5955 passed.
  • vitest run scripts/ gave Test Files 177 passed | 2 skipped (179), Tests 5333 passed | 2 skipped.
  • pnpm --filter @object-ui/plugin-form type-check (tsc --noEmit && tsc -p tsconfig.test.json) exits 0. The test project lists the new pin file (--listFilesOnly).
  • pnpm --filter @object-ui/types type-check exits 0.
  • Before these, the plugin-form dependency closure was built (turbo run build --filter='@object-ui/plugin-form^...', 11 of 11 tasks, restored from the turbo cache), and plugin-form itself was built.
  • eslint over the four touched source files: 0 errors. The new test file has no warnings.

Gates (each exit 0): check:new-line-citations (0 new citations), check:control-bytes, check-changeset-presence, changeset:check, check:changeset-claims, check:pending-changeset-literals, check:vi-mock-specifiers, check:vi-mock-inherit, check:vi-mock-override-shape, check:test-path-roots, check:phantom-deps, check:handler-key-reads, check:i18n-keys, check:unreferenced-sources. check-governed-queue-guard --test over the five paths answers NOT GOVERNED.

Bundle. No new module enters the graph: @object-ui/i18n was already imported by this file, and only pickLocalized and useObjectTranslation join the named imports. A single-file esbuild minify of MasterDetailForm.tsx, base against head, gives +95 bytes minified and +50 bytes gzip. That is a proxy for the plugin-form chunk, not the console's eager-closure gate: see NOT MEASURED.

Bundle, repair round (from delta review 5872990965). apps/console's register-plugins.ts imports @object-ui/plugin-form eagerly, so the three new input descriptions ship in the eager closure. The budget comment on c357b233a reads 3104.4 KB against the 3104.5 KB ceiling (PASS), against 3103.9 KB on dbb004b16 and on every other PR built after objectui#10992. So this round costs about +0.5 KB, and landing it leaves about 0.1 KB of eager headroom on main. The exhausted headroom is carried by objectui#10996.

Acceptance notes

  • NOT MEASURED: check:eager-closure / Bundle Analysis. Reason: it needs a full console build. The proxy delta is above.
  • NOT MEASURED: check:readme-exports. Reason: it refuses to judge while 24 packages are unbuilt ("population collapsed"). This PR touches no README and no export name.
  • The registry inputs for these three keys now declare type: ['string', 'object'] (object-master-detail-form in plugin-form/src/index.tsx), with descriptions that teach the { en, 'zh-CN' } map. This repairs defect 1 of contract review 5871765302. Through the production public door (ComponentRegistry.getPublicConfigs() → manifestFromConfigs → validateTree), the 'string'-only declaration reported type-mismatch on a legal locale map, with the message prop "submitText" expected a string, and the same for title and cancelText. My first probe missed it because it read getAllConfigs(), which keys the block by its namespace, so the bare type answered unknown-component. apps/console/src/__tests__/masterDetailFormI18nLabelManifest.test.ts pins the door. With index.tsx at dbb004b16 it gave 6 failed | 7 passed (13); at c357b233a all 13 pass. A string, and a number that is still reported, are the controls. The console parity gate's three MEMBER_PINS rows point at MasterDetailForm.i18nLabels.test.tsx.
  • The English defaults stay English, as before: 'Create', 'Save', 'Cancel', 'Created', 'Saved', 'Saving…', and the " saved" suffix after an authored title. This PR only resolves authored labels.
  • The same shape on the sibling object-form node is in the report as a finding for the seat to judge, and is not touched here. The spec row takes a locale map for submitText, and a probe through the real SchemaRenderer renders Component "form" failed to render with "Objects are not valid as a React child". ObjectFormSchema types it string.

Generated by Claude Code

Body amended by the domain:ui seat #1 with the dev's repair-round text, after contract review 5871765302; no code claim moved beyond that round's commit.

…itText and cancelText (objectui#10935)

`@objectstack/spec` types the three as `I18nLabel` in
`ComponentPropsMap['object-master-detail-form']`, and the validator arms the
node with that row, so a per-locale map is an accepted document. The form read
them raw: a map as a Button child threw, and a map `title` toasted
"[object Object] saved". Each now goes through `pickLocalized` against
`useObjectTranslation().language`, as `ObjectMetricWidget` does, and
`MasterDetailFormSchema` types them `I18nLabel`.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DuWo5bdP9SdVebamn99GGk
… members (objectui#10935)

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DuWo5bdP9SdVebamn99GGk
…g to any (objectui#10935)

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DuWo5bdP9SdVebamn99GGk
… two sentences the widening made false (objectui#10935)

- `MasterDetailForm.i18nLabels.test.tsx` gains a compile-time pin
  (`assertionLabelMembersAreI18nLabel`) that the three members equal the
  row's `I18nLabel`.
- `ObjectMasterDetailFormBlockSchema`'s docblock said `MasterDetailFormSchema`
  types the three as a plain string; it now says `I18nLabel` and names the pin.
- The existing host-string `cancelText` test no longer calls the plugin
  locale-agnostic: it resolves per-locale maps now.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DuWo5bdP9SdVebamn99GGk
@github-actions

github-actions Bot commented Sep 28, 2026 •

Copy link
Copy Markdown
Contributor

changeset-claim-re-read

⚠️ 21 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.

.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/7200-object-form-section-style-keys-undeclared.md

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

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

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

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

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

.changeset/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/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/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/8067-component-input-member-kind.md

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

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

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

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

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

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 79a935c87 (merge-base with origin/main): 7 file(s) changed outside .changeset/, read against 1700 pending declaration(s) that publish a body (2302 pending in total). · run

@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Eager closure (gzip, 330 chunks) 3103.9 KB 3104.5 KB
Main entry chunk (gzip) 149.7 KB 350 KB
Entry file index-FBdBufRh.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.58KB 6.17KB
app-shell (runtime-config.js) 20.68KB 7.36KB
app-shell (types.js) 0.01KB 0.04KB
app-shell (urlParams.js) 10.06KB 3.86KB
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.52KB 3.45KB
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.59KB 133.88KB
core (index.js) 9.93KB 3.94KB
create-plugin (index.js) 27.94KB 9.51KB
data-objectstack (index.js) 227.61KB 63.16KB
fields (index.js) 261.01KB 66.28KB
i18n (LocalizationContext.js) 1.76KB 0.96KB
i18n (builtinAggregateLabels.js) 0.86KB 0.49KB
i18n (currency.js) 2.59KB 1.22KB
i18n (fallbackInterpolation.js) 6.25KB 2.77KB
i18n (i18n.js) 8.87KB 3.64KB
i18n (index.js) 5.24KB 2.27KB
i18n (pickLocalized.js) 9.86KB 3.95KB
i18n (provider.js) 39.40KB 12.91KB
i18n (translateFn.js) 0.20KB 0.18KB
i18n (useDisplayLocale.js) 3.52KB 1.76KB
i18n (useObjectLabel.js) 34.34KB 9.17KB
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.52KB 4.88KB
permissions (PermissionContext.js) 0.31KB 0.25KB
permissions (PermissionGuard.js) 0.89KB 0.45KB
permissions (PermissionProvider.js) 6.24KB 2.16KB
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) 51.96KB 14.83KB
plugin-charts (index.js) 84.09KB 22.93KB
plugin-chatbot (index.js) 198.22KB 46.97KB
plugin-dashboard (index.js) 137.83KB 36.71KB
plugin-designer (index.js) 215.78KB 44.42KB
plugin-detail (index.js) 233.51KB 61.80KB
plugin-editor (index.js) 2.23KB 1.05KB
plugin-form (index.js) 161.33KB 41.45KB
plugin-gantt (index.js) 170.35KB 42.19KB
plugin-grid (index.js) 228.33KB 62.59KB
plugin-kanban (index.js) 48.43KB 15.11KB
plugin-list (index.js) 115.86KB 28.64KB
plugin-map (index.js) 22.90KB 7.62KB
plugin-markdown (index.js) 13.88KB 4.80KB
plugin-report (index.js) 44.17KB 12.20KB
plugin-timeline (index.js) 31.07KB 9.15KB
plugin-tree (index.js) 11.21KB 3.89KB
plugin-view (index.js) 89.44KB 22.41KB
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.16KB 39.05KB
react (data-invalidation.js) 5.05KB 2.08KB
react (index.js) 4.03KB 1.86KB
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) 7.50KB 3.05KB
sdui-parser (dashboard-widget-options.js) 3.08KB 1.30KB
sdui-parser (index.js) 6.16KB 2.71KB
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) 18.27KB 6.22KB
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) 2.93KB 1.49KB
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) 17.15KB 6.32KB
types (system-fields.js) 3.33KB 1.54KB
types (theme.js) 6.27KB 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: dbb004b16ed21503c4a1aa1bd48da34d4af487a5

Card objectui#10935 · PR objectui#10986 · branch fetched as refs/review/pr-10986; merge-base 328abeb55, origin/main at 42687baf2. Every local probe ran in a throwaway worktree at the head (pnpm install --frozen-lockfile --offline, then the plugin-form dependency closure built with turbo run build --filter='@object-ui/plugin-form...', 12 of 12 tasks). Each probe file was removed afterwards and the worktree ended with an empty git status.

① Derived judgments

Renderer. MasterDetailForm reads useObjectTranslation().language once and resolves title (once, as titleText), submitText and cancelText with pickLocalized. That is the same pair ObjectMetricWidget, and the MetricWidget it forwards to, use for their I18nLabel members (pickLocalized(label, language) against useObjectTranslation().language); useDisplayLocale() stays the number locale and here still feeds only toLocaleString. A map resolves exact tag → base → region sibling (zh finds zh-CN) → default → en → first string entry → '', after which the two buttons fall to their English default: sane. A plain string passes through pickLocalized unchanged (control pinned). The ?? → || move is stated in the changeset and the PR body, and a stored document can reach it: the installed spec row (17.4.0) and objectui's safeValidateSchema both accept submitText: '' and cancelText: {} on the properties form (measured), so a document that used to render an empty Save or Cancel button now renders 'Save'/'Create' or 'Cancel'. title is forwarded to the parent ObjectForm as titleText || undefined; ObjectForm has no display read of title (its only schema.title use is the delegation back into MasterDetailForm), and '' was already falsy in the toast, so nothing else changes. useObjectTranslation degrades to i18n.language || 'en' without a provider, which is why the provider-less MasterDetailForm.test.tsx still passes.

Type. MasterDetailFormSchema.title, .submitText and .cancelText are I18nLabel imported from @object-ui/types, whose built index.d.ts re-exports it by reference from @objectstack/spec/ui (z.input of I18nLabelSchema: string | InlineLocaleMap), the same type the row uses. The zod arm is untouched: the objectql.zod.ts hunk has no non-comment line, and ObjectMasterDetailFormBlockSchema still takes properties as the spec row by reference through stripImportedDefaults. The built dist/MasterDetailForm.d.ts reads I18nLabel for all three and dist/index.d.ts exports the interface. My own one-shot consumer outside the repo, through paths to dist/index.d.ts, compiles locale maps on title and submitText; with its @ts-expect-error dropped it fails submitText: 42 with TS2322 naming string | (Record of string to string …) | undefined, so the rebuilt declaration is read and is not any. The MasterDetailDetailConfig.title?: string two interfaces up is the detail-collection heading, outside the row (details is z.array(z.unknown())).

Census. I enumerated the installed row at runtime (ComponentPropsMap['object-master-detail-form'], 17.4.0): 14 members, exactly three string | record unions, title, submitText, cancelText. objectstack origin/main (8cdbe0c6e, spec still 17.4.0) declares the same three with I18nLabelSchema, with objectName and details optional. The dev's census holds.

Extra files. objectql.zod.ts docblock: the old sentence said MasterDetailFormSchema types the three "as a plain string", which this diff makes false; the new text (I18nLabel, pinned by assertionLabelMembersAreI18nLabel; objectName/details still required here and optional on the row) is true against the row. MasterDetailForm.test.tsx: the test name "i18n is the host's job" and the comment "the plugin is locale-agnostic" were made false; the assertion is unchanged. Both changes were needed. No other stale sentence found: the repo's remaining "host's job" / "locale-agnostic" hits belong to other components, and none of the 20 pending changesets that name objectql.zod.ts (nor 10859-pivot-object-block-zod-arms.md) states the old string typing.

Pins. At head, MasterDetailForm.i18nLabels.test.tsx with MasterDetailForm.test.tsx: 2 files, 16 passed. Every map lists en first under a zh provider, so the rows pass only by following the active language. Ablation, run by me: the renderer replaced on disk by its 328abeb55 blob (hash 1a74a580d, pickLocalized count 0) gives 3 failed | 3 passed (6): submitText and cancelText fail on "Objects are not valid as a React child (found: object with keys {en, zh-CN})", title on expected [ '[object Object] saved' ] to deeply equal [ '采购单 saved' ]; the validator row and both controls stay green. tsc -p tsconfig.test.json in the same state: exactly three TS2344 (Type 'false' does not satisfy the constraint 'true') on the three Expect lines. Restored to the head blob (80947252f), git status empty; at head the test project type-checks (exit 0) and lists the pin file. The controls are real.

CI. 43 check-runs on the head, all completed: 40 success, 3 skipped (dependabot and the two coverage rows), 1 failure, Spec Main Shape Gate. Its annotations are TS2322 and TS2339 ("Property 'isDefault' does not exist on type 'never'") on packages/types/src/__tests__/p1-spec-alignment.test.ts, compiled against objectstack 6e3e5462c6d1, a file this PR does not touch. Sibling PR #10984 (head cdb89afa4, an unrelated diff) fails the same gate with the same two annotations, same file, same sha: the red on this head has exactly the base's signature and no other, so it does not count against this PR (the repair is objectui#10987). Bundle Analysis is success; the console budget comment reads eager closure 3103.9 KB against a 3104.5 KB budget, PASS, the same 3103.9 KB that #10983 and #10985 report, so this PR adds nothing to the eager closure (plugin-form is outside it; the file's own delta is +50 B gzip).

Commits. Four, dfff0591c through dbb004b16; no model name in any subject, body or trailer (the trailers carry the co-author line and the session link).

② Semver level

minor for @object-ui/plugin-form is right: three members of an exported interface widen from string to I18nLabel; every in-repo writer (ObjectForm's delegation, DrawerForm, ModalForm, the console's DevMasterDetail) passes strings and compiles unchanged; nothing accepted before is refused; the house rule is that Clause-②: yes takes at least minor. The fleet reader (clause2-line.mjs) reads both the changeset and the PR body as declared: yes, no arm (the (widening) arm is optional).

Changeset sentences: all true but one. "On the two buttons, an authored empty string, or a map with no string entry, now shows the English default. The old ?? fallback rendered an empty button for both." is false for the map half: under ?? a map is not nullish, so {} (a document both validators accept, measured) reached the Button as a child and threw; only the empty string rendered an empty button. Defect 2 below.

③ Boundary flags

Clause-②: yes is honest and complete: the widening is the published MasterDetailFormSchema, the zod arm does not move, and no other export changes.

Registry inputs (defect 1). The dev's note is true: the object-master-detail-form inputs for title, submitText and cancelText in packages/plugin-form/src/index.tsx still declare type: 'string', and so does the manifest built from them. It is not a harmless deferral. The ComponentInput docblock in packages/types/src/base.ts rules: declare only arms the contract accepts AND the renderer resolves; element:record_picker.emptyText was held at 'string' until objectui#5590 taught its render site to resolve the map, "and the second arm was declared in that same change — which is the order this rule prescribes, not an exception to it". This PR is that change for these three keys and leaves the arm out. And the public door does answer wrongly: through the production route (ComponentRegistry.getPublicConfigs() → manifestFromConfigs → validateTree), a document carrying a locale map on submitText gets a type-mismatch warning, "prop submitText expected a string" (the same for title and cancelText; a string control gets no diagnostic). That is the self-contradiction the docblock names, on a write the spec row, safeValidateSchema and this head's renderer all accept. The dev's probe stopped at unknown-component because it fed getConfig's namespaced entry (plugin-form:object-master-detail-form) rather than the public configs, so the PR body's "I have no public-door reading of a wrong answer" is false.

Out-of-scope finding, class b: verified. The installed object-form row has seven I18nLabelSchema members (title, description, submitText, cancelText, nextText, prevText, successMessage) and parses a map submitText. Through the real SchemaRenderer under a zh provider, { type: 'object-form', properties: { objectName, submitText: { en, 'zh-CN' } } } renders Component "form" failed to render with "Objects are not valid as a React child (found: object with keys {en, zh-CN})"; the string control renders Save order. ObjectForm reads schema.submitText || … straight into submitLabel, cancelText into cancelLabel and successMessage into the toast; DrawerForm and ModalForm render schema.title and description raw as headings. One difference from #10935 the card should carry: objectui's own object-form arm is a flat mirror (ObjectFormSchema, submitText: z.string()), so safeValidateSchema refuses the map ("expected string, received object") and refuses the properties form at objectName; on this node only the spec-side validator accepts the map, and the objectui mirror is narrower than the spec. Yes, a family card: "spec row declares I18nLabel, the objectui renderer reads it raw", with #10935 as its first member (closed by this PR), object-form's seven as the second, and a census of the other rows for the same shape.

PR body. No line addresses, no angle brackets. Two false sentences: the "instead of an empty button" claim for the string-less map (as in the changeset), and the "no public-door reading of a wrong answer" conclusion (above).

Defects, each with its fix.

  1. packages/plugin-form/src/index.tsx: the three inputs declare 'string' only while this PR teaches the render site to resolve the map, so the manifest gate warns type-mismatch on a legal locale map. Fix: declare type: ['string', 'object'] on title, submitText and cancelText, with a description teaching the { en, 'zh-CN' } map as the element:text_input trio does, and add the three MEMBER_PINS rows the console registry-inputs-spec-parity.test.ts asks for, pointing at MasterDetailForm.i18nLabels.test.tsx, which already pins the resolution; then correct the PR body's acceptance note.
  2. .changeset/10935-master-detail-i18n-labels.md, and the PR body's "What changed": "The old ?? fallback rendered an empty button for both" is false, the map half threw. Fix, one sentence: "An authored empty string used to render an empty button; a map with no string entry used to throw as a React child."

Implemented-by: claude/issue-10935-master-detail-i18n-labels
Reviewed-by: session_01DuWo5bdP9SdVebamn99GGk

VERDICT: FAIL


Generated by Claude Code

…l inputs, and correct the changeset's empty-button sentence (objectui#10935)

Contract review 5871765302 on PR objectui#10986 found two defects.

- The `object-master-detail-form` registration still declared `title`,
  `submitText` and `cancelText` as `'string'` while this PR teaches the render
  site to resolve the `I18nLabel` map. Through the public door
  (`getPublicConfigs()` -> `manifestFromConfigs` -> `validateTree`) a legal map
  drew `type-mismatch`. The three now declare `['string', 'object']`, with
  descriptions teaching the map, as `ComponentInput.type` prescribes. The
  console parity gate gets the three `MEMBER_PINS` rows it requires, and
  `masterDetailFormI18nLabelManifest.test.ts` pins the public door: a map is
  not reported, a string is not reported, a number still is.
- The changeset said the old `??` fallback rendered an empty button for an
  empty string AND a string-less map. The map half is false: it threw as a
  React child. The sentence now says so, and the changeset records the arm
  widening.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DuWo5bdP9SdVebamn99GGk
@github-actions github-actions Bot added the apps label Sep 28, 2026
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Eager closure (gzip, 330 chunks) 3104.4 KB 3104.5 KB
Main entry chunk (gzip) 149.8 KB 350 KB
Entry file index-D2bK6IOa.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.58KB 6.17KB
app-shell (runtime-config.js) 20.68KB 7.36KB
app-shell (types.js) 0.01KB 0.04KB
app-shell (urlParams.js) 10.06KB 3.86KB
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.52KB 3.45KB
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.59KB 133.88KB
core (index.js) 9.93KB 3.94KB
create-plugin (index.js) 27.94KB 9.51KB
data-objectstack (index.js) 227.62KB 63.16KB
fields (index.js) 261.01KB 66.28KB
i18n (LocalizationContext.js) 1.76KB 0.96KB
i18n (builtinAggregateLabels.js) 0.86KB 0.49KB
i18n (currency.js) 2.59KB 1.22KB
i18n (fallbackInterpolation.js) 6.25KB 2.77KB
i18n (i18n.js) 8.87KB 3.64KB
i18n (index.js) 5.24KB 2.27KB
i18n (pickLocalized.js) 9.86KB 3.95KB
i18n (provider.js) 39.40KB 12.91KB
i18n (translateFn.js) 0.20KB 0.18KB
i18n (useDisplayLocale.js) 3.52KB 1.76KB
i18n (useObjectLabel.js) 34.35KB 9.18KB
i18n (useSafeTranslation.js) 5.60KB 2.33KB
layout (index.js) 39.32KB 11.09KB
mobile (MobileProvider.js) 0.92KB 0.49KB
mobile (ResponsiveContainer.js) 0.94KB 0.38KB
mobile (breakpoints.js) 1.51KB 0.70KB
mobile (createOfflineDataSource.js) 5.61KB 1.75KB
mobile (index.js) 1.99KB 0.87KB
mobile (offlineQueue.js) 3.91KB 1.35KB
mobile (pwa.js) 0.97KB 0.49KB
mobile (serviceWorker.js) 1.48KB 0.62KB
mobile (serviceWorkerSource.js) 3.41KB 1.48KB
mobile (useBreakpoint.js) 1.54KB 0.65KB
mobile (useGesture.js) 6.96KB 1.98KB
mobile (useOfflineSync.js) 1.99KB 0.72KB
mobile (usePullToRefresh.js) 6.62KB 2.45KB
mobile (useResponsive.js) 0.72KB 0.42KB
mobile (useSpecGesture.js) 5.52KB 2.10KB
mobile (useTouchTarget.js) 1.01KB 0.54KB
permissions (MePermissionsProvider.js) 13.53KB 4.89KB
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) 51.96KB 14.83KB
plugin-charts (index.js) 84.09KB 22.93KB
plugin-chatbot (index.js) 198.22KB 46.97KB
plugin-dashboard (index.js) 137.83KB 36.71KB
plugin-designer (index.js) 215.78KB 44.42KB
plugin-detail (index.js) 233.49KB 61.79KB
plugin-editor (index.js) 2.23KB 1.05KB
plugin-form (index.js) 162.67KB 41.95KB
plugin-gantt (index.js) 170.35KB 42.19KB
plugin-grid (index.js) 228.33KB 62.59KB
plugin-kanban (index.js) 48.43KB 15.11KB
plugin-list (index.js) 115.86KB 28.64KB
plugin-map (index.js) 22.90KB 7.62KB
plugin-markdown (index.js) 13.88KB 4.80KB
plugin-report (index.js) 44.17KB 12.20KB
plugin-timeline (index.js) 31.07KB 9.15KB
plugin-tree (index.js) 11.21KB 3.89KB
plugin-view (index.js) 89.44KB 22.41KB
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.16KB 39.05KB
react (data-invalidation.js) 5.05KB 2.08KB
react (index.js) 4.03KB 1.86KB
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) 7.50KB 3.05KB
sdui-parser (dashboard-widget-options.js) 3.08KB 1.30KB
sdui-parser (index.js) 6.16KB 2.71KB
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) 18.27KB 6.22KB
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) 2.93KB 1.49KB
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) 17.15KB 6.32KB
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: c357b233abd719badeeb150090120ded9706048c

Delta over record 5871765302 (FAIL at dbb004b16), card objectui#10935, PR objectui#10986. The new head is one commit on dbb004b16 (its parent), touching four files: .changeset/10935-master-detail-i18n-labels.md, packages/plugin-form/src/index.tsx, apps/console/src/__tests__/registry-inputs-spec-parity.test.ts and the new apps/console/src/__tests__/masterDetailFormI18nLabelManifest.test.ts. Merge-base is still 328abeb55; origin/main is at 4b742f41d. Every judgment from 5871765302 that the delta does not touch stands as written there. Local probes ran in a fresh throwaway worktree at the head (pnpm install --frozen-lockfile --offline); each probe file was removed and the worktree ended with an empty git status.

① Derived judgments

Defect 1 (registry inputs): repaired. packages/plugin-form/src/index.tsx now declares type: ['string', 'object'] on title, submitText and cancelText, each with a description that teaches the { en, 'zh-CN' } map, the default, and the fallback chain (exact tag, base, region sibling, default, en, any remaining entry), which is pickLocalized's order; the toast and onCancel conditions in the descriptions match the renderer. registry-inputs-spec-parity.test.ts gains the three MEMBER_PINS rows, all pointing at MasterDetailForm.i18nLabels.test.tsx, and every claim in their prose is one 5871765302 or this review measured. The new public-door pin runs the production route, ComponentRegistry.getPublicConfigs() → manifestFromConfigs → validateTree, under the block's bare type, with a reachability row first so a missing block cannot pass the "not reported" rows vacuously. Re-run by me: at the head, 13 passed. With index.tsx replaced on disk by its dbb004b16 blob (hash 059806a71 verified): 6 failed | 7 passed (13), the three arm rows and the three map rows red, the reachability row and the string and number controls green; restored to c4ee697bf, git status empty. My own probe through the same route at the head: a map on each key draws no diagnostic, a string draws none, and a number (also a boolean, also an array) still draws type-mismatch, now worded "expected a string or an object". The parity suite passes at the head (199 tests).

Defect 2 (changeset sentence): repaired. The replaced sentence, "An authored empty string used to render an empty button; a map with no string entry used to throw as a React child", is true: under ?? the empty string reached the Button as '', and a map, {} included, reached it as an object child and threw. The new bullet (both arms declared; the manifest built from getPublicConfigs() no longer reports type-mismatch on a locale map for these keys; a number is still reported) is true as measured above. The Clause-② clause now also names the inputs widening from 'string' to ['string', 'object'], true, and "nothing that was accepted before is refused now" holds on both surfaces. Every other sentence is unchanged from the version 5871765302 read as true. The frontmatter is byte-identical across the two heads (same sha256 over the three lines).

Commit and CI. The one new commit carries no model name (its trailers are the co-author line and the session link). CI on c357b233a: 43 check-runs, all completed, 40 success, 3 skipped, 0 failed. Spec Main Shape Gate is success with no failure annotation: objectui#10992 merged at 14:25Z (40c076fc2, in origin/main's ancestry) and the gate builds the merge with main. Bundle Analysis, Type Check, Lint, the eight test shards, Test (dist pins), Build & E2E, Changeset Bump Policy, Changeset Claim Re-read, Line Citation Gate and Governed Surface Queue Guard are all success.

Bundle, a fact the PR body does not carry. The console budget comment on this head reads eager closure 3104.4 KB against the 3104.5 KB budget, PASS; on dbb004b16 it read 3103.9 KB. The base did not move: every other PR built after objectui#10992 merged (#10985 at 14:31Z, #10994 at 14:46Z, #10995 at 14:49Z) reads 3103.9 KB, so the +0.5 KB is this PR's repair round. The console's register-plugins.ts imports @object-ui/plugin-form eagerly, so the three new description strings (about 1.3 KB of prose) ship in the eager closure. The gate is green, so this is not a defect of this PR, but the remaining headroom is 0.1 KB, and the PR body's Bundle paragraph still reports only the first round's +50 B gzip while its repair-round bullet names the CI tally and not this figure. One sentence in the PR body would carry it; the seat should know before the next landing that touches an eagerly imported module.

② Semver level

minor for @object-ui/plugin-form still holds: the published interface widening from 5871765302 plus a registration widening (an input's declared arms grow), both additive; nothing accepted before is refused. The fleet reader parses the changeset's Clause-② line as declared: yes.

③ Boundary flags

PR body amendments, each checked against the head. (a) The opening now names head c357b233a with dbb004b16 as the first round: true. (b) The Defaults sentence is the corrected one and is true. (c) Two new "What changed" bullets (the index.tsx arms and descriptions; the three MEMBER_PINS rows and the new pin file): true. (d) The Evidence intro and the repair-round bullet: the pin's 6 failed | 7 passed (13) at dbb004b16 and 13 at the head are reproduced above; the CI tally (43 / 40 / 3 / 0, Spec gate included) matches REST; the local suite counts (10 files / 309 tests, 2 / 601, type-check and gates at exit 0) were not re-run by me and are covered by the green Test and Type Check runs. (e) The acceptance bullet now states the type-mismatch reading through the production door, names the pin, its red/green pair and its controls, and explains the first probe's unknown-component by the namespaced key: all consistent with what 5871765302 and this review measured. (f) The appended amendment note is accurate. Nothing else in the body changed, and no line addresses or angle brackets entered. The one thing the body now understates is the bundle cost above.

Clause-②: yes remains honest and complete: the interface widening and the inputs widening are both named in the changeset.

The 21 pending changesets the re-read bot lists (comment 5870894969). This PR's diff edits packages/types/src/zod/objectql.zod.ts in one docblock only (the ObjectMasterDetailFormBlockSchema comment; no non-comment line moves), and edits registry-inputs-spec-parity.test.ts by adding three MEMBER_PINS rows. I read all 21 in full at the head. None ships a sentence this diff makes false:

  • 5903-objectgantt-declared-keys (gantt keys, ObjectGanttSchema): no sentence about this block; unaffected.
  • 6940-rowactions-boolean-mirror (DataTableSchema.rowActions; the list-view rowActions in objectql.zod.ts "is not touched"): still true.
  • 7113-chart-data-model (ChartSchema; names objectql.zod.ts as a shipped refined mirror): still true.
  • 7200-object-form-section-style-keys-undeclared ("ObjectFormSchema in zod/objectql.zod.ts does not declare sections"): unchanged by this diff.
  • 7265-types-user-filter-field-derives (UserFilterFieldSchema.label stays a plain string because plugin-list renders it raw): a different schema; unaffected.
  • 7322-object-kanban-group-by-limit: kanban only; unaffected.
  • 7352-drill-down-config-mirror (ObjectDataTableSchema.drillDown): unaffected.
  • 7363-objectql-union-arms (ObjectQLComponentSchema gains gallery and data-table): unaffected; this diff adds no arm.
  • 7917-export-breadcrumb-object-tree-zod-schemas: barrel exports; unaffected.
  • 8067-component-input-member-kind (names the parity test; it is about ComponentInput.of): this diff declares type arms, not of, and adds no of; the "fifteen keys declare one" census and the union semantics it states are unchanged.
  • 8478-describe-line-addresses (no NAME.ext:NNN in .describe() text): the edited docblock is a comment, not a .describe(), and carries no line address; Line Citation Gate is green.
  • 8735-objectql-mirror-docblocks-not-defaulted (four other docblocks): unaffected.
  • 8801-object-kanban-allow-collapse-retired: kanban only; unaffected.
  • 8885-object-chart-drilldown-title-compareto (ObjectChartSchema.title is the spec's I18nLabel): unaffected.
  • 8913-object-kanban-columns-declared, 8990-object-kanban-groupby-optional, 9606-object-kanban-card-title: kanban only; unaffected.
  • 8992-user-actions-collapse-and-docblock (UserActionsSchema): unaffected.
  • 9309-object-gallery-filter-destination-typed, 9549-tree-filter-declared: filter on gallery and tree; unaffected.
  • 9511-record-id-is-a-string (its "lit" control, title: 42 still refuses on ObjectFormSchema): that is the flat object-form mirror, submitText: z.string() and title string-only, which this diff does not touch; the master-detail arm reads the spec row and is not that mirror. Still true.

Beyond the bot's list, the pending entries that discuss ['string', 'object'] arms (record-picker-empty-text-i18n-5590, text-input-i18n-label-arms-5717) claim nothing exclusive about which blocks declare the arm, so they stay true too.

Out-of-scope items from 5871765302 (the object-form family card; the eager-closure headroom above) are the seat's, unchanged.

Implemented-by: claude/issue-10935-master-detail-i18n-labels
Reviewed-by: session_01DuWo5bdP9SdVebamn99GGk

VERDICT: PASS


Generated by Claude Code

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review September 28, 2026 15:21
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Sep 28, 2026
Merged via the queue into main with commit 8aa68b1 Sep 28, 2026
45 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/issue-10935-master-detail-i18n-labels branch September 28, 2026 15:36
huangyiirene pushed a commit that referenced this pull request Sep 28, 2026
…osure

Brings in PR #10986 (objectui#10935), which touched zod/objectql.zod.ts
away from the `exportOptions` note this branch edits.

Claude-Session: https://claude.ai/code/session_012UwY3ahMixEFkfTUxMVkYm
Co-authored-by: Claude <noreply@anthropic.com>
akarma-synetal pushed a commit to akarma-synetal/objectui that referenced this pull request Oct 7, 2026
…e parser tier answers (objectui#10981) (objectstack-ai#11006)

Fixes objectstack-ai#10981
Clause-②: no — message, `.describe()` and docblock text only; every
accept or refuse verdict stays byte-identical.

Implemented by the `os-dev` agent dispatched from the `domain:spec @
objectui` seat, session
`https://claude.ai/code/session_012UwY3ahMixEFkfTUxMVkYm`
(`mode:subagent`). Base `4b742f41d`. PR objectstack-ai#10986 landed on
`zod/objectql.zod.ts` meanwhile, so `main` `665025908` was merged in
before the final push. Head `ebd156ef5`.

## What changed

Only the false clause changes, on four sites. Each clause names the code
the parser tier answers for that key and value shape, as measured on the
live registry (next section).

| site | face | before | after |
|---|---|---|---|
| `zod/form.zod.ts`, the `FormSchema.mode` refusal (objectui#10286) |
zod message and `.describe()`, one string through `retirementTombstone`
| "rendered the same form — no error, no warning" | "rendered the same
form — no render-time error or warning; only the parser tier's
`unknown-prop` warning noticed it" |
| `data-display.ts`, the `TimelineSchema.events` docblock
(objectui#6170) | TS face, emitted `.d.ts` | "drew an EMPTY rail, with
no error and no warning" | "drew an EMPTY rail, with no render-time
error or warning; only the parser tier's `unknown-prop` warning noticed
it" |
| `base.ts`, the `data-table` sentence in the `BaseSchema.bind` docblock
| TS face, emitted `.d.ts` | "renders its header over an empty body,
with no error and no warning" | "renders its header over an empty body,
with no render-time error; nothing on the page says why, but a
render-time console warning (`[ObjectUI] DataTable bind:`,
objectui#6575) and the parser tier's `unknown-prop` warning both name
it" |
| `zod/objectql.zod.ts`, the note on
`OBJECT_GRID_EXPORT_OPTIONS_GUIDANCE` (objectui#7762) | source note on a
non-exported constant; it ships in the emitted JS, in no `.d.ts` | "no
error, no warning, no console line" | "no render-time error, warning or
console line (only the parser tier's `type-mismatch` warning noticed
it)" |

- **Why `base.ts` has its own wording.** That site was not silent at
render either. `data-table` has logged `[ObjectUI] DataTable bind:` for
an authored `bind` since objectui#6575 (`aff3d7aef`, 2026-08-28). The
docblock was written a day later (`99a3c2db6`, 2026-08-29). `merge-base
--is-ancestor aff3d7a 99a3c2d` answers 0, with the control leg
`94d4876df` also answering 0. So "only the parser tier noticed it" would
be false there too.
- **Why `exportOptions` names `type-mismatch`.** The `object-grid`
registration declares `exportOptions` as `type: 'object'`. So
`checkType` answers an array, not the undeclared-key branch.
- **New pin:**
`packages/types/src/__tests__/silence-clause-parser-tier-closure-10981.test.ts`.
- **Optional item, taken:**
`packages/components/src/renderers/__tests__/header-bar-unknown-prop-parser-tier-10981.test.tsx`,
a new file beside `container-declaration-ratchet`.
- **Changeset:**
`.changeset/10981-silence-clause-parser-tier-closure.md`,
`@object-ui/types: patch`.

## The parser tier, measured before each clause was written

**Instrument.** The same as PR objectstack-ai#10980: a one-shot probe, deleted and
never committed, under `packages/components/src/renderers/__tests__/`.
- It imported the components renderers, `fields` and 18 of the 19
`plugin-*` packages (all but `plugin-designer`). `plugin-tree` has no
vitest alias, so it came in by relative source path.
- The manifest was built from every KNOWN registry key through
`manifestFromConfigs`, the way `container-declaration-ratchet.test.tsx`
builds it.
- Each leg ran `validateTree` on a control and on the control plus the
one key, and reports only the diagnostics that key added.
- Each leg also rendered through the real `SchemaRenderer`, inside
`PredicateScopeProvider` and `SchemaRendererProvider` with a stub data
source, all inside `act`. `console.warn` and `console.error` were spied,
and the HTML was compared with the control's.

| site | keys | value shapes | the parser tier adds | render leg |
|---|---|---|---|---|
| `FormSchema.mode` | `form`, `ui:form`. Bare `form` resolves to
`ui:form`; `view:form` is `skipFallback` | `edit`, `read`, `disabled`,
`create`, `view` (the objectui#10286 pin's set) | exactly one
`unknown-prop` on `mode`, 10 of 10 legs | 0 warnings, 0 errors on every
variant. HTML equals the control's except for one `mode` attribute on
the form element |
| `TimelineSchema.events` | `timeline` (resolves to `view:timeline`),
`view:timeline`, `plugin-timeline:timeline` (the `TimelineRenderer`
registration `TimelineSchema` describes) | 2 events; `[]`; `items` plus
`events` | exactly one `unknown-prop` on `events`, 7 of 7 |
byte-identical HTML, 0 warnings, 0 errors |
| `BaseSchema.bind` on `data-table` | `data-table`, `ui:data-table` |
`bind: 'users'` and `bind: 'app.settings.users'`, each with `data`
absent and with `data: []` | exactly one `unknown-prop` on `bind`, 8 of
8 | byte-identical HTML (the "No results" body in both), 0 errors, and
exactly 1 console warning on every variant: `[ObjectUI] DataTable bind:
… is ignored` |
| `exportOptions` | `object-grid`, `plugin-grid:object-grid` | `['csv',
'xlsx']`, `['csv']`, `[]` | exactly one `type-mismatch` ("expected an
object"), 6 of 6 | the control is `{ formats: ['csv', 'json'] }`, the
default an array loses to. Byte-identical HTML, the export control and
the rows present in both, 0 warnings, 0 errors |

**Controls.**
- `form` with an invented key draws `unknown-prop`. `view:form` with
`mode: 'edit'` draws nothing, because that registration declares `mode`.
- On `object-grid`: an `exportOptions` object draws nothing, a string
draws `type-mismatch`, and an invented key draws `unknown-prop`. So the
probe tells the two codes apart on one node.
- `accordion` with a `body` list draws `not-a-container`, the third
code.
- With `data` absent, the `data-table` control already carries
`missing-required-prop` on `data`; the table above counts only what
`bind` adds.
- The first control render in the file logged one `react-i18next`
initialisation warning. No variant did.

**The same answer before each retirement (source reading, not
executed).** The `mode`, `events` and `exportOptions` sentences are past
tense.
- `FormSchema.mode`, at `a05c35063^`, the parent of the objectui#10286
commit: the `ui:form` registration declared the same 13 inputs as today,
none named `mode`. `plugin-form`'s `form` was `namespace: 'view',
skipFallback: true`. `validate.ts` had the same base-prop set, without
`mode`, and one `unknown-prop` branch.
- `TimelineSchema.events`, at `195052fff^`, the parent of the
objectui#6170 tombstone commit: neither `plugin-timeline:timeline` nor
`view:timeline` declared `events`. A `git log -G` for an `events` input
name over `packages` finds no registration that ever declared one, back
to the shallow boundary (2026-07-20).
- `exportOptions`, at `f08d1a86a^`, the parent of the objectui#7762
refusal: `GRID_QUERY_INPUTS` declared `exportOptions` as `type:
'object'`. It had done so since `375efb402` (2026-08-17), and
objectui#7762 was filed on 2026-09-05. `checkType` gave a single
non-enum arm `type-mismatch`, as today.
- `BaseSchema.bind` is present tense, measured today.
- The history reads ran on a deepened checkout (`git fetch
--deepen=4000`, shallow boundary 2026-07-20).

## Census over joined text (the closure nail)

**Instrument.** A one-shot script. It enumerates the non-test `.ts`
files of `packages/types/src` from the ref with `git ls-tree` and reads
each one from the ref. It joins JSDoc and line-comment continuations and
`'…' + '…'` concatenations, and unescapes quotes. Then it counts:
- A: "no (adjective) error" and "no (adjective) warning" within 60
characters, with no sentence break;
- B: the same, in reverse order;
- C: "no (adjective) error or / nor / and (adjective) warning", the
family's corrected shape;
- D: "without (an) error or / and warning";
- E: "neither error nor warning";
- F: "silent…" within 80 characters of a negated warning, in either
order;
- G, the wide net: any negated warning ("no warning", "without a
warning", "never warns", …);
- H, the wider net over other nouns: "no diagnostic", "no console line",
"without complaint", "not a warning".

| reading | base `4b742f41d` | main `665025908` | head `ebd156ef5` |
|---|---|---|---|
| files enumerated | 67 | 67 | 67 |
| A | 4 | 4 | 0 |
| B, D, E | 0 | 0 | 0 |
| C, and how many are followed by "the parser tier's" | 302, 300 | 302,
300 | 302, 302 |
| F | 1 | 1 | 0 |
| G | 6 | 6 | 2 |
| H | 25 | 25 | 24 |
| control: the objectui#10928 clause | 282 | 282 | 282 |
| control: the objectui#10959 tail | 18 | 18 | 20 |

- **Positive control.** C fires on every one of the 300 known
objectui#10928 / objectui#10959 control strings. A fires on exactly the
four sites the card names, which were found independently.
- **Every non-control hit at base, read.**
- A is the four sites and nothing else. The two C hits without the
parser tier's clause are two of the four (`base.ts`, `data-display.ts`).
F is the fourth site again.
- G adds two hits that say "no warning window" (`objectql.ts`,
`zod/objectql.zod.ts`, objectui#6475). That is a deprecation window, not
a silence claim, so they stay.
- H is outside the card's phrasing family. It is listed under Acceptance
notes and not changed.

## Every other byte, and only comment and string lines

- Substitute the old clause back for the new one on joined text: each
head file equals its base file, 4 of 4, against `4b742f41d` and again
against `665025908`.
- The first four ablation legs put the old clause back with the anchor
tool. Each mutated blob equals the base blob byte for byte: `eba1c495f`,
`4dfce4ac6`, `b1abfe909` and `e2c561734` are the base blobs of the four
files.
- `git diff -U0` over the four files: every changed line is a ` * `
JSDoc line or a `+ '…'` string-literal line.
- The objectui#10286, objectui#6170, objectui#7762 and `bind` pins pass
unchanged; they are in the full `types` run.
- **Emitted `dist`**, after `pnpm --filter @object-ui/types build` at
head:
  - the old clauses, both spellings: 0 files;
  - the `mode` clause: `zod/form.zod.js`, `zod/index.zod.js`;
- the `events` clause: `data-display.d.ts`; the `bind` clause:
`base.d.ts`;
- the `exportOptions` note: `zod/objectql.zod.js`, `zod/index.zod.js`
(the emitted JS keeps comments).

## Reverse verification (ablation), after the fix was committed

**Instrument.** objectstack's `scripts/ablation-replace.mjs` in WRAP
mode, run from this worktree.
- The anchor must hit exactly once and the on-disk blob must move. The
tool proves each restore: blob equals HEAD and `git diff HEAD` is empty.
- The driver re-proved each restore independently, and ran inside an
EXIT / INT / TERM trap.
- The types pin imports `../zod/*` source and reads the files from disk.
The components pin resolves the registry from source through the vitest
aliases. So no build sits between a mutation and a run.

| leg | red | red tests |
|---|---|---|
| the old clause back into `FormSchema.mode` | 3 / 9 | the `mode` arm,
the spelling walk, the pairs walk |
| the old clause back into `TimelineSchema.events` | 3 / 9 | the
`events` docblock, the pairs walk, the parser-tier walk |
| the old clause back into `BaseSchema.bind` | 3 / 9 | the `bind`
docblock, both walks |
| the old clause back into the `exportOptions` note | 2 / 9 | the note,
the pairs walk |
| the note names `unknown-prop` in place of `type-mismatch` | 1 / 9 |
the note |
| `zod/ai.zod.ts`, which the pin does not name: an objectui#10928 clause
loses its parser-tier half | 1 / 9 | the parser-tier walk |
| `header-bar.tsx` starts declaring `title` (components pin) | 4 / 6 |
both population legs, both lit controls |

- Every leg restored to its HEAD blob (`3a595a747`, `33e3a4770`,
`a12a28cf1`, `4e75a80cf` twice, `a40e67ff6`, `ce3850854`) with an empty
`git diff HEAD`. After the run, `git status --porcelain` was empty.
- **The first `header-bar` attempt was a no-op.** Its replacement
contained the anchor, so the tool refused (anchor count 1 to 1) and ran
no test. It was re-run with a replacement that does not contain the
anchor. The table shows the second run.

## The pins

**`silence-clause-parser-tier-closure-10981.test.ts`.** It follows the
house style of PR objectstack-ai#10956 and PR objectstack-ai#10980. `@object-ui/types` imports no
workspace package, so it holds the prose.
- **Zod face.** `FormSchema.mode` is read through its refusal: code
`invalid_type` and path `['mode']` first, then the clause. The walk
covers every spelling of the objectui#10286 pin, and the `.describe()`
text.
- **`exportOptions`.** The bare array is refused with `invalid_type` at
`['exportOptions']`, with exactly the `.describe()` string. Then the
note above `OBJECT_GRID_EXPORT_OPTIONS_GUIDANCE` must carry the clause
and name no `unknown-prop`.
- **TS face.** The `TimelineSchema.events` and `BaseSchema.bind`
docblocks.
- **The closure walk.** It reads every non-test `.ts` file of
`packages/types/src` from disk, with the root taken from the pin's own
`import.meta.url`.
- No joined text may pair "no error" with "no warning" (patterns A, B,
D, E).
- Every C phrasing must have "the parser tier's" within 80 characters
after it.
- Non-vacuity, by name: the four site files are reached, and both
earlier waves' clauses are found.

**`header-bar-unknown-prop-parser-tier-10981.test.tsx`.** It answers the
open question from PR objectstack-ai#10956 and PR objectstack-ai#10980, on the live components
registry.
- On `header-bar` and `ui:header-bar`, each of the 9 refused keys draws
exactly one `unknown-prop` naming the key. This holds in every value
shape the objectui#10387 and objectui#10286 pins refuse.
- The control draws nothing.
- The lit control shows `crumbs` declared, and no refused key declared.

## Dispatch assumptions, checked

1. **Holds.** There are exactly four sites, one per file, on `4b742f41d`
and on `665025908`. The census was widened to patterns A–H above, with
controls.
2. **Holds, and made more exact.**
- `form` with `mode` draws `unknown-prop`: for all five spellings, on
`form` and `ui:form`.
- A bare `timeline` with `events` draws `unknown-prop`. The bare key
resolves to `view:timeline`. The docblock describes `TimelineRenderer`,
which is registered as `plugin-timeline:timeline` (`skipFallback`). That
registration also answers `unknown-prop`, so the clause is true for the
node the docblock names and for the bare key.
- `data-table` with `bind` draws `unknown-prop`. But "only the parser
tier noticed it" would be false there, because a render-time console
warning fires too (objectui#6575). The site names both.
- `object-grid` with an `exportOptions` array draws `type-mismatch`: for
three array shapes, on both keys.
3. **Holds**, by source reading at the parent of each retirement. See
above.
4. **Changed during the run.** PR objectstack-ai#10986 landed (`8aa68b159`) on
`zod/objectql.zod.ts`. `main` `665025908` was merged in before the final
push, with no conflict. The note edited here is away from that hunk. PR
objectstack-ai#10997 had not landed at `665025908`.

## Gates, head `ebd156ef5`

All ran after the merge commit. Exit codes were captured before any
pipe.
- `pnpm --filter @object-ui/types build` → 0. Its
`check-dist-completeness` verified 134 emitted files.
- `pnpm --filter @object-ui/types type-check` → 0. `tsc -p
tsconfig.test.json --listFiles` includes the new pin.
- `pnpm --filter @object-ui/types lint` → 0, with 0 errors. The 295
warnings are existing `no-explicit-any` ones; none is in the new file.
- `pnpm exec vitest run packages/types/` → 0: 272 files and 5993 tests
passed.
- Outside `types` → 0: 27 files and 931 tests passed.
- The script tests that read `packages/types/src`:
`body-dialect-producer-scan`, `changeset-polarity-census`,
`check-action-forward-parity`, `check-changeset-claims`,
`check-component-surface-parity`, `check-doc-component-types`,
`check-doc-example-types`, `check-handler-key-read-sites`,
`check-readme-exports`, `check-spec-symbol-derivation`,
`component-node-vocabulary-7434`,
`one-authority-per-exported-name-6273`,
`check-pending-changeset-literals`, `spec-symbol-ledger-types-7265`,
`unconsumed-widget-option-claim-6186`, `vite-types-zod-lazy`;
  - `component-fixture-declared-keys` in `examples/schema-catalog`;
- the new components pin, `container-declaration-ratchet`, the four
`header-bar` component tests, `skill-guide-data-table-binding` and
`data-table-bind-diagnostic`;
- the parser pins `body-dialect-6771` and
`containment-declared-slot-9910`.
- `pnpm --filter '@object-ui/components^...' build`, then `pnpm --filter
@object-ui/components type-check` → 0. `tsconfig.test.json --listFiles`
includes the new components pin. `eslint` over the two new test files →
0, with no findings.
- `check-changeset-presence`, `check-changeset-no-major`,
`check-changeset-fixed`, `check-changeset-overwrite` → 0.
`check-changeset-claims` → 0; it is report-only.
- `pnpm check:control-bytes`, `check:new-line-citations` (0 new),
`check:pending-changeset-literals`, `check:test-path-roots`,
`check:phantom-deps` and `check:self-import` → 0.
- `check-governed-queue-guard --test` over the 7 paths → NOT GOVERNED.
- Declared to CI: repo-wide `pnpm lint`, full `pnpm test` and the other
workflow gates.

## Acceptance notes

Not changed here, and left for the seat to route.
- **Same false claim, outside this package.**
`skills/objectui/rules/protocol.md`, the "Readers only." paragraph, says
a `bind` on `data-table` renders its header over an empty body "no
error, no warning". The render leg and the parser leg above both
contradict it. It is on the governed `skills/**` surface, outside this
card.
- **Same claim in unpublished test prose.** The headers of
`base-bind-declared.test.ts` and
`object-grid-export-options-refusal-7762.test.ts` repeat it.
`mirror-partial-record-narrowing-8516.test.ts` has a different "no error
and no warning" claim, about grid columns. All three are test files,
outside the census.
- **Pending changesets.**
`.changeset/7762-object-grid-export-options-bare-array-refusal.md` and
`.changeset/6575-data-table-bind-diagnostic.md` describe the pre-fix
behaviour in the old words. Pending changesets are out of scope here.
- **Adjacent wording, left in place.**
- The `base.ts` heading "the one documented silent failure" stays. The
corrected sentence now says in which sense it is silent: nothing on the
page.
- The `exportOptions` guidance string says an array "is silently dropped
for the csv/json default". That is present tense about the renderer, and
"silently" alone is outside the phrasing family.
- **H, the "no diagnostic" nouns (24 at head).** Most are scoped to a
named tier or instrument: "on either face" (`objectql.ts`), "zero
unresolved-module diagnostics" from `tsc`, `checkType` by name, the TS
index signature. A few are unscoped claims about an authored value:
`ai.ts` (`maxResults`), `crud.ts`, `data-display.ts` (three) and
`zod/data-display.zod.ts`. They were not measured here.
- **Observation.** A `mode` on a `form` reaches the DOM as an inert
`mode` attribute on the form element. The visible form is identical.
- **Boundary.** `view:form` declares `mode` (enum `create`, `edit`,
`view`). It is a different registration and node type, which the
`FormSchema` docblock does not describe.
- **A parser-tier finding, reported to the seat.** `validateTree`
answers `bind` with `unknown-prop` on `list` and `tree-view` too,
although both read `bind` through `useDataScope`. No registration
declares a `bind` input, and the parser's base-prop set has no `bind`.

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

---------

Co-authored-by: Claude <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

1 participant