---
title: "Eligibility Engine & Compliance Tab"
slug: eligibility-engine
section: Rules & Compliance
status: stable
description: "Globus decides camp counselor eligibility with an automated **eligibility engine** instead of applicant-selected checkboxes and ad-hoc staff judgment."
last-updated: 2026-10-07
---

# Eligibility Engine & Compliance Tab

Globus decides camp counselor eligibility with an automated **eligibility engine** instead of applicant-selected checkboxes and ad-hoc staff judgment. The engine continuously evaluates each application against a defined qualification model, records an auditable assessment, and holds the applicant's journey at the eligibility gate until a human compliance officer issues a formal sponsor determination. Staff see and act on all of this in the **Compliance tab** of the applicant profile.

## Status & Principle

> **Live on development** — promotes to QA and production with the standard release train. Camp Counselor is the first program on the engine with qualifying paths; Intern/Trainee and Teacher are seeded with the universal minimum-age requirement only (ruleset v1, no qualifying paths authored yet). (Designed under ADR 0003, 2026-08-14; now shipped.)

The core principle:

> The machine assembles the evidence and checks the rules; a person makes the call. The engine never approves anyone on its own.

## Problem & Background

Before this work, applicants effectively self-declared their eligibility basis (student / youth worker / etc.) with no systematic check that the claim was backed by verified evidence. Eligibility knowledge lived in staff heads and spreadsheets — no consistent record of *why* someone was considered eligible, no re-check when their information later changed, and no defensible audit trail for the sponsor determination. Compliance information was scattered across surfaces with no single view answering "is this applicant compliant, and if not, why not?"

The legacy self-declared "basis" model has been **fully retired**. Applicants never pick a basis anymore; the engine derives qualification from what they actually submit. Applicants who were already verified under the old model had their approvals preserved as legacy records and are never re-opened automatically.

## How the Eligibility Engine Works

### Qualification Model (Camp Counselor, Ruleset v3)

An applicant qualifies when **every universal requirement passes AND at least one qualifying path is satisfied**.

**Universal requirement (applies to everyone):**

| Requirement | Bar | Behavior |
| --- | --- | --- |
| Minimum age | 18 or older | Fail-closed: if age is unknown, the requirement is not treated as met. A failed universal dominates the overall result regardless of paths. |

**Qualifying paths (any one is sufficient):**

| Path | Applicant-entered information checked (ruleset v3) | Document evidence |
| --- | --- | --- |
| **Post-secondary student** | Applicant states current study or current post-secondary enrollment (About Me education information) | None required — the path reads applicant information only |
| **Youth worker** | A child/youth-related work or volunteer entry, **or** a real "My Experience with Children" narrative. Generic work experience does not count | None required |
| **Teacher** | An education entry that names a teaching qualification (keyword-matched). Generic work experience does not count | None required |
| **Special skills** | Skills claimed on the Skills & Hobbies step. **Always routed to manual review** — this path can never auto-satisfy, by design. | None required |

Ruleset v3 replaced the earlier document-evidence paths: no Camp Counselor path demands an uploaded or staff-verified document, and a satisfied path means **ready for sponsor review** — the sponsor determination remains the sole qualification authority. Each path rule is a non-blocking machine signal: a failing path routes to eligibility review (compliance review queue) and never blocks progression by itself.

**Per-path outcomes:** not claimed → *requirements not satisfied*; claim unknown (information missing) → *insufficient information*; claimed and the path rule passes → *requirements satisfied*; claimed but the rule has not passed → *possibly qualified*; special skills when claimed → *requires manual review*.

**Overall assessment results:**

| Result | Meaning |
| --- | --- |
| **Qualifying path identified** | Universals pass and at least one path is fully satisfied — ready for sponsor review |
| **More information required** | A path is possible but something is still missing, pending, or awaiting manual review |
| **No qualifying path** | No claimed path can qualify |
| **Universal requirements not met** | A universal (e.g. minimum age) failed — this overrides everything else |

### When the Engine Evaluates

Evaluation is automatic and event-driven. It re-runs whenever the inputs could have changed:

- **Application lifecycle events** (submission, step completion, stage movement) via the event pipeline.
- **Document uploads** — both applicant portal uploads and staff uploads.
- **Document review decisions** — verifying or rejecting a document immediately re-evaluates.
- **Staff field edits** — `POST /api/applicants/:applicantId/field-edits` (and its `/batch` counterpart) await both the step-snapshot write and the promotion onto the canonical applicant record before responding; if either fails the call returns HTTP 500 `FIELD_EDIT_SYNC_FAILED` (`details.stage` = `persist` or `promote`) instead of a success whose write was dropped. Once both have committed, an edit touching an applicant-editable fact field on a completed step queues the same generic plus fact-targeted re-evaluation pair as staff step completion, so the verdict refreshes on its own.
- **Manual re-evaluate** — a staff recovery action on the Compliance tab. The call (`POST /api/rules-engine/applicants/:applicantId/re-evaluate`) runs the evaluation, then refreshes the application's operational-exception breach projection, reconciles exception queues and schedules step-access reconciliation *before* it responds; a failed breach projection returns an error instead of a contradictory success, and concurrent calls for the same application coalesce onto one run.
- **Boot-time catch-up sweep** — an opt-in one-shot sweep, scheduled about 90 seconds after boot only where `ELIGIBILITY_ASSESSMENT_CATCHUP` is exactly `on` (off by default), finds active camp applications with no assessment (or an outdated one) and evaluates them. This is how applicants created before the engine cutover were brought on board. It is replica-safe, idempotent, and never touches preserved legacy approvals.

Evaluations are idempotent: if nothing material changed, re-running produces no new record ("unchanged"). Every assessment records exactly which ruleset version and input snapshot produced it.

### Sponsor Determination — the Human Decision

