Rules Engine

The most legally and morally important piece of the project. A standalone open-source library that takes a structured flight scenario and returns eligibility, legal basis, compensation amount, time limits, and required next steps under every relevant passenger-rights regime.

This is the layer that every claims company in the world currently maintains privately, expensively, and frequently incorrectly. Making it open, audited, and neutral is the project's most defensible strategic move.

What the engine does

Input: a Scenario object (route, distance, fare, delay reason, delay duration, passenger count, special circumstances). Output: a Determination object (eligibility per regime, legal basis, expected compensation, time limits, required actions, edge-case flags).

The engine is purely functional: same input always produces same output. No I/O, no external API calls, no state. This makes it trivially testable, cacheable, and embeddable in any environment — from a Lambda function to a mobile app to a court filing.

The engine is versioned independently of the schemas and the dataset. A consumer can pin to a specific engine version and know that the rules will not change unexpectedly. This matters because passenger-rights law is contested terrain: court rulings shift interpretations, and consumers using the engine in production (claims companies, insurance underwriters) need stability for the duration of a policy term or a claim window.

Repository structure

The engine lives in flighthelp/rules-engine, licensed MIT.

/rules-engine/
├── /regimes/
│   ├── eu-261/
│   │   ├── rules.ts                  Executable logic
│   │   ├── triggers.ts               Eligibility conditions
│   │   ├── compensation.ts           Compensation amount logic
│   │   ├── exceptions.ts             Extraordinary circumstances
│   │   ├── time-limits.ts            Claim deadlines per member state
│   │   ├── test-cases.json           300+ fixture cases
│   │   ├── legal-references.md       Article-by-article citations
│   │   └── case-law.md               Relevant court decisions
│   ├── uk-261/
│   ├── brazil-400/
│   ├── montreal-convention/
│   ├── us-dot-tarmac-delay/
│   ├── us-dot-bumping/
│   ├── canada-appr/
│   ├── india-dgca/
│   ├── israel-aviation-services-law/
│   ├── australia-acl/
│   └── japan-civil-aeronautics/
├── /engine/
│   ├── evaluator.ts                  The composer that runs all applicable regimes
│   ├── types.ts                      Scenario, Determination, etc.
│   ├── currency.ts                   Conversion helpers (no live rates; pinned table)
│   └── distance.ts                   Great-circle distance calculations
├── /playground/
│   ├── cli.ts                        Terminal playground
│   └── web/                          Browser-based scenario explorer
├── /sdks/
│   ├── node/                         npm: @flighthelp/rules-engine
│   ├── python/                       PyPI: flighthelp-rules-engine
│   ├── php/                          Composer: flighthelp/rules-engine
│   ├── go/                           pkg: flighthelp.dev/rules-engine
│   ├── rust/                         crates.io: flighthelp-rules-engine
│   ├── java/                         Maven Central: net.flighthelp:rules-engine
│   └── swift/                        SPM: flighthelp/rules-engine-swift
├── package.json
├── README.md
└── VERSIONING.md

The Node.js implementation is canonical; SDKs in other languages are transpiled or maintained in parallel with strict API parity. Each language's tests run the same fixture cases against the same expected outputs.

The input: Scenario

type Scenario = {
  flight: {
    operating_carrier: string         // IATA code
    marketing_carrier?: string        // IATA code, if different (codeshare)
    flight_number: string             // e.g. "LH441"
    origin: string                    // IATA airport code
    destination: string               // IATA airport code
    scheduled_departure: string       // ISO 8601 with timezone
    scheduled_arrival: string         // ISO 8601 with timezone
    actual_departure?: string         // ISO 8601 with timezone
    actual_arrival?: string           // ISO 8601 with timezone
    fare_class?: string               // e.g. "lufthansa:economy-light"
  }

  disruption: {
    type: "delay" | "cancellation" | "denied_boarding" |
          "downgrade" | "missed_connection" | "diversion" |
          "tarmac_delay" | "baggage_lost" | "baggage_delayed" |
          "baggage_damaged"
    delay_minutes?: number            // for delay / tarmac_delay
    cancellation_notice_days?: number // for cancellation
    reason?: DisruptionReason         // see below
    rebooking?: {
      offered: boolean
      accepted: boolean
      new_arrival?: string            // ISO 8601
    }
    care_provided?: {
      meals: boolean
      hotel: boolean
      transport: boolean
      communication: boolean          // e.g. phone calls
    }
  }

  passenger: {
    count: number                     // group size for compensation calc
    has_disability?: boolean          // affects denied_boarding priority
    unaccompanied_minor?: boolean
    ticket_price?: { amount: number; currency: string }
                                      // needed for refund calcs in some regimes
  }

  context?: {
    booking_was_part_of_package?: boolean
    eu_consumer?: boolean             // for jurisdiction edge cases
    claim_date?: string               // for time-limit calculations
  }
}

