Skip to content

fix(platform-objects): point sys_user.role's prose at the path that exists, not the retired Set Platform Role action - #17102

Merged
os-musk merged 1 commit into
mainfrom
claude/issue-15188-sys-user-role-prose-retired-action
Sep 9, 2026
Merged

fix(platform-objects): point sys_user.role's prose at the path that exists, not the retired Set Platform Role action#17102
os-musk merged 1 commit into
mainfrom
claude/issue-15188-sys-user-role-prose-retired-action

Conversation

@os-musk

@os-musk os-musk commented Sep 9, 2026

Copy link
Copy Markdown
Collaborator

Fixes #15188

Clause-②: no — route ① only. Prose in one package: no new exported symbol, no new key on any published payload, no widened or narrowed accept set. The role field is not retired here.

The sites, re-derived by content on this branch's base

Tree: 854639b311f714d936ae4ca239ef838ecc785845 (origin/main at branch creation — the dispatch quoted edf59e3599, main moved). Located by grepping the content, not by trusting a line number. On this tree the numbers happen to agree with triage's:

line text disposition
:282 // #9968 — set_user_role (target: /api/v1/auth/admin/set-role) retired tombstone — untouched
:287 // working "Set Platform Role" button was a supported, one-user-at-a-time past tense, true history — untouched
:614 readonly: true, // ADR-0092 — set via the Set Platform Role action, never the edit form fixed
:617 description: 'Platform-level role (admin, user, …). Set via the Set Platform Role action.' fixed

:287 needed the opposite treatment and got it. It is inside the removal note and it is past tense — "a working 'Set Platform Role' button was a supported, one-user-at-a-time resurrection channel…". It narrates what was removed and it is true; "correcting" it would turn a true sentence false. Same trap as #15202: within one batch of literals the present-tense ones are stale descriptions and the past-tense ones are real history, judged site by site rather than swept.

Proof the two history sites are unchanged. The whole diff to sys-user.object.ts is a single hunk, @@ -611,10 +611,11 @@ — nothing within 300 lines of the removal note is in it. Grepping the branch head for the retired names now returns exactly the two tombstone lines and nothing else:

282:    // #9968 — `set_user_role` (target: /api/v1/auth/admin/set-role) retired
287:    // working "Set Platform Role" button was a supported, one-user-at-a-time

What all four set_user_role non-test source hits under packages/ turned out to be

This was the first act, before any edit. All four are tombstones. None is live residue. The retirement was complete, so route ②'s premise is re-confirmed rather than disturbed, and this card's scope stays at two strings.

  1. packages/spec/src/kernel/public-auth-features.ts:209 — a commented-out entry in PUBLIC_AUTH_FEATURES.admin.gatedInputs: // 'sys_user.actions.set_user_role' retired (#9968) — see the removal note beside \impersonate_user` in sys-user.object.ts.` The live array does not contain it. Tombstone.
  2. packages/platform-objects/src/identity/sys-user.object.ts:282 — the removal note itself. Tombstone (and the two live-prose sites this PR fixes).
  3. packages/platform-objects/CHANGELOG.md:1310, 1318, 9579 — published release history. :1310/:1318 are the impersonate_user and set_user_role still 403 every platform admin — neither route is safely raw-mountable, and each blocks for a different reason #9968 entry announcing the retirement; :9579 is an older entry from before it, describing the action as extant at that time. Both are correct as history and are release-owned.
  4. packages/spec/CHANGELOG.md:10618, 10626 — the same impersonate_user and set_user_role still 403 every platform admin — neither route is safely raw-mountable, and each blocks for a different reason #9968 entry on the spec side. Published release history.

Control that the grep is not broken: admin_full_access hits 164 files under packages/ on this tree (triage measured 163 on theirs). The two *.test.ts hits excluded from the four are both pins that assert the retirement is completeplatform-objects.test.ts's #9968 test (set_user_role must be gone, every sibling survivor present by name) and feature-gate-guard.test.ts's floor of 37 booked inputs, "38 before #9968".

The route named in the new prose resolves in this tree

