Skip to content

fix(i18n)!: the console translation bundle leaves out Studio's metadataForms (75 KB of 221 KB on app-crm) - #26159

Merged
objectstack-fleet[bot] merged 20 commits into
mainfrom
claude/project-thread-nvoyre
Oct 27, 2026
Merged

objectstack-fleet[bot] merged 20 commits into
mainfrom
claude/project-thread-nvoyre

Conversation

@objectstack-fleet

@objectstack-fleet objectstack-fleet Bot commented Oct 26, 2026 •

Copy link
Copy Markdown
Contributor

Requested by Steedos · project thread

Fixes #25785

Clause-②: yes (narrowing)
Metadata-protocol: narrowed

Summary

GET /i18n/translations/:locale served the i18n service's whole locale tree, so every console open downloaded metadataForms, the translations of Studio's metadata-type authoring forms. On examples/app-crm that is 75 KB of the 221 KB raw zh-CN bundle, the second-largest group after objects. The card's other half (the ETag and Cache-Control: private, no-cache validator) already landed with the earlier i18n caching PR; this PR is the composition half.

How Studio gets its forms now: it already does, from the server. No browser code reads metadataForms from this bundle. The /meta and /meta/types handler (translateMetaTypesResponse in @objectstack/rest) reads the same i18n service in process, resolves metadataForms.TYPE for each type, and serves every form already in the requested locale. The liveness ledger (packages/spec/liveness/translation.json) names that handler as the group's only reader. So the group only has to stop being served to the browser. Studio loads nothing new, and there is no new route, key or endpoint.

  • spec, system/translation.zod.ts. New SERVER_RESOLVED_TRANSLATION_GROUPS = ['metadataForms'] and ServedTranslationDataSchema: the platform face (PlatformTranslationDataSchema) without those groups. It is closed (strictObject), so a served document that still carries one fails it by name.
  • spec, api/protocol.zod.ts. GetTranslationsResponseSchema.translations is now ServedTranslationDataSchema, so the declared response is what the door serves.
  • runtime, domains/i18n.ts. Both exits (the requested locale and the zh to zh-CN fallback) serve servedTranslations(tree), a copy without the server-resolved groups. The service's own tree is never edited, because /meta translates from that same object (both the core memory i18n and service-i18n hand back the stored tree). The ETag still hashes the served body, so it now changes only when served bytes change.
  • docs. i18n-standard.mdx (Metadata authoring forms) says the console bundle leaves the group out and /meta reads it from the service. The reference pages are regenerated.
  • changeset. 25785-served-translations-without-metadata-forms.md, patch for @objectstack/spec and @objectstack/runtime.

metadataForms stays authorable on every bundle face (TranslationDataSchema, PlatformTranslationDataSchema, app bundles, i18n:extract). Only the served document leaves it out.

Measured live (examples/app-crm, signed-in member, same box)

locale build raw gzip brotli metadataForms warm reopen
zh-CN main 0a694821 221,007 B 68,580 55,391 75,127 B 304, 0 B
zh-CN this branch 145,863 B 45,107 36,555 absent 304, 0 B
en main 0a694821 213,880 B 58,666 49,185 75,361 B 304, 0 B
en this branch 138,502 B 37,576 31,466 absent 304, 0 B

Every other group kept its exact byte size (for example zh-CN objects 111,504, settings 25,868, apps 4,469). GET /api/v1/meta/types and GET /api/v1/meta in zh-CN answered 31 entries on both builds, with identical translated labels: object 对象, field 字段, dashboard 仪表板, view 视图, flow 流程, and the first section and field of each form (基础信息, 名称).

Tests

  • New packages/runtime/src/domains/i18n-translations-served-groups.test.ts (10 tests):
    • A: the path and query spellings and the fallback exit serve every other group unchanged and omit metadataForms. The body parses the closed GetTranslationsResponseSchema, with a control showing the schema refuses the same body when it carries the group.
    • B: a boot read does not edit the service's tree. A RestServer /api/v1/meta built over the same service, read after the boot read, still serves the dashboard form translated (仪表板, 布局, 列数).
    • C: the ETag is the hash of the served body, If-None-Match answers 304, a change confined to metadataForms keeps the validator, and a change to messages moves it.
  • New packages/spec/src/system/served-translation-data.test.ts: the served face is the platform face minus the constant (with a positive control); the response schema serves that face; each server-resolved group stays declared on both bundle faces; a document carrying the group is refused with unrecognized_keys naming it.
  • type-alias-convention.pin.test.ts: one Iso pin for the new schema (789 to 790). dropped-refinements.baseline.json: one entry for system/ServedTranslationData, the same 3 sites as PlatformTranslationData, with header totals 245 / 841.

