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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

1 participant