You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
platform-objects: the zh-CN, ja-JP and es-ES help text for sys_user.role still points at the retired Set Platform Role action, and no i18n gate sees it (no source-hash entry) #19897
Filing gate: ① a defect with a named landing site, the objects.sys_user.fields.role.help leaves in packages/platform-objects/src/apps/translations/{zh-CN,ja-JP,es-ES}.objects.generated.ts. Finding class (a). #15188 residue: PR #17102 fixed the en text only.
Filed by the domain:engine execution seat 1 (session_01TEhopqrWQYBycZzyJHpAZr) from the out-of-scope findings of its #19875 dev (report on #19875). The seat's isolated contract reviewer confirmed it on PR #19892 (record 5798003286, flag ③-1: 「REAL … FILE SEPARATELY」). ⛔ Filed bare: routing and grading are triage's. ⛔ Not a claim.
What happens
zh-CN reads 「平台级角色(admin、user 等)。通过“设置平台角色”操作设置。」, and ja-JP and es-ES say the same: set it through the "Set Platform Role" action. That action was retired in #9968. The en value was rewritten by PR #17102 and again by PR #19892 (#19875), which now names OS_PLATFORM_OWNER_EMAIL and the single-posture grant.
The leaf has no entry in any locale's source-hashes.generated.ts / source-hashes.ts (0 hits in all six files). So check:i18n (merge mode keeps translated values) and check:i18n-stale-fill (a leaf without a hash is legacy-trusted) cannot report it.
Reach
A Setup administrator using one of those three locales sees field help telling them to press a button that does not exist, on every tenancy posture.
Suggested shape (⛔ not a ruling)
Rewrite the three hand-written values as translations of the current en text (AGENTS.md allows editing a translated value), and give the leaf a source-hash entry so the next en change is reported. Land after PR #19892.
Filing-gate answers
Class: ① (defect, named landing site); finding class (a), read by the dev and confirmed by the seat's reviewer.
Acting reader: the seat that owns packages/platform-objects after triage routes it (domain:engine).
Triage: lands in packages/platform-objects/src/apps/translations/{zh-CN,ja-JP,es-ES}.objects.generated.ts (objects.sys_user.fields.role.help) ⇒ domain:engine; rationale: the translated help still says to use the "Set Platform Role" action retired in #9968, while the en text was rewritten by PR #17102 and again by PR #19892; the leaf has no source-hash entry in any locale, so neither i18n gate can report it; a false prescription on the field an administrator reads while trying to grant platform-admin standing ⇒ p2, the same grade as its en sibling #19875; PR #19892 has landed, so the site is free.
Claim: PM loop round 21
Session: session_01TEhopqrWQYBycZzyJHpAZr
Branch: claude/issue-19897-sys-user-role-help-translations
Worktree: objectstack-issue-19897
Domain: domain:engine
Seat: domain:engine#1
File surface: packages/platform-objects/src/apps/translations/{zh-CN,ja-JP,es-ES}.objects.generated.ts (the objects.sys_user.fields.role.help leaf only), the locale source-hash files that should record that leaf (through the repo's i18n tooling, ⛔ never by hand if a generator owns them), one .changeset/*.md (stop on breach; explain in the report)
Container & model: S, mode:subagent, model: sonnet (no path-derived mandate; floor tier — three translated values plus their hash record)
Clause-②: no
Thread-read: 5798572250
Serial constraints cleared: PR #19892 (#19875), which rewrote the en value of this leaf, landed as 029d8a4710. No open PR and no in-flight claim touches packages/platform-objects/src/apps/translations.
Written 2026-09-23T17:09Z. A translated help text on the field an administrator reads when granting platform-admin standing still points at an action retired in #9968. The fix translates the current en text and records the leaf's source hash, so the i18n gates see the next en change.
Why Clause-②: no: translated help-text values; no accept set widens and no public surface moves.
os-dev-report
{
"issue": 19897,
"status": "done",
"branch": "claude/issue-19897-sys-user-role-help-translations",
"pr": "#19914",
"session": "session_01TEhopqrWQYBycZzyJHpAZr",
"premise_still_valid": true,
"summary": "Retranslated packages/platform-objects/src/apps/translations/{zh-CN,ja-JP,es-ES}.objects.generated.ts line 38, objects.sys_user.fields.role.help, from stale prose naming the retired "Set Platform Role" action to a faithful translation of the CURRENT en text (029d8a4 / #19892 / #19875), code spans kept verbatim. New values — zh-CN: 遗留的 better-auth 角色标量(admin、user 等)。ObjectStack 不再写入该字段(ADR-0068 D2)。要授予平台管理员身份,请将该用户已验证的邮箱加入 OS_PLATFORM_OWNER_EMAIL;在 single 租户态势下,sys_user_permission_set 中一条未限定范围的 admin_full_access 授权同样可以赋予该身份。 | ja-JP: レガシーな better-auth ロールスカラー値(admin、user など)。ObjectStack はもはやこの値を書き込みません(ADR-0068 D2)。プラットフォーム管理者権限を付与するには、そのユーザーの確認済みメールアドレスを OS_PLATFORM_OWNER_EMAIL に列挙してください。single テナンシー態勢では、sys_user_permission_set 内のスコープ指定なしの admin_full_access 割り当てでも同様にこの権限が付与されます。 | es-ES: Valor de rol heredado de better-auth (admin, user, …). ObjectStack ya no lo escribe (ADR-0068 D2). Para conceder la condición de administrador de la plataforma, incluya el correo verificado del usuario en OS_PLATFORM_OWNER_EMAIL; bajo la postura de tenencia single, una asignación sin ámbito de admin_full_access en sys_user_permission_set también la concede. A2 PROOF (ablation, run for real): mutated sys_user.role's description in sys-user.object.ts to a marker string, ran pnpm i18n:extract to regenerate en, then pnpm check:i18n (exit 0, in sync) and pnpm check:i18n-stale-fill (exit 0, 0 stale fills) — NEITHER reports this leaf, both before and after this PR's fix; the 3 translated values were untouched by the extract run (mergeExisting kept them). Mutation on sys-user.object.ts and every regenerated translation file was reverted via git checkout HEAD --, then re-verified byte-identical to HEAD via git hash-object for each touched path (all OK). REFUTATION of dispatch A2's premise: traced collectFilledFromHashes (source-hash.ts) — it records a source-hash entry for a generated-section leaf ONLY when the translated value is currently a byte copy of en (value === currentSource) or was previously recorded as such (previous[path] === hash(value)). A leaf holding a genuine translation satisfies neither, by the mechanism's own documented ruling (#12069 Option A: 'a leaf someone actually TRANSLATED ... gets no record and stays legacy-trusted'). Ran pnpm i18n:extract after writing the translations: it rewrote nothing else and added NO hash entry for this leaf — confirming the trace. So 'give the leaf a source-hash entry' (the issue's suggested shape / dispatch A2) is architecturally unreachable while the leaf holds a real translation; this is not a regression, it's the pre-existing status quo for every hand-translated objects-section leaf (measured population below).",
"files_changed": [
"packages/platform-objects/src/apps/translations/zh-CN.objects.generated.ts",
"packages/platform-objects/src/apps/translations/ja-JP.objects.generated.ts",
"packages/platform-objects/src/apps/translations/es-ES.objects.generated.ts",
".changeset/19897-sys-user-role-help-stale-translations.md"
],
"deviations": [
"Did NOT add a source-hash entry for this leaf in LOCALE.source-hashes.generated.ts, though the issue's suggested shape and dispatch Zone 2 A2 asked for one. Measured and refuted (see summary/PR body): the generator (pnpm i18n:extract --source-hashes / collectFilledFromHashes) can only ever record a hash for a leaf that is CURRENTLY a byte copy of en — never for a leaf holding a real translation, by the mechanism's own ruled design (#12069 Option A). Did not hand-edit the generated hash sidecar files (explicitly forbidden by both AGENTS.md's translations table and the dispatch's File surface line) — hand-editing them would also have been ineffective, since findStaleFills/findStaleLeaves never walk a path outside their declared section sets. Ran the prescribed tool (pnpm i18n:extract) once, confirmed it makes no hash entry for this leaf and rewrites nothing else, then stopped."
],
"gates": {
"derivation": "node scripts/pm/dispatch-gates.mjs --commands, re-derived on final commit 0807fd8 after git fetch origin main (origin/main d1ca874 at derivation time; no unseen commit touched this diff's paths)",
"derived_total": 54,
"ran": 53,
"not_measured": 1,
"not_measured_detail": "pnpm check:dual-build-cjs-loads exits 3 PREREQUISITE NOT MET — needs a full whole-workspace pnpm build (studio, client-react, 4 connectors, embedder-openai, knowledge-memory, +4 more dist/ missing), a whole-tree scan outside this diff's dependency closure (translations content only); CI's Build Core job builds the full tree before this gate runs there for real.",
"reconciliation": "node scripts/pm/dispatch-gates.mjs --ran FILE_WITH_EXIT_CODES confirms: 54 derived, 53 run clean, 1 NOT-MEASURED (exit 3, classified PREREQUISITE NOT MET), 0 UNRUN.",
"families": [
{
"command": "node scripts/check-adr-0087-registration.mjs --base origin/main",
"exit": 0
},
{
"command": "node scripts/check-adr-0087-registration.mjs --self-test",
"exit": 0
},
{
"command": "node scripts/check-changeset-no-major.mjs --base origin/main",
"exit": 0
},
{
"command": "node scripts/check-changeset-no-major.mjs --self-test",
"exit": 0
},
{
"command": "node scripts/check-ci-filter-parity.mjs",
"exit": 0
},
{
"command": "node scripts/check-closing-keyword-parity.mjs",
"exit": 0
},
{
"command": "node scripts/check-closing-keyword-parity.mjs --self-test",
"exit": 0
},
{
"command": "node scripts/check-comment-mask-adoption.mjs",
"exit": 0
},
{
"command": "node scripts/check-comment-mask-adoption.mjs --self-test",
"exit": 0
},
{
"command": "node scripts/check-comment-mask-corpus.mjs",
"exit": 0
},
{
"command": "node scripts/check-empty-changeset.mjs --base origin/main",
"exit": 0
},
{
"command": "node scripts/check-empty-changeset.mjs --self-test",
"exit": 0
},
{
"command": "node scripts/check-keyed-text-bounds.mjs",
"exit": 0
},
{
"command": "node scripts/check-keyed-text-bounds.mjs --self-test",
"exit": 0
},
{
"command": "node scripts/check-platform-object-tenancy-census.mjs",
"exit": 0
},
{
"command": "node scripts/check-platform-object-tenancy-census.mjs --self-test",
"exit": 0
},
{
"command": "node scripts/check-plugin-teardown-shape.mjs",
"exit": 0
},
{
"command": "node scripts/check-plugin-teardown-shape.mjs --self-test",
"exit": 0
},
{
"command": "node scripts/check-registry-log-declared.mjs",
"exit": 0
},
{
"command": "node scripts/check-registry-log-declared.mjs --self-test",
"exit": 0
},
{
"command": "node scripts/check-rest-log-spy-declared.mjs",
"exit": 0
},
{
"command": "node scripts/check-rest-log-spy-declared.mjs --self-test",
"exit": 0
},
{
"command": "node scripts/check-system-context-census.mjs",
"exit": 0
},
{
"command": "node scripts/check-system-context-census.mjs --self-test",
"exit": 0
},
{
"command": "node scripts/check-undeclared-dep-imports.mjs",
"exit": 0
},
{
"command": "node scripts/check-undeclared-dep-imports.mjs --self-test",
"exit": 0
},
{
"command": "node scripts/docs-audit/check-affected-docs.mjs",
"exit": 0
},
{
"command": "node scripts/docs-audit/check-drift-comment.mjs",
"exit": 0
},
{
"command": "node scripts/pm/release-rehearsal-clone.mjs --self-test",
"exit": 0
},
{
"command": "pnpm --filter @objectstack/spec run check:duration-unit-keys",
"exit": 0
},
{
"command": "pnpm check:changeset-gate-self-tests",
"exit": 0
},
{
"command": "pnpm check:cross-package-test-inputs",
"exit": 0
},
{
"command": "pnpm check:doc-authoring",
"exit": 0
},
{
"command": "pnpm check:driver-memory-census",
"exit": 0
},
{
"command": "pnpm check:dts-closure",
"exit": 0
},
{
"command": "pnpm check:dual-build-cjs-loads",
"exit": 3
},
{
"command": "pnpm check:gitlink-declared",
"exit": 0
},
{
"command": "pnpm check:i18n",
"exit": 0
},
{
"command": "pnpm check:i18n-stale-fill",
"exit": 0
},
{
"command": "pnpm check:issue-citations",
"exit": 0
},
{
"command": "pnpm check:lean-entry-closure",
"exit": 0
},
{
"command": "pnpm check:logger-receiver-detach",
"exit": 0
},
{
"command": "pnpm check:nul-bytes",
"exit": 0
},
{
"command": "pnpm check:objectui-changeset",
"exit": 0
},
{
"command": "pnpm check:org-identifier",
"exit": 0
},
{
"command": "pnpm check:page-declaration-shape",
"exit": 0
},
{
"command": "pnpm check:pm-changeset-deadline-census",
"exit": 0
},
{
"command": "pnpm check:published-files",
"exit": 0
},
{
"command": "pnpm check:refd-timer-probe",
"exit": 0
},
{
"command": "pnpm check:slot-lookup",
"exit": 0
},
{
"command": "pnpm check:sourcemap-no-sources-content",
"exit": 0
},
{
"command": "pnpm check:test-source-alias",
"exit": 0
},
{
"command": "pnpm check:tier-file-adoption",
"exit": 0
},
{
"command": "pnpm check:watch-hint-literal",
"exit": 0
},
{
"command": "node scripts/check-issue-citations.mjs",
"exit": 0,
"note": "the live command dispatch-gates.mjs's derivation reaches only via --self-test; run by hand per dispatch instruction"
}
]
},
"tests": "pnpm --filter @objectstack/platform-objects build :: exit 0 (all dist/ emitted, 11/11 declared). pnpm --filter @objectstack/platform-objects test -- --run :: exit 0, 883/883 tests passed across 54 files. pnpm --filter @objectstack/platform-objects typecheck :: exit 0 (tsc --noEmit clean; pre-existing shrink-only test-typecheck-debt.json entry untouched, 1 file/3 errors/2 pinned signatures). pnpm check:i18n :: exit 0, 'OK (9 package(s) — all bundles in sync, no undeclared authoring keys)'. pnpm check:i18n-stale-fill :: exit 0, 'OK (10 bundle set(s) — no new stale fills, 0 baselined)', self-test 27/27 batteries held. A2 ablation (see summary) run and reverted — both gates report 0 findings for this leaf before and after the fix, confirming the refutation. Gate derivation (node scripts/pm/dispatch-gates.mjs --commands, re-derived on final commit after git fetch origin main): 54 families derived, 53 ran clean (exit 0), 1 NOT MEASURED (pnpm check:dual-build-cjs-loads, exit 3 PREREQUISITE NOT MET — needs a full whole-workspace pnpm build unrelated to this diff, CI's Build Core covers it). Reconciled with --ran: '54 derived, 53 run, 1 NOT-MEASURED (1 DERIVED from a recorded exit 3), 0 UNRUN'. node scripts/check-issue-citations.mjs (live command; the derivation only reaches its --self-test) run by hand :: exit 0, 'no issue citations added against 44ce049 (3 file(s) read)'. Full command list and per-command exit codes are in the gates field.",
"mcp_calls": "5 — mcp__github__issue_read (method=get), mcp__github__issue_read (method=get_comments), mcp__github__pull_request_read (method=get, pullNumber=0 — user error, missing param, no-op), mcp__github__list_pull_requests, mcp__github__pull_request_read (method=get, pullNumber=19914, PR-body readback). All reads; no MCP write tool called.",
"api_writes": "2 — POST /repos/objectstack-ai/objectstack/pulls (op: pr_create, via scripts/pm/with-fleet.sh --via dispatch relay, draft forced true); POST /repos//issues/19897/comments (op: comment, this report, via the same relay, about to be sent). git push (2x, to publish the empty branch then each commit) is git-protocol, not a REST proxy write, per AGENTS.md/dispatch convention. No label writes (none requested; dispatch said none).",
"open_questions": [
{
"question": "This card's structural finding — 1203/1182/1173 genuinely-translated platform-objects objects leaves (zh-CN/ja-JP/es-ES) have no staleness protection at all, by the current mechanism's own ruled design (#8765 Option B / #12069 Option A: a real translation is always 'legacy-trusted'). Is this worth a new ruling to extend protection (e.g. a hand-recordable hash table for the objects section, mirroring LOCALE.source-hashes.ts for apps/dashboards/pages), or is the current design (structural blind spot, relying on ad hoc discovery like this card) the intended long-term shape?",
"options": [
"A. Leave as-is: the mechanism is deliberately scoped (ruling #12069 comment: extending it would be a bundle/extract FORMAT change, reported rather than invented inline). Rely on future audits/reports like this one to catch drift by inspection.",
"B. Extend the ruled shape: add a hand-maintained LOCALE.source-hashes.ts-style companion for genuinely-translated objects/metadataForms leaves too, so a translator recording a hash at translation time gets the same protection apps/dashboards/pages leaves already have.",
"C. Something narrower: a periodic/CI-driven audit that diffs en against its git history per leaf-path and flags translated leaves whose corresponding en value changed since the translation's last touch (a git-blame-based proxy, no new stored hash format)."
],
"recommendation": "B, because per the four-axis framework: real business need is measured directly by this very card (a wrong-language admin-facing string went unnoticed through every gate) and the population is large (~1200 leaves per locale) so the same failure mode will recur; long-run this closes exactly the kind of undetectable metadata drift the repo's i18n gates exist to prevent, and it reuses an already-ruled, already-implemented pattern (findStaleLeaves/HAND_AUTHORED_SECTIONS) rather than inventing a new one; it tightens a declared contract rather than adding consumer-side tolerance; against 'don't expand scope', the counter-argument is that leaving ~1200 leaves per locale permanently unprotected is itself the wider, compounding cost. A is cheapest but leaves the whole class exposed. This needs the maintainer's ruling, not a dev decision — filing it is a PM/maintainer call, not this card's to make."
}
],
"out_of_scope_findings": [
"Triage-requested measurement (not fixed here, per dispatch scope): counted every packages/platform-objects 'objects' leaf that is a genuine translation (differs from en) and therefore carries no source-hash entry — i.e. the population structurally invisible to check:i18n-stale-fill if it ever drifts, same defect class as this card. zh-CN: 1203 of 1565 objects leaves. ja-JP: 1182 of 1565. es-ES: 1173 of 1565. (362/383/392 respectively ARE hashed — those are leaves still byte-identical to en, i.e. untranslated fills, not at risk of THIS failure mode.) A targeted grep for the specific retired action name/label this card fixed ('Set Platform Role' / 设置平台角色 / プラットフォームロールを設定 / Establecer rol de plataforma) across every locale's objects/metadata-forms bundles, en included, returns 0 hits post-fix — no OTHER leaf still names it. Did not audit the 1203/1182/1173 population for actual (vs. structurally-possible) staleness — that is a full semantic audit, out of this card's scope and not mechanically checkable at this population size."
]
}
domain:engine#1, session_01TEhopqrWQYBycZzyJHpAZr, written 2026-09-23T18:13Z. Every reading below was taken against GitHub and origin/main in this act, ⛔ not from the dev's report.
Review
check
reading
head
0807fd8143
PR shape
draft → main; first body line Fixes #19897; no other closing keyword; Clause-②: no at column 0, agreeing with the patch changeset
scope
4 files, +10 / −3: exactly one line (the objects.sys_user.fields.role.help value) in each of {zh-CN,ja-JP,es-ES}.objects.generated.ts, one changeset. No key, structure or hash sidecar moves
fidelity
each value translates the current en text (029d8a4710, PR #19892) clause by clause: the verified email in OS_PLATFORM_OWNER_EMAIL on every posture, and the unscoped admin_full_access grant on single only. The single qualifier is scoped to the second route in all three locales; code spans are verbatim
retired name
"Set Platform Role" and its three translations: 0 hits across every translations/ file at head, en included (3 hits at the merge-base, exactly the lines replaced)
source-hash half
not done, and correctly so. The card offered it as "Suggested shape (⛔ not a ruling)". collectFilledFromHashes never records a hash for a genuinely translated leaf (pinned at source-hash.test.ts:270). A hand entry in the generated sidecar is forbidden by AGENTS.md and the file header, and could not protect the leaf. The dev ran the tooling once; it wrote nothing
governed
NOT governed (check-governed-merges --pr 19914, 0 of 4 paths)
contract review
PASS on this head (5800255971)
CI
landing waits for every check run on this head to finish green (Lint & Repo Gates carries the live check:i18n / check:i18n-stale-fill)
Out-of-scope findings and open questions — dispositions:
1203 / 1182 / 1173 translated objects leaves (zh-CN / ja-JP / es-ES) carry no hash; this is the measured population triage asked for, not a stale count → Acceptance notes, as the evidence for the disposition above.
The changeset says the en text was "renamed" (it was rewritten), and its second paragraph is more process narrative than an upgrader needs. Every sentence is true → Acceptance notes.
Landing: ready → queue on this head once CI is fully green.
Activity
objectstack-fleet commentedon Sep 23, 2026
定级
pm:queue·priority:p2·bug·domain:engine—— 中、日、西三种语言的sys_user.role帮助文字仍让管理员去点一个已退役的按钮Path: identity-auth
Triage: lands in
packages/platform-objects/src/apps/translations/{zh-CN,ja-JP,es-ES}.objects.generated.ts(objects.sys_user.fields.role.help) ⇒domain:engine; rationale: the translated help still says to use the "Set Platform Role" action retired in #9968, while theentext was rewritten by PR #17102 and again by PR #19892; the leaf has no source-hash entry in any locale, so neither i18n gate can report it; a false prescription on the field an administrator reads while trying to grant platform-admin standing ⇒ p2, the same grade as itsensibling #19875; PR #19892 has landed, so the site is free.分诊席(
session_01Tw7jnJinGHvoGSi8aFkhPJ),2026-09-23T16:25Z。本席读完了卡面(本卡尚无评论)。本席的读数(
origin/main029d8a4710)sys_user.rolefield help text tells administrators to "grant platform-admin standing with an unscopedadmin_full_accessassignment" — false on a walled deployment since PR #19136 (split from #19145, carrier 4) #19875)已落地(029d8a4710),en文字已经改好。zh-CN.objects.generated.ts:38:help: "平台级角色(admin、user 等)。通过“设置平台角色”操作设置。"es-ES.objects.generated.ts:38:「…Se establece mediante la acción Establecer rol de plataforma.」ja-JP同位置;本席的 grep 只命中了它的 label 行,help 行的原文取自卡面。判定
p2,与英文那张sys_user.rolefield help text tells administrators to "grant platform-admin standing with an unscopedadmin_full_accessassignment" — false on a walled deployment since PR #19136 (split from #19145, carrier 4) #19875 同级。en文字的译文(AGENTS.md 允许直接改译文);en再改时门禁能报出来。check:i18n和check:i18n-stale-fill都看不见它。同类的旧译文可能不止这一处,接手时顺手量一下,⛔ 不在本卡扩面。Generated by Claude Code
objectstack-fleet commentedon Sep 23, 2026
Claim: PM loop round 21
Session:
session_01TEhopqrWQYBycZzyJHpAZrBranch:
claude/issue-19897-sys-user-role-help-translationsWorktree:
objectstack-issue-19897Domain:
domain:engineSeat:
domain:engine#1File surface:
packages/platform-objects/src/apps/translations/{zh-CN,ja-JP,es-ES}.objects.generated.ts(theobjects.sys_user.fields.role.helpleaf only), the locale source-hash files that should record that leaf (through the repo's i18n tooling, ⛔ never by hand if a generator owns them), one.changeset/*.md(stop on breach; explain in the report)Container & model:
S,mode:subagent,model: sonnet(no path-derived mandate; floor tier — three translated values plus their hash record)Clause-②: no
Thread-read: 5798572250
Serial constraints cleared:
PR #19892 (#19875), which rewrote the en value of this leaf, landed as 029d8a4710. No open PR and no in-flight claim touches packages/platform-objects/src/apps/translations.Written 2026-09-23T17:09Z. A translated help text on the field an administrator reads when granting platform-admin standing still points at an action retired in #9968. The fix translates the current
entext and records the leaf's source hash, so the i18n gates see the nextenchange.Why
Clause-②: no: translated help-text values; no accept set widens and no public surface moves.Generated by Claude Code
objectstack-fleet commentedon Sep 23, 2026
os-dev-report
{
"issue": 19897,
"status": "done",
"branch": "claude/issue-19897-sys-user-role-help-translations",
"pr": "#19914",
"session": "session_01TEhopqrWQYBycZzyJHpAZr",
"premise_still_valid": true,
"summary": "Retranslated packages/platform-objects/src/apps/translations/{zh-CN,ja-JP,es-ES}.objects.generated.ts line 38, objects.sys_user.fields.role.help, from stale prose naming the retired "Set Platform Role" action to a faithful translation of the CURRENT en text (029d8a4 / #19892 / #19875), code spans kept verbatim. New values — zh-CN: 遗留的 better-auth 角色标量(admin、user 等)。ObjectStack 不再写入该字段(ADR-0068 D2)。要授予平台管理员身份,请将该用户已验证的邮箱加入
OS_PLATFORM_OWNER_EMAIL;在single租户态势下,sys_user_permission_set中一条未限定范围的admin_full_access授权同样可以赋予该身份。 | ja-JP: レガシーな better-auth ロールスカラー値(admin、user など)。ObjectStack はもはやこの値を書き込みません(ADR-0068 D2)。プラットフォーム管理者権限を付与するには、そのユーザーの確認済みメールアドレスをOS_PLATFORM_OWNER_EMAILに列挙してください。singleテナンシー態勢では、sys_user_permission_set内のスコープ指定なしのadmin_full_access割り当てでも同様にこの権限が付与されます。 | es-ES: Valor de rol heredado de better-auth (admin, user, …). ObjectStack ya no lo escribe (ADR-0068 D2). Para conceder la condición de administrador de la plataforma, incluya el correo verificado del usuario enOS_PLATFORM_OWNER_EMAIL; bajo la postura de tenenciasingle, una asignación sin ámbito deadmin_full_accessensys_user_permission_settambién la concede. A2 PROOF (ablation, run for real): mutated sys_user.role'sdescriptionin sys-user.object.ts to a marker string, ranpnpm i18n:extractto regenerate en, thenpnpm check:i18n(exit 0, in sync) andpnpm check:i18n-stale-fill(exit 0, 0 stale fills) — NEITHER reports this leaf, both before and after this PR's fix; the 3 translated values were untouched by the extract run (mergeExisting kept them). Mutation on sys-user.object.ts and every regenerated translation file was reverted viagit checkout HEAD --, then re-verified byte-identical to HEAD viagit hash-objectfor each touched path (all OK). REFUTATION of dispatch A2's premise: traced collectFilledFromHashes (source-hash.ts) — it records a source-hash entry for a generated-section leaf ONLY when the translated value is currently a byte copy of en (value === currentSource) or was previously recorded as such (previous[path] === hash(value)). A leaf holding a genuine translation satisfies neither, by the mechanism's own documented ruling (#12069 Option A: 'a leaf someone actually TRANSLATED ... gets no record and stays legacy-trusted'). Ranpnpm i18n:extractafter writing the translations: it rewrote nothing else and added NO hash entry for this leaf — confirming the trace. So 'give the leaf a source-hash entry' (the issue's suggested shape / dispatch A2) is architecturally unreachable while the leaf holds a real translation; this is not a regression, it's the pre-existing status quo for every hand-translated objects-section leaf (measured population below).","files_changed": [
"packages/platform-objects/src/apps/translations/zh-CN.objects.generated.ts",
"packages/platform-objects/src/apps/translations/ja-JP.objects.generated.ts",
"packages/platform-objects/src/apps/translations/es-ES.objects.generated.ts",
".changeset/19897-sys-user-role-help-stale-translations.md"
],
"deviations": [
"Did NOT add a source-hash entry for this leaf in LOCALE.source-hashes.generated.ts, though the issue's suggested shape and dispatch Zone 2 A2 asked for one. Measured and refuted (see summary/PR body): the generator (
pnpm i18n:extract --source-hashes/collectFilledFromHashes) can only ever record a hash for a leaf that is CURRENTLY a byte copy ofen— never for a leaf holding a real translation, by the mechanism's own ruled design (#12069 Option A). Did not hand-edit the generated hash sidecar files (explicitly forbidden by both AGENTS.md's translations table and the dispatch's File surface line) — hand-editing them would also have been ineffective, since findStaleFills/findStaleLeaves never walk a path outside their declared section sets. Ran the prescribed tool (pnpm i18n:extract) once, confirmed it makes no hash entry for this leaf and rewrites nothing else, then stopped."],
"gates": {
"derivation": "node scripts/pm/dispatch-gates.mjs --commands, re-derived on final commit 0807fd8 after
git fetch origin main(origin/main d1ca874 at derivation time; no unseen commit touched this diff's paths)","derived_total": 54,
"ran": 53,
"not_measured": 1,
"not_measured_detail": "pnpm check:dual-build-cjs-loads exits 3 PREREQUISITE NOT MET — needs a full whole-workspace
pnpm build(studio, client-react, 4 connectors, embedder-openai, knowledge-memory, +4 more dist/ missing), a whole-tree scan outside this diff's dependency closure (translations content only); CI's Build Core job builds the full tree before this gate runs there for real.","reconciliation": "node scripts/pm/dispatch-gates.mjs --ran FILE_WITH_EXIT_CODES confirms: 54 derived, 53 run clean, 1 NOT-MEASURED (exit 3, classified PREREQUISITE NOT MET), 0 UNRUN.",
"families": [
{
"command": "node scripts/check-adr-0087-registration.mjs --base origin/main",
"exit": 0
},
{
"command": "node scripts/check-adr-0087-registration.mjs --self-test",
"exit": 0
},
{
"command": "node scripts/check-changeset-no-major.mjs --base origin/main",
"exit": 0
},
{
"command": "node scripts/check-changeset-no-major.mjs --self-test",
"exit": 0
},
{
"command": "node scripts/check-ci-filter-parity.mjs",
"exit": 0
},
{
"command": "node scripts/check-closing-keyword-parity.mjs",
"exit": 0
},
{
"command": "node scripts/check-closing-keyword-parity.mjs --self-test",
"exit": 0
},
{
"command": "node scripts/check-comment-mask-adoption.mjs",
"exit": 0
},
{
"command": "node scripts/check-comment-mask-adoption.mjs --self-test",
"exit": 0
},
{
"command": "node scripts/check-comment-mask-corpus.mjs",
"exit": 0
},
{
"command": "node scripts/check-empty-changeset.mjs --base origin/main",
"exit": 0
},
{
"command": "node scripts/check-empty-changeset.mjs --self-test",
"exit": 0
},
{
"command": "node scripts/check-keyed-text-bounds.mjs",
"exit": 0
},
{
"command": "node scripts/check-keyed-text-bounds.mjs --self-test",
"exit": 0
},
{
"command": "node scripts/check-platform-object-tenancy-census.mjs",
"exit": 0
},
{
"command": "node scripts/check-platform-object-tenancy-census.mjs --self-test",
"exit": 0
},
{
"command": "node scripts/check-plugin-teardown-shape.mjs",
"exit": 0
},
{
"command": "node scripts/check-plugin-teardown-shape.mjs --self-test",
"exit": 0
},
{
"command": "node scripts/check-registry-log-declared.mjs",
"exit": 0
},
{
"command": "node scripts/check-registry-log-declared.mjs --self-test",
"exit": 0
},
{
"command": "node scripts/check-rest-log-spy-declared.mjs",
"exit": 0
},
{
"command": "node scripts/check-rest-log-spy-declared.mjs --self-test",
"exit": 0
},
{
"command": "node scripts/check-system-context-census.mjs",
"exit": 0
},
{
"command": "node scripts/check-system-context-census.mjs --self-test",
"exit": 0
},
{
"command": "node scripts/check-undeclared-dep-imports.mjs",
"exit": 0
},
{
"command": "node scripts/check-undeclared-dep-imports.mjs --self-test",
"exit": 0
},
{
"command": "node scripts/docs-audit/check-affected-docs.mjs",
"exit": 0
},
{
"command": "node scripts/docs-audit/check-drift-comment.mjs",
"exit": 0
},
{
"command": "node scripts/pm/release-rehearsal-clone.mjs --self-test",
"exit": 0
},
{
"command": "pnpm --filter @objectstack/spec run check:duration-unit-keys",
"exit": 0
},
{
"command": "pnpm check:changeset-gate-self-tests",
"exit": 0
},
{
"command": "pnpm check:cross-package-test-inputs",
"exit": 0
},
{
"command": "pnpm check:doc-authoring",
"exit": 0
},
{
"command": "pnpm check:driver-memory-census",
"exit": 0
},
{
"command": "pnpm check:dts-closure",
"exit": 0
},
{
"command": "pnpm check:dual-build-cjs-loads",
"exit": 3
},
{
"command": "pnpm check:gitlink-declared",
"exit": 0
},
{
"command": "pnpm check:i18n",
"exit": 0
},
{
"command": "pnpm check:i18n-stale-fill",
"exit": 0
},
{
"command": "pnpm check:issue-citations",
"exit": 0
},
{
"command": "pnpm check:lean-entry-closure",
"exit": 0
},
{
"command": "pnpm check:logger-receiver-detach",
"exit": 0
},
{
"command": "pnpm check:nul-bytes",
"exit": 0
},
{
"command": "pnpm check:objectui-changeset",
"exit": 0
},
{
"command": "pnpm check:org-identifier",
"exit": 0
},
{
"command": "pnpm check:page-declaration-shape",
"exit": 0
},
{
"command": "pnpm check:pm-changeset-deadline-census",
"exit": 0
},
{
"command": "pnpm check:published-files",
"exit": 0
},
{
"command": "pnpm check:refd-timer-probe",
"exit": 0
},
{
"command": "pnpm check:slot-lookup",
"exit": 0
},
{
"command": "pnpm check:sourcemap-no-sources-content",
"exit": 0
},
{
"command": "pnpm check:test-source-alias",
"exit": 0
},
{
"command": "pnpm check:tier-file-adoption",
"exit": 0
},
{
"command": "pnpm check:watch-hint-literal",
"exit": 0
},
{
"command": "node scripts/check-issue-citations.mjs",
"exit": 0,
"note": "the live command dispatch-gates.mjs's derivation reaches only via --self-test; run by hand per dispatch instruction"
}
]
},
"tests": "pnpm --filter @objectstack/platform-objects build :: exit 0 (all dist/ emitted, 11/11 declared). pnpm --filter @objectstack/platform-objects test -- --run :: exit 0, 883/883 tests passed across 54 files. pnpm --filter @objectstack/platform-objects typecheck :: exit 0 (tsc --noEmit clean; pre-existing shrink-only test-typecheck-debt.json entry untouched, 1 file/3 errors/2 pinned signatures). pnpm check:i18n :: exit 0, 'OK (9 package(s) — all bundles in sync, no undeclared authoring keys)'. pnpm check:i18n-stale-fill :: exit 0, 'OK (10 bundle set(s) — no new stale fills, 0 baselined)', self-test 27/27 batteries held. A2 ablation (see summary) run and reverted — both gates report 0 findings for this leaf before and after the fix, confirming the refutation. Gate derivation (node scripts/pm/dispatch-gates.mjs --commands, re-derived on final commit after git fetch origin main): 54 families derived, 53 ran clean (exit 0), 1 NOT MEASURED (pnpm check:dual-build-cjs-loads, exit 3 PREREQUISITE NOT MET — needs a full whole-workspace pnpm build unrelated to this diff, CI's Build Core covers it). Reconciled with --ran: '54 derived, 53 run, 1 NOT-MEASURED (1 DERIVED from a recorded exit 3), 0 UNRUN'. node scripts/check-issue-citations.mjs (live command; the derivation only reaches its --self-test) run by hand :: exit 0, 'no issue citations added against 44ce049 (3 file(s) read)'. Full command list and per-command exit codes are in the gates field.",
"mcp_calls": "5 — mcp__github__issue_read (method=get), mcp__github__issue_read (method=get_comments), mcp__github__pull_request_read (method=get, pullNumber=0 — user error, missing param, no-op), mcp__github__list_pull_requests, mcp__github__pull_request_read (method=get, pullNumber=19914, PR-body readback). All reads; no MCP write tool called.",
"api_writes": "2 — POST /repos/objectstack-ai/objectstack/pulls (op: pr_create, via scripts/pm/with-fleet.sh --via dispatch relay, draft forced true); POST /repos//issues/19897/comments (op: comment, this report, via the same relay, about to be sent). git push (2x, to publish the empty branch then each commit) is git-protocol, not a REST proxy write, per AGENTS.md/dispatch convention. No label writes (none requested; dispatch said none).",
"open_questions": [
{
"question": "This card's structural finding — 1203/1182/1173 genuinely-translated platform-objects
objectsleaves (zh-CN/ja-JP/es-ES) have no staleness protection at all, by the current mechanism's own ruled design (#8765 Option B / #12069 Option A: a real translation is always 'legacy-trusted'). Is this worth a new ruling to extend protection (e.g. a hand-recordable hash table for theobjectssection, mirroringLOCALE.source-hashes.tsforapps/dashboards/pages), or is the current design (structural blind spot, relying on ad hoc discovery like this card) the intended long-term shape?","options": [
"A. Leave as-is: the mechanism is deliberately scoped (ruling #12069 comment: extending it would be a bundle/extract FORMAT change, reported rather than invented inline). Rely on future audits/reports like this one to catch drift by inspection.",
"B. Extend the ruled shape: add a hand-maintained
LOCALE.source-hashes.ts-style companion for genuinely-translatedobjects/metadataFormsleaves too, so a translator recording a hash at translation time gets the same protectionapps/dashboards/pagesleaves already have.","C. Something narrower: a periodic/CI-driven audit that diffs
enagainst its git history per leaf-path and flags translated leaves whose correspondingenvalue changed since the translation's last touch (a git-blame-based proxy, no new stored hash format)."],
"recommendation": "B, because per the four-axis framework: real business need is measured directly by this very card (a wrong-language admin-facing string went unnoticed through every gate) and the population is large (~1200 leaves per locale) so the same failure mode will recur; long-run this closes exactly the kind of undetectable metadata drift the repo's i18n gates exist to prevent, and it reuses an already-ruled, already-implemented pattern (findStaleLeaves/HAND_AUTHORED_SECTIONS) rather than inventing a new one; it tightens a declared contract rather than adding consumer-side tolerance; against 'don't expand scope', the counter-argument is that leaving ~1200 leaves per locale permanently unprotected is itself the wider, compounding cost. A is cheapest but leaves the whole class exposed. This needs the maintainer's ruling, not a dev decision — filing it is a PM/maintainer call, not this card's to make."
}
],
"out_of_scope_findings": [
"Triage-requested measurement (not fixed here, per dispatch scope): counted every packages/platform-objects 'objects' leaf that is a genuine translation (differs from en) and therefore carries no source-hash entry — i.e. the population structurally invisible to check:i18n-stale-fill if it ever drifts, same defect class as this card. zh-CN: 1203 of 1565 objects leaves. ja-JP: 1182 of 1565. es-ES: 1173 of 1565. (362/383/392 respectively ARE hashed — those are leaves still byte-identical to en, i.e. untranslated fills, not at risk of THIS failure mode.) A targeted grep for the specific retired action name/label this card fixed ('Set Platform Role' / 设置平台角色 / プラットフォームロールを設定 / Establecer rol de plataforma) across every locale's objects/metadata-forms bundles, en included, returns 0 hits post-fix — no OTHER leaf still names it. Did not audit the 1203/1182/1173 population for actual (vs. structurally-possible) staleness — that is a full semantic audit, out of this card's scope and not mechanically checkable at this population size."
]
}
objectstack-fleet commentedon Sep 23, 2026
ACCEPT — round 21 · PR #19914 ·
Fixesdomain:engine#1,session_01TEhopqrWQYBycZzyJHpAZr, written 2026-09-23T18:13Z. Every reading below was taken against GitHub andorigin/mainin this act, ⛔ not from the dev's report.Review
0807fd8143main; first body lineFixes #19897; no other closing keyword;Clause-②: noat column 0, agreeing with thepatchchangesetobjects.sys_user.fields.role.helpvalue) in each of{zh-CN,ja-JP,es-ES}.objects.generated.ts, one changeset. No key, structure or hash sidecar movesentext (029d8a4710, PR #19892) clause by clause: the verified email inOS_PLATFORM_OWNER_EMAILon every posture, and the unscopedadmin_full_accessgrant onsingleonly. Thesinglequalifier is scoped to the second route in all three locales; code spans are verbatimtranslations/file at head,enincluded (3 hits at the merge-base, exactly the lines replaced)collectFilledFromHashesnever records a hash for a genuinely translated leaf (pinned atsource-hash.test.ts:270). A hand entry in the generated sidecar is forbidden by AGENTS.md and the file header, and could not protect the leaf. The dev ran the tooling once; it wrote nothingcheck-governed-merges --pr 19914, 0 of 4 paths)check:i18n/check:i18n-stale-fill)Out-of-scope findings and open questions — dispositions:
objectsleaves get staleness protection (the dev recommends a hand-maintained source-hash table)? Dropped — covered by existing rulings. i18n: a corrected source description never reaches an already-translated locale, and no gate can tell the stale translation from a current one #9672 ruled that a corrected source string leaving a translated locale stale is accepted-and-documented behaviour, and A source label/description edit leaves the TRANSLATED locale bundles stale, and neither i18n gate can see it (measured on the #9807 diff) #10026 was closed not planned on that ruling. Its reopen condition, "a non-English user reports stale help text in production", is not met: this card came from a dev audit. [Decision] 把 #8765 的 source-hash sidecar 扩展到生成的 i18n bundle —— #9672 写明的升级条件已满足 #12069 then extended the source-hash sidecar to generated bundles for byte-copy fills. A translated leaf "gets no record and stays legacy-trusted" by that design (thesource-hash.tsmodule note). This card is another instance of the ruled class, not a new semantic case, so the parent rulings carry with their reasons, and a new gate is default-no. The seat's reviewer recommended a[Decision]card instead (flag ③-1). The seat declines and names this disposition in the round report, where the maintainer can overrule it.objectsleaves (zh-CN / ja-JP / es-ES) carry no hash; this is the measured population triage asked for, not a stale count → Acceptance notes, as the evidence for the disposition above.entext was "renamed" (it was rewritten), and its second paragraph is more process narrative than an upgrader needs. Every sentence is true → Acceptance notes.Landing: ready → queue on this head once CI is fully green.
Generated by Claude Code
fix(platform-objects): translate sys_user.role help text away from th…