Not invented and not taken on the card's word:

  • sys_user_permission_set is a real declared object — packages/plugins/plugin-security/src/objects/sys-user-permission-set.object.ts, label "User Permission Set".
  • admin_full_access is a real shipped permission set, resolved by name in packages/core/src/security/admin-standing-surface.ts and resolve-authz-context.ts.
  • ADR-0068 D2's own table maps platform_admin to "unscoped sys_user_permission_setadmin_full_access".

What changed

readonly: true, // ADR-0092 — never the edit form; no writer since #9968 (platform admin: `sys_user_permission_set` / `admin_full_access`)

description:
  'Legacy better-auth role scalar (admin, user, …). ObjectStack no longer writes it (ADR-0068 D2) — grant platform-admin standing with an unscoped `admin_full_access` assignment in `sys_user_permission_set`.',

The description deliberately does not say the field confers nothing. That would have replaced one false sentence with another: judgePlatformAdmin (packages/plugins/plugin-auth/src/platform-admin-gate.ts) still reads user.role === 'admin' "ONLY as the legacy fallback it has always been", so a deployment carrying the scalar from before D2 is not locked out. What is unambiguously true, and what an operator needs, is that nothing writes it any more and where the grant actually goes.

"No ObjectStack action writes it" was checked rather than assumed. ADR-0092 notes role as an action param of create_user / invite_user / set_user_role; on this tree create_user's params are email / phoneNumber / name / generatePassword / password / mustChangePassword — no role — and invite_user's role param carries objectOverride: 'sys_member', i.e. the org membership role, not this scalar.

Generated-mirror sweep

packages/platform-objects/src/apps/translations/en.objects.generated.ts:38 carried the same sentence and follows by regeneration (pnpm i18n:extract), never a hand edit — the default locale's leaves are rewritten from the source on every run. One line changed there, derived from :617.

Swept for other mirrors of both changed strings, tracked and untracked, across the whole tree (git grep plus a filesystem grep excluding node_modules / .git / dist / .turbo): exactly two files carry "Set via the Set Platform Role action" — the source and that one generated bundle. Nothing in content/docs/references/, no second generated page. The readonly comment is a source comment and mirrors nowhere.

The generator run rewrote all 11 bundle files and left 10 of them byte-identical, which is the documented merge behaviour (translated-locale values are preserved). See the acceptance note below for what that means for the three translated locales.

Changeset — decided on a measurement, not on the diff's apparent kind

patch on @objectstack/platform-objects. The measurement, taken from the committed state (the gate reads content via git show REV:PATH, so a working-tree edit is invisible to it):

  1. force-build @objectstack/platform-objects with the fix, sha256 every published artefact under dist/;
  2. git checkout 854639b311 -- the two authored files, verify on disk that the new text is gone (0 occurrences) and the old text is back (1 occurrence in each file);
  3. force-build again, sha256 the same 66 files;
  4. restore with git checkout HEAD -- , verify both blob hashes equal their HEAD blobs and git diff HEAD is empty.

10 of the 66 published files change hash between the two builds:

dist/apps/index.js        dist/apps/index.mjs
dist/identity/index.js    dist/identity/index.mjs     (+ both .map)
dist/index.js             dist/index.mjs              (+ both .map)
dist/plugin.js            dist/plugin.mjs

So this publishes, skip-changeset would be wrong, and a patch on a released package is the right entry. The rebuild after restore reproduced the "after" hash exactly (dist/index.mjs = 6f86336b…), so the two readings are a real before/after and not build nondeterminism.

Ablation — APPLICABLE, and it fires in both directions

The diff adds a guard, so this is not reported as inapplicable. New pin: #15188 — the retired action survives only as HISTORY; no live sys_user prose points at it, in platform-objects.test.ts beside the existing #9968 test. It reads the object source as text and asserts occurrence counts, which is what makes both directions fail. Fix committed first; each leg proved on disk before it was believed; both restore legs verified by blob hash and an empty git diff HEAD.

leg mutation on-disk proof vitest
0 none (baseline) exit 0, 127 passed
A the past-tense history sentence turned present tense (wasis) — the exact trap target occurrences 1 → 0, injected text 1; blob 5fda0c01… ≠ HEAD bd4ad934… exit 1, this test fails, 126 pass
B the retired name re-introduced into the live description Set Platform Role occurrences 1 → 2; blob 5d87a79a… ≠ HEAD exit 1, this test fails, 126 pass

