Summary
addRunway in lib/utils/flight_plan_utils.ts parses the R: flight-plan element with hard-coded offsets that assume the value is exactly 8 chars (xxx(yyy)). For any value of length 1–7 the function (a) skips the arrival runway entirely and (b) emits a wrong departure-runway string sliced out of whatever value.substring(0, 3) happens to land on. For length 1–3 single-runway values it happens to produce a correct departure, but for the common single-digit-runway shape 9L(27R) (7 chars) or 9(27R) (6 chars) the departure runway is set to '9L(' / '9(2' and the arrival side is silently dropped.
Real ACARS H1 FPN messages routinely carry runway pairings shorter than 8 chars (any runway whose number is a single digit — 1, 9, etc., or whose suffix is missing), so this is observable on production traffic, not just hand-rolled input.
Affected code
lib/utils/flight_plan_utils.ts:157-164
function addRunway(decodeResult: DecodeResult, value: string) {
// xxx(yyy) where xxx is the departure runway and yyy is the arrival runway
if (value.length === 8) {
ResultFormatter.arrivalRunway(decodeResult, value.substring(4, 7));
}
ResultFormatter.departureRunway(decodeResult, value.substring(0, 3));
}
The function is dispatched from FlightPlanUtils.processFlightPlan at line 51 whenever the H1 flight-plan stream contains an R:<value> element. The same path is exercised by arinc_702_helper.ts:134 for RP/RS envelopes — so this affects every H1 FPN/POS decode whose route segment carries a runway field.
Reproduction
// 1. Single-letter runway pair (7 chars) — observed in real-world traffic.
addRunway(r, '9L(27R)');
// departure_runway: '9L(' ← garbage
// arrival_runway: undefined
// 2. Single-digit dep / 3-char arrival (6 chars).
addRunway(r, '9(27R)');
// departure_runway: '9(2'
// arrival_runway: undefined
// 3. 3-char departure / single-digit arrival (7 chars).
addRunway(r, '27L(9R)');
// departure_runway: '27L' ← happens to be correct
// arrival_runway: undefined ← silently dropped
// 4. Single-letter departure only (1-3 chars) — also goes through addRunway
// via 'R:11O'-style elements like the one in Label_H1_FPN.test.ts:285:
addRunway(r, '11O');
// departure_runway: '11O' ✓
// arrival_runway: undefined ✓ (intentional — no arrival side)
// 5. Empty value (truncated message) — still emits an empty departure_runway entry.
addRunway(r, '');
// departure_runway: ''
// → ResultFormatter.departureRunway adds an items[] entry with value: ''
The existing test Label_H1_FPN.test.ts:15 carries R:27L(26O) (8 chars), which is the only shape the current code handles correctly, so the tests pass.
For the 8-char case the code also happens to do the right thing only because the ( lands at index 3 and the ) at index 7 — both single-character delimiters that the substrings naturally skip. Any deviation from that exact width breaks it.
Why it matters
Runway designators show up in route displays, ATC correlation, and any downstream lookup keyed on runway. Putting '9L(' (a non-runway string with an unbalanced paren) into raw.departure_runway corrupts those consumers, and silently dropping the arrival side hides half the information the flight crew filed. The truncated/empty-input case also pushes an empty items[] entry, which renders as a stray "Departure Runway: " line.
Suggested fix
Parse the format by structure rather than by fixed offset — value is "" or "()":
function addRunway(decodeResult: DecodeResult, value: string) {
if (!value) return;
const m = /^([^()]+)(?:\(([^()]+)\))?$/.exec(value);
if (!m) return;
const [, dep, arr] = m;
ResultFormatter.departureRunway(decodeResult, dep);
if (arr) {
ResultFormatter.arrivalRunway(decodeResult, arr);
}
}
That handles 3-char single-runway (11O, 27R), 6-char (9(27R)), 7-char in either direction (9L(27R), 27L(9R)), 8-char (27L(26O)), and rejects empty input. The early return on empty value also fixes the "stray empty Departure Runway entry" side effect.
Test coverage
There is no dedicated flight_plan_utils.test.ts. The only coverage of addRunway today is the indirect 8-char case via Label_H1_FPN.test.ts:285 (R:11O) — and that one only exercises the single-runway branch. A small set of targeted tests asserting:
R:9L(27R) → departure_runway === '9L' and arrival_runway === '27R'
R:9(27R) → departure_runway === '9' and arrival_runway === '27R'
R:27L(9R) → departure_runway === '27L' and arrival_runway === '9R'
R:27L(26O) (regression for current happy path)
R:11O (regression for the existing single-runway case)
R: (no items emitted)
…would lock the fix in.
Summary
addRunwayinlib/utils/flight_plan_utils.tsparses theR:flight-plan element with hard-coded offsets that assume the value is exactly 8 chars (xxx(yyy)). For any value of length 1–7 the function (a) skips the arrival runway entirely and (b) emits a wrong departure-runway string sliced out of whatevervalue.substring(0, 3)happens to land on. For length 1–3 single-runway values it happens to produce a correct departure, but for the common single-digit-runway shape9L(27R)(7 chars) or9(27R)(6 chars) the departure runway is set to'9L('/'9(2'and the arrival side is silently dropped.Real ACARS H1 FPN messages routinely carry runway pairings shorter than 8 chars (any runway whose number is a single digit —
1,9, etc., or whose suffix is missing), so this is observable on production traffic, not just hand-rolled input.Affected code
lib/utils/flight_plan_utils.ts:157-164The function is dispatched from
FlightPlanUtils.processFlightPlanat line 51 whenever the H1 flight-plan stream contains anR:<value>element. The same path is exercised byarinc_702_helper.ts:134forRP/RSenvelopes — so this affects every H1 FPN/POS decode whose route segment carries a runway field.Reproduction
The existing test
Label_H1_FPN.test.ts:15carriesR:27L(26O)(8 chars), which is the only shape the current code handles correctly, so the tests pass.For the 8-char case the code also happens to do the right thing only because the
(lands at index 3 and the)at index 7 — both single-character delimiters that the substrings naturally skip. Any deviation from that exact width breaks it.Why it matters
Runway designators show up in route displays, ATC correlation, and any downstream lookup keyed on runway. Putting
'9L('(a non-runway string with an unbalanced paren) intoraw.departure_runwaycorrupts those consumers, and silently dropping the arrival side hides half the information the flight crew filed. The truncated/empty-input case also pushes an emptyitems[]entry, which renders as a stray "Departure Runway: " line.Suggested fix
Parse the format by structure rather than by fixed offset —
valueis "" or "()":That handles 3-char single-runway (
11O,27R), 6-char (9(27R)), 7-char in either direction (9L(27R),27L(9R)), 8-char (27L(26O)), and rejects empty input. The earlyreturnon emptyvaluealso fixes the "stray empty Departure Runway entry" side effect.Test coverage
There is no dedicated
flight_plan_utils.test.ts. The only coverage ofaddRunwaytoday is the indirect 8-char case viaLabel_H1_FPN.test.ts:285(R:11O) — and that one only exercises the single-runway branch. A small set of targeted tests asserting:R:9L(27R)→departure_runway === '9L'andarrival_runway === '27R'R:9(27R)→departure_runway === '9'andarrival_runway === '27R'R:27L(9R)→departure_runway === '27L'andarrival_runway === '9R'R:27L(26O)(regression for current happy path)R:11O(regression for the existing single-runway case)R:(no items emitted)…would lock the fix in.