The Rules section is one record table grouped by kind, Limit then Boolean, on fixed columns, edited in place: Edit opens a row's controls in their columns, Add rule opens a dense row above the table, and every change is staged into a tray that lists the deltas with Undo and applies them as one rule-change act. The reduction policy is a column of the rule beside its limit. History shows counts only. Group rows are a quiet heading rather than a divider, the maintainer's pick from four rounds of outside-model ideation. Dense rows align to the top and render each error under its control in every form family (design D16), replacing the below-row error block; the forms library gains the batch form (rows plus one tray) and the RowField dense and label-hidden options. Migration 00019 records the governing reduction policy on effect rows. Archive staged-rule-changes with its spec updates (entitlement-set- management, entitlement-set-history, entitlements, form-library, form-conventions, ui-quality-gate). Screens accepted 2026-09-19.
44 KiB
entitlements
Purpose
Defines the entitlement system that manages resource pools, grants, provisions, and numeric entitlement limits for organizations and workspaces.
Requirements
Requirement: Resource keys as shared namespace
The system SHALL maintain a resource_keys table where each key is a globally-unique, bare canonical identifier for a resource type. Resource keys SHALL be FK-referenced by entitlement set rules, numeric entitlements, and numeric entitlement usage records; a resource key that does not exist in this table SHALL NOT be usable in any entitlement context.
Each resource key SHALL carry three separable roles: the bare resource_key as the machine identifier and FK target; a nullable provider column used for grouping and filtering; and a display_name as the only user-facing label. The provider column SHALL encode fact-source cardinality — provider = <slug> means usage originates from exactly one provider, while provider IS NULL means usage is pooled across providers or platform-native.
Resource-key names SHALL follow a namespace-by-slug convention: the platform owns the unprefixed namespace (e.g., seats, members, pooled storage_bytes); each provider owns the <provider-slug>_* prefix (e.g., fedwiki_sites). There SHALL be no dotted provider.resource form. Whether a resource key is metered SHALL be independent of provider; a provider IS NULL key MAY be metered.
Scenario: Seed FedWiki resource key under the provider namespace
- WHEN migrations and provider registration have run on a fresh database
- THEN the
resource_keystable SHALL contain afedwiki_siteskey withprovider = 'fedwiki'andunit = 'site' - AND the table SHALL NOT contain a bare
siteskey
Scenario: Platform-owned key has a NULL provider
- WHEN a platform-owned resource key (e.g.,
storage_bytes) exists - THEN its
providercolumn SHALL be NULL - AND its name SHALL be unprefixed
Scenario: A platform-owned key may be metered
- WHEN a
provider IS NULLkey is configured as metered - THEN the system SHALL accept it, because metering is not constrained to
provider IS NOT NULL
Scenario: Display name is the only user-facing label
- WHEN a resource key is rendered in any UI
- THEN the system SHALL display its
display_name - AND the system SHALL NOT render the bare key or any dotted
provider.resourceform to users
Requirement: Resource pools belong to organizations
The system SHALL maintain resource pools as containers for entitlements. Each resource pool SHALL belong to exactly one organization. A pool SHALL have a pool_type (one of default, shared, dedicated), an is_auto_managed flag, and a status (one of active, suspended, archived).
Scenario: Default pool created during auto-provisioning
- WHEN a new organization is auto-provisioned
- THEN the system SHALL create a resource pool with
pool_type = 'default',is_auto_managed = true, andstatus = 'active'
Requirement: Pool assignments link workspaces to pools
The system SHALL maintain pool assignments that link workspaces to resource pools. Each workspace SHALL have exactly one primary pool assignment (is_primary = true). A workspace draws its entitlements from its assigned pool(s).
Scenario: Primary pool assignment created during auto-provisioning
- WHEN a default workspace is auto-provisioned
- THEN the system SHALL create a pool assignment with
is_primary = truelinking the workspace to the organization's default pool
Scenario: Workspace has exactly one primary assignment
- WHEN a pool assignment with
is_primary = trueis created for a workspace - THEN no other pool assignment for that workspace SHALL have
is_primary = true
Requirement: Grants confer product entitlements to organizations
The system SHALL support grants as an administrative or system-initiated access mechanism. A grant SHALL target an organization (via granted_to_org_id). Grants SHALL record who authorized them (granted_by_person_id), a classification (grant_reason), and optional validity bounds (valid_from, valid_until). A grant SHALL have a status of active, expired, or revoked — this status is a pure, append-only lifecycle ledger. It SHALL NEVER be read or filtered to mean "currently delivering": whether a grant's conferral is presently in effect is a fact about pool_provisions, not about grants.status. Callers that need to answer "is this delivering right now" MUST join to pool_provisions (the entitlement-conferral capability owns the mechanics that keep that table current).
granted_by_person_id SHALL be nullable. grant_reason SHALL be NOT NULL and constrained by a CHECK to the fixed domain {manual, default, evaluation, promotional, complimentary, sponsored, board_decision, legacy}. A second CHECK, default_iff_system_authored, SHALL enforce the biconditional (grant_reason = 'default') = (granted_by_person_id IS NULL): a grant is classified 'default' if and only if it has no human author. Both CHECKs require grant_reason to be NOT NULL to hold: under three-valued logic a NULL grant_reason would satisfy an IN (...) domain CHECK and the biconditional permissively, reopening the unattributed/unclassified grant state the domain and biconditional exist to close.
A grant SHALL name a real product: product_id (FK to billing.products) SHALL be NOT NULL. The entitlement_set_id column and the direct-set conferral path are dropped from entitlements.grants — a grant that conferred capabilities via a bare entitlement set had no product, and therefore no ladder answer, no lifecycle status, and no checkable shape. Ad hoc capability sets that previously used the direct-set path are conferred through internal "wrap products," a mechanism owned by the entitlement-conferral capability. The entitlement set a grant confers is always resolved from products.entitlement_set_id at provision-creation time; there is no longer a branch on how a grant names its capabilities.
Scenario: Admin creates a grant naming a product
- WHEN an admin creates a grant with a
product_idtargeting an organization - THEN the system SHALL create a grant record with
status = 'active'andproduct_idset to the named product - AND conferral onto the organization's pool SHALL be enacted by the
entitlement-conferralcapability's primitive, which resolvesentitlement_set_idfromproducts.entitlement_set_id
Scenario: System creates a default grant during auto-provisioning
- WHEN the system creates a grant with
granted_by_person_id = NULLandgrant_reason = 'default' - THEN the system SHALL create a grant record with
status = 'active',granted_by_person_idNULL, andproduct_idset to the default product - AND the grant SHALL follow the same conferral path as admin-created grants
Scenario: Grant requires a real product
- WHEN a grant is created with
product_idNULL - THEN the database SHALL reject the insert with a
NOT NULLconstraint violation - AND there SHALL be no
entitlement_set_idcolumn onentitlements.grantsfor the insert to name instead
Scenario: Grant reason is required and constrained to a fixed domain
- WHEN a grant is created with
grant_reasonNULL, or with a value outside{manual, default, evaluation, promotional, complimentary, sponsored, board_decision, legacy} - THEN the database SHALL reject the insert with a
NOT NULLorgrant_reason_domainCHECK constraint violation, respectively
Scenario: Grant requires attribution unless system-authored
- WHEN a grant is created with
granted_by_person_id = NULLandgrant_reasonis not'default' - THEN the database SHALL reject the insert with a
default_iff_system_authoredCHECK constraint violation
Scenario: Default reason requires no human author
- WHEN a grant is created with
grant_reason = 'default'andgranted_by_person_idIS NOT NULL - THEN the database SHALL reject the insert with a
default_iff_system_authoredCHECK constraint violation
Scenario: Grant revocation is a ledger fact independent of delivery
- WHEN an admin revokes an active grant
- THEN the grant's
statusSHALL change torevoked - AND the system SHALL enact the ending of any live conferral via the
entitlement-conferralcapability'send_conferralprimitive, resolved by source (the grant), not by anygrants.statusfilter - AND
grants.status = 'revoked'SHALL be recorded even when the primitive finds nothing live to end (a decree that never delivered, expired, or was already superseded)
Scenario: Delivery state is answered by provisions, never by grants.status
- WHEN any caller determines whether a grant is "currently delivering" entitlements
- THEN the system SHALL answer by joining to
pool_provisionsfor that grant and inspecting provision status - AND the system SHALL NOT filter or interpret
grants.statusas a proxy for current delivery
Requirement: Pool provisions as uniform interface
The system SHALL create a pool provision record for each grant or subscription. The pool provision SHALL reference the source (via grant_id for grants, or subscription_id and billing_account_id for subscriptions), the target pool (via pool_id), the conferred product (via product_id), and the resolved entitlement set (via entitlement_set_id). Both product_id (FK to billing.products) and entitlement_set_id SHALL be NOT NULL and SHALL be resolved and denormalized at provision-creation time — product_id from grants.product_id for grants, and from the price's product for subscription items and purchases; entitlement_set_id in turn from products.entitlement_set_id. product_id exists to record WHICH product was conferred, disambiguating cases where multiple products share one entitlement_set_id (e.g. monthly/annual price variants, or several ladder tiers sharing a baseline set) — a fact entitlement_set_id alone cannot recover, since the set reference is non-injective by design. The provision's quantity field SHALL determine entitlement multipliers for resource_per_unit rules. The provision's status SHALL be active, suspended, or ended. The per-source live-uniqueness invariants and the position machinery that write these rows are owned by the entitlement-conferral capability; this requirement states only the provision's required shape.
Scenario: Grant creates a pool provision
- WHEN a grant is created targeting an organization
- THEN a pool provision SHALL be created with
grant_idset,subscription_idandpurchase_idNULL,product_idset to the grant'sproduct_id,entitlement_set_idset to the resolved set, andstatus = 'active'
Scenario: Subscription creates a pool provision
- WHEN a subscription is fulfilled (via webhook) for a billing account belonging to an organization
- THEN a pool provision SHALL be created with
subscription_idset,billing_account_idset,grant_idandpurchase_idNULL,pool_idset to the organization's default resource pool,product_idset to the subscription item's product,entitlement_set_idresolved from that product,quantityset to the subscription item quantity, andstatus = 'active'
Scenario: Subscription provision targets default pool
- WHEN a subscription-based pool provision is created
- THEN the
pool_idSHALL be the organization's resource pool withpool_type = 'default'andis_auto_managed = true
Scenario: Provision quantity multiplies per-unit rules
- WHEN a pool provision has
quantity = 3and its entitlement set has aresource_per_unit = truerule withresource_value = 1 - THEN the contributed value for that provision SHALL be
3(1 x 3)
Scenario: Provision product_id disambiguates a shared entitlement set
- WHEN two products (e.g. a monthly and an annual price variant of the same plan, or two ladder tiers sharing a baseline set) share one
entitlement_set_id - AND a pool provision is created conferring one of those products
- THEN the provision's
product_idSHALL record which of the two products was actually conferred - AND this fact SHALL be recoverable even though
entitlement_set_idalone does not distinguish the two products
Scenario: Provision requires a product
- WHEN a pool provision is inserted with
product_idNULL - THEN the database SHALL reject the insert with a
NOT NULLconstraint violation
Requirement: Entitlement materialization on provision changes
The system SHALL re-materialize effective entitlements for a pool whenever any provision on that pool changes status (activated, suspended, or ended), and whenever a rule on an entitlement set that a live provision on that pool resolves to is added, changed, or deactivated. Materialization SHALL aggregate resource_value across all active provisions sharing the same resource_key, respecting resource_per_unit quantity multipliers, and write the results to numeric_entitlements, numeric_entitlement_contributions, and numeric_entitlement_usage.
A rule change SHALL NOT wait for a later conferral to reach a pool: the commit that changes the rule SHALL re-materialize every pool carrying the set, or SHALL record a durable obligation naming each pool it has not yet recomputed, per the entitlement-set-management capability.
Materialization SHALL lower a limit when a resource key loses its last contribution. A key that is present on the pool but is carried by no active provision's active rule SHALL be written to resource_limit = 0, its surviving contribution rows SHALL be deleted, and its entitlement and usage rows SHALL be retained, so usage history survives and a later rule add restores the key on the same rows. A contribution SHALL be deleted only for the provision and entitlement it belongs to, so recomputing one resource key never removes another key's freshly written contribution.
Scenario: First provision activates on a pool
- WHEN a pool's first provision is activated with an entitlement set that has a
limitrule forsiteswithresource_value = 5 - THEN the system SHALL create a
numeric_entitlementsrecord withresource_key = 'sites',entitlement_type = 'limit',resource_limit = 5 - AND the system SHALL create a
numeric_entitlement_contributionsrecord linking the provision to the entitlement withcontributed_value = 5 - AND the system SHALL create a
numeric_entitlement_usagerecord withcurrent_usage = 0
Scenario: Additive stacking of multiple provisions
- WHEN a second provision is activated on a pool that already has an active provision for the same
resource_key - THEN the system SHALL sum the contributed values and update
numeric_entitlements.resource_limitto the total
Scenario: Provision suspended reduces effective limit
- WHEN a provision is suspended (e.g., subscription goes
past_due) - THEN the system SHALL remove the suspended provision's contribution from the effective limit
- AND
numeric_entitlements.resource_limitSHALL be recalculated from remaining active provisions only
Scenario: Provision ended reduces effective limit
- WHEN a provision contributing 2 sites is ended on a pool that also has a provision contributing 5 sites
- THEN the system SHALL remove the ended provision's contribution
- AND the
numeric_entitlements.resource_limitSHALL be recalculated to5
Scenario: All provisions ended zeroes the limit
- WHEN all provisions on a pool are ended
- THEN the
numeric_entitlements.resource_limitSHALL be0 - AND the
numeric_entitlement_usagerecord SHALL be retained (not deleted)
Scenario: A rule change re-materializes a pool whose provisions did not change
- WHEN a rule is added to an entitlement set that an already-active provision on a pool resolves to
- THEN that pool's entitlements SHALL be re-materialized by the commit, or the pool SHALL be named by a durable obligation the drain settles
- AND the pool SHALL NOT be left carrying a rule state that no longer exists
Scenario: A key that loses its last rule falls to zero
- WHEN the last active rule for a resource key on the only entitlement set an active provision resolves to is deactivated
- THEN the pool's
numeric_entitlements.resource_limitfor that key SHALL be0 - AND its orphaned
numeric_entitlement_contributionsrows SHALL be deleted - AND its
numeric_entitlementsandnumeric_entitlement_usagerows SHALL be retained with the usage value they held
Scenario: Recomputing one key does not delete another key's contribution
- WHEN one provision funds two resource keys on a pool and the pool is re-materialized
- THEN each key retains its own contribution row for that provision
Scenario: Idempotent materialization
- WHEN
materialize_pool_entitlementsis called multiple times with the same pool state - THEN the resulting entitlements, contributions, and usage records SHALL be identical each time
Requirement: Numeric entitlement usage check
The system SHALL enforce numeric entitlement limits via an atomic check-and-increment operation. The operation SHALL atomically verify that current_usage < resource_limit and increment current_usage in a single UPDATE statement. If the limit would be exceeded, the operation SHALL fail (zero rows updated) without modifying the usage. For a counted resource that has a lifecycle, current_usage SHALL count only instances in their active/counted state; the same atomic check-and-increment SHALL gate every transition that brings an instance into the counted state — initial creation and any later reactivation or restore — not creation alone.
Scenario: Usage within limit
- WHEN a workspace's pool has a
fedwiki_sitesentitlement withresource_limit = 5andcurrent_usage = 2 - AND a site creation is requested
- THEN
current_usageSHALL be atomically incremented to3
Scenario: Usage at limit
- WHEN a workspace's pool has a
fedwiki_sitesentitlement withresource_limit = 5andcurrent_usage = 5 - AND a site creation is requested
- THEN the increment operation SHALL fail (zero rows updated)
- AND
current_usageSHALL remain5
Scenario: Reactivation is gated by the same check
- WHEN a workspace's pool has
resource_limit = 1andcurrent_usage = 1(one active instance) - AND a request is made to return a non-active instance to the active state
- THEN the increment operation SHALL fail (zero rows updated)
- AND the instance SHALL remain non-active
Requirement: Usage decrement on resource deletion
The system SHALL decrement numeric_entitlement_usage.current_usage when a counted resource leaves its active/counted state — whether deleted, or for a lifecycle resource transitioned out of the active state (e.g. to read-only or archived). The decrement SHALL NOT reduce current_usage below 0. A transition between two non-active states (e.g. read-only → archived), or the permanent purge of an already-non-active instance, SHALL NOT change current_usage (it was decremented when the instance first left the active state).
Scenario: Site deletion decrements usage
- WHEN an active site is deleted from a workspace
- THEN the system SHALL decrement
current_usageby 1 for thefedwiki_sitesentitlement on the workspace's pool
Scenario: Transition out of active decrements usage
- WHEN an active site is transitioned to read-only or archived
- THEN the system SHALL decrement
current_usageby 1 for thefedwiki_sitesentitlement
Scenario: Purging an already-non-active instance does not double-decrement
- WHEN a site that is already archived is permanently purged
- THEN
current_usageSHALL be unchanged by the purge
Scenario: Decrement does not go below zero
- WHEN
current_usageis0and a decrement is attempted - THEN
current_usageSHALL remain0
Requirement: Usage counter reconciliation from owned rows
The entitlements store SHALL expose two operations for an integration that owns the rows a countable resource counts: a raise-only reconcile that sets a pool's current_usage for a resource key to the greater of its current value and an observed count, and an exact set used only by boot reconciliation. Neither operation SHALL create a usage row; a missing row remains the materializer's to create. The atomic gated increment SHALL remain the sole reservation path for creates.
Scenario: Raise-only reconcile never lowers
- WHEN a pool's
current_usageis 5 and the raise-only reconcile is called with an observed count of 4 - THEN
current_usageSHALL remain 5
Scenario: Raise-only reconcile lifts an understated counter
- WHEN a pool's
current_usageis 1 and the raise-only reconcile is called with an observed count of 64 - THEN
current_usageSHALL become 64 - AND a subsequent gated increment against a limit of 64 SHALL affect zero rows
Scenario: Exact set at boot
- WHEN boot reconciliation calls the exact set with an observed count of 12 on a pool whose
current_usageis 20 - THEN
current_usageSHALL become 12
Requirement: Grants record lineage via a self-referential extends_grant_id column
The entitlements.grants table SHALL include a nullable extends_grant_id UUID column with a foreign key reference to entitlements.grants(grant_id). When set, this column indicates that the grant extends a prior grant within the same lifecycle chain — a replacing grant names the grant it supersedes. When NULL, the grant has no parent and is the root of its lineage. Every grant created prior to this requirement SHALL have extends_grant_id = NULL.
A CHECK constraint SHALL prevent self-reference: CHECK (extends_grant_id IS NULL OR extends_grant_id != grant_id). A partial index WHERE extends_grant_id IS NOT NULL SHALL exist on the column to support lineage walks without bloating the index for the common case.
This lineage is consulted by the entitlement-conferral capability's confer primitive to distinguish a genuine extend-as-replace supersession (the replacing grant's extends_grant_id names the incumbent's source grant) from an accidental duplicate decree; the enactment mechanics — including the transfer transition an equal-rank extend-as-replace produces — are specified there, not here.
Scenario: Existing grants have NULL lineage
- WHEN the migration applies on a database with existing grants
- THEN every existing
entitlements.grantsrow SHALL haveextends_grant_id = NULL - AND all existing reads of the
grantstable SHALL succeed unchanged
Scenario: A grant cannot extend itself
- WHEN an INSERT or UPDATE attempts to set
extends_grant_id = grant_id - THEN the database SHALL reject the operation with a CHECK constraint violation
Requirement: Grant lineage is constrained to a single organization
A grant whose extends_grant_id is non-NULL SHALL reference a parent grant that shares the same granted_to_org_id. A BEFORE INSERT OR UPDATE trigger on entitlements.grants SHALL enforce this by raising an exception when the parent's organization differs from the new grant's organization.
Scenario: Same-org lineage permitted
- WHEN a grant is inserted with
extends_grant_idpointing to a grant for the same organization - THEN the insert SHALL succeed
Scenario: Cross-org lineage rejected
- WHEN a grant is inserted with
extends_grant_idpointing to a grant for a different organization - THEN the trigger SHALL raise
extends_grant_id must reference a grant for the same organization - AND the new grant SHALL NOT be inserted
Requirement: Grant lineage is immutable after insert
The extends_grant_id column SHALL be set at INSERT time and SHALL NOT be modifiable by subsequent UPDATEs. A BEFORE UPDATE trigger on entitlements.grants SHALL raise an exception when OLD.extends_grant_id IS DISTINCT FROM NEW.extends_grant_id. This prevents post-hoc creation of cycles in the lineage graph and makes the chain a write-once relationship.
UPDATEs to other columns on a grant with non-NULL extends_grant_id (status changes, revocation fields, description updates) SHALL succeed unaffected.
Scenario: Lineage column cannot be changed after insert
- WHEN an UPDATE attempts to change a grant's
extends_grant_idfrom one value to another (including NULL → non-NULL or non-NULL → NULL) - THEN the trigger SHALL raise
extends_grant_id is immutable after insert - AND the row SHALL remain unchanged
Scenario: Other column updates remain permitted
- WHEN an UPDATE changes a grant's status, revoked_at, revocation_reason, or description without touching
extends_grant_id - THEN the update SHALL succeed regardless of whether
extends_grant_idis NULL or set
Requirement: GetGrantLineage walks the ancestry of a grant
The system SHALL provide a GetGrantLineage query that, given a focal grant_id, returns the focal grant followed by every grant in its ancestry chain (parent, grandparent, …) walked via extends_grant_id. The query SHALL be implemented as a recursive CTE in internal/entitlements/queries/grants.sql and exposed through sqlc-generated code. The query SHALL be available for future consumers but MAY be uncalled when this requirement first lands.
Scenario: Lineage of a root grant returns only itself
- WHEN
GetGrantLineageis called with agrant_idwhoseextends_grant_idis NULL - THEN the result SHALL contain exactly one row: the focal grant
Scenario: Lineage of a chain returns all ancestors
- WHEN grants A, B, C exist such that B extends A and C extends B
- AND
GetGrantLineageis called with C'sgrant_id - THEN the result SHALL contain three rows in chain-walk order: C, B, A
Scenario: Revoked ancestors do not break the walk
- WHEN a grant chain exists and an intermediate ancestor has
status = 'revoked' - AND
GetGrantLineageis called with the chain's most recent grant - THEN the result SHALL include the revoked ancestor and continue past it to its parent
Requirement: Boolean entitlement materialization on provision changes
The system SHALL materialize boolean entitlement rules whenever pool entitlements are re-materialized (the same trigger points as numeric materialization, including a rule on an entitlement set that a live provision resolves to being added, changed, or deactivated). For each resource_key carried by a boolean rule on any of the pool's provisions, the system SHALL write a boolean_entitlements row whose granted value is true if and only if at least one active provision on the pool carries that boolean rule. Conferral SHALL derive from provision status only; grants.status SHALL NOT be consulted.
Scenario: Boolean rule materializes on provision activation
- WHEN a provision is activated on a pool with an entitlement set containing a
booleanrule fordiscourse_posting - THEN the system SHALL write a
boolean_entitlementsrow for the pool withresource_key = 'discourse_posting'andgranted = true
Scenario: OR-aggregation across provisions
- WHEN two active provisions on a pool both carry a boolean rule for the same
resource_keyand one provision is ended - THEN the row SHALL remain
granted = truebecause an active provision still carries the rule
Scenario: Last carrying provision ends
- WHEN the only active provision carrying a boolean rule is ended or suspended
- THEN the row SHALL be updated to
granted = falseand SHALL NOT be deleted
Scenario: A deactivated boolean rule withdraws the grant
- WHEN the only active boolean rule for a key on the set an active provision resolves to is deactivated
- THEN the pool's
boolean_entitlementsrow for that key SHALL be updated togranted = falseand SHALL NOT be deleted
Scenario: Never-conferred key has no row
- WHEN no provision on a pool has ever carried a boolean rule for a given
resource_key - THEN no
boolean_entitlementsrow exists for that pool and key
Scenario: Idempotent boolean materialization
- WHEN materialization runs multiple times with the same pool state
- THEN the resulting
boolean_entitlementsrows SHALL be identical each time
Requirement: Boolean entitlement read surface
The system SHALL provide queries answering, without callers re-deriving the provisions join: whether a given pool currently has a boolean resource_key granted, and which pools (with their owning organizations) currently have a given boolean resource_key granted.
Scenario: Consumer reads conferral for a key
- WHEN a consumer queries pools granted
discourse_postingwhile two orgs' pools have active provisions carrying that rule and a third org's carrying provision has ended - THEN the query returns exactly the two pools with active carrying provisions, each with its owning organization
Requirement: One default pool per organization is schema-enforced
The database SHALL reject a second resource pool with pool_type = 'default' for the same organization, via a partial unique index on core.resource_pools(org_id) WHERE pool_type = 'default'. The pool_type column SHALL carry a CHECK constraint pinning it to the documented value set, so an unknown pool type cannot evade the index.
Scenario: Second default pool is rejected
- WHEN an insert or update would give an organization a second pool with
pool_type = 'default'(under any slug) - THEN the database rejects it with a unique-violation error
- AND every org-to-default-pool resolution therefore has at most one candidate row
Requirement: One primary pool assignment per workspace is schema-enforced
The database SHALL reject a second is_primary = TRUE assignment for the same workspace, via a partial unique index on core.pool_assignments(workspace_id) WHERE is_primary. The migration introducing the index SHALL first demote any existing duplicate primaries deterministically (keeping the earliest assignment), so it applies cleanly to existing data.
Scenario: Second primary assignment is rejected
- WHEN an insert or update would mark a workspace primary in a second pool
- THEN the database rejects it with a unique-violation error
- AND quota acquisition's primary-assignment subquery therefore matches at most one pool, so one acquisition can never debit two pools' counters
Requirement: Pools carrying an entitlement set are enumerable
The system SHALL be able to enumerate the pools that carry a given entitlement set, by pool_provisions.entitlement_set_id restricted to provisions with status = 'active', which is the same predicate materialization itself reads, so the population a preview names and a commit acts on is exactly the population a recomputation would change. The enumeration SHALL be orderable by pool id, so a transaction that locks many pools has one lock order, and SHALL be countable in pools and in organizations without listing them, so a population too large to name can still be stated exactly.
Pools whose provisions carrying the set are all suspended SHALL NOT be part of that population. They SHALL be countable separately, so a surface can disclose them as a count that is unchanged until they resume.
Scenario: The carrying population is the materializing population
- WHEN a set is carried by 13 pools through active provisions and by 2 more pools through suspended provisions only
- THEN the enumeration yields the 13 pools
- AND the 2 suspended-only pools are countable separately and are not listed among them
Scenario: A large population is counted without being listed
- WHEN a set is carried by more pools than a request may materialize
- THEN the number of carrying pools and the number of organizations holding them are obtainable exactly without enumerating the pools
Requirement: A transaction that materializes a pool is ordered against a rule change
Every transaction that materializes a pool SHALL take the entitlement_materialization advisory lock in shared mode as its first acquisition, before it takes any row, and every transaction that adds, changes, deactivates or reactivates an entitlement set rule SHALL take the same lock in exclusive mode as its first acquisition, before it takes any row. The rule-change transaction SHALL then take the entitlement set's row before any pool row. Any transaction that takes more than one pool row SHALL take them in ascending pool id order. Because the advisory lock is the first acquisition on both sides, no holder waits for something a would-be holder holds, so a rule change and a recomputation are strictly ordered whichever begins first and neither can hold a row the other waits for.
A dry run of the materializer SHALL take neither mode, because it materializes nothing.
Shared holders SHALL NOT block each other, so recomputations continue to run concurrently with one another. Each such transaction SHALL set a lock timeout, so a wait surfaces as a refusal an operator can read and retry rather than as a hang; a timed-out wait for the materialization lock SHALL refuse with "Another entitlement set change is in progress. Try again.", and a deadlock reported by the database SHALL surface as the same retryable refusal rather than as a server error.
Possession SHALL be enforced rather than trusted. Before it writes anything, the materializer's apply step SHALL assert that its own transaction holds the rendezvous, by calling the one shared database assertion that queries the lock state for the calling backend, and SHALL refuse with a named error identifying the pool when it does not. Every transaction that adds, changes, deactivates or reactivates a rule SHALL call the same assertion in its exclusive form as its own first statement. A caller that forgets the rendezvous therefore refuses a conferral, an activation or a grant at once, instead of writing limits derived from a rule state another transaction has already replaced. Lock order is not asserted: a transaction that takes a pool row first surfaces its mistake as a lock timeout or a deadlock, which is a refused request rather than a wrong limit. Other advisory locks MAY be taken before the rendezvous, which stays safe because no rule-change transaction takes any of them.
Membership is decided by what a transaction does, not by which primitive it calls. The transactions bound by this requirement today are the rule-change commit and its drain, the tier add and tier removal enactments, the org-type default change, grant issuance and grant revocation, the demonstration seed, and the subscription reconcile's suspend and resume paths; every one of them SHALL open through the shared opener in this change, each covered by a test that drives the caller and asserts it does not refuse. The system SHALL provide one transaction opener for each side, so that a transaction which materializes a pool without opening through them is a defect that can be found by reading the code as well as by running it.
Scenario: A conferral that begins first finishes before the rule change
- WHEN a conferral transaction has taken the shared lock and a rule commit arrives
- THEN the rule commit waits for the exclusive lock until that conferral's transaction ends
- AND the conferral materializes against the rules that were live when it began
Scenario: A conferral that begins during a rule change waits for it
- WHEN a rule commit holds the exclusive lock and a conferral for a pool that is about to carry the set begins
- THEN the conferral waits before taking any row
- AND it materializes against the committed rule state, so no pool is left materialized from a rule state the commit replaced
Scenario: Two recomputations do not block each other
- WHEN two conferrals on different pools run at the same time with no rule change in flight
- THEN both hold the lock in shared mode and neither waits for the other
Scenario: A wait that exceeds the timeout is a readable refusal
- WHEN a transaction waits longer than the configured lock timeout for the materialization lock
- THEN it rolls back and the refusal reads "Another entitlement set change is in progress. Try again."
- AND no partial write survives
Scenario: The reconcile's resume path is bound by the invariant
- WHEN a subscription reconcile resumes a suspended provision and materializes its pool
- THEN its transaction takes the shared materialization lock before any row
- AND it cannot write limits derived from a rule state a rule commit has already replaced
Scenario: A materializer that holds no rendezvous refuses
- WHEN a transaction that did not take the materialization lock calls the materializer's apply step
- THEN it refuses with a named error identifying the pool and writes nothing
Scenario: A preceding advisory lock is permitted
- WHEN the subscription reconcile takes its per-subscription advisory lock and then the shared materialization lock
- THEN the assertion passes and the materialization proceeds
Requirement: The materializer computes and applies as two steps sharing one fold
The materializer SHALL be a compute step that folds a pool's active provisions and their active rules into the would-be state per resource key, and an apply step that reconciles the pool's stored rows to that state. Both steps SHALL share one fold, so there is exactly one answer to what a pool is entitled to and no second implementation can diverge from it. The fold SHALL carry each contribution's reduction policy and SHALL report, per resource key, the governing policy across the contributions that fund it, by the order stated in "The reduction policy governing a pool and key is the strongest across the rules funding it".
The compute step SHALL take a list of rule overlays and a dry-run flag. Each overlay carries exactly one rule's proposed declarative fields, keyed by the rule being changed, carrying no rule identity for a rule being added, or expressing a deactivation; the list carries at most one overlay per resource key; and the fold SHALL substitute each overlay for its rule's stored row, so a staged batch is folded once per pool against the whole would-be ruleset, as the commit will apply it. A change to one rule is the one-element list. With the dry-run flag set, the compute step SHALL return the would-be state and the apply step SHALL NOT run, so nothing is written, nothing is rolled back, no lock is taken and the possession assertion does not apply. The compute step SHALL return a value and SHALL take no writer, so there is no path by which a dry run can write.
Scenario: A dry run and the commit agree
- WHEN a dry run of a proposed rule change reports a pool's limit becoming 16, and that change is then committed with nothing else intervening
- THEN the committed limit for that pool and key is 16
Scenario: A batch dry run and a batch commit agree
- WHEN a dry run of two overlays on keys A and B reports a pool's limits becoming 16 and 3, and that batch is then applied with nothing else intervening
- THEN the committed limits for that pool are 16 for A and 3 for B, from one materialization of the pool
Scenario: A dry run leaves no trace
- WHEN the compute step runs with the dry-run flag for every pool carrying a set
- THEN no row is written or deleted in any entitlement, contribution, usage or rule table
- AND no advisory lock and no row lock is held at any point
Scenario: An overlay for a rule being added has no rule identity
- WHEN a dry run evaluates a rule that does not yet exist
- THEN the fold applies the overlay alongside the set's stored rules and returns the would-be state
Scenario: The fold reports the governing policy
- WHEN the compute step folds a pool whose key is funded by rules carrying
clampandforce_reduce - THEN its result for that key carries
force_reduceas the governing policy beside the resolved limit
Requirement: Entitlement set rule writes are enclosed and their DML is revoked
Insert, update and delete on entitlement_set_rules SHALL be revoked from the application's database role, and one enclosed database function SHALL be the only path that writes a rule row. That function SHALL write the rule, the change ledger's act row and one pending obligation per pool carrying the set in a single transaction, so a rule cannot exist without the record of how it got there. A second enclosed function SHALL settle one obligation from the caller's own diff of that pool's state before and after materialization: given that diff, it SHALL write one effect row per resource key the diff names and mark the obligation settled; given a failure instead of a diff, it SHALL write no effect row, record that failure and mark the obligation failed; and it SHALL return without writing when the obligation is already settled. Neither function materializes a pool or computes the diff itself; the caller holds the rendezvous, materializes, and passes what it read.
Every direct writer of rule rows outside migrations SHALL move to the function in the same change as the revoke, seeds and test fixtures included. Migration-time writes, which run as the schema's owning role, are unaffected.
Scenario: The application role cannot write a rule directly
- WHEN the application's role issues an insert, update or delete against
entitlement_set_rules - THEN the database refuses it
Scenario: The enclosing function does not materialize
- WHEN the rule-writing function returns
- THEN no pool has been materialized by it, and the caller materializes each pool it owes under the rendezvous
Scenario: Settling an already-settled obligation writes nothing
- WHEN the settling function is called for an obligation whose status is already settled
- THEN it returns without writing an effect row and without changing the obligation
Requirement: Resource keys declare their provider's over-limit behavior
core.resource_keys SHALL carry over_limit_behavior, constrained to deny_new, park or reclaim, NOT NULL, defaulting to deny_new, and over_limit_consequence, the sentence an operator preview renders when that behavior is exercised. A key with no provider SHALL be constrained to deny_new (platform_keys_deny_new), because only a provider can act on a surplus. Values SHALL be declared by the integration migration that owns the key or, for the core-seeded fedwiki_sites key, by core migration 00018.
Scenario: A provider key declares park with its sentence
- WHEN a provider's integration migration registers a resource key
- THEN it MAY set
over_limit_behavior = 'park'with anover_limit_consequencesentence for that key
Scenario: A platform key is refused park
- WHEN a migration attempts to set
over_limit_behaviorto a value other thandeny_newfor a resource key with no provider - THEN the database refuses it
Requirement: The reduction policy governing a pool and key is the strongest across the rules funding it
When more than one active rule funds one resource key on one pool, from one set or from several sets carried through several provisions, the reduction policy that governs that pool and key SHALL be the strongest among them, in this order from strongest to weakest: force_reduce, clamp, block, defer. The order SHALL be stated once as a list in the Go fold and once as an explicit ordered CASE in the SQL query that answers the governing policy (GetGoverningReductionPolicy), never as a comparison of the values' text, and a test SHALL feed both the same mixtures and require the same answer. The fold SHALL expose the governing policy per resource key beside the resolved value, so every reader of a pool's consequence reads the pool's policy and not one rule's: the effect rows a commit writes, the provider sweep that acts on a surplus, and any console line that discloses what will happen to usage above a limit. No console path SHALL present a single rule's policy as what governs a pool.
Scenario: Two sets disagree on one pool
- WHEN a pool carries one set whose rule for a key says
clampand another set whose rule for the same key saysforce_reduce - THEN the fold reports
force_reduceas the governing policy for that pool and key, and the SQL query answers the same
Scenario: A dormant policy never outranks an acting one
- WHEN a pool's key is funded by rules carrying
deferandclamp - THEN the governing policy is
clamp
Scenario: One rule governs alone
- WHEN a pool's key is funded by one rule
- THEN the governing policy is that rule's own