Skip to content

fix(metadata-protocol,rest): a .custom pair's declared override is labelled in the ruled order on every /meta read - #30353

Merged
objectstack-fleet[bot] merged 8 commits into
mainfrom
claude/project-thread-c288y8
Nov 5, 2026
Merged

objectstack-fleet[bot] merged 8 commits into
mainfrom
claude/project-thread-c288y8

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Requested by Bill · project thread

domain:server#5 · os-bill · session_01Utp6CYVV6fcAQrNTDQRvPb

维护者速读

  • 改了什么:客户在 <基础包>.custom 配对包里覆盖了基础包的页面、数据集、应用或书(仪表盘、视图、报表同样适用),改过的标签现在在所有 /meta 读上按裁定顺序显示:配对包自己的译文优先,其次是覆盖项自己写的文字,最后才是基础包的译文。
  • 为什么改:以前页面、数据集、应用、书的覆盖项在非源语言下被基础包的译文盖住,客户改的名字看不到;七类覆盖项都不读配对包自己发布的译文。
  • 风险与回滚:只有声明覆盖项多一次读,读的是配对包的 active translation 行,列表按基础包每次请求只读一次;不是覆盖项的条目没有新增读。回滚即撤本 PR,没有数据迁移。
  • 先看:packages/rest/src/meta-item-read-gate.ts 的 pairTranslationBundleOf 和 PACKAGED_BASE_ACCESSORS 新加的四行,再看 packages/metadata-protocol/src/protocol-discovery-and-bases.ts 的 getPairTranslationBundle。
  • 两分钟验证:pnpm --filter @objectstack/rest exec vitest run src/meta-dashboard-view-i18n-explicit-override.test.ts,预期 108 条全绿;修复前的代码上有 19 条是红的。
  • 需要你做的:无,席位落地。

Fixes #30062

Clause-②: yes (widening) — ObjectStackProtocolImplementation gains six public methods, getPackagedPageBase, getPackagedDatasetBase, getPackagedAppBase, getPackagedBookBase, servedOverrideBaseOf and getPairTranslationBundle (measured on the built packages/metadata-protocol/dist/index.d.ts); @objectstack/rest's exported MetaBookTreeSources accepts one more optional key, resolveProtocol (measured on the built packages/rest/dist/index.d.ts). REST's new pairTranslationBundleOf is module-internal: it is not in the package's export list. Nothing accepted is taken away.
Metadata-protocol: semantic
Skills-docs: none — the "Customizing an installed package (ADR-0146)" paragraph of skills/objectstack-platform/SKILL.md says nothing about how an override's labels are translated, so no sentence there turns false; its stale served-type list is #30229's.

What changes

