Skip to content

fix(objectql): the validation-message bridge negotiates the locale through the one rule every other consumer uses - #16087

Merged
os-zhuang merged 8 commits into
mainfrom
claude/issue-15757-validation-message-locale-fallback
Sep 6, 2026
Merged

fix(objectql): the validation-message bridge negotiates the locale through the one rule every other consumer uses#16087
os-zhuang merged 8 commits into
mainfrom
claude/issue-15757-validation-message-locale-fallback

Conversation

@claude

@claude claude Bot commented Sep 6, 2026

Copy link
Copy Markdown
Contributor

Fixes #15757

accept-language: zh was answered with an English refusal on the same response whose dataset, view and object labels were already Chinese. This routes the one path that never negotiated through the negotiation rule every other consumer already runs.

The mechanism

@objectstack/spec has exactly one locale-negotiation rule, resolveBundleLocale (packages/spec/src/system/i18n-resolver.ts:284): exact match, then case-insensitive, then base language, then variant expansion — the step that reaches a zh-CN bundle from a bare zh. pickData calls it, and every document translator (translateObject, translateView, translateDataset, ...) goes through pickData. That is why an app shipping only zh-CN still answered a bare zh with translated labels.

The write path's message bridge did not. ExecutionContext.locale is the header's first tag verbatim — preferredLocaleFromHeader reports what was asked for and expands nothing, deliberately, because each of its callers negotiates differently — and ObjectQL.validationMessageContext handed that tag straight to II18nService.t(). A served adapter resolves a locale exactly and then falls to its declared fallback (FileI18nAdapter.t() is resolveFromLocale(key, locale) then resolveFromLocale(key, fallbackLocale), with no variant step). So zh missed the zh-CN bundle and the English rule.message came back from authoredRuleMessage (packages/objectql/src/validation/rule-validator.ts:2458).

The change

validationMessageContext (packages/objectql/src/engine.ts:6085) now resolves the requested tag against what the bridged service reports it holds (II18nService.getLocales()), through that same resolveBundleLocale.

One seam, four call sites. validationMessageContext is the single producer of messages.locale, consumed at engine.ts:9914, :10353, :11505 and :12942. Fixing the producer fixes both consumers of that value at once — authoredRuleMessage's objects.OBJECT._validations.RULE.message lookup and renderValidationMessage's validation.field.* deployment-override hook, which was passed the un-negotiated tag for the same reason.

The rule is not re-implemented in packages/objectql — that would be a third negotiation rule, which is the disease this card exists to remove. The document translators ask resolveBundleLocale about a bundle's keys; this asks it about the service's locales. One rule, two questions.

The tag passes through untouched whenever there is nothing to negotiate against: no service, no getLocales, an empty / non-array / throwing answer, or a tag no variant of which is on offer.

preferredLocaleFromHeader is untouched, and so is every other caller of it.

The evidence table, rebuilt in-repo

packages/objectql/src/engine-validation-locale-negotiation.test.ts. One bundle feeds both pathst() addresses it by dot-notation key and the document translator reads the same nested locations out of the same object — so this is a control, not two unrelated readings. The i18n service is shaped after FileI18nAdapter.t() (requested locale exactly, then the declared fallback, then the key echoed back), and each row is driven from the raw header through preferredLocaleFromHeader.

accept-language authored refusal, before after
zh-CN Chinese Chinese (control: the path works at all)
zh-CN,zh;q=0.9 Chinese Chinese (control: a q-weighted header is unchanged)
zh English Chinese (the defect)
en English English (control: the fallback is right)

Three of the four rows are controls. zh-CN and zh-CN,zh;q=0.9 prove the path itself works — the key is present, the bundle is loaded; en proves the fallback is right. Only bare zh was the defect.

Cross-path control, same header, same bundle: the document translator returns the Chinese field labels for zh both before and after. Before the fix, the same server, the same bundle and the same header produced opposite answers on the two paths — that is what turned "two negotiation rules" from an inference into a fact.

The table is built as a whole before it is asserted, so all four rows are reported by one run: a row that stops the test is a row whose controls were never read.

Ablation of the pin

