Skip to content
This repository was archived by the owner on Nov 4, 2026. It is now read-only.
This repository was archived by the owner on Nov 4, 2026. It is now read-only.

Account settings: a "my language" form item for the user's own sys_user.locale #7501

Closed

Description

@os-sales

⛔ The Blocked-by: objectstack-ai/objectstack#14787 line that opened this body is removed by the domain:ui#2 execution seat at 2026-09-20T07:02Z, on landing PR objectui#10050. It had not named this card's blocker since 2026-09-05: comment 5548755854 and comment 5555380524 both recorded that #14787 had already landed and that the real blocker was the object gate (member_default declaring allowEdit: false on sys_user). That gate is now open in the packages this repo installs — the three readings are in the claim, comment 5748077210 — so there is no blocker to re-point the line at, and a line that names a non-blocker is worse than none: the unlock scan fires on it. ⭐ Recorded rather than silently deleted.

Why

The platform now has a first-class per-user notification language. sys_user.locale
(a BCP-47 tag such as zh-CN / ja-JP) is read per recipient at delivery time by the
messaging channels, with the deployment default underneath — so two people in one
deployment already receive the same notification in different languages, and nothing in
the product lets either of them choose.

A maintainer ruling on 2026-09-03 (objectstack-ai/objectstack#14787, option B) made the
column user-writable: the identity table's user-writable set grew from {name, image} to
{name, image, locale}, the column's readonly was removed, and a malformed tag is now
refused loudly by a BCP-47 shape check on the column rather than stored. The ruling names
this UI card as its other half — a setting that exists only as a database column is not a
setting.

What to build

A "Language" item in the account / profile settings surface, on the user's own record:

  • a picker rather than a free-text box. The stored value is a BCP-47 tag; the label the
    user reads should be the language's own name.
  • the offered set should come from what the deployment actually ships (the published
    locales), not a hard-coded list — an unknown-but-well-formed tag is accepted by the
    platform and simply falls back at delivery time, which is a confusing thing to let a
    picker produce.
  • clearing the value must stay possible and must read as "use the deployment default".
    That is the documented meaning of an unset column, and it is the only way back to the
    default once a choice has been made.
  • a malformed value comes back as a 400 VALIDATION_FAILED envelope with a per-field
    { field: "locale", code: "invalid_format" } entry and a localized message. Surface
    the field error on the item rather than as a bare toast.

⚠️ The write route is not settled yet — check before building

The ruling opened the COLUMN, not a self-service route, and the two are separate
decisions in ADR-0092 (D2 decides which columns, D5 leaves who to the permission layer).
As of the ruling:

  • the identity write guard now admits locale on a user-context update to sys_user;
  • but member_default still declares allowEdit: false on sys_user, so a rank-and-file
    member's PATCH to their own row is refused by the permission layer before the guard is
    consulted;
  • and better-auth's /update-user (what the existing update_my_profile action posts to)
    cannot carry the field: locale is deliberately not a better-auth additionalFields
    entry, so that endpoint does not know the column.

So a platform admin can set it today and an ordinary member cannot. Granting the member
route is a further security-boundary decision, tracked on the platform side; confirm which
route exists before wiring the item, and do not work around a 403 with a bespoke endpoint.

Acceptance

  • the item appears on the signed-in user's own account settings and nowhere else;
  • picking a language persists and survives a reload;
  • clearing it returns the account to the deployment default;
  • a refused value renders the server's per-field message on the item;
  • the item is hidden (or read-only with a reason) when the deployment's write route is not
    available to the signed-in user, rather than rendering a control that answers 403.

Generated by Claude Code


Generated by Claude Code

Activity

  1. added
    enhancementNew feature or request
    domain:uiobjectui ui stream: fix lands on the published library or apps — objectui execution seat
    on Sep 5, 2026
  2. os-zhuang commented on Sep 5, 2026

    @os-zhuang
    Contributor

    分诊 — domain:ui / priority:p3 / pm:blocked / enhancement / i18n

    锚定 (anchoring):账户/个人资料设置界面 ⇒ packages/app-shell ⇒ domain:ui。

    ⚠️ 这是本轮少见的一张产品需求卡(不是 finding、不是缺陷),由上游裁决点名要求的另一半。

    ⛔ 为什么是 pm:blocked,而且不是卡片头上写的那个 blocker

    卡片写 Blocked-by: objectstack-ai/objectstack#14787。⚠️ 但按卡片正文,那条裁决已经落地了(列开放、readonly 移除、BCP-47 形状校验上线)。真正挡路的是卡片自己在 ⚠️ 段落里写的第二件事:

    the identity write guard now admits locale … but member_default still declares allowEdit: false on sys_user, so a rank-and-file member's PATCH to their own row is refused by the permission layer before the guard is consulted; and better-auth's /update-user … cannot carry the field.

    ⇒ 今天平台管理员能设,普通成员不能。 而这张卡的验收第一条是"出现在登录用户自己的账户设置里" —— 对绝大多数用户,那个控件今天会答 403。

    真正的 blocker 是"自助写入路由"这条尚未做出的安全边界裁决(卡片说它 tracked on the platform side)。⛔ 建议接手者把 Blocked-by: 改成那张卡的编号;#14787 已经不挡了。

    Restart-when(⛔ 判据是普通成员能写,不是"裁决合并"):

    # 以一个 member_default 身份的用户登录后跑;退出 0 即解锁
    curl -sS -X PATCH "$OS_BASE/api/v1/data/sys_user/$MY_ID" \
      -H "authorization: Bearer $MEMBER_TOKEN" -H 'content-type: application/json' \
      -d '{"locale":"zh-CN"}' -o /dev/null -w '%{http_code}\n' | grep -q '^2'

    定级理由

    priority:p3:

    • 不是缺陷:没有东西坏了。今天的状态是"一个能力只以数据库列的形式存在"。
    • 卡片给的业务论据成立且具体:sys_user.locale 已经在投递时被逐收件人读取,所以同一个部署里两个人已经在收到不同语言的通知,而产品不让任何一方选择。这是真实的、已经在发生的不一致。
    • 不上 p2:有变通(管理员代设),无用户可见故障,且被上游未决裁决挡住。

    ⛔ 定型:Feature ⇒ 踩 manual floor

    新增一个用户可写的设置项 ⇒ 加宽授权面;⊕ 它坐在一条权限边界上(谁可以改自己的哪些列)⇒ 双重。⛔ 分诊席不裁决、不派单。

    ⭐ 卡片的验收清单质量很高,请原样保留

    四条里有两条特别值得点名,因为它们是最容易被做丢的:

    1. clearing the value must stay possible and must read as "use the deployment default". That is the documented meaning of an unset column, and it is the only way back to the default once a choice has been made.

      ⛔ 一个只能选、不能清的选择器会把用户永久锁在一个选择上。

    2. the item is hidden (or read-only with a reason) when the deployment's write route is not available to the signed-in user, rather than rendering a control that answers 403.

      ⭐ 这条直接对应上面的 blocker:即使解锁后,仍会有权限不足的部署/角色,控件必须能自己判断并优雅退化。

    另外两条也是硬要求:选择器的候选集来自部署实际发布的语言(不是硬编码列表 —— 因为平台会接受一个格式正确但未知的 tag 然后在投递时回落,让选择器产出那种值是误导);以及 400 VALIDATION_FAILED 的逐字段错误要渲染在表单项上,不是裸 toast。

    ⛔ 给执行席的边界

    1. ⛔ 解锁前不要开工(见上的 Restart-when)。
    2. ⛔ 不要用一个自建端点绕过 403 —— 卡片明说 "do not work around a 403 with a bespoke endpoint"。那会把一个待裁的安全边界变成一个既成事实。
    3. ⚠️ 注意 update_my_profile 这个既有 action 走的是 better-auth 的 /update-user,而 locale 刻意不是 better-auth 的 additionalFields ⇒ 那条路天生带不了这个字段。⛔ 不要试图往那个端点上加 —— 先确认平台侧最终开的是哪条路由。
    4. 选择器的语言标签用语言自己的名字(卡片要求),⛔ 不要用当前界面语言去翻译语言名("日语/Japanese/日本語"三选一时,用户要找的是第三个)。

    ⛔ 分诊席不认领、不派单、不写码、不裁决自助写入路由。


    Generated by Claude Code

  3. claude commented on Sep 5, 2026

    @claude
    Contributor

    PM 席:半状态普查 H19 在本卡上报「block 已过期」—— ⛔ 本席不放行,理由如下

    check-half-states 本轮(2026-09-05T22:5xZ,objectui 全板 146 个半状态)在本卡上报了 H19:

    pm:blocked while 1 of 1 Blocked-by: target(s) is CLOSED — the block has outlived its blocker.

    按「放行双查」第 ① 条 —— 只按最近一次转换评论里的条件放行,绝不按线程上更早的那个 blocker —— 本卡不满足放行条件。H19 读的是 body 里那个 Blocked-by: 目标;本卡真正的解锁条件写在最近一次转换评论里,是另一件事。

    本卡最近一次转换评论(os-zhuang,2026-09-05T02:31:10Z)已经预先驳回了 H19 这次触发的判据,原文:

    卡片写 Blocked-by: objectstack-ai/objectstack#14787。⚠️ 但按卡片正文,那条裁决已经落地了。真正挡路的是卡片自己在 ⚠️ 段落里写的第二件事:

    the identity write guard now admits locale … but member_default still declares allowEdit: false on sys_user,所以普通成员对自己那行的 PATCH 在权限层就被拒;而 better-auth 的 /update-user 带不了这个字段。

    ⇒ 本卡的 Blocked-by: 行指向一个已经不是 blocker 的目标,而真 blocker 从来没写进那一行 —— 这正是 H19 这条规则最容易误报的形状。

    ⚠️ 与本板上另外四张同批 H19 卡不同,本卡不在 @objectstack/spec pin 那道闸后面:它等的两件事都在 objectstack 侧的权限/认证面,与 spec 17.3.0 无关。⇒ ⛔ PR #7685 落地不会解锁本卡。

    ⭐ 可做而未做的一件事:把真 blocker 写进 Blocked-by: 行本身(现在它只活在正文的 ⚠️ 段落和这条分诊评论里)。⛔ 本席不代改卡面 —— 改 Blocked-by: 是改一张卡的机器可读状态,应由认领它的席位或分诊席在同一笔里做。


    Generated by Claude Code

  4. self-assigned this
    on Sep 20, 2026
  5. os-tesla commented on Sep 20, 2026

    @os-tesla
    Collaborator

    Claim: PM loop — 2026-09-20 shift, batch slot 3/3
    Session: session_018HrVaotisyhgmot9o2MLRq
    Branch: claude/issue-7501-my-language-profile
    Worktree: objectui-issue-7501
    Domain: domain:ui
    Seat: domain:ui#2
    File surface: apps/console/src/pages/system/ProfilePage.tsx and its tests, plus i18n keys where this repo's convention puts them, and a changeset — stop on breach and explain it in the report
    Container & model: M, mode:subagent, model: opus — ⛔ no tier output is quoted: scripts/pm/dispatch-gates.mjs REFUSES for this repo, so the tier is stated, ⛔ not fabricated
    Clause-②: no
    Thread-read: 5731280479
    Serial constraints cleared: file faces disjoint from both cards in flight (objectui#9954 packages/app-shell/src/ · objectui#7613 packages/fields/src/widgets/); the dependency note below says what this card owes them


    ⭐ The blocker this card actually had — and why it is now clear

    ⚠️ This card's Blocked-by: line has been pointing at the wrong card since 2026-09-05, and two seats said so
    at the time. 5548755854 (triage) and 5555380524 (a PM refusing an H19 release) both recorded that
    objectstack#14787 had already landed and that the REAL blocker was a different, unmade decision: member_default
    declared allowEdit: false on sys_user, so a rank-and-file member's PATCH to their own row was refused by the
    object gate before the identity guard was ever consulted. The 2026-09-18 unlock (5731280479) released the card
    on the body's line alone and said in so many words that it had ⛔ not verified the premise, handing that to the taker.

    ⇒ This is that verification, by the 消费方可安装 criterion — ⛔ not by 「上游已关」. objectui's pnpm-lock.yaml
    resolves the @objectstack/* family at 17.4.0, which is also npm view's latest for each package below.
    All three refusal layers the platform's own route contract names were read in the published tarballs:

    layer reading in the published 17.4.0 package
    ① object gate @objectstack/plugin-security ships the member-default target set with sys_user: { allowRead: true, allowCreate: false, allowEdit: true, allowDelete: false }, carrying the inline rationale 「a rank-and-file member may edit their OWN sys_user row」
    ② row scope the same package ships the sys_user_self policy at operation: "all" (using: "id == current_user.id"), with the comment that it was 「widened from select to all by the same ruling」 — while sys_user_org_members stays select-only so the write cannot spread to the org
    ③ identity guard @objectstack/plugin-auth ships SYS_USER_PROFILE_EDIT_FIELDS as {name, image, locale}, with the 2026-09-03 ruling quoted at the locale entry

    ⇒ the 403 the 2026-09-05 seats measured is gone, at the version this repo installs. ⛔ Not inferred from a closed
    issue, and ⛔ not read from objectstack's main: read from the tarballs npm view resolves.

    The premise, re-read on objectui origin/main e86445f57

    apps/console/src/pages/system/ProfilePage.tsx (427 lines) edits name and the avatar and carries zero
    locale or language controls — exactly the old {name, image} whitelist. Controls for that zero: the same file's
    name / avatar hits light up, and the bare word locale matches 830 files across packages + apps. The one
    my language hit in the repo is prose in packages/i18n/src/provider.tsx, ⛔ unrelated. ⇒ the gap is live.

    并行判据 vs 重验判据

    • Parallel (file face): apps/console/src/pages/system/ is disjoint from both in-flight surfaces ⇒ all three run at once.
    • Re-verification (dependency closure): apps/console/package.json carries "@object-ui/app-shell": "workspace:*"
      and "@object-ui/fields": "workspace:*" (control: 33 @object-ui/* entries) ⇒ this card is downstream of both.
      If either objectui#9954 or objectui#7613 lands first, this branch owes the re-verification on the merged ref; neither owes it to this one.

    NORTH-STAR clause 3, re-taken in this act at 2026-09-20T06:17Z

    objectstack 4 p0 / 45 p1 ⇒ gate CLOSED. ⛔ Not a tool card and ⛔ not a contract-hygiene card: 「客户的语言」
    is named in the north star's own definition of the product, and this is the user-facing half of a maintainer ruling.


    Generated by Claude Code

  6. os-tesla commented on Sep 20, 2026

    @os-tesla
    Collaborator

    Dispatched to an os-dev subagent (mode:subagent, model: opus) at 2026-09-20T06:17Z by the domain:ui#2 execution seat, batch slot 3/3 — the lane is now 3/3 in flight. This comment is the order's record.

    What the order binds

    • The control reads and writes the signed-in user's sys_user.locale as a BCP-47 tag; a malformed tag is refused server-side by the column's own shape rule, and the UI must do something sane with that refusal rather than swallow it.
    • Save semantics must match what ProfilePage.tsx already does for name, unless the dev can show why not.

    What the order deliberately asks the dev to DECIDE and report, rather than assume

    • Which language list the control offers — reuse of this repo's existing i18n list or a different one, with the reason. ⛔ No second source of truth if one already exists.
    • Whether the UI language and sys_user.locale are the same concept here. The dev was told to read packages/i18n/src/provider.tsx first and to ⛔ NOT silently wire one to the other if they turn out to be two things — that question goes in open_questions instead.

    Why this card was dispatchable today, when two seats had it blocked

    Its Blocked-by: line has pointed at the wrong card since 2026-09-05; the real blocker was the object gate
    (member_default.objects.sys_user.allowEdit: false). Verified cleared in this act in the published tarballs the lockfile resolves, ⛔ not from a closed issue and ⛔ not from objectstack's main — all three refusal layers (object gate · sys_user_self row scope at operation: "all" · the {name, image, locale} identity-guard whitelist) admit the write at 17.4.0. Full readings in the claim, 5748077210.

    Evidence the seat will check at review

    Same-subject control in the same command for every zero · measurement taken AFTER the last edit · red-before / green-after for every behaviour test · repo fast checks quoted with exit codes · PR stays draft · ⛔ no cross-file path:line in comments or docs · ⛔ packages/app-shell and packages/fields are out of bounds (both are in flight on other cards).


    Generated by Claude Code

  7. os-tesla commented on Sep 20, 2026

    @os-tesla
    Collaborator

    os-dev-report

    {
      "issue": 7501,
      "status": "done",
      "branch": "claude/issue-7501-my-language-profile",
      "pr": "https://github.com/objectstack-ai/objectui/pull/10050",
      "session": "session_018HrVaotisyhgmot9o2MLRq",
      "premise_still_valid": true,
      "summary": "Premise re-verified on origin/main e86445f57 before any edit: ProfilePage edited only `name` and the avatar and carried no locale/language control (control in the same command: the file's own `name` and `avatar` hits lit up). Added a Language card to ProfilePage that reads and writes the signed-in user's own `sys_user.locale` through the data adapter, plus seven `profile.language.*` keys in all ten locale packs and a minor changeset for `@object-ui/console` and `@object-ui/i18n`. The write CANNOT use the same writer as `name`: `useAuth().updateUser` is a pass-through to better-auth's `/update-user` and `locale` is not a better-auth additionalFields entry, so the card is its own card with its own submit -- the shape PasswordCard already uses in the same file for the same reason -- while keeping the form's feedback vocabulary (same Alert pair, same disabled-while-saving submit). The offered set is the i18n provider's `offerableLanguages`, the same set LocaleSwitcher renders, so no second language list exists; entries are named in their own language; a stored tag the deployment no longer publishes is shown rather than dropped. Clearing writes `null`. A `400 VALIDATION_FAILED` with a per-field `locale` entry renders on the item with aria-invalid / aria-describedby. When the field/object permission answer for `sys_user.locale` is no, the control is read-only with a reason; an unread or refused row read renders nothing. No assignee was set by me (os-tesla was already on the card from the PM claim). PR left draft; no labels written by me -- `apps`, `tests`, `package: i18n` arrived from an automation, and `needs:contract-review` is absent. Note for the seat: GitHub APPENDED its own session-URL attribution footer to the PR body on create (+90 bytes, pure addition, nothing eaten -- byte-diffed on read-back), which is the documented create-time normalisation; not repaired, since a PATCH would add a second one.",
      "tests": "All commands run from the repo root in the dedicated worktree, after the last edit they measure. (1) NEW PINS: `pnpm exec vitest run apps/console/src/pages/system/__tests__/ProfilePage.language.test.tsx` exit 0 -- `Test Files 1 passed (1)` / `Tests 7 passed (7)`. (2) REVERSE VERIFICATION, both legs, from the COMMITTED fix, each proving the mutation reached disk (occurrence counts in both directions plus a git hash-object comparison against the HEAD blob) and each restoring under a trap with `git checkout HEAD -- ABSOLUTE_PATH`, verified by an equal blob hash and an empty `git diff HEAD`. Leg A, page restored to its origin/main state (no card): exit 1, `Tests 7 failed (7)`. Leg B, write re-pointed at `useAuth().updateUser` and nothing else changed: exit 1, `Tests 3 failed | 4 passed (7)` -- the three reds are exactly the save cases, the menu / seeding / permission / hidden cases stay green. Leg B is the one that shows the assertions are about the WRITE. Both legs re-ran green afterwards. (3) AFFECTED-PACKAGE SUITES, through the shared verify lock, slot `ui-7501`: `pnpm exec vitest run packages/i18n/` exit 0, `Test Files 69 passed (69)` / `Tests 1145 passed (1145)`; `pnpm exec vitest run apps/console/` exit 0, `Test Files 103 passed (103)` / `Tests 1173 passed (1173)`. An earlier attempt at the i18n suite returned the lock's `exit 99 queue-timeout` after 540s behind another lane's app-shell run; that reading was NOT MEASURED and the slot was re-taken by name rather than re-queued. (4) TYPES / LINT: `pnpm --filter '@object-ui/console^...' build` exit 0 first (the closure was unbuilt and tsc reported 40+ bogus `Cannot find module` errors until it was built); then `pnpm --filter @object-ui/console type-check` exit 0 and `pnpm --filter @object-ui/i18n type-check` exit 0; `pnpm --filter @object-ui/i18n lint` and `pnpm --filter @object-ui/console lint` exit 0 -- `212 problems (0 errors, 212 warnings)`, and the single warning on the edited file is the pre-existing `setName`-in-effect one, so my code adds no finding. (5) GATES, each exit 0: check-i18n-call-site-keys (every new key resolves, every inline defaultValue matches its en value), check-i18n-en-drift (`0 en value(s) changed (7 key(s) added, 0 removed)`), check-i18n-dead-keys, check-i18n-designer-table-parity, check-changeset-presence (`11 source file(s) of 2 released package(s) ... declares 1 changeset(s)`), check-changeset-no-major, check-changeset-claims, check-pending-changeset-literals, check-new-cross-file-line-citations (`0 new citation(s)`), check-control-bytes (8101 tracked text files), check-vi-mock-inherit, check-vi-mock-specifiers, check-vi-mock-override-shape, check-test-path-roots, check-unreferenced-sources. One run of `node scripts/check-i18n-designer-parity.mjs` returned exit 1 with MODULE_NOT_FOUND -- a wrong script name, not a red gate; re-run under the real name `check-i18n-designer-table-parity.mjs` it is exit 0. (6) DECLARED NARROWING: the repo-wide `pnpm lint` / `pnpm test` runs were NOT taken here; the two affected packages were linted and their whole projects tested instead, which is the affected-package scope AGENTS.md defines. Repo-wide is CI's. (7) NOT MEASURED: no browser / live-stack verification was run -- no backend was started, so the 403-free write path at 17.4.0 is read from the published packages and from the permission instrument, not exercised end to end.",
      "mcp_calls": "0 -- no MCP GitHub tool was called, read or write; every GitHub read and write went through the REST proxy with curl",
      "api_writes": "2 -- POST /repos/objectstack-ai/objectui/pulls (draft PR 10050); POST /repos/objectstack-ai/objectui/issues/7501/comments (this report). No POST to /issues/7501/labels: the dispatch named no label, and AGENTS.md forbids the skip-changeset label in this repo (a real changeset is declared anyway). Two `git push` calls (the empty-branch routing probe and the two commits) are git, not REST.",
      "open_questions": [
        {
          "question": "Is the UI language and `sys_user.locale` one setting or two? Read before deciding: the i18n provider keeps the interface language in localStorage under LOCALE_STORAGE_KEY as a PER-DEVICE preference, seeded per device from the tenant's /auth/me/localization answer through cacheLanguageSeed, and switched by the globe menu; `sys_user.locale` is a PER-USER server column the messaging channels read per recipient at delivery time. Nothing in this repo reads one into the other today. I did NOT wire them, as the order required. But a user who sets their language on the profile page and sees the console stay in English will read that as a bug.",
          "options": [
            "A. Keep them separate and say so in the copy (what this PR does). Cost: zero risk, but the profile item is narrower than the word 'Language' suggests, and the description has to carry the explanation.",
            "B. Saving the profile item ALSO calls changeLanguage on this device. Cost: one-way coupling; a per-device preference now gets written by a per-user setting, and the globe menu can still diverge from it silently.",
            "C. Make sys_user.locale the source of truth for both: the globe menu writes the column and the provider seeds the UI from it. Cost: the biggest change and the one furthest outside this card -- it touches app-shell (in flight on objectui#9954) and the provider's documented seed precedence, and it makes every UI language switch a server write.",
            "D. Split the naming instead: label this item 'Notification language' and leave the globe menu as the UI language. Cost: diverges from the card's own wording ('a Language item'), but removes the ambiguity entirely."
          ],
          "recommendation": "A for this PR (it is what landed and it is honest), with D as the follow-up I would take if the maintainer agrees the word 'Language' is doing too much work. C is the only option that makes the two genuinely one setting, and it is a product decision plus an app-shell change, so it should not ride on this card."
        },
        {
          "question": "`offerableLanguages` is an INTERSECTION -- the deployment's published locales AND what this renderer can resolve. That is exactly right for a UI switcher and slightly narrow for a notification language: a locale the deployment publishes for messaging but ships no console UI pack for is a perfectly valid, non-falling-back delivery tag, and this picker will not offer it. The card asked for 'what the deployment actually ships (the published locales)'.",
          "options": [
            "A. Keep the intersection (what this PR does). Cost: a deployment whose messaging supports more locales than its console UI cannot offer those through this control; the user can still be set there by an admin.",
            "B. Offer the app's raw locale list for this control only, bypassing the renderer-resolvable filter. Cost: needs a second reader of the locales endpoint or a new provider member, i.e. exactly the second source of truth the order forbade -- unless the provider exposes the unfiltered list as well, which is a packages/i18n change."
          ],
          "recommendation": "A, and file B as a card against packages/i18n if a deployment is ever measured where the two sets differ. Today nothing measures that difference, so B would be speculative."
        }
      ],
      "out_of_scope_findings": [
        "noted, not filed: `localeLabel` (the native-name helper, with the hand-kept map for the built-in ten) is defined in app-shell's LocaleSwitcher module but is NOT on the @object-ui/app-shell entry -- only LocaleSwitcher itself is -- and that package's exports map has no subpath, so the console cannot import it. This card therefore names languages through Intl.DisplayNames rather than re-typing the ten strings. Measured in this container, Intl + capitalisation reproduces all ten hand-kept labels exactly, so the two agree today and nothing re-derives that they still will. Taker: objectui#9954 is in flight on packages/app-shell/src this round; a one-line barrel export would close it. Not filed as a card because it fits none of the three filing classes (no reproducible defect, no declared-contract violation, no metadata trap) -- if the seat disagrees, dedupe words: localeLabel, app-shell barrel export, native language name, LocaleSwitcher, console reuse.",
        "noted, not filed: this card's `Blocked-by:` line still names a platform card that stopped being the blocker on 2026-09-05; two seats recorded that at the time and the 2026-09-18 unlock released it on that stale line. Not a code defect and not mine to edit -- the card face belongs to the triage seat or the seat landing this PR. Taker: the seat that lands PR 10050.",
        "noted, not filed: under the DOM environment these console tests run in, `userEvent.selectOptions` leaves a controlled select's value unchanged and a `userEvent.click` on a submit button never reaches the form's onSubmit. Both were measured here and both fail SILENTLY -- the assertion downstream just reads the unchanged state. This repo's other form tests already use fireEvent for exactly this, so it is a known shape rather than a new one; noted because a reader of my test file will wonder why fireEvent. Taker: none -- it is a property of the environment, not of any file a PR will touch.",
        "PR-BODY CORRECTION for the seat (dev writes the body once and does not PATCH): the body's 'Not measured here' paragraph says the two project suites were not run because the verify lock timed out. That was true when the PR was opened and is now stale -- the slot was re-taken and both ran green afterwards: packages/i18n 69 files / 1145 tests, apps/console 103 files / 1173 tests, both exit 0. The paragraph should be replaced with those two readings, leaving the browser/live-stack line as the only genuinely unmeasured item."
      ]
    }

    Generated by Claude Code


    Generated by Claude Code

  8. os-tesla commented on Sep 20, 2026

    @os-tesla
    Collaborator

    ACCEPTED on review — PR objectui#10050, head dfb29eb68. Reviewed by the domain:ui#2 execution seat (PM session session_018HrVaotisyhgmot9o2MLRq) at 2026-09-20T07:04Z. Implemented-by: os-dev subagent · Reviewed-by: this seat. Landing armed: flipped ready early, auto-merge (squash) enabled. ⛔ No approval submitted, ⛔ no hand merge.

    What the seat verified FIRST-HAND, ⛔ not on the dev's word

    ① The load-bearing claim — 「the write CANNOT use the same writer as name」. Read on origin/main: packages/auth/src/createAuthClient.ts's updateUser is await betterAuth.updateUser(userData) — a thin pass-through — and packages/auth/src/AuthProvider.tsx hands that straight through. objectstack's own platform notes say in so many words that better-auth has no locale on its user model. ⇒ a locale written through updateUser would have nowhere to land. The separate card with its own submit is ⛔ not a style choice; it is the only shape available, and it is the one PasswordCard already uses in this same file for the same reason.

    ② 「no second language list」. offerableLanguages is a member of packages/i18n's provider, and packages/app-shell's LocaleSwitcher is its other reader. This card reuses it ⇒ one source of truth for the offered set, as the order required.

    ③ The dev's own caveat about that set is accurate, and I re-derived it. The provider builds offerableLanguages as appLocales.filter(canResolveLanguage) — an intersection of the deployment's published locales with what this renderer can resolve, with the built-in packs as the fallback when the app answers nothing resolvable. ⇒ a locale a deployment publishes for messaging but ships no console pack for is a valid delivery tag this control will not offer. Recorded here as a known, measured narrowing; ⛔ not filed as a card, because nothing today measures a deployment where the two sets differ.

    ④ Scope. 13 files, all inside the declared surface (ProfilePage.tsx, its new pins, ten locale packs, one changeset). check-governed-merges.mjs re-run on the FINAL file list: 0 of 13 paths hit the register ⇒ NOT governed.

    What the seat did ⛔ NOT verify

    • ⛔ Did not re-run the suites: the merge queue's run on the merge commit is the reading that gates the landing.
    • ⛔ Did not exercise the write end to end. The dev says so too, and it is the honest limit: no backend was started, so 「a member may now write their own locale」 rests on the published packages (plugin-security's member-default set and sys_user_self at operation: "all", plugin-auth's {name, image, locale} whitelist) and on the permission instrument — ⛔ not on a live 200.

    Three things the seat carried, so nothing lives only in a hand-back

    1. The residual question is now a card: objectui#10051 — 「is the UI language and sys_user.locale one setting or two?」, in the decision inbox with its 维护者速读 and four-facet block. ⭐ Per the half-state patrol's H52 remedy this is a new card, ⛔ never a needs-user-decision re-hung on this one. ⚠️ This seat's recommendation there is C, which differs from the dev's A — derived from facet ① (the two-year platform shape) rather than from this PR's scope, and said so on the card.
    2. This card's stale Blocked-by: line is gone. It had named a non-blocker since 2026-09-05; the body now carries the record of why, and ⛔ nothing was silently deleted.
    3. The PR body's 「Not measured here」 paragraph was replaced by this seat, because the dev writes that body once and does not PATCH it: the verify-lock timeout it described was superseded when the slot was re-taken and both suites ran green (packages/i18n 69 files / 1145 tests, apps/console 103 files / 1173 tests). ⚠️ Measured while doing it: a PR-body PATCH makes the platform append a second attribution footer (+58 bytes, pure addition, nothing eaten, diffed line by line) — which is exactly why the dev's 「write once, never PATCH」 rule exists.

    Generated by Claude Code

  9. removed their assignment
    on Sep 20, 2026
  10. os-tesla commented on Sep 20, 2026

    @os-tesla
    Collaborator

    LANDED and content-verified on the merged ref. domain:ui#2 execution seat, 2026-09-20T08:06Z.

    reading value
    merge PR objectui#10050 merged 2026-09-20T07:31:56Z
    ⭐ healthy, ⛔ not an ejection merged and removed_from_merge_queue carry the same second
    ⭐ squash, confirmed by artefact c2f0f4832 has one parent (23b99585f) and the (#10050) title suffix — ⛔ not read from the API field

    The content reading, control lit on BOTH refs

    ref subject: the locale write (update('sys_user'…) subject: the new en.ts keys control: setName in ProfilePage.tsx
    23b99585f (the ref this landed on) 0 0 3
    c2f0f4832 (merged) 1 2 3

    ⇒ both subjects moved while the control held at 3 on both refs — the existing name card is untouched, and the language card writes through the data adapter rather than through updateUser, exactly as reviewed.

    State closed out in the same act: pm:dispatched stripped, assignee released; domain:ui · priority:p3 · enhancement · i18n stay.

    Still open behind this card, ⛔ not dropped

    • objectui#10051 — the decision this card's residual raised: is the UI language and sys_user.locale one setting or two? In the inbox with its 维护者速读 and four-facet block; this seat recommends C, the dev recommended A, and the disagreement is on the card rather than hidden.
    • The stale Blocked-by: line on this card's body was removed while landing, with the record of why kept in place of it.

    Generated by Claude Code

  11. added a commit that references this issue on Sep 28, 2026
    c2f0f48
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    domain:uiobjectui ui stream: fix lands on the published library or apps — objectui execution seatenhancementNew feature or requesti18npriority:p3

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions