Skip to content

fix(i18n): a tenant's rewrite of an extension-contributed validation rule message is served as written, on the refusal and on /meta - #27932

Merged
objectstack-fleet[bot] merged 3 commits into
mainfrom
claude/project-thread-2irh6q
Oct 30, 2026
Merged

objectstack-fleet[bot] merged 3 commits into
mainfrom
claude/project-thread-2irh6q

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Requested by Elon · project thread

domain:server#4 · os-elon-musk · session_016WV4cytRisTLYYDef35A9G

Fixes #27559

Clause-②: yes (widening) — SchemaRegistry (@objectstack/objectql) gains the public read getPackagedObjectBase(name)
Metadata-protocol: semantic

维护者速读

  • 改了什么:租户在 overlay 里改写了“扩展包”(objectExtensions)带来的校验规则提示语后,非默认语言下拒收报错和 /meta 仍回扩展包译文;现在两处都拿“owner + 代码扩展包”这一套打包规则做比较,改写的照原样显示,没改的照常翻译。
  • 为什么改:fix(i18n): a tenant-rewritten validation rule message beats the packaged translation, on the refusal and on /meta #27543 只拿 owner 的声明比较,扩展包的规则无从比较,租户为自己用户改的话一换语言就变回扩展包译文;折叠里还会出现两条同名规则,拒收同时报两句。
  • 风险与回滚:只扩 validations 一项(label/description 仍只认 owner,Object-extension fold leaks the extension's label onto ?layers=true only — the two reads #7556 reconciled on FIELDS now disagree on LABEL #8037 不受影响);折叠里 overlay 带的同名规则顶替扩展包原规则(仅当 owner 未声明该名),owner 与扩展包同名规则仍并存。回滚即 revert 本 PR。
  • 先看:packages/objectql/src/registry.ts(getPackagedObjectBase、tenantCarriedRuleNames)、packages/objectql/src/engine-validation-messages.ts(packagedRuleLookup)、packages/metadata-protocol/src/protocol.ts(getPackagedObjectBase)。
  • 两分钟验证:pnpm --filter @objectstack/objectql exec vitest run src/engine-validation-tenant-rewrite.test.ts src/protocol-packaged-object-base.test.ts,预期 16 passed:zh-CN/zh/en 下 /meta 与拒收是同一句,扩展规则改写为租户原句、只出现一次。
  • 需要你做的:无,席位落地(欠一次契约复审,席位安排)。

What changes

ADR-0029 D9.2a compares a served rule message with the packaged rule of the same name. Since #27543 the packaged side was the OWNER's declaration only, so a rule a second package contributes through objectExtensions had nothing to compare with, and the catalog won over a tenant's rewrite on both readers.

  • One packaged rule set, both readers. SchemaRegistry.getPackagedObjectBase(name) (new, beside getPackagedObjectOwner) answers the packaged owner's declaration with each CODE extend contributor's validations appended in fold order. By reference when no code extender ships a rule; undefined exactly where getPackagedObjectOwner is. ObjectStackProtocolImplementation.getPackagedObjectBase (what /meta hands translateObject as packagedBase) and packagedRuleLookup (what the rule evaluator judges the refusal by) both read it; a registry double without it is still read through getPackagedObjectOwner, the owner alone. The comparison stays valueOverridesPackagedBase / scalarOverridesPackagedBase, unchanged.
  • ⛔ The object scalars' base is not widened. Only validations gains the extenders' entries; label / pluralLabel / description stay the owner's, so an extension's label still reads as diverged and keeps winning over the owner's catalog (Object-extension fold leaks the extension's label onto ?layers=true only — the two reads #7556 reconciled on FIELDS now disagree on LABEL #8037). Pinned.
  • The fold (design choice the card left open). An extender's top-level rule yields to a rule of the same name that the fold's BASE carries and the own contributor does NOT declare (tenantCarriedRuleNames, computed once over the base, the rule-name mirror of tenantAuthoredScalars). On an overlay base those are the tenant's copies of extender rules (a Studio GET → edit → PUT stores the folded object), so the rule is served and evaluated once, as the tenant last wrote it. Conservative edges, as for the scalars: no own contributor, or a base that IS the owner (every no-overlay read and resolveOwnerLayer), returns undefined and the fold concatenates exactly as before; a name the owner declares itself is not counted, so an owner rule and an extender rule sharing a name stay two rules (pinned). Composes with A tenant object overlay drops every objectExtensions field from the resolved schema, and effective contradicts code in the same response #8027's subtractExtenderContributions (that runs first, on foldObjectExtendersOnto).
  • Prose (seat-granted surface amendment, prose only): packages/spec/src/system/i18n-resolver.ts docblocks of TranslateDocumentOptions.packagedBase and translateObjectValidations; content/docs/ui/translations.mdx, the validation-rule paragraph of "An edit beats the packaged catalog". No predicate change in packages/spec.
  • ⛔ packages/rest/src/meta-item-read-gate.ts is not edited (held by fix(rest)!: both import doors answer for the object as the data door does (#27728, #27729) #27897): its packagedObjectBaseOf already calls the protocol's getPackagedObjectBase, which now carries the extenders' rules.

Why

A tenant who rewords a rule an add-on package ships, for their own users, saw it revert to the add-on's translation whenever a user's locale differed from the default, and the refusal also carried the add-on's original rule beside the tenant's.

Verification

Card probe re-run on main 784dc9ae (real ObjectQL engine + SchemaRegistry; owner px_duty with no rules, an extend contributor adding ext_blocked "Say what is blocking the duty.", a zh-CN bundle entry, a tenant overlay; not committed). Locale zh-CN:

Case Reading Before After
no overlay fold / /meta / refusal 1 rule / 请写明阻塞原因。 / 请写明阻塞原因。 unchanged
overlay rewords ext_blocked to "Tell us what blocks this duty." fold 2 rules (rewrite + appended original) 1 rule (the rewrite)
/meta 请写明阻塞原因。, 请写明阻塞原因。 Tell us what blocks this duty.
refusal 请写明阻塞原因。 | 请写明阻塞原因。 Tell us what blocks this duty.
overlay carries ext_blocked untouched fold / /meta / refusal 2 rules / translated twice / translated twice 1 rule / 请写明阻塞原因。 / 请写明阻塞原因。

Pins (red first on main, 6 red; all green after):

Ablations (each restored, suite green after):

  • A1 getPackagedObjectBase answers the owner only → 4 red (both readers).
  • A2 fold yield off → 3 red (fold count, both readers).
  • A3 packagedRuleLookup reads the owner only → 1 red (refusal reader alone).

Suites: @objectstack/objectql full vitest 492 files / 9264 tests pass; @objectstack/rest i18n override files (4 files / 109 tests) pass against the rebuilt dists; objectql + metadata-protocol typecheck pass. dispatch-gates --commands (107 families, derived on a tree identical to head 5697abaa) and pnpm lint: see the os-dev report comment on #27559.

Human Minimum

  • 1. Schemas start as Zod; packages/spec holds no runtime logic.
  • 2. Every name I added uses the casing for its kind.
  • 3. No formatter ran; pnpm lint passes.
  • 4. One issue, one branch off an up-to-date main, one pull request.
  • 5. A change that publishes carries a changeset, and both declaration lines are filled in here and in the changeset.
  • 6. packages/spec touched: its generated artifacts are regenerated and committed (docblock-only on a non-schema interface; check:docs / check:api-surface green).
  • 7. No hand edits to content/docs/references/, packages/*/CHANGELOG.md or content/docs/releases/.
  • 8. Title, body and comments are in English.
  • The Before Submitting list in CONTRIBUTING.md is done.

🤖 Generated with Claude Code

https://claude.ai/code/session_016WV4cytRisTLYYDef35A9G

…rule is served as written, on the refusal and on /meta

ADR-0029 D9.2a compared a served rule message with the packaged OWNER's
declaration only, so a rule a second package contributes through
objectExtensions had nothing to compare with and the extension's
translation won over a tenant's rewrite on both readers.

- SchemaRegistry.getPackagedObjectBase: the packaged owner's declaration
  with each code extender's validations appended (scalars stay the
  owner's, #8037). The protocol's getPackagedObjectBase (/meta) and
  packagedRuleLookup (refusal) both read it.
- The fold: an extender's rule yields to an overlay rule of the same
  name the owner does not declare, so the rule is served once.
- Prose: i18n-resolver docblocks and content/docs/ui/translations.mdx.

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

github-actions Bot commented Oct 30, 2026 •

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 3 package(s): @objectstack/metadata-protocol, @objectstack/objectql, @objectstack/spec, touching 5 documentable anchor(s).

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

  • content/docs/concepts/metadata-lifecycle.mdx (via ObjectStackProtocolImplementation (symbol, a top-level class), SchemaRegistry (symbol, a top-level class))
  • content/docs/deployment/environment-variables.mdx (via SchemaRegistry (symbol, a top-level class))
  • content/docs/kernel/services-checklist.mdx (via SchemaRegistry (symbol, a top-level class))
  • content/docs/plugins/adding-a-metadata-type.mdx (via SchemaRegistry (symbol, a top-level class))
  • content/docs/protocol/kernel/plugin-spec.mdx (via SchemaRegistry (symbol, a top-level class))

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

  • 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-4.mdx (via SchemaRegistry (symbol, a top-level class))

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
  • the SDK route bridge reached 58 of 210 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 — 171 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 bacb3e6a0ff6e8bfdeade49d6830e34fb5522589 → packageMentionDocs.

Which tree this was computed on

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

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

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

…dy carries is served once

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

Copy link
Copy Markdown
Contributor Author

domain:server#4 · os-elon-musk · session_016WV4d1Np3hU7RQnA13YZcL

Contract-review-claim: session_016WV4d1Np3hU7RQnA13YZcL · head 029633f · 2026-10-30T04:30Z

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:server#4 · os-elon-musk · session_016WV4d1Np3hU7RQnA13YZcL

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 029633fbf886f52f38225f91ac2cbf226c0dd183
Local-runs: none
Scope: packages/spec/src/system/i18n-resolver.ts + packages/objectql/src/registry.ts (getPackagedObjectBase, tenantCarriedRuleNames, withoutRulesNamed, foldExtendersOntoDefinition, mergeObjectDefinitions, subtractExtenderContributions, tenantAuthoredScalars, foldObjectExtendersOnto, resolveOwnerLayer, getPackagedObjectOwner, registerObject), packages/metadata-protocol/src/protocol.ts (getPackagedObjectBase), packages/objectql/src/engine-validation-messages.ts (packagedRuleLookup, validationMessageContext), packages/objectql/src/validation/rule-validator.ts (authoredRuleMessage), packages/objectql/src/validation/record-validator.ts (ValidationMessageContext), packages/spec/src/system/i18n-resolver.ts (translateObjectValidations, packagedRulesByName, valueOverridesPackagedBase), packages/rest/src/meta-item-read-gate.ts (packagedObjectBaseOf, PACKAGED_BASE_ACCESSORS), packages/rest/src/form-routes.ts (packagedBase call sites), packages/metadata-core/src/code-artifact-provenance.ts (isCodeArtifactBody), packages/spec/src/data/validation.zod.ts (BASE_VALIDATION_SHAPE, ConditionalValidationSchema), packages/objectql/src/engine.ts (registerObject(…, 'extend', priority) call, _registry), packages/objectql/src/index.ts (SchemaRegistry export), docs/adr/0029 D9.2 / D9.2a, docs/adr/0126 regime E, .changeset/27559-extension-rule-tenant-rewrite-wins.md, .claude/skills/pm-dispatch/references/review-checklist.md, scripts/check-changeset-no-major.mjs

Written 2026-10-30T04:46Z. Read-only: card #27559 (body and all four comments: triage 6497467466, park note 6498065104, claim 6514064935 with its amendments, dev report 6514910503), PR #27932 body and file list, git diff origin/main...029633fb (merge-base 88d6020; origin/main has moved 17 commits since, touching one cited file, packages/rest/src/meta-item-read-gate.ts, in loadObjectItem only — packagedObjectBaseOf is byte-identical, so no conflict with the PR's claim about that file), merged PR #27543 and its contract review 6496586363, the head's check-runs. Dispatched by the seat; the seat's own conclusions were not an input. PR author is objectstack-fleet[bot] (relay write): no off-relay note owed.

Head and check-runs. Two commits on the head: 5697aba (the fix) and 029633f (prose, content/docs/data-modeling/object-extensions.mdx, covered by the claim amendment). Check-runs on 029633f at the time of writing: 48 runs, 0 failed; 29 success — among them Lint & Repo Gates, Check Changeset, Governed Surface Queue Guard, Lint · PM dispatch-gates, Type Check · source gates, Type Check · consumer gates, Spec property liveness, Test UI, Test Core (8/8); 9 skipped (opt-in and scheduled lanes); 10 still in progress — Build Core, Test Core (1/8) through (7/8), Type Check UI, Type Check · workspace. Pending is pending: landing condition ② (every check green) is not yet readable on this head.

① Derived judgments

  1. SchemaRegistry.getPackagedObjectBase(name) is a public-surface widening of @objectstack/objectql — Clause-②: yes (widening) is RIGHT. SchemaRegistry is exported from the package root (packages/objectql/src/index.ts:38); a new public method on it is reachable from the entry type graph. It cannot stay internal: @objectstack/metadata-protocol reads it through engine.registry (duck-typed, protocol.ts getPackagedObjectBase), and the alternative — composing owner + extenders' rules once in the protocol and once in engine-validation-messages.ts — is two copies of the composition, i.e. the very split spec(i18n): translateObject does not translate validations[].message, so a client reading /meta shows a validation rule's authored message while the server's refusal of the same write is translated #27135 closed. Public, beside the public getPackagedObjectOwner and foldObjectExtendersOnto (both public for the same cross-package reason), is the right surface. metadata-protocol's own getPackagedObjectBase keeps its signature (unknown); its ANSWER changes — see ①.3.
  2. One packaged rule set, both readers — RIGHT, verified on every path. /meta list and by-name reads both go through packagedObjectBaseOf(p, 'object', name) → PACKAGED_BASE_ACCESSORS → protocol getPackagedObjectBase → registry.getPackagedObjectBase(name); the public form (form-routes.ts:319) goes through RestServer.packagedObjectBase, the same accessor; the refusal goes validationMessageContext → packagedRuleLookup(this.registry, objectName) → registry.getPackagedObjectBase(objectName) on the same SchemaRegistry instance (ObjectQL._registry, read through the registry getter the protocol also reaches). Both resolve the name through resolveObjectKey, both index rules by name with conditional branches under their own names to depth 10, first name wins (packagedRulesByName / packagedRuleLookup, unchanged). Both fall back to getPackagedObjectOwner(name)?.definition ONLY on a registry double that lacks the new method, in the same order (base first, owner second, undefined third), so a double cannot make them disagree either. No split found.
  3. Accept-set change on the metadata protocol: semantic — RIGHT, and it names TWO meaning changes, both real. (a) An authored overlay validations[].message that rewords a rule a second package ships through objectExtensions now wins over that package's catalog entry in every locale (before: the catalog won). (b) An overlay rule whose name an extension ships and the owner does not declare now REPLACES the extension's rule in the fold instead of running beside it. No key is added or refused. semantic is the right kind; (b) is the one that is not yet recorded where it must be — ④ must-fix 1.
  4. The object scalars' base is NOT widened — RIGHT (Object-extension fold leaks the extension's label onto ?layers=true only — the two reads #7556 reconciled on FIELDS now disagree on LABEL #8037). getPackagedObjectBase spreads owner.definition and replaces validations only; label / pluralLabel / description / fields stay the owner's, so an extension's label still reads as diverged and keeps winning over the owner's catalog. Pinned (protocol-packaged-object-base.test.ts: base.label is the owner's 'Ignored' while the served label is the extension's).
  5. By-reference edge — RIGHT. When no CODE extender ships a rule the owner's definition is returned by identity (pinned), so every object without rule-shipping extensions pays nothing and getPackagedObjectBase is undefined exactly where getPackagedObjectOwner is (runtime-authored owner, unknown name, partial double).
  6. Code extenders only (isCodeArtifactBody) — RIGHT. The packaged set is what code shipped; a tenant-authored body can never be a packaged baseline. Every extend registration the engine makes carries its package id (engine.ts:8702, stampPackage(extDef, id, version) then applyProtection), so the shipped extensions are all code artifacts; a bare registerObject(def, undefined, undefined, 'extend') fixture is left out of the packaged set, correctly.
  7. No regenerated artifact under packages/spec moves — RIGHT. The file list carries no api-surface/**, export-origins/** or JSON schema; the i18n-resolver.ts change is two docblocks on the ./system entry (one on the published TranslateDocumentOptions.packagedBase, one on the private translateObjectValidations), no type or predicate moves. check:api-surface records names only, so — exactly as 6496586363 ①.1 said — the carriers are the only record of what moves here, which is why ② below is strict about them.
  8. Prose files are the lane seat's ACCEPT at TIER_DEFAULT (.changeset/*, content/docs/ui/translations.mdx, content/docs/data-modeling/object-extensions.mdx) and the two test files. Not reviewed here; read only where a ① or ② judgment needed them.

② Semver level

  • Changeset: '@objectstack/objectql': minor, '@objectstack/metadata-protocol': minor — both RIGHT. Kind semantic ≠ none; both packages move src/** (AGENTS.md Post-Task Checklist 3: a kind takes at least minor on a package the diff moves). The protocol package's answer to getPackagedObjectBase changes meaning, so minor there is not a courtesy.
  • @objectstack/spec is ABSENT from the changeset frontmatter although the diff moves packages/spec/src/system/i18n-resolver.ts — WRONG. Must-fix. @objectstack/spec is published (private unset, version 18.2.0); the touched file is the ./system entry, and the TranslateDocumentOptions.packagedBase docblock ships in the published .d.ts. review-checklist.md 〈范围与 changeset〉: 「每个被触及的包,private: false 即已发布 ⇒ 读 package.json 核 changeset 在不在 … ⛔ 缺了不入队」 — and every packages/spec/src commit on origin/main in the last ten days (21 of 21 read) carries spec in its changeset. The claim's "prose-only" label on this file is a surface grant, not a publishing judgment: under contract-review.md the prose faces are .changeset, CHANGELOG, content/docs/**, apps/docs/** — packages/spec/src/** is the schema face whatever the hunk contains. Right spelling: add '@objectstack/spec' to the frontmatter. Level: minor by Post-Task Checklist 3's letter (kind ≠ none, package moved); check-changeset-no-major.mjs would also accept patch under its discharged verdict, and whether a docblock-only move earns the ratchet's minor is the seat's call at ACCEPT — ABSENT is neither. Prose-only correction; delta-reviewed by this reviewer.
  • Clause-②: line: yes (widening) with trailing em-dash reasoning on both carriers (PR body, changeset) — RIGHT (①.1; clause2-line.mjs reads the arm and keeps trailing reasoning). No (narrowing) arm, so no BREAKING banner, no ADR-0087 disposition, no dogfood battery is owed.
  • Skills-docs: line ABSENT from the PR body — should-fix before ACCEPT. Kind ≠ none makes it required (review-checklist.md: 「种类非 none …:Skills-docs: 行在且属实 … 缺 ⇒ REWORK」). The content exists on this head (translations.mdx validation-rule paragraph, object-extensions.mdx callout sentence), so the line reads updated here — content/docs/ui/translations.mdx, content/docs/data-modeling/object-extensions.mdx; whether updated here also requires the package's evals is the seat's call, as in 6496586363 ②.

③ Boundary flags

Report 6514910503: open_questions: []. Each deviations entry and the out_of_scope_findings entry:

  1. Declarations moved from the claim's provisional no / none to yes (widening) / semantic, seat amended the claim — RIGHT (①.1, ①.3). The claim thread carries the amendment (6514064935), so the triage record does not stand on the wrong reading.
  2. Prose-only edits to i18n-resolver.ts and translations.mdx under the seat-granted surface amendment — the edits are accurate (the new docblock sentences match the code: the serving layer appends each code extender's rules, scalars stay the owner's), but "prose-only" does not take i18n-resolver.ts out of the published package — see ② must-fix. The grant covered the file surface; it could not waive the changeset.
  3. The fix(i18n): a tenant-rewritten validation rule message beats the packaged translation, on the refusal and on /meta #27543 pin "the overlay is what both readers see" now expects the packaged base to list the extension rule; the refusal helper joins every violation — RIGHT. The amended assertion is the behaviour this card changes, and joining every violation is what makes a duplicated rule visible to the pin (the old fields[0] read would have passed on a fold that served the rule twice).
  4. packages/rest/src/meta-item-read-gate.ts untouched (held by fix(rest)!: both import doors answer for the object as the data door does (#27728, #27729) #27897) — RIGHT, and still right on today's origin/main. packagedObjectBaseOf already dispatches 'object' to the protocol's getPackagedObjectBase; the file moved on main since the merge-base (rest: pin the async import job door's disabled-package refusal, and decide whether it should resolve a legacy ns__short name as the data door does #27729, loadObjectItem) without touching that function.
  5. No local test:repo run — ANSWERED by CI, partially: Test Core (8/8) is success, shards 1–7 are in progress at the time of writing. Pending is pending; the landing read is on the check-runs.
  6. out_of_scope_findings class c: resolveObject with an overlay carrying a BYTE-EQUAL copy of an extender rule served the rule twice before this PR (A tenant object overlay drops every objectExtensions field from the resolved schema, and effective contradicts code in the same response #8027's subtraction runs only in foldObjectExtendersOnto); collapsed to one by this PR's yield, pinned, not filed — ACCEPTED as not owed a card. Fixed and pinned on this head ("overlay carries the extension rule untouched → served once, translated on both readers"), so Prime Directive 10's filing gate (a reproducible defect left open) is not met. Note that the collapse rides on the fold yield, which ④ must-fix 1 governs: if the yield is split off, this finding reopens and then IS a card.
  7. The fold design "the card left open" — the dev answered it in code; the ADR that governs the fold was not amended. Escalated as ④ must-fix 1. The card can delegate a design to the implementer; it cannot delegate past ADR-0029.

④ Design judgment

packages/spec/src/system/i18n-resolver.ts (the hits file), judged against the code it cites and the accepted scenario (card #27559 "Expected": one comparison, both readers; a tenant rewrite of an extension rule served as written; an untouched extension rule still translated; the scalars' base not widened):

  • The two docblocks now describe what the live tree does: TranslateDocumentOptions.packagedBase for an object carries the owner's declaration with each code extender's validations appended, scalars the owner's (registry.ts getPackagedObjectBase); translateObjectValidations judges an extension rule against the extension package's message because that rule is now in packaged (packagedRulesByName(opts?.packagedBase)). No predicate or walk changed in this file; valueOverridesPackagedBase's three conservative edges still carry the extension case: absent base → catalog; equal message → catalog (untouched extension rule translated); diverged → served raw in every locale.
  • Accepted scenario holds on both readers, pinned end to end on one real SchemaRegistry + ObjectQL (engine-validation-tenant-rewrite.test.ts): an overlay rewording ext_blocked_needs_reason is served and refused as the tenant's sentence on zh-CN / zh / en, folded once; an overlay carrying it untouched is folded once and translated on both readers; the fix(i18n): a tenant-rewritten validation rule message beats the packaged translation, on the refusal and on /meta #27543 owner-rule rows are unchanged (returned_needs_note rewrite served raw, done_needs_owner translated); the no-overlay control is unchanged. Consistent with the three ablations the PR body reports (A1 both readers, A2 fold count, A3 refusal alone).
  • Owner-rule behaviour from fix(i18n): a tenant-rewritten validation rule message beats the packaged translation, on the refusal and on /meta #27543 is unchanged by construction: tenantCarriedRuleNames never counts a name the owner declares, and withoutRulesNamed filters extenders only; an owner's rule can only ever come from the base, as before.

packages/objectql/src/registry.ts — the fold and the new public read, judged against ADR-0029 D9.2 / D9.2a, mergeObjectDefinitions, subtractExtenderContributions, tenantAuthoredScalars and the shipped call sites:

  • Mechanics are sound. tenantCarriedRuleNames is computed ONCE over the fold's base (D9.2a's "never re-derived from the running merge"), declines when there is no own contributor (orphan overlay, D9.5) or the base IS the owner by identity (every no-overlay resolveObject and every resolveOwnerLayer, so ?layers=true's code layer is byte-for-byte what it was), and counts only top-level names the owner does not declare. withoutRulesNamed returns the extension by reference when nothing matches, so mergeObjectDefinitions sees the same body it saw before on every object the rule does not touch. Idempotence (A tenant object overlay drops every objectExtensions field from the resolved schema, and effective contradicts code in the same response #8027) holds on both shipped foldObjectExtendersOnto call sites: a byte-equal copy is subtracted first and re-added once; a reworded copy is kept and the extender's original yields — a second fold of either answer is a no-op (pinned through the overlay rows; meta-object-overlay-extension-fold.test.ts and meta-object-extension-property-classes.test.ts on main pin ['owner_rule', 'ext_rule'] with distinct names, so neither relied on the duplicate; no other test or package found that did). A conditional wrapper carries a required name (BASE_VALIDATION_SHAPE), so the yield addresses it by wrapper name while both readers address its branches by branch name — consistent: an overlay copy of an extension's conditional with a reworded branch yields the whole extender wrapper, and the branch is judged diverged against the extension's packaged branch.
  • Owner + extender same-name rules stay two (pinned) — sound as a conservative edge, with one pre-existing consequence worth naming: both readers' indexes are first-name-wins, so the extender's served copy of a name the owner also ships is judged against the OWNER's packaged message, reads as diverged, and is served untranslated in every locale. Unchanged since fix(i18n): a tenant-rewritten validation rule message beats the packaged translation, on the refusal and on /meta #27543 (the owner's declaration was the whole packaged set then), not a regression of this head; recorded so the seat can file it if it wants the edge closed.
  • Must-fix 1 — the fold yield reverses a recorded ADR-0029 decision without amending the ADR (Prime Directive 13). ADR-0029 D9.2a reads: 「D9.2 above says the fold runs "exactly as it does today", and for fields, validations and indexes it still does. For the three scalars … it no longer does, and this clause is the difference.」 — the ADR's own precedent is that a change to how the fold treats a property class is an AMENDMENT clause, and it binds until a superseding clause says otherwise. ADR-0126's regime E row records the same property: object extensions are 「Package-grain, additive, upgrade-safe」. This head makes validations NOT additive whenever the overlay carries the rule's name, and it inherits D9.2a's "honest edge" for an ENFORCED artifact rather than a label: a tenant who round-trips an extended object in Studio without editing anything freezes the extension's rule into the overlay row, and a later package change to that rule (a tightened condition, a new branch) no longer reaches that tenant — before this head the extension's current rule still ran beside the stale copy. That may well be the right ruling (it is the "tenant wins" sentence of D9.2a applied to rules, and it is what makes the refusal show one sentence), but it is a decision in docs/adr/-governed territory, declared semantic, and docs/adr/0029 is not in the file list: the PR body, the changeset and object-extensions.mdx record it, the ADR says the opposite. Two conforming shapes: (a) carry an ADR-0029 amendment clause on this head (D9.2a's validations sentence corrected, the rule-name yield and its accepted cost recorded, the anchor in scripts/adr-anchors/packages__objectql__src__registry.ts.json extended) — which puts a Tier H path in the file list, so the PR then lands on an authorized approval, not on this record; or (b) split the fold yield (tenantCarriedRuleNames, withoutRulesNamed, the foldExtendersOntoDefinition line, their pins) to its own card with that amendment, and land here only the one-packaged-rule-set half — the card's "Expected" on /meta and on the refusal's rewrite sentence is met by that half alone; the duplicate the fold still serves is then that card's symptom. The seat picks; this reviewer re-reviews either head.
  • getPackagedObjectBase iterates objectContributors.get(fqn) — the same priority-sorted list the fold iterates — so "in fold order" is literally true, and isCodeArtifactBody on each extender is the D9.6 test the owner is already held to. No defect.

packages/metadata-protocol/src/protocol.ts — getPackagedObjectBase now prefers registry.getPackagedObjectBase and keeps the owner read as the fallback for a registry double; the try still swallows a throwing read into undefined (a served schema never becomes a 5xx). The docblock's new paragraph is accurate against registry.ts. The unknown return type is unchanged, so no published type of this package moves; the answer's meaning does (①.3) and the changeset grades it minor. No defect.

packages/objectql/src/engine-validation-messages.ts — packagedRuleLookup reads the same registry method in the same fallback order as the protocol; the lazy by-name index, depth bound and branch walk are unchanged and identical to packagedRulesByName. ValidationMessageHost['registry'] is ObjectQL['registry'] (SchemaRegistry), so the typeof … === 'function' probe is for doubles only and type-checks against the class. No defect.

Findings:

  • Must-fix 1 (④, registry.ts): the validations fold yield contradicts ADR-0029 D9.2a's recorded sentence (and ADR-0126 regime E's "additive") with no amendment — record it in ADR-0029 on this head (Tier H landing) or split the yield off with its amendment.
  • Must-fix 2 (②, .changeset/27559-extension-rule-tenant-rewrite-wins.md): '@objectstack/spec' missing from the frontmatter while the diff moves packages/spec/src/system/i18n-resolver.ts; prose-only correction, delta-reviewed.
  • Should-fix (② / before ACCEPT): Skills-docs: line on the PR body.
  • Should-fix (lane, optional card): owner + extender same-name rule — the extender's served copy is judged against the owner's packaged message and served untranslated (pre-existing since fix(i18n): a tenant-rewritten validation rule message beats the packaged translation, on the refusal and on /meta #27543; first-name-wins in both indexes).
  • Before ACCEPT: landing condition ② read on the landing head once Build Core, Test Core (1–7/8), Type Check UI and Type Check · workspace complete.

Implemented-by: claude/project-thread-2irh6q
Reviewed-by: session_016WV4d1Np3hU7RQnA13YZcL

VERDICT: FAIL


Generated by Claude Code

… yield off

Per contract review 6515204227: the fold yield reversed ADR-0029 D9.2a's
additive validations sentence without an amendment, so it leaves this
change. The one packaged rule set (getPackagedObjectBase, both readers)
stays. Pins restated for the additive fold; '@objectstack/spec' patch added
to the changeset for the i18n-resolver docblocks.

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

Copy link
Copy Markdown
Contributor Author

domain:server#4 · os-elon-musk · session_016WV4d1Np3hU7RQnA13YZcL

Contract-review-claim: session_016WV4d1Np3hU7RQnA13YZcL · head c8c5460 · 2026-10-30T05:26Z

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:server#4 · os-elon-musk · session_016WV4d1Np3hU7RQnA13YZcL

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: c8c5460d6b86e61fba18230464ef041449c793a6
Local-runs: none
Scope: packages/spec/src/system/i18n-resolver.ts + packages/objectql/src/registry.ts (getPackagedObjectBase, foldExtendersOntoDefinition, mergeObjectDefinitions, subtractExtenderContributions, tenantAuthoredScalars, getPackagedObjectOwner), packages/metadata-protocol/src/protocol.ts (getPackagedObjectBase), packages/objectql/src/engine-validation-messages.ts (packagedRuleLookup, validationMessageContext), packages/objectql/src/validation/rule-validator.ts (authoredRuleMessage), packages/spec/src/system/i18n-resolver.ts (translateObjectValidations, packagedRulesByName, valueOverridesPackagedBase), packages/rest/src/meta-item-read-gate.ts (packagedObjectBaseOf), packages/rest/src/form-routes.ts (packagedBase call sites), packages/metadata-core/src/code-artifact-provenance.ts (isCodeArtifactBody), docs/adr/0029 D9.2 / D9.2a, docs/adr/0126 regime E, .changeset/27559-extension-rule-tenant-rewrite-wins.md, content/docs/ui/translations.mdx (read for ②/③ only), scripts/check-changeset-no-major.mjs

Delta review of record 6515204227 (FAIL at 029633f), by the same reviewer. Written 2026-10-30T05:30Z. Read-only: the one commit on top of 029633f (git diff 029633fb c8c5460d: 6 files, +44/−116 — .changeset/*, object-extensions.mdx, translations.mdx, the two test files, registry.ts), the net diff git diff origin/main...c8c5460d (8 files, +230/−29; origin/main at bacb3e6), the dev's round-2 report 6515694156 on #27559, card #27949, the PR body and file list, the head's check-runs. The seat's own posts on the PR (two Contract-review-claim: lines) were read for their shape only; neither is a record. PR author is objectstack-fleet[bot]: no off-relay note owed.

Head and check-runs. 029633f finished 41 success / 9 skipped / 0 failed after the first record. On c8c5460 at the time of writing: 42 runs, 0 failed; 13 success — Check Changeset, Governed Surface Queue Guard, Lint · PM dispatch-gates, Lint · scoped · tooling self-tests, PR-body guards, Spec property liveness, Dogfood Regression Gate, Plan Test Core shards among them; 7 skipped; 22 in progress — every Lint · lane but two, every Type Check · lane, Build Core, Build Docs, Test Core (1–8/8), Live E2E UI. Pending is pending: landing condition ② is not yet readable on this head; Lint & Repo Gates and TypeScript Type Check are the two jobs the seat reads before any landing act.

① Derived judgments

  1. Must-fix 1 CLEARED — the fold is main's again, byte-identical. tenantCarriedRuleNames, withoutRulesNamed, topLevelRuleNames and the one line in foldExtendersOntoDefinition are gone (git grep over packages/ and content/ at this head finds no remnant); the net registry.ts diff is ONE additive hunk, getPackagedObjectBase (+38, −0). mergeObjectDefinitions concatenates validations exactly as ADR-0029 D9.2a records; object-extensions.mdx is back to main (empty diff); translations.mdx drops the "takes the place of" clause; the changeset's second bullet now says the fold is unchanged and cites D9.2a. The decision is filed as card objectql: a tenant overlay copy of an extension's validation rule runs BESIDE the extension's original in the fold — should the overlay replace it (ADR-0029 D9.2a says validations stay additive)? #27949 with the ADR-0029 question and the Tier H consequence named — the shape (b) the first record offered. RIGHT.
  2. Must-fix 2 CLEARED — '@objectstack/spec': patch is in the frontmatter, with a body bullet naming what moves (the TranslateDocumentOptions.packagedBase docblock; no type or behaviour). RIGHT; the level is judged in ②.
  3. The one-packaged-rule-set half is unchanged from the reviewed head. git diff 029633fb c8c5460d touches none of protocol.ts, engine-validation-messages.ts, i18n-resolver.ts, nor the getPackagedObjectBase hunk of registry.ts, so record 6515204227 ①.1, ①.2, ①.4, ①.5, ①.6 and ①.7 carry over verbatim: public SchemaRegistry.getPackagedObjectBase is the right surface (cross-package read from @objectstack/metadata-protocol; internal would re-create the split); /meta list, by-name, the public form and the refusal all read it through one accessor with the same fallback order; scalars stay the owner's (Object-extension fold leaks the extension's label onto ?layers=true only — the two reads #7556 reconciled on FIELDS now disagree on LABEL #8037, pinned); by reference when no code extender ships a rule; undefined where getPackagedObjectOwner is; no regenerated spec artifact moves.
  4. Clause-②: yes (widening) — RIGHT, unchanged (①.3). Metadata-protocol: semantic — RIGHT, now on ONE meaning change: an authored overlay validations[].message that rewords a rule a second package ships wins over that package's catalog entry in every locale (before: the catalog won). The fold's meaning no longer changes on this head, so the second meaning change the first record named under ①.3(b) is gone with the yield.
  5. What a reader sees now, on the card's probe (dev's reading, consistent with the code and pinned): overlay rewords ext_blocked; the fold holds the overlay's copy and then the extender's original; /meta serves the copy as written and the original translated; the refusal reports both sentences. The card's "Expected" — one comparison, both readers; the tenant's rewrite served as written; an untouched extension rule still translated — is met. The two-sentence refusal is the duplicate the card body itself delegated to design, now objectql: a tenant overlay copy of an extension's validation rule runs BESIDE the extension's original in the fold — should the overlay replace it (ADR-0029 D9.2a says validations stay additive)? #27949's subject; it is not a regression (the fold served two rules before this PR too, both translated).

② Semver level

  • '@objectstack/objectql': minor, '@objectstack/metadata-protocol': minor — RIGHT (kind semantic, both packages move src/**; the protocol package's answer changes meaning).
  • '@objectstack/spec': patch — ACCEPTED. The first record said minor by AGENTS.md Post-Task Checklist 3's letter, patch tolerated by check-changeset-no-major.mjs's discharged verdict (a kind declared, a moved package graded minor+, others patch), and the choice the seat's. The seat chose patch for a docblock-only move on the ./system entry; the gate's own verdict table admits it, and Check Changeset is success on this head. The carrier now matches what the diff publishes from spec: a changed docblock on a published interface, no type, no accept set.
  • Clause-②: line on both carriers — RIGHT (yes (widening) with trailing reasoning; no (narrowing) arm, so no BREAKING banner, ADR-0087 disposition or dogfood battery owed).
  • Skills-docs: line — carried by the round-2 report (updated here — content/docs/ui/translations.mdx), still absent from the PR body; the seat carries it in the ACCEPT. Judged addressed there, as the dispatch said; the value is true on this head (the validation-rule paragraph names the extending package's shipped message).

③ Boundary flags

Round-2 report 6515694156: open_questions: []. Each deviations entry and the out_of_scope_findings entry:

  1. The PR body still describes the fold yield, the object-extensions.mdx edit, round-1 pin counts and ablation A2; the relay cannot re-edit it; the report supersedes those parts — flagged, should-fix before ACCEPT. The tree is judged, not the body (and the tree is right), but the body is a carrier the maintainer reads (维护者速读 「折叠里 overlay 带的同名规则顶替扩展包原规则」, the "What changes" fold bullet, the verification table's "1 rule (the rewrite)" rows, "A2 fold yield off → 3 red") and it now states the opposite of what lands. The two declaration lines on it are right and need no edit. The seat can rewrite the body through post-stamped --body=27932 before flipping the PR out of draft; the record does not wait on it.
  2. packages/rest/src/meta-item-read-gate.ts untouched (held by fix(rest)!: both import doors answer for the object as the data door does (#27728, #27729) #27897) — RIGHT, still right (packagedObjectBaseOf byte-identical on origin/main at bacb3e6).
  3. out_of_scope_findings: the overlay-copy yield and the byte-equal duplicate it would collapse belong to objectql: a tenant overlay copy of an extension's validation rule runs BESIDE the extension's original in the fold — should the overlay replace it (ADR-0029 D9.2a says validations stay additive)? #27949 — RIGHT. The card exists, is open, carries the ADR-0029 D9.2a / ADR-0126 regime E question, the Tier H consequence and the "replace or beside" decision for the maintainer, and links back to 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, this PR and record 6515204227. The first record's ③.6 note ("if the yield is split off, this finding reopens and then IS a card") is discharged by it.
  4. Tests — ANSWERED on the new head. The two yield pins are removed; a new pin states the additive fold's reading on both readers (served: [rewrite, translated original], refusal: 'rewrite | translated original', on zh-CN / zh / en) and the fix(i18n): a tenant-rewritten validation rule message beats the packaged translation, on the refusal and on /meta #27543 owner rows are re-pinned beside it; protocol-packaged-object-base.test.ts keeps the three packaged-base pins (append, by-reference, unmoved by an overlay). Ablations A1 (both readers) and A3 (refusal alone) re-run on this head and red as before; A2 has no subject any more. Test Core shards are in progress at the time of writing.

④ Design judgment

packages/spec/src/system/i18n-resolver.ts (the hits file; unchanged since 029633f) — the two docblocks remain accurate against the live tree: the serving layer hands translateObjectValidations a packagedBase whose validations carry each code extender's rules, so an extension rule the overlay rewords is judged diverged against the extension package's message and served raw, and the extension's own appended copy — byte-equal to its packaged message — is still translated through the unchanged localeChain walk. Both readers compare through valueOverridesPackagedBase / scalarOverridesPackagedBase with the same three conservative edges. The accepted scenario (card "Expected") holds on both readers and is pinned on one real SchemaRegistry + ObjectQL; owner-rule behaviour from #27543 is unchanged and re-pinned. No defect.

packages/objectql/src/registry.ts — only getPackagedObjectBase remains: a read over objectContributors.get(fqn) in the fold's own order, isCodeArtifactBody on each extender (the D9.6 test the owner is already held to), owner by reference when nothing is appended. It changes no fold, no cache, no registration door; ADR-0029 D9.2 / D9.2a and ADR-0126 regime E read exactly as the code behaves. The first record's "owner + extender same-name rule" note still stands as a pre-existing edge (both indexes first-name-wins; the extender's served copy is judged against the owner's packaged message and served untranslated) — unchanged since #27543, not this head's, filed or not at the seat's discretion. No defect.

packages/metadata-protocol/src/protocol.ts and packages/objectql/src/engine-validation-messages.ts — unchanged since 029633f; record 6515204227 ④ carries over: same registry method, same fallback order, same lazy index and depth bound as packagedRulesByName; a throwing read stays undefined. No defect.

Findings:

  • No must-fix. Both must-fixes of 6515204227 are cleared on this head.
  • Should-fix (before ACCEPT): the PR body's narrative (fold bullet, 维护者速读 fold sentence, verification table, ablation A2, object-extensions.mdx mention) contradicts the tree; rewrite it through the relay. Declaration lines stay.
  • Should-fix (before ACCEPT, carried by the seat): Skills-docs: updated here — content/docs/ui/translations.mdx on the ACCEPT, as the round-2 report states it.
  • Should-fix (lane, optional card): owner + extender same-name rule served untranslated (pre-existing since fix(i18n): a tenant-rewritten validation rule message beats the packaged translation, on the refusal and on /meta #27543).
  • Before landing: landing condition ② read on this head once the 22 in-progress runs complete — Lint & Repo Gates and TypeScript Type Check first.

Implemented-by: claude/project-thread-2irh6q
Reviewed-by: session_016WV4d1Np3hU7RQnA13YZcL

VERDICT: PASS


Generated by Claude Code

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:server#4 · os-elon-musk · session_016WV4d1Np3hU7RQnA13YZcL

维护者速读

席位意见:ACCEPT。

席位核对

Head-sha: c8c5460d6b86e61fba18230464ef041449c793a6

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review October 30, 2026 05:52
@objectstack-fleet
objectstack-fleet Bot enabled auto-merge October 30, 2026 05:52
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Oct 30, 2026
Merged via the queue into main with commit 4a5396b Oct 30, 2026
51 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/project-thread-2irh6q branch October 30, 2026 06:21
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/m tests tooling

Projects

None yet

2 participants