type DisruptionReason =
  | "weather"
  | "atc_strike"
  | "airline_staff_strike"
  | "airline_operational"             // staffing, scheduling
  | "technical_aircraft"              // maintenance, malfunction
  | "security"
  | "medical_emergency"
  | "bird_strike"
  | "political_unrest"
  | "natural_disaster"
  | "pandemic_restrictions"
  | "regulatory"                      // e.g. drone closures
  | "unknown"
  | "airline_stated_but_disputed"     // when airline blames weather but contributors dispute

Note that some inputs (like delay_minutes) are only required for some disruption types. The engine validates the input shape at runtime and returns a typed error if required fields are missing.

The output: Determination

type Determination = {
  scenario_id: string                 // hash of input, for caching
  evaluated_at: string                // ISO 8601 timestamp
  engine_version: string              // semver of the engine

  applicable_regimes: RegimeResult[]
  best_outcome: BestOutcome           // highest-compensation regime, summarized
  next_steps: NextStep[]
  warnings: Warning[]
}

type RegimeResult = {
  regime: string                      // e.g. "eu-261"
  applies: boolean
  reason: string                      // why it applies or doesn't
  legal_basis: LegalCitation[]
  compensation?: {
    amount: number
    currency: string
    confidence: "certain" | "likely" | "contested"
  }
  rights: Right[]
  exceptions_considered: string[]     // which exceptions were evaluated
  time_limits: TimeLimit[]
}

type LegalCitation = {
  regulation_id: string               // e.g. "eu-261-2004"
  article: string                     // e.g. "Article 7(1)(b)"
  description: string
  url?: string
}

type Right = {
  type: "compensation" | "rerouting" | "refund" | "care" |
        "downgrade_refund" | "upgrade" | "communication"
  description: string
  conditions?: string
  legal_basis: LegalCitation[]
}

type TimeLimit = {
  action: string                      // e.g. "file claim with airline"
  deadline_days: number
  starts_from: "disruption_date" | "scheduled_arrival" | "claim_response"
  jurisdiction_specific?: boolean
}

type BestOutcome = {
  regime: string
  compensation: { amount: number; currency: string }
  summary: string
}

type NextStep = {
  order: number
  action: string
  who_to_contact: string
  template_message_id?: string        // links to Scenario.template_messages
  deadline_days?: number
}

type Warning = {
  level: "info" | "caution" | "critical"
  message: string
  applies_to_regimes: string[]
}

The confidence field on compensation is the engine's honest acknowledgment that legal interpretation is not always settled. "Certain" means the case law is unambiguous. "Likely" means the prevailing interpretation supports compensation but airlines sometimes contest it. "Contested" means courts have ruled both ways, or the regulation is being amended.

How a regime is structured

Each regime is a self-contained module that exports a standard interface:

interface Regime {
  id: string
  jurisdiction: string[]              // ISO 3166 codes where applicable
  applies(scenario: Scenario): { applies: boolean; reason: string }
  evaluate(scenario: Scenario): RegimeResult
  testCases: TestCase[]
}

The composer (engine/evaluator.ts) iterates through every regime, calls applies() to filter, then calls evaluate() on each applicable regime, then composes the results into a Determination.

When multiple regimes apply (e.g. a UK-departing flight on an EU-registered carrier can trigger both UK 261 and EU 261), the engine returns all applicable results and identifies the highest-compensation outcome as best_outcome. The user (or the application built on top) decides which to pursue.

Example: EU 261 evaluation

The EU 261 module is the most developed and is the reference implementation for new regimes. Its structure:

triggers.ts — determines whether EU 261 applies. The conditions:

  • Flight departed from an EU member state (any airline), OR
  • Flight operated by an EU-licensed carrier into an EU member state
  • AND passenger was denied boarding, flight cancelled, or flight arrived ≥3 hours late
  • AND passenger held a confirmed reservation
  • AND passenger presented for check-in on time

If any of these fail, the regime does not apply and the result records the reason.