After each leg: restored blob bd4ad9344b352ccfd5d024cb83bf6697c59c0208 = HEAD blob, git diff HEAD empty, git status --porcelain empty. The mutation script carried a trap … EXIT INT TERM restore with absolute paths throughout. No rebuild leg is involved: the pin reads source text and the test's SysUser import resolves to src/, not dist/.

Direction observed is the ordinary one (turns red), stated because a template's expectation is not a reading.

Verification

Derived with node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack — it read the change set from git itself, named this repo and this checkout, and asserted --repo against the origin remote. Derived twice: 51 families before the changeset existed, 57 after it, and re-derived at the final commit c06938d24e where the family list is byte-identical.

Reconciliation: 57 derived · 57 run · 0 NOT MEASURED · 0 UNRUN · 57 green. Every exit captured by redirect-then-$?, never across a pipe.

Two families first returned 3 PREREQUISITE NOT MET, which is not a pass and was not recorded as one — check:dual-build-cjs-loads and check:i18n both want built output. Cleared with a full pnpm build (73/73 tasks) and re-run to exit 0. A third, check:type-check-debt, returned 3 on the final-commit pass for a different reason it named itself: the ablation's restore had bumped sys-user.object.ts's mtime past platform-objects' type entry point. Rebuilt that package and re-ran — exit 0, "5 ledger entries re-measured, 55 raw tsc errors, none above its recorded number, surplus: none".

Beyond the derived set:

  • pnpm --filter @objectstack/platform-objects testexit 0, 37 files / 546 tests passed.
  • pnpm --filter @objectstack/platform-objects typecheckexit 0. Worth naming: this package's tsconfig.json excludes **/*.test.ts, so tsc --noEmit does not see the new test. Its typecheck script also runs check:test-typecheck against tsconfig.test.json, which does, and reported "OK — the test layer compiles; 1 file / 3 errors / 2 pinned signatures held", unchanged and all in feature-gate-guard.test.ts. The new test therefore is type-checked, by that leg rather than the first one.
  • Repo-wide pnpm lint (eslint . --no-inline-config) — exit 0, run in full. No narrowing to declare.
  • Four more gates run because the derivation flagged their rosters as sitting under a directory this diff touches, or because they are obvious neighbours of an i18n change: check:authz-resolver, check:error-code-casing, check:filter-alias-parity, check:i18n-coverage — all exit 0.
  • Control-character self-scan over all four changed files: no hits.

验收备注

Noted, not filed — reported to the PM with its measurement rather than filed blind.

The same stale sentence survives in three translated locales, and no gate can ever see it. zh-CN / ja-JP / es-ES objects.generated.ts:38 each carry a hand-written translation of the old English text, each naming the retired button:

zh-CN  平台级角色(admin、user 等)。通过“设置平台角色”操作设置。
ja-JP  プラットフォームレベルのロール(admin、user など)。「プラットフォームロールを設定」アクションから設定します。
es-ES  Rol a nivel de plataforma (admin, user, …). Se establece mediante la acción Establecer rol de plataforma.

