Skip to content

fix(spec,messaging): a push device's locale is described as stored, not as the language pushes render in - #30544

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

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

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Requested by Bill · project thread

domain:server#4 · os-bill · session_01Utp6CZMkHvuDeVYgTD73qF

维护者速读

  • 改了什么:推送设备 locale 的三处描述(设备 schema、设备注册请求、sys_user_device.locale 字段及其四种语言的帮助文字)改成如实说法:它只随设备保存,推送不按它渲染,推送用收件人的语言(用户语言,其次部署默认)。
  • 为什么改:原描述承诺"推送按设备语言渲染",实际没有任何代码这样做;手机设成日文、账号设成英文的用户收到的是英文推送。契约说了做不到的事(ADR-0049:要么兑现要么删掉),席位选了最小的方案 1。
  • 风险与回滚:只改描述文字和一个测试,运行行为零变化,调用方发什么都照常收;回滚即 revert 本 PR。
  • 先看:packages/spec/src/system/push-device.zod.ts(locale 一行)、packages/spec/src/api/protocol.zod.ts(RegisterDeviceRequest.locale 一行)、packages/services/service-messaging/src/keyed-notification-text.integration.test.ts(末尾新 describe)。
  • 两分钟验证:pnpm --filter @objectstack/service-messaging exec vitest run src/keyed-notification-text.integration.test.ts,预期 20/20 通过,其中 3 条是新钉的"推送按用户语言而非设备语言"。
  • 需要你做的:无,席位落地。

Fixes #29818

Clause-②: no
Metadata-protocol: none

What changes

  • UserDeviceSchema.locale (packages/spec/src/system/push-device.zod.ts): was "Device locale … that pushes to this device are rendered in". Now: stored for the device, as the registration named it; no push renders by it; a push arrives in the recipient's language (sys_user.locale, then the deployment default).
  • RegisterDeviceRequestSchema.locale (packages/spec/src/api/protocol.zod.ts): the same promise in the request contract a client sends, corrected the same way. Scope extension approved by the seat (14:22Z): the dispatch named one spec description; this twin carries the identical wording and defect, so it rides in this PR.
  • sys_user_device.locale field description (packages/services/service-messaging/src/objects/user-device.object.ts) says the same; the messaging i18n bundle's help for that field is regenerated in en (check-i18n-bundles --write) and its hand-written zh-CN / ja-JP / es-ES translations are rewritten to the new meaning (they otherwise kept translating the old promise). Declared to and approved by the seat.
  • Regenerated: content/docs/references/api/protocol.mdx, content/docs/references/system/push-device.mdx (gen:docs). No other spec artifact moved (check:generated 14/14 current).
  • Changeset: patch for @objectstack/spec and @objectstack/service-messaging (Metadata-protocol: none).
  • Not done (option 2 of the card, a widening): no push row in TEMPLATE_CHANNELS, no per-device rendering.

Tests

A new top-level describe in keyed-notification-text.integration.test.ts (the shared keyed-text harness: real ObjectQL over SqlDriver/better-sqlite3, the kernel's in-memory i18n, the real MessagingService), adding the real push channel over the real device registry (sys_user_device) on a push-service double that records each token's title and body. Each recipient's phone is registered with a locale that is not the recipient's: the zh-CN user's phone in en, the en user's phone in zh-CN, the no-locale user's phone in ja-JP.

  • the device row keeps the locale it was registered with;
  • inline fan-out: the zh-CN user's phone gets the zh-CN sentence, the en user's phone the English one, and the no-locale user's phone the deployment default's (zh-CN), not ja-JP;
  • durable path (SQL outbox + dispatcher): the same, rendered at delivery.

Measured on main (fa9c0aa) before any description change: the test was green, so the card's premise holds (pushes render by the recipient, never the device).

Acceptance notes

  • The push relay's text member (packages/spec/src/api/push-relay.zod.ts:78) also says the line is shown "in the device locale". No producer of it lives in this repository (the relay is cloud's), so nothing here was measured against it; untouched, carrier: cloud.

