Skip to content

fix(objectql,rest)!: serve an object an extension renames without the base's plural (#30391) - #30459

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

objectstack-fleet[bot] merged 7 commits into
mainfrom
claude/project-thread-ktw7m9

Conversation

@objectstack-fleet

@objectstack-fleet objectstack-fleet Bot commented Nov 5, 2026 •

Copy link
Copy Markdown
Contributor

Requested by Bill · project thread

domain:server#5 · os-bill · session_01Utp6CYVV6fcAQrNTDQRvPb

维护者速读

  • 改了什么:对象扩展(.custom 配对包里的 object_extension,或代码包的 objectExtensions)只改了 label、没写 pluralLabel 时,/meta/object 不再返回基础包的复数名,也不再返回基础包对旧复数的译文;对象按“没声明复数”的样子返回,列表标题就显示新名字。配对包自己保存了该复数的译文时,返回配对包的译文。
  • 为什么改:客户改了对象名,侧栏和列表标题(先读复数)还是旧名字,一个对象两个名字;中文下尤其明显,因为旧复数还被翻成了“任务”。
  • 风险与回滚:这是一次已返回值的收窄(BREAKING):所有只改 label 的扩展都会少返回 pluralLabel,包括 showcase 自带的 showcase_account(列表标题从 “Accounts” 变成 “Account (Success Overlay)”)。没有数据迁移,回滚即撤本 PR。
  • 先看:packages/objectql/src/registry.ts 的 mergeObjectDefinitions(标量覆盖那一段),再看 packages/rest/src/meta-item-read-gate.ts 的 withoutStaleObjectPlural 和它在 translateMetaList / translateMetaDocument 里的两处调用。
  • 两分钟验证:pnpm --filter @objectstack/rest exec vitest run src/meta-object-extension-plural-label.test.ts,预期 11 条全绿;修复前的代码上有 4 条是红的。
  • 需要你做的:无,席位落地(非受治理面,narrowing 已过达档合同复核 PASS 6658126879)。

Fixes #30391

Clause-②: no (narrowing) — /meta/object (item and list reads, both transports) and every registry read of an object (SchemaRegistry.getObject, the extension fold) stop serving pluralLabel for an object an extension renames without a plural of its own, where they served the base's plural (and, in a translated locale, the base's translation of it). The shipped showcase's showcase_account is one. Nothing an author writes is refused, and no exported symbol moves.
Metadata-protocol: semantic
Skills-docs: follow-up #30466 — skills/objectstack-data/SKILL.md "Object Extension Model" says an extension "can add but never remove"; a renaming label now takes the base's pluralLabel off. The sentence wants that exception (skills/** is outside this change's surface).

What changes

pluralLabel is the plural of label. The seat's route (served side, under ruling record 6631250894's label order): an extension that changes label without pluralLabel serves what an object with no pluralLabel serves, the pair's own translation first. The new pin measures that reference on the same boot (crm_note, which declares no plural): it is served with no pluralLabel key in en and in zh-CN, and the Console then titles a list by the label (useNavTargetLabel's objectPluralLabel: served plural, else served label).

