Files
cgalo5758 3727ff31d8 Add entitlement set rule change ledger and preview flow
Add an append-only ledger of entitlement set rule changes with per-pool
effect rows, a preview-and-commit rule change flow, and an automatic
drain that settles deferred recomputations. Rules gain a tier reduction
policy, resource keys declare over-limit behavior, and the materializer
now lowers limits when a rule stops applying.
Add entitlement set rule change ledger and preview flow

Add an append-only ledger of entitlement set rule changes with a
preview-and-commit operator flow. Rule writes now go through an enclosed
`core.commit_rule_change` function that files an act row and one
obligation per carrying pool, with a drain workflow settling deferred
recomputations. The preview dry-runs the materializer with a rule
overlay and renders per-pool buckets, reduction-policy disclosures, and
provider over-limit consequences. Materializing transactions take a
shared advisory rendezvous that rule changes hold exclusively, enforced
by a possession assertion. Add History and Entitlement changes surfaces,
a rule-less warning on five product-selection surfaces, and a
`tier_reduction_policy` column that gates FedWiki parking.
2026-09-15 03:53:28 -05:00

10 KiB

entitlement-sets

Purpose

Defines the entitlement set abstraction that decouples capability specification from commercial packaging. Entitlement sets are named, reusable collections of entitlement rules that products and grants reference to declare what capabilities they confer.

Requirements

Requirement: Entitlement sets as named capability collections

The system SHALL maintain an entitlement_sets table where each row represents a named, reusable collection of entitlement rules. Each entitlement set SHALL have a set_id (UUID), name (VARCHAR(255)), description (TEXT), and is_active (BOOLEAN). Entitlement sets decouple capability specification from commercial packaging, but conferral SHALL flow exclusively through products: an entitlement set SHALL be reference-able only by a product's entitlement_set_id, including an internal wrap product minted solely to confer the set (see product-management, entitlement-conferral), and SHALL NOT be referenced directly by a grant. An entitlement set with is_active = false SHALL NOT be assignable to new products, but existing references SHALL remain valid.

Scenario: Create an entitlement set

  • WHEN an operator creates an entitlement set with name "FedWiki Capabilities"
  • THEN the system SHALL create an entitlement_sets record with is_active = true
  • AND the set SHALL be available for reference by products, including internal wrap products minted solely to confer the set

Scenario: Deactivated set remains valid for existing references

  • WHEN an entitlement set is deactivated (is_active = false)
  • THEN existing products referencing that set SHALL continue to function
  • AND existing pool provisions resolved to that set SHALL continue to materialize

Requirement: Entitlement set rules define capabilities within a set

The system SHALL maintain an entitlement_set_rules table where each row defines a single capability within an entitlement set. Each rule SHALL reference a set_id (FK to entitlement_sets) and have a rule_type discriminator. rule_type = 'limit' and rule_type = 'boolean' are supported; quota and credit remain unmaterialized. Each limit rule SHALL have a resource_key (FK to resource_keys), resource_value (BIGINT), resource_per_unit (BOOLEAN), and stacking_policy (one of additive, maximum, replace). Each boolean rule SHALL have a resource_key and NULL resource_value, resource_per_unit, and stacking_policy.

Every rule SHALL carry a tier_reduction_policy, NOT NULL and constrained to block, defer, clamp or force_reduce. It is the platform's promise to a member about headroom when a limit this rule funds drops below what the member is already using, and it is a declarative field of the rule: it is snapshotted with the rest when a change is recorded, and changing it alone is a recorded rule edit that moves no pool's limit. It is the rule's policy, never the resource key's: the key declares what its provider does with a surplus, and the policy decides whether that behavior is exercised at all.

A rule stops applying by is_active = FALSE and no path SHALL delete a rule row. Materialization SHALL read active rules only, so a deactivated rule contributes nothing to any pool, while the row survives with the value it carried so history can state it. Deactivating a rule SHALL free its resource key for a new active rule on the same set, because the uniqueness the schema enforces is over active rules.

Scenario: Limit rule within a set

  • WHEN an entitlement set has a limit rule with resource_key = 'sites', resource_value = 5, resource_per_unit = false, stacking_policy = 'additive'
  • THEN any pool provision resolved to this set SHALL contribute 5 sites to the pool's numeric entitlements

Scenario: Per-unit limit rule within a set

  • WHEN an entitlement set has a limit rule with resource_key = 'sites', resource_value = 1, resource_per_unit = true, stacking_policy = 'additive'
  • AND a pool provision resolved to this set has quantity = 3
  • THEN the contributed value SHALL be 3 (1 x 3)

Scenario: Boolean rule within a set

  • WHEN an entitlement set has a boolean rule with resource_key = 'discourse_posting'
  • THEN any active pool provision resolved to this set SHALL cause the pool's boolean_entitlements row for that key to be granted = true

Scenario: Rule type CHECK constraint

  • WHEN a limit rule is inserted into entitlement_set_rules
  • THEN resource_key and resource_value SHALL NOT be NULL
  • AND credit_amount and credit_currency SHALL be NULL

