CommunityPay evaluates an association's eligibility with a declarative rule framework. Rules are defined as criteria, not code. They are versioned, immutable once active, and produce deterministic results. This page describes the rule architecture, the evaluation process, and the audit artifacts produced.
Design Principles
Declarative, Not Procedural
Eligibility rules are stored as data. A criterion names a signal, an operator, and a value:
{"signal": "reserve_funding_ratio", "op": ">=", "value": 0.10}
Criteria can nest AND and OR groups. Because they are data and not application code:
- Rules can be inspected without reading source code
- Rule changes are versioned and auditable
- The same rule engine evaluates all rules uniformly
- Historical evaluations can be reconstructed from the stored criteria and signal values
Each rule links to the legal authority that permits it. When an authorizing statute changes, the rule is flagged for legal review.
Deterministic
Given the same rule version and the same signal values, the evaluation always produces the same result. There is no randomness, no external state dependency, and no side effects.
This is critical for audit. An evaluation performed on January 15 can be reproduced exactly by loading the rule versions active on January 15 and the signal values captured at evaluation time.
Fail Closed
An evaluation reaches ELIGIBLE only from rules that ran on signals that were actually obtained. An empty rule set, a rule whose signals cannot be resolved, and a trigger that could not be evaluated all produce WITHHELD. WITHHELD is not a refusal. It is the engine stating that it has no basis to judge, and every surface that reads it treats it as not evaluated, never as a pass.
Rule Structure
Each eligibility rule defines:
| Field | Purpose |
|---|---|
| rule_id | Stable identifier across versions |
| code | Machine-readable code (e.g., TENURE_6M, RESERVE_RATIO_MIN) |
| criteria | JSON expression referencing signal values |
| criteria_hash | SHA-256 of the criteria, checked on every save |
| category | GOVERNANCE, FINANCIAL, COMPLIANCE, VOLUME, TENURE, or COMPOSITE |
| action | ALLOW, DENY, or REFER (see below) |
| priority | Evaluation order (lower = earlier) |
| signal_namespace | TELEMETRY, RISK_SIGNAL, or UNDERWRITING |
| status | DRAFT, ACTIVE, DEPRECATED, or ARCHIVED |
| version | Integer version, incrementing |
| parent_version | Foreign key to the version this one was derived from |
| effective_date | When this version becomes applicable (optional) |
| end_date | When this version stops being evaluated (optional) |
Rule Actions
| Action | Behavior |
|---|---|
| ALLOW | The criteria must hold. A rule that does not pass makes the entity INELIGIBLE. |
| DENY | A rule that does not pass makes the entity INELIGIBLE. |
| REFER | A rule whose criteria pass sends the evaluation to a person. The result is CONDITIONAL. |
A REFER rule is how a tier above a floor is written: below a tenth funded is a refusal, and below a quarter funded is a referral.
Results
| Result | Meaning |
|---|---|
| ELIGIBLE | Every rule ran, every signal was obtained, and every rule passed |
| INELIGIBLE | A rule that must pass did not |
| CONDITIONAL | A REFER rule fired, or a trigger opened a review exclusion; a person must review |
| EXCLUDED | An active exclusion blocks the entity, or a trigger opened a block exclusion |
| WITHHELD | There is no basis to judge: no rule in force, a signal could not be obtained, or a trigger could not be evaluated |
| ERROR | The evaluation itself raised an error |
Installed Rules
The framework defines nine rules. 0 are in force in production today. A rule is active only when every signal its criteria read has a live writer; a rule whose signal nothing writes is held. A held rule is written down, versioned, hashed, and linked to its statute, and it decides nothing.
Tenure Rules
| Code | Criteria | Action | Description |
|---|---|---|---|
| TENURE_6M | months_on_platform >= 6 |
ALLOW | Refuses an association live on the platform for less than six complete months |
| TENURE_12M | months_on_platform < 12 |
REFER | Refers an association with less than a year to review: six to twelve months is a referral, under six is a refusal |
Financial Rules
| Code | Criteria | Action | Description |
|---|---|---|---|
| RESERVE_RATIO_MIN | reserve_funding_ratio >= 0.10 |
ALLOW | Refuses an association funded below a tenth of the target its reserve study set; withheld where no reserve fund carries a target |
| RESERVE_RATIO_HEALTHY | reserve_funding_ratio < 0.25 |
REFER | Refers an association funded below a quarter of its target to review |
| OPERATING_RATIO | operating_expense_ratio <= 1.10 |
ALLOW | Refuses an association whose trailing-year expenses exceed its revenue by more than a tenth; withheld for a year with no revenue posted |
| DELINQUENCY_MAX | assessment_delinquency_rate <= 0.15 |
ALLOW | Refuses an association whose past-due share of its open receivable exceeds fifteen percent |
Governance Rules
| Code | Criteria | Action | Description |
|---|---|---|---|
| GOV_SCORE_MIN | governance_risk_tier in [LOW, MEDIUM] |
ALLOW | Refuses an association whose governance risk tier is HIGH or CRITICAL |
| BOARD_ATTESTATION | board_attestation_current == True |
ALLOW | Requires a board attestation signed within the last year; withheld, not failed, for a board never asked to attest |
| QUORUM_COMPLIANCE | quorum_compliance_rate >= 0.75 |
ALLOW | Held: there is no meeting or quorum record, so nothing writes the signal and the rule decides nothing |
Signals
Rules reference signals, which are named data points registered in the signal registry with a source and a declared type. A rule can reference only a registered signal; a criterion that names an unknown signal does not save. The registry, its namespaces, and how signals are captured with each decision are described in Enforcement Dispatcher & Guard Architecture.
Activation asks one more question: does anything write each signal the rule reads? A signal nothing produces would leave the rule permanently indeterminate, and a signal with a writer that means something other than what the rule reads is refused as contested. Either way the rule is installed and held, with the reason written on the row. There is no flag to override it.
Evaluation Process
When eligibility is evaluated, the following sequence executes:
1. Gather Signals
The signal values for the entity are gathered into a flat dictionary. A signal that cannot be obtained, or that is present with a null value, is recorded as unavailable.
2. Check Existing Exclusions
If an active exclusion blocks the entity, the result is EXCLUDED and no further evaluation runs.
3. Evaluate Triggers
Exclusion triggers are evaluated and every trigger is recorded. A trigger that opens a block exclusion makes the result EXCLUDED. A trigger that opens a review exclusion makes the result CONDITIONAL and skips the rules. A trigger that could not see a signal it needs has not cleared the entity.
4. Evaluate Each Rule
Active rules are evaluated in priority order, filtered by product, entity type, and effective dates. Each rule's criteria are evaluated against the gathered signals. The outcome is PASS, FAIL, or indeterminate: a rule that could not obtain its signals, or whose criteria could not be evaluated, has no verdict and is never read as a failure.
5. Combine
The result is chosen least-favourable-first:
- A rule that must pass did not: INELIGIBLE
- A REFER rule fired: CONDITIONAL
- A rule or trigger was indeterminate, or no rule produced a determination, or no rule is in force: WITHHELD
- Every rule ran and passed: ELIGIBLE
A definite refusal outranks a referral, which outranks a withheld verdict, which outranks a pass. Withholding never softens a refusal: a rule that failed on a signal that was obtained is INELIGIBLE whatever else was missing.
6. Create the Evaluation Record
An EligibilityEvaluation is created with:
- A reference to every rule evaluated: rule id, version, code, and criteria hash, with a snapshot of the criteria
- All signal values gathered, and the list of signals unavailable
- Per-rule results and the combined result, with the reason
- The active exclusions and trigger hits that affected it
- A fingerprint of the rules and triggers in force, the evaluation date, and what triggered the evaluation
The model refuses to update an existing record and refuses to delete one.
Rule Versioning
Why Versioning Matters
Consider this scenario: On January 1, the RESERVE_RATIO_MIN rule requires a 10% reserve ratio. On March 1, the requirement is updated to 15%.
Without rule versioning, an auditor reviewing a January evaluation would see the 15% rule and might incorrectly conclude that the entity was evaluated against a 15% threshold. With versioning, the January evaluation references the exact rule version that was active on January 1 — the 10% version.
How Versioning Works
- Each rule has a stable
rule_id, an integerversion, and aparent_versionforeign key to the version it was derived from - The criteria of an ACTIVE rule cannot be changed: saving different criteria raises an error, and a new version is created instead
- A new version starts as a DRAFT with the version number incremented
- Two ACTIVE versions of one rule cannot cover overlapping dates
effective_dateandend_datedefine the temporal scope
Each evaluation records the rule id, version, and criteria hash of every rule it ran, and a snapshot of the criteria, so even if a rule code is reused across versions, the evaluation references the exact version.
Where Evaluations Come From
An evaluation runs when it is requested: through the authenticated eligibility endpoint for an association, and by the backfill run that initializes exclusion state. The verdict is written as an immutable EligibilityEvaluation.
Evaluations reach the enforcement chain through exclusions. When an evaluation opens a block exclusion, the l5_exclusion guard refuses every posting for that association while the exclusion is active, and records the refusal in the EnforcementDecision. That guard applies to all flows. The separate fund_eligibility guard answers a different question: whether an expense account posting to a reserve fund is marked reserve-eligible.
What This System Proves
The eligibility evaluation framework ensures that:
- Rules are transparent: Any auditor can inspect the rule criteria — they are data, not buried in code
- Evaluations are reproducible: The same rule version plus the same signals always produce the same result
- History is preserved: Every evaluation is stored as an immutable artifact with complete context
- Changes are traceable: Rule version chains show exactly when requirements changed and who changed them
- Absence is not a pass: A rule that cannot see its signals withholds a verdict, so a gap in the data never reads as a clean result
This describes prod
How CommunityPay Enforces This