feat(spec,hono-server): retire SessionUser.language — /auth/me/localization is the one read face for the user's language (#14788) - #15386
Conversation
📓 Docs Drift CheckThis PR changes 2 package(s): 28 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: ⛔ 5 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails. What this run could not see
Coarse fallback — 129 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 ce9219371a31e6b389c71bb1dc37950dcaeb616d && git checkout ce9219371a31e6b389c71bb1dc37950dcaeb616d
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 5b2ad1b41af24a9dd4bf159802dcee3d527b7db7 06f8e2268a567073a69171138ea828f5ded9af26 && git checkout -B drift-repro 5b2ad1b41af24a9dd4bf159802dcee3d527b7db7 && git merge --no-ff 06f8e2268a567073a69171138ea828f5ded9af26
node scripts/docs-audit/affected-docs.mjs --json 5b2ad1b41af24a9dd4bf159802dcee3d527b7db7
|
|
Landing provenance — Generated by Claude Code |
Fixes #14788
Implemented-by: session_01H2oQebDDxYKfWZusyd8GXk (domain:spec execution seat, os-dev subagent, CONTRACT_REVIEW_TIER — Clause ② yes: draft,
needs:contract-review, in-seat review before ready).Ruling executed
Maintainer ruling D (comment 5522880441, 2026-09-03, verbatim reply 「同意」): retire
SessionUserSchema.languageunder ADR-0049 enforce-or-remove with an ADR-0087 registration;GET /auth/me/localizationkeepslocaleand becomes the single read face for "this user's language", with the precedencesys_user.localewhen set → the request'sAccept-Language→ the deployment default. No replacement field on the session contract; no dual-spelling window.Premise re-verified on
origin/mainc4d1354e3before editing:git grep -n "language" origin/main -- packages/spec/src/api/auth.zod.ts packages/plugins/plugin-auth/src packages/rest/srchitauth.zod.ts:36once and otherwise onlyAccept-Language/ prose uses — no producer appeared. Controlgit grep -n SessionUserSchema origin/main -- packages/spec/src/api/auth.zod.ts→ 3 hits.Spec half (
@objectstack/spec, minor + BREAKING)packages/spec/src/api/auth.zod.ts:languageis aretiredKey()tombstone (the schema is a non-strictz.object, so a bare delete would strip the key silently — ADR-0104). Writing it is atscerror and a parse error carrying the prescription, onSessionUserSchemaand through both envelopes that embed it. The prescription cites the ADR only —check:doc-authoringforbids issue ids in customer-facing spec text, which the retirement playbook's guidance-string convention 2 still prescribes (see findings).entries/retired-keys/18.api__SessionUser__language.ts(api/SessionUser:languageunder major 18) +entries/semantic/18.session-user-language-retired.ts(D3 semantic entry, idsession-user-language-retired). A RESPONSE surface, so no D2 conversion — theapi/AuthFeaturesConfig:passkeysdisposition.gen:migration-registryregeneratedregistry.ts;check:generated --fixregeneratedcontent/docs/references/api/auth.mdx; the spec build wroteauthorable-surface/api.json(api/SessionUser:language [RETIRED]) andauthorable-defaults/api.json(the= "en"line gone). Liveness ledger:packages/spec/liveness/api.jsoncarries noSessionUserrows (0 hits), so no ledger edit.api/auth.test.ts: lines 48/68 now pin the ABSENCE; new describe — prescription on parse (/SessionUser\.language.*removed.*\/auth\/me\/localization/s), absence + no default minted, both envelopes refuse the key, and apackages/spec/src-scoped scan for any reader of.languageoff aSessionUser(ledger + tombstone + pin excluded with reasons).packages/spec/src, not repo-wide. A repo-wide walk from a spec test needs a declared radius inscripts/cross-package-test-inputs.mjs+ turbo.json, and apackages/**/*.tsglob would put spec's suite on every PR — the radius that gate exists to keep narrow. Beyond spec, the tombstone'sneverinput type is the enforcement (writers failtsc), and objectui was measured at zero readers at dispatch.Endpoint half (
@objectstack/plugin-hono-server, minor) — precedence + pin onlypackages/plugins/plugin-hono-server/src/current-user-endpoints.ts(the card'splugin-auth/src/current-user-endpoints.tspath is stale — this file is the handler). NewresolveSignedInUserLocale(exported, for hosts composing the endpoints directly):sys_user.locale— read under a system context by the caller's own id (thetryFindshaperesolveUserAuthzGrantsalready uses for the same row), accepted only when it passes the column's OWN shape rule: the endpoint reads theformatrule(s) on fieldlocaleoffgetSchema('sys_user').validationsand evaluatesnew RegExp(rule.regex)exactly as objectql's rule validator does on the write. No second locale parser; the one spelling staysSYS_USER_LOCALE_TAG_PATTERNon the object (pinned byte-equal to service-messaging'sLOCALE_TAG_SHAPE). Blank, malformed, or UNVERIFIABLE (no rule declared) falls through — never served.preferredLocaleFromHeader(Accept-Language)from@objectstack/spec/system— the same parse REST and the runtime dispatcher feedexecCtx.localefrom (Field validation messages are hardcoded English + API field name —penalty_amount must be ≥ 0reaches end users verbatim #3957); no third reader.resolveLocalizationContext({ ql, settings, tenantId, userId })from@objectstack/core→.locale— thelocalization.localecascadeexecCtx.localealready derives from on the dispatcher; flooren-US.currency/timezoneand the unauthenticated answer are untouched; the response shape objectui reads (json?.locale,currency) is pinned unchanged. New pin filecurrent-user-endpoints-localization.test.ts(9 cases): rung 1 over rungs 2+3, rung 2 over 3, rung 3 + floor, malformed column ⇒ next rung (four shapes), the registry's rule governs (narrowed rule moves the answer; a malformed rule is skipped, not bypassed), unverifiable column falls through, failed identity read never fails the answer, unauthenticated unchanged, system-context read asserted.makeExecutionContextResolver) assembled NO localization at all —execCtx.locale/currency/timezonewere alwaysundefinedhere, so the endpoint answeredlocale: null(andcurrency: null,timezone: null) for every authenticated caller.localeis now always a string for an authenticated caller;currency/timezoneremain as they were (null on this resolver) — out of this card's scope, filed separately (see findings).Contract text
git grep -n "localization" origin/main -- packages/spec/srchits only unrelated schemas (execution-context, settings, value-domain, etc.); the endpoint is raw-mounted and unledgered (docs/qa FOLLOW-UPS K5 records that). The contract text lives in the handler and the module header, both rewritten to saylocaleis the user's stated preference first.content/docssentences describing/auth/me/localizationas the request's / tenant's regional defaults: none found —git grep -n -i "me/localization|regional default" -- content/docs docs skillsoutsidecontent/docs/releases,content/docs/referencesanddocs/qareturns 0 hits.docs/qa/platform-checklist/areas/access-security.json:2539,2575("the resolved currency/locale/timezone", "localization rides the ExecutionContext without a setup gate") are left as-is: still accurate (locale is still a resolved value; the gate clause is unchanged), anddocs/qais checklist prose, notcontent/docs.Consumers
origin/main0b24d7f): readslocale(andcurrency) off this endpoint inapps/console/src/languageSeed.ts:84,LocalizationFetchProvider.tsx,AppContent.tsx:233; zero readers ofSessionUser.language. Response shape kept ⇒ no objectui card.Serial resync (PR #15299)
PR #15299 (#14676) merged into
main(squash13c48c2a5) and regeneratedpackages/spec/src/migrations/registry.ts. Mergedorigin/mainat27875fc7ethroughscripts/pm/os-regen-merge.sh(merge commitc13d17c2e, merge basec4d1354e3; head before the merge22710c48a, after regeneration06f8e2268), thengen:migration-registry(152 semantic / 89 retired-key / 97 retired-def), spec build,check:generated --fix(onlycheck:docsstale ⇒gen:docs),check:migration-registry✓ current. Both sides present inregistry.ts: this card's entries (3 hits) and #14676's connector entries (13 hits).Verification (all on head
06f8e2268unless stated; verdict lines quoted from the gates' own output)pnpm --filter @objectstack/spec build(underos-verify-lock.sh):VERDICT command-exit 0 · held the lock 156s.pnpm --filter @objectstack/spec exec vitest run --maxWorkers=2 src/api/auth.test.ts src/migrations/migrations.test.ts src/shared/retired-key-migrate-sentence.test.ts src/shared/alias-integrity.test.ts src/type-alias-convention.pin.test.ts src/conversions:Test Files 10 passed (10),Tests 426 passed (426),VERDICT command-exit 0.pnpm --filter @objectstack/plugin-hono-server exec vitest run --maxWorkers=2 src/current-user-endpoints-localization.test.ts src/current-user-endpoints-multi-tenant.test.ts src/hono-current-user-endpoints.test.ts src/current-user-endpoints-position-grants.test.ts:Test Files 4 passed (4),Tests 36 passed (36),VERDICT command-exit 0.pnpm --filter @objectstack/plugin-hono-server typecheck:check:test-typecheck: OK — 0 file(s) / 0 error(s),VERDICT command-exit 0.pnpm --filter @objectstack/spec typecheck:check:test-typecheck: OK,VERDICT command-exit 0.node scripts/pm/dispatch-gates.mjs --commands(derived at06f8e2268, 89 commands, every one run with its exit code captured by redirect): 84 exit 0 after serial reruns;check:doc-authoringwas red once on the tombstone's#14788and is green after the fix;check:skill-examples,check:doc-formula-expressions,check:doc-security-posturerefused until@objectstack/client-react...,@objectstack/lint...,@objectstack/formula...were built, then green (✅ 257 prose examples type-check across 3 surface(s));check:docs-audit-scope/check-affected-docswent red once while a parallel lane'scheck:authorable-surfacewas writing its gitignored tree, and are green on a serial rerun (self-test568 cases pass; bisected: every changed file applied onto pristine27875fc7epasses).pnpm check:dual-build-cjs-loads—Run pnpm build first. ⛔ This is NOT a pass: nothing was measured(needs every package built);pnpm check:type-check-debt—--re-measure cannot run: 28 workspace dependenc(ies) of the ledgered packages have no built type entry point on disk.grep -c, restore proven bygit diff HEADempty + blob hash equal toHEAD): revert the handler toc4d1354e3⇒ 7/7 precedence pins RED (expected null to be 'ja-JP'etc.); revert the tombstone ⇒ 4 spec pins RED (expected … to not have property "language",expected [Function] to throw an error). Direction as predicted in both legs.Contract flags for the in-seat review
execCtx.localeconsumers: none change meaning — the dispatcher's assembly (assemble-execution-context.ts:requestLocale ?? localization.locale) is untouched; this endpoint adds the user column ABOVE that same pair, and it never fedexecCtx.localehere anyway (see the measurement above).sys_user.locale: read undercontext: { isSystem: true }by the caller's own id — the same shape as core'stryFindfor the identical row and plugin-auth's stored-locale reader; a user-context read would route the caller's own identity row through theorg_user_idsRLS wall. Reviewer to confirm that fail direction is the intended one.formatrule(s), so no mismatch with the WRITE side. Against service-messaging's READ-sidenormalizeRecipientLocalethere is one deliberate asymmetry: that normalizer additionally refuses the literalsundefined/null;null(4 letters) is shape-legal on the write rule and would be served by this endpoint aslocale: "null"if a lossy producer stored it (undefinedis 9 letters and fails the shape). A stored"null"is a data-at-rest defect the write rule already refuses at the API; flagged, not bridged.formatrule forlocale): chosen fail-closed toward the request/default rather than trusting an unvouched value. Reviewer may prefer the other direction.Attribution note
Commits on this branch carry no
Co-Authored-Bytrailer: the harness attribution line names a model, and the dispatch forbids model identifiers in commits. Recorded here rather than silently chosen.🤖 Generated with Claude Code
https://claude.ai/code/session_01H2oQebDDxYKfWZusyd8GXk
Generated by Claude Code