docs(configure): define the org-membership tier vocabulary on the users page - #245

Merged
os-zhuang merged 1 commit into
mainfrom
claude/issue-153-org-membership-tier-vocabulary
Aug 29, 2026
Merged

docs(configure): define the org-membership tier vocabulary on the users page#245
os-zhuang merged 1 commit into
mainfrom
claude/issue-153-org-membership-tier-vocabulary

Conversation

@os-zhuang

Copy link
Copy Markdown
Contributor

Fixes#153

The four-value org-membership tier vocabulary (owner, admin, delegated_admin,
member — ADR-0108, sys_member.role) was used on two pages and defined on none.
A reader who met delegated_admin had nowhere in the corpus to go, and the nearest
thing they would reach for — "Delegated administration" on the permission-sets page —
is a different mechanism.

Where the section went, and why

content/docs/configure/users.mdx, in a new ## Organization-membership tiers
section. Three reasons, all checkable:

  1. configure/users.mdx:12 already lists the objects the page covers and sys_member
    is among them. The page had claimed the object whose role field this is and then
    never defined its tiers — an omission on an existing owner page, not a new home.
  2. configure/permissions/ documents what a principal may do. Upstream is explicit
    that this tier is not that (objectstack:packages/spec/src/identity/membership-role.ts):
    "It carries NO ObjectStack authority by construction … Role = can reach the
    endpoint
    ; adminScope = what the endpoint permits." A tier that confers no
    authority, filed inside the authority model, invites the exact misreading the card
    exists to prevent.
  3. The same file states the placement principle: "the other two are rules that belong
    near what they govern." Near what it governs = near sys_member.

Position within the page: between "Add people" and "Place people in the org tree
(memberships)".
The tier is chosen at admission — it is a value on the invitation the
preceding section just described — so this is where the chronology puts it. It also
puts the two senses of "membership" side by side, which is where a reader conflates
them, so the distinction is drawn at the seam instead of pages apart. It is deliberately
not folded into the existing memberships section: that section is
sys_business_unit_member, a different object, and merging the two would merge two of
the three facts the spec names as "look like one — do not merge them".

What the section says, and what each claim was measured against

Everything below is read off objectstackorigin/main, not inferred from the name:

ClaimSource
The vocabulary is closed and platform-owned; nothing widens it at bootADR-0108 D1; membership-role.tsBUILTIN_MEMBERSHIP_ROLE_OPTIONS
owner may invite at any tierinvitation-role-cap.tsorgRoleGrade (owner = 3, the ceiling)
owner is the only tier that may remove another ownerbetter-auth's removeMember predicate, quoted in member-role-canonical.ts
admin may invite at any tier except ownerinvitationRoleCapFailure (requested grade may not exceed the issuer's) plus better-auth's creatorRole check
delegated_admin may issue invitations without being an org admin, and that reach is the whole of itMEMBERSHIP_ROLE_DELEGATED_ADMIN doc comment; isOrgAdminGrade returns false for it
An invitation may never confer a tier above the issuer's own; an issuer below admin may invite only as memberinvitationRoleCapFailure, both refusal branches
An app's own business role cannot be stored in either field, and an invitation naming one is rejected before any row is writtenADR-0108 D2 (ROLE_NOT_FOUND at better-auth's door)
owner/admin memberships are auto-granted an organization-admin permission set scoped to that organizationplugin-security/src/auto-org-admin-grant.ts

That last row is why the "grade, not a bundle" line can be stated without hand-waving:
it names where the visible power of owner/admin actually comes from (a permission
set, on the ordinary permission path), so the tier is not left looking like the source
of it.

delegated_admin was the value most at risk of being written wrong, so it gets its own
subsection rather than a table cell. Its "on its own" row is the narrow, measured claim
lets you invite a plain member, and place nobody — rather than anything the name
suggests: the invitation cap holds a below-admin issuer to plain member, and placement
authority comes solely from a separately-granted adminScope. Nothing about "what a
delegated admin may do" is claimed beyond what those two files enforce.

The two consuming pages

configure/notifications.mdx and build/automation/approvals.mdx now link to the new
section. These are link-only edits: no word is added or removed on either page —
the diff is a Markdown link wrapped around text that was already there, plus a re-wrap
so the lines stay under the files' existing width. The tier enumerations stay in place;
they are useful where they are, and removing them would have been the prose rewrite the
card ruled out.

Verification

All runs on the final commit 5c7c4a1, from the repo root, exit codes captured before
any pipe:

CheckResult
pnpm turbo run build type-check test --forceTasks: 3 successful, 3 total, wrapper exit 0
node .github/scripts/check-locale-surface.mjsexit 0 — "every advertised URL has a source file and every source file is advertised"
node apps/docs/scripts/gen-zh-hant.mjs --checkexit 0 — "73 generated file(s) match the zh-Hans sources byte for byte"
node .github/scripts/check-translation-ownership.mjsexit 0 — "touches 0 translation artifact(s) and 3 other file(s)"
node .github/scripts/check-translations.mjsexit 0 — "translations gate passed"
node .github/scripts/check-translation-output.mjs --self-testexit 0
node .github/scripts/check-node-floor.mjsexit 0

The locale-surface oracle is unchanged, and that is positive evidence. The gate
reports 79 logical pages over 8 locales = 397 docs entries. Adding a section adds no
page and drops none, and the claim is measured rather than asserted: the gate's oracle
inputs are the content/docs/**/*.mdx path set plus each file's frontmatter title:,
and that (path, title) set is byte-identical between b0b159b and 5c7c4a1 — 397
pairs on both sides, diff exit 0
. apps/docs/lib/i18n.ts is untouched, and
git diff --diff-filter=ADR over the range is empty.

Anchors resolve — checked against the built HTML, not assumed. No gate here catches
a broken in-page anchor. In .next/server/app/en/docs/configure/users.html:
id="organization-membership-tiers", id="delegated_admin-is-not-delegated-administration"
and id="place-people-in-the-org-tree-memberships" all present; the new
href="/docs/configure/users#organization-membership-tiers" appears once in the built
notifications page and twice in approvals; and the outbound
permission-sets#delegated-administration target exists in that page's built HTML. The
slug oracle was confirmed independently: github-slugger@2.0.0 reproduces the existing
#layer-1--identity link on this very page from its heading text.

The rendered section was read back out of the built HTML: two tables, nine rows, no raw
pipes — a malformed MDX table degrades to a paragraph silently and no gate catches that
either.

Scope

No changeset (this repo has none). No configure/permissions/ restructuring;
permission-sets.mdx is untouched (it was read in full, not as a snippet, because the
new section links into it). No locale siblings. No new page.

Possible follow-up, deliberately not done here because it is inside the section the card
put out of scope: the sys_member row of the Layer 1 identity table in
configure/permissions/index.mdx could also link to the new section.


Generated by Claude Code

…rs page
`sys_member.role`'s four tiers (`owner`, `admin`, `delegated_admin`,
`member` — ADR-0108) were used on two pages and defined on none, so a
reader who met `delegated_admin` had nowhere in the corpus to go.
Add a dedicated "Organization-membership tiers" section to
`configure/users.mdx` — the page that already lists `sys_member` among
the objects it covers. The section gives each tier a one-line meaning,
states that a tier is a grade deciding what you can reach rather than a
bundle of what you may do, separates `delegated_admin` from the
`adminScope` mechanism documented as "Delegated administration" on the
permission-sets page, and separates `sys_member` from the
`sys_business_unit_member` org-tree placement the page already covers.
The two consuming pages (`configure/notifications.mdx`,
`build/automation/approvals.mdx`) now link to it. Those are link-only
edits: no word is added or removed, only re-wrapped.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016TUrhcggSFrYctvp5dsV1A
@os-zhuang
os-zhuang marked this pull request as ready for review August 29, 2026 14:34
@os-zhuang
os-zhuang merged commit 9597116 into mainAug 29, 2026
3 checks passed
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[finding] No page defines the org-membership tier vocabulary — delegated_admin is addressable in two places and explained in none

2 participants

@os-zhuang@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