Scenario: Boolean rule CHECK constraint

  • WHEN a boolean rule is inserted into entitlement_set_rules
  • THEN resource_key SHALL NOT be NULL
  • AND resource_value, stacking_policy, resource_per_unit, credit_amount, and credit_currency SHALL be NULL

Scenario: A removed rule is retained as inactive

  • WHEN an operator removes a rule from a set
  • THEN the row SHALL be retained with is_active = FALSE and its resource_value unchanged
  • AND no DELETE SHALL be issued against entitlement_set_rules

Scenario: A deactivated rule contributes nothing

  • WHEN a pool carrying the set is materialized after a rule was deactivated
  • THEN that rule SHALL contribute nothing to the pool's numeric or boolean entitlements

Scenario: A rule carries its reduction policy

  • WHEN a limit rule is inserted into entitlement_set_rules
  • THEN tier_reduction_policy SHALL be one of block, defer, clamp or force_reduce and SHALL NOT be NULL

Scenario: A deactivated key can be ruled again

  • WHEN a rule for a resource key is deactivated and a new rule for the same key is added to the same set
  • THEN the new rule SHALL be accepted, and the set SHALL carry exactly one active rule for that key

Requirement: Seed entitlement sets for existing products

The system SHALL seed entitlement sets via migration corresponding to each seed product:

  1. A "FedWiki Capabilities" set with a limit rule for resource_key = 'sites', resource_value = 5, resource_per_unit = false, stacking_policy = 'additive'.
  2. A "Site Credit Capabilities" set with a limit rule for resource_key = 'sites', resource_value = 1, resource_per_unit = true, stacking_policy = 'additive'.

Scenario: Fresh database has seed entitlement sets

  • WHEN migrations have run on a fresh database
  • THEN the entitlement_sets table SHALL contain a "FedWiki Capabilities" set with is_active = true
  • AND the entitlement_sets table SHALL contain a "Site Credit Capabilities" set with is_active = true
  • AND each set SHALL have its corresponding entitlement_set_rules rows

Requirement: Every pool provision resolves to exactly one entitlement set

The system SHALL require that every pool_provisions record has a non-NULL entitlement_set_id (FK to entitlement_sets). The entitlement_set_id SHALL be resolved at provision-creation time and SHALL NOT change after creation. The materialization pipeline SHALL use pool_provisions.entitlement_set_id as the sole key for looking up rules. Entitlement sets decouple capability specification from commercial packaging, but conferral flows exclusively through products: a grant SHALL NOT reference an entitlement set directly, and pool_provisions.entitlement_set_id SHALL always be resolved from the conferring product's entitlement_set_id (see entitlement-conferral). Ad hoc conferral of a raw entitlement set, with no ordinary commercial product behind it, SHALL be accomplished by minting an internal wrap product whose entitlement_set_id names the set — never by a grant naming the set directly (see product-management, entitlement-conferral).

Scenario: Provision from a product-based grant

  • WHEN a grant references a product with entitlement_set_id = X
  • THEN the pool provision created for this grant SHALL have entitlement_set_id = X

Scenario: Ad hoc set conferral is mediated by a wrap product

  • WHEN an operator needs to confer a raw entitlement set that has no ordinary commercial product behind it
  • THEN the operator SHALL mint an internal wrap product (is_public = FALSE, published, priceless) whose entitlement_set_id names the set
  • AND the grant SHALL name that wrap product's product_id
  • AND the pool provision created for the grant SHALL have entitlement_set_id resolved from the wrap product, exactly as for any other product-backed conferral

Scenario: A grant SHALL NOT reference an entitlement set directly

  • WHEN a grant is created
  • THEN the grant SHALL name a product_id
  • AND the grant SHALL NOT carry an entitlement_set_id column
  • AND no code path SHALL resolve a pool provision's entitlement_set_id from a grant-supplied set reference rather than from the conferring product

Scenario: Provision entitlement_set_id is immutable after creation

  • WHEN a pool provision exists with entitlement_set_id = X
  • AND the referenced product's entitlement_set_id is changed to Z
  • THEN the pool provision's entitlement_set_id SHALL remain X

Requirement: Resource keys declare a kind

The system SHALL record a shape discriminator on core.resource_keys: kind VARCHAR(20) NOT NULL DEFAULT 'numeric' with CHECK (kind IN ('boolean', 'numeric')). boolean keys are conferred by boolean rules and consumed via boolean_entitlements; numeric keys are conferred by limit (and, when materialized, quota) rules and consumed via numeric_entitlements. Migrations and seeds that create resource-key rows SHALL declare kind explicitly.

Scenario: Boolean key is marked boolean

  • WHEN migrations have run on a fresh database with the discourse stream enabled
  • THEN the core.resource_keys row for discourse_posting SHALL have kind = 'boolean'

Scenario: Numeric key defaults to numeric

  • WHEN migrations have run on a fresh database
  • THEN the core.resource_keys row for fedwiki_sites SHALL have kind = 'numeric'

Scenario: Invalid kind is rejected

  • WHEN a row is inserted into core.resource_keys with kind = 'quota'
  • THEN the insert SHALL fail with a CHECK constraint violation