Skip to content

ApproverType.role is the last surface projecting the reserved word — the ADR-0090 D3 exception it relies on has a false premise #3133

Description

@os-zhuang

Recording a finding surfaced while fixing the check:role-word ratchet failure on main (#3120). No code change proposed here — this is a decision record for later.

TL;DR

ApproverType.role (packages/spec/src/automation/approval.zod.ts) is the last place the platform projects better-auth's org-membership tier under the reserved word "role". It is not covered by ADR-0090 D3's exception — and the sentence that would cover it asserts something that never happened.

The D3 exception has a false premise

ADR-0090 D3 (docs/adr/0090-permission-model-v2-concept-convergence.md:202-204):

Single documented exception: the better-auth boundary — sys_member.role is third-party schema we do not own; it remains, already relabelled org_membership_level in the platform projection (ADR-0057 D7), and its UI label is "organization membership", never "role".

The exception's logic is: the third-party column is stuck with the word, but everything the platform surfaces is org_membership_level — the word never reaches an author. That premise does not hold:

Claim Reality
ADR-0090:203 — "already relabelled" org_membership_level appears once in the whole repo, as a comment in packages/spec/src/identity/position.zod.ts:25. Zero implementation.
ADR-0057 D7 (0057-...md:335) Explicitly lists it under "Deferred (evidence-gated, P4)": sys_member.roleorg_membership_level relabel

ADR-0090 states as complete a relabel that ADR-0057 deferred and that no code implements. Because the projection never landed, the word leaked from the third-party column up onto our own authoring surface.

Why ApproverType.role isn't covered by the exception

The exception protects better-auth's column (sys_member.role) — a thing we cannot rename. ApproverType is our own enum, in our own spec, that an author types into a flow node. It is the projection of that concept onto our authoring surface, and per the ADRs the projection is named org_membership_level, labelled "organization membership", never "role".

Every other surface already converged (ADR-0090 D3):

  • sys_role / sys_user_rolesys_position / sys_user_position
  • ShareRecipientType.roleposition ✅ (enum is now ['user','group','position','unit_and_subordinates','guest'])
  • ctx.roles[] / current_user.rolepositions / position
  • ApproverType.role → still role ❌ — the sole hold-out

It has no real users, and it costs us a lint rule

Repo-wide scan for type: 'role': only lint tests, docs warnings, and CHANGELOG entries. Zero examples, zero showcase, zero real callers.

Meanwhile it is a proven trap with a real incident behind it — packages/lint/src/validate-approval-approvers.ts exists specifically to catch it, and its rule table marks the origin "hotcrm class":

After ADR-0090 D3 renamed sys_rolesys_position, downstream apps that authored { type: 'role', value: 'sales_manager' } silently route the approval to nobody

The failure is silent: expansion finds no sys_member row → falls back to the role:sales_manager literal (expandApprovers pushes `${a.type}:${a.value}` for every type) → the request waits forever on an approver that cannot exist.

Anecdotally, this mental model ("role was deprecated, replaced by position — so ApproverType.role must be the legacy half") is exactly what a maintainer reached for unprompted while reviewing #3120. The name manufactures the confusion.

Per ADR-0077's principle — the authoring surface should make the mistake unwritable rather than lint it after the fact — a lint rule that exists to disambiguate a name is a smell pointing at the name.

Suggested shape (for whoever picks this up)

Rename, don't remove: roleorg_membership_level, matching the projection label the ADRs already mandate.

  • Removing would drop a legitimate (if rare) capability: routing to owner/admin/member.
  • Renaming kills the trap dead — nobody accidentally writes { type: 'org_membership_level', value: 'sales_manager' }; the name refuses the mistake, and approval-role-not-membership-tier can be deleted.
  • It closes out the deferred ADR-0057 P4 relabel and corrects ADR-0090:203's false statement in the same pass.

Cost: breaking change to a published spec enum (15.1 is out), so unlike D3's "one-step renames, no aliases" this needs a deprecation window + a major.

Refs

🤖 Generated with Claude Code

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions