Automating AD/SB Correlation with Python

Correlating Airworthiness Directives and Service Bulletins against a fleet’s live configuration is where hand-tracking quietly fails: a spreadsheet cannot prove that an AD’s effectivity actually intersects a given tail’s installed part numbers and serial ranges. This page builds a deterministic correlation engine in Python that computes applicability and compliance status instead of asking an engineer to remember it. It belongs to the FAA Part-145 Recordkeeping Standards work, where § 145.219 makes the maintained record of directive compliance an auditable obligation rather than a courtesy.

Prerequisites & inputs

The engine needs two well-defined feeds. The first is a normalized directive feed: each AD or SB carries an identifier, an effectivity expressed as applicable part numbers plus serial-number ranges, an ATA reference, and a compliance deadline in calendar time or accumulated hours/cycles. The second is the fleet configuration snapshot — for every tail, the installed part numbers, their serials, and current airframe hours. Keeping the directive feed fresh is a separate concern handled upstream by Regulatory Change Tracking Pipelines; this page assumes today’s directive set is already retrieved and parsed, and focuses solely on the join.

The subject aircraft here is N512QX, carrying a mode-S transponder at ATA 34-52 with part number 011-03302-00, serial SN-84417. The directive under test targets a serial band that we must decide whether that unit falls inside.

Correlation at a glance

AD/SB applicability correlationAD / SBfeedEffectivitymatchFleetconfigCompliancestatus

Step-by-step implementation

1. Normalize part numbers before comparing. An AD written against a manufacturer’s dashed part number will never string-match a fleet record that stores it without dashes. Applicability that depends on cross-vendor equivalence should route through the canonical mapping described in Mapping Airbus vs Boeing Part-Number Schemes in Python so 011-03302-00 and 011033020 resolve to the same key.

2. Decide part-number intersection. A directive applies only if at least one installed part number is in its effectivity set. This is a cheap set membership test and the first gate.

3. Decide serial-range intersection. For matched part numbers, confirm the installed serial falls inside a declared range. Serials are frequently alphanumeric, so parse the numeric tail and compare inclusively against serial_low and serial_high.

4. Compute compliance status, not just applicability. An applicable directive is OPEN until a compliance record exists; if the deadline (calendar or hours) has passed with no record, it is OVERDUE. This tri-state — NOT_APPLICABLE, OPEN, COMPLIED / OVERDUE — is exactly what a Part-145 recordkeeping audit expects to see computed.

from __future__ import annotations

import logging
import re
from dataclasses import dataclass
from datetime import date
from enum import Enum
from typing import Optional

logging.basicConfig(level=logging.INFO, format="%(asctime)s | %(levelname)s | %(message)s")
logger = logging.getLogger("mro.adsb.correlator")


class ComplianceStatus(str, Enum):
    NOT_APPLICABLE = "NOT_APPLICABLE"
    OPEN = "OPEN"
    OVERDUE = "OVERDUE"
    COMPLIED = "COMPLIED"


@dataclass(frozen=True)
class Directive:
    ident: str
    ata_chapter: str
    applicable_pns: frozenset[str]
    serial_low: int
    serial_high: int
    due_date: date


@dataclass(frozen=True)
class InstalledComponent:
    registration: str
    part_number: str
    serial: str


def _serial_tail(serial: str) -> Optional[int]:
    m = re.search(r"(\d+)\D*$", serial)
    return int(m.group(1)) if m else None


def _canonical_pn(pn: str) -> str:
    return re.sub(r"[^A-Z0-9]", "", pn.upper())


def correlate(
    directive: Directive,
    component: InstalledComponent,
    complied_on: Optional[date],
    today: date,
) -> ComplianceStatus:
    """Compute AD/SB status for one component.

    FAA 14 CFR § 43.9 requires the record of maintenance to describe the
    work; AC 43-9C explains that AD compliance must be recorded with the
    directive number, method of compliance, and date.
    """
    installed = _canonical_pn(component.part_number)
    applicable = {_canonical_pn(p) for p in directive.applicable_pns}
    if installed not in applicable:
        return ComplianceStatus.NOT_APPLICABLE

    tail = _serial_tail(component.serial)
    if tail is None or not (directive.serial_low <= tail <= directive.serial_high):
        return ComplianceStatus.NOT_APPLICABLE

    if complied_on is not None:
        logger.info(
            "%s COMPLIED on %s for %s",
            directive.ident, complied_on.isoformat(), component.registration,
        )
        return ComplianceStatus.COMPLIED

    status = ComplianceStatus.OVERDUE if today > directive.due_date else ComplianceStatus.OPEN
    logger.warning(
        "%s %s for %s (ATA %s, due %s)",
        directive.ident, status.value, component.registration,
        directive.ata_chapter, directive.due_date.isoformat(),
    )
    return status

Worked example

Instantiate the directive AD-2026-11-07 against ATA 34-52, effective for part 011-03302-00 across serials 80000–89999, due 2026-09-30. The component on N512QX is serial SN-84417. Its numeric tail parses to 84417, which sits inside the band, and the canonical part number matches, so the directive is applicable. With no complied_on record and today’s date at 2026-07-16, the engine returns OPEN and logs the due date — the tail is exposed but still inside its window. Advance the clock past 2026-09-30 with the record still empty and the same call returns OVERDUE, the status that must trigger a grounding decision and a Part-145 finding if it is ever discovered by an auditor rather than the operator.

Swap in serial SN-90210 and the tail 90210 falls outside the band; the engine returns NOT_APPLICABLE without ever consulting the deadline. That short-circuit is deliberate: an inapplicable directive should generate zero noise in the compliance dashboard.

Edge cases & compliance gotchas

Serials that are not numeric-monotonic. Some OEMs reset serial counters per production block or embed a facility prefix. A raw numeric-tail comparison then produces false matches. When a program uses non-monotonic serials, correlation must key on explicit enumerated serial lists, not ranges.

Superseded directives. A new AD frequently supersedes an earlier one. Correlating against a flat feed marks both OPEN, double-counting the exposure. The upstream feed must carry supersession links so the engine suppresses the retired directive.

Hours-based deadlines masquerading as calendar. Many directives are due at accumulated flight hours, not a date. Comparing today > due_date for such a directive is simply wrong; the deadline field must be typed so hour-based thresholds compare against the tail’s current airframe hours instead.

Effectivity by installation, not just presence. A part removed last week but still listed in a stale snapshot yields a phantom applicability. Drive correlation from the current-configuration record required under § 43.11, never from historical installation events.

Method-of-compliance is part of the record, not an afterthought. A COMPLIED status is only defensible if the underlying record captures the directive number, the method of compliance, and the date, exactly as AC 43-9C describes. Store those three attributes with the complied_on date, because a green dashboard cell backed by no method-of-compliance evidence is precisely the gap an auditor probes first. Treat the compliance record and the computed status as two halves of the same obligation.

Computed correlation replaces “we think we tracked that” with a status every applicable directive can defend line by line. Feed the engine a fresh directive set and an honest configuration snapshot, and AD/SB compliance stops being a periodic scramble and becomes a queryable property of the fleet — which is precisely what § 145.219 recordkeeping was written to guarantee.