docs(configure): define the org-membership tier vocabulary on the users page - #245

Merged
os-zhuang merged 1 commit into
mainfrom
claude/issue-153-org-membership-tier-vocabulary
Aug 29, 2026
Merged

docs(configure): define the org-membership tier vocabulary on the users page#245
os-zhuang merged 1 commit into
mainfrom
claude/issue-153-org-membership-tier-vocabulary

Conversation

@os-zhuang

Copy link
Copy Markdown
Contributor

Fixes#153

The four-value org-membership tier vocabulary (owner, admin, delegated_admin,
member — ADR-0108, sys_member.role) was used on two pages and defined on none.
A reader who met delegated_admin had nowhere in the corpus to go, and the nearest
thing they would reach for — "Delegated administration" on the permission-sets page —
is a different mechanism.

Where the section went, and why

content/docs/configure/users.mdx, in a new ## Organization-membership tiers
section. Three reasons, all checkable:

  1. configure/users.mdx:12 already lists the objects the page covers and sys_member
    is among them. The page had claimed the object whose role field this is and then
    never defined its tiers — an omission on an existing owner page, not a new home.
  2. configure/permissions/ documents what a principal may do. Upstream is explicit
    that this tier is not that (objectstack:packages/spec/src/identity/membership-role.ts):
    "It carries NO ObjectStack authority by construction … Role = can reach the
    endpoint
    ; adminScope = what the endpoint permits." A tier that confers no
    authority, filed inside the authority model, invites the exact misreading the card
    exists to prevent.
  3. The same file states the placement principle: "the other two are rules that belong
    near what they govern." Near what it governs = near sys_member.

Position within the page: between "Add people" and "Place people in the org tree
(memberships)".
The tier is chosen at admission — it is a value on the invitation the
preceding section just described — so this is where the chronology puts it. It also
puts the two senses of "membership" side by side, which is where a reader conflates
them, so the distinction is drawn at the seam instead of pages apart. It is deliberately
not folded into the existing memberships section: that section is
sys_business_unit_member, a different object, and merging the two would merge two of
the three facts the spec names as "look like one — do not merge them".

What the section says, and what each claim was measured against

Everything below is read off objectstackorigin/main, not inferred from the name:

ClaimSource
The vocabulary is closed and platform-owned; nothing widens it at bootADR-0108 D1; membership-role.tsBUILTIN_MEMBERSHIP_ROLE_OPTIONS
owner may invite at any tierinvitation-role-cap.tsorgRoleGrade (owner = 3, the ceiling)
owner is the only tier that may remove another ownerbetter-auth's removeMember predicate, quoted in member-role-canonical.ts
admin may invite at any tier except ownerinvitationRoleCapFailure (requested grade may not exceed the issuer's) plus better-auth's creatorRole check
delegated_admin may issue invitations without being an org admin, and that reach is the whole of itMEMBERSHIP_ROLE_DELEGATED_ADMIN doc comment; isOrgAdminGrade returns false for it
An invitation may never confer a tier above the issuer's own; an issuer below admin may invite only as memberinvitationRoleCapFailure, both refusal branches
An app's own business role cannot be stored in either field, and an invitation naming one is rejected before any row is writtenADR-0108 D2 (ROLE_NOT_FOUND at better-auth's door)
owner/admin memberships are auto-granted an organization-admin permission set scoped to that organizationplugin-security/src/auto-org-admin-grant.ts

That last row is why the "grade, not a bundle" line can be stated without hand-waving:
it names where the visible power of owner/admin actually comes from (a permission
set, on the ordinary permission path), so the tier is not left looking like the source
of it.

delegated_admin was the value most at risk of being written wrong, so it gets its own
subsection rather than a table cell. Its "on its own" row is the narrow, measured claim
lets you invite a plain member, and place nobody — rather than anything the name
suggests: the invitation cap holds a below-admin issuer to plain member, and placement
authority comes solely from a separately-granted adminScope. Nothing about "what a
delegated admin may do" is claimed beyond what those two files enforce.

The two consuming pages

configure/notifications.mdx and build/automation/approvals.mdx now link to the new
section. These are link-only edits: no word is added or removed on either page —
the diff is a Markdown link wrapped around text that was already there, plus a re-wrap
so the lines stay under the files' existing width. The tier enumerations stay in place;
they are useful where they are, and removing them would have been the prose rewrite the
card ruled out.

Verification

All runs on the final commit 5c7c4a1, from the repo root, exit codes captured before
any pipe:

CheckResult
pnpm turbo run build type-check test --forceTasks: 3 successful, 3 total, wrapper exit 0
node .github/scripts/check-locale-surface.mjsexit 0 — "every advertised URL has a source file and every source file is advertised"
node apps/docs/scripts/gen-zh-hant.mjs --checkexit 0 — "73 generated file(s) match the zh-Hans sources byte for byte"
node .github/scripts/check-translation-ownership.mjsexit 0 — "touches 0 translation artifact(s) and 3 other file(s)"
node .github/scripts/check-translations.mjsexit 0 — "translations gate passed"
node .github/scripts/check-translation-output.mjs --self-testexit 0
node .github/scripts/check-node-floor.mjsexit 0

The locale-surface oracle is unchanged, and that is positive evidence. The gate
reports 79 logical pages over 8 locales = 397 docs entries. Adding a section adds no
page and drops none, and the claim is measured rather than asserted: the gate's oracle
inputs are the content/docs/**/*.mdx path set plus each file's frontmatter title:,
and that (path, title) set is byte-identical between b0b159b and 5c7c4a1 — 397
pairs on both sides, diff exit 0
. apps/docs/lib/i18n.ts is untouched, and
git diff --diff-filter=ADR over the range is empty.

Anchors resolve — checked against the built HTML, not assumed. No gate here catches
a broken in-page anchor. In .next/server/app/en/docs/configure/users.html:
id="organization-membership-tiers", id="delegated_admin-is-not-delegated-administration"
and id="place-people-in-the-org-tree-memberships" all present; the new
href="/docs/configure/users#organization-membership-tiers" appears once in the built
notifications page and twice in approvals; and the outbound
permission-sets#delegated-administration target exists in that page's built HTML. The
slug oracle was confirmed independently: github-slugger@2.0.0 reproduces the existing
#layer-1--identity link on this very page from its heading text.

The rendered section was read back out of the built HTML: two tables, nine rows, no raw
pipes — a malformed MDX table degrades to a paragraph silently and no gate catches that
either.

Scope

No changeset (this repo has none). No configure/permissions/ restructuring;
permission-sets.mdx is untouched (it was read in full, not as a snippet, because the
new section links into it). No locale siblings. No new page.

Possible follow-up, deliberately not done here because it is inside the section the card
put out of scope: the sys_member row of the Layer 1 identity table in
configure/permissions/index.mdx could also link to the new section.


Generated by Claude Code

…rs page
`sys_member.role`'s four tiers (`owner`, `admin`, `delegated_admin`,
`member` — ADR-0108) were used on two pages and defined on none, so a
reader who met `delegated_admin` had nowhere in the corpus to go.
Add a dedicated "Organization-membership tiers" section to
`configure/users.mdx` — the page that already lists `sys_member` among
the objects it covers. The section gives each tier a one-line meaning,
states that a tier is a grade deciding what you can reach rather than a
bundle of what you may do, separates `delegated_admin` from the
`adminScope` mechanism documented as "Delegated administration" on the
permission-sets page, and separates `sys_member` from the
`sys_business_unit_member` org-tree placement the page already covers.
The two consuming pages (`configure/notifications.mdx`,
`build/automation/approvals.mdx`) now link to it. Those are link-only
edits: no word is added or removed, only re-wrapped.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016TUrhcggSFrYctvp5dsV1A
@os-zhuang
os-zhuang marked this pull request as ready for review August 29, 2026 14:34
@os-zhuang
os-zhuang merged commit 9597116 into mainAug 29, 2026
3 checks passed
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[finding] No page defines the org-membership tier vocabulary — delegated_admin is addressable in two places and explained in none

2 participants

@os-zhuang@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