compensation.ts — calculates the compensation amount based on the EU 261 distance bands:

  • ≤1,500 km: €250
  • 1,500–3,500 km intra-EU: €400
  • 1,500–3,500 km extra-EU: €400
  • 3,500 km extra-EU: €600

Compensation is halved if the airline offered re-routing that arrived within 2/3/4 hours of the original arrival (depending on distance band). The engine handles this calculation explicitly with citations.

exceptions.ts — evaluates the "extraordinary circumstances" defense. The engine codifies the prevailing interpretations from CJEU rulings:

  • Weather: extraordinary if severe and unforeseeable, not extraordinary for typical seasonal conditions.
  • ATC strikes: extraordinary.
  • Airline staff strikes: not extraordinary (Krüsemann ruling, 2018).
  • Technical defects: not extraordinary unless from a hidden manufacturing defect (Wallentin-Hermann, 2008).
  • Bird strikes: extraordinary, but only if airline took all reasonable measures (Pešková, 2017).

Each ruling is cited with the case name and year. The engine returns exceptions_considered listing which exceptions were evaluated for the scenario.

time-limits.ts — implements the claim deadlines, which vary by EU member state:

  • Germany: 3 years.
  • France: 5 years.
  • Italy: 2 years.
  • Spain: 5 years.
  • ...and so on for every EU jurisdiction.

The time limit returned depends on which member state's courts would hear the claim — typically the carrier's HQ jurisdiction or the passenger's residence.

test-cases.json — 300+ fixture cases pinning specific inputs to specific expected outputs. Each case cites its legal basis. Cases include:

  • Straightforward delay scenarios at each distance band
  • Cancellation with various notice periods
  • Extraordinary circumstance edge cases
  • Multi-carrier itineraries
  • Codeshare scenarios
  • Cases that were litigated, with the actual court outcome as expected output

Test-driven legal updates

When a court rules on a case that affects the engine's interpretation:

  1. The ruling is captured as a new test case with the actual outcome.
  2. The engine is run against the new case.
  3. If the engine's output matches, the case is added as documentation only (no code change).
  4. If the engine's output differs, the rule is updated and a new engine version is released.
  5. The change is documented in case-law.md and the version bump notes.

This makes the engine's evolution traceable to specific legal events. Every interpretation change can be traced to a court ruling or regulatory amendment.

Versioning policy

The engine follows semver, with stronger guarantees than typical software:

Major version (2.0.0) — when a rule's interpretation changes in a way that affects existing claims. For example, if a court ruling overturns previous case law and the engine must change to match. Major versions are supported for at least 36 months.

Minor version (1.1.0) — when a new regime is added, or a new rule is added that doesn't conflict with existing rules.

Patch version (1.0.1) — when a bug in rule implementation is fixed, when documentation is improved, when a new test case is added.

Each version is permanently published. A consumer that pinned to 1.5.3 in 2027 can still install 1.5.3 in 2040.

The engine maintains a LEGAL-VERSIONING.md document mapping engine versions to the legal state-of-the-world they represent: "Engine version 2.x reflects the interpretation prevailing after the CJEU ruling in X (date Y)."

Calling the engine

As a library (Node):

import { evaluate } from "@flighthelp/rules-engine"

const determination = evaluate({
  flight: { ... },
  disruption: { ... },
  passenger: { ... },
})

As a library (Python):

from flighthelp_rules_engine import evaluate

determination = evaluate({
  "flight": {...},
  "disruption": {...},
  "passenger": {...},
})

As an API endpoint (HTTP):

POST https://api.flighthelp.net/v1/compensation/evaluate
Content-Type: application/json
Authorization: Bearer YOUR_API_KEY

{
  "flight": {...},
  "disruption": {...},
  "passenger": {...}
}

All three return the same Determination shape.

What the engine is not

Not a claims-filing service. The engine determines eligibility and returns the playbook. The user (or the application) does the filing.

Not legal advice. The engine's output is informational. Every Determination includes a warning to that effect. Complex cases should be reviewed by a lawyer; the engine cites the relevant articles to make the lawyer's job easier.

Not omniscient. The engine evaluates against the inputs it receives. It cannot verify whether the passenger presented for check-in on time, whether the disruption reason given by the airline is accurate, or whether the airline actually offered the care it claims to have offered. The user is responsible for the truthfulness of inputs.

Not a substitute for the underlying regulation. The engine is a computational shortcut for getting to a probable answer fast. The authoritative source is always the regulation itself and the case law interpreting it. The engine's output cites both so users can verify.