Part 117 Crew Legality Audit
Ticket CREW-1184, from Marisol Okafor-Lind, Manager, Crew Scheduling, Bluestem Air
Our FAA principal operations inspector (POI) wants an audit of last week's flying against Part 117, built from what actually happened, not the roster: every pilot's duty periods and every limit that was broken. And before Monday's first departures I need to know which of next week's trips we can't legally start. We did the same audit by hand for the week of 12 October and the inspector accepted it; it's in
data/reference_case/, so match its conventions. We want a function that we can run again every week.I'm in schedule meetings most of the week. Ask me what you need, but I can answer a few questions, not dozens.
Bluestem Air is a fictional US regional airline. It flies under 14 CFR Part 121, and its flightcrew members are subject to 14 CFR Part 117 (Flightcrew Member Duty and Rest Requirements).
Deliverable
Write solve(files) in solution.mjs. files maps each path under data/ to its text contents, including the files in data/reference_case/. The function returns the audit of the week in data/ and the check of next week's first trips, described under Output. It must ignore everything under data/reference_case/.
data/reference_case/ holds the week of 12 to 18 October 2026 with the same layout as data/, apart from files that we didn't have that week, plus approved_audit.json. If you pass your function the files under data/reference_case/ (with that prefix replaced by data/), it must return approved_audit.json. Anything that Part 117 leaves open, such as how report and release times are set, how crew scheduling's entries apply and how one duty period is told apart from the next, follows the approved audit.
The graders run solve on other weeks with the same layout, generated with different crews, schedules and irregular operations. Those weeks have no data/reference_case/ folder.
Working on this challenge
- Questions. Marisol knows what the audit covers, how Bluestem's crews are assigned and the arrangements behind the files, and much of that isn't written down in the folder. Ask her with
vibrometer ask "your question". You have 7 questions, she answers only what you ask, and a vague question gets a question back. - Follow-up. Run
vibrometer updatefrom time to time. Anything new from Marisol arrives in your folder with aCHANGE.md. The graders always test the latest requirements.
Input files
data/roster/: the roster extracts (fixed-width) and next week's pairing extract.data/ops/: the two operations exports of what was flown.data/crew/: the crew list, the duty history before the week, crew scheduling's entries and other crew files.data/ref/: stations and equipment.data/misc/: files that people sent along; not all of them matter.
Regulations
The eCFR text of 14 CFR Part 117 is the authority for every limit: Table A (117.11), Table B (117.13), reserve status (117.21), the cumulative limits (117.23) and rest (117.25), with the definitions in 117.3. Which parts the audit checks, and the conditions that held during the week, are Marisol's to tell you.
Output
{
dutyPeriods: [{
employee, // string, emp_no as in crew.csv, e.g. "41233"
start, // start of the duty period
end, // release from duty
fdpEnd, // end of the flight duty period, or null if the duty period has none
segments, // integer, flight segments in the sense of Table B
flightMinutes, // flight time in this duty period
fdpMinutes, // fdpEnd - start, or null
fdpLimitMinutes, // Table B limit, or null when there are no segments
flightTimeLimitMinutes, // Table A limit, or null when there are no segments
restBeforeMinutes, // minutes from the previous release (this week or earlier) to start, or null if there is none
}],
violations: [{
employee, // string, as above
rule, // "117.11" | "117.13" | "117.23(b)(1)" | "117.23(c)(1)" | "117.25(b)" | "117.25(e)"
dutyStart, // start of the flight duty period that was checked
limitMinutes, // the limit, or for 117.25(b) and 117.25(e) the minimum required
actualMinutes, // the measured value
excessMinutes, // actualMinutes - limitMinutes; for 117.25(b) and 117.25(e), limitMinutes - actualMinutes
}],
summary: {
pilotsChecked, // number of distinct pilots with at least one duty period in the week
dutyPeriods, // number of entries in dutyPeriods
byRule, // { "117.11": n, "117.13": n, ... } with every rule code, including zeros
},
nextWeek: [{
employee, // string, as above
pairing, // pairing id in next week's extract, e.g. "B45777"
report, // planned report for the pilot's first duty day next week
restBeforeMinutes, // minutes from the pilot's last actual release to report, or null if there is none
fdp168Minutes, // FDP minutes in the 168 hours that end at the planned end of that FDP, including it
flight672Minutes, // flight time in the 672 hours that end at the same moment, including the planned block time
issues, // the rule codes above that this duty day would break, sorted; [] when it is legal
}],
}
- All instants are UTC strings in the form
YYYY-MM-DDTHH:MMZ, for example2026-10-26T11:35Z. - All durations are integer minutes.
- Each violation is a separate entry, so one flight duty period can appear under several rules.
nextWeekcovers every pilot who is listed on a leg of next week's pairing extract, as a crew member or as a deadheading crew member. The pilot's first duty day is the earliest duty day (pairing and duty-day number) in which they are listed. Its planned times come from the scheduled times in the extract, with the same report and release conventions as the audit. Planned block time is STA minus STD of the legs that the pilot operates.fdp168Minutesandflight672Minutesarenullwhen that duty day has no flight segment for the pilot, and thenissuesis[].- Sort
dutyPeriodsbystart, thenemployee. SortviolationsbydutyStart, thenemployee, thenrule. SortnextWeekbyreport, thenemployee. Use plain string comparison throughout.
The solution has to be one file that imports nothing except node: built-ins. It gets no file system or network access, and every input arrives through files.