The spec half (PR #30236, #30193) gave every document translator packagedBase and pairBundle. Its "Server-half contract for #30062", quoted:

For a served item that is a declared .custom override, on item, list and container reads alike, for all eight types:

  • packagedBase = the BASE package's item (pre-override: the document the override replaces), never the override itself;
  • pairBundle = { locale: data } built from that .custom package's own translations only: its code-shipped translation bundle and its authored translation rows, both of which are the pair's own;
  • keep passing packagedBase for object, dashboard, view and report exactly as today, on every served item (PACKAGED_BASE_ACCESSORS, form-routes.ts :385 :421): the object reads it for extension-folded field labels too (ADR-0029 D9.2a, i18n: a tenant's rewrite of an objectExtensions-contributed validation rule message still loses to that package's translation, on the refusal and on /meta #27559), so it is not override-only. Add the same accessor for page, dataset, app and book;
  • omit pairBundle for an item that is not a declared override.

This PR is that server half, with the seat's rulings on where the contract does not fit the code (Deviations below).

packagedBase for page, dataset, app and book. Four protocol accessors, getPackagedPageBase / getPackagedDatasetBase / getPackagedAppBase / getPackagedBookBase, answer the base package's own item through the same shadow-immune lookup getPackagedDashboardBase / ViewBase / ReportBase use (packagedArtifactBase, now typed over the seven artifact types). They answer the base's item while the override is served, right after it is published and after a restart, and undefined for an unknown name. PACKAGED_BASE_ACCESSORS in the REST read gate gains the four rows, so GET /meta/:type/:name and GET /meta/:type hand them to the translators. action is now the one translatable type without a row.

pairBundle for a declared override. servedOverrideBaseOf(type, document) names the base a served document overrides: an overridable type, an overrides.package, and a served _packageId equal to it (the envelope protocol-meta-item-reads.ts stamps on a served override). Anything else, a base's own item included, answers undefined. getPairTranslationBundle(base) reads that base's pair's own active translation rows (package_id = customizationPackageId(base)) through readAuthoredTranslationLayer, the same reader the i18n service's authored layer uses (conversions, locale resolution and bookkeeping stripped), and answers { locale: data }, or undefined when the pair has none, the id is empty or invalid, or the read fails. The REST gate's pairTranslationBundleOf joins the two, feature-detected on the protocol, and passes the result as pairBundle on the item read and on every listed item. A list reads each base's pair once per request.

The book tree. createMetaBookTreeAnswer forced resolveProtocol to undefined, so GET /meta/book/:name/tree translated a book with no packaged base. MetaBookTreeSources now takes the transport's resolveProtocol, and the RestServer route passes it. The runtime dispatcher's tree sources already spread it. The docs sidebar and the book list now serve the same label (#26690).

Deviations

  • packages/rest/src/meta-item-read-routes.ts (+2): the book tree route's sources, outside the dispatched surface; widened by the seat for the tree fix above.
  • packages/objectql/src/protocol-custom-override-bare-slot-envelope.test.ts (+93, test only): the real-boot pins reuse metadata-protocol: page, app, dataset, doc and book serve a published override only from the registry bare slot; build the row-served path, then take them out of it #29613's sealed-boot fixture there. They cannot live in packages/metadata-protocol: it does not depend on @objectstack/objectql (objectql depends on it), so the import would be a cycle. Widened by the seat.
  • scripts/engine-double-contract.pinned.json (+5 / -1, generated): check:engine-double-contract ratchets the two new findOne doubles the new pins assert (protocol.pair-translation-bundle.test.ts 0 → 1, meta-dashboard-view-i18n-explicit-override.test.ts 1 → 2), written by node scripts/check-engine-double-contract.mjs --write. Coverage grew; nothing else in the ledger moved.
  • No code translation source. The contract names the pair's "code-shipped translation bundle". A .custom package is authored, never a code package, so it has none; and code bundles reach the i18n service's static layer with no package provenance. pairBundle is built from the pair's active translation rows only (seat ruling).
  • Seven types, not eight. An object is extended, never replaced (ADR-0146 D10): it carries no declared override, so servedOverrideBaseOf answers undefined for it and no object read takes a pairBundle. Its packagedBase is passed exactly as before.
  • Not covered here. The view container read builds its slots without the served-override envelope, so servedOverrideBaseOf answers undefined there and the slots keep today's packagedBase order. form-routes.ts (public form views) is outside the surface and is unchanged: it still passes packagedBase, with no pairBundle.
  • A package-less authored translation is in the deployment catalog but is not the pair's, so it no longer outranks a changed label on a declared override. That is the ruled order (6631250894: only the pair's translation ranks first).

Tests

  • packages/rest/src/meta-dashboard-view-i18n-explicit-override.test.ts (real SchemaRegistry and protocol over fake engine rows, through RestServer): §1 now asks each of the eight accessor rows, the four new ones included, and asks no accessor for action, flow, a plural and a prototype key. New §5 over page, dataset, app, book and view: an override's own label beats the base's translation (zh-CN and en); a translation the pair ships wins in zh-CN while en reads the override's label, on the item and the list read; no override reads the base's translation; a member reads what an admin reads; the book tree serves the label the book list serves for three seeds. 19 red on the pre-fix code, 108/108 after.
  • packages/objectql/src/protocol-custom-override-bare-slot-envelope.test.ts: a real boot, page / app / dataset / book, with and without a metadata service: each accessor answers the base's item before the override, right after it and after a restart; servedOverrideBaseOf answers the base on the served and listed items and nothing on the base's own; getPairTranslationBundle is undefined before, then exactly the pair's saved translation beside a package-less row of the same key, and undefined for the pair's own id; fed to translateMetadataDocument the three give pair → own → base. 16 new, file 87/87.
  • packages/metadata-protocol/src/served-override.test.ts (servedOverrideBaseOf table) and the new protocol.pair-translation-bundle.test.ts (pair rows only, every read naming package_id, bookkeeping stripped, undefined with no read for an empty, invalid or pair id, undefined on a read failure): 15/15. Its fake engine refuses any operator or combinator, holds the caller's limit after the filter, and opens findOne with assertEngineFindOnePredicate.

Ablations

Through scripts/ablation-replace.mjs, each restored to blob == HEAD with git diff HEAD empty. A metadata-protocol mutation was rebuilt and proved in dist/ with ablation-dist-preflight.mjs (exit 0) before the objectql and rest legs, which read its dist/; after the last one a clean rebuild was proved free of all three markers (--absent, exit 0).

Mutation Red
A1 the four new PACKAGED_BASE_ACCESSORS rows deleted rest 18/108
A2 pairTranslationBundleOf never fetches rest 11/108
A3 the book tree route's resolveProtocol deleted rest 1/108 (tree == list)
A4 servedOverrideBaseOf always undefined mp 1/15, objectql 8/87, rest 11/108
A5 the pair's package_id filter dropped mp 2/15, objectql 8/87
A6 getPackagedBookBase answers undefined objectql 4/87, rest 4/108

A5's first run left objectql green: the layer merges rows in name order, and the package-less row sorted before the pair's, so its text never showed. The row was renamed to sort after the pair's, and the pin now goes red.

Docs

content/docs/ui/translations.mdx, "An edit beats the packaged catalog": the bullet "An edited string is served in the language it was written in, to every reader" is false for a declared override whose pair translates the string. It now names that exception, and a new paragraph states the order for a declared override (page, dataset, app, book, dashboard, view, report; the pair's translations first; only translation items saved in the pair; the item, list and book tree reads agree). content/docs/concepts/metadata-lifecycle.mdx and content/docs/api/metadata-api.mdx were read sentence by sentence: neither says how an override's labels are translated, so neither changes.


Generated by Claude Code

…verride (red on main)

A page, dataset, app or book override serves the base's translation over
its own label, and a view override never reads the pair's translation.

Claude-Session: https://claude.ai/code/session_01Utp6CajcU31aZRQuQzwWj6
Co-authored-by: Claude <noreply@anthropic.com>
…s the ruled label order on every type

The page, dataset, app and book packaged bases join their siblings, and
the /meta reads hand each translator the pair's own translations, so a
translation the pair ships beats the override's own label, which beats
the base's translation (ruling record 6631250894).

Claude-Session: https://claude.ai/code/session_01Utp6CajcU31aZRQuQzwWj6
Co-authored-by: Claude <noreply@anthropic.com>
…'s own translations on a real boot

Claude-Session: https://claude.ai/code/session_01Utp6CajcU31aZRQuQzwWj6
Co-authored-by: Claude <noreply@anthropic.com>
…the pair's bundle would show

Claude-Session: https://claude.ai/code/session_01Utp6CajcU31aZRQuQzwWj6
Co-authored-by: Claude <noreply@anthropic.com>
…anslation first; changeset

Claude-Session: https://claude.ai/code/session_01Utp6CajcU31aZRQuQzwWj6
Co-authored-by: Claude <noreply@anthropic.com>
…s the bound and asserts findOne; ratchet the engine-double ledger

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

github-actions Bot commented Nov 5, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 2 package(s): @objectstack/metadata-protocol, @objectstack/rest, touching 31 documentable anchor(s).

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

  • content/docs/api/client-sdk.mdx (via getBookTree (sdk, the bare tail of client method meta.getBookTree, bound to GET /api/v1/meta/book/:name/tree), meta.getBookTree (sdk, the route ledger binds it to GET /api/v1/meta/book/:name/tree, selected by route anchor /book/:name/tree))
  • content/docs/api/metadata-api.mdx (via hotcrm.custom (literal, a string literal in a comment on a changed line))
  • content/docs/build-without-code.mdx (via hotcrm.custom (literal, a string literal in a comment on a changed line))
  • content/docs/capabilities/integrations.mdx (via hotcrm.custom (literal, a string literal in a comment on a changed line))
  • content/docs/concepts/metadata-lifecycle.mdx (via ObjectStackProtocolImplementation (symbol, a top-level class), hotcrm.custom (literal, a string literal in a comment on a changed line))
  • content/docs/data-modeling/object-extensions.mdx (via hotcrm.custom (literal, a string literal in a comment on a changed line))
  • content/docs/ui/apps.mdx (via /book/:name/tree (route, bridged from symbol createMetaBookTreeAnswer — its route source's handler names it))
  • content/docs/ui/translations.mdx (via /book/:name/tree (route, bridged from symbol createMetaBookTreeAnswer — its route source's handler names it))

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

  • content/docs/releases/v14.mdx (via /book/:name/tree (route, bridged from symbol createMetaBookTreeAnswer — its route source's handler names it))
  • content/docs/releases/v16.mdx (via ObjectStackProtocolImplementation (symbol, a top-level class))
  • content/docs/releases/v17/17-0.mdx (via ObjectStackProtocolImplementation (symbol, a top-level class))
  • content/docs/releases/v17/17-5.mdx (via /book/:name/tree (route, bridged from symbol createMetaBookTreeAnswer — 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
  • 1 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 61 of 219 client-bound route-ledger rows — the other 158 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 158: 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; 103 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 — 26 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 8c9f48f87c3d5aa99778d5809ceb92128a6f5ea4 → packageMentionDocs.

Which tree this was computed on

This run read content/docs from 4897081b20eb77633859b86e4fa384855fc67096 — the merge of head 644edc243cb470e69d05b182ed261aa3fc9f1bda into base 8c9f48f87c3d5aa99778d5809ceb92128a6f5ea4, 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 4897081b20eb77633859b86e4fa384855fc67096 && git checkout 4897081b20eb77633859b86e4fa384855fc67096
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 8c9f48f87c3d5aa99778d5809ceb92128a6f5ea4 644edc243cb470e69d05b182ed261aa3fc9f1bda && git checkout -B drift-repro 8c9f48f87c3d5aa99778d5809ceb92128a6f5ea4 && git merge --no-ff 644edc243cb470e69d05b182ed261aa3fc9f1bda

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

⚠️ 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 8c9f48f87c3d5aa99778d5809ceb92128a6f5ea4 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:server#5 · os-bill · session_01Utp6CYVV6fcAQrNTDQRvPb

席位核对 · ACCEPT · 2026-11-05T07:12Z · head 644edc24

Reviewed against order server-seat5/dispatch-30062-item3.md (Claim 6650380068), option A.

  • Four packaged bases. getPackagedPageBase / Dataset / App / Book answer through the one packagedArtifactBase body (type widened to PackagedArtifactBaseType), and PACKAGED_BASE_ACCESSORS gains its four rows — one table, no second resolver.
  • The pair's translations rank first. servedOverrideBaseOf reads the served envelope (_packageId equal to overrides.package, overridable types only, never object per D10); getPairTranslationBundle reads the pair's active translation rows through readAuthoredTranslationLayer, the authored layer's own reader, narrowed to the pair binding; a failed read answers undefined (the override's own text), never a 5xx. REST passes pairBundle on the item read, the list read (memoised per base) and the book tree (now given resolveProtocol, so the tree and the sidebar list agree). This is ruling 6631250894's order.
  • Deviations accepted as seat rulings: meta-item-read-routes.ts (+2, book tree sources), the objectql real-boot pin file (cycle: metadata-protocol cannot import objectql), the generated engine-double-contract.pinned.json ratchet (coverage grew only), no code-bundle source for a .custom pair, seven types not eight, the view container read and form-routes.ts left as is (follow-up if measured).
  • Declarations. Clause-②: yes (widening) — six public protocol methods and one optional MetaBookTreeSources key, nothing taken away. Metadata-protocol: semantic, minor changeset. Skills-docs: none holds (SKILL.md's ADR-0146 paragraph says nothing on override label translation; its served-type list is skills(objectstack-platform): the ADR-0146 paragraph lists six types a pair's override is served for; page, app, dataset, doc, book and mapping are served too, and the pair can remove its own override #30229's).
  • Gates. check-governed-merges: NOT governed, 903 lines. PR-body guards, governed queue guard and spec liveness green; remaining CI runs gate the queue.

Landing: ready + auto-merge. Fixes #30062 closes the card (items 1/2/4/5 landed in PR #30226).

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review November 5, 2026 07:13
@objectstack-fleet
objectstack-fleet Bot enabled auto-merge November 5, 2026 07:13
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Nov 5, 2026
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:server#5 · os-bill · session_01Utp6CYVV6fcAQrNTDQRvPb

Re-queued · 2026-11-05T07:50Z: dequeued by the merge group's Spec Liveness Check failure, not this PR's. The failing citation is query/searchFields → packages/objectql/src/engine.ts (a key-mention anchor). This PR touches neither engine.ts nor the spec liveness ledger; the first red group was pr-30341 (PR #30341 moves engine.ts docblocks out, the ones that named searchFields), and every later group carried it. main (8589756) still names searchFields in engine.ts. PR #30341 has left the queue, so auto-merge is re-enabled on head 644edc24 unchanged.

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 size/l tests tooling

Projects

None yet

1 participant