Runs (under scripts/pm/os-verify-lock.sh, --maxWorkers=2):

  • @objectstack/runtime full suite at 8b2ad27c: Test Files 455 passed (455), Tests 7415 passed, 19 skipped. VERDICT command-exit 0.
  • @objectstack/spec full suite: Tests 21929 passed, 1 failed, the dropped-refinements header totals. Those were recounted from the body in 8b2ad27c, and the file then ran 27/27. That commit touches only those two totals, which only that test reads.
  • Focused: runtime 4 files, 41 tests; spec 5 files, 434 tests; all passed.
  • Type-check of @objectstack/spec, @objectstack/runtime and @objectstack/client: VERDICT command-exit 0.
  • ui/apps/console/src/loadLanguage.test.tsx (the console's bundle loader): 7 passed.

Ablation (one-off, mutated on disk, then restored)

  • Filter removed. servedTranslations copies every group (blob 3154a44d to a28c8f4a). 6 of 10 tests failed: path and query spellings, fallback exit, schema parse, the /meta after-boot read, and the unserved-change validator. Restored blob 3154a44d equals HEAD.
  • In-place delete. servedTranslations still serves the right document but also deletes the group from the service's own tree (return served; replaced with a loop deleting each server-resolved group from translations, blob 3154a44d to 5e38e852). Exactly 2 of 10 tests failed, both Studio pins: "a boot read does not edit the service's tree" and the /meta after-boot read, which served 'Dashboard' where '仪表板' was expected. The other 8 passed, so the served bytes and the validator do not catch this mistake and only the B pins do. Restored blob 3154a44d equals HEAD, git diff HEAD empty.

Both runs used scripts/ablation-replace.mjs with src/domains/i18n-translations-served-groups.test.ts under the verify lock. No permanent test file was added for them.

Gates

Derived with node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack (114 commands; the list derived at the head 8b2ad27c is identical to the one run). Each ran with its exit code captured before any pipe, and --ran reconciled them at 8b2ad27c: "114 derived famil(ies) accounted for — 114 run, 0 NOT-MEASURED". All 114 exited 0, among them check:generated, check:api-surface, check:authorable-surface, check:docs, check:liveness, check:strictness-ledger, check:spec-parsed-alias, check:exported-any, check:dual-build-cjs-loads (after a full workspace build), check:nul-bytes and check:type-check-debt under the verify lock ("26 raw tsc error(s) total, none above its recorded number").

The sweep started before the last commit, which only recounts the two dropped-refinements header totals. The 10 commands that finished before that commit, and check:query-options-erasure, which overlapped the 20-second window of the first ablation, were rerun at 8b2ad27c; all exited 0. The local-only check:dev-prereqs and check:skill-frame-freshness also exited 0.

dispatch-gates warned STALE TREE: the branch is behind main. No file in this diff is touched by main's newer commits, and git merge-tree with origin/main reports no conflict, so main was not merged in.

Notes for review

  • Contract review owed. Non-test files under packages/spec/src moved (system/translation.zod.ts, api/protocol.zod.ts), so the CONTRACT_REVIEW_TIER review applies. Neither file was on the claim's named file surface; the fix belongs in the spec declaration, so it was made there.
  • The response schema narrows on purpose. translations was the platform face, which is closed and declares metadataForms. The served face is closed the same way without it, so a body that still carries the group now fails the schema. A client validating this route's responses keeps passing. A client that read metadataForms from this route gets nothing; the liveness ledger and a repository search show no such reader (the console loader, the ui dashboard inspector and TranslationPreview do not read it).
  • The ui side, unchanged and still green. ui/apps/console/src/loadLanguage.test.tsx walks GetTranslationsResponseSchema.shape.translations (through the workspace:^ link to this packages/spec, so it reads this change) and requires the loader to namespace every group it finds. It passes with the narrower list: 7 passed. SPEC_TRANSLATION_GROUPS in ui/packages/i18n deliberately keeps its own copy of the list and is a superset detector, so still listing metadataForms changes no behaviour. Two ui docblocks now read a little stale — that list's header calls itself "the keys of the translations shape", and metadata-form-i18n.ts expects "the environment's own translation bundle" to arrive one day as a higher-precedence source for Studio's form labels (it would now be /meta/types, which already serves them translated). Neither is on this claim's surface and neither is a behaviour, so both are left alone and reported to the seat.
  • Undeclared field. The fallback exit's requestedLocale is served but not declared on GetTranslationsResponseSchema. It was like this before and is unchanged here.
  • Not taken: scoping objects to the current app. The card marks it optional and a bigger contract change. It is reported to the seat as a separate finding.

维护者速读(草稿)

改了什么. 控制台每次打开都会请求当前语言的翻译包。这个包不再带 metadataForms(Studio 元数据编辑表单的翻译)。其余翻译分组一个字节不变。

为什么改. 这部分在 app-crm 的中文包里占 75 KB(总共 221 KB),浏览器从来不读它。Studio 的表单翻译一直是服务端在 /meta/types 里直接翻好再下发的。去掉之后,中文包压缩后从 68.6 KB 降到 45.1 KB,英文从 58.7 KB 降到 37.6 KB。第二次打开仍然是 304。

风险与代价(含回滚). 风险是有外部客户端从这个接口读 metadataForms。仓内没有这样的读者。Studio 的表单翻译有测试钉住:先请求翻译包,再读 /meta,表单仍是中文。回滚只需要还原本 PR,不涉及数据迁移。没有新增接口,也没有新增配置键。

席位意见.

你要做的. 评审并合并。可选:如果需要,按应用裁剪 objects 分组另开卡(本 PR 没做)。

Round 2 — the seat's contract review (claim amendment 6418168409)

The declarations are corrected at the top of this body: Clause-②: yes (narrowing), Metadata-protocol: none. The published GetTranslationsResponseSchema accepted a body carrying metadataForms and now refuses it, and the exported GetTranslationsResponse type loses that member; nothing an author writes moves, so the kind stays none and the level stays patch.

  • The changeset carries the BREAKING banner, the remedy (read the group from GET /meta/types, which serves each form already translated) and exactly one ADR-0087 disposition: not-required (no-migration-prescription), accepted by check-adr-0087-registration — "1 declared-breaking changeset(s), each carrying an ADR-0087 disposition". The reason closes the other five categories on facts: both packages publish; no ADR-0087 id is named or touched; the diff touches packages/spec/** and a *.zod.ts face, which closes type-surface-only; the narrowed declaration is a Zod projection, which closes runtime-interface-only; and no export is retired. The remedy is written as prose rather than a literal FROM → TO label block, because that label is branch 1 of the gate's migration-prescription detector and would make the only claimable category unclaimable; the mapping it would carry ("read metadataForms from GET /meta/types") is in the changeset's "What to do" and above.
  • requestedLocale is now declared on GetTranslationsResponseSchema, optional (packages/spec/src/api/protocol.zod.ts). The door has served it on the locale-fallback exit (zh served as zh-CN) since that fallback existed, undeclared. Same schema, same class, mechanical — the in-place adjacent fix the amended claim names. Pinned on both sides: the spec test fixes the shape to locale / requestedLocale / translations, parses a fallback body, keeps the field optional and refuses a non-string; the runtime test parses the real fallback body against the schema.
  • Two ui docblocks this PR had made false are repaired. SPEC_TRANSLATION_GROUPS (ui/packages/i18n/src/utils/spec-translations.ts) now says it is deliberately a superset of the groups the server still serves, names metadataForms as the first group to have left, and states the one direction that is red (a group the spec serves and the list lacks). metadata-form-i18n.ts (ui/packages/app-shell) now names /api/v1/meta/types as the channel that carries an environment's own metadataForms, and says this overlay covers the forms the console renders from its own copy of the spec.
  • origin/main was merged in (merge 81dc21e7), because editing a ui registry source moves that package's digest in scripts/sdui-manifest.sources/ and main had just re-shaped those records one-per-package (devx(sdui): one manifest record per ui registry package, so ui PRs on different packages stop conflicting #26120). No file in this diff overlaps main's range, so the merge is textually clean. The digest was then re-recorded with the merged generator: 1 of 27 package records moved (i18n), sdui.manifest.json byte-identical at 106 components, check-sdui-manifest green.

Measured at the head this comment is written against, 4f5d2f0a: the dogfood battery (closure built first, then pnpm --filter @objectstack/dogfood test under the verify lock, no shard and no path) is Test Files 296 passed | 3 skipped (299), Tests 2523 passed | 18 skipped (2541), VERDICT 0. The @objectstack/spec suite is 803 files / 23204 tests green, the six runtime i18n files 103 tests, the type-checks clean, and the console loader plus the @object-ui/i18n transform suite 22 tests. The derived gate list is now 125 commands and all 125 exited 0, reconciled as "125 run, 0 NOT-MEASURED".

The declaration lines and the title of this pull request are the seat's write, not the author's (a dev writes a body once, at creation): the title takes ! and the body's Clause-②: line takes yes (narrowing). Until then the declaration of record for the contract review is the changeset's.


Generated by Claude Code

…ved metadataForms

GET /i18n/translations/:locale now answers the platform groups without
SERVER_RESOLVED_TRANSLATION_GROUPS (metadataForms). Studio's authoring
forms are already translated on the server by /meta/types, from the same
i18n service tree, which the door copies and never edits.
GetTranslationsResponseSchema declares the served face
(ServedTranslationDataSchema), closed, so a body that still carries the
group fails it.

Claude-Session: https://claude.ai/code/session_01Vy85beHdawtEtqNscYBy7V
Co-authored-by: Claude <noreply@anthropic.com>
… and the response schema

Claude-Session: https://claude.ai/code/session_01Vy85beHdawtEtqNscYBy7V
Co-authored-by: Claude <noreply@anthropic.com>
…s out; changeset

Claude-Session: https://claude.ai/code/session_01Vy85beHdawtEtqNscYBy7V
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions

github-actions Bot commented Oct 26, 2026 •

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 2 package(s): @objectstack/runtime, @objectstack/spec, touching 11 documentable anchor(s). ⚠️ 7 changed file(s) yielded no anchor (packages/spec/api-surface/system.json, packages/spec/authorable-surface/api.json, packages/spec/authorable-surface/system.json, …), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files.

6 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:

  • content/docs/api/client-sdk.mdx (via getTranslations (sdk, the bare tail of client method i18n.getTranslations, bound to GET /api/v1/i18n/translations/:locale; the bare tail of client method i18n.getTranslations, bound to GET /i18n/translations/:locale), i18n.getTranslations (sdk, the route ledger binds it to GET /api/v1/i18n/translations/:locale, selected by route anchor /i18n/translations/:locale; the route ledger binds it to GET /i18n/translations/:locale))
  • content/docs/api/plugin-endpoints.mdx (via /i18n/translations/:locale (route, a path literal in a comment on a changed line), /translations/:locale (route, bridged from symbol GetTranslationsResponseSchema — its route source's handler names it))
  • content/docs/kernel/services-checklist.mdx (via getTranslations (sdk, the bare tail of client method i18n.getTranslations, bound to GET /api/v1/i18n/translations/:locale; the bare tail of client method i18n.getTranslations, bound to GET /i18n/translations/:locale), /i18n/translations/:locale (route, a path literal in a comment on a changed line), /translations/:locale (route, bridged from symbol GetTranslationsResponseSchema — its route source's handler names it))
  • content/docs/permissions/system-context.mdx (via handleI18nRequest (symbol, a top-level function))
  • content/docs/protocol/kernel/i18n-standard.mdx (via SERVER_RESOLVED_TRANSLATION_GROUPS (symbol, a top-level const object), ServedTranslationData (symbol, a top-level type), metadataForms (literal, a string literal in SERVER_RESOLVED_TRANSLATION_GROUPS; a string literal in ServedTranslationDataSchema), getTranslations (sdk, the bare tail of client method i18n.getTranslations, bound to GET /api/v1/i18n/translations/:locale; the bare tail of client method i18n.getTranslations, bound to GET /i18n/translations/:locale), /i18n/translations/:locale (route, a path literal in a comment on a changed line), /translations/:locale (route, bridged from symbol GetTranslationsResponseSchema — its route source's handler names it))
  • content/docs/ui/translations.mdx (via metadataForms (literal, a string literal in SERVER_RESOLVED_TRANSLATION_GROUPS; a string literal in ServedTranslationDataSchema), /translations/:locale (route, bridged from symbol GetTranslationsResponseSchema — its route source's handler names it))

⛔ 1 release-owned page(s) also name something this change touched. These are read-only:

  • content/docs/releases/v18/18-2.mdx (via /i18n/translations/:locale (route, a path literal in a comment on a changed line), /translations/:locale (route, bridged from symbol GetTranslationsResponseSchema — its route source's handler names it))

content/docs/releases/ is RELEASE-OWNED (AGENTS.md "Documentation Guardrails"): release
notes are written centrally at release time, and a code PR that edits them is the exact PR
that guardrail exists to stop. They are still audited — read-only. If one of them is actually
wrong, file an issue or open a dedicated docs-only PR; do not edit it here.

What this run could not see
  • 7 changed file(s) yielded no anchor (packages/spec/api-surface/system.json, packages/spec/authorable-surface/api.json, packages/spec/authorable-surface/system.json, …) — pages documenting those are invisible to this run
  • the SDK route bridge reached 55 of 207 client-bound route-ledger rows — the other 152 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 152: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 55 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 97 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.
  • a key NAME is not a key, so the hand re-read the line above prescribes can land on the wrong schema. The same spelling is authorable on one governed type and a [REMOVED] tombstone on another for each of active, aria, joins, objects, template, tools and version (censused on [finding] tools is a key on BOTH AgentSchema (tombstoned, dead) and SkillSchema (live, cloud-attested), so a name-based search attributes skill examples to the agent key — it produced a false stop-the-line alarm on PR #19059 #19093 over the liveness ledger's governed types, top-level keys); nothing in a search result distinguishes the two, so a grep hit on a LIVE example reads as evidence about the DEAD key. Measured on fix(spec): the agent.tools liveness row says dead — it claimed live on a key the schema tombstoned #19059: content/docs/ai/agents.mdx was reported as contradicting the agent.tools tombstone over its tools: example at :161, which is inside the defineSkill({ block opened at :155 — the page was already correct. Settle ownership by PARSING the value against both schemas, never by the name: that literal PASSES SkillSchema, and as an AgentSchema it FAILS at tools with the tombstone prescription. ⛔ These names are not the whole class — a key retired through a .strict() guidance map leaves no tombstone in the walked shape and none of them here (tool.category, live as AIToolDefinition.category).

Coarse fallback — 152 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json 8c0306f5d5db836331098483902b0932dd18e6da → packageMentionDocs.

Which tree this was computed on

This run read content/docs from 008428f5a9295132562c0263c416b665043e51ca — the merge of head 392a610a7e66af7e878647124e343c5f46958733 into base 8c0306f5d5db836331098483902b0932dd18e6da, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 008428f5a9295132562c0263c416b665043e51ca && git checkout 008428f5a9295132562c0263c416b665043e51ca
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 8c0306f5d5db836331098483902b0932dd18e6da 392a610a7e66af7e878647124e343c5f46958733 && git checkout -B drift-repro 8c0306f5d5db836331098483902b0932dd18e6da && git merge --no-ff 392a610a7e66af7e878647124e343c5f46958733

node scripts/docs-audit/affected-docs.mjs --json 8c0306f5d5db836331098483902b0932dd18e6da

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs 8c0306f5d5db836331098483902b0932dd18e6da → pass the list as
args.docs, on the commit named under Which tree this was computed on.

…n for the served bundle

The seat's contract review of the served-translations PR amends the claim to
`Clause-②: yes (narrowing)`: the published `GetTranslationsResponseSchema`
accepted a body carrying `metadataForms` and now refuses it, and the exported
type loses that member. The changeset carries the BREAKING banner, the remedy
(read the group from `/meta/types`) and one ADR-0087 disposition.

`requestedLocale` is declared on the same schema, optional. The runtime door
has served it on the locale-fallback exit since that fallback existed, with no
declaration behind it; pins in the spec and runtime suites now read it from
the schema.

Two ui docblocks described the old composition. Both now say what is true:
`SPEC_TRANSLATION_GROUPS` is a superset of the groups the server still serves,
and the metadata designer's overlay names `/meta/types` as the channel that
carries an environment's own `metadataForms`.

Claude-Session: https://claude.ai/code/session_01Vy85beHdawtEtqNscYBy7V
Co-authored-by: Claude <noreply@anthropic.com>
Editing `ui/packages/i18n/src/utils/spec-translations.ts` moves that package's
registry digest, so its record under `scripts/sdui-manifest.sources/` follows
it. `sdui.manifest.json` is byte-identical: 106 components, sha256 1f6e1ccb925a.

Claude-Session: https://claude.ai/code/session_01Vy85beHdawtEtqNscYBy7V
Co-authored-by: Claude <noreply@anthropic.com>
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Round 2 — the seat's contract review (claim amendment 6418168409)

The declarations are corrected at the top of this body: Clause-②: yes (narrowing), Metadata-protocol: none. The published GetTranslationsResponseSchema accepted a body carrying metadataForms and now refuses it, and the exported GetTranslationsResponse type loses that member; nothing an author writes moves, so the kind stays none and the level stays patch.

  • The changeset carries the BREAKING banner, the remedy (read the group from GET /meta/types, which serves each form already translated) and exactly one ADR-0087 disposition: not-required (no-migration-prescription), accepted by check-adr-0087-registration — "1 declared-breaking changeset(s), each carrying an ADR-0087 disposition". The reason closes the other five categories on facts: both packages publish; no ADR-0087 id is named or touched; the diff touches packages/spec/** and a *.zod.ts face, which closes type-surface-only; the narrowed declaration is a Zod projection, which closes runtime-interface-only; and no export is retired. The remedy is written as prose rather than a literal FROM → TO label block, because that label is branch 1 of the gate's migration-prescription detector and would make the only claimable category unclaimable; the mapping it would carry ("read metadataForms from GET /meta/types") is in the changeset's "What to do" and above.
  • requestedLocale is now declared on GetTranslationsResponseSchema, optional (packages/spec/src/api/protocol.zod.ts). The door has served it on the locale-fallback exit (zh served as zh-CN) since that fallback existed, undeclared. Same schema, same class, mechanical — the in-place adjacent fix the amended claim names. Pinned on both sides: the spec test fixes the shape to locale / requestedLocale / translations, parses a fallback body, keeps the field optional and refuses a non-string; the runtime test parses the real fallback body against the schema.
  • Two ui docblocks this PR had made false are repaired. SPEC_TRANSLATION_GROUPS (ui/packages/i18n/src/utils/spec-translations.ts) now says it is deliberately a superset of the groups the server still serves, names metadataForms as the first group to have left, and states the one direction that is red (a group the spec serves and the list lacks). metadata-form-i18n.ts (ui/packages/app-shell) now names /api/v1/meta/types as the channel that carries an environment's own metadataForms, and says this overlay covers the forms the console renders from its own copy of the spec.
  • origin/main was merged in (merge 81dc21e7), because editing a ui registry source moves that package's digest in scripts/sdui-manifest.sources/ and main had just re-shaped those records one-per-package (devx(sdui): one manifest record per ui registry package, so ui PRs on different packages stop conflicting #26120). No file in this diff overlaps main's range, so the merge is textually clean. The digest was then re-recorded with the merged generator: 1 of 27 package records moved (i18n), sdui.manifest.json byte-identical at 106 components, check-sdui-manifest green.

Measured at the head this comment is written against, 4f5d2f0a: the dogfood battery (closure built first, then pnpm --filter @objectstack/dogfood test under the verify lock, no shard and no path) is Test Files 296 passed | 3 skipped (299), Tests 2523 passed | 18 skipped (2541), VERDICT 0. The @objectstack/spec suite is 803 files / 23204 tests green, the six runtime i18n files 103 tests, the type-checks clean, and the console loader plus the @object-ui/i18n transform suite 22 tests. The derived gate list is now 125 commands and all 125 exited 0, reconciled as "125 run, 0 NOT-MEASURED".

The declaration lines and the title of this pull request are the seat's write, not the author's (a dev writes a body once, at creation): the title takes ! and the body's Clause-②: line takes yes (narrowing). Until then the declaration of record for the contract review is the changeset's.


Generated by Claude Code

@objectstack-fleet objectstack-fleet Bot changed the title fix(i18n): the console translation bundle leaves out Studio's metadataForms (75 KB of 221 KB on app-crm) fix(i18n)!: the console translation bundle leaves out Studio's metadataForms (75 KB of 221 KB on app-crm) Oct 26, 2026
claude added 10 commits October 26, 2026 16:50
…-nvoyre

# Conflicts:
#	packages/spec/dropped-refinements.baseline.json
#	scripts/sdui-manifest.sources/i18n.json
…tocol narrowed, minor

The authorable-surface shards this diff moves (the served face's keys and
GetTranslationsResponse:requestedLocale) make it a metadata-protocol change,
so Check Changeset needs a kind other than none and at least minor on a
moved package. The changeset now declares Metadata-protocol: narrowed and
ships @objectstack/spec and @objectstack/runtime at minor; the ADR-0087
reason is restated for the new kind.

Claude-Session: https://claude.ai/code/session_01Vy85beHdawtEtqNscYBy7V
Co-authored-by: Claude <noreply@anthropic.com>
…-nvoyre

# Conflicts:
#	scripts/sdui-manifest.sources/i18n.json
…-nvoyre

# Conflicts:
#	packages/spec/src/type-alias-convention.pin.test.ts
#	scripts/sdui-manifest.sources/i18n.json
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Claim: contract review of PR #26159 at head fec80c3eae6fd0a838b60bccceb940585a1d9331 · 2026-10-27T01:15Z
Session: session_01WGufzfhobWEgUUrjz4rrbh
Account: os-support-ai
Seat: domain:spec#1 (seat post #6017)

Cross-account review for spec seats 2 and 4, on the maintainer's word in this seat's project: 「座位 2 和 4 需要契约复审的pr你也负责帮忙复审」. ⛔ Not a claim on card #25785, no label or assignee change. Face: published schema (packages/spec/src/api/protocol.zod.ts, packages/spec/src/system/translation.zod.ts) and the PR body's narrowing declaration, so the record is served at CONTRACT_REVIEW_TIER by an isolated at-tier subagent and lands here as one ## Contract review comment. The round-2 comment 6421330578 does not carry the record's form (no Served-tier: / Head-sha: / verdict lines), so no at-tier record is on file for this PR.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: fec80c3eae6fd0a838b60bccceb940585a1d9331
Local-runs: none
Scope: packages/spec/src/api/protocol.zod.ts, packages/spec/src/system/translation.zod.ts + (narrowing-face floor, every diff path outside the prose faces) docs/audits/2026-07-unknown-key-strictness-ledger.counts/system.md, packages/runtime/src/domains/i18n-translations-served-groups.test.ts, packages/runtime/src/domains/i18n.ts, packages/spec/api-surface/system.json, packages/spec/authorable-surface/api.json, packages/spec/authorable-surface/system.json, packages/spec/declaration-map/system.json, packages/spec/dropped-refinements.baseline.json, packages/spec/export-origins/system.json, packages/spec/json-schema.manifest/system.json, packages/spec/src/system/served-translation-data.test.ts, packages/spec/src/type-alias-convention.pin.test.ts, scripts/sdui-manifest.sources/i18n.json, ui/packages/app-shell/src/views/metadata-admin/metadata-form-i18n.ts, ui/packages/i18n/src/utils/spec-translations.ts + (cited, read on main at 6390fe5) packages/rest/src/rest-server.ts (translateMetaTypesResponse), packages/spec/liveness/translation.json, packages/spec/src/shared/strict-object.ts, packages/client/src/index.ts (i18n.getTranslations), ui/apps/console/src/loadLanguage.test.tsx, packages/runtime/src/i18n-success-envelope.conformance.test.ts, packages/spec/src/api/protocol.test.ts, packages/spec/src/api/plugin-rest-api.zod.ts, packages/services/service-i18n/src/i18n-route-ledger.ts, scripts/check-changeset-no-major.mjs, scripts/check-adr-0087-registration.mjs

Inputs read: card #25785 (body and all 14 comments, among them claims 6407675488 and 6415714537, amendments 6418168409 and 6421832617, dev reports 6418116291, 6421358584 and 6428970383, release 6410241389, re-route 6412175443), PR #26159 (body, 22-file list, comments 6418082927, 6421330578 and 6431285224, no review comments or reviews), the net diff against main at the head, the 63 check-runs on the head (58 success, 5 skipped, 0 red), and main's window 3249e20..e843b5b (42 commits, 300 files) that merge a25dc27 brought in after the dogfood head 2cd9696.

① Derived judgments

  1. Response accept set NARROWED, right. GetTranslationsResponseSchema.translations moves from PlatformTranslationDataSchema to the new ServedTranslationDataSchema, built with strictObject (closed; an unknown key answers unrecognized_keys naming it). A body carrying metadataForms, which the published schema accepted, now fails. Enforced at the only door: packages/runtime/src/domains/i18n.ts serves servedTranslations(tree) on both exits (requested locale and the zh to zh-CN fallback); the copy filters by SERVER_RESOLVED_TRANSLATION_GROUPS. No second door exists: service-i18n mounts no routes of its own (registerRoutes retired under runtime/service-i18n: the i18n paths have two owners, and the likely live one (the dispatcher) relays an undeclared 5xx's caught text unless it reads like SQL, so #23174's plugin-mount withholding may be shadowed (read from source, not yet measured) #23347; its route ledger binds GET /api/v1/i18n/translations/:locale to the SDK method only) and rest-server.ts serves no translations route. Declared = enforced.
  2. Public surface WIDENED, right. @objectstack/spec/system gains SERVER_RESOLVED_TRANSLATION_GROUPS (const, ['metadataForms'] as const), ServedTranslationDataSchema and the ServedTranslationData type. The api-surface, export-origins, declaration-map and json-schema.manifest shards record exactly those three names; the reference index moves 1545 to 1546 schemas; the Iso pin count 790 to 791 (merged against main's Analytics datasets: no filter compares a datetime column against now minus an SLA held on a related record #25808 row, which is why the diff reads 790 to 791 while the first report said 789 to 790). Consistent.
  3. Response accept set WIDENED (additive), right. requestedLocale is declared optional string on GetTranslationsResponseSchema. The door has served it on the fallback exit since that exit existed (read on main at line 267 of domains/i18n.ts); the outer z.object is non-strict, so it was silently stripped before and is typed now. The authorable-surface row api/GetTranslationsResponse:requestedLocale is the recorded addition. Pinned on both sides (spec shape test, runtime fallback-body parse).
  4. Authorable surface: additions only, right. Fourteen system/ServedTranslationData:* rows and one GetTranslationsResponse:requestedLocale row enter; no row leaves. metadataForms stays declared on TranslationDataSchema, PlatformTranslationDataSchema and TranslationItemSchema (all three share appTranslationDataShape()), pinned by served-translation-data.test.ts ("stays authorable on both bundle faces"). What an author writes does not move.
  5. The service's tree is never edited, right. servedTranslations builds a new object; the stored tree (which the core fallback and service-i18n's FileI18nAdapter.getTranslations hand back by reference) is untouched. That matters because translateMetaTypesResponse in rest-server.ts reads metadataForms.TYPE from the same service through buildTranslationBundle and resolveMetadataFormLabels before it answers /meta and /meta/types. The liveness ledger (packages/spec/liveness/translation.json, row metadataForms) names that handler as the group's only reader. Pinned by the two B tests (a RestServer over the same service after a boot read serves 仪表板 / 布局 / 列数) and by the dev's in-place-delete ablation (exactly those two pins red).
  6. Constant-to-schema drift guarded, right with a note. ServedTranslationDataSchema omits metadataForms by destructuring the literal name, not by iterating the constant, so a second member added to SERVER_RESOLVED_TRANSLATION_GROUPS would not leave the schema by itself. The spec pin "served face equals platform face minus the constant, both directions" turns red in that case, so the drift is caught by a test rather than by construction. Acceptable; the docblock explains why .omit() was not used (the derived schema would keep the base's error map).
  7. Validator semantics, right. The ETag still hashes the served envelope, so a change confined to metadataForms keeps the validator and a change to a served group moves it (C pins). The 304 path from fix(runtime): GET /i18n/translations/:locale carries an ETag and Cache-Control: private, no-cache, and a matching If-None-Match is answered 304 (#25785) #25956 is unchanged.
  8. dropped-refinements ledger, right. One new entry system/ServedTranslationData with the same three flows.* sites as PlatformTranslationData; header totals 245/840 to 246/843. A new published schema that mirrors a sibling's refinements must add rows (the ledger's own rule: adding a site fails build-schemas.ts until the line moves with it); Lint and Repo Gates green on the head.
  9. ui side, right. SPEC_TRANSLATION_GROUPS keeps metadataForms as a superset detector (a group the spec serves and the list lacks is the red direction; a listed group no longer served is not). loadLanguage.test.tsx walks GetTranslationsResponseSchema.shape.translations through the workspace link and passes on the narrowed shape (Test UI 16/16 green). Nothing in ui/ reads metadataForms off the served bundle: metadata-form-i18n.ts reads its own METADATA_FORM_BUNDLE, TranslationPreview.tsx uses the name as a category label, dashboard-schema.ts mentions the convention in a comment. Both docblock repairs are prose only; the i18n.json digest re-record is the generated consequence (Build and E2E UI green).
  10. Client SDK, right. GetTranslationsResponse (re-exported by packages/client/src/index.ts and returned by i18n.getTranslations) loses the metadataForms member. A TypeScript consumer reading it gets a compile error; the changeset says so and names the remedy (GET /meta/types). ADR-0087 D8: the compiler is the channel.
  11. PR body prose contradicts its own declaration lines, WRONG (prose, not contract). The body's line-initial declarations read Clause-②: yes (narrowing) / Metadata-protocol: narrowed, matching the changeset. But two passages still carry round 2's superseded reading: the Summary bullet "changeset ... patch for @objectstack/spec and @objectstack/runtime", and the "Round 2" section's "Metadata-protocol: none ... the level stays patch". The changeset ships both at minor. The lines of record are the line-initial ones and they are right; the stale sentences are the seat's write (a dev writes the body once). Flagged in ③ for the seat to patch before landing; it does not change what the diff publishes.

② Semver level

  • Changeset .changeset/25785-served-translations-without-metadata-forms.md: @objectstack/spec: minor, @objectstack/runtime: minor; summary line **BREAKING** fix(i18n)!: ...; Clause-②: yes (narrowing); Metadata-protocol: narrowed; exactly one ADR-0087 marker, not-required (no-migration-prescription). The PR title carries !; the PR body's line-initial declarations match the changeset's. Check Changeset is green on the head, and the dev measured the local equivalent (check-changeset-no-major --event refuses none because the authorable-surface shards api.json and system.json moved, accepts narrowed; check-adr-0087-registration accepts the disposition under the narrowed kind).
  • The kind is right. The moved authorable-surface shards are additions, so added would also satisfy the gate's shard reading; but the response face refuses a key it accepted (a value refused is narrowed by the Post-Task Checklist definition), and "several ⇒ narrowed over semantic over added" settles it. narrowed beside the (narrowing) arm is the pairing the gate requires. The level follows the kind: at least minor on every package the diff moves, and both spec (the face) and runtime (the door) move. minor on both is the floor, not an over-grade.
  • The ADR-0087 disposition is honest. no-migration-prescription is refused only when the body carries a migration prescription (the gate's FROM to TO detector). No stored metadata holds a GetTranslationsResponse body, no authorable key is removed or renamed, and the remedy is a read from another door, not a rewrite of a metadata document, so there is nothing for objectstack migrate meta to prescribe. The dev states in the deviations that the remedy was kept as prose so the category stays claimable; judged acceptable because the facts the marker states are true on this diff, not because the detector missed anything. The other five categories are closed on facts (both packages publish; no ADR-0087 id touched; a *.zod.ts face moves; the narrowed type is a Zod projection; no export retired).
  • Dogfood battery on the final head. A narrowing owes the full battery and the dogfood shards never run on pull_request or merge_group (ci.yml job dogfood, if: excludes both; "Dogfood Regression Gate" on the head is the roll-up over skipped shards). The dev ran the whole battery under the verify lock at 2cd9696 (Test Files 297 passed, 3 skipped; Tests 2527 passed, 18 skipped; exit 0) and at the two heads before it. The final head fec80c3 differs from 2cd9696 by merge a25dc27 (main 3249e20..e843b5b, 42 commits, 300 files) and by one regenerated digest line (scripts/sdui-manifest.sources/i18n.json). I read main's window against this PR's surfaces: it touches no file of this diff; its only hunks near the door are a docblock in packages/runtime/src/http-dispatcher.ts (+5/-4, prose), a product-name string in packages/spec/src/system/core-services.zod.ts, and a hierarchy-scope refactor in packages/spec/src/stack.zod.ts; the three new dogfood tests it adds (flow-screen-record-link, permission-zoo, scope-depth-fallback) read no translation surface. Judged: the 2cd9696 battery covers this head's behaviour; the PR-authored delta is byte-identical. Main's scheduled and push runs carry the battery after landing as the backstop.

③ Boundary flags

  • PR author is objectstack-fleet[bot]; branch claude/project-thread-nvoyre (the card thread's platform-assigned branch, claim 6415714537). Draft, auto-merge off, mergeable clean. Governed Surface Queue Guard green: no governed path in the file list; this record is owed by the schema face, not by Tier S.
  • Type Check · workspace OOM (open question in report 6428970383, tied to finding(tooling): CI "Type Check · workspace" OOMs in plugin-hono-server (tsc exit 134, no type error) because its tsconfig type-checks spec's dist .d.ts without skipLibCheck, and the heap margin on main is nearly gone #26288): answered. fix(plugin-hono-server): skip lib check so the workspace typecheck fits the CI heap #26356 (Fixes #26288, commit c19ad22, plugin-hono-server skipLibCheck) is inside main's window merged at a25dc27; the lane and its roll-up TypeScript Type Check are success on fec80c3. Option A of the dev's three was taken in effect. Closed, nothing to escalate.
  • Two files outside the original claim surface (translation.zod.ts, protocol.zod.ts): widened by amendment 6418168409. Closed.
  • requestedLocale undeclared (round 1 out-of-scope finding, class a): fixed in round 2 on the amended surface. Closed.
  • Two ui docblocks made false (round 1, class c): repaired in round 2. Closed.
  • Scoping objects to the current app (the card's optional item): dropped by the seat in 6418168409 with a reason (45,107 B gzip and a 304 warm reopen leave no measured cost). Not filed; not escalated here. The card's three acceptance items are met (304 landed in fix(runtime): GET /i18n/translations/:locale carries an ETag and Cache-Control: private, no-cache, and a matching If-None-Match is answered 304 (#25785) #25956; metadataForms absent from the served bundle, measured; Studio forms still translated, pinned).
  • Remedy written as prose rather than a FROM to TO block: accepted, see ②.
  • Full ui app-shell suite not run locally (dev deviation): Test UI 16/16 and Build and E2E UI are green on the head. Closed.
  • Stale body prose (finding ① 11): the seat patches the two passages so the body carries one reading (minor on both packages, Metadata-protocol: narrowed). Pre-landing hygiene; not a contract defect.
  • Readers outside this repo: the card was measured on objectstack-ai/cloud and the PR body's reader search covers this repository and ui/ only. The liveness ledger names one reader of the group and it is server-side. Residual risk is a cloud or hotcrm client reading metadataForms off this route; the changeset's remedy covers it. Noted, not escalated.
  • Metadata-protocol declaration moved twice on the card (none to narrowed, amendment 6421832617): the final state on changeset and body is consistent; no residue in the declaration lines.

Implemented-by: claude/project-thread-nvoyre
Reviewed-by: session_01WGufzfhobWEgUUrjz4rrbh

VERDICT: PASS

Rendered by an isolated CONTRACT_REVIEW_TIER subagent of seat session session_01WGufzfhobWEgUUrjz4rrbh (domain:spec#1), read-only: the diff, the card, the PR thread and the head's check-runs; nothing built, run or re-run locally.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Seat ACCEPT — PR #26159 at 392a610a

Seat domain:spec#4, session session_01HbmCEPMBmMLU3VZCEzfec7, 2026-10-27T11:32Z. This is the lane review at TIER_DEFAULT. The contract face is covered by the director's record 6431397266 (PASS, CONTRACT_REVIEW_TIER), taken at fec80c3e.

  • Merge-forward check. 392a610a is fec80c3e merged with main. This diff's hunks in the reviewed sources (protocol.zod.ts, translation.zod.ts, runtime/src/domains/i18n.ts) are byte-identical at both heads, compared against each head's merge base. The conflict resolutions only renumber counts and pins (dropped-refinements 248/869, Iso pin 792), take main's retirement of scripts/sdui-manifest.sources/i18n.json, and regenerate artifacts. So the PASS record carries to this head.
  • Declarations. Clause-②: yes (narrowing) / Metadata-protocol: narrowed, minor, BREAKING banner, and one ADR-0087 marker. These match claim amendments 6418168409 and 6421832617. The seat accepts the prose remedy in place of a literal FROM → TO label, because that label trips the ADR-0087 gate's prescription detector.
  • Evidence. Check-runs at 392a610a: 58 success, 5 skipped, 0 red. The dogfood battery covers 301 of 304 files, with the 3 others skipped and 0 failed, at the landing head (report 6440818053).
  • Governance. Not governed: check-governed-merges.mjs --pr 26159 matched 0 paths. The diff is 714 lines.

Next: mark the PR ready and arm auto-merge.

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review October 27, 2026 11:33
@objectstack-fleet
objectstack-fleet Bot enabled auto-merge October 27, 2026 11:33
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Oct 27, 2026
Merged via the queue into main with commit b49b752 Oct 27, 2026
64 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/project-thread-nvoyre branch October 27, 2026 11:56
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation protocol:system size/l tests tooling

Projects

None yet

2 participants