Repository navigation
fix(platform-objects): sys_member transfer_ownership says the target becomes an owner and the caller keeps their role - #24412
Conversation
… step-down on a booted stack Claude-Session: https://claude.ai/code/session_01Jjomb6DDwUaLVDFvej7759 Co-authored-by: Claude <noreply@anthropic.com>
…becomes an owner and the caller keeps their role better-auth's update-member-role writes only the target's role, so the caller was never demoted to admin as the confirm promised. The label, confirm, success message and comment now say what the door does and name Change Role on one's own row as the step-down. Claude-Session: https://claude.ai/code/session_01Jjomb6DDwUaLVDFvej7759 Co-authored-by: Claude <noreply@anthropic.com>
…slate transfer_ownership's copy en is regenerated by check-i18n-bundles --write; the zh-CN, ja-JP and es-ES values are hand-written translations, which merge mode keeps, so they are re-translated to the same honest copy. Claude-Session: https://claude.ai/code/session_01Jjomb6DDwUaLVDFvej7759 Co-authored-by: Claude <noreply@anthropic.com>
…and measure the caller's step-down; changeset Claude-Session: https://claude.ai/code/session_01Jjomb6DDwUaLVDFvej7759 Co-authored-by: Claude <noreply@anthropic.com>
…ansfer-ownership-honest
📓 Docs Drift CheckThis PR changes 1 package(s): 7 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
⛔ 6 release-owned page(s) also name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 3 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 9b0998f172db5c84199632708b1323387505940f && git checkout 9b0998f172db5c84199632708b1323387505940f
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 44a5011fe623c8f210d13177bc2776d0bb0b0afa f31e05504fd7445bde22cf5f95d704ddf7f821eb && git checkout -B drift-repro 44a5011fe623c8f210d13177bc2776d0bb0b0afa && git merge --no-ff f31e05504fd7445bde22cf5f95d704ddf7f821eb
node scripts/docs-audit/affected-docs.mjs --json 44a5011fe623c8f210d13177bc2776d0bb0b0afa
|
Contract reviewServed-tier: What this record read:
① Derived judgmentsThe shape: B, honest copy, right. The dev measured first, on a booted stack with two organisations. The B-breaker the claim named did not fire:
So a declared, honest step-down exists, and no part of A (a transfer door) was built. The copy: true now, right. The confirm used to promise "You will be demoted to admin", and the vendor writes only the target. At the head:
Every one of these is now a statement of what the door does. Unchanged and right: The bundles: right.
The pins: real controls.
What stays with the old wording, none of it served copy:
② Semver levelOne ③ Boundary flags
Implemented-by: VERDICT: PASS Generated by Claude Code |
席位核对 ·
|
| Item | Reading |
|---|---|
| Shape | Draft PR → main. The body opens Fixes #24273. 8 files, +334 / −21. Not governed (check-governed-merges.mjs --pr 24412: 0 of 8 paths hit the register). |
| Scope | sys-member.object.ts: transfer_ownership's label, confirm, success message and comment. The four *.objects.generated.ts bundles carry the same copy (en regenerated; zh-CN, ja-JP, es-ES re-translated). A copy pin in platform-objects, and a dogfood file declared to domain:cli (6257430307, 6257435263). One patch changeset. No door, route or reach row. |
| Seat's own read | The record's own: only copy moves; name, target, body, visible, requiresFeature, requiresMembershipReach and variant are unchanged. No es-ES source-hash entry names the action, so the new translations are served. |
| Measured | On a booted stack with two organisations: the action leaves both rows owner (the card). The caller's own Change Role to admin answers 200, leaving them admin and the new member owner. A sole owner's same step is refused 400 YOU_CANNOT_LEAVE_THE_ORGANIZATION_WITHOUT_AN_OWNER. Change Role is offered on one's own row. So the honest step-down exists, and the B-breaker did not fire. |
| Review | TIER_DEFAULT record of record 6257422660 on this head: PASS, in-seat. |
| Declaration | Clause-②: no on the claim, the PR body and the changeset. Copy only. |
| Pins | The copy pin forbids "demot" and "transfer" in the served copy per locale and ties the confirm to Change Role's own label. Restoring the base files turned it red, 6 of 8. The dogfood file reads both rows after each step and asserts the served copy; its copy case was red before the fix. |
| Gates | Dev: 69 derived, 68 run with exit 0, 1 NOT MEASURED. check:dual-build-cjs-loads locally exited 3, PREREQUISITE NOT MET; CI's Lint & Repo Gates is green on this head. Typecheck exit 0 for platform-objects and the dogfood package. |
| CI | 41 check-runs on f31e05504f: 35 success, 6 skipped, 0 failed. The skips are the rostered path and opt-in skips. |
Disposition:
open_questions: both accepted. The label is "Make Owner", and thenamestaystransfer_ownership(its id and reach-row key).deviations: all accepted, as the record's ③ judges them:- two organisations on the single boot shape;
successMessagemoved with the rest of the copy;- three locales translated by hand;
- the detached shared checkout;
- the footer;
- the pre-fix run cited.
out_of_scope_findings: thepackages/specfixture labels and the released CHANGELOG are not served copy; noted, not filed.- Downstream: consumer pointer 6257439197 on [PM seat] repo:cloud · seat 2 — ⏳ vacant #24145 for cloud#2912. Its booted test's
admin-after-action expectation readsowneron a pin carrying this, andadminonly after the caller's own Change Role.
Landing: ordinary queue (pr_ready + automerge_enable through the relay). Fixes #24273 then closes the card.
Generated by Claude Code
Fixes #24273
Clause-②: no (provisional: copy only; if the measurement forces another shape, the dev reports before building it; owes the seat's review of record at
TIER_DEFAULTbefore the queue)What changed
sys_member.transfer_ownershipsends better-auth'sPOST /organization/update-member-rolewithrole: 'owner'. better-auth 1.7.3'supdateMemberRolewrites the target's role and nothing else, so the caller stays an owner. The action used to promise "You will be demoted to admin". Shape B (honest copy) from the claim: the action now promises what the door does.packages/platform-objects/src/identity/sys-member.object.ts,transfer_ownership:name,target,recordIdParam,bodyExtra,visible,requiresFeature,requiresMembershipReach,variant,icon.en.objects.generated.tswas regenerated bynode scripts/check-i18n-bundles.mjs --write. Thezh-CN/ja-JP/es-ESvalues are hand-written translations, and merge mode keeps them, so each one would have kept the false promise. I translated them again to the same honest copy. Each confirm names that locale's own Change Role label (更改角色 / ロールを変更 / Cambiar rol).packages/platform-objects/src/identity/transfer-ownership-action-copy.test.tschecks four things on the declared copy and every locale bundle: no demotion or transfer is promised, the confirm names the locale'supdate_member_rolelabel,enequals the declared copy, and the request and identity are unchanged.packages/qa/dogfood/test/member-transfer-ownership-step-down.dogfood.test.tsis the booted measurement below..changeset/24273-transfer-ownership-honest.md(@objectstack/platform-objects: patch).Measurement (booted stack, before the copy changed)
The dogfood file boots the showcase, makes two organizations with two signed-up non-platform-admin principals, and activates the organization under test. It reads both
sys_memberrows after each write.{ owner: 'owner', target: 'owner' }, so the organization has two owners. This reproduces the card.update_member_rolepredicate, evaluated withcelEngineagainst the caller's own row and the caller's served session (org_ownerin positions), returns true: Change Role is offered on one's own row.update-member-roleon their OWN row withrole: 'admin': 200, rows read{ owner: 'admin', target: 'owner' }. The step-down works through a declared action.YOU_CANNOT_LEAVE_THE_ORGANIZATION_WITHOUT_AN_OWNER, and the rows are unchanged. This is the vendor's last-owner guard (crud-members.mjs,updateMemberRole).So B holds: the previous owner can step down through a declared action, and nothing calls for shape A.
Pre-fix run (
2ba4d44d): 4 behavior cases pass and the served-copy case is red (expected 'Transfer Ownership | Transfer ownersh…' not to match /demot/i). Post-fix: 5/5 pass.Verification (final head
f31e05504, after mergingorigin/main31e7d0ba2)pnpm --filter '@objectstack/dogfood^...' build: exit 0pnpm --filter @objectstack/platform-objects typecheck: exit 0.pnpm --filter @objectstack/dogfood typecheck: exit 0pnpm --filter @objectstack/platform-objects test: 77 files, 1184 tests passedpnpm --filter @objectstack/dogfood exec vitest run --maxWorkers=2 test/member-transfer-ownership-step-down.dogfood.test.ts test/org-admin-affordance-reach.dogfood.test.ts: 2 files, 19 tests passedsys-member.object.tsand the four bundles restored to2a1e48a1c1(tree only, restored withgit checkout HEAD, thengit diff HEADempty),transfer-ownership-action-copy.test.tsfails 6 of 8. It passes 8 of 8 on the fix.node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstackgave 69 families onccac32c44. 68 exited 0.pnpm check:dual-build-cjs-loadsexited 3 with PREREQUISITE NOT MET, because unrelated packages had nodist/, so it is NOT MEASURED locally and left to CI.--ranreconciliation: "69 derived famil(ies) accounted for — 68 run, 1 NOT-MEASURED".f31e05504, all exit 0:check:i18n,check:i18n-stale-fill,check:type-check-coverage,check:type-check-debt,check-adr-0087-registration --base origin/main,check-empty-changeset --base origin/main,check-changeset-no-major --base origin/main,check:nul-bytes,check:cross-package-test-inputs,check:test-source-alias.Downstream (cloud)
cloud#2912's booted test (cloud PR #3003) expects the caller's
sys_member.roleto beadminright aftertransfer_ownership. Under this PR that expectation isowner. Theadminreading belongs only after the caller's ownupdate-member-roleon their own row withrole: 'admin'. If that test asserts the copy, it should also expect the new label "Make Owner".Acceptance notes
singleboot shape.@objectstack/organizationsis not a dependency of@objectstack/dogfood, so the framework's multi-org gates skip here (enterprise-organizations.ts). The two organizations are rows written in system context, the shapeorg-admin-affordance-reach.dogfood.test.tsuses. The door under test is the vendor's own endpoint either way.packages/spectest fixtures (action.test.ts,action-requires-membership-reach.test.ts) use the label "Transfer Ownership" on standalone fixture actions. They are not the served copy and are outside this lane, so they are left as they are.packages/platform-objects/CHANGELOG.mdholds an old entry that repeats the vendor claim ("auto-demotes the previous owner to admin"). It is release-owned and not edited in a code PR.Generated by Claude Code