| Action | Who | Requirements | Effect |
| --- | --- | --- | --- |
| **Approve** | Compliance officer / system admin (`eligibility.approve`, session login only) | Reviewer selects the qualifying path(s) being approved; all universals must be satisfied; selected paths must be *requirements satisfied* or *requires manual review* with any evidence meeting the path's configured bar (Camp Counselor ruleset v3 configures none); assessment must be current (not stale) | Determination = **Approved**, approval becomes the active record (superseding any prior one), validity flag turns on |
| **Reject** | Same | A live reason code (global or the program's brand); any active approval must be revoked first | Determination = **Rejected**, validity off, reason recorded |
| **Revoke** | Same | A live reason code **plus** a written rationale | Approval marked revoked; determination = **Reassessment required**, validity off |

**Refusal codes.** The approve, reject and revoke routes (`POST /api/eligibility/applications/:applicationId/approve`, `.../reject`, `.../approvals/:approvalId/revoke`, all `eligibility.approve`) return the code in `details.code`. HTTP 400: `NO_APPROVED_PATHS` (empty path list), `PRIMARY_PATH_NOT_APPROVED` (primary path not among the approved paths), `UNIVERSAL_REQUIREMENT_NOT_APPROVABLE` (a universal requirement was sent as a path), `PATH_NOT_APPROVABLE` (path result is not approvable in the assessed revision), `PATH_DEFINITION_MISSING` (no published definition for an approved path), and, on reject and revoke, `REASON_CODE_NOT_FOUND`, `REASON_CODE_DEPRECATED` and `REASON_CODE_BRAND_MISMATCH`. HTTP 409: `ASSESSMENT_NOT_EVALUATED`, `STALE_ASSESSMENT_REVISION` (the submitted `expectedInputRevision` differs from the stored one), `NO_PATH_DEFINITIONS` (no published path definitions for the assessment type), `INPUTS_CHANGED_SINCE_EVALUATION` (live inputs no longer match the stored revision; re-evaluate first), `UNIVERSAL_REQUIREMENTS_NOT_MET` (lists the unmet requirements), `EVIDENCE_NOT_AT_BAR` (an approved path evidence requirement is not satisfied at its configured bar), `ACTIVE_APPROVAL_EXISTS` (reject while an approval is active; revoke it first), `APPROVAL_NOT_ACTIVE` (revoke of an approval that is not active) and `APPROVAL_CONCURRENTLY_MODIFIED` (the approval changed during the revoke). An unknown approval id on revoke returns 404.

**Validity is continuously protected.** An approval is only trusted while the facts and evidence it was based on still hold. If a later evaluation detects material drift — an approved path's evidence replaced or un-verified, or a universal (like age data) no longer satisfied — the determination automatically moves to **Reassessment required**. Gate checks additionally re-verify the approval against the exact evidence documents (identity, version, review state) at read time and fail closed if anything mismatches.

### Camp Basis Assessments (Staff-Claimed, Reviewer-Verified)

Alongside the machine assessment, a Camp Counselor application can carry per-basis assessment rows for the four bases (`post_secondary_student`, `youth_worker`, `teacher`, `special_skills`), non-exclusive, under `/api/applications/:applicationId/camp-basis-assessments`. Each row is `suggested` (migrated from the legacy declaration, not yet claimed), `pending_review` (claimed), `verified` or `rejected`.

| Route | Purpose | Permission |
| --- | --- | --- |
| `GET /api/applications/:applicationId/camp-basis-assessments` | List rows with evidence links and the derived aggregate (empty list for a non-camp application) | `dashboard.view` |
| `POST .../camp-basis-assessments/:basisKey/claim` | Claim a basis (idempotent; moves `suggested`/`rejected` back to `pending_review`) | `applications.manage` |
| `POST .../camp-basis-assessments/:basisKey/review` | `verify` (needs at least one evidence link) or `reject` (needs review notes or a reason code); only a `pending_review` row can be decided | `applications.manage` |
| `POST .../camp-basis-assessments/:basisKey/evidence` | Attach a document from the same application and/or a note | `applications.manage` |
| `DELETE /api/applications/:applicationId/camp-basis-evidence/:evidenceId` | Remove an evidence link | `applications.manage` |

A **verified** row is immutable — no re-claim, review, note edit or evidence change (409 `VERIFIED_IMMUTABLE`). Writes to a non-camp application are rejected. Error mapping: `400 VALIDATION_ERROR` with `details.code` `NOT_CAMP_APPLICATION` (the application's program category is not `camp_counselor`), `REVIEW_RATIONALE_REQUIRED` (reject with neither review notes nor a reason code) or `EVIDENCE_REQUIRED` (verify with no evidence link, or an evidence request with neither a document nor a note); `409 CONFLICT` with `details.code` `BASIS_NOT_CLAIMED` (review of a row that is not `pending_review`) or `VERIFIED_IMMUTABLE`; and a plain `404 NOT_FOUND` (no distinct code) for an unknown application, for a basis that was never claimed (`BASIS_NOT_FOUND`, on review or evidence), for a document not on the application and for an evidence link not on the application (`EVIDENCE_NOT_FOUND`). An unknown `basisKey` in the path is a `400` with `details.code` `UNKNOWN_BASIS_KEY`. The overall result is derived on read and never stored: an OR aggregate that is fail-closed, eligible-by-basis only when at least one row is `verified`.

`GET /api/eligibility/camp-parity` is a read-only shadow-mode parity report comparing the engine's per-application overall result with that legacy verified-basis aggregate (any `verified` row). It needs an authenticated session or an API key carrying both `compliance.read` and `admin.read`, plus the `compliance.console.view` permission, and returns 503 with `code` `ELIGIBILITY_ENGINE_DISABLED` when the engine is switched off. Summary counts cover the caller's full in-scope camp population; the row list holds only the non-matching applications (capped).

### The Journey Gate

The camp counselor journey is blocked at the eligibility gate (rule CAMP-001) until the applicant has an **approved and currently valid** determination. The gate reads only the engine's determination — never an applicant-entered field — and fails closed if the fact is missing or unreadable. Disabling the engine (emergency flag) freezes determinations at their last value rather than opening the gate.

## Compliance Tab (Staff Experience)

The Compliance tab on the applicant profile is the single pane for eligibility and rule compliance.

### Eligibility Card

Shown for camp counselor applications, titled "*Program* Eligibility":

- **Machine assessment summary** — overall result, when it last ran, ruleset version, with expandable "Technical details".
- **Universal requirements** and **Qualifying paths (at least one must qualify)** — per-path checklists showing exactly what is satisfied, missing, or awaiting verification. Intern/Trainee and Teacher applications show the universal minimum-age requirement only.
- **Sponsor determination** — current status with badges such as "Approved", "Approval current", "Approval needs re-verification".
- **Actions** — "Approve eligibility", "Reject eligibility", "Revoke approval" (dialogs enforce path selection, reason codes, rationale), plus "Re-evaluate assessment" as a recovery action. Staff without the approve permission see a read-only view; approval is blocked with an explanatory message when universals are unmet or the assessment is stale.
- **Empty state** — "No eligibility assessment yet — it runs automatically as the applicant submits information and evidence." (Where the opt-in boot catch-up sweep is enabled, active applicants should not sit in this state.)

### Compliance Rules Zone

Shown for all applications:

- **Summary zone** above the rules: overall compliance verdict and counts, SEVIS readiness, Market Ready, a "What's changed" log of changes since the previous evaluation, and a "Compliance attention" summary with the overrides summary. When the current readiness outcome is SEVIS, the summary card's "What is preventing progress" list shows the same complete SEVIS readiness verdict as the SEVIS card (all structural and rule blockers, plus a "SEVIS readiness evaluated <time>" line); while that verdict is pending or stale, previous blockers are withheld and the card says SEVIS readiness is being refreshed. Other destinations keep the generic priority-issue list with its "+N more" pointer.
- **Market Ready card.** Market Ready is a host-market visibility permission, not part of the tab's proceed/blocking answer. It is a single global operational gate (`market_ready.j1_all_programs`, every J-1 program) with exactly three prerequisites — profile completion, minimum age and sponsor screening. The program-scoped Camp Counselor and SWT Camp Market Ready gates are retired and terms acceptance is no longer a prerequisite; a missing or unevaluated prerequisite fails the gate closed. The card lists unmet prerequisites grouped under their gate, the prerequisites already met, and the Host Talent Pool visibility verdict. Gate blockers carry a reason category and accepted outcome alongside the named prerequisite, and every surface (this card, the readiness panel, SEVIS pool and batch views) words them from the shared rule vocabulary rather than printing a server-built sentence. The vocabulary covers every gate blocker, not only Market Ready ones, and it also states who acts next (for example the applicant, a sponsor reviewer, or sponsor staff correcting a configuration problem), worded for an applicant or a host audience. Refusals from the host activation gate can carry a `reasonCategory` and a `nextActor` string alongside their message; both are absent on legacy rows.
- **Four decision buckets, not one flat table.** The rules list ("Compliance rules" with a count badge) is sorted into four collapsible sections — **Cannot proceed** (genuine failures that block the applicant until resolved or a governed override is recorded), **Needs sponsor review** (a reviewer must examine evidence and record a decision; system faults also land here, and this section carries the request-review guidance), **Waiting on applicant / partner** (evidence missing or pending; re-evaluates automatically once submitted) and **Informational & passed** (collapsed by default, still searchable and filterable). A rule's bucket is the server-stamped decision bucket, falling back to a client classification. Inside each bucket, rules are grouped by journey area (Identity, Eligibility, Background, Sponsor, Program, Documents, Fees, Pre-Departure, Communications…).
- **"What your sponsor needs to do" panel.** The sponsor actions panel groups every unresolved check (fail, pending, needs review or awaiting evidence) by where the sponsor would act, not by rule name. One exact document on file becomes a single "Review the document already on file" action however many rules read it; a required document type becomes "Upload and verify the ... document"; a workflow step becomes "Complete the ... workflow step"; a screening target becomes "Record the screening outcome"; an operational gate becomes "Clear the ... gate", keyed by the gate and never merged across gates; a manual-review check becomes "Record a sponsor review decision" (naming an owning team only when one is authored); awaiting-evidence checks collapse into "Collect the outstanding applicant evidence"; anything else stands alone as "Resolve ...". A rule that could not be evaluated is shown apart as "Report a broken rule", with no applicant-side destination and not counted among the blockers. Groups are ordered blocking first, then system problems, sponsor review and waiting, and exact duplicates (same rule key and version) are dropped. The panel is display only and changes no counts, gating or readiness.
- **Honest statuses.** A rule that cannot pass yet because information hasn't arrived shows **"Awaiting Evidence"** — "Awaiting the required information — this rule will be re-checked once it is on record." — and is never dressed up as a hard failure. Genuine failures show why, the applicant-facing message (if any), and the failing conditions. Failures sort to the top.
- **"Explain this decision"** — every rule row expands to a plain-English explanation: outcome, reason code, required actions, failing conditions and when it was evaluated. Sensitive actual values are hidden unless the viewer holds the sensitive-data permission.
- **Compared dates for time-window rules.** Date-window checks (for example the passport six-month validity rule: passport expiry after program end date + 180 days) show the compared values like any other comparison: **Expected** is the computed cutoff with its derivation (e.g. "After 28 Mar 2028 (Program end date + 180 days)"), **Actual** is the recorded date, "Not recorded" when the date is missing (with the expected evidence source, e.g. Applicant Profile), or the raw malformed value when it cannot be parsed. The date row is judged on the window result itself, so a failing child condition does not paint the date red. Decisions persisted before the evaluator recorded these details show no comparison entry until they are re-evaluated.
- **Filters & quick actions** — outcome status pills, a rule search box, rule-type filters (Operational Gates / Regulatory / Program-Business), lifecycle (stage) filters, and an Expand all / Collapse all toggle; quick actions to Request Document, Add Internal Note, Override Rule (only when an overridable rule exists and the viewer has the permission): holders of `compliance.review.manage` apply the override immediately ("Apply Compliance override") only when the rule is not dual-approval; everyone else, and every dual-approval rule, submits "Request Compliance Override", which opens a linked Compliance Case and stays inactive until approved (the response reports `applied` or `pending_compliance_authorization` from the resulting override status), and Move Stage.
- **Failure-loading honesty** — if the evaluation feed itself fails to load, the tab says so explicitly ("This is a loading problem, not a compliance verdict") rather than implying a compliance state.
- **Seeded outcome copy for identity and document rules.** `server/seed/ruleOutcomeCopy/identityDocuments.ts` is the authored outcome-copy catalogue for the identity, passport, documentation and age rules that apply across J-1 programmes. The boot seeder and the paired data migration both project it, and each writer fills only fields the live rule lacks, never overwriting authored values. An entry, keyed by `rule_definitions.key`, carries a remediation `kind` with an applicant instruction and operator guidance, and may add a separate `needsReviewRemediation` for a `needs_review` outcome, a queue, an applicant message, a reason code or a description. Entries include `documentation.academic_transcript_verified.j1_teacher`, `documentation.criminal_background_upload_required.j1_all_programs.p11_4`, `documentation.degree_certificate_verified.j1_teacher`, `documentation.passport_upload_required.j1_all_programs.p11_4` (queue `missing-compliance-documents`), `documentation.qualification_details_match.j1_teacher`, `documentation.qualification_documents_complete.j1_teacher`, `documentation.qualification_institution_match.j1_teacher`, `documentation.teacher_certification_verified.j1_teacher`, `eligibility.minimum_age.j1_all_programs.p11_4` (age measured on the programme start date), `identity.duplicate_passport_detection.j1_all_programs.p11_4` (reason code `DUPLICATE_PASSPORT_DETECTED`, staff review) and `identity.qualification_name_match.j1_teacher` (the applicant uploads name-change evidence). The catalogue also holds `background_check.package_verified`, `documentation.name_change_evidence.j1_teacher`, `documentation.passport_data_complete_readable.j1_all_programs`, `identity.passport_citizenship_resolved.j1_all_programs` and `identity.passport_identity_applied.j1_all_programs` (both of these identity keys use staff-review remediation), and `j1_primary.passport_expiry_meets_requirement` with one variant per brand (`.brand:<brand key>`).

### Evidence Pack, Passport Citizenship & Review Routing (Sep 2026)

Three Compliance-tab behaviours shipped in the Aug 17 – Sep 15 2026 window, with follow-up changes noted below:

- **Operations Compliance Evidence Pack (redesigned).** The Compliance tab's **Evidence pack** action prints (or saves as PDF, via the browser print dialog) a decision-focused record of the evaluation currently shown. Page one leads with the applicant, programme, visa/category context, season, evaluation time and overall decision, followed by separate counts for blocking, awaiting evidence, awaiting review, **Met**, not applicable, not evaluated and evaluation errors; blocking issues and next actions come before supporting detail. **Met** is used only for a check that ran and passed. Operational gates have their own section (lifecycle impact) and their underlying rule issues are shown beneath them but are *not* counted a second time in the base-rule summary. The pack is a presentation of the tab's server-authoritative payload — it is client-side, needs no second export endpoint, and changes no enforcement, progression or override authority. It reports the payload at the recorded `evaluatedAt` with rule/bundle versions, evidence provenance, override caveats, identifiers and comparison history, but does not promise byte-identical regeneration: the downloaded file is the historical artefact. A loading, failed or unauthorised override read prints as a caveat, never as "no overrides"; historical stage targets are labelled historical; programme applicability comes from the authoritative programme/category/visa/brand/season context (Camp Counselor rows are not filtered by name).
- **Passport Citizenship uses a manual passport-nationality answer.** The Passport Information step now carries a dedicated required field, `passport_nationality` ("Nationality shown on passport", country select), separate from the issuing country. The **Passport Citizenship** identity check compares the recorded citizenship (applicant profile nationality) with the nationality on the passport using OCR **and** this manual answer when available; when both are present, both must agree with the recorded citizenship, and each source is labelled in the comparison (Applicant Profile / Passport OCR / Applicant Portal). Comparison is by canonical country code, so ZA, ZAF and South Africa are the same country. A genuine mismatch, or no citizenship recorded at all, fails the rule; a recorded citizenship that does not resolve to a country, or a failed OCR lookup, is held for human review. If neither passport source is available the rule passes but is flagged **Awaiting Evidence** (amber, not a verified match) rather than failing, because the passport upload and readability rules already gate those applicants. Editing the applicant's recorded nationality re-evaluates this rule automatically (the match fact is declared as derived from nationality), so the verdict does not go stale after a correction. The passport's country of issue is never citizenship evidence. Genuine conflicts remain a human adjudication via the reviewer actions (Confirm Match / Confirm Acceptable Variation / Request Applicant Correction / Reject Identity Match). The schema correction reaches existing applications through the step-library override path.
- **Sufficient-finances review routes to Eligibility & Documents.** The Intern/Trainee `FINANCES_NOT_VERIFIED` reason code (severity hint *review*; 22 CFR 62.22(e)(2)) now carries the `eligibility_documents` routing domain (Eligibility & Documents), so documented-funds review lands with that team rather than the finance area; the domain is permitted for the `compliance-review-required` queue. The applicant-facing message is unchanged.
- **Seeded reason codes for eligibility outcomes.** `J1_CORE_REQUIREMENTS_NOT_MET` (blocking): the shared J-1 compliance gate is not passed because required dependency rules failed, are blocked or need unresolved review. `ELIGIBILITY_DETERMINATION_REJECTED` (blocking): a compliance reviewer rejected the sponsor determination; a reason code is mandatory at the API. `ELIGIBILITY_APPROVAL_REVOKED` (review): an active approval was revoked, the assessment moves to reassessment required and dependent gates fail closed. `WORKFLOW_AGREEMENTS_NOT_ACCEPTED` (blocking): required agreements resolved for the workflow context of the application (Agreement Library) have no acceptance row. `SPONSOR_SCREENING_NOT_CLEARED` (blocking): the sponsor interview has no terminal passed outcome, which blocks market readiness.

## What Applicants See (Portal)

Applicants never see the raw engine. When a rule fails **and** its reason code is marked *applicant-visible*, the approved applicant-facing message appears as inline feedback on the specific portal step the applicant can act on (e.g. a passport expiry problem surfaces on the passport step). Feedback carries a freshness indicator; failures on facts the applicant cannot edit surface only at the phase/banner level. Anything not marked applicant-visible stays internal.

## Evaluation Run History (Nightly Batch)

The nightly eligibility batch pipeline records each run, and staff can review the history:

- `GET /api/admin/rules/evaluation-runs` — paginated list (`{ items, total, limit, offset }`, limit 1–100, default 20), newest first by start time, optional `status` filter (`running`, `completed`, `failed`). Each item carries the outcome buckets: *passed* (pass + eligible with grace + not applicable), *failed*, *waitlisted* (pending + needs review — held awaiting data or human review; the engine has no literal waitlist outcome) and *errored*.
- `GET /api/admin/rules/evaluation-runs/:id` — the run summary, bucket counts and per-applicant rows with the engine's per-rule results verbatim; 404 for an unknown id.
- Both routes require the `rules.evaluationRuns.view` permission (independently grantable from `compliance.console.view`; granted to the compliance officer, compliance admin and program manager roles).
- Admin pages: `/admin/rule-runs` (latest-run cards and a paginated run table) and `/admin/rule-runs/:id` (per-applicant drill-down). The cohort-trigger (POST) call sites were not restored by this API.

## Season Anchor for Date-Window Rules

Date-window evaluations (passport expiry against program end, SWT vacation window, grace-period anchors, age rules referencing program start) anchor to the application's explicitly scoped season **when that season belongs to the addressed program**; otherwise — absent, unknown or cross-program season id — they fall back to the program's active season, and if neither resolves the program dates are null. Age rules that reference program start therefore re-anchor to the scoped season's start. Existing persisted outcomes change only on their next normal re-evaluation.

Passport-validity rules also branch on the fact `isSixMonthClubMember` (owner applicant identity, boolean): true when the applicant's nationality belongs to the Six-Month Club, whose passports need only be valid through the program end date instead of six months beyond it. It is read from the `countries.is_six_month_club_member` reference column, matched on the applicant's nationality (alpha-2, alpha-3 or country name). In the seeded passport tree a passport passes when its expiry is more than 180 days after the program end date, or when the applicant is a club member and the expiry is after the program end date. When membership cannot be determined and the expiry falls inside the 180-day buffer, the fail-safe holds the case for review instead of blocking it.

## Permissions & Access

| Capability | Permission |
| --- | --- |
| See the Compliance tab & rules table | `compliance.view` |
| See the Eligibility card / assessment data, see override summary | `compliance.console.view` (granted alongside `compliance.view` for compliance roles) |
| Trigger re-evaluate from the Compliance tab | `compliance.rules.author` |
| See the nightly evaluation run history | `rules.evaluationRuns.view` |
| Approve / reject / revoke eligibility | `eligibility.approve` (session login required — not available to API keys) |
| See sensitive actual values in explanations | `sensitive_data.view` |
| Change a requirement gate (`PATCH /api/requirement-gates/:id`; overrides and state changes are audited) | `override.apply` |
| Decide a Compliance Case item (`POST /api/compliance-cases/:caseId/items/:itemId/decision` with `approve`, `reject` or `request_more_information` and a reason of 8 to 2000 characters) or refresh its request-time evidence binding (`.../refresh`, same reason rule) | `compliance.review.manage` |
| Resubmit a Compliance Case item after an information request (`POST /api/compliance-cases/:caseId/items/:itemId/resubmit`; only the original requester may attach evidence) | `eligibility:override:create` or `programs.manage` |
| Author or publish rules | `compliance.rules.author` (Compliance Console; out of scope here) |
| Read the latest rule evaluations for an applicant (`GET /api/applicants/:id/latest-eligibility`) | `applicants.list.view` (API keys need `applicants.read` or `compliance.read`) |

`latest-eligibility` returns the newest rule evaluation records for the applicant (from the last 20, one per rule version, program and season, limited to the caller scope) and 404 for an unknown applicant. The `/api/eligibility/*` routes (assessment read, approve, reject, revoke, evaluate, camp-parity) return HTTP 503 `ELIGIBILITY_ENGINE_DISABLED` while the `ELIGIBILITY_ENGINE_ENABLED` environment variable is set to exactly `false`; any other value, including unset, leaves the engine on.

## Safeguards & Auditability

- **Fail closed everywhere.** Missing facts, unreadable evidence, or gate errors block progression; nothing passes by accident.
- **Append-only approvals.** Approvals are never edited in place — new approvals supersede, revocations are recorded with reason and rationale, and every determination stores who, when, and why.
- **Idempotent, versioned assessments.** Each assessment records its ruleset version and input snapshot; re-evaluations that change nothing write nothing.
- **Legacy protection.** Pre-engine verified approvals were migrated as preserved records and are excluded from every automatic sweep.
- **Ruleset upgrades are managed.** When the qualification model changes (version bump), existing assessments are systematically re-evaluated — most recently via the opt-in boot catch-up sweep in environments where it is enabled — rather than left silently stale.

## Architecture Notes (ADR 0003)

The engine was designed under ADR 0003 and shipped on that architecture:

- **Built on Rules Engine V2, not beside it.** Versioned/pinned rule definitions, `all`/`any` condition trees, per-node evaluation records (the explainability trail), publish gates, and idempotent evaluation are all V2 — no second condition evaluator exists.
- **Typed evidence resolution** is the genuinely new layer: a deterministic resolver matches application documents by document type + verification state to each path's declared evidence requirements. The mechanism is retained, but Camp Counselor ruleset v3 paths declare no document requirements — they read applicant-information signal facts — so for camp the path rules consume information facts rather than derived evidence facts. Raw step media is never evidence — documents must have crossed the step-media → applicant-documents bridge.
- **The assessment is a projection, not a decision-maker** — a read model over V2 evaluation records. Machine evaluation and sponsor determination are separate state machines: staff never set machine results; the system never sets determinations.
- **Approvals are immutable snapshots** capturing the approved path, pinned ruleset version, facts snapshot, and evidence snapshot (artifact ids + versions + verification states). Validity is derived continuously from those snapshots — material drift flips validity without rewriting history.
- **Path definitions are seeded, versioned config** — required facts, optional `one_of` evidence-type lists (empty for Camp Counselor v3), manual-review flags, and material-input lists — superseded on change, never mutated. Intern/Trainee and Teacher register their own assessment types with universal-requirement definitions only.
- **Persistence tables.** `eligibility_path_definitions` holds the versioned path and universal-requirement definitions (assessment type, path key, requirement kind, version, status `published` or `superseded`, rule definition key, required fact keys, evidence requirements, material input fact keys). `eligibility_path_evaluations` holds one row per assessment and path (unique on the pair) with the result, explanation lines, the pinned path definition, rule version and evaluation record, and a manual-review flag. `eligibility_evidence_references` snapshots the document matched to each evidence requirement (document id, type, version, verification state and resolution source at match time). `eligibility_approvals` is the append-only approval record: approved path keys (at least one), optional primary path, ruleset key and version, the assessed input revision, facts and evidence snapshots, approver, rationale, status and revocation fields, with a unique index allowing one active approval per assessment. `eligibility_override_audit_log` (defined with the rules-engine models) records one row per override event (`created`, `approved`, `rejected`, `revoked`, `expired`, `expiry_changed`) with the original rule result, the decision and a justification snapshot.

## Rollout Status & Operations

- **Live now (development):** engine on by default for camp counselor, Compliance tab fully wired, and the boot catch-up sweep is opt-in per environment (`ELIGIBILITY_ASSESSMENT_CATCHUP=on`). QA receives everything on the next release promotion.
- **Verification:** an environment with the sweep enabled logs a catch-up summary line (scanned / evaluated / failed counts) ~90 seconds after boot, so ops can confirm the backfill ran without database access; an environment without it logs an `[eligibility-catchup] Not scheduled` line.
- **Kill switch:** the engine can be disabled by environment flag (freezes determinations; keeps the gate closed); this is not expected in normal operation. The catch-up sweep is a separate opt-in that is off unless `ELIGIBILITY_ASSESSMENT_CATCHUP` is set to `on`.
- **Job environment flags:** `ELIGIBILITY_ASSESSMENT_CATCHUP` is opt-in: the one-shot boot catch-up pass is scheduled only when it is exactly `on`; unset or any other value logs a not-scheduled line and does nothing. `ELIGIBILITY_BATCH_SCHEDULER` set to `off` disables the nightly eligibility batch scheduler (any other value leaves it running) and `ELIGIBILITY_BATCH_HOUR_UTC` (integer 0 to 23; invalid values fall back to 03:00 UTC) sets the hour of its daily run. `RULES_ENGINE_V2_PERSIST_EVALUATIONS` set to exactly `on` makes the eligibility rules engine also persist each per-applicant evaluation; the write is fail-soft (a failure is logged, never surfaced to the caller).

## Future Scope

- Author qualification paths for **Intern/Trainee** and **Teacher** (their ruleset v1 carries only the universal minimum-age requirement, so the overall result for an of-age applicant is *no qualifying path* and sponsor approval stays impossible until paths ship; Intern/Trainee is pending its regulatory bundle's pathway definition).
- Deeper Compliance Console integration for determination workload (queues of "ready for sponsor review" assessments).
- Season-level eligibility rule overrides reporting (tracked separately on the project board).

## Known Open Items

- The manual "Re-evaluate assessment" action always reports success in the UI even when the engine returns a non-persisting outcome (e.g. paths not published in that environment). Cosmetic, known, on the backlog.
- Regulatory citations are still being filled in for a subset of compliance rules (tracked on the project board).

## Related Pages

> **Related pages**
>
> See the Rules Engine for the V2 evaluation substrate the engine builds on, Camp Program Rules and SEVIS Readiness for the CAMP-001 gate this engine now drives, and Intern/Trainee Scope for the sibling assessment the architecture was validated against. The Compliance Console (rule authoring, override governance) is a separate surface with its own documentation.