They are not stale by accident of this PR — they were already stale — and pnpm i18n:extract deliberately leaves them alone. Nothing will ever flag them either: objects.sys_user.fields.role.help has no entry in any *.source-hashes.generated.ts, and a path with no entry is LEGACY-TRUSTED and never reported stale. This is a known class in this repo (#14931, with #15511 and #16165 as prior instances, each filed and closed as its own card), which is why it is reported rather than ridden along.

It is deliberately not fixed here. The bounded in-place exemption needs the file to be held by no other claim, and it is: PR #17068 (claude/issue-16185-migration-flag-column-move-attest) edits all three of those bundles plus their source-hashes files. Its hunk is at ~:3569 (sys_migration), so there is no textual conflict with the sys_user block at :38, but the file is taken. Measured over all 19 open PR heads via the REST files endpoint; #16883 also appears but only on the hand-written per-locale .ts bundles (en.ts, zh-CN.ts, …), which this diff does not touch.

Lane. Triage recorded a tension and left it open, and this seat does not re-route unilaterally. The landing site is packages/platform-objects/**domain:engine by the anchoring rule. Having read the file: the two strings turned out to be identity semantics — what sys_user.role is post-ADR-0068 D2, what judgePlatformAdmin still accepts, which grant confers platform-admin standing — and getting the wording right meant reading plugin-auth, core/src/security and two ADRs, none of which is engine territory. My recommendation is that a services seat reviews the wording, not that the card be re-routed; the mechanical half (regenerate the bundle, pin the boundary, gates) is squarely engine's and is done.


Generated by Claude Code

… exists, not the retired Set Platform Role action (#15188)

The `role` field's `description` and its `readonly` comment both named
`set_user_role` / "Set Platform Role", an action retired in #9968. A field
`description` is authored data, not a source comment: it ships in the published
bundle and is extracted into the i18n bundles, so it reaches an operator as
field help in the admin UI — telling them to press a button that no longer
exists anywhere in the product.

Both now name the route that actually resolves in this tree: platform-admin
standing comes from an unscoped `admin_full_access` grant in
`sys_user_permission_set` (ADR-0068 D2), which is what the #9968 removal note in
this same file already says. The description deliberately does not claim the
scalar confers nothing — `judgePlatformAdmin` still reads `user.role === 'admin'`
as the legacy fallback it has always been, so a pre-D2 deployment carrying the
value is not locked out.

`en.objects.generated.ts` follows by `pnpm i18n:extract`, never by hand.

The third mention in this file — inside the removal note, past tense, "a working
'Set Platform Role' button WAS a supported, one-user-at-a-time resurrection
channel" — is history and is true, so it is left byte-identical. A new pin holds
both directions as occurrence counts over the source text: deleting the history
drops a count to 0, re-introducing the retired name in live prose pushes one
past 1.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ADLdAs2pVcH17h9tZKWMBg
@github-actions github-actions Bot added the size/s label Sep 9, 2026
@github-actions

github-actions Bot commented Sep 9, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

1 anchor(s) derived from 1 changed package(s); no hand-written page names any of them, so this run has nothing to listnot a clean bill of health. This check sees only pages that NAME a derived anchor: one that documents this change in prose, or enumerates it in an authoring dialect, names none and stays invisible to it on every run.

What this run could not see
  • 1 anchor(s) matched too much of the corpus to be a work list: sys_user (symbol, 31 pages)
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.

Coarse fallback — 2 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json e26ea69b85d4d2a82522baa87394bcfc4779e1bcpackageMentionDocs.

Which tree this was computed on

This run read content/docs from 8954d87ac6c3906465b54ac8b6292ddeba5f2147 — the merge of head c06938d24eca0e1712d3f1b45e76fee3ee0f78f6 into base e26ea69b85d4d2a82522baa87394bcfc4779e1bc, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 8954d87ac6c3906465b54ac8b6292ddeba5f2147 && git checkout 8954d87ac6c3906465b54ac8b6292ddeba5f2147
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin e26ea69b85d4d2a82522baa87394bcfc4779e1bc c06938d24eca0e1712d3f1b45e76fee3ee0f78f6 && git checkout -B drift-repro e26ea69b85d4d2a82522baa87394bcfc4779e1bc && git merge --no-ff c06938d24eca0e1712d3f1b45e76fee3ee0f78f6

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

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

@github-actions github-actions Bot added documentation Improvements or additions to documentation tests tooling labels Sep 9, 2026
@os-musk
os-musk marked this pull request as ready for review September 9, 2026 08:57
@os-musk
os-musk enabled auto-merge September 9, 2026 08:58
@os-musk
os-musk added this pull request to the merge queue Sep 9, 2026
Merged via the queue into main with commit 4215417 Sep 9, 2026
35 checks passed
@os-musk
os-musk deleted the claude/issue-15188-sys-user-role-prose-retired-action branch September 9, 2026 09:23
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/s tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[finding] sys-user.object.ts: the role field description and readonly comment still point at the retired Set Platform Role action (set_user_role)

2 participants