---
title: "Rules Engine Architecture"
slug: rules-engine-architecture
section: Rules & Compliance
status: stable
description: "Tiered rule cascade model, conflict detection, and admin governance policy for the Globus Rules Engine Phase 3"
last-updated: 2026-10-07
---

# Rules Engine Architecture

Tiered rule cascade model, conflict detection, and admin governance policy for the Globus Rules Engine Phase 3

## Overview

The Globus Rules Engine Phase 3 uses a declarative, tiered cascade model to resolve which rules apply to a given applicant at any point in their journey. Rules are defined at multiple scopes and resolved in a strict priority order. The engine is built on the ruleCascadeResolver.ts service and dispatches side effects via actionOrchestrator.ts. For the full list of action types, see Rule Actions Reference .

## 6-Layer Rule Cascade

Rules are resolved in a strict priority order from most-specific to least-specific. A rule at a higher specificity layer always overrides a rule at a lower layer for the same ruleKey. This allows global defaults to be set at the regulatory bundle level while permitting brand- or program-specific overrides.

### Layer Reference

| Priority | Layer | Schema Table | Override Allowed |
| --- | --- | --- | --- |
| 1 (lowest) | Rule Default | rule_parameter_defaults | Yes — by all layers above |
| 2 | Visa Type | visa_type_rule_defaults | Yes — by category and above |
| 3 | Category | category_rule_defaults | Yes — by brand and above |
| 4 | Brand | brand_rule_defaults_v2 | Yes — by program and above |
| 5 | Program | program_rule_configs | Yes — by season only |
| 6 (highest) | Season | season_rule_overrides_v2 | No — highest specificity |

## Conflict Detection

Conflict detection runs at publish time, when a draft rule version is published. Scope uniqueness is checked per rule definition: a definition must not have two live published versions covering the same scope (overlap with a different definition is allowed). An immediate publish auto-supersedes the prior overlapping published version (archived, reason `superseded_by:<id>`); a future-dated publish cannot supersede yet, so it is rejected with a scope uniqueness violation listing the conflicting published version IDs, and the prior version must be archived first. The exception is a version bound to gate progression and published with a reviewed publication scope: when every conflicting published version is the definition's current published version, the future-dated publish is not rejected for scope uniqueness. This prevents silent overrides and ensures the cascade remains deterministic.