Two halves, and each one alone leaves a locale red (Ablations below):

  • The fold (mergeObjectDefinitions, @objectstack/objectql). When an extension's label is applied (it does not yield to a tenant-authored label, ADR-0029 D9.2a), differs from the running label, and the extension sets no pluralLabel, the running pluralLabel is deleted. A plural the tenant authored stays (the same D9.2a yield). The fold stays idempotent over an already-folded base: the second pass yields on the label and leaves the plural absent. getPackagedObjectBase is untouched and keeps the owner's plural, which the translators compare against.
  • The /meta read (withoutStaleObjectPlural, @objectstack/rest). The spec's translateObject answers a missing pluralLabel with the catalog's objects.NAME.pluralLabel, and that entry is the base package's translation of the plural the extension left behind (the en catalog repeats the source text, so en is affected too). Where the served object carries no plural, the packaged base declares one, and the served label diverged from the packaged label, the catalog's plural is replaced by the owner package's .custom pair's own translation of objects.NAME.pluralLabel (getPairTranslationBundle(_packageId of the packaged base), read once per base per request, translated through translateMetadataDocument over the pair's bundle alone), or dropped when the pair has none. Every other object is returned unchanged, and the pair is read only when the rule applies. It runs on both translateMetaList and translateMetaDocument, which both transports use.

No spec change: the catalog lookup stays the spec's; the server decides what it serves. No ADR text changes: ADR-0146 D10 says an object is extended, never replaced; the base's declaration is untouched and only the served fold changes.

Tests

  • packages/rest/src/meta-object-extension-plural-label.test.ts (new, real SchemaRegistry + real ObjectStackProtocolImplementation + RestServer, i18n service over a deployment catalog with the pair's row merged in, both reads, en and zh-CN): the reference (crm_note, no plural); a label-only rename serves what the reference serves; the pair's zh-CN plural is served and en gets none; a pair row translating only label does not let the base's plural back; controls: label + plural served as set, plural only, neither (base plural and 任务 kept), repeated label, an untouched object. 4 red on main 09720e2c (en and zh-CN rename, pair, non-pair), 11/11 after.
  • packages/objectql/src/registry-extension-plural-label.test.ts (new, the fold): rename drops the plural; packaged base keeps Tasks and _packageId; idempotent refold; plural set, plural only, fields only, repeated label; tenant-authored plural stays; tenant-authored label makes the rename yield; two extenders in priority order both ways. 11/11.
  • Suites at f71f7bdc (after merging origin/main; the two later commits touch only the changeset and the engine-double ledger): @objectstack/objectql 551 files, 10983 passed; @objectstack/rest 387 files, 7346 passed / 332 skipped; typecheck for both exit 0 (including check:test-typecheck); eslint on the four changed source/test files exit 0.

Ablations

Through scripts/ablation-replace.mjs, each restored to blob == HEAD with git diff HEAD empty.

Mutation Red
A the fold's delete merged.pluralLabel guarded off (rebuilt; ablation-dist-preflight.mjs exit 0, then --absent exit 0 after the restore rebuild) objectql 2/11, rest 4/11
B withoutStaleObjectPlural returns the translator's answer unchanged (rest tests import src) rest 4/11 (both locales: the en catalog repeats the source plural)

Acceptance notes

Verification

Derived gates, dogfood, lint — final head a1107d3
  • node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack at f71f7bdc: 91 commands; the ledger commit added 8 (99). Each run with its exit code captured before any pipe. 94 exit 0 on the first pass. Five did not:
    • check-adr-0087-registration --base origin/main exit 1: a FROM → TO block contradicted not-required (no-migration-prescription). Nothing has to be rewritten, so the block became a sentence saying so; re-run exit 0.
    • check:engine-double-contract exit 1: the new pin's findOne double was not in the ledger; node scripts/check-engine-double-contract.mjs --write added one row (scripts/engine-double-contract.pinned.json, +5); re-run exit 0.
    • check-plugin-teardown-shape --self-test, check:skill-examples, check:dual-build-cjs-loads exit 3 (prerequisite): exit 0 after fetching the pinned commit and a whole-workspace build.
  • dispatch-gates --ran at a1107d3c6: 99 derived, 99 run, 0 NOT-MEASURED, 0 UNRUN, exit 0.
  • pnpm lint (eslint . --no-inline-config) at a1107d3c6: exit 0.
  • Full dogfood (Clause-② carries the narrowing arm), pnpm --filter @objectstack/dogfood test under os-verify-lock.sh at f71f7bdc (same code as the final head): VERDICT command-exit 0, Test Files 419 passed / 3 skipped (422), Tests 3492 passed / 36 skipped (3528), 0 failures.
  • Ablation legs: see Ablations. CI check-runs: not read at the time of writing.

Docs

content/docs/data-modeling/object-extensions.mdx: the merge table's label row now says a new label without a plural takes the target's plural off. content/docs/ui/translations.mdx: the object paragraph of "An edit beats the packaged catalog" now says such an object is served with no plural unless the owning package's .custom pair translates it. releases/** untouched.


Generated by Claude Code

@github-actions github-actions Bot added size/l documentation Improvements or additions to documentation tests tooling Fix lands in tooling, not a product package — enters pm:queue only with Unblocks: or a named surface labels Nov 5, 2026
@github-actions

github-actions Bot commented Nov 5, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 2 package(s): @objectstack/objectql, @objectstack/rest, touching 8 documentable anchor(s).

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

  • content/docs/api/wire-format.mdx (via pluralLabel (literal, a string literal in mergeObjectDefinitions; a string literal in withoutStaleObjectPlural))
  • content/docs/concepts/metadata-driven.mdx (via pluralLabel (literal, a string literal in mergeObjectDefinitions; a string literal in withoutStaleObjectPlural))
  • content/docs/data-modeling/index.mdx (via pluralLabel (literal, a string literal in mergeObjectDefinitions; a string literal in withoutStaleObjectPlural))
  • content/docs/data-modeling/object-extensions.mdx (via pluralLabel (literal, a string literal in mergeObjectDefinitions; a string literal in withoutStaleObjectPlural))
  • content/docs/data-modeling/objects.mdx (via pluralLabel (literal, a string literal in mergeObjectDefinitions; a string literal in withoutStaleObjectPlural))
  • content/docs/data-modeling/schema-design.mdx (via pluralLabel (literal, a string literal in mergeObjectDefinitions; a string literal in withoutStaleObjectPlural))
  • content/docs/getting-started/build-with-claude-code.mdx (via pluralLabel (literal, a string literal in mergeObjectDefinitions; a string literal in withoutStaleObjectPlural))
  • content/docs/getting-started/examples.mdx (via pluralLabel (literal, a string literal in mergeObjectDefinitions; a string literal in withoutStaleObjectPlural))
  • content/docs/getting-started/quick-start.mdx (via pluralLabel (literal, a string literal in mergeObjectDefinitions; a string literal in withoutStaleObjectPlural))
  • content/docs/getting-started/your-first-project.mdx (via pluralLabel (literal, a string literal in mergeObjectDefinitions; a string literal in withoutStaleObjectPlural))
  • content/docs/protocol/kernel/i18n-standard.mdx (via pluralLabel (literal, a string literal in mergeObjectDefinitions; a string literal in withoutStaleObjectPlural))
  • content/docs/protocol/kernel/plugin-spec.mdx (via pluralLabel (literal, a string literal in mergeObjectDefinitions; a string literal in withoutStaleObjectPlural))
  • content/docs/protocol/objectql/schema.mdx (via pluralLabel (literal, a string literal in mergeObjectDefinitions; a string literal in withoutStaleObjectPlural))
  • content/docs/ui/translations.mdx (via pluralLabel (literal, a string literal in mergeObjectDefinitions; a string literal in withoutStaleObjectPlural))
What this run could not see
  • 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 — 34 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 c58008187fb14259cae52461aff9971a1b4b15e9 → packageMentionDocs.

Which tree this was computed on

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

node scripts/docs-audit/affected-docs.mjs --json c58008187fb14259cae52461aff9971a1b4b15e9

⚠️ 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 c58008187fb14259cae52461aff9971a1b4b15e9 → 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

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: a1107d3c65641be8cbc9650ff48eb8466a776741
Local-runs: none
Scope: packages/objectql/src/registry-extension-plural-label.test.ts, packages/objectql/src/registry.ts, packages/rest/src/meta-item-read-gate.ts, packages/rest/src/meta-object-extension-plural-label.test.ts, scripts/engine-double-contract.pinned.json + packages/spec/src/data/object.zod.ts, packages/spec/src/system/i18n-resolver.ts, packages/spec/src/system/translation.zod.ts, docs/adr/0029-kernel-object-ownership-and-platform-objects-decomposition.md, docs/adr/0146-studio-designs-setup-administers-one-editor-per-item-paired-customize-package.md, packages/rest/src/rest-server.ts, packages/rest/src/form-routes.ts, packages/runtime/src/domains/meta.ts, packages/metadata-protocol/src/protocol.ts, packages/metadata-protocol/src/served-override.ts, packages/metadata-protocol/src/protocol-discovery-and-bases.ts, examples/app-showcase/src/data/extensions/account.extension.ts, examples/app-showcase/src/system/translations/index.ts, ui/packages/i18n/src/useObjectLabel.ts, skills/objectstack-data/SKILL.md, scripts/check-adr-0087-registration.mjs, AGENTS.md

Inputs read: card #30391 (body, triage 6655169453, claim 6656144660, os-dev-report 6657882269), PR #30459 body and file list, the net diff against main (merge-base 1013ef11; no commit on origin/main since touches a PR path), and the check-runs on the head. Read-only: nothing built, run or re-run.

Check-runs on the head as they stand: every completed run is success or skipped (Build Core, Build Docs, Check Changeset, Check PR Size, Governed Surface Queue Guard, PR-body guards, Spec property liveness, Lint & Repo Gates, Lint · ESLint, Lint · PM dispatch-gates, Lint · CI, release & engine gates, Lint · docs & code guards, Lint · repo & tooling gates, the three scoped lints, Type Check · consumer / debt ledger / source gates, Test Core (7/8), Test UI, Dogfood Regression Gate, Live E2E UI, Check Documentation Links, Flag docs affected, Auto Label, filter). Still in_progress at write time: Test Core (1/8), (2/8), (3/8), (4/8), (5/8), (6/8), (8/8) and Type Check · workspace. No run has failed. A red among the in-progress runs reopens this record.

① Derived judgments

  • Accept-set: unchanged — right. packages/spec is untouched; ObjectExtensionSchema (object.zod.ts) still accepts label and pluralLabel as independent optional strings, and nothing an author writes is refused or newly accepted. Metadata-protocol: semantic is the right kind: what an extension's label without pluralLabel means to the served object changed.
  • Public surface: no exported symbol moves — right. withoutStaleObjectPlural, pairTranslationBundleOfBase and nonEmptyText are module-private in meta-item-read-gate.ts (no +export in the diff; packages/rest/src/index.ts and packages/objectql/src/index.ts untouched). pairTranslationBundleOf keeps its signature and now delegates its second question to the new private helper; its undefined-on-every-uncertainty rule is preserved.
  • Served-value change 1 — the fold (mergeObjectDefinitions) — right. An applied label (not yielded under ADR-0029 D9.2a) that differs from the running label, with extension.pluralLabel === undefined and no tenant-authored plural, deletes merged.pluralLabel before writing the label. merged is a spread copy, so the base definition and getPackagedObjectBase (the owner contributor's body) keep the owner's plural, which the translators compare against. Idempotent over an already-folded base: the refold reads the folded label as tenant-diverged and yields on label, so the block is skipped and the plural stays absent. Two extenders resolve by priority as the scalars always did (a later rename with no plural drops an earlier extender's plural; a later plural lands over an earlier rename). The rule is generic over every extend contributor (code objectExtensions and .custom items alike), which is right: the spec has one ObjectExtensionSchema and the registry one merge.
  • Served-value change 2 — /meta/object read (withoutStaleObjectPlural) — right. Applied where the served object carries no non-empty plural, the packaged base declares one, and the served label differs from the packaged label. Then the pair bundle of the packaged base's _packageId is read (memoised per base for the list, the same key space pairTranslationBundleOf uses, since both memoise getPairTranslationBundle(basePackageId)), the pair's objects.NAME.pluralLabel is read through the spec's own translateMetadataDocument over the pair bundle alone, and otherwise the catalog's plural is removed (no key when the input had none; the input's own value when it had a key). Every other object returns translated by reference, and the pair is read only when the rule fires. With no protocol or no packaged base the translator's answer stands, matching the gate's existing degradation rule.
  • Both reads, both transports — covered. translateMetaList is the last step of the list chain on RestServer.translateMetaItems and on the runtime dispatcher's translateList (packages/runtime/src/domains/meta.ts); translateMetaDocument is called by RestServer.translateMetaItem and, through translateMetaEnvelope, by the runtime dispatcher's translateEnvelope. The new step sits inside both exported functions, after the view branch, so neither transport can skip it. p and packagedBase are in scope in the PR-head translateMetaDocument.
  • Tenant-authored plural — kept, on both halves. Fold: !yields('pluralLabel') guards the delete, and a tenant-authored label makes the whole rename yield (pinned). Read: a served object carrying a non-empty pluralLabel returns untouched, so a tenant's To-dos survives and translates under the existing D9.2a comparison. A tenant rename that kept the old plural is untouched, as the PR says. The D9.2a honest edge (an overlay that round-tripped the extender's rename with the base plural frozen in) keeps serving that plural; that is the ADR's recorded cost, not this PR's.
  • scripts/engine-double-contract.pinned.json (+5) — right, named as out-of-surface. One ledger row for the new rest pin's findOne double, in the shape and alphabetical slot of its neighbours, written by check-engine-double-contract --write and required by that gate (precedent PR fix(metadata-protocol,rest): a .custom pair's declared override is labelled in the ruled order on every /meta read #30353). Generated output, not reviewed further; it is the gate's own record of the test the diff adds.
  • Tests — pin what the change claims. The rest pin boots a real SchemaRegistry, protocol and RestServer, reads both /meta/object routes in en and zh-CN, measures the reference (crm_note, no plural) on the same boot, and pins the pair's plural first, a pair row that translates only label not reviving the base plural, and the controls (plural set, plural only, neither, repeated label, untouched object). The objectql pin covers the fold's rules, including D9.2a yields and two-extender order.

② Semver level

  • .changeset/30391-extension-rename-drops-stale-plural.md: @objectstack/objectql: minor, @objectstack/rest: minor. AGENTS.md § Post-Task Checklist 3: a protocol kind other than none takes at least minor on a package the diff moves; Metadata-protocol: semantic is such a kind, and both packages move. Level matches.
  • Clause-②: no (narrowing) — the arm fits: no exported member or accepted value moves, and a served value is narrowed (a pluralLabel key stops being served where it was). The arm is BREAKING by rule; the changeset carries the BREAKING banner, the ADR-0087 marker not-required (no-migration-prescription) with its reason, and both declaration lines line-initial. Nothing an author can write is removed or renamed, so no FROM-to-TO migration is owed; the body's "declare pluralLabel beside label" is a remedy for a cosmetic loss, not a rewrite prescription. Check Changeset, Lint · PM dispatch-gates and Lint · CI, release & engine gates are green on the head.
  • The PR body carries the same two lines, identically.

③ Boundary flags

  1. Ledger outside the order's surface — answered above: generated by the gate, shape-checked, precedent recorded. Accepted.
  2. Clause-② corrected from provisional no to no (narrowing) — right; the diff moves an answered shape for every label-only extension, the showcase's showcase_account included.
  3. PR assign/review skipped per the order — procedural; no finding.
  4. Fold rule generic over every extender — right; one schema, one merge (see ①).
  5. open_questions: land a BREAKING served-value narrowing for a p2 label fix (A merge as declared / B add pluralLabel to the showcase's own extension in a follow-up) — escalated to the maintainer, whose word the narrowing requires: this record judges the contract, not the merge. On the design the dev's recommendation holds (A, with B as a small examples follow-up). On B: examples/app-showcase/src/data/extensions/account.extension.ts renames showcase_account to Account (Success Overlay) with no plural, and the showcase's own bundle (examples/app-showcase/src/system/translations/index.ts) translates the base's plural as Accounts / 客户; after this PR the list title reads the label in both locales (the nav entry keeps its own label: 'Accounts'). Should-fix (follow-up, outside the surface): add pluralLabel to that extension.
  6. Skills-docs follow-up flag — confirmed: skills/objectstack-data/SKILL.md line 349 says "an extension can add but never remove", which this PR makes false for the plural scalar. skills/** is outside the dev's surface. Should-fix (follow-up card): amend the sentence with the exception. The same follow-up should carry a one-line D9.2a note in ADR-0029, whose D9.2 rationale states that mergeObjectDefinitions "has no expression for removal at all"; that argument (about fields) still holds, but the sentence is now imprecise for pluralLabel. docs/adr/** is outside the surface; no ADR decision moves.
  7. form-routes.ts not measured (out_of_scope_findings 2 / acceptance note 3) — the dev calls it "outside this change's surface"; it is in packages/rest/src/**, which the order names. The public form's object schema is built as { name, label, fields } and translated through translateMetadataDocument('object', …, { packagedBase }) with no post-step, so for a renamed object the catalog's plural can still be served on that payload. The form payload never carried the served plural and the card is about list titles, so this is not a defect of the card's fix. Should-fix (seat carrier, as the dev flagged): measure it and either apply withoutStaleObjectPlural there or record why the form's plural has no reader.
  8. out_of_scope_findings 1 (an object's served label reads no pair translation; PR fix(metadata-protocol,rest): a .custom pair's declared override is labelled in the ruled order on every /meta read #30353 left objects out of pairBundle) — pre-existing, noted by the seat; not widened here. The plural now reads the pair while the label does not, which the dev records honestly as "two names still" for a pair translating both strings. Carrier: seat.
  9. Served-tier — Clause-②: no (narrowing) on body and changeset, no published schema or governed path in the committed tree: CONTRACT_REVIEW_TIER, narrowing face, as asked.

④ Design judgment

Does the narrowing take a declared capability away at the consumer, or make the spec's own refusal real? Read against main:

  • ObjectExtensionSchema declares label as "Override label for the extended object" and pluralLabel as "Override plural label for the extended object", both optional, and declares nothing about the base's plural under a label-only override. ObjectSchema.pluralLabel is optional, so "an object with no plural" is a shape the spec already admits, and the Console's objectPluralLabel (ui/packages/i18n/src/useObjectLabel.ts) titles such a list by the translated plural, else the declared plural, else the label. No published text promises that a renamed object keeps serving the base's plural; the only thing serving it was last-writer-wins on a key the extension never wrote.
  • ADR-0029 D9.2a: an extender's scalar yields to a tenant-diverged base; the PR applies the same yield to its delete and to the label that triggers it. ADR-0146 D10: an object is extended, never replaced; the base declaration and getPackagedObjectBase are untouched, only the fold's served body changes. Neither ADR declares the capability being narrowed.
  • The spec translator (translateObject, i18n-resolver.ts): catalog ?? authored per scalar, with the catalog yielding only where the served scalar provably diverged from packagedBase, and an absent scalar counting as "no opinion" by the ruling's conservative edges. For a renamed object with no plural that edge serves objects.NAME.pluralLabel, which is the base package's translation of a plural the object no longer carries; the en catalog repeats the source, so en is affected too. The spec's rule is that the deployment catalog translates the base's text; serving it on a scalar the served object does not carry is an invented value, the same shape as the view label the gate already removes (withoutInventedViewLabel, meta read: a view stored with no label is served with label = its qualified name (recheck_gate.default), invented by the read-side translator, so the console view tab shows the machine name #23967). The PR follows that precedent, keeps the spec's lookup intact, and lets the owner's .custom pair rank first under the ruled label order (ruling record 6631250894, cited in the code and in labelCatalogs). So the narrowing corrects a served value the spec does not promise. It takes no declared capability away: an author who wants a plural for the new name declares pluralLabel, which the spec always offered, and a pair can save objects.NAME.pluralLabel.
  • One asymmetry, noted and judged sound: an unrenamed object with no declared plural but a catalog plural entry still receives the catalog's plural; the renamed object does not. That is the point of the rule (the catalog entry translates the old plural), though "served as an object that declares no pluralLabel" in the PR and changeset is strictly "as one that declares no plural and has no catalog plural for the new name". Prose only; no fix owed.

Trying to break it:

  • Fold: compared against the running merged.label, not the owner's, so extender order is honoured; a repeated label renames nothing; a base with no plural makes the delete a no-op; the refold yields (idempotence pinned). A lower-priority extender's plural-only contribution is dropped by a higher-priority rename without a plural; consistent with "the plural is the plural of the running label" and pinned.
  • Read: the input to the comparison is the pre-translation served document, so translations never confuse the label test; packaged._packageId comes from the owner contributor (pinned); the memo key space is basePackageId in both helpers, so no collision; the pair lookup runs along the i18n fallback chain as labelCatalogs does for every pair string; with pluralLabel: '' on the input the served value becomes '' where the catalog's plural was served before, a degenerate authoring no scenario reaches. With the protocol unreachable the old answer stands, by the gate's stated rule.
  • A tenant overlay that renamed the label and removed the plural key is now served with none where the catalog's base plural was served; the tenant authored the absence, so this is the reference shape, not a lost value.

No must-fix. Should-fix: items 5, 6 and 7 of ③ (showcase extension plural; Skills sentence plus ADR-0029 D9.2a note; form-routes.ts measured and aligned or recorded), each a follow-up or seat carrier, none a defect in the diff's own claims.

Implemented-by: claude/project-thread-ktw7m9
Reviewed-by: session_01Utp6CYVV6fcAQrNTDQRvPb

VERDICT: PASS

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:server#5 · os-bill · session_01Utp6CYVV6fcAQrNTDQRvPb

席位核对 · ACCEPT — held for the maintainer's word (BREAKING)

Head a1107d3c6 · 2026-11-05T12:37Z

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:server#5 · os-bill · session_01Utp6CYVV6fcAQrNTDQRvPb

维护者速读

  • 改了什么:某个包的扩展只改了对象的 label、没写 pluralLabel 时,对象不再返回基础包的旧复数,也不再返回旧复数的译文。如果改名的配对包里有复数译文,就返回那份译文;没有就不返回复数,界面回落到新单数名。租户自己写的复数照旧保留。
  • 为什么改:改了名的对象在侧栏和列表标题里仍显示旧复数,一个对象出现两个名字;在中文下,旧复数还会被译成旧词。
  • 风险与回滚:这是一次已返回值的收窄(BREAKING),所有只改 label 的扩展都会少返回 pluralLabel。showcase 自带的 showcase_account 就在其中,它的列表标题会变成单数名,补复数的跟进卡是 showcase: the showcase_account extension renames the object without a pluralLabel, so its list title loses Accounts / 客户 once PR #30459 lands #30465。不涉及数据迁移,回滚只需撤掉本 PR。
  • 先看:packages/objectql/src/registry.ts 里的 mergeObjectDefinitions 标量覆盖段;packages/rest/src/meta-item-read-gate.ts 里的 withoutStaleObjectPlural,以及它在列表读和单条读上的两处调用。
  • 两分钟验证:运行 pnpm --filter @objectstack/rest exec vitest run src/meta-object-extension-plural-label.test.ts,预期 11 条全绿;在修复前的代码上有 4 条是红的。
  • 需要你做的:无,席位落地。

席位意见:同意落地。这次收窄改的是一个 spec 并未承诺的返回值,没有拿掉任何已声明的能力。达档合同复核 PASS(6658126879),钉住 head a1107d3c6。路径不触受治理面,规模 +536/−8,没有超过人工合并门槛。

席位核对

Head a1107d3c65641be8cbc9650ff48eb8466a776741 · 2026-11-05T13:49Z

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review November 5, 2026 21:48
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Nov 5, 2026
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 Fix lands in tooling, not a product package — enters pm:queue only with Unblocks: or a named surface

Projects

None yet

Development

Successfully merging this pull request may close these issues.

A .custom object extension that renames only label keeps the base package's translated pluralLabel, so every list title keeps the old name

1 participant