D71 — DefaultRoleRegistry: default roles are contributed per module (RESOLVED)
Architecture decision record, relocated verbatim from the retired single-file
docs/tenancy/decision log. Status, thematic clusters, and how to record a new ADR: the decision log index.
The default-role analogue of the D36 per-module PermissionRegistry. Previously the explicit OPERATOR
(web) default roles were hard-coded in the Authorization module's Support\DefaultRoles
(backoffice() named them by BackofficePermission enum case; complianceBackoffice() by string) — so
the DEFINITIONS of operator/platform-support/platform-compliance + the compliance analyst/officer/manager/
admin ladder lived in Authorization even though their capabilities are OWNED by the Backoffice/Compliance
surfaces. That blocked moving the operator-surface permission catalog out of the central enum (the
docs/tracking/permission-catalog-migration.md "default-role contribution seam" deferral).
Decision: an Authorization-owned Permissions\DefaultRoleRegistry singleton (append-only
register()/all()/forGuard()) that modules PUSH their default-role definitions into from their
ServiceProvider::boot() — via Permissions\Contracts\ProvidesDefaultRoles + each module's
src/Authorization/DefaultRoleProvider.php, exactly mirroring ProvidesPermissions/PermissionProvider.
A Permissions\DefaultRoleDefinition (name, guard, permissionKeys) names its permissions by KEY
STRING, never an enum — so a module can define a default role over ANOTHER module's permission catalog
without importing it. Backoffice contributes operator/compliance/platform-*; Compliance contributes
the compliance-* ladder. BackofficeRoleSeeder + SyncPermissions::validateDefaultRoles() iterate
DefaultRoleRegistry->forGuard('web') (composed through RoleComposer, unchanged); DefaultRoles keeps
only the full-catalog role names, the explicit TENANT map, and the derived protected role-name lists
(backofficeRoleNames() reads the registry, so ProtectedOperatorRoles can't drift).
Why (control inversion, not a graph edge): the modules depend DOWN on Authorization and PUSH into its
registry at boot — Authorization never imports Backoffice/Compliance, so the dependency graph stays
acyclic (ModuleBoundaryTest unchanged; the registry-inversion precedent of PermissionRegistry,
FeatureGuard, the webhooks/dlp registries). The relocation is behaviour-preserving: the seeded
role→permission sets are byte-identical to before the seam (snapshot: DefaultRoleRegistryTest;
ProtectedOperatorRolesTest pins the protected set). The operator permission KEYS themselves stay in the
central BackofficePermission enum for now (referenced by string) — relocating those cases is the
remaining, separately-gated catalog-migration work.