docs(configure): define the org-membership tier vocabulary on the users page - #245

Merged
os-zhuang merged 1 commit into
mainfrom
claude/issue-153-org-membership-tier-vocabulary
Aug 29, 2026
Merged

docs(configure): define the org-membership tier vocabulary on the users page#245
os-zhuang merged 1 commit into
mainfrom
claude/issue-153-org-membership-tier-vocabulary

Conversation

@os-zhuang

Copy link
Copy Markdown
Contributor

Fixes#153

The four-value org-membership tier vocabulary (owner, admin, delegated_admin,
member — ADR-0108, sys_member.role) was used on two pages and defined on none.
A reader who met delegated_admin had nowhere in the corpus to go, and the nearest
thing they would reach for — "Delegated administration" on the permission-sets page —
is a different mechanism.

Where the section went, and why

content/docs/configure/users.mdx, in a new ## Organization-membership tiers
section. Three reasons, all checkable:

  1. configure/users.mdx:12 already lists the objects the page covers and sys_member
    is among them. The page had claimed the object whose role field this is and then
    never defined its tiers — an omission on an existing owner page, not a new home.
  2. configure/permissions/ documents what a principal may do. Upstream is explicit
    that this tier is not that (objectstack:packages/spec/src/identity/membership-role.ts):
    "It carries NO ObjectStack authority by construction … Role = can reach the
    endpoint
    ; adminScope = what the endpoint permits." A tier that confers no
    authority, filed inside the authority model, invites the exact misreading the card
    exists to prevent.
  3. The same file states the placement principle: "the other two are rules that belong
    near what they govern." Near what it governs = near sys_member.

Position within the page: between "Add people" and "Place people in the org tree
(memberships)".
The tier is chosen at admission — it is a value on the invitation the
preceding section just described — so this is where the chronology puts it. It also
puts the two senses of "membership" side by side, which is where a reader conflates
them, so the distinction is drawn at the seam instead of pages apart. It is deliberately
not folded into the existing memberships section: that section is
sys_business_unit_member, a different object, and merging the two would merge two of
the three facts the spec names as "look like one — do not merge them".

What the section says, and what each claim was measured against

Everything below is read off objectstackorigin/main, not inferred from the name:

ClaimSource
The vocabulary is closed and platform-owned; nothing widens it at bootADR-0108 D1; membership-role.tsBUILTIN_MEMBERSHIP_ROLE_OPTIONS
owner may invite at any tierinvitation-role-cap.tsorgRoleGrade (owner = 3, the ceiling)
owner is the only tier that may remove another ownerbetter-auth's removeMember predicate, quoted in member-role-canonical.ts
admin may invite at any tier except ownerinvitationRoleCapFailure (requested grade may not exceed the issuer's) plus better-auth's creatorRole check
delegated_admin may issue invitations without being an org admin, and that reach is the whole of itMEMBERSHIP_ROLE_DELEGATED_ADMIN doc comment; isOrgAdminGrade returns false for it
An invitation may never confer a tier above the issuer's own; an issuer below admin may invite only as memberinvitationRoleCapFailure, both refusal branches
An app's own business role cannot be stored in either field, and an invitation naming one is rejected before any row is writtenADR-0108 D2 (ROLE_NOT_FOUND at better-auth's door)
owner/admin memberships are auto-granted an organization-admin permission set scoped to that organizationplugin-security/src/auto-org-admin-grant.ts

That last row is why the "grade, not a bundle" line can be stated without hand-waving:
it names where the visible power of owner/admin actually comes from (a permission
set, on the ordinary permission path), so the tier is not left looking like the source
of it.

delegated_admin was the value most at risk of being written wrong, so it gets its own
subsection rather than a table cell. Its "on its own" row is the narrow, measured claim
lets you invite a plain member, and place nobody — rather than anything the name
suggests: the invitation cap holds a below-admin issuer to plain member, and placement
authority comes solely from a separately-granted adminScope. Nothing about "what a
delegated admin may do" is claimed beyond what those two files enforce.

The two consuming pages

configure/notifications.mdx and build/automation/approvals.mdx now link to the new
section. These are link-only edits: no word is added or removed on either page —
the diff is a Markdown link wrapped around text that was already there, plus a re-wrap
so the lines stay under the files' existing width. The tier enumerations stay in place;
they are useful where they are, and removing them would have been the prose rewrite the
card ruled out.

Verification

All runs on the final commit 5c7c4a1, from the repo root, exit codes captured before
any pipe:

CheckResult
pnpm turbo run build type-check test --forceTasks: 3 successful, 3 total, wrapper exit 0
node .github/scripts/check-locale-surface.mjsexit 0 — "every advertised URL has a source file and every source file is advertised"
node apps/docs/scripts/gen-zh-hant.mjs --checkexit 0 — "73 generated file(s) match the zh-Hans sources byte for byte"
node .github/scripts/check-translation-ownership.mjsexit 0 — "touches 0 translation artifact(s) and 3 other file(s)"
node .github/scripts/check-translations.mjsexit 0 — "translations gate passed"
node .github/scripts/check-translation-output.mjs --self-testexit 0
node .github/scripts/check-node-floor.mjsexit 0

The locale-surface oracle is unchanged, and that is positive evidence. The gate
reports 79 logical pages over 8 locales = 397 docs entries. Adding a section adds no
page and drops none, and the claim is measured rather than asserted: the gate's oracle
inputs are the content/docs/**/*.mdx path set plus each file's frontmatter title:,
and that (path, title) set is byte-identical between b0b159b and 5c7c4a1 — 397
pairs on both sides, diff exit 0
. apps/docs/lib/i18n.ts is untouched, and
git diff --diff-filter=ADR over the range is empty.

Anchors resolve — checked against the built HTML, not assumed. No gate here catches
a broken in-page anchor. In .next/server/app/en/docs/configure/users.html:
id="organization-membership-tiers", id="delegated_admin-is-not-delegated-administration"
and id="place-people-in-the-org-tree-memberships" all present; the new
href="/docs/configure/users#organization-membership-tiers" appears once in the built
notifications page and twice in approvals; and the outbound
permission-sets#delegated-administration target exists in that page's built HTML. The
slug oracle was confirmed independently: github-slugger@2.0.0 reproduces the existing
#layer-1--identity link on this very page from its heading text.

The rendered section was read back out of the built HTML: two tables, nine rows, no raw
pipes — a malformed MDX table degrades to a paragraph silently and no gate catches that
either.

Scope

No changeset (this repo has none). No configure/permissions/ restructuring;
permission-sets.mdx is untouched (it was read in full, not as a snippet, because the
new section links into it). No locale siblings. No new page.

Possible follow-up, deliberately not done here because it is inside the section the card
put out of scope: the sys_member row of the Layer 1 identity table in
configure/permissions/index.mdx could also link to the new section.


Generated by Claude Code

…rs page
`sys_member.role`'s four tiers (`owner`, `admin`, `delegated_admin`,
`member` — ADR-0108) were used on two pages and defined on none, so a
reader who met `delegated_admin` had nowhere in the corpus to go.
Add a dedicated "Organization-membership tiers" section to
`configure/users.mdx` — the page that already lists `sys_member` among
the objects it covers. The section gives each tier a one-line meaning,
states that a tier is a grade deciding what you can reach rather than a
bundle of what you may do, separates `delegated_admin` from the
`adminScope` mechanism documented as "Delegated administration" on the
permission-sets page, and separates `sys_member` from the
`sys_business_unit_member` org-tree placement the page already covers.
The two consuming pages (`configure/notifications.mdx`,
`build/automation/approvals.mdx`) now link to it. Those are link-only
edits: no word is added or removed, only re-wrapped.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016TUrhcggSFrYctvp5dsV1A
@os-zhuang
os-zhuang marked this pull request as ready for review August 29, 2026 14:34
@os-zhuang
os-zhuang merged commit 9597116 into mainAug 29, 2026
3 checks passed
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[finding] No page defines the org-membership tier vocabulary — delegated_admin is addressable in two places and explained in none

2 participants

@os-zhuang@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

docs(configure): define the org-membership tier vocabulary on the users page - #245

Merged
os-zhuang merged 1 commit into
mainfrom
claude/issue-153-org-membership-tier-vocabulary
Aug 29, 2026
Merged

docs(configure): define the org-membership tier vocabulary on the users page#245
os-zhuang merged 1 commit into
mainfrom
claude/issue-153-org-membership-tier-vocabulary

Conversation

@os-zhuang

Copy link
Copy Markdown
Contributor

Fixes#153

The four-value org-membership tier vocabulary (owner, admin, delegated_admin,
member — ADR-0108, sys_member.role) was used on two pages and defined on none.
A reader who met delegated_admin had nowhere in the corpus to go, and the nearest
thing they would reach for — "Delegated administration" on the permission-sets page —
is a different mechanism.

Where the section went, and why

content/docs/configure/users.mdx, in a new ## Organization-membership tiers
section. Three reasons, all checkable:

  1. configure/users.mdx:12 already lists the objects the page covers and sys_member
    is among them. The page had claimed the object whose role field this is and then
    never defined its tiers — an omission on an existing owner page, not a new home.
  2. configure/permissions/ documents what a principal may do. Upstream is explicit
    that this tier is not that (objectstack:packages/spec/src/identity/membership-role.ts):
    "It carries NO ObjectStack authority by construction … Role = can reach the
    endpoint
    ; adminScope = what the endpoint permits." A tier that confers no
    authority, filed inside the authority model, invites the exact misreading the card
    exists to prevent.
  3. The same file states the placement principle: "the other two are rules that belong
    near what they govern." Near what it governs = near sys_member.

Position within the page: between "Add people" and "Place people in the org tree
(memberships)".
The tier is chosen at admission — it is a value on the invitation the
preceding section just described — so this is where the chronology puts it. It also
puts the two senses of "membership" side by side, which is where a reader conflates
them, so the distinction is drawn at the seam instead of pages apart. It is deliberately
not folded into the existing memberships section: that section is
sys_business_unit_member, a different object, and merging the two would merge two of
the three facts the spec names as "look like one — do not merge them".

What the section says, and what each claim was measured against

Everything below is read off objectstackorigin/main, not inferred from the name:

ClaimSource
The vocabulary is closed and platform-owned; nothing widens it at bootADR-0108 D1; membership-role.tsBUILTIN_MEMBERSHIP_ROLE_OPTIONS
owner may invite at any tierinvitation-role-cap.tsorgRoleGrade (owner = 3, the ceiling)
owner is the only tier that may remove another ownerbetter-auth's removeMember predicate, quoted in member-role-canonical.ts
admin may invite at any tier except ownerinvitationRoleCapFailure (requested grade may not exceed the issuer's) plus better-auth's creatorRole check
delegated_admin may issue invitations without being an org admin, and that reach is the whole of itMEMBERSHIP_ROLE_DELEGATED_ADMIN doc comment; isOrgAdminGrade returns false for it
An invitation may never confer a tier above the issuer's own; an issuer below admin may invite only as memberinvitationRoleCapFailure, both refusal branches
An app's own business role cannot be stored in either field, and an invitation naming one is rejected before any row is writtenADR-0108 D2 (ROLE_NOT_FOUND at better-auth's door)
owner/admin memberships are auto-granted an organization-admin permission set scoped to that organizationplugin-security/src/auto-org-admin-grant.ts

That last row is why the "grade, not a bundle" line can be stated without hand-waving:
it names where the visible power of owner/admin actually comes from (a permission
set, on the ordinary permission path), so the tier is not left looking like the source
of it.

delegated_admin was the value most at risk of being written wrong, so it gets its own
subsection rather than a table cell. Its "on its own" row is the narrow, measured claim
lets you invite a plain member, and place nobody — rather than anything the name
suggests: the invitation cap holds a below-admin issuer to plain member, and placement
authority comes solely from a separately-granted adminScope. Nothing about "what a
delegated admin may do" is claimed beyond what those two files enforce.

The two consuming pages

configure/notifications.mdx and build/automation/approvals.mdx now link to the new
section. These are link-only edits: no word is added or removed on either page —
the diff is a Markdown link wrapped around text that was already there, plus a re-wrap
so the lines stay under the files' existing width. The tier enumerations stay in place;
they are useful where they are, and removing them would have been the prose rewrite the
card ruled out.

Verification

All runs on the final commit 5c7c4a1, from the repo root, exit codes captured before
any pipe:

CheckResult
pnpm turbo run build type-check test --forceTasks: 3 successful, 3 total, wrapper exit 0
node .github/scripts/check-locale-surface.mjsexit 0 — "every advertised URL has a source file and every source file is advertised"
node apps/docs/scripts/gen-zh-hant.mjs --checkexit 0 — "73 generated file(s) match the zh-Hans sources byte for byte"
node .github/scripts/check-translation-ownership.mjsexit 0 — "touches 0 translation artifact(s) and 3 other file(s)"
node .github/scripts/check-translations.mjsexit 0 — "translations gate passed"
node .github/scripts/check-translation-output.mjs --self-testexit 0
node .github/scripts/check-node-floor.mjsexit 0

The locale-surface oracle is unchanged, and that is positive evidence. The gate
reports 79 logical pages over 8 locales = 397 docs entries. Adding a section adds no
page and drops none, and the claim is measured rather than asserted: the gate's oracle
inputs are the content/docs/**/*.mdx path set plus each file's frontmatter title:,
and that (path, title) set is byte-identical between b0b159b and 5c7c4a1 — 397
pairs on both sides, diff exit 0
. apps/docs/lib/i18n.ts is untouched, and
git diff --diff-filter=ADR over the range is empty.

Anchors resolve — checked against the built HTML, not assumed. No gate here catches
a broken in-page anchor. In .next/server/app/en/docs/configure/users.html:
id="organization-membership-tiers", id="delegated_admin-is-not-delegated-administration"
and id="place-people-in-the-org-tree-memberships" all present; the new
href="/docs/configure/users#organization-membership-tiers" appears once in the built
notifications page and twice in approvals; and the outbound
permission-sets#delegated-administration target exists in that page's built HTML. The
slug oracle was confirmed independently: github-slugger@2.0.0 reproduces the existing
#layer-1--identity link on this very page from its heading text.

The rendered section was read back out of the built HTML: two tables, nine rows, no raw
pipes — a malformed MDX table degrades to a paragraph silently and no gate catches that
either.

Scope

No changeset (this repo has none). No configure/permissions/ restructuring;
permission-sets.mdx is untouched (it was read in full, not as a snippet, because the
new section links into it). No locale siblings. No new page.

Possible follow-up, deliberately not done here because it is inside the section the card
put out of scope: the sys_member row of the Layer 1 identity table in
configure/permissions/index.mdx could also link to the new section.


Generated by Claude Code

…rs page
`sys_member.role`'s four tiers (`owner`, `admin`, `delegated_admin`,
`member` — ADR-0108) were used on two pages and defined on none, so a
reader who met `delegated_admin` had nowhere in the corpus to go.
Add a dedicated "Organization-membership tiers" section to
`configure/users.mdx` — the page that already lists `sys_member` among
the objects it covers. The section gives each tier a one-line meaning,
states that a tier is a grade deciding what you can reach rather than a
bundle of what you may do, separates `delegated_admin` from the
`adminScope` mechanism documented as "Delegated administration" on the
permission-sets page, and separates `sys_member` from the
`sys_business_unit_member` org-tree placement the page already covers.
The two consuming pages (`configure/notifications.mdx`,
`build/automation/approvals.mdx`) now link to it. Those are link-only
edits: no word is added or removed, only re-wrapped.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016TUrhcggSFrYctvp5dsV1A
@os-zhuang
os-zhuang marked this pull request as ready for review August 29, 2026 14:34
@os-zhuang
os-zhuang merged commit 9597116 into mainAug 29, 2026
3 checks passed
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[finding] No page defines the org-membership tier vocabulary — delegated_admin is addressable in two places and explained in none

2 participants

@os-zhuang@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

docs(configure): define the org-membership tier vocabulary on the users page - #245

Merged
os-zhuang merged 1 commit into
mainfrom
claude/issue-153-org-membership-tier-vocabulary
Aug 29, 2026
Merged

docs(configure): define the org-membership tier vocabulary on the users page#245
os-zhuang merged 1 commit into
mainfrom
claude/issue-153-org-membership-tier-vocabulary

Conversation

@os-zhuang

Copy link
Copy Markdown
Contributor

Fixes#153

The four-value org-membership tier vocabulary (owner, admin, delegated_admin,
member — ADR-0108, sys_member.role) was used on two pages and defined on none.
A reader who met delegated_admin had nowhere in the corpus to go, and the nearest
thing they would reach for — "Delegated administration" on the permission-sets page —
is a different mechanism.

Where the section went, and why

content/docs/configure/users.mdx, in a new ## Organization-membership tiers
section. Three reasons, all checkable:

  1. configure/users.mdx:12 already lists the objects the page covers and sys_member
    is among them. The page had claimed the object whose role field this is and then
    never defined its tiers — an omission on an existing owner page, not a new home.
  2. configure/permissions/ documents what a principal may do. Upstream is explicit
    that this tier is not that (objectstack:packages/spec/src/identity/membership-role.ts):
    "It carries NO ObjectStack authority by construction … Role = can reach the
    endpoint
    ; adminScope = what the endpoint permits." A tier that confers no
    authority, filed inside the authority model, invites the exact misreading the card
    exists to prevent.
  3. The same file states the placement principle: "the other two are rules that belong
    near what they govern." Near what it governs = near sys_member.

Position within the page: between "Add people" and "Place people in the org tree
(memberships)".
The tier is chosen at admission — it is a value on the invitation the
preceding section just described — so this is where the chronology puts it. It also
puts the two senses of "membership" side by side, which is where a reader conflates
them, so the distinction is drawn at the seam instead of pages apart. It is deliberately
not folded into the existing memberships section: that section is
sys_business_unit_member, a different object, and merging the two would merge two of
the three facts the spec names as "look like one — do not merge them".

What the section says, and what each claim was measured against

Everything below is read off objectstackorigin/main, not inferred from the name:

ClaimSource
The vocabulary is closed and platform-owned; nothing widens it at bootADR-0108 D1; membership-role.tsBUILTIN_MEMBERSHIP_ROLE_OPTIONS
owner may invite at any tierinvitation-role-cap.tsorgRoleGrade (owner = 3, the ceiling)
owner is the only tier that may remove another ownerbetter-auth's removeMember predicate, quoted in member-role-canonical.ts
admin may invite at any tier except ownerinvitationRoleCapFailure (requested grade may not exceed the issuer's) plus better-auth's creatorRole check
delegated_admin may issue invitations without being an org admin, and that reach is the whole of itMEMBERSHIP_ROLE_DELEGATED_ADMIN doc comment; isOrgAdminGrade returns false for it
An invitation may never confer a tier above the issuer's own; an issuer below admin may invite only as memberinvitationRoleCapFailure, both refusal branches
An app's own business role cannot be stored in either field, and an invitation naming one is rejected before any row is writtenADR-0108 D2 (ROLE_NOT_FOUND at better-auth's door)
owner/admin memberships are auto-granted an organization-admin permission set scoped to that organizationplugin-security/src/auto-org-admin-grant.ts

That last row is why the "grade, not a bundle" line can be stated without hand-waving:
it names where the visible power of owner/admin actually comes from (a permission
set, on the ordinary permission path), so the tier is not left looking like the source
of it.

delegated_admin was the value most at risk of being written wrong, so it gets its own
subsection rather than a table cell. Its "on its own" row is the narrow, measured claim
lets you invite a plain member, and place nobody — rather than anything the name
suggests: the invitation cap holds a below-admin issuer to plain member, and placement
authority comes solely from a separately-granted adminScope. Nothing about "what a
delegated admin may do" is claimed beyond what those two files enforce.

The two consuming pages

configure/notifications.mdx and build/automation/approvals.mdx now link to the new
section. These are link-only edits: no word is added or removed on either page —
the diff is a Markdown link wrapped around text that was already there, plus a re-wrap
so the lines stay under the files' existing width. The tier enumerations stay in place;
they are useful where they are, and removing them would have been the prose rewrite the
card ruled out.

Verification

All runs on the final commit 5c7c4a1, from the repo root, exit codes captured before
any pipe:

CheckResult
pnpm turbo run build type-check test --forceTasks: 3 successful, 3 total, wrapper exit 0
node .github/scripts/check-locale-surface.mjsexit 0 — "every advertised URL has a source file and every source file is advertised"
node apps/docs/scripts/gen-zh-hant.mjs --checkexit 0 — "73 generated file(s) match the zh-Hans sources byte for byte"
node .github/scripts/check-translation-ownership.mjsexit 0 — "touches 0 translation artifact(s) and 3 other file(s)"
node .github/scripts/check-translations.mjsexit 0 — "translations gate passed"
node .github/scripts/check-translation-output.mjs --self-testexit 0
node .github/scripts/check-node-floor.mjsexit 0

The locale-surface oracle is unchanged, and that is positive evidence. The gate
reports 79 logical pages over 8 locales = 397 docs entries. Adding a section adds no
page and drops none, and the claim is measured rather than asserted: the gate's oracle
inputs are the content/docs/**/*.mdx path set plus each file's frontmatter title:,
and that (path, title) set is byte-identical between b0b159b and 5c7c4a1 — 397
pairs on both sides, diff exit 0
. apps/docs/lib/i18n.ts is untouched, and
git diff --diff-filter=ADR over the range is empty.

Anchors resolve — checked against the built HTML, not assumed. No gate here catches
a broken in-page anchor. In .next/server/app/en/docs/configure/users.html:
id="organization-membership-tiers", id="delegated_admin-is-not-delegated-administration"
and id="place-people-in-the-org-tree-memberships" all present; the new
href="/docs/configure/users#organization-membership-tiers" appears once in the built
notifications page and twice in approvals; and the outbound
permission-sets#delegated-administration target exists in that page's built HTML. The
slug oracle was confirmed independently: github-slugger@2.0.0 reproduces the existing
#layer-1--identity link on this very page from its heading text.

The rendered section was read back out of the built HTML: two tables, nine rows, no raw
pipes — a malformed MDX table degrades to a paragraph silently and no gate catches that
either.

Scope

No changeset (this repo has none). No configure/permissions/ restructuring;
permission-sets.mdx is untouched (it was read in full, not as a snippet, because the
new section links into it). No locale siblings. No new page.

Possible follow-up, deliberately not done here because it is inside the section the card
put out of scope: the sys_member row of the Layer 1 identity table in
configure/permissions/index.mdx could also link to the new section.


Generated by Claude Code

…rs page
`sys_member.role`'s four tiers (`owner`, `admin`, `delegated_admin`,
`member` — ADR-0108) were used on two pages and defined on none, so a
reader who met `delegated_admin` had nowhere in the corpus to go.
Add a dedicated "Organization-membership tiers" section to
`configure/users.mdx` — the page that already lists `sys_member` among
the objects it covers. The section gives each tier a one-line meaning,
states that a tier is a grade deciding what you can reach rather than a
bundle of what you may do, separates `delegated_admin` from the
`adminScope` mechanism documented as "Delegated administration" on the
permission-sets page, and separates `sys_member` from the
`sys_business_unit_member` org-tree placement the page already covers.
The two consuming pages (`configure/notifications.mdx`,
`build/automation/approvals.mdx`) now link to it. Those are link-only
edits: no word is added or removed, only re-wrapped.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016TUrhcggSFrYctvp5dsV1A
@os-zhuang
os-zhuang marked this pull request as ready for review August 29, 2026 14:34
@os-zhuang
os-zhuang merged commit 9597116 into mainAug 29, 2026
3 checks passed
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[finding] No page defines the org-membership tier vocabulary — delegated_admin is addressable in two places and explained in none

2 participants

@os-zhuang@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

docs(configure): define the org-membership tier vocabulary on the users page - #245

Merged
os-zhuang merged 1 commit into
mainfrom
claude/issue-153-org-membership-tier-vocabulary
Aug 29, 2026
Merged

docs(configure): define the org-membership tier vocabulary on the users page#245
os-zhuang merged 1 commit into
mainfrom
claude/issue-153-org-membership-tier-vocabulary

Conversation

@os-zhuang

Copy link
Copy Markdown
Contributor

Fixes#153

The four-value org-membership tier vocabulary (owner, admin, delegated_admin,
member — ADR-0108, sys_member.role) was used on two pages and defined on none.
A reader who met delegated_admin had nowhere in the corpus to go, and the nearest
thing they would reach for — "Delegated administration" on the permission-sets page —
is a different mechanism.

Where the section went, and why

content/docs/configure/users.mdx, in a new ## Organization-membership tiers
section. Three reasons, all checkable:

  1. configure/users.mdx:12 already lists the objects the page covers and sys_member
    is among them. The page had claimed the object whose role field this is and then
    never defined its tiers — an omission on an existing owner page, not a new home.
  2. configure/permissions/ documents what a principal may do. Upstream is explicit
    that this tier is not that (objectstack:packages/spec/src/identity/membership-role.ts):
    "It carries NO ObjectStack authority by construction … Role = can reach the
    endpoint
    ; adminScope = what the endpoint permits." A tier that confers no
    authority, filed inside the authority model, invites the exact misreading the card
    exists to prevent.
  3. The same file states the placement principle: "the other two are rules that belong
    near what they govern." Near what it governs = near sys_member.

Position within the page: between "Add people" and "Place people in the org tree
(memberships)".
The tier is chosen at admission — it is a value on the invitation the
preceding section just described — so this is where the chronology puts it. It also
puts the two senses of "membership" side by side, which is where a reader conflates
them, so the distinction is drawn at the seam instead of pages apart. It is deliberately
not folded into the existing memberships section: that section is
sys_business_unit_member, a different object, and merging the two would merge two of
the three facts the spec names as "look like one — do not merge them".

What the section says, and what each claim was measured against

Everything below is read off objectstackorigin/main, not inferred from the name:

ClaimSource
The vocabulary is closed and platform-owned; nothing widens it at bootADR-0108 D1; membership-role.tsBUILTIN_MEMBERSHIP_ROLE_OPTIONS
owner may invite at any tierinvitation-role-cap.tsorgRoleGrade (owner = 3, the ceiling)
owner is the only tier that may remove another ownerbetter-auth's removeMember predicate, quoted in member-role-canonical.ts
admin may invite at any tier except ownerinvitationRoleCapFailure (requested grade may not exceed the issuer's) plus better-auth's creatorRole check
delegated_admin may issue invitations without being an org admin, and that reach is the whole of itMEMBERSHIP_ROLE_DELEGATED_ADMIN doc comment; isOrgAdminGrade returns false for it
An invitation may never confer a tier above the issuer's own; an issuer below admin may invite only as memberinvitationRoleCapFailure, both refusal branches
An app's own business role cannot be stored in either field, and an invitation naming one is rejected before any row is writtenADR-0108 D2 (ROLE_NOT_FOUND at better-auth's door)
owner/admin memberships are auto-granted an organization-admin permission set scoped to that organizationplugin-security/src/auto-org-admin-grant.ts

That last row is why the "grade, not a bundle" line can be stated without hand-waving:
it names where the visible power of owner/admin actually comes from (a permission
set, on the ordinary permission path), so the tier is not left looking like the source
of it.

delegated_admin was the value most at risk of being written wrong, so it gets its own
subsection rather than a table cell. Its "on its own" row is the narrow, measured claim
lets you invite a plain member, and place nobody — rather than anything the name
suggests: the invitation cap holds a below-admin issuer to plain member, and placement
authority comes solely from a separately-granted adminScope. Nothing about "what a
delegated admin may do" is claimed beyond what those two files enforce.

The two consuming pages

configure/notifications.mdx and build/automation/approvals.mdx now link to the new
section. These are link-only edits: no word is added or removed on either page —
the diff is a Markdown link wrapped around text that was already there, plus a re-wrap
so the lines stay under the files' existing width. The tier enumerations stay in place;
they are useful where they are, and removing them would have been the prose rewrite the
card ruled out.

Verification

All runs on the final commit 5c7c4a1, from the repo root, exit codes captured before
any pipe:

CheckResult
pnpm turbo run build type-check test --forceTasks: 3 successful, 3 total, wrapper exit 0
node .github/scripts/check-locale-surface.mjsexit 0 — "every advertised URL has a source file and every source file is advertised"
node apps/docs/scripts/gen-zh-hant.mjs --checkexit 0 — "73 generated file(s) match the zh-Hans sources byte for byte"
node .github/scripts/check-translation-ownership.mjsexit 0 — "touches 0 translation artifact(s) and 3 other file(s)"
node .github/scripts/check-translations.mjsexit 0 — "translations gate passed"
node .github/scripts/check-translation-output.mjs --self-testexit 0
node .github/scripts/check-node-floor.mjsexit 0

The locale-surface oracle is unchanged, and that is positive evidence. The gate
reports 79 logical pages over 8 locales = 397 docs entries. Adding a section adds no
page and drops none, and the claim is measured rather than asserted: the gate's oracle
inputs are the content/docs/**/*.mdx path set plus each file's frontmatter title:,
and that (path, title) set is byte-identical between b0b159b and 5c7c4a1 — 397
pairs on both sides, diff exit 0
. apps/docs/lib/i18n.ts is untouched, and
git diff --diff-filter=ADR over the range is empty.

Anchors resolve — checked against the built HTML, not assumed. No gate here catches
a broken in-page anchor. In .next/server/app/en/docs/configure/users.html:
id="organization-membership-tiers", id="delegated_admin-is-not-delegated-administration"
and id="place-people-in-the-org-tree-memberships" all present; the new
href="/docs/configure/users#organization-membership-tiers" appears once in the built
notifications page and twice in approvals; and the outbound
permission-sets#delegated-administration target exists in that page's built HTML. The
slug oracle was confirmed independently: github-slugger@2.0.0 reproduces the existing
#layer-1--identity link on this very page from its heading text.

The rendered section was read back out of the built HTML: two tables, nine rows, no raw
pipes — a malformed MDX table degrades to a paragraph silently and no gate catches that
either.

Scope

No changeset (this repo has none). No configure/permissions/ restructuring;
permission-sets.mdx is untouched (it was read in full, not as a snippet, because the
new section links into it). No locale siblings. No new page.

Possible follow-up, deliberately not done here because it is inside the section the card
put out of scope: the sys_member row of the Layer 1 identity table in
configure/permissions/index.mdx could also link to the new section.


Generated by Claude Code

…rs page
`sys_member.role`'s four tiers (`owner`, `admin`, `delegated_admin`,
`member` — ADR-0108) were used on two pages and defined on none, so a
reader who met `delegated_admin` had nowhere in the corpus to go.
Add a dedicated "Organization-membership tiers" section to
`configure/users.mdx` — the page that already lists `sys_member` among
the objects it covers. The section gives each tier a one-line meaning,
states that a tier is a grade deciding what you can reach rather than a
bundle of what you may do, separates `delegated_admin` from the
`adminScope` mechanism documented as "Delegated administration" on the
permission-sets page, and separates `sys_member` from the
`sys_business_unit_member` org-tree placement the page already covers.
The two consuming pages (`configure/notifications.mdx`,
`build/automation/approvals.mdx`) now link to it. Those are link-only
edits: no word is added or removed, only re-wrapped.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016TUrhcggSFrYctvp5dsV1A
@os-zhuang
os-zhuang marked this pull request as ready for review August 29, 2026 14:34
@os-zhuang
os-zhuang merged commit 9597116 into mainAug 29, 2026
3 checks passed
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[finding] No page defines the org-membership tier vocabulary — delegated_admin is addressable in two places and explained in none

2 participants

@os-zhuang@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

docs(configure): define the org-membership tier vocabulary on the users page - #245

Merged
os-zhuang merged 1 commit into
mainfrom
claude/issue-153-org-membership-tier-vocabulary
Aug 29, 2026
Merged

docs(configure): define the org-membership tier vocabulary on the users page#245
os-zhuang merged 1 commit into
mainfrom
claude/issue-153-org-membership-tier-vocabulary

Conversation

@os-zhuang

Copy link
Copy Markdown
Contributor

Fixes#153

The four-value org-membership tier vocabulary (owner, admin, delegated_admin,
member — ADR-0108, sys_member.role) was used on two pages and defined on none.
A reader who met delegated_admin had nowhere in the corpus to go, and the nearest
thing they would reach for — "Delegated administration" on the permission-sets page —
is a different mechanism.

Where the section went, and why

content/docs/configure/users.mdx, in a new ## Organization-membership tiers
section. Three reasons, all checkable:

  1. configure/users.mdx:12 already lists the objects the page covers and sys_member
    is among them. The page had claimed the object whose role field this is and then
    never defined its tiers — an omission on an existing owner page, not a new home.
  2. configure/permissions/ documents what a principal may do. Upstream is explicit
    that this tier is not that (objectstack:packages/spec/src/identity/membership-role.ts):
    "It carries NO ObjectStack authority by construction … Role = can reach the
    endpoint
    ; adminScope = what the endpoint permits." A tier that confers no
    authority, filed inside the authority model, invites the exact misreading the card
    exists to prevent.
  3. The same file states the placement principle: "the other two are rules that belong
    near what they govern." Near what it governs = near sys_member.

Position within the page: between "Add people" and "Place people in the org tree
(memberships)".
The tier is chosen at admission — it is a value on the invitation the
preceding section just described — so this is where the chronology puts it. It also
puts the two senses of "membership" side by side, which is where a reader conflates
them, so the distinction is drawn at the seam instead of pages apart. It is deliberately
not folded into the existing memberships section: that section is
sys_business_unit_member, a different object, and merging the two would merge two of
the three facts the spec names as "look like one — do not merge them".

What the section says, and what each claim was measured against

Everything below is read off objectstackorigin/main, not inferred from the name:

ClaimSource
The vocabulary is closed and platform-owned; nothing widens it at bootADR-0108 D1; membership-role.tsBUILTIN_MEMBERSHIP_ROLE_OPTIONS
owner may invite at any tierinvitation-role-cap.tsorgRoleGrade (owner = 3, the ceiling)
owner is the only tier that may remove another ownerbetter-auth's removeMember predicate, quoted in member-role-canonical.ts
admin may invite at any tier except ownerinvitationRoleCapFailure (requested grade may not exceed the issuer's) plus better-auth's creatorRole check
delegated_admin may issue invitations without being an org admin, and that reach is the whole of itMEMBERSHIP_ROLE_DELEGATED_ADMIN doc comment; isOrgAdminGrade returns false for it
An invitation may never confer a tier above the issuer's own; an issuer below admin may invite only as memberinvitationRoleCapFailure, both refusal branches
An app's own business role cannot be stored in either field, and an invitation naming one is rejected before any row is writtenADR-0108 D2 (ROLE_NOT_FOUND at better-auth's door)
owner/admin memberships are auto-granted an organization-admin permission set scoped to that organizationplugin-security/src/auto-org-admin-grant.ts

That last row is why the "grade, not a bundle" line can be stated without hand-waving:
it names where the visible power of owner/admin actually comes from (a permission
set, on the ordinary permission path), so the tier is not left looking like the source
of it.

delegated_admin was the value most at risk of being written wrong, so it gets its own
subsection rather than a table cell. Its "on its own" row is the narrow, measured claim
lets you invite a plain member, and place nobody — rather than anything the name
suggests: the invitation cap holds a below-admin issuer to plain member, and placement
authority comes solely from a separately-granted adminScope. Nothing about "what a
delegated admin may do" is claimed beyond what those two files enforce.

The two consuming pages

configure/notifications.mdx and build/automation/approvals.mdx now link to the new
section. These are link-only edits: no word is added or removed on either page —
the diff is a Markdown link wrapped around text that was already there, plus a re-wrap
so the lines stay under the files' existing width. The tier enumerations stay in place;
they are useful where they are, and removing them would have been the prose rewrite the
card ruled out.

Verification

All runs on the final commit 5c7c4a1, from the repo root, exit codes captured before
any pipe:

CheckResult
pnpm turbo run build type-check test --forceTasks: 3 successful, 3 total, wrapper exit 0
node .github/scripts/check-locale-surface.mjsexit 0 — "every advertised URL has a source file and every source file is advertised"
node apps/docs/scripts/gen-zh-hant.mjs --checkexit 0 — "73 generated file(s) match the zh-Hans sources byte for byte"
node .github/scripts/check-translation-ownership.mjsexit 0 — "touches 0 translation artifact(s) and 3 other file(s)"
node .github/scripts/check-translations.mjsexit 0 — "translations gate passed"
node .github/scripts/check-translation-output.mjs --self-testexit 0
node .github/scripts/check-node-floor.mjsexit 0

The locale-surface oracle is unchanged, and that is positive evidence. The gate
reports 79 logical pages over 8 locales = 397 docs entries. Adding a section adds no
page and drops none, and the claim is measured rather than asserted: the gate's oracle
inputs are the content/docs/**/*.mdx path set plus each file's frontmatter title:,
and that (path, title) set is byte-identical between b0b159b and 5c7c4a1 — 397
pairs on both sides, diff exit 0
. apps/docs/lib/i18n.ts is untouched, and
git diff --diff-filter=ADR over the range is empty.

Anchors resolve — checked against the built HTML, not assumed. No gate here catches
a broken in-page anchor. In .next/server/app/en/docs/configure/users.html:
id="organization-membership-tiers", id="delegated_admin-is-not-delegated-administration"
and id="place-people-in-the-org-tree-memberships" all present; the new
href="/docs/configure/users#organization-membership-tiers" appears once in the built
notifications page and twice in approvals; and the outbound
permission-sets#delegated-administration target exists in that page's built HTML. The
slug oracle was confirmed independently: github-slugger@2.0.0 reproduces the existing
#layer-1--identity link on this very page from its heading text.

The rendered section was read back out of the built HTML: two tables, nine rows, no raw
pipes — a malformed MDX table degrades to a paragraph silently and no gate catches that
either.

Scope

No changeset (this repo has none). No configure/permissions/ restructuring;
permission-sets.mdx is untouched (it was read in full, not as a snippet, because the
new section links into it). No locale siblings. No new page.

Possible follow-up, deliberately not done here because it is inside the section the card
put out of scope: the sys_member row of the Layer 1 identity table in
configure/permissions/index.mdx could also link to the new section.


Generated by Claude Code

…rs page
`sys_member.role`'s four tiers (`owner`, `admin`, `delegated_admin`,
`member` — ADR-0108) were used on two pages and defined on none, so a
reader who met `delegated_admin` had nowhere in the corpus to go.
Add a dedicated "Organization-membership tiers" section to
`configure/users.mdx` — the page that already lists `sys_member` among
the objects it covers. The section gives each tier a one-line meaning,
states that a tier is a grade deciding what you can reach rather than a
bundle of what you may do, separates `delegated_admin` from the
`adminScope` mechanism documented as "Delegated administration" on the
permission-sets page, and separates `sys_member` from the
`sys_business_unit_member` org-tree placement the page already covers.
The two consuming pages (`configure/notifications.mdx`,
`build/automation/approvals.mdx`) now link to it. Those are link-only
edits: no word is added or removed, only re-wrapped.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016TUrhcggSFrYctvp5dsV1A
@os-zhuang
os-zhuang marked this pull request as ready for review August 29, 2026 14:34
@os-zhuang
os-zhuang merged commit 9597116 into mainAug 29, 2026
3 checks passed
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[finding] No page defines the org-membership tier vocabulary — delegated_admin is addressable in two places and explained in none

2 participants

@os-zhuang@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

docs(configure): define the org-membership tier vocabulary on the users page - #245

Merged
os-zhuang merged 1 commit into
mainfrom
claude/issue-153-org-membership-tier-vocabulary
Aug 29, 2026
Merged

docs(configure): define the org-membership tier vocabulary on the users page#245
os-zhuang merged 1 commit into
mainfrom
claude/issue-153-org-membership-tier-vocabulary

Conversation

@os-zhuang

Copy link
Copy Markdown
Contributor

Fixes#153

The four-value org-membership tier vocabulary (owner, admin, delegated_admin,
member — ADR-0108, sys_member.role) was used on two pages and defined on none.
A reader who met delegated_admin had nowhere in the corpus to go, and the nearest
thing they would reach for — "Delegated administration" on the permission-sets page —
is a different mechanism.

Where the section went, and why

content/docs/configure/users.mdx, in a new ## Organization-membership tiers
section. Three reasons, all checkable:

  1. configure/users.mdx:12 already lists the objects the page covers and sys_member
    is among them. The page had claimed the object whose role field this is and then
    never defined its tiers — an omission on an existing owner page, not a new home.
  2. configure/permissions/ documents what a principal may do. Upstream is explicit
    that this tier is not that (objectstack:packages/spec/src/identity/membership-role.ts):
    "It carries NO ObjectStack authority by construction … Role = can reach the
    endpoint
    ; adminScope = what the endpoint permits." A tier that confers no
    authority, filed inside the authority model, invites the exact misreading the card
    exists to prevent.
  3. The same file states the placement principle: "the other two are rules that belong
    near what they govern." Near what it governs = near sys_member.

Position within the page: between "Add people" and "Place people in the org tree
(memberships)".
The tier is chosen at admission — it is a value on the invitation the
preceding section just described — so this is where the chronology puts it. It also
puts the two senses of "membership" side by side, which is where a reader conflates
them, so the distinction is drawn at the seam instead of pages apart. It is deliberately
not folded into the existing memberships section: that section is
sys_business_unit_member, a different object, and merging the two would merge two of
the three facts the spec names as "look like one — do not merge them".

What the section says, and what each claim was measured against

Everything below is read off objectstackorigin/main, not inferred from the name:

ClaimSource
The vocabulary is closed and platform-owned; nothing widens it at bootADR-0108 D1; membership-role.tsBUILTIN_MEMBERSHIP_ROLE_OPTIONS
owner may invite at any tierinvitation-role-cap.tsorgRoleGrade (owner = 3, the ceiling)
owner is the only tier that may remove another ownerbetter-auth's removeMember predicate, quoted in member-role-canonical.ts
admin may invite at any tier except ownerinvitationRoleCapFailure (requested grade may not exceed the issuer's) plus better-auth's creatorRole check
delegated_admin may issue invitations without being an org admin, and that reach is the whole of itMEMBERSHIP_ROLE_DELEGATED_ADMIN doc comment; isOrgAdminGrade returns false for it
An invitation may never confer a tier above the issuer's own; an issuer below admin may invite only as memberinvitationRoleCapFailure, both refusal branches
An app's own business role cannot be stored in either field, and an invitation naming one is rejected before any row is writtenADR-0108 D2 (ROLE_NOT_FOUND at better-auth's door)
owner/admin memberships are auto-granted an organization-admin permission set scoped to that organizationplugin-security/src/auto-org-admin-grant.ts

That last row is why the "grade, not a bundle" line can be stated without hand-waving:
it names where the visible power of owner/admin actually comes from (a permission
set, on the ordinary permission path), so the tier is not left looking like the source
of it.

delegated_admin was the value most at risk of being written wrong, so it gets its own
subsection rather than a table cell. Its "on its own" row is the narrow, measured claim
lets you invite a plain member, and place nobody — rather than anything the name
suggests: the invitation cap holds a below-admin issuer to plain member, and placement
authority comes solely from a separately-granted adminScope. Nothing about "what a
delegated admin may do" is claimed beyond what those two files enforce.

The two consuming pages

configure/notifications.mdx and build/automation/approvals.mdx now link to the new
section. These are link-only edits: no word is added or removed on either page —
the diff is a Markdown link wrapped around text that was already there, plus a re-wrap
so the lines stay under the files' existing width. The tier enumerations stay in place;
they are useful where they are, and removing them would have been the prose rewrite the
card ruled out.

Verification

All runs on the final commit 5c7c4a1, from the repo root, exit codes captured before
any pipe:

CheckResult
pnpm turbo run build type-check test --forceTasks: 3 successful, 3 total, wrapper exit 0
node .github/scripts/check-locale-surface.mjsexit 0 — "every advertised URL has a source file and every source file is advertised"
node apps/docs/scripts/gen-zh-hant.mjs --checkexit 0 — "73 generated file(s) match the zh-Hans sources byte for byte"
node .github/scripts/check-translation-ownership.mjsexit 0 — "touches 0 translation artifact(s) and 3 other file(s)"
node .github/scripts/check-translations.mjsexit 0 — "translations gate passed"
node .github/scripts/check-translation-output.mjs --self-testexit 0
node .github/scripts/check-node-floor.mjsexit 0

The locale-surface oracle is unchanged, and that is positive evidence. The gate
reports 79 logical pages over 8 locales = 397 docs entries. Adding a section adds no
page and drops none, and the claim is measured rather than asserted: the gate's oracle
inputs are the content/docs/**/*.mdx path set plus each file's frontmatter title:,
and that (path, title) set is byte-identical between b0b159b and 5c7c4a1 — 397
pairs on both sides, diff exit 0
. apps/docs/lib/i18n.ts is untouched, and
git diff --diff-filter=ADR over the range is empty.

Anchors resolve — checked against the built HTML, not assumed. No gate here catches
a broken in-page anchor. In .next/server/app/en/docs/configure/users.html:
id="organization-membership-tiers", id="delegated_admin-is-not-delegated-administration"
and id="place-people-in-the-org-tree-memberships" all present; the new
href="/docs/configure/users#organization-membership-tiers" appears once in the built
notifications page and twice in approvals; and the outbound
permission-sets#delegated-administration target exists in that page's built HTML. The
slug oracle was confirmed independently: github-slugger@2.0.0 reproduces the existing
#layer-1--identity link on this very page from its heading text.

The rendered section was read back out of the built HTML: two tables, nine rows, no raw
pipes — a malformed MDX table degrades to a paragraph silently and no gate catches that
either.

Scope

No changeset (this repo has none). No configure/permissions/ restructuring;
permission-sets.mdx is untouched (it was read in full, not as a snippet, because the
new section links into it). No locale siblings. No new page.

Possible follow-up, deliberately not done here because it is inside the section the card
put out of scope: the sys_member row of the Layer 1 identity table in
configure/permissions/index.mdx could also link to the new section.


Generated by Claude Code

…rs page
`sys_member.role`'s four tiers (`owner`, `admin`, `delegated_admin`,
`member` — ADR-0108) were used on two pages and defined on none, so a
reader who met `delegated_admin` had nowhere in the corpus to go.
Add a dedicated "Organization-membership tiers" section to
`configure/users.mdx` — the page that already lists `sys_member` among
the objects it covers. The section gives each tier a one-line meaning,
states that a tier is a grade deciding what you can reach rather than a
bundle of what you may do, separates `delegated_admin` from the
`adminScope` mechanism documented as "Delegated administration" on the
permission-sets page, and separates `sys_member` from the
`sys_business_unit_member` org-tree placement the page already covers.
The two consuming pages (`configure/notifications.mdx`,
`build/automation/approvals.mdx`) now link to it. Those are link-only
edits: no word is added or removed, only re-wrapped.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016TUrhcggSFrYctvp5dsV1A
@os-zhuang
os-zhuang marked this pull request as ready for review August 29, 2026 14:34
@os-zhuang
os-zhuang merged commit 9597116 into mainAug 29, 2026
3 checks passed
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[finding] No page defines the org-membership tier vocabulary — delegated_admin is addressable in two places and explained in none

2 participants

@os-zhuang@claude