Repository navigation
fix(i18n): a tenant-rewritten validation rule message beats the packaged translation, on the refusal and on /meta - #27543
Conversation
… /meta (red) ADR-0029 D9.2a: under a non-default locale the packaged translation of a validation rule message must not replace a tenant's rewrite of it. Red first, on both readers: translateObject's validations overlay, the rule evaluator's authored message, and the two together off one registry. Claude-Session: https://claude.ai/code/session_01L6bhgW82YGUW8s8X4Dgiqc Co-authored-by: Claude <noreply@anthropic.com>
…tion on both readers ADR-0029 D9.2a. translateObjectValidations (the /meta overlay) and the rule evaluator's authoredRuleMessage (the refusal) now apply the catalog only while the served message still equals the packaged rule's of the same name, judged by the one predicate: valueOverridesPackagedBase, which objectql reaches through the exported scalarOverridesPackagedBase (its key union gains 'message'). The engine's validation message context carries the packaged owner's rule lookup. Claude-Session: https://claude.ai/code/session_01L6bhgW82YGUW8s8X4Dgiqc Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01L6bhgW82YGUW8s8X4Dgiqc Co-authored-by: Claude <noreply@anthropic.com>
…lation A rule an objectExtensions entry folds onto a packaged object is absent from the owner's declaration by construction and is translated under its own name. Counting it as diverged served it untranslated on both readers. A rule with no packaged counterpart now has nothing to diverge from, as with no base at all, and the catalog applies. Claude-Session: https://claude.ai/code/session_01L6bhgW82YGUW8s8X4Dgiqc Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01L6bhgW82YGUW8s8X4Dgiqc Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01L6bhgW82YGUW8s8X4Dgiqc Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift CheckThis PR changes 2 package(s): 14 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
What this run could not see
Coarse fallback — 168 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 4ccb3bca139066cbf1d91b1a100dff4d4187bffe && git checkout 4ccb3bca139066cbf1d91b1a100dff4d4187bffe
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin d4ff1a025fa4568505bfbc66a0f8cbeb3bab06ff 3e8364560d176c6cae02faa7db982154b88fd54d && git checkout -B drift-repro d4ff1a025fa4568505bfbc66a0f8cbeb3bab06ff && git merge --no-ff 3e8364560d176c6cae02faa7db982154b88fd54d
node scripts/docs-audit/affected-docs.mjs --json d4ff1a025fa4568505bfbc66a0f8cbeb3bab06ff
|
|
domain:server#1 · os-sales · session_01L6bhgXcN9HaZYSEUdJWEjw Contract-review-claim: session_01L6bhgXcN9HaZYSEUdJWEjw · head cb1b688 · 2026-10-29T12:11Z Face: published schema ( |
Claude-Session: https://claude.ai/code/session_01L6bhgW82YGUW8s8X4Dgiqc Co-authored-by: Claude <noreply@anthropic.com>
|
domain:server#1 · os-sales · session_01L6bhgXcN9HaZYSEUdJWEjw Contract reviewServed-tier: Written 2026-10-29T12:23Z. Read-only: card #27221 (body and all 7 comments through 6496338780), PR #27543 body and file list, Head and check-runs. The PR head moved after dispatch: e4a45ed (pushed 2026-10-29T12:13:54Z) adds 13 lines to ① Derived judgments
② Semver level
③ Boundary flagsReport 6496263463:
④ Design judgment
Findings:
Implemented-by: VERDICT: FAIL Generated by Claude Code |
… body returns; changeset declares Clause-② yes (widening) The contract review of record found the declaration short: the exported scalarOverridesPackagedBase key union and EvaluateRulesOptions.messages' packagedRule both widen a published surface. Claude-Session: https://claude.ai/code/session_01L6bhgW82YGUW8s8X4Dgiqc Co-authored-by: Claude <noreply@anthropic.com>
|
domain:server#1 · os-sales · session_01L6bhgXcN9HaZYSEUdJWEjw Contract-review-claim: session_01L6bhgXcN9HaZYSEUdJWEjw · head 3e83645 · 2026-10-29T12:54Z Delta review of the FAIL record 6496586363 (on cb1b688) by the same reviewer: the must-fix (declaration) and the should-fix (declared return type) landed in 3e83645; e4a45ed is prose-only. The PR body was corrected by the seat to |
|
domain:server#1 · os-sales · session_01L6bhgXcN9HaZYSEUdJWEjw Contract reviewServed-tier: Delta review, written 2026-10-29T12:56Z, by the same reviewer as the FAIL record 6496586363 on cb1b688 (contract-review.md: a commit that only lands findings gets a delta review by the same reviewer). Inputs: What moved since cb1b688 (3 files, +16/-3): e4a45ed — Check-runs on 3e83645, read 2026-10-29T12:55Z: 50 runs, 41 success, 9 skipped, 0 failure, 0 in progress — Lint & Repo Gates, TypeScript Type Check, Build Core, Test Core, Governed Surface Queue Guard, Check Changeset all success. ① Derived judgments
② Semver levelUnchanged and right: ③ Boundary flags
④ Design judgment
No must-fix on this head. The should-fix left open for the lane is the extension-rule rewrite card (③.2). Implemented-by: VERDICT: PASS Generated by Claude Code |
|
domain:server#1 · os-sales · session_01L6bhgXcN9HaZYSEUdJWEjw 维护者速读(席位终稿)
席位意见:接受。首轮复核只挑出申报写错(应为 Clause-② yes),已在两个载体上改正,增量复核 PASS;设计判断在两个读取端都成立。 席位核对 — ACCEPT · domain:server#1 · head 3e83645 · 2026-10-29T12:58Z
|
domain:server#1 · os-sales · session_01L6bhgXcN9HaZYSEUdJWEjw
Requested by Sales · project thread
Fixes #27221
Clause-②: yes (widening) — the exported
scalarOverridesPackagedBase(@objectstack/spec/system) accepts'message'as a key, andValidationMessageContextgains the optionalpackagedRule(reached fromEvaluateRulesOptions.messages); corrected by the seat per contract review 6496586363Metadata-protocol: semantic
Skills-docs: updated here — content/docs/ui/translations.mdx ("An edit beats the packaged catalog": validation rule messages), prose-only commit e4a45ed
维护者速读
packages/spec/src/system/i18n-resolver.ts(translateObjectValidations 的比较)、packages/objectql/src/validation/rule-validator.ts(authoredRuleMessage)、packages/objectql/src/engine-validation-messages.ts(packaged 规则查找)。pnpm --filter @objectstack/objectql exec vitest run src/engine-validation-tenant-rewrite.test.ts,预期 3 passed:zh-CN/zh/en 三个语言下 /meta 与拒收给出同一句,改写的规则是租户原句。What changes
ADR-0029 D9.2a, applied to validation rule messages on both of their readers, with one comparison.
/meta(translateObjectValidations,packages/spec/src/system/i18n-resolver.ts). The overlay ofvalidations[].messagefromobjects.OBJECT._validations.RULE.messagenow applies only while the served message still equals the message of the packaged rule of the same name inTranslateDocumentOptions.packagedBase(aconditionalbranch by its own name). The test isvalueOverridesPackagedBase, the predicate the object's scalars, embedded list views and field descriptions already use. The docblock's "No packaged-base comparison" section, which recorded the deliberate deviation of fix(spec): translateObject serves validations[].message in the reader's language #27202, is replaced.authoredRuleMessage,packages/objectql/src/validation/rule-validator.ts). The same rule, through the exportedscalarOverridesPackagedBase, whose key union gains'message'. That widening is the only change to a@objectstack/spec/systemexport: a parameter type accepts one more literal; the behaviour for the three existing keys is unchanged, and no generated artifact moves (api-surface/system.jsonrecords names,api-surface-signatures.jsonrecords only thedefine*signatures).packages/objectql/src/engine-validation-messages.ts,record-validator.ts,engine.ts).ValidationMessageContextgains an optionalpackagedRule(ruleName)lookup.validationMessageContextfills it fromSchemaRegistry.getPackagedObjectOwner(name).definition, the same body/metahands its translator throughgetPackagedObjectBase; the index is built on the first lookup.engine.tschanges only the host literal ofObjectQL#validationMessageContext(oneregistrygetter), added to the claim's file surface by the seat (6495205921). No packaged owner, or a registry that cannot answer, leaves the lookup absent, and the catalog applies as before.One rule, two readers:
/metaand the refusal of the same write still show one sentence for one rule and locale, pinned together inengine-validation-tenant-rewrite.test.ts.Why
A tenant who reworded a packaged rule's message for their own users saw it reverted to the vendor's translation as soon as a user's locale had a bundle entry, on the server's refusal and on the served form. The refusal half predates #27202; the
/metahalf followed from #27202 keeping the two readers identical (triage 6468439288: "One precedence rule, used by both readers").Tests
Red first (commit
7976cc1a, tests only, onorigin/main57c51aab4):pnpm --filter @objectstack/spec exec vitest run src/system/i18n-resolver.object-validations.test.ts: 3 failed / 13 passed (16). The three new/metacases failed; the 13 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 cases passed.pnpm --filter @objectstack/objectql exec vitest run src/validation/rule-validator.test.ts src/engine-validation-tenant-rewrite.test.ts: 4 failed / 192 passed (196). The rewrite cases on the evaluator failed, and so did the two-reader engine case. Underzhthe engine case read the packaged translation on BOTH readers, and underenit read the packaged English. The overlay sanity case and the no-overlay control passed.Final head (
cb1b6888,origin/main6892888emerged in):i18n-resolver.object-validations.test.ts(four D9.2a cases: rewrite served as written, untouched rules translated, a rule the base does not declare keeps its own entry, no base means catalog as before, throughtranslateMetadataDocument('object')too);rule-validator.test.ts(five evaluator cases, aconditionalbranch included);engine-validation-tenant-rewrite.test.ts(one realSchemaRegistrywith a packaged owner, a tenant overlay and anobjectExtensionscontributor, the refusal of real writes besidetranslateObjectwithgetPackagedObjectBaseaspackagedBase, forzh-CN,zhanden; plus a no-overlay control).pnpm --filter @objectstack/spec testunder the verify lock: VERDICT command-exit 0, Test Files 751 passed (751), Tests 22332 passed, 1 todo.pnpm --filter @objectstack/objectql testunder the verify lock: VERDICT command-exit 0, Test Files 485 passed (485), Tests 9109 passed (9109).pnpm --filter @objectstack/spec typecheckandpnpm --filter @objectstack/objectql typecheck(with itscheck:test-typecheck): both VERDICT command-exit 0.Ablation (negative pins). Each run uses
node scripts/ablation-replace.mjson the committed heade7578942. Every mutation was proven on disk (anchor 1 to 0, blob changed), and every restore matched the HEAD blob byte for byte. All three subjects resolve throughsrc(relative imports), so nodistpreflight applies:rule-validator.ts:scalarOverridesPackagedBase(messages.packagedRule?.(rule.name), 'message', rule.message)replaced byfalse. 3 failed / 193 passed (196). The two evaluator rewrite cases went red, and so did the engine case, where only the refusal half differed from the served half.i18n-resolver.ts: thevalueOverridesPackagedBase(packaged.get(rule.name), 'message', rule.message)line deleted. 2 failed / 14 passed (16), the rewrite cases.engine.ts: the host literal'sget registry()line deleted. 1 failed / 2 passed (3), the engine case. This proves the wiring, not only the predicate.The deviation in Acceptance notes is measured too. The final tests were run against the first implementation (
1a6b3305), which counted a rule absent from the base as authored.engine-validation-tenant-rewrite.test.tsfailed 2 of 3. The extension package's rule answered its English sentence underzh-CNandzh, both in the overlay group and in the no-overlay control.Acceptance notes
objects.OBJECT._validations.RULEalso addresses rules anobjectExtensionsentry folds onto the object (content/docs/protocol/kernel/i18n-standard.mdx), the registry concatenates those rules intovalidations, and bothgetPackagedObjectBaseandgetPackagedObjectOwneranswer the OWNER's declaration only. Counting an absent rule as authored served every extension package's translated rule in the source language, on both readers, with no tenant overlay at all. Measured: the engine test's extension rule, run against the first implementation (1a6b3305), went red in both the overlay group and the no-overlay control (zh-CN answered the English sentence); green on the final head. Unlike a list view, a rule is translated under its own name, which is why the list-view convention ("a view the base does not carry counts as diverged") is not copied here.objectExtensionsentry contributed still gets that package's translation (the catalog applies, as before this PR; not a regression). Covering it needs the packaged base that/metareceives to include code extenders' rules, i.e. a change to where@objectstack/restsourcespackagedBase(packagedObjectBaseOf/getPackagedObjectBase), and the same source on the refusal side so the two readers stay identical. Left to the seat to file or not.valueOverridesPackagedBaseon both readers (objectql reaches it throughscalarOverridesPackagedBase). The by-name walk over the packagedvalidations(branches included, the same depth bound of 10, first name wins) exists once per package:packagedRulesByNameini18n-resolver.tsandpackagedRuleLookupinengine-validation-messages.ts. Sharing it would need a new@objectstack/spec/systemexport, which regeneratespackages/spec/api-surface/system.jsonandexport-origins/system.json, outside "nothing else under packages/spec".ObjectQL#validationMessageContext's declared return type inengine.tsstill nameslocale/translate/objectNameonly; the object it returns carriespackagedRuletoo and reaches the validators unchanged. Widening the declaration is a secondengine.tshunk, outside the approved host-literal edit.i18n-resolver.ts,rule-validator.ts,record-validator.tsorengine-validation-messages.ts; only fix(objectql): extend the credential-column guard from aggregates to filter and sort keys #27477 touchesengine.ts(hunks L13315–20160 per the seat; this PR's hunk is at L9548), andgit merge-tree --write-treeof this head with fix(objectql): extend the credential-column guard from aggregates to filter and sort keys #27477's head5eb62f09is clean. The threepm:mailboxPRs were not read; the Version Packages PR chore: version packages #26517 hits none of these files on its first 100. Since thenorigin/maingained four commits (tod4ff1a02, read at 12:05Z), none touching this PR's nine files;git merge-tree --write-treeof this head with it is clean.claude/project-thread-i8f8j5is the card thread's harness-pinned branch and names no issue; the claim (6495130451) is the identity record.packages/spec, pm-dispatch(contract-review): a seat sends a PR to the maintainer for approval only after a contract PASS on the current head, and a PR that landed still owing one is re-reviewed after landing, with each finding filed as a new card #27345).Verification
Derived local subset on the final head
cb1b6888:node scripts/pm/dispatch-gates.mjs --commands, no paths, against merge base6892888e4, 9 paths, 589 changed lines. Every one of the 85 commands was run and its exit code captured before any pipe. The reconciliation--ran: "85 derived famil(ies) accounted for — 85 run, 0 NOT-MEASURED (a DERIVED zero — all 85 recorded an exit code and none of them is 3)". Two first answered exit 3 (a prerequisite, not a verdict) and were re-run green:check-plugin-teardown-shape.mjs --self-testpassed after fetching its pinned fixture commit: 48 cases.check:dual-build-cjs-loadspassed after a fullturbo run build: 115/115 tasks.Also run:
pnpm --filter @objectstack/spec check:generated: "All 14 generated artifacts are up to date". No artifact underpackages/specmoves.check:dev-prereqsandcheck:skill-frame-freshness(local-only): exit 0.Repo-wide lint and the CI-owned families are read from this PR's check-runs.
Human Minimum
packages/specholds no runtime logic. (The change is inside the existing translator module; no schema moves.)pnpm lintpasses. (Repo-wide lint is CI's run.)main, one pull request.packages/spectouched: its generated artifacts are regenerated and committed. (check:generatedreports none stale.)content/docs/references/,packages/*/CHANGELOG.mdorcontent/docs/releases/.Generated by Claude Code
🤖 Generated with Claude Code
https://claude.ai/code/session_01L6bhgW82YGUW8s8X4Dgiqc