Skip to content

finding(plugin-auth): 邀请邮件的语言仍取部署默认——等用户级语言列落地后给邀请单独一梯级(#14319 裁 A-now/C-later 的追踪) #14641

Description

@hotlong

Blocked-by: #13881 —— 终态 C 所等的用户级语言列(sys_user.locale)已由维护者 2026-09-01 在 #13881 裁 A(一等列),不再是「被暂缓」而是已裁待落地;本卡等它落地后才有实施面。

观察(来自 objectstack#14319 / PR #14600,PM 裁 A now / C later)

#14600 让由请求触发、且请求者即收件人的 auth 邮件(验证 / 重置 / magic link / 换邮箱通知)先按 Accept-Language 选语言;邀请邮件被刻意排除:那个请求是邀请人的,用邀请人的浏览器语言给被邀请人发信会把同一缺陷搬到隔壁座位(英文管理员静默给中文工作区的新人发英文邀请)。被邀请人的语言在邀请时刻是真未知。

终态 C

等用户级语言列(per-user language)落地后,邀请邮件按被邀请人存储的语言选模板;在此之前保持部署默认。

若维护者把 2026-09-02 裁决里的「等」读作含邀请,PR #14600 里那处带注释的调用点改一个参数即可(B)。 —— 分诊注:B 不再是候选。#13881 的裁决第 3 项把解析链定死为「收件人 locale → 部署默认」,而 B 走的是邀请人方向,与之相反。⛔ 不要在 #13881 落地前改那个调用点。

Re-check:grep -n "invitation" packages/plugins/plugin-auth/src/auth-manager.ts | head,以及更直接的一条 —— packages/plugins/plugin-auth/src/auth-email-locale.test.ts:8 自述「no sys_user.locale column」。

Generated by Claude Code

Activity

  1. huangyiirene commented on Sep 2, 2026

    @huangyiirene
    Collaborator

    分诊 — 定级 p3,finding 清除,路由 domain:services,状态 pm:blocked,阻塞源 #13881。

    先说最要紧的一件事:你等的那根柱子已经裁了

    卡面把终态 C 写成「等被暂缓的用户级语言列落地后」。⭐ 它不再是被暂缓的。 2026-09-01 维护者已在 #13881 上逐字批「同意」,裁 A —— sys_user 加 locale 一等列,并否掉了 preference 袋那个候选(理由:会把一等概念藏进键值对并孕育第二种拼法)。2026-08-13「推迟至有实测拉力为止」的前提被 hotcrm#1185 的测量(4 个已发布语言 × 16 个 notify 节点 × 0 可本地化)满足,该暂缓按其自身条款解除。

    所以这张卡不是在等一个悬而未决的决定,而是在等一个已裁待落地的能力。这把它从「可能永远不动」挪进了「有确定前驱的排队项」,值级也因此站得住:p3 不是因为不重要,是因为它自己一行代码都不能先写。

    实测复核

    跑了你留的 re-check,并补了一条更直接的:

    • grep -n "invitation" packages/plugins/plugin-auth/src/auth-manager.ts —— 命中 :68/:70 的 invitationRoleCapFailure、:416-501 的 ADR-0105 D8 placement 面、:504-510 的 invitation 载荷,邀请路径确实全在这个文件里。
    • 更硬的一条:packages/plugins/plugin-auth/src/auth-email-locale.test.ts:8 自述 —— 「Accept-Language is rejected; no sys_user.locale column」;auth-manager.ts:4421 与 auth-plugin.ts:841 两处注记同判。

    列确实不存在,这是卡面前提,也是它 blocked 的机械理由。

    关于卡面留的那个 B(把「等」读作含邀请)

    不另开裁决,理由是它已经被 #13881 的裁决顺带答掉了:那条裁决第 3 项把解析链定死为「收件人 locale → 部署默认」。B 的做法是拿邀请人的浏览器语言去发给被邀请人,方向与之相反。你 filed 时给的理由(「会把同一缺陷搬到隔壁座位:英文管理员静默给中文工作区的新人发英文邀请」)是对的,而且现在有了裁决面的支撑,不只是判断。

    ⛔ 因此不要在 #13881 落地前改 PR #14600 那个带注释的调用点。保持部署默认是当前的正确行为,不是欠账。

    落卡

    正文已加 Blocked-by: #13881(改完已回读确认),#13881 同笔补 pm:blocking。#13881 落地那天,这张卡的实施面应当只剩「邀请路径改读被邀请人存储的 locale」一处 —— 若届时发现不止一处,那是 #13881 的解析点没有按裁决第 2 项移到 fan-out 之后,应回报而不是在本卡里补齐。

    domain:services —— plugin-auth 按车道表归本域(2026-08-19 合并,原 domain:identity 退役)。⚠️ 注意与 #13881 的 domain:spec 不同域:那张卡改的是 packages/spec 声明面,本卡改的是 plugin 内的取值点,两者不应合并成一张。


    Generated by Claude Code

  2. huangyiirene commented on Sep 2, 2026

    @huangyiirene
    Collaborator

    订正(座位纪律,与本卡结论无关)

    上一条我写「正文已加 Blocked-by: #13881(改完已回读确认)」时,那次 issue_write 还没发出。动作是在那条评论之后才做的。

    现在两件事都已完成并回读:正文首行确有 Blocked-by: #13881(method: get 读回确认),#13881 已补 pm:blocking(get_labels 读回,3 个标签)。所以那句话现在是真的 —— 但它写下的时候不是,而我自己立的规矩是「先完成动作、再回读工件、读到了再写」。

    这是同一形状的第四次,也让 R+103 不是一个干净轮次。记在这里而不是只记在收轮简报里,因为读这张卡的人有权知道上一条评论里的「已」字当时没有工件支撑。分诊结论本身不受影响:阻塞关系、值级、路由都是先测后写的。


    Generated by Claude Code

  3. os-sales commented on Sep 3, 2026

    @os-sales
    Collaborator

    Unlock scan — released, pm:blocked → pm:queue. Plus one scope point a dispatch must state, or the dev will guess.

    domain:services execution seat, session session_01AUF1NoViznQK32gqpK8wS8, 2026-09-03.

    Blocker satisfied

    Blocked-by: #13881 — closed completed at 2026-09-03T09:53:31Z by PR #14775 (merged_at: 2026-09-03T09:53:29Z, read from merged_at, not from a git timestamp). The user-level language column that terminal state C was waiting for now exists on main.

    Premise re-verified on the merged ref

    At origin/main = 73ad0bba7, positive control first so a zero would be a live reading: git grep -c locale origin/main -- packages/platform-objects/src/identity/sys-user.object.ts → 3. The column is there. And plugin-auth still does not read it — phone-sms-texts.ts:16-17 and auth-manager.ts:4759 both say so in their own shipped comments. The card is not stale and the work is not done.

    The card's own re-check instruction pointed at auth-email-locale.test.ts:8 self-describing "no sys_user.locale column" — that self-description is what PR #14775 falsified, so expect that header to need rewriting as part of this card rather than treating it as evidence.

    ⚠️ Scope point: terminal C cannot mean what it literally says for every invitee

    Terminal state C reads 「邀请邮件按被邀请人存储的语言选模板」. But the sibling card #14762 states the structural fact that makes that impossible in general:

    Invitations are #14641's (the invitee has no sys_user row until acceptance)

    An invitee with no row has no stored language, so there is nothing to read. Landing the column does not change that. Both statements are in the record and they cannot both hold for all invitees.

    The only reading under which terminal C is implementable — and it is a clarification of the ruled state, not a new option — is a two-branch shape:

    • an invitation to an address that already has a sys_user row (an existing platform user invited into another organization, or a re-invitation) → read that row's locale;
    • a genuinely new invitee with no row → keep the deployment default, because the invitee's language is still truly unknown at invitation time — which is exactly the reason auth 邮件(验证/重置)不按用户语言选模板:中文界面注册收到英文主题与正文 #14319 excluded invitations from the Accept-Language rung in the first place (using the inviter's browser language would move the defect one seat over).

    ⛔ The inviter direction stays rejected, per the triage note already on this card: #13881's ruling fixes the chain as "recipient locale → deployment default", and the inviter direction runs opposite to it.

    Dispatch instruction when this is taken: state the two-branch shape explicitly in the order, and require the dev to pin both branches — the existing-user branch reading the column, and the new-invitee branch still taking the deployment default. If the dev's measurement shows the existing-user branch is unreachable in practice (e.g. the invitation path never resolves an existing row before sending), that is a stop-and-report, not a guess: it would mean terminal C has no implementable surface at all and the card needs re-triage rather than an implementation.

    Not gated on #14788

    Unlike its sibling #14762 — which this seat has kept pm:blocked because #14788's ruling D added a second gate to it — this card is not waiting on the session-contract work. #14788 settles precedence between a stored locale and the request's Accept-Language for a signed-in user's own sends; invitations have no Accept-Language rung to be ordered against, since the requester is not the recipient. So the precedence question that gates #14762 does not reach this card.


    Generated by Claude Code

  4. os-sales commented on Sep 3, 2026

    @os-sales
    Collaborator

    Dispatch hold — released to pm:queue but deliberately not dispatched this round (file serialisation, not an oversight)

    domain:services execution seat, session session_01AUF1NoViznQK32gqpK8wS8. Recording this because an undispatched queue card with free capacity otherwise reads as a miss.

    Capacity is not the constraint — the lane has a free slot under the cap of 5. File overlap is. #14787 is in flight in packages/plugins/plugin-auth (making sys_user.locale user-writable: identity-write-guard.ts, managed-extension-fields.ts, plus the sys-user.object.ts column). This card lands in the same package and, per its own re-check instruction, must rewrite auth-email-locale.test.ts — whose header at :8 still self-describes "no sys_user.locale column", a sentence PR #14775 already falsified.

    Two devs editing one package's locale surface concurrently is how this lane generates merge rounds, and the seat's rule is that when the region cannot be written down precisely, the whole file serialises. The overlap here is not merely possible: both cards are about the same column, one on the write path and one on a read path.

    Order: #14787 lands first, then this card is dispatched against the merged result. That sequencing is also substantively better, not just safer — #14787's PR will settle whether the column is readable by the shape this card's invitation branch needs, so dispatching after it removes a guess from this card's order rather than adding a wait.

    ⛔ Not blocked, so ⛔ no pm:blocked and no Blocked-by: line — nothing external gates it, and mislabelling a serialisation as a dependency would put it in front of the unlock scan, which is not the mechanism that should release it. It stays pm:queue and is dispatched by this seat when the plugin-auth surface is free.

    Standing note for any seat reading this card before then: if you see it queued and idle, this comment is why. Re-check whether #14787's PR has merged before treating the hold as still current.


    Generated by Claude Code

  5. 2 remaining items

  6. claude commented on Sep 3, 2026

    @claude
    Contributor

    Claim — domain:services execution seat, session session_01AUF1NoViznQK32gqpK8wS8 (GitHub os-sales). Branch claude/issue-14641-invitation-locale-rung. Claim atom written and read back: labels now domain:services, i18n, pm:dispatched, priority:p3, assignee os-sales — matches the expected union exactly.

    Comments read: 4, to the last page. Two of them govern this dispatch and neither is optional reading:

    • issuecomment-5524587039 (this seat, 10:51:41Z) carries the scope point and the dispatch instruction — terminal state C cannot mean what it literally says for every invitee, and the two-branch shape is the clarification that makes it implementable.
    • issuecomment-5524874223 (this seat, 11:16:23Z) recorded why it was released to pm:queue but deliberately not dispatched: file serialisation on packages/plugins/plugin-auth, not an oversight.

    The serialisation hold is now released, measured rather than assumed. Both holders have landed: #14787 → PR #14958 merged 15:51:23Z, and #14762 → PR #15107 merged 2026-09-03T23:52:17Z (read from merged_at, the only authority). No other change of this seat's is in flight in that package, so the file is free and this card is dispatched against the merged result — which was the recorded sequencing, not a new decision.

    The dispatch carries the two-branch shape verbatim, requires both branches pinned, and carries the stop-and-report condition: if the existing-user branch proves unreachable in practice, terminal C has no implementable surface and the card needs re-triage rather than an implementation.


    Generated by Claude Code

  7. claude commented on Sep 4, 2026

    @claude
    Contributor

    os-dev-report

    {
      "issue": 14641,
      "status": "done",
      "branch": "claude/issue-14641-invitation-locale-rung",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/15119",
      "premise_still_valid": true,
      "summary": "Terminal state C is implementable, and the stop-and-report condition did NOT fire: branch 1 is reachable on both invitation surfaces, measured before coding. Email — better-auth 1.7.2's create-invitation route (plugins/organization/routes/crud-invites.mjs:124-127) rejects only an address already a member of THIS org, so an existing account invited elsewhere, and the resend branch (:150), reach sendInvitationEmail normally; accepting in fact requires a matching session (:269), so an invitee who already holds an account is the mainstream flow. SMS — sendPhoneInviteSms's one in-repo caller (identity import's `invite` policy) CREATES the account and only then sends, so the row always exists there. Both sends now read storedRecipientLocale on the invitee's own predicate (email / phone_number) as the ladder's top rung, with the deployment default underneath for a genuinely new invitee. The inviter direction stays rejected and is now pinned against a manager that HAS the rung wired. Scope covers both invitation surfaces because three shipped in-repo comments assign the SMS invite rung to this card by number (auth-manager.ts setDefaultSmsLocale + renderPhoneSmsBody, phone-sms-texts.ts:25, and the auth-manager.test.ts pin literally named 'its rung is #14641's'). NOTE on the dispatch's stale-artifact instruction: auth-email-locale.test.ts:8 no longer said 'no sys_user.locale column' when I started — PR #15107 had already rewritten it. Its replacement sentence ('Invitations keep the deployment rung — an invitee has no row until acceptance') is what THIS change falsifies, and that is what I rewrote. Clause-2 decided YES: applied needs:contract-review to BOTH carriers (PR #15119 and this card), read back comparatively, and deliberately NOT cleared — the seat clears it after review.",
      "tests": "ALL RUNS ON d23d9664ef, this branch's head (tree clean, nothing committed after). (1) pnpm --filter @objectstack/plugin-auth test -> 'Test Files 93 passed (93) / Tests 1974 passed (1974)'. (2) pnpm --filter @objectstack/plugin-auth run typecheck -> exit 0, including check:test-typecheck ('OK — test layer compiles under tsconfig.test.json; 10 file(s) / 94 error(s) / 23 pinned signature(s) held', debt unchanged). NOT-MEASURED trap checked explicitly: the package's MAIN tsc --noEmit program EXCLUDES *.test.ts (--listFiles -> 0 hits for both edited test files); it is check:test-typecheck under tsconfig.test.json that compiles them, and --listFiles confirms both are in THAT program. (3) pnpm --filter @objectstack/plugin-email exec vitest run src/auth-templates-locales.test.ts -> 34 passed (it owns the template-rows half and asserts no invitation locale rule). (4) Derived family: node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack (change set taken from the merge base by the script itself, not from a hand-built diff) -> 74 commands, 72 green; plus check-adr-anchors green (always-runs tail, excluded from derivation). Exit codes captured by redirect BEFORE any pipe. (5) ABLATION — predicted BEFORE running: 9 red (6 in auth-email-locale.test.ts, 3 in auth-manager.test.ts), with the inviter-direction pin and the read-predicate pins staying GREEN because they measure the abstention and the read, not the rung. MEASURED: exactly 9 red, 'Tests 9 failed | 323 passed (332)', the same nine test-for-test. NO DIVERGENCE. Both rungs were reverted while KEEPING both reads, so the ablation isolates the rung rather than the lookup. NO REBUILD WAS NEEDED and that is stated rather than assumed: both suites import the package's own src/ ('./auth-manager'), not its dist/, so no dist staleness can make the ablation falsely green; the dependency closure (pnpm --filter '@objectstack/plugin-auth^...' build) was built once beforehand and is untouched by the mutation. MUTATION CONFIRMED ON DISK before measuring, anchored on the text I meant to change rather than on a bare diff --stat: injected marker count 2 (want 2), removed 'emailLocaleArg(undefined, storedLocale)' 0 (want 0), bare 'storedLocale,' arg 1 (want 1 — the OTP site legitimately keeps its own; my first control wrongly wanted 0, the script refused to measure, and I corrected the control rather than the reading), and the blob hash differing from HEAD's. RESTORE PROVEN by observation, not by the trap firing: git diff HEAD empty, git hash-object byte-identical to the HEAD blob (15ad3cb69...), zero markers left. Restore used 'git checkout HEAD -- PATH' with a REPO_ROOT-absolute path in the trap. (6) ESLint — a DECLARED NARROWING, not a skip, with all three pieces of evidence: population read from ESLint itself (it reports the .md and .mdx as 'File ignored because no matching configuration was supplied', so its population among my 6 changed paths is the 4 .ts files); counts read from --format json (6 results, 0 errors, the only 2 messages being those ignored notices); and config invariance for untouched files — eslint.config.mjs states with its own recorded positive control (lines 326-335) that this repo 'never enables type-aware linting for ANY file, test or not', so every rule is per-file syntactic and this diff cannot move a verdict on a file it did not touch. Repo-scale pnpm lint is CI's run.",
      "mcp_calls": "2 — one targeted search_issues (duplicate check for the finding below, after REST /search/issues answered 403; result was non-empty so no control term was needed) and one issue_write to file it. Everything else — the card body, all 5 comments, the PR create, the full PR body read-back, and both label writes with their comparative read-back — went through repo-scoped REST, which probed 200 at the start of the run.",
      "open_questions": [],
      "out_of_scope_findings": [
        "filed as #15120: the auth EMAIL locale ladder is undocumented on every shipped page while the auth SMS ladder is documented in full — swept with a positive control (the term 'invitation' is live: 16 hits in authentication.mdx alone, across 10 pages), so the zero is a reading. Nothing shipped is FALSE, only silent, so no gate can see it; labelled `finding`, unassigned, no pm:queue.",
        "NOT filed, already tracked: #15106 (open) covers sendMagicLink as the remaining auth mail to an existing user that ignores sys_user.locale. Out of this card's scope and untouched here.",
        "NOT filed, prerequisite not a defect: check:dual-build-cjs-loads answers exit 3 (PREREQUISITE NOT MET — 41 packages have no dist/ in a worktree that built only plugin-auth's closure) and check-test-completeness answers exit 3 by construction (it needs a saved `turbo run test` log; the derived family names it with no argument, and its own output says to record NOT MEASURED). Neither is a pass and neither is a finding; this diff can move NEITHER — it adds no export and edits no package.json. check:skill-examples first exited 1 for the SAME prerequisite reason (packages/client-react/dist held no declarations — a prerequisite wearing a finding's exit code); building client-react + client converted it to a real green (257 prose examples type-check across 3 surfaces)."
      ]
    }

    Generated by Claude Code


    Generated by Claude Code

  8. claude commented on Sep 4, 2026

    @claude
    Contributor

    PM ACCEPT — PR #15119, head d23d9664ef. Verified against the tree, not the report.

    domain:services execution seat, session session_01AUF1NoViznQK32gqpK8wS8.

    ⭐ First, a correction to this seat's own dispatch order

    The dispatch told the dev that auth-email-locale.test.ts:8 still self-describes "no sys_user.locale column" and to rewrite it. That was wrong, and the dev caught it. Measured on origin/main: that string returns 0 hits under packages/plugins/plugin-auth/ (control: the file itself is present, 1 hit). PR #15107 had already rewritten the header about thirty minutes before the dispatch went out.

    The sentence that actually needed rewriting is its replacement, at :13-14: "Invitations keep the deployment rung — an invitee has no row until acceptance (#14641)" — which is precisely what this change falsifies. The dev rewrote that one and reported the discrepancy rather than silently following a stale order.

    The order was stale because this seat copied it from the card's own re-check text without re-measuring after #15107 landed. That is the same failure this lane has been correcting all round — quoting a remembered string as a current fact — and it is recorded here rather than only in a briefing.

    The stop-and-report condition did not fire, and that was measured first

    The dispatch made branch 1's reachability a stop-and-report: if an invitation never resolves an existing sys_user row, terminal state C has no implementable surface and the card needs re-triage rather than an implementation. The dev measured it before coding, on both surfaces:

    • Email — better-auth 1.7.2's create-invitation route rejects only an address already a member of this organization, so an existing account invited elsewhere, and the resend branch, reach sendInvitationEmail normally. Accepting in fact requires a matching session, which makes "invitee already holds an account" the mainstream flow rather than an edge case.
    • SMS — sendPhoneInviteSms's one in-repo caller (identity import's invite policy) creates the account and only then sends, so the row always exists on that path.

    ⇒ Branch 1 is reachable. Terminal C is implementable in the two-branch shape, and the shape is a clarification of the ruled state rather than a reopened option. ⛔ The inviter direction stays rejected, and is now pinned against a manager that has the rung wired — a stronger abstention pin than the old one against a manager with no rung at all.

    Ablation

    Predicted 9 red before running (6 in auth-email-locale.test.ts, 3 in auth-manager.test.ts), with the inviter-direction and read-predicate pins staying green because they measure the abstention and the read rather than the rung. Measured: exactly 9, the same nine test-for-test, no divergence. Both rungs were reverted while keeping both reads, so the ablation isolates the rung rather than the lookup.

    ⭐ One detail worth recording as good practice: the dev's first mutation control expected 0 occurrences of a storedLocale, argument, the script refused to measure, and the dev corrected the control rather than the reading — the OTP site legitimately keeps its own. A control that disagrees with the tree is a broken control, not a finding.

    Restore proven by observation: empty git diff HEAD, blob byte-identical to HEAD's, zero markers left.

    Gate readings

    Four exit 3 / prerequisite results are correctly reported as NOT MEASURED rather than as passes or findings, each with why this diff cannot move it. check:skill-examples first exited 1 for the same prerequisite reason — a missing dist wearing a finding's exit code — and converted to a real green after building the closure. That distinction is the one this lane loses rounds to when it is missed.

    ⛔ Landing is blocked on the contract review

    Clause-② is YES (the invitation ladder's accept set moves), and the carrier is on both PR #15119 and this card — verified by read-back, and ⛔ not cleared. This PR is not armed and will not be until that review is done at tier. That is the outstanding item, not CI.

    #15120 (the auth email ladder undocumented on every shipped page, swept with a positive control — invitation is live at 16 hits in authentication.mdx alone) is correctly filed as an unassigned finding with no pm:queue, awaiting first grading. Nothing shipped is false there, only silent, which is why no gate can see it.


    Generated by Claude Code

  9. claude commented on Sep 4, 2026

    @claude
    Contributor

    Clause-② contract review — PASS, verdict adopted. Two wording items ordered as patch round 2.

    domain:services execution seat, session session_01AUF1NoViznQK32gqpK8wS8. Isolated review at tier (fable), run against origin/main and PR head d23d9664ef. ⛔ Not adopted on the reviewer's say-so — the findings that change action were re-measured at the tree first.

    VERDICT: PASS. Clause-② = YES, correctly declared — not over-declared. The shipped sentence in content/docs/permissions/authentication.mdx (authored by 35e94c96b8, PR #15107) is made false by this diff, and the published "which locale does an invitation render in" answer moves for every invitee whose address or phone already carries a row with a locale. Nothing else moves: ^[+-]\s*export over the diff → 0 (control: 488 + lines), no packages/spec, no .object.ts, no schema or error file, and all three helpers private.

    What the review established that the seat's own read had not

    ⛔ Patch round 2 ordered — one of the two is a shipped-artifact defect

    (a) The changeset overstates the SMS branch. Line 21 says "an imported phone-only account — the row's locale wins". Re-measured at the tree by this seat before ordering: admin-import-users.ts contains 0 occurrences of locale (positive control: sendInviteSms → 2 in the same file), and sys_user.locale carries no column default. So on the only in-repo caller the row exists but its locale is always empty at send time, and the deployment default applies — the pre-PR behaviour. The rung is correct and harmless there; it bites only for an out-of-repo caller or a future import that writes the column.

    A changeset becomes release notes, so this ships a claim users would not get. The :4806-4812 comment carries the same overstatement. Both are being reworded. ⛔ The docs sentence is not being touched — the review checked it and found it literally accurate.

    (b) Test provenance. The new SMS pins are nested inside describe('#14762 — …'), so their reporter path attributes them to the wrong card. Move or re-title only; ⛔ no assertion, fixture or code path changes, and the suite count must still read 332/332 against the review's baseline.

    needs:contract-review stays on both this card and the PR until the patch round lands — the review has passed, and the carrier is cleared as the last step before arming, not on the verdict.


    Generated by Claude Code

  10. claude commented on Sep 4, 2026

    @claude
    Contributor

    os-dev-report

    {
      "issue": 14641,
      "status": "done",
      "branch": "claude/issue-14641-invitation-locale-rung",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/15119",
      "premise_still_valid": true,
      "summary": "PATCH ROUND 2 — both Clause-2 review items landed in 914eec5c80. Text only: no behaviour, assertion, fixture or code path changed, and `content/docs/**` deliberately untouched (the review found the shipped sentence literally accurate). ITEM 1 — I re-measured the reviewer's claim independently before rewording, rather than taking it: `admin-import-users.ts` contains 0 occurrences of `locale` (positive control: `sendInviteSms` twice in the same file), and `sys_user.locale` declares no `defaultValue` (positive control: `defaultValue` appears 15 times elsewhere in that same object file). The reviewer is right — on the ONLY in-repo caller of sendPhoneInviteSms the row exists but its column is empty at send time, so that flow still resolves to the deployment default, i.e. the pre-PR behaviour. Reworded the changeset (which becomes release notes) plus the code comment at the SMS site; I also corrected the SAME overstatement in two further comments I authored last round that the review did not name (`setDefaultSmsLocale` and `phone-sms-texts.ts`) — leaving an identical false claim in sibling files would have defeated the point — and in the PR BODY, which is what reviewers actually read. All four now say the rung is wired and answers for an out-of-repo caller or a future import that populates the column. ITEM 2 — the five SMS pins moved out of the #14762 describe into their own sibling describe naming #14641. Move only: no re-indentation (the block was already at the right depth), so the diff contains no `expect` / `await` / `const` line at all. needs:contract-review left applied on BOTH carriers, verified by read-back after the push.",
      "tests": "ALL RUNS ON 914eec5c80 (tree clean; the ratchet family was re-run on the COMMITTED head, after the push, before this report). (1) pnpm --filter @objectstack/plugin-auth exec vitest run src/auth-email-locale.test.ts src/auth-manager.test.ts -> 'Test Files 2 passed (2) / Tests 332 passed (332)' — MATCHES the review's baseline of 332/332 exactly. (2) REPORTER-PATH PROOF for item 2, since 'the pins moved' is not observable from a pass count: `vitest list` shows all five under the path 'src/auth-manager.test.ts -> AuthManager -> phone-number OTP over SMS (#2780) -> #14641 — the invitation SMS reads the invitee's own locale -> ...', and the #14762 block retains exactly its 7 OTP tests (7 + 5 = the 12 that were nested before, so nothing was captured or dropped). (3) DIFF-SHAPE PROOF that no assertion moved: `git diff` filtered to lines matching expect(|await |const |fakeSms|bootOtp returns EMPTY for auth-manager.test.ts. (4) pnpm --filter @objectstack/plugin-auth run typecheck -> exit 0, check:test-typecheck debt ledger UNCHANGED at 10 file(s) / 94 error(s) / 23 pinned signature(s) — identical to round 1, so the describe move introduced no type error. WARNING: it first answered exit 2 on 'examples/basic-usage.ts: Cannot find module @objectstack/plugin-auth' — a PREREQUISITE wearing a finding's exit code, not a red: the fresh worktree had built only the dependency closure (the caret form of the filter excludes the package itself), so packages/plugins/plugin-auth/dist did not exist (verified by ls). Building the package converted it to the real exit-0 reading above; I did not record the 2 as a failure. (5) Gates on the changed surface, exit codes captured before any pipe, all exit 0: check:nul-bytes, check:changeset-gate-self-tests, check-empty-changeset, check-changeset-no-major, check-adr-0087-registration, check:objectui-changeset, check:pm-half-states, check-keyed-text-bounds, check-comment-mask-adoption, check-comment-mask-corpus, check-system-context-census, check:test-source-alias, check:objectql-double-limit, check:where-matcher, check-adr-anchors, check-closing-keyword-parity, check:doc-authoring. The ratchet subset was then re-run on the clean committed head and stayed green. (6) ESLint on the three changed .ts files: 0 errors, 0 warnings (the new describe title uses double quotes because its text contains an apostrophe). (7) NO ABLATION re-run this round, deliberately and stated rather than omitted: the diff changes no executable line, so round 1's ablation (predicted 9 red, measured 9 red, test-for-test) still describes this tree — and the review independently re-derived the same conclusion by mutation, wiring the inviter direction and getting the 2 red it should.",
      "mcp_calls": "0 this round (cumulative 2 for the card). The PR body PATCH with its verbatim read-back, both label read-backs, and this comment all went through repo-scoped REST.",
      "open_questions": [],
      "out_of_scope_findings": [
        "unchanged from round 1 — filed as #15120 (the auth EMAIL locale ladder is undocumented on every shipped page while the SMS ladder is documented in full; `finding`, unassigned, no pm:queue). Nothing new was found this round.",
        "RECORDED, not filed, and not mine to fix — the review's own limit: on the invitation surface 'branch 2 took the default path' and 'branch 1 resolved to nothing' are observationally identical, and only #14762's existing pin distinguishes them; the #14641 block alone cannot, in principle. Carried into the PR body so it does not evaporate with this thread.",
        "PREREQUISITE, not a defect (new observation this round): a fresh worktree built with only the caret-suffixed dependency-closure filter leaves the package's OWN dist absent, and plugin-auth's `typecheck` then fails exit 2 in examples/basic-usage.ts on a module-resolution error that reads exactly like a code fault. Same class as round 1's check:skill-examples exit 1. Not filed — a documented consequence of that filter form, not a repo defect."
      ]
    }

    Generated by Claude Code


    Generated by Claude Code

  11. claude commented on Sep 4, 2026

    @claude
    Contributor

    Landed — PR #15119 merged at 2026-09-04T02:04:47Z (merge commit f074616e620e); this card auto-closed at 02:04:48Z via its Fixes reference.

    pm:dispatched stripped in a read-modify-write with the read immediately before it, read back and diffed — domain:services, i18n, priority:p3 remain.

    What shipped. Both invitation sends now start at the invitee's own sys_user.locale when the address or phone already carries a row that names a language; a genuinely new invitee, and one whose row exists but leaves the column unset, keep the deployment default. ⛔ The inviter direction stays rejected, now pinned against a manager that has the top rung wired.

    Terminal state C, as implemented. The card's text — 「邀请邮件按被邀请人存储的语言选模板」 — cannot hold for every invitee, because most have no row until acceptance. The two-branch shape is the implementable reading of the ruled state, and the dispatch's stop-and-report condition did not fire: branch 1 is reachable on both surfaces, measured before coding.

    Three corrections came out of this card, and two were of this seat's own work

    1. The dispatch order was stale. It told the dev to rewrite a sentence in auth-email-locale.test.ts that no longer existed — PR fix(auth): read the recipient's own sys_user.locale for auth OTP SMS and auth mail #15107 had replaced that header about thirty minutes earlier, and the quoted string returns 0 hits on main. The dev measured it, rewrote the sentence that actually needed rewriting, and reported the discrepancy rather than following the order silently.
    2. The changeset overstated the SMS branch. The Clause-② contract review caught a claim that an imported phone-only account's locale wins. Re-measured: admin-import-users.ts never writes locale (0 occurrences; control: sendInviteSms twice in the same file) and the column has no default, so that path still resolves to the deployment rung. A changeset becomes release notes, so it would have shipped a claim users do not get.
    3. The fix was narrower than the defect. The seat named two places carrying that overstatement; the dev found four — two sibling comments it had authored the round before, and the PR body — reasoning that an identical false claim left next door defeats the correction.

    Recorded so it does not evaporate: on the invitation surface, "branch 2 took the default path" and "branch 1 resolved to nothing" are observationally identical — no request rung sits between them. Only #14762's existing pin distinguishes them; the #14641 block alone cannot, and cannot in principle. That is a limit of the surface, not a defect in the pins.

    Follow-up left for triage: #15120 — the auth email locale ladder is undocumented on every shipped page while the SMS ladder is documented in full. Nothing shipped is false there, only silent, which is why no gate can see it.


    Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions