Uh oh!
There was an error while loading. Please reload this page.
fix(spec): derive ORG_MEMBERSHIP_LEVELS from BUILTIN_MEMBERSHIP_ROLES — delegated_admin becomes an addressable approver tier - #9942
Conversation
The approver tier list hand-spelled ['owner', 'admin', 'member'] while sys_member.role enforces four values including delegated_admin (ADR-0105 D8), so an enforced, storable tier could not be authored as an approver. Maintainer ruling 2026-08-19 (issue comment, Option B): this is drift, not deliberate exclusion — derive the list from the one membership vocabulary so the next tier addition cannot silently miss the approver surface. Also corrects the constant's provenance doc-comment: the vocabulary is ObjectStack's own closed membership-role list (ADR-0108), not better-auth's closed set — delegated_admin is ObjectStack's ADR-0105 D8 addition. Pins: spec asserts ORG_MEMBERSHIP_LEVELS === BUILTIN_MEMBERSHIP_ROLES as a list (so the derivation can never be silently replaced by a copy), that delegated_admin is admitted, and that the wire picker projection (APPROVER_VALUE_SOURCES, deprecated role alias included) carries the whole vocabulary; plugin-approvals pins that a delegated_admin approver expands to its members at runtime. Fixes#9806 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016D9wdJR14KKCxz1WgdAzcw
…in pin The TEST_DEBT ratchet (check:type-check-debt) is frozen at 348 raw errors for this package; the new pin's bare pending_approvers access added a 349th TS2339. Guard the union the way the position test does so the pin contributes zero new debt. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016D9wdJR14KKCxz1WgdAzcw
📓 Docs Drift CheckThis PR changes 1 package(s): 1 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
⛔ 1 release-owned page(s) also name something this change touched. These are read-only:
What this run could not seeCoarse fallback — 115 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 c28da976a4e66b9a37bb14f5475e1cd3e53f355a && git checkout c28da976a4e66b9a37bb14f5475e1cd3e53f355a
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 90417a8085ce3747a590e1ecb2c62fe859f195a2 8ec58a4db38b8f80816e0b2ecadfda71bb1a9b6d && git checkout -B drift-repro 90417a8085ce3747a590e1ecb2c62fe859f195a2 && git merge --no-ff 8ec58a4db38b8f80816e0b2ecadfda71bb1a9b6d
node scripts/docs-audit/affected-docs.mjs --json 90417a8085ce3747a590e1ecb2c62fe859f195a2
|
Uh oh!
There was an error while loading. Please reload this page.
Fixes#9806
Clause-② notice: this PR widens an accept set —
delegated_admin(ObjectStack's ADR-0105 D8 membership tier, already storable and enforced onsys_member.role) becomes a valid, offeredorg_membership_levelapprover value. Additive only; nothing is removed or narrowed. Draft by dispatch contract — the PM reviews and lands it.Ruling implemented
Maintainer ruling of 2026-08-19 on #9806 (comment 5339439775, Option B): this is drift, not deliberate exclusion. Derive
ORG_MEMBERSHIP_LEVELSfromBUILTIN_MEMBERSHIP_ROLES(single source) instead of re-spelling it, so the approver picker offers the wholesys_member.rolevocabulary and the next tier addition cannot silently miss the approver surface; fix the provenance doc-comment in the same change.Derivation shape chosen, and why
export const ORG_MEMBERSHIP_LEVELS = BUILTIN_MEMBERSHIP_ROLES;— a pure re-export alias, not a spread copy.BUILTIN_MEMBERSHIP_ROLESis anas constreadonly tuple of literal types, so the alias keeps the exact shape consumers relied on (readonly [...]tuple, literal member types,typeof ORG_MEMBERSHIP_LEVELS[number]still works) — it simply grows from a 3-tuple to the 4-tuple the column enforces.[...BUILTIN_MEMBERSHIP_ROLES] as const) would also type correctly but manufactures a second array object for no benefit; the alias makes "one source" literal.ApproverValueBindingenum arm typesvaluesasreadonly string[], soAPPROVER_VALUE_BINDINGS/APPROVER_VALUE_SOURCES(including the deprecatedrolealias row) follow the derivation with no signature change.Provenance fix
The constant's doc-comment claimed the list was "
sys_member.role: better-auth's closed set" — false since ADR-0105 D8:delegated_adminis ObjectStack's own addition. The comment now states the vocabulary isBUILTIN_MEMBERSHIP_ROLES, ObjectStack-owned (ADR-0108's closed membership list), with ADR-0105 D8 as the fourth member's provenance, and records why deriving (not transcribing) is the fix. TheApproverTypeenum's inline comment on theorg_membership_levelmember repeated the same stale three-value/better-auth attribution and is corrected in the same change. (ADR prose referenced only — nodocs/adr/**edits.)Anti-drift pins
approval.test.ts): ①[...ORG_MEMBERSHIP_LEVELS]equals[...BUILTIN_MEMBERSHIP_ROLES]— same list, same display order, so the derivation can never be silently replaced by a copy; ②delegated_adminis admitted; ③ the wire picker surface (APPROVER_VALUE_SOURCES.org_membership_level, deprecatedrolealias included) carries the whole vocabulary.approval-service.test.ts): adelegated_adminapprover expands to itssys_membermembers at runtime like any other tier (the expander filterssys_member.roleby the authored value — no tier list on that path).delegated_adminand refusal ofguest— see triage below.Consumer fixture triage (accept-set change — every known consumer audited, real readings)
packages/lint/src/validate-approval-approvers.tsMEMBERSHIP_TIERS = new Set(BUILTIN_MEMBERSHIP_ROLES)(line 120, four values), diagnostics render from the same listpackages/lint/src/validate-approval-approvers.test.tsdelegated_adminand refusal pin forguestalready present (lines 37–57) — no fixture pinned the old three-value refusalpackages/plugins/plugin-approvals/src/approval-service.tsexpandMembershipTierUserspasses the authored value straight to thesys_member.rolefilter — no hardcoded tier list; the alias/type tables carry type names, not tier valuespackages/plugins/plugin-approvals/src/approver-org-scope.tspackages/spec/src/automation/approval.test.tsORG_MEMBERSHIP_LEVELSsymbolically — they follow the derivation but could not catch a copy (proven: they stayed green under the reverse-verification mutation)examples/app-showcaseflowsvalue: 'owner'only — valid under both setsorg_membership_level, three-value tier spellings)position.zod.ts, service comments) — no other fixture pins the old set in this repoFlowReferenceField.tsxhardcodes a three-value fallback and its strict select ignores the server-published enum source; storeddelegated_adminrenders "(invalid)"; two label/comment echoes inflow-node-config.tsRemaining references audit
ORG_MEMBERSHIP_LEVELSreferents after the change: the derivation site, the twoAPPROVER_VALUE_BINDINGSrows (org_membership_level+ deprecatedrole), theAPPROVER_VALUE_SOURCESprojection, spec tests, and theapi-surface/export-originssnapshots (existence entries — unchanged,check:generatedconfirms all 13 artifacts current). No consumer re-spells the tier list in this repo any more; the only remaining hand copy is objectui's (filed above).Reverse verification (from the committed state)
Replaced the derivation with the old
['owner', 'admin', 'member'] as constcopy insrc→ exactly the 3 new pins red, all 11,019 other spec tests green (Test Files 1 failed | 413 passed,Tests 3 failed | 11019 passed). Observed direction as predicted — and the pre-existing symbolic assertions stayed green under the copy, confirming the new pins are the only guard against this drift. Restored by checking the mutated file back out of the branch's committed fix (git checkout of the branch ref, path-scoped — the fix was committed before the mutation, so a real restore point existed). Both legs ran source-resolved (the spec test imports./approval.zodrelatively, in-package — nodistin that measurement loop, so no rebuild was required for either leg; specdistwas rebuilt afterwards before the ratchet re-measure).Verification (all at head
8ec58a4)tsc --noEmit; spec includes its test-layer ledger check, "OK").pnpm --filter @objectstack/spec check:generated: "All 13 generated artifacts are up to date" — no artifact moved (the doc-comment is not a.describe(), andapi-surfacerecords export existence).node scripts/pm/dispatch-gates.mjsre-derived on the actual committed diff (no hand-fed paths). Every named gate run and green at head8ec58a4: all PM dispatch-time readings (check:cross-package-test-inputs+ node twin,check:doc-formula-expressions, speccheck:empty-state/check:liveness/check:strictness-ledger/check:variant-docs,check:merge-driver,check:slot-lookup,check:spec-parsed-alias,check:test-source-alias,check:type-source-resolution,check-dev-prereqs,check-affected-docs) plus the derivation's additions (check:changeset-gate-self-tests,check:objectui-changeset,check-adr-0087-registration,check-changeset-no-major,check-empty-changeset,check:nul-bytes) and the convention-triggered set on edited test files (check:query-options-erasure,check:type-check-coverage,check:engine-double-contract,check:where-matcher,check:i18n, andcheck:type-check-debt --re-measureon the fully built workspace — "1926 raw tsc error(s) total, none above its recorded number", surplus none).TS2339on the un-narrowedopenNodeRequestunion); fixed by guarding the union the way the sibling test does — the ledger was not touched.Changeset / ADR-0087
One changeset:
@objectstack/specminor (accept-set widened, additive — never major). No changeset for lint (untouched) or plugin-approvals (test-only edit, nothing user-visible ships). ADR-0087: not required — the widening admits an already-enforced value of the same column; no authored metadata changes shape, nothing is removed or renamed, so there is no conversion or migration to register, and the marker gate applies only to declared-breaking changesets (check-adr-0087-registrationgreen at head).Out of scope
Filed unassigned: objectstack-ai/objectui#5309 (Studio's membership-tier select hardcodes the three-value fallback, ignores the server-published enum source on the enum path, and renders a stored
delegated_adminas "(invalid)"). The ObjectOS approvals docs echo was already on file per the issue body and inherits the four-value vocabulary via objectstack-ai/objectos#131.Generated by Claude Code