**Definition and version API.** The read routes below use the rules-engine read gate (any of `compliance.rules.author`, `compliance.console.view`, `compliance.bundles.read` or `programs.manage`; API-key scopes `compliance.read` or `admin.read`), and Program Managers only reach rules in their program scope. `GET /api/rules-engine/definitions/by-key/:key` returns a definition by its unique key (404 when missing). `GET /api/rules-engine/definitions/:id/scope-chain` returns the chain across the layers `rule_default`, `visa_type`, `category`, `brand`, `program` and `season`, each marked `this_rule`, `inherited` or `not_present`, plus the business rule sets that contain the rule. `GET /api/rules-engine/definitions/:id/applicant-gate-state` reads the live state of an `operational_gate` definition for one applicant (query `applicantId`, required; optional `applicationId`, otherwise the applicant's latest application); it returns 400 for a definition that is not an operational gate, and 404 for an unknown, out-of-scope or application-less applicant. Draft-version writes: `POST /api/rules-engine/versions/:id/scopes` and `DELETE /api/rules-engine/scopes/:id` need `compliance.rules.assign`, and `PUT /api/rules-engine/versions/:id/parameter-defaults` needs `compliance.rules.author`; all three return 400 when the version is not a draft.

## Admin-Only Creation Policy

> **Rule Creation is Permission-Gated**
>
> New rule definitions are created through POST /api/rules-engine/definitions, which requires the compliance.rules.author RBAC permission. Program managers who pass that gate may only create business-classification rules and may only edit business rules within their program scope; they cannot create or re-classify compliance rules. Other business users can view and configure rule parameters within permitted bounds. This policy is enforced at the API layer.

The admin-only policy prevents uncontrolled proliferation of rules and ensures that every new rule definition has been reviewed for compliance implications before activation.

**Business rule set transitions.** `POST /api/rules-engine/business-sets/:id/transition` (body `nextState` of `draft`, `published`, `deprecated` or `archived`, optional `reason`) moves a set through its lifecycle and writes an audit row in the same transaction. Forward edges (`draft` to `published`, `published` to `deprecated`, `deprecated` to `archived`) need `business.rules.manage` or `compliance.rules.author`. Override edges (`published` to `draft`, `deprecated` to `published`, `archived` to `deprecated`) need `compliance.rules.govern` and a non-empty `reason`. God-mode administrators may perform any legal edge; every other pair is illegal. Errors: 422 `RULE_SET_TRANSITION_ILLEGAL` (no such edge), 403 `RULE_SET_TRANSITION_FORBIDDEN` (missing permission for the edge kind), 422 `RULE_SET_TRANSITION_REASON_REQUIRED` (override without a reason). Callers holding neither `compliance.rules.author` nor `compliance.rules.govern` must also have program scope that intersects the set's attached programs, or brand scope covering its brand, otherwise the call returns 403.

## Rule Families and Classifications

Every rule definition carries two classification fields that govern how it is processed and displayed:

| Field | Values | Meaning |
| --- | --- | --- |
| classification | compliance \| business \| operational_gate | Compliance = regulatory mandate; Business = operational policy; Operational gate = readiness aggregator over existing Business/Regulatory rule outcomes |
| ruleCategory | e.g. eligibility, document, sevis | Groups rules for display and bulk evaluation |

**Business rule sets.** A set has a lifecycle state (`draft`, `published`, `deprecated`, `archived`). `GET` and `PUT /api/rules-engine/business-sets/:family/metadata` read and write a set's owner, description, brand, attached programs and attached seasons (write needs `compliance.rules.author`; unknown program, season or brand ids are rejected with 400). `POST /api/rules-engine/business-sets/:id/fork` (`compliance.rules.author`) copies a published set into a new draft revision and returns 400 for any other state. `POST /api/rules-engine/business-sets/:id/rules` and `DELETE /api/rules-engine/business-sets/:id/rules/:ruleDefinitionId` add and remove one member. `POST .../rules/bulk-add` and `POST .../rules/bulk-remove` take `ruleDefinitionIds` (1 to 500 UUIDs) and an optional `reason`, apply each rule independently and return `batchId`, a `summary` (ok, failed, total) and per-rule `outcomes`. Metadata and membership writes on a published set fail with 409 `RULE_SET_PUBLISHED_IMMUTABLE`; the bulk routes also fail with 409 `RULE_SET_ARCHIVED` on an archived set.

## Monotone Lock Floor

Regulatory bundle parameters marked as compliance_locked = true establish a monotone lock floor: no layer above the bundle (brand, program, or season) can override the locked parameter to a value that violates the regulatory constraint. For example, if the j1-camp-counselor bundle locks max_duration_months = 4, no program configuration can set a higher value. The lock is enforced at the API validation layer and surfaced as a read-only field in the Rules Studio UI.

**Effective-rules read API.** The read-only routes under `/api/rules-engine/effective` use the same rules-engine read gate and Program Manager scoping. `GET /effective/bundles` returns the resolved scope (query `visaType`, `categoryId`, `categoryCode`, `brandId`, `programId`, `seasonId`; a Program Manager is pinned to their program) and the bundles in it with their resolution `status`. `GET /effective/dependencies/:ruleId` returns, for the rule's active published version, its evidence facts, actions, bindings, containing rule sets, queue definitions, program attachments and the regulatory bundles that contain it; forward applicant count and rule-to-rule used-by are reported as `unavailable`. `GET /effective/runtime-detail/:ruleId` returns the active version (number, publisher, publish reason, `failSafeMode`), the winning source bundle with its lock settings, actions and scope chain, and answers 404 when the rule has no active published version.

**Applicant evaluation filters bundle members before precedence.** A bundle can be shared as an assignment or governance container across several evaluator families, so being in a bundle does not by itself put a rule in the applicant regulatory evaluator. Before the applicant evaluation resolves bundle precedence, it drops bundle-sourced rules owned by another evaluator (`filterRegulatoryApplicantBundleRules`, using `isRegulatoryApplicantBundleRule`). Some bundles use an allowlist (for `j1-phase-gate-governance`, only one criminal-background upload rule stays in the applicant stream) and some use a denylist of members owned by a narrower path (for `j1-camp-counselor`, `j1.camp_counselor.max_program_months`, which Ready for SEVIS evaluates itself with live placement facts). Bundles with no policy keep all members, and direct program attachments are not filtered. The filter runs first so that an ineligible governance-bundle copy of a rule definition cannot win precedence over the eligible program-bundle copy and then be discarded, which would make the rule vanish from applicant checks. A rule definition with reviewed, activated declared scope versions can also enter the applicant evaluation without a bundle or program attachment: when its declared scopes resolve to a matching version, it is attached as a `direct` attachment pinned to that version. An unpinned declaration whose scope does not match is dropped rather than falling back to the newest head, while explicit pins keep their attachment authority. Competing declared versions with no explicit direct pin raise a `RULE_SCOPE_VERSION_CONFLICT` conflict error. After attachments are selected, each attachment that has a resolved version has its parameters resolved with `resolveRulePolicy`, and the resulting `policy.parameters` replace the attachment's `parameterOverrides`.

## Published compliance review and override audit

A read-only deploy/operations report checks every current published compliance or regulatory applicant rule (rule definitions whose classification is compliance or regulatory, at their current published version). It is run with `npx tsx server/scripts/auditPublishedComplianceReviewOverrides.ts`, which delegates to the pure auditPublishedComplianceRules service (publishedComplianceReviewOverrideAudit.ts), prints a human-readable report followed by the same result as JSON, and exits 1 when any finding is reported. It never writes.

For each rule the report shows the version, whether the rule routes to review (a REVIEW_QUEUE or needs_review action, or a hold_for_review fail-safe mode) or resolves automatically, the fact keys the rule tree reads, and the override policy (permitted by default when no version-scoped policy exists). Every published applicant rule is classified as overridable, because all must support the governed override path.

A review path counts as complete only when it has a live (active) review queue, a reason code, and an authoritative producer for each fact the rule reads (a fact with a producing process or workflow-step producer; merely closing a case is not evidence). Findings are reported as configuration findings with these codes: MISSING_OVERRIDE_POLICY (no version-scoped policy; runtime uses the standard governed override path), FORBIDDEN_OVERRIDE_POLICY (published applicant rules may not disable governed override), INVALID_REVIEW_ROUTE (hold-for-review with no review-queue action, or an action pointing at a missing or inactive queue), MISSING_REVIEW_REASON_CODE, MISSING_REVIEW_PRODUCER, and UNKNOWN_FAIL_SAFE_MODE (anything other than fail_closed, fail_open_with_warning or hold_for_review).