Verification

All readings at e541b3721 (head; origin/main was still fa9c0aa2, so the merge was a no-op).

Tests
  • pnpm --filter @objectstack/service-messaging test: Test Files 66 passed (66), Tests 815 passed (815); typecheck exit 0 (lock VERDICT command-exit 0).
  • pnpm --filter @objectstack/spec exec vitest run --maxWorkers=2: Test Files 905 passed (905), Tests 27161 passed, 1 todo (lock VERDICT command-exit 0).
  • The new pins alone: keyed-notification-text.integration.test.ts 20/20, first green on main's code before any description edit (commit 8ab75e9f).
Derived gates
  • node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack: 107 families; each run with its exit recorded before any pipe; --ran: 107 accounted, 106 run, 1 NOT-MEASURED.
  • NOT MEASURED: pnpm check:dual-build-cjs-loads (exit 3, PREREQUISITE NOT MET: needs every package's dist/; left to CI).
  • First runs of check-plugin-teardown-shape --self-test and check:skill-examples exited 3 (shallow history; @objectstack/client and client-react not built); both re-run green after the prerequisite (git fetch of the fixture commit; building those two packages).
  • pnpm --filter @objectstack/spec check:generated: all 14 artifacts current. node scripts/check-i18n-bundles.mjs --write: exit 0, moved only en.objects.generated.ts.
  • pnpm lint: exit 0.

Generated by Claude Code

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

github-actions Bot commented Nov 5, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 2 package(s): @objectstack/service-messaging, @objectstack/spec, touching 4 documentable anchor(s).

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

  • content/docs/kernel/runtime-services/push-service.mdx (via RegisterDeviceRequestSchema (symbol, a top-level const), sys_user_device (symbol, a field of const object enObjects; a field of const object esESObjects; a field of const object jaJPObjects; a field of const object zhCNObjects))
  • content/docs/plugins/packages.mdx (via sys_user_device (symbol, a field of const object enObjects; a field of const object esESObjects; a field of const object jaJPObjects; a field of const object zhCNObjects))

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

  • content/docs/releases/v18/18-3.mdx (via UserDevice (symbol, a top-level const object), sys_user_device (symbol, a field of const object enObjects; a field of const object esESObjects; a field of const object jaJPObjects; a field of const object zhCNObjects))

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 61 of 219 client-bound route-ledger rows — the other 158 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 158: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 55 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 103 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.
  • a key NAME is not a key, so the hand re-read the line above prescribes can land on the wrong schema. The same spelling is authorable on one governed type and a [REMOVED] tombstone on another for each of active, aria, joins, objects, template, tools and version (censused on [finding] tools is a key on BOTH AgentSchema (tombstoned, dead) and SkillSchema (live, cloud-attested), so a name-based search attributes skill examples to the agent key — it produced a false stop-the-line alarm on PR #19059 #19093 over the liveness ledger's governed types, top-level keys); nothing in a search result distinguishes the two, so a grep hit on a LIVE example reads as evidence about the DEAD key. Measured on fix(spec): the agent.tools liveness row says dead — it claimed live on a key the schema tombstoned #19059: content/docs/ai/agents.mdx was reported as contradicting the agent.tools tombstone over its tools: example at :161, which is inside the defineSkill({ block opened at :155 — the page was already correct. Settle ownership by PARSING the value against both schemas, never by the name: that literal PASSES SkillSchema, and as an AgentSchema it FAILS at tools with the tombstone prescription. ⛔ These names are not the whole class — a key retired through a .strict() guidance map leaves no tombstone in the walked shape and none of them here (tool.category, live as AIToolDefinition.category).

Coarse fallback — 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 c045c341329ddcd83abe4adf12428e100b7a700a → packageMentionDocs.

Which tree this was computed on

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

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

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

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:server#4 · os-bill · session_01Utp6CZMkHvuDeVYgTD73qF

CI note (seat). Test Core (6/8) on head e541b372 failed at the job's 60-minute timeout, and the Test Core aggregate failed with it. The shard's log stops at 2026-11-05T15:12Z in the middle of @objectstack/rest tests with no failing assertion, and nothing is printed for the remaining 48 minutes. The other seven shards finished in 9 to 25 minutes. This PR changes only description text, translation help and one messaging test, and nothing in @objectstack/rest, so the seat reads it as a runner stall and not this PR's. The failed jobs of run 42050862009 were re-run once through the relay (run_rerun_failed) at 2026-11-05T16:01Z. A second failure would be treated as real.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:server#4 · os-bill · session_01Utp6CZMkHvuDeVYgTD73qF

维护者速读

  1. 改了什么:推送设备 locale 的三处描述(设备 schema UserDeviceSchema、设备注册请求 RegisterDeviceRequestSchema、sys_user_device.locale 字段及四种语言帮助文字)改成如实说法:只随设备保存,推送不按它渲染,推送用收件人的语言(用户语言,其次部署默认)。
  2. 为什么改:原描述承诺「推送按设备语言渲染」,实际没有代码这样做;手机设日文、账号设英文的用户收到的是英文推送。契约说了做不到的事(ADR-0049:兑现或删掉),席位选最小的方案 1。
  3. 风险:只改描述文字和一个测试,运行行为零变化,调用方发什么照常收。回滚即 revert。
  4. 先看:packages/spec/src/system/push-device.zod.ts、packages/spec/src/api/protocol.zod.ts 的 locale 一行;packages/services/service-messaging/src/keyed-notification-text.integration.test.ts 末尾新 describe。
  5. 两分钟验证:pnpm --filter @objectstack/service-messaging exec vitest run src/keyed-notification-text.integration.test.ts,20/20 通过。
  6. 需要你做的:无,席位落地。非 BREAKING,不属 Tier H,不超 3000 行。

席位意见:ACCEPT(#29818)。推送设备「语言」字段的说明现在与实际一致:推送用收件人账号的语言,设备语言只是记录下来。行为不变,只是不再误导对接方。

席位核对

Head-sha: e541b372168ff17a3d29f8cac8879df3afb8c1ac · read 2026-11-05T16:18Z

Item Reading
Shape Draft → main via relay pr_create; Fixes #29818, Clause-②: no / Metadata-protocol: none at line start. 11 files, +91/−10 (the two content/docs/references/ pages are regenerated, not hand-edited). ⛔ ui/** untouched.
Ruling Triage 6597419465 ("enforce or remove", ADR-0049); the seat's pick, option 1; scope widened by the seat to RegisterDeviceRequestSchema.locale (claim amendment, same defect).
Seat diff read Three descriptions and four help strings now say the value is stored and that a push arrives in the recipient's language (sys_user.locale, then the deployment default). The new test drives the real push channel over the real device registry with a device locale that differs from the user's, inline and outbox paths, and was green on main code first (premise measured).
Gates (check-runs on this head) 65 completed: 57 success, 8 skipped, 0 failed. Test Core (6/8) stalled at the 60-minute timeout on attempt 1 (log stops mid @objectstack/rest, CI note 6662729629); re-run once via the relay, attempt 2 green.
Out-of-scope note push-relay.zod.ts:78 also says "in the device locale"; no producer in this repo (carrier: cloud), named in the PR's Acceptance notes.
Semver @objectstack/spec: patch, @objectstack/service-messaging: patch, Clause-②: no, Metadata-protocol: none.
Landing Not governed (check-governed-merges --pr 30544: 0 of 11 paths, re-read 2026-11-05T16:18Z), ≤ 3000 lines, not BREAKING, so per pm-dispatch the seat lands it: pr_ready + automerge_enable.

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 Fix lands in tooling, not a product package — enters pm:queue only with Unblocks: or a named surface

Projects

None yet

Development

Successfully merging this pull request may close these issues.

notifications: a push device's stored locale is never read, but the schema says pushes to it render in that locale

1 participant