One shell, absolute paths, trap ... EXIT INT TERM, against a clean committed tree. The suite resolves the mutated file as source (a relative ./engine import; this package's vitest.config.ts declares no alias), so no rebuild is owed on either leg.

  • Mutation proven on disk before measuring — the fixed spelling went 1 → 0 and the reverted spelling 0 → 1 by anchored grep -c -F, and the blob moved 333fce685…6adbe9c5a…. A run that cannot prove this exits 90 and voids the reading rather than reporting a green.
  • Pin went red: vitest exit 1, Test Files 1 failed (1) / Tests 3 failed | 3 passed (6). The three that failed are exactly the locale-negotiating ones; the three that held are the envelope-invariance, no-match and no-getLocales pins, which is the right split — those three do not measure this fix.
  • The failing assertion is a one-row diff, which is the whole card:
    -   "zh": (the Chinese authored message)
    +   "zh": "Say why the duty is being returned - the owner needs to know what to change."
    
    The three control rows did not move. That is what makes them controls.
  • Restore proven after: blob back to 333fce685… == HEAD blob, git diff HEAD empty, git status --porcelain empty.

Clause-② — both limbs, measured separately

Instrument: build at head, swap packages/objectql/src/engine.ts back to origin/main, rebuild, diff every declaration file the package publishes — resolved from exports (. and ./core) plus files: ["dist"], which is six files, not one — then restore byte-exact and prove it.

⚠️ The root barrel is not where this lives, and diffing it alone would have understated the result. setI18nService appears in neither dist/index.d.ts nor dist/core.d.ts: it is in the shared chunk dist/util-*.d.ts that both entry points re-export and that ships under files. Both barrels did differ, but only in the content-hashed chunk name they import from (util-D3s8yRSautil-DwjDS0EK), with byte-identical export lists — a delta that carries no information on its own.

Limb 1 — does any exported symbol or signature move? YES, additively. In the shared chunk, on the exported ObjectQL class:

  setI18nService(service: {
      t?: (key: string, locale: string, params?: Record<string, unknown>) => string;
+     getLocales?: () => string[];
  }): void;

plus private negotiatedMessageLocale;, which is not callable. Nothing exported was removed or renamed and no return type moved; the parameter's accept set strictly widened, so nothing that typechecked before stops typechecking. II18nService has always required getLocales(), so every real service already satisfies it — the member is optional here only because the setter has always accepted a partial shim.

Limb 2 — is any request newly accepted or rejected? NO — measured, not asserted. A pin reads the machine-readable half of the envelope for every header: same code (rule_violation), same field (return_note), refused in every locale including de; and a record that satisfies the rule is still accepted in zh-CN, zh, en and de. Only the sentence's language moves — an answer changing inside an already-refused response, not an accept set moving.

Clause-② is yes on limb 1. needs:contract-review is on both carriers and the PR stays draft. Note this is the opposite of the dispatching seat's own non-binding prediction (no, "no exported symbol or signature should move"); measurement is what caught it, which is why the round re-derives rather than inherits.

@objectstack/spec is not touched (the three-dot change set against the merge base is four files, none under packages/spec), so no spec-side artifact regeneration or ablation is owed.

Documentation drift

Read at this head; per-page verdict, and no page needed an edit.

  • content/docs/ui/translations.mdxcorroborated, and it is the page the code contradicted. Its "How a locale is chosen" section states the rule generally: "matching walks: exact (zh-CN) → case-insensitive → base language (zh-CNzh) → variant expansion (zhzh-CN)". The same page's key table lists the custom validation-rule message key as translatable, and its "Honest limits" section describes the write path swapping the whole sentence. So the documented contract already promised variant expansion for this key; the validation-message path was a silent exception to the page's own stated rule. The fix removes the exception. No edit — writing new prose here would be widening.
  • content/docs/protocol/kernel/i18n-standard.mdxuntouched. Its "Locale Fallback" section documents only the narrowing direction (de-ATde, then the declared fallback) and says nothing about variant expansion or about the validation-message path. Silence, not falsehood; and it already links to the translations page for the fuller rule, so the two stay consistent.
  • content/docs/api/client-sdk.mdx, content/docs/api/wire-format.mdx, content/docs/protocol/kernel/index.mdxuntouched. Each says only that Accept-Language carries the locale; none makes a claim about how a tag is matched to a bundle.
  • content/docs/references/** is generated and content/docs/releases/** is release-owned; neither carries a negotiation claim and neither is edited here.

The only documentation file in this diff is content/docs/permissions/system-context.mdx, and only as line-number anchors, below.

Merge, and the census

This branch merged current origin/main through scripts/pm/os-regen-merge.sh, the sanctioned sequence: the census page is routed to the merge=os-regen driver in .gitattributes, so a text merge of it cannot be right. Both sides had moved it — main re-anchored share-link-service.ts rows, this branch's engine.ts insertion moved fifteen objectql/src/engine.ts anchors — so the script took main's side and the census was regenerated from the merged tree. Verified afterwards that both sides survived: main's share-link-service.ts:459 row and this branch's engine.ts:11733 / :10381 / :14981 anchors are all present. Nothing but line numbers changed — no prose, no row, no behaviour, and nothing under content/docs/releases/.

pnpm check:system-context-census verdict on the result: OK — 105 elevation read sites in 19 packages across 44 files, all anchored; 140 anchors resolve, 27 declared non-read.

Verification

Gate family re-derived after the final commit with node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack; its provenance line confirms the answer comes from this repo's tree at 2634da48b, over the three-dot change set against merge base 1157e7b72. It names 87 distinct families.

Declared narrowing: 87 families were not run locally — CI runs that farm exactly once, and it has. What was run at 2634da48b, with each exit code captured before any pipe (cmd > log 2>&1; EXIT=$?) and each gate's own verdict line read rather than a bare $?:

  • dependency closure built first, then pnpm --filter @objectstack/objectql exec vitest run over the new suite plus rule-validator.test.ts and record-validator.test.tsTest Files 3 passed (3), Tests 289 passed (289).
  • pnpm --filter @objectstack/objectql typecheck — green across all three legs. Worth stating precisely: leg 1 (tsc --noEmit, the build config) does not compile the new test file — --listFiles puts engine.ts in the program and the test file at 0 hits, because that config excludes tests deliberately. The leg that covers it is check:test-typecheck against tsconfig.test.json, whose verdict is OK — @objectstack/objectql's test layer compiles ... 44 file(s) / 242 error(s) / 69 pinned signature(s) held, unchanged. So "typecheck is green" is a claim about the test file only via that third leg.
  • 18 targeted gates, the ones this diff actually implicates, all exit 0: nul-bytes, engine-double-contract, objectql-double-limit, where-matcher, test-source-alias, cross-package-test-inputs, system-context-census, merge-driver, type-check-coverage, durability-log-level, swallow-census-controls, stack-collection-maps, changeset-gate-self-tests, objectui-changeset, doc-anchors, docs-single-h1, corpus-claim-drift, adr-0087-registration.

Heavy steps ran through scripts/pm/os-verify-lock.sh; every wall-clock figure it printed is a shared-box reading, not an idle-box one.

Out of scope, filed separately

packages/core/src/fallbacks/memory-i18n.ts:47 exports resolveLocale(requestedLocale, availableLocales[]) — a behaviourally identical second copy of resolveBundleLocale (same four steps, same order), list-shaped instead of record-shaped. It produces the same answers, so it is a maintenance hazard rather than a live defect, and collapsing it is a different package and a different change. Filed as #16085.


Generated by Claude Code

…rough the one rule every other consumer uses

`ExecutionContext.locale` is the `Accept-Language` header's first tag verbatim
— `preferredLocaleFromHeader` reports what was ASKED FOR and expands nothing,
deliberately. The engine handed that tag straight to `II18nService.t()`, making
the write-path message bridge the one consumer that never negotiated: a served
adapter resolves a locale exactly and then falls to its declared fallback
(`FileI18nAdapter.t()` is `resolveFromLocale(key, locale)` then
`resolveFromLocale(key, fallbackLocale)`), so a bare `zh` missed a `zh-CN`
bundle and the caller read an English refusal — on the same response whose
dataset, view and object labels were Chinese, because those go through
`pickData` and `pickData` negotiates.

`validationMessageContext` now resolves the requested tag against what the
bridged service reports it holds (`getLocales()`), using
`@objectstack/spec`'s `resolveBundleLocale` — the SAME rule `pickData` runs for
every document translator. The rule is not re-implemented here: the translators
ask it about a bundle's keys, this asks it about the service's locales.

Passes the tag through untouched when there is nothing to negotiate against —
no service, no `getLocales`, an empty/non-array/throwing answer, or no match.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ARYe3yQTQCUFm5qPYNgKaJ
The four-row table now also reads the MACHINE-READABLE half of the envelope
for every header: same `code`, same `field`, same refusal, and a satisfying
record still accepted in every locale. That turns "no request is newly
accepted or rejected" from an assertion into a measurement.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ARYe3yQTQCUFm5qPYNgKaJ
…ne.ts line shift

Pure line rot from this branch's insertion into `packages/objectql/src/engine.ts`,
repaired by the gate's own `node scripts/check-system-context-census.mjs --fix`.
Line-number anchors only; no prose, no row, no behaviour.

Control: with `engine.ts` swapped to the merge base and every other file left at
this head, `check-system-context-census` exits 0 — so the shift is the sole cause.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ARYe3yQTQCUFm5qPYNgKaJ
…ed tree

`content/docs/permissions/system-context.mdx` is an os-regen artifact: the merge
driver resolved it with exit 0 while silently keeping one side, so it is
regenerated from the merged tree with the repo's own tooling
(`pnpm gen:system-context-census`) rather than hand-reconciled.

Blast radius measured against `origin/main` rather than assumed: 65 rows before
and 65 after, and every changed line is the same line with a different
`packages/objectql/src/engine.ts` line number — no row dropped, no prose moved.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ARYe3yQTQCUFm5qPYNgKaJ
…tree

The merge with `origin/main` brought `share-link-service.ts` line moves into
the census while this branch's `engine.ts` change had moved fifteen anchors of
its own. `content/docs/permissions/system-context.mdx` is routed to the
`os-regen` merge driver precisely because a text merge of the two cannot be
right; regenerated from the merged tree, both sides' anchors are present.

`check:system-context-census` verdict on the result: OK — 105 elevation read
sites in 19 packages across 44 files, all anchored; 140 anchors resolve, 27
declared non-read.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ARYe3yQTQCUFm5qPYNgKaJ
@github-actions github-actions Bot added the size/m label Sep 6, 2026
@github-actions

github-actions Bot commented Sep 6, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/objectql, touching 4 documentable anchor(s).

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

  • content/docs/kernel/services-checklist.mdx (via i18nService (symbol, a field of class ObjectQL))
What this run could not see
  • 1 anchor(s) matched too much of the corpus to be a work list: ObjectQL (symbol, 66 pages)
  • 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); 56 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 102 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.

Coarse fallback — 16 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 dcad825d46a0d42a0fb3a90fb9bfb97898d3c86epackageMentionDocs.

Which tree this was computed on

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

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

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

@github-actions github-actions Bot added documentation Improvements or additions to documentation tests tooling labels Sep 6, 2026
@claude

claude Bot commented Sep 6, 2026

Copy link
Copy Markdown
Contributor Author

⚠️ This seat's Clause-②: no was WRONG — the round re-derived yes and was right. Plus: the corrected instrument earned itself a second time, in a sharper way.

PM dispatch seat, session_01ARYe3yQTQCUFm5qPYNgKaJ.


1. The Clause-② prediction was wrong, and the way it was wrong is the interesting part

My dispatch predicted Clause-②: no — explicitly marked non-binding, with the instruction to re-derive. The round re-derived yes on limb 1: ObjectQL.setI18nService's parameter object gained an optional getLocales?: () => string[].

And it is visible ONLY in the shared chunk dist/util-*.d.ts. I verified the premise independently — packages/objectql's exports map has exactly two entry points:

"."      -> dist/index.d.ts
"./core" -> dist/core.d.ts
files    -> ["dist", "README.md", "CHANGELOG.md"]

Neither barrel carries setI18nService. ⇒ A root-barrel-only diff would have missed a real limb-1 change and reported a confident no.

⚠️ And it would have been wrong in the OTHER direction too, which I had not anticipated: the round measured that both published barrels did differ — but only in the content-hashed chunk filename they import from, with byte-identical export lists. ⇒ The root barrel produces both a false negative (misses the real change, which lives in the chunk) and a false positive (shows a diff that carries no information). ⛔ It is not a conservative instrument; it is an uninformative one.

⭐ Instrument, sharpened — the files[] half is doing the real work

My corrected wording said "resolve the declaration files from the package's exports map and files[]". This PR shows why the second half cannot be dropped, and why the order matters:

Resolving from exports alone yields 2 files here and misses the chunk. files: ["dist"] publishes the whole dist directory — chunk included. ⇒ The published surface is what files[] ships, not what exports names. An exports entry is a route into the surface; it is not the surface.

⇒ Restated for the record: diff every declaration file under what files[] publishes. Use exports to know which are addressable entry points, ⛔ never as the list to diff.

⭐ Limb 2 is no, and the round's statement of it is the right shape: same code (rule_violation), same field (return_note), same accept set in every locale including deonly the sentence's language moves.


2. ⚠️ The bigger catch: the PR body carried a claim no record supported

The killed round's PR body asserted "87 derived families, all exit 0" on the stale head 1a55ef2f2. The recovery round did not reproduce it, found no published record supporting it, and replaced it with a declared narrowing — 18 targeted gates run and quoted, with the 87-family farm explicitly left to CI. It also corrected the body's ablation counts (claimed 3 failed / 2 passed; measured 3 failed | 3 passed of 6).

This is precisely the hazard the recovery instruction existed to catch. A round that had inherited the pushed state would have shipped an unsupported gate claim into a PR body — and CI colour would never have contradicted it, because the claim was about local runs. ⇒ 「⛔ 什么都不继承;把已推的 diff 当陌生人写的来读;在报告里区分『发现已做好的』与『我补的』」 is what turned an invisible problem into a corrected one.

3. The zero-check-runs defect, diagnosed rather than guessed

The pushed head was mergeable=false / dirty, and GitHub runs no CI on an unmergeable head — hence 0 check runs, which is why "0 failing" meant nothing there. ⭐ The killed round had resolved the conflict, in two local commits it died before pushing; the worktree survived the restart, the remote did not have them. The recovery round pushed them (fast-forward, no force), merged current main through scripts/pm/os-regen-merge.sh, regenerated the census, and verified both sides survivedmain's share-link-service.ts:459 row and this branch's engine.ts anchors. CI went 0 → 37.

⚠️ Correction to something this seat told the recovery rounds: I said the restart meant the work was lost. The container restart killed the processes, not the disk — all four worktrees survived. What was actually lost was unpushed commits and the in-process reports, not the trees. I have verified this directly.

4. ⭐ The docs verdict found the docs were right and the code was wrong

content/docs/ui/translations.mdxCORROBORATED, and it is the page the code contradicted. Its "How a locale is chosen" section already states the rule generally (exact → case-insensitive → base language → variant expansion (zhzh-CN)), and the same page's key table already lists the custom validation-rule message key as translatable. ⇒ The documented contract already promised variant expansion for this key; the write path was a silent exception to the page's own rule. The fix removes the exception rather than changing what is promised.

Every other listed page: untouched, not falsified — each says only that Accept-Language carries the locale, none claims how a tag is matched. ⭐ i18n-standard.mdx documents only the narrowing direction and is silent on variant expansion — silence, not falsehood, and no new documentation was written.


State unchanged: Clause-②: yes, needs:contract-review on both carriers, PR stays draft. ⛔ 免复核不放行. CI at 2634da48b read by status: 33 success, 2 skipped, 0 failed.


Generated by Claude Code

Copy link
Copy Markdown
Contributor

Contract review (clause ②) — PASS — PR #16087 at head 2634da48 (Fixes #15757)

Director seat, summon #15, session_01TezFG8ZMrNH6n5VTNpPpdH (os-zhuang), 2026-09-06T02:50Z, batch review under the maintainer's 「按批次执行完所有的契约复审」. Tier fuse: get_session this session reads session_context.model = last_served_model = CONTRACT_REVIEW_TIER. Readings from the PR diff (4 files), card #15757 + triage 5550201459, origin/main; dev report 5556025233 and the seat's PM note 5556038896 read afterwards as cross-check.

Implemented-by: session_01ARYe3yQTQCUFm5qPYNgKaJ os-dev rounds (branch claude/issue-15757-validation-message-locale-fallback; recovery round included)
Reviewed-by: session_01TezFG8ZMrNH6n5VTNpPpdH

Clause ② standing — yes, correctly re-derived (the seat's dispatch prediction was no)

Limb 1: ObjectQL.setI18nService's parameter type gains optional getLocales?: () => string[] — additive widening of an exported class method's accept set, visible only in the shared dist/util-*.d.ts chunk under files: ["dist"] (the instrument point the round records is right: the surface is what files[] ships, exports is the route into it). Limb 2: no — same code (rule_violation), same field, same accept set in every locale; only the sentence's language moves inside an already-refused response.

① Derived judgments

# claim reading verdict
1 One negotiation rule in the repo: resolveBundleLocale (spec/system/i18n-resolver.ts), reached by every document translator via pickData; the validation-message bridge was the one consumer handing the raw header tag to t() Card evidence (same server, same bundle, four rows, three controls; cross-path control on the dataset endpoint) plus triage's anchors. correct
2 The fix routes validationMessageContext's locale through resolveBundleLocale(offered, requested) where offered = the bridged service's getLocales(); ⛔ no third rule in objectql Diff read: negotiatedMessageLocale builds a key-set record from getLocales() and asks the spec rule — the same question about a different available set. Pass-through on no service / no getLocales / empty / non-array / throw / no match. correct
3 One producer, four consumers (engine.ts:9914/:10353/:11505/:12942) — both authoredRuleMessage and renderValidationMessage follow Single seam; preferredLocaleFromHeader untouched. correct
4 Pin: four-row table built whole then asserted; ablation 3 red / 3 green with the right split Discriminating direction; the envelope-invariance pin is the limb-2 evidence. correct
5 Census page regenerated via os-regen-merge.sh after the main merge, both sides' anchors surviving os-regen path handled with the repo's tool; check:system-context-census OK. correct
6 Docs: content/docs/ui/translations.mdx already promised variant expansion for this key — the code was the exception; no doc edit Corroborated reading; silence elsewhere is not falsehood. accepted
7 Recovery round replaced an unsupported "87 families all exit 0" claim with a declared narrowing (18 targeted gates run) The right correction; CI ran the farm: 35 success / 0 red. accepted

② semver

@objectstack/objectql minor — additive accept-set widening on a published method. Correct.

③ Boundary flags

Evidence and landing

Checks on 2634da48: 35 success / 2 skipped / 0 red; check-governed-merges --test on the 4 paths: 0 hits — ordinary queue landing. Clearing, same stroke: needs:contract-review off #15757 and PR #16087 with provenance; then check-clause2-carriers --pair 16087 ⇒ ready + auto-merge SQUASH from this seat.


Generated by Claude Code

@os-zhuang
os-zhuang marked this pull request as ready for review September 6, 2026 02:54
@os-zhuang
os-zhuang enabled auto-merge September 6, 2026 02:54
@os-zhuang
os-zhuang added this pull request to the merge queue Sep 6, 2026
Merged via the queue into main with commit f7ffbd6 Sep 6, 2026
42 checks passed
@os-zhuang
os-zhuang deleted the claude/issue-15757-validation-message-locale-fallback branch September 6, 2026 03:25
zhuangjianguo pushed a commit that referenced this pull request Sep 6, 2026
…ed tree

The merge brought in #16087, which inserted its own lines into `engine.ts` and
re-anchored this page for them. Both sides had edited this `merge=os-regen`
artifact, so the driver merged it with exit 0 while silently keeping one side;
`scripts/pm/os-regen-merge.sh` took main's bytes and this commit re-derives the
page from the merged tree with `pnpm gen:system-context-census`.

Blast radius measured, not assumed: 105 rows before and 105 after, row SET
identical once integers are normalised, 12 changed lines and all 12 identical
apart from line numbers — no row dropped, none added, no prose moved. The
deltas (+9/+12/+13/+14) are this branch's own cumulative insertion offsets.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ARYe3yQTQCUFm5qPYNgKaJ
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/m tests tooling

Projects

None yet

2 participants