Skip to content

Wire up ADS submodule, codegen, runtime path aliases (no behavioral swap yet)#445

Open
kevinelliott wants to merge 7 commits into
masterfrom
dsl/adopt-v1
Open

Wire up ADS submodule, codegen, runtime path aliases (no behavioral swap yet)#445
kevinelliott wants to merge 7 commits into
masterfrom
dsl/adopt-v1

Conversation

@kevinelliott

@kevinelliott kevinelliott commented May 28, 2026

Copy link
Copy Markdown
Contributor

Summary

Integrates airframesio/acars-decoder as a git submodule and wires up the codegen toolchain, without replacing any hand-written plugin yet. Behavior is identical to master after this PR — every one of the 407 existing tests still passes, byte-for-byte.

This is the foundation PR for Stage 2 of the cross-language ACARS decoder unification. The actual swap (registering generated plugins in MessageDecoder) is deferred to a follow-up because the emitter design needs one more pass before swapping is safe (see "What's deferred" below).

What landed

  • Submodule: vendor/airframes-decoder pinned to init/ads-v1.
  • tsconfig path aliases:
    • @airframes/ads-runtime-tsvendor/airframes-decoder/runtimes/typescript/index.ts
    • @airframes/ads-runtime-ts/helpersvendor/airframes-decoder/runtimes/typescript/helpers.ts
    • @airframes/ads-runtime-ts/escape_hatchesvendor/airframes-decoder/runtimes/typescript/escape_hatches/index.ts
  • npm scripts:
    • npm run ads:codegen-build — installs + builds the codegen tool
    • npm run ads:generate — emits lib/plugins/generated/*.ts from spec YAML
    • npm run ads:check — fails if lib/plugins/generated/ is out-of-date
  • Generated tree committed: 68 plugins under lib/plugins/generated/. Committed (not gitignored) so contributors don't need the codegen toolchain to build/test, and reviewers see exactly what the runtime would use.
  • ESLint excludes: vendor/** and lib/plugins/generated/** (avoid linting third-party code and auto-generated source).
  • Jest excludes: /vendor/ (skip vendored submodule's own test files).
  • CI workflow: .github/workflows/ads-check.yml calls the central reusable codegen-check.yml from airframes-decoder. Single source of CI logic across all three language repos.

What's deferred (to a follow-up "Stage 2.5" PR)

The emitter currently double-bookkeeps raw fields: it auto-emits result.raw.<fieldName> = value from the spec's fields block AND then formatter calls write to raw under formatter-canonical keys (position, altitude, etc.). The original hand-written plugins only do the latter. Adopting the generated plugins as-is would produce extra raw entries that diverge from the existing test expectations.

The fix is in the emitter (vendor/airframes-decoder/codegen/src/emit-typescript.ts): track which raw keys the formatter writes and suppress the auto-emit for those keys. Once that lands in airframes-decoder and propagates here via submodule bump, the registration swap is safe.

The original lib/utils/*.ts helpers also stay in place until then; once the swap happens, they'll be removed in favor of the vendored copies.

Test plan

  • npm test — 407 / 416 tests pass (9 skipped, matches baseline)
  • npm run ads:codegen-build succeeds
  • npm run ads:generate produces clean output; no diff vs committed
  • tsc resolves the path aliases (verified by running the test suite, which goes through the same module resolution)
  • CI runs the new ads-check workflow on this PR (verifies generated tree stays in sync)

Related

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Chores

    • Added CI workflow to validate ADS-generated code, vendored ADS decoder submodule, and scripts to build/generate/check generated artifacts.
    • Updated tooling (lint, test, TypeScript) to ignore and map vendored/generated code.
  • Refactor

    • Decoder now integrates generated plugin implementations for many labels.
  • New Features

    • Numerous escape-hatch decoder plugins added to expand supported message formats.

…wap yet)

Sets the foundation for the cross-language Airframes Decoder Spec (ADS)
unification by integrating airframes-decoder into this repo as a git
submodule, without yet replacing any hand-written plugins. Behavior is
identical to master after this PR.

What this PR does:

- Add `vendor/airframes-decoder` as a git submodule (pinned to its
  init/ads-v1 branch).
- Add tsconfig path aliases (`@airframes/ads-runtime-ts` and friends)
  resolving into the submodule's runtimes/typescript/.
- Add npm scripts:
    npm run ads:codegen-build  — build the codegen tool
    npm run ads:generate       — emit lib/plugins/generated/*.ts from spec
    npm run ads:check          — fail if generated tree is out-of-date
- Generate the 68 plugins into lib/plugins/generated/ and commit them
  (avoids requiring the codegen toolchain in every contributor's env).
- Exclude vendor/ and lib/plugins/generated/ from ESLint.
- Exclude vendor/ from Jest test discovery.
- Add .github/workflows/ads-check.yml that calls the central reusable
  `codegen-check.yml` workflow (single source of CI logic across repos).

What this PR does NOT do (intentionally — separate Stage 2.5 PR):

- Does NOT register generated plugins in MessageDecoder.ts. The current
  emitter double-bookkeeps `raw` fields (field assignments + formatter
  writes), which would diverge from existing test expectations. Resolving
  this needs an emitter design pass (track which raw keys the formatter
  owns, suppress auto-emit in those cases) before a behavioral swap
  is safe.
- Does NOT remove the original lib/utils/*.ts helpers yet. The runtime
  is duplicated between this repo and the submodule until Stage 2.5
  swaps imports.

Verification:
- All 407 existing tests pass.
- `npm run ads:generate` produces 68 .ts files with no diff vs committed.
- Generated files compile against `@airframes/ads-runtime-ts` path
  aliases (resolution verified via tsc paths).

See airframesio/acars-decoder#1 for the central spec, codegen, runtimes,
docs, and 288-sample corpus this submodule references.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings May 28, 2026 06:26
@coderabbitai

coderabbitai Bot commented May 28, 2026

Copy link
Copy Markdown

Review Change Stack

Walkthrough

Adds a vendored ADS decoder submodule, configures TypeScript path mappings and tooling ignores/mappings, adds npm codegen scripts and a CI check, swaps MessageDecoder to use generated plugin constructors, and adds many escape-hatch decoder implementations with a barrel export.

Changes

ADS Decoder Submodule Integration

Layer / File(s) Summary
Submodule Registration and Update
.gitmodules, vendor/airframes-decoder
Git submodule vendor/airframes-decoder is registered and its commit pointer updated.
TypeScript Configuration for Vendored Code
tsconfig.json
Enables moduleResolution: "node", sets baseUrl, adds paths mappings for @airframes/ads-runtime-ts/*, and adds vendor to exclude.
Linter and Test Configuration
eslint.config.mts, jest.config.ts
ESLint ignores vendor/** and lib/plugins/generated/**; Jest maps @airframes/ads-runtime-ts imports to vendored runtime files and ignores node_modules/ and vendor/ for test discovery.
Code Generation Scripts
package.json
Adds ads:codegen-build, ads:generate, and ads:check npm scripts to build the vendored codegen tool, generate plugins into lib/plugins/generated, and verify generated artifacts are committed.
Decoder Plugin Generated Wiring
lib/MessageDecoder.ts, lib/plugins/escape_hatches/index.ts
Replaces prior official plugin constructors with ADS-generated constructors and adds a barrel export for escape-hatch implementations.
Escape-hatch Implementations
lib/plugins/escape_hatches/*
Adds many new label-specific escape-hatch decoder/formatter modules (e.g., ARINC_702, CBand, Label_10_, Label_12_, Label_16_, Label_20_, Label_21_POS, Label_22_, Label_24_Slash, Label_44_ and many H1/H2 variants) implementing parsing and ResultFormatter usage.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Suggested reviewers

  • taclane

🐰 A rabbit hopped in to mend the code,
Vendored wings and scripts bestowed,
Paths aligned and plugins spun,
Escape hatches bloom in sun,
Tests and checks — our garden grows.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch dsl/adopt-v1

Comment on lines +11 to +16
uses: airframesio/acars-decoder/.github/workflows/codegen-check.yml@init/ads-v1
with:
language: ts
generated-path: lib/plugins/generated
spec-path: vendor/airframes-decoder/spec
codegen-path: vendor/airframes-decoder/codegen

import { DecoderPlugin } from "@airframes/ads-runtime-ts";
import type { DecodeResult, Message, Options } from "@airframes/ads-runtime-ts";
import { ResultFormatter } from "@airframes/ads-runtime-ts";
import { DecoderPlugin } from "@airframes/ads-runtime-ts";
import type { DecodeResult, Message, Options } from "@airframes/ads-runtime-ts";
import { ResultFormatter } from "@airframes/ads-runtime-ts";
import * as helpers from "@airframes/ads-runtime-ts/helpers";

import { DecoderPlugin } from "@airframes/ads-runtime-ts";
import type { DecodeResult, Message, Options } from "@airframes/ads-runtime-ts";
import { ResultFormatter } from "@airframes/ads-runtime-ts";
import { DecoderPlugin } from "@airframes/ads-runtime-ts";
import type { DecodeResult, Message, Options } from "@airframes/ads-runtime-ts";
import { ResultFormatter } from "@airframes/ads-runtime-ts";
import * as helpers from "@airframes/ads-runtime-ts/helpers";

import { DecoderPlugin } from "@airframes/ads-runtime-ts";
import type { DecodeResult, Message, Options } from "@airframes/ads-runtime-ts";
import { ResultFormatter } from "@airframes/ads-runtime-ts";
import { DecoderPlugin } from "@airframes/ads-runtime-ts";
import type { DecodeResult, Message, Options } from "@airframes/ads-runtime-ts";
import { ResultFormatter } from "@airframes/ads-runtime-ts";
import * as helpers from "@airframes/ads-runtime-ts/helpers";
Comment thread lib/plugins/generated/Label_10_POS.ts Fixed
Comment thread lib/plugins/generated/Label_10_Slash.ts Fixed
Comment thread lib/plugins/generated/Label_10_Slash.ts Fixed
Comment thread lib/plugins/generated/Label_12_N_Space.ts Fixed

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: c3d7008546

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

import type { DecodeResult, Message, Options } from "@airframes/ads-runtime-ts";
import { ResultFormatter } from "@airframes/ads-runtime-ts";
import * as helpers from "@airframes/ads-runtime-ts/helpers";
import * as hatches from "../escape_hatches";

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Import the generated escape hatches from an existing module

When any generated plugin is type-checked or imported, this relative import resolves to lib/plugins/escape_hatches, but this commit does not add that module (a repo search for escape_hatches only finds these generated imports). The new generated tree is included by tsconfig.json, so consumers running a project type-check or later wiring these plugins into the decoder will hit a missing-module error before the generated plugins can be used.

Useful? React with 👍 / 👎.

Comment thread package.json
"test": "jest",
"ads:codegen-build": "cd vendor/airframes-decoder/codegen && npm install && npm run build",
"ads:generate": "node vendor/airframes-decoder/codegen/dist/cli.js generate --target ts --spec vendor/airframes-decoder/spec --out lib/plugins/generated",
"ads:check": "git diff --exit-code -- lib/plugins/generated || (echo 'lib/plugins/generated is out of date. Run npm run ads:generate and commit.' && exit 1)"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Run generation before diffing in ads:check

In a clean checkout where the ADS spec/submodule was changed but lib/plugins/generated was not regenerated, this command only checks for an existing uncommitted diff and exits successfully. That means npm run ads:check can pass with stale committed generated files, contrary to the script's stated purpose; it needs to run the generator (or invoke a check mode that does) before git diff --exit-code.

Useful? React with 👍 / 👎.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR lays the groundwork for adopting the shared “ADS” decoder spec/codegen by adding a vendored airframesio/acars-decoder submodule, wiring TypeScript path aliases to the vendored TS runtime, and committing the generated plugin sources so builds/tests don’t require running codegen.

Changes:

  • Add vendor/airframes-decoder as a git submodule and introduce TS path aliases for the ADS TS runtime.
  • Add npm scripts to build/run/check ADS codegen output and commit lib/plugins/generated/* to the repo.
  • Update ESLint/Jest config to ignore vendored and generated sources, and add a CI workflow to verify generated output is up-to-date.

Reviewed changes

Copilot reviewed 5 out of 75 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
tsconfig.json Adds baseUrl/paths for ADS runtime and excludes vendor from compilation inputs.
package.json Adds ADS codegen build/generate/check scripts.
jest.config.ts Ignores tests under vendor/.
eslint.config.mts Ignores vendor/** and lib/plugins/generated/**.
.gitmodules Adds vendor/airframes-decoder submodule reference.
.github/workflows/ads-check.yml Adds CI job to verify generated tree matches the spec.
lib/plugins/generated/ARINC_702.ts Adds generated plugin wrapper.
lib/plugins/generated/CBand.ts Adds generated plugin wrapper.
lib/plugins/generated/Label_10_LDR.ts Adds generated plugin wrapper.
lib/plugins/generated/Label_10_POS.ts Adds generated plugin implementation.
lib/plugins/generated/Label_10_Slash.ts Adds generated plugin wrapper.
lib/plugins/generated/Label_12_N_Space.ts Adds generated plugin wrapper.
lib/plugins/generated/Label_12_POS.ts Adds generated plugin wrapper.
lib/plugins/generated/Label_13Through18_Slash.ts Adds generated plugin wrapper.
lib/plugins/generated/Label_15.ts Adds generated plugin wrapper.
lib/plugins/generated/Label_15_FST.ts Adds generated plugin wrapper.
lib/plugins/generated/Label_16_AUTPOS.ts Adds generated plugin wrapper.
lib/plugins/generated/Label_16_Honeywell.ts Adds generated plugin wrapper.
lib/plugins/generated/Label_16_N_Space.ts Adds generated plugin wrapper.
lib/plugins/generated/Label_16_POSA1.ts Adds generated plugin wrapper.
lib/plugins/generated/Label_16_TOD.ts Adds generated plugin wrapper.
lib/plugins/generated/Label_1L_070.ts Adds generated plugin wrapper.
lib/plugins/generated/Label_1L_3Line.ts Adds generated plugin wrapper.
lib/plugins/generated/Label_1L_660.ts Adds generated plugin wrapper.
lib/plugins/generated/Label_1L_Slash.ts Adds generated plugin wrapper.
lib/plugins/generated/Label_1M_Slash.ts Adds generated plugin wrapper.
lib/plugins/generated/Label_20_CFB01.ts Adds generated plugin wrapper.
lib/plugins/generated/Label_20_POS.ts Adds generated plugin wrapper.
lib/plugins/generated/Label_21_POS.ts Adds generated plugin wrapper.
lib/plugins/generated/Label_22_OFF.ts Adds generated plugin wrapper.
lib/plugins/generated/Label_22_POS.ts Adds generated plugin wrapper.
lib/plugins/generated/Label_24_Slash.ts Adds generated plugin wrapper.
lib/plugins/generated/Label_2P_FM3.ts Adds generated plugin wrapper.
lib/plugins/generated/Label_2P_FM4.ts Adds generated plugin wrapper.
lib/plugins/generated/Label_2P_FM5.ts Adds generated plugin wrapper.
lib/plugins/generated/Label_30_Slash_EA.ts Adds generated plugin wrapper.
lib/plugins/generated/Label_44_Slash.ts Adds generated plugin wrapper.
lib/plugins/generated/Label_44_POS.ts Adds generated plugin implementation.
lib/plugins/generated/Label_44_ON.ts Adds generated plugin implementation.
lib/plugins/generated/Label_44_OFF.ts Adds generated plugin implementation.
lib/plugins/generated/Label_44_IN.ts Adds generated plugin implementation.
lib/plugins/generated/Label_44_ETA.ts Adds generated plugin implementation.
lib/plugins/generated/Label_4A.ts Adds generated plugin implementation.
lib/plugins/generated/Label_4A_01.ts Adds generated plugin wrapper.
lib/plugins/generated/Label_4A_Slash_01.ts Adds generated plugin wrapper.
lib/plugins/generated/Label_4A_DOOR.ts Adds generated plugin wrapper.
lib/plugins/generated/Label_4A_DIS.ts Adds generated plugin wrapper.
lib/plugins/generated/Label_4N.ts Adds generated plugin wrapper.
lib/plugins/generated/Label_4T_AGFSR.ts Adds generated plugin wrapper.
lib/plugins/generated/Label_4T_ETA.ts Adds generated plugin wrapper.
lib/plugins/generated/Label_58.ts Adds generated plugin wrapper.
lib/plugins/generated/Label_5Z_Slash.ts Adds generated plugin wrapper.
lib/plugins/generated/Label_80.ts Adds generated plugin wrapper.
lib/plugins/generated/Label_83.ts Adds generated plugin wrapper.
lib/plugins/generated/Label_8E.ts Adds generated plugin wrapper.
lib/plugins/generated/Label_B6_Forwardslash.ts Adds generated plugin wrapper.
lib/plugins/generated/Label_ColonComma.ts Adds generated plugin wrapper.
lib/plugins/generated/Label_H1_ATIS.ts Adds generated plugin wrapper.
lib/plugins/generated/Label_H1_EZF.ts Adds generated plugin wrapper.
lib/plugins/generated/Label_H1_FLR.ts Adds generated plugin wrapper.
lib/plugins/generated/Label_H1_M_POS.ts Adds generated plugin wrapper.
lib/plugins/generated/Label_H1_OFP.ts Adds generated plugin wrapper.
lib/plugins/generated/Label_H1_OHMA.ts Adds generated plugin implementation.
lib/plugins/generated/Label_H1_Paren.ts Adds generated plugin wrapper.
lib/plugins/generated/Label_H1_StarPOS.ts Adds generated plugin wrapper.
lib/plugins/generated/Label_H1_WRN.ts Adds generated plugin wrapper.
lib/plugins/generated/Label_H2_02E.ts Adds generated plugin wrapper.
lib/plugins/generated/Label_HX.ts Adds generated plugin wrapper.
lib/plugins/generated/Label_MA.ts Adds generated plugin wrapper.
lib/plugins/generated/Label_QP.ts Adds generated plugin wrapper.
lib/plugins/generated/Label_QQ.ts Adds generated plugin wrapper.
lib/plugins/generated/Label_QR.ts Adds generated plugin wrapper.
lib/plugins/generated/Label_QS.ts Adds generated plugin wrapper.
lib/plugins/generated/Label_SQ.ts Adds generated plugin wrapper.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread package.json
Comment on lines +19 to +21
"ads:codegen-build": "cd vendor/airframes-decoder/codegen && npm install && npm run build",
"ads:generate": "node vendor/airframes-decoder/codegen/dist/cli.js generate --target ts --spec vendor/airframes-decoder/spec --out lib/plugins/generated",
"ads:check": "git diff --exit-code -- lib/plugins/generated || (echo 'lib/plugins/generated is out of date. Run npm run ads:generate and commit.' && exit 1)"
Comment on lines +9 to +12
jobs:
ads-generated-up-to-date:
uses: airframesio/acars-decoder/.github/workflows/codegen-check.yml@init/ads-v1
with:

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🧹 Nitpick comments (1)
package.json (1)

19-19: ⚡ Quick win

Prefer npm ci for reproducible codegen builds.

Using npm install may produce non-deterministic builds if package-lock.json is out of sync. Since this installs dependencies for the vendored codegen tool, npm ci will enforce the lockfile and fail fast if it's stale.

♻️ Proposed fix
-    "ads:codegen-build": "cd vendor/airframes-decoder/codegen && npm install && npm run build",
+    "ads:codegen-build": "cd vendor/airframes-decoder/codegen && npm ci && npm run build",
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@package.json` at line 19, Replace the npm install call in the npm script
"ads:codegen-build" (which runs in vendor/airframes-decoder/codegen) with npm ci
so the vendored codegen build uses the lockfile for reproducible installs and
fails fast if the lockfile is stale; update the script invocation to run npm ci
&& npm run build instead of npm install && npm run build.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In @.github/workflows/ads-check.yml:
- Around line 10-16: The workflow job ads-generated-up-to-date is missing an
explicit permissions block and currently inherits default GITHUB_TOKEN rights;
add a minimal permissions section to the workflow (at the top-level of the job
or workflow) granting only what the reusable workflow needs—e.g., permissions:
contents: read—so the codegen-check invocation (uses:
airframesio/acars-decoder/.github/workflows/codegen-check.yml@init/ads-v1) runs
with least privilege.
- Line 11: Update the reusable workflow reference used by the
ads-generated-up-to-date job (the line using
airframesio/acars-decoder/.github/workflows/codegen-check.yml@init/ads-v1) to
pin it to the specific commit SHA provided in the review and replace the branch
ref with that SHA; also add an explicit permissions: block (either at workflow
top or inside the ads-generated-up-to-date job) that scopes GITHUB_TOKEN to the
minimum required permissions for the codegen check so the workflow no longer
relies on default, broad token scopes.

In `@jest.config.ts`:
- Around line 162-165: The string literals in the Jest config's
testPathIgnorePatterns array use double quotes which conflicts with the
project's Prettier single-quote rule; update the array entries (the values
within testPathIgnorePatterns) to use single quotes (e.g., '/node_modules/' and
'/vendor/') so formatting/linting passes, leaving the rest of the
testPathIgnorePatterns property and its comments unchanged.

In `@tsconfig.json`:
- Line 5: tsconfig.json's compilerOptions.paths mapping for the alias
"`@airframes/ads-runtime-ts`" points to non-existent targets; update the paths
entry so each key under "compilerOptions.paths" for "`@airframes/ads-runtime-ts`"
(and any subpaths like "`@airframes/ads-runtime-ts/`*") points to the actual
TypeScript source file locations (for example the real index, helpers, and
escape_hatches files in the repo) instead of
"vendor/airframes-decoder/runtimes/typescript/index.ts",
"vendor/airframes-decoder/runtimes/typescript/helpers.ts", and
"vendor/airframes-decoder/runtimes/typescript/escape_hatches/index.ts"; locate
and replace those target strings in the tsconfig.json paths section so
TypeScript can resolve imports of `@airframes/ads-runtime-ts` correctly.

---

Nitpick comments:
In `@package.json`:
- Line 19: Replace the npm install call in the npm script "ads:codegen-build"
(which runs in vendor/airframes-decoder/codegen) with npm ci so the vendored
codegen build uses the lockfile for reproducible installs and fails fast if the
lockfile is stale; update the script invocation to run npm ci && npm run build
instead of npm install && npm run build.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 7f552c64-d4d6-4287-bee0-24d3f64f2f3a

📥 Commits

Reviewing files that changed from the base of the PR and between 1748860 and c3d7008.

⛔ Files ignored due to path filters (68)
  • lib/plugins/generated/ARINC_702.ts is excluded by !**/generated/**
  • lib/plugins/generated/CBand.ts is excluded by !**/generated/**
  • lib/plugins/generated/Label_10_LDR.ts is excluded by !**/generated/**
  • lib/plugins/generated/Label_10_POS.ts is excluded by !**/generated/**
  • lib/plugins/generated/Label_10_Slash.ts is excluded by !**/generated/**
  • lib/plugins/generated/Label_12_N_Space.ts is excluded by !**/generated/**
  • lib/plugins/generated/Label_12_POS.ts is excluded by !**/generated/**
  • lib/plugins/generated/Label_13Through18_Slash.ts is excluded by !**/generated/**
  • lib/plugins/generated/Label_15.ts is excluded by !**/generated/**
  • lib/plugins/generated/Label_15_FST.ts is excluded by !**/generated/**
  • lib/plugins/generated/Label_16_AUTPOS.ts is excluded by !**/generated/**
  • lib/plugins/generated/Label_16_Honeywell.ts is excluded by !**/generated/**
  • lib/plugins/generated/Label_16_N_Space.ts is excluded by !**/generated/**
  • lib/plugins/generated/Label_16_POSA1.ts is excluded by !**/generated/**
  • lib/plugins/generated/Label_16_TOD.ts is excluded by !**/generated/**
  • lib/plugins/generated/Label_1L_070.ts is excluded by !**/generated/**
  • lib/plugins/generated/Label_1L_3Line.ts is excluded by !**/generated/**
  • lib/plugins/generated/Label_1L_660.ts is excluded by !**/generated/**
  • lib/plugins/generated/Label_1L_Slash.ts is excluded by !**/generated/**
  • lib/plugins/generated/Label_1M_Slash.ts is excluded by !**/generated/**
  • lib/plugins/generated/Label_20_CFB01.ts is excluded by !**/generated/**
  • lib/plugins/generated/Label_20_POS.ts is excluded by !**/generated/**
  • lib/plugins/generated/Label_21_POS.ts is excluded by !**/generated/**
  • lib/plugins/generated/Label_22_OFF.ts is excluded by !**/generated/**
  • lib/plugins/generated/Label_22_POS.ts is excluded by !**/generated/**
  • lib/plugins/generated/Label_24_Slash.ts is excluded by !**/generated/**
  • lib/plugins/generated/Label_2P_FM3.ts is excluded by !**/generated/**
  • lib/plugins/generated/Label_2P_FM4.ts is excluded by !**/generated/**
  • lib/plugins/generated/Label_2P_FM5.ts is excluded by !**/generated/**
  • lib/plugins/generated/Label_30_Slash_EA.ts is excluded by !**/generated/**
  • lib/plugins/generated/Label_44_ETA.ts is excluded by !**/generated/**
  • lib/plugins/generated/Label_44_IN.ts is excluded by !**/generated/**
  • lib/plugins/generated/Label_44_OFF.ts is excluded by !**/generated/**
  • lib/plugins/generated/Label_44_ON.ts is excluded by !**/generated/**
  • lib/plugins/generated/Label_44_POS.ts is excluded by !**/generated/**
  • lib/plugins/generated/Label_44_Slash.ts is excluded by !**/generated/**
  • lib/plugins/generated/Label_4A.ts is excluded by !**/generated/**
  • lib/plugins/generated/Label_4A_01.ts is excluded by !**/generated/**
  • lib/plugins/generated/Label_4A_DIS.ts is excluded by !**/generated/**
  • lib/plugins/generated/Label_4A_DOOR.ts is excluded by !**/generated/**
  • lib/plugins/generated/Label_4A_Slash_01.ts is excluded by !**/generated/**
  • lib/plugins/generated/Label_4N.ts is excluded by !**/generated/**
  • lib/plugins/generated/Label_4T_AGFSR.ts is excluded by !**/generated/**
  • lib/plugins/generated/Label_4T_ETA.ts is excluded by !**/generated/**
  • lib/plugins/generated/Label_58.ts is excluded by !**/generated/**
  • lib/plugins/generated/Label_5Z_Slash.ts is excluded by !**/generated/**
  • lib/plugins/generated/Label_80.ts is excluded by !**/generated/**
  • lib/plugins/generated/Label_83.ts is excluded by !**/generated/**
  • lib/plugins/generated/Label_8E.ts is excluded by !**/generated/**
  • lib/plugins/generated/Label_B6_Forwardslash.ts is excluded by !**/generated/**
  • lib/plugins/generated/Label_ColonComma.ts is excluded by !**/generated/**
  • lib/plugins/generated/Label_H1_ATIS.ts is excluded by !**/generated/**
  • lib/plugins/generated/Label_H1_EZF.ts is excluded by !**/generated/**
  • lib/plugins/generated/Label_H1_FLR.ts is excluded by !**/generated/**
  • lib/plugins/generated/Label_H1_M_POS.ts is excluded by !**/generated/**
  • lib/plugins/generated/Label_H1_OFP.ts is excluded by !**/generated/**
  • lib/plugins/generated/Label_H1_OHMA.ts is excluded by !**/generated/**
  • lib/plugins/generated/Label_H1_Paren.ts is excluded by !**/generated/**
  • lib/plugins/generated/Label_H1_StarPOS.ts is excluded by !**/generated/**
  • lib/plugins/generated/Label_H1_WRN.ts is excluded by !**/generated/**
  • lib/plugins/generated/Label_H2_02E.ts is excluded by !**/generated/**
  • lib/plugins/generated/Label_HX.ts is excluded by !**/generated/**
  • lib/plugins/generated/Label_MA.ts is excluded by !**/generated/**
  • lib/plugins/generated/Label_QP.ts is excluded by !**/generated/**
  • lib/plugins/generated/Label_QQ.ts is excluded by !**/generated/**
  • lib/plugins/generated/Label_QR.ts is excluded by !**/generated/**
  • lib/plugins/generated/Label_QS.ts is excluded by !**/generated/**
  • lib/plugins/generated/Label_SQ.ts is excluded by !**/generated/**
📒 Files selected for processing (7)
  • .github/workflows/ads-check.yml
  • .gitmodules
  • eslint.config.mts
  • jest.config.ts
  • package.json
  • tsconfig.json
  • vendor/airframes-decoder

Comment on lines +10 to +16
ads-generated-up-to-date:
uses: airframesio/acars-decoder/.github/workflows/codegen-check.yml@init/ads-v1
with:
language: ts
generated-path: lib/plugins/generated
spec-path: vendor/airframes-decoder/spec
codegen-path: vendor/airframes-decoder/codegen

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Add an explicit permissions block.

The workflow inherits default GITHUB_TOKEN permissions, which may be broader than required. For a validation-only workflow that runs codegen checks, read-only access should suffice.

🛡️ Proposed fix to add minimal permissions
 jobs:
   ads-generated-up-to-date:
+    permissions:
+      contents: read
     uses: airframesio/acars-decoder/.github/workflows/codegen-check.yml@init/ads-v1

Adjust permissions based on what the reusable workflow actually requires. If it only validates generated code, contents: read should be sufficient.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
ads-generated-up-to-date:
uses: airframesio/acars-decoder/.github/workflows/codegen-check.yml@init/ads-v1
with:
language: ts
generated-path: lib/plugins/generated
spec-path: vendor/airframes-decoder/spec
codegen-path: vendor/airframes-decoder/codegen
ads-generated-up-to-date:
permissions:
contents: read
uses: airframesio/acars-decoder/.github/workflows/codegen-check.yml@init/ads-v1
with:
language: ts
generated-path: lib/plugins/generated
spec-path: vendor/airframes-decoder/spec
codegen-path: vendor/airframes-decoder/codegen
🧰 Tools
🪛 GitHub Check: CodeQL

[warning] 11-16: Workflow does not contain permissions
Actions job or workflow does not limit the permissions of the GITHUB_TOKEN. Consider setting an explicit permissions block, using the following as a minimal starting point: {{}}

🪛 zizmor (1.25.2)

[warning] 10-17: overly broad permissions (excessive-permissions): default permissions used due to no permissions: block

(excessive-permissions)


[error] 11-11: unpinned action reference (unpinned-uses): action is not pinned to a hash (required by blanket policy)

(unpinned-uses)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/ads-check.yml around lines 10 - 16, The workflow job
ads-generated-up-to-date is missing an explicit permissions block and currently
inherits default GITHUB_TOKEN rights; add a minimal permissions section to the
workflow (at the top-level of the job or workflow) granting only what the
reusable workflow needs—e.g., permissions: contents: read—so the codegen-check
invocation (uses:
airframesio/acars-decoder/.github/workflows/codegen-check.yml@init/ads-v1) runs
with least privilege.


jobs:
ads-generated-up-to-date:
uses: airframesio/acars-decoder/.github/workflows/codegen-check.yml@init/ads-v1

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Get the commit SHA for the init/ads-v1 branch
gh api repos/airframesio/acars-decoder/git/ref/heads/init/ads-v1 --jq '.object.sha'

Repository: airframesio/acars-decoder-typescript

Length of output: 121


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Show the workflow content around the referenced `uses:` line and top-level permissions
echo "=== File head (with line numbers) ==="
nl -ba .github/workflows/ads-check.yml | head -n 120

echo
echo "=== Grep for permissions/use blocks ==="
rg -n "^[[:space:]]*permissions:|^[[:space:]]*uses:" .github/workflows/ads-check.yml || true

Repository: airframesio/acars-decoder-typescript

Length of output: 179


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== Confirm file exists ==="
ls -la .github/workflows/ads-check.yml

echo
echo "=== File head (with line numbers) ==="
cat -n .github/workflows/ads-check.yml | head -n 120

echo
echo "=== Grep for permissions/use blocks ==="
rg -n "^[[:space:]]*permissions:|^[[:space:]]*uses:" .github/workflows/ads-check.yml || true

echo
echo "=== Show lines around uses: (approx) ==="
# Show a reasonable window around the reported uses line
sed -n '1,80p' .github/workflows/ads-check.yml

Repository: airframesio/acars-decoder-typescript

Length of output: 1299


🏁 Script executed:

#!/bin/bash
set -euo pipefail
echo "=== Full file ==="
cat -n .github/workflows/ads-check.yml

Repository: airframesio/acars-decoder-typescript

Length of output: 605


Pin the reusable workflow to a commit SHA.

.github/workflows/ads-check.yml (job ads-generated-up-to-date) currently calls codegen-check.yml via a branch ref (@init/ads-v1), which is a supply-chain risk. Pin it to the current commit for that ref:

uses: airframesio/acars-decoder/.github/workflows/codegen-check.yml@6da198f085a6ae56eb0291fe938c239aef8faa93

Also add an explicit permissions: block (workflow- or job-level) to scope GITHUB_TOKEN to the minimum required; this workflow currently has no permissions: configured.

🧰 Tools
🪛 GitHub Check: CodeQL

[warning] 11-16: Workflow does not contain permissions
Actions job or workflow does not limit the permissions of the GITHUB_TOKEN. Consider setting an explicit permissions block, using the following as a minimal starting point: {{}}

🪛 zizmor (1.25.2)

[error] 11-11: unpinned action reference (unpinned-uses): action is not pinned to a hash (required by blanket policy)

(unpinned-uses)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/ads-check.yml at line 11, Update the reusable workflow
reference used by the ads-generated-up-to-date job (the line using
airframesio/acars-decoder/.github/workflows/codegen-check.yml@init/ads-v1) to
pin it to the specific commit SHA provided in the review and replace the branch
ref with that SHA; also add an explicit permissions: block (either at workflow
top or inside the ads-generated-up-to-date job) that scopes GITHUB_TOKEN to the
minimum required permissions for the codegen check so the workflow no longer
relies on default, broad token scopes.

Comment thread jest.config.ts
Comment on lines +162 to +165
testPathIgnorePatterns: [
"/node_modules/",
"/vendor/", // skip vendored airframes-decoder submodule's tests
],

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Fix quote style to match Prettier configuration.

The string literals use double quotes, but Prettier is configured to enforce single quotes. This will cause the build to fail or require a formatting pass.

🎨 Proposed fix
  testPathIgnorePatterns: [
-    "/node_modules/",
-    "/vendor/", // skip vendored airframes-decoder submodule's tests
+    '/node_modules/',
+    '/vendor/', // skip vendored airframes-decoder submodule's tests
  ],
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
testPathIgnorePatterns: [
"/node_modules/",
"/vendor/", // skip vendored airframes-decoder submodule's tests
],
testPathIgnorePatterns: [
'/node_modules/',
'/vendor/', // skip vendored airframes-decoder submodule's tests
],
🧰 Tools
🪛 ESLint

[error] 163-163: Replace "/node_modules/" with '/node_modules/'

(prettier/prettier)


[error] 164-164: Replace "/vendor/" with '/vendor/'

(prettier/prettier)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@jest.config.ts` around lines 162 - 165, The string literals in the Jest
config's testPathIgnorePatterns array use double quotes which conflicts with the
project's Prettier single-quote rule; update the array entries (the values
within testPathIgnorePatterns) to use single quotes (e.g., '/node_modules/' and
'/vendor/') so formatting/linting passes, leaving the rest of the
testPathIgnorePatterns property and its comments unchanged.

Comment thread tsconfig.json
"compilerOptions": {
"target": "esNext",
"module": "commonjs",
"moduleResolution": "node",

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: Verify that all TypeScript path mappings point to existing files

echo "Checking `@airframes/ads-runtime-ts` path mappings..."

files=(
  "vendor/airframes-decoder/runtimes/typescript/index.ts"
  "vendor/airframes-decoder/runtimes/typescript/helpers.ts"
  "vendor/airframes-decoder/runtimes/typescript/escape_hatches/index.ts"
)

missing=0
for file in "${files[@]}"; do
  if [[ -f "$file" ]]; then
    echo "$file"
  else
    echo "✗ MISSING: $file"
    missing=$((missing + 1))
  fi
done

if [[ $missing -eq 0 ]]; then
  echo "All path mappings verified."
else
  echo "ERROR: $missing file(s) missing."
  exit 1
fi

Repository: airframesio/acars-decoder-typescript

Length of output: 370


Fix tsconfig.json compilerOptions.paths targets for @airframes/ads-runtime-ts — the mappings point to missing files (vendor/airframes-decoder/runtimes/typescript/index.ts, helpers.ts, and escape_hatches/index.ts), so TypeScript will fail to resolve these aliases anywhere they’re imported.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tsconfig.json` at line 5, tsconfig.json's compilerOptions.paths mapping for
the alias "`@airframes/ads-runtime-ts`" points to non-existent targets; update the
paths entry so each key under "compilerOptions.paths" for
"`@airframes/ads-runtime-ts`" (and any subpaths like
"`@airframes/ads-runtime-ts/`*") points to the actual TypeScript source file
locations (for example the real index, helpers, and escape_hatches files in the
repo) instead of "vendor/airframes-decoder/runtimes/typescript/index.ts",
"vendor/airframes-decoder/runtimes/typescript/helpers.ts", and
"vendor/airframes-decoder/runtimes/typescript/escape_hatches/index.ts"; locate
and replace those target strings in the tsconfig.json paths section so
TypeScript can resolve imports of `@airframes/ads-runtime-ts` correctly.

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for wiring up the ADS submodule/codegen path. I think this needs another pass before merge because the generated tree is not currently reproducible or type-safe once exercised.

What I ran:

  • npm ci failed with an existing @typescript-eslint peer-resolution conflict, so I installed with npm ci --legacy-peer-deps to continue review.
  • npm run build passed.
  • npm test -- --runInBand passed.
  • npm run ads:codegen-build passed.
  • npm run ads:generate rewrote every generated file header from the author’s local absolute path to the checkout path, and npm run ads:check then failed.
  • npx tsc --noEmit fails; many failures are pre-existing test/source strictness issues, but the new lib/plugins/generated/** files also add errors such as missing ../escape_hatches, invalid ResultFormatter.fuel(...), and block-scoped variables used out of scope.

The current package build passes mostly because the generated files are not imported/exported yet. Since this PR is establishing the generated source and tooling foundation, I’d fix these now so the next PR does not inherit a generated tree that cannot be regenerated or imported cleanly.

Open in Web View Automation 

Sent by Cursor Automation: acars-decoder-typescript: PR Review

@@ -0,0 +1,40 @@
// AUTO-GENERATED from /Users/kevin/Cloud/Dropbox/work/airframes/acars-decoder-typescript/vendor/airframes-decoder/spec/labels/10/POS.yaml. Do not edit.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This generated header embeds the author’s absolute local path. That makes the generated tree non-reproducible: after npm run ads:codegen-build && npm run ads:generate in this checkout, every generated file changed only from /Users/kevin/... to /workspace/..., and npm run ads:check then failed.

Please make the generator emit a stable path, usually repo-relative to the spec root (or omit the source path entirely), then regenerate and commit the stable output. Otherwise the new CI check will fail or create noise for every contributor whose checkout path differs.

import type { DecodeResult, Message, Options } from "@airframes/ads-runtime-ts";
import { ResultFormatter } from "@airframes/ads-runtime-ts";
import * as helpers from "@airframes/ads-runtime-ts/helpers";
import * as hatches from "../escape_hatches";

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All generated plugins import ../escape_hatches, but this PR does not add lib/plugins/escape_hatches or otherwise make that relative module resolvable. A full type-check reports TS2307: Cannot find module '../escape_hatches' for each generated file.

Even plugins that do not call any hatch import it unconditionally, so this blocks importing any generated plugin. Either emit this import only when needed and point it at a real module, or add the local escape-hatch module with the functions the specs require.

Comment thread lib/plugins/generated/Label_44_POS.ts Outdated
ResultFormatter.timestamp(result, day);
ResultFormatter.timestamp(result, timestamp);
ResultFormatter.timestamp(result, eta);
ResultFormatter.fuel(result, fuel_in_tons);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This generated code does not compile and would also be unsafe at runtime. fuel_in_tons is declared with const inside the if block above, but it is used here outside that block and may not exist for the ***/**** cases. The runtime ResultFormatter also exposes currentFuel(...)/burnedFuel(...), not fuel(...), so npx tsc --noEmit reports both Cannot find name 'fuel_in_tons' and Property 'fuel' does not exist.

The same pattern appears in the generated label 44 variants with fuel_remaining. This should be fixed in the spec/codegen/runtime contract before these generated plugins are committed as a usable baseline.

Comment thread package.json
"test": "jest",
"ads:codegen-build": "cd vendor/airframes-decoder/codegen && npm install && npm run build",
"ads:generate": "node vendor/airframes-decoder/codegen/dist/cli.js generate --target ts --spec vendor/airframes-decoder/spec --out lib/plugins/generated",
"ads:check": "git diff --exit-code -- lib/plugins/generated || (echo 'lib/plugins/generated is out of date. Run npm run ads:generate and commit.' && exit 1)"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This freshness check only detects modifications to already-tracked generated files. If regeneration creates a new plugin file, git diff --exit-code -- lib/plugins/generated still exits successfully because untracked files are not part of git diff.

For a generated-code guard, prefer checking porcelain status for the path after generation, for example test -z "$(git status --porcelain -- lib/plugins/generated)", or combine git diff --exit-code with an explicit git ls-files --others --exclude-standard -- lib/plugins/generated check.

Comment thread tsconfig.json
"strict": true,
"baseUrl": ".",
"paths": {
"@airframes/ads-runtime-ts": ["vendor/airframes-decoder/runtimes/typescript/index.ts"],

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

compilerOptions.paths helps TypeScript resolve this alias during type-checking, but it does not by itself make the alias available to Jest/Babel or to Node for emitted JavaScript. Right now that is hidden because lib/plugins/generated/** is not imported by index.ts, so the package build never exercises these imports.

Before switching behavior to generated plugins, please add the matching runtime/test/build resolution path: for example a real workspace/package dependency for @airframes/ads-runtime-ts, Jest moduleNameMapper, and/or tsup alias/bundling configuration. Otherwise the first generated plugin import is likely to fail with Cannot find module '@airframes/ads-runtime-ts' outside the TypeScript compiler.

kevinelliott and others added 2 commits May 28, 2026 00:07
…oder

Proof point that the full vertical works end-to-end:

  spec/labels/10/POS.yaml
    → ads-gen --target ts
      → lib/plugins/generated/Label_10_POS.ts
        → @airframes/ads-runtime-ts {DecoderPlugin, ResultFormatter, helpers}
          → MessageDecoder dispatcher
            → 3/3 Label_10_POS tests + 407/407 full suite passes

Changes:

- Bump vendor/airframes-decoder submodule to e839bbc, which includes
  the emitter fix that suppresses raw auto-emit for fields consumed
  by a formatter (no more divergent raw.latitude/longitude/altitude
  next to the formatter's raw.position/altitude). Bytes now match
  the hand-written plugin.

- jest.config.ts: add moduleNameMapper for the @airframes/ads-runtime-ts
  path aliases (Jest doesn't read tsconfig paths by default).

- lib/plugins/escape_hatches/index.ts: placeholder so generated
  plugins' `import * as hatches from '../escape_hatches'` resolves
  (no hatches needed for Label_10_POS; later plugins populate this).

- lib/MessageDecoder.ts: import Label_10_POS from the generated tree
  and register it in place of the hand-written Plugins.Label_10_POS.

Verification:
  npm test -- --testPathPatterns=Label_10_POS  →  3/3 pass
  npm test                                      →  407/407 pass

Next: extend the pilot to the other declarative ports
(Label_44_IN/ON/OFF/ETA — also pure data, no escape hatches). The
remaining 60+ plugins need their escape-hatch implementations in
lib/plugins/escape_hatches/ before they can swap.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…orts)

Extends the pilot to the remaining 4 fully-declarative spec ports
identified during the bulk-port phase. Same byte-for-byte parity proof
as Label_10_POS: no escape hatches needed, runtime helpers cover all
the decode-fn calls (coordinate_decimal_minutes, integer, float,
timestamp_hhmmss, airport).

Verification:
  npm test -- --testPathPatterns='Label_44_(IN|ON|OFF|ETA)'
    → 14/14 pass
  npm test
    → 407/407 pass (no regression)

All 5 of the v1 declarative ports now run through the generated tree.
The remaining ~60 plugins use whole-plugin escape hatches; their
behavioral swap waits on the corresponding hatch implementations under
lib/plugins/escape_hatches/, ported from the original TS plugin
sources.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Comment thread lib/plugins/generated/Label_12_POS.ts Fixed

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@jest.config.ts`:
- Around line 91-98: Update the string literals in the moduleNameMapper object
to use single quotes to match Prettier settings: change the keys and mapped path
values inside moduleNameMapper (the "^`@airframes/ads-runtime-ts`$",
"^`@airframes/ads-runtime-ts/helpers`$",
"^`@airframes/ads-runtime-ts/escape_hatches`$" entries) from double-quoted to
single-quoted strings so linting/formatting passes.

In `@lib/MessageDecoder.ts`:
- Around line 14-18: lib/plugins/official.ts currently re-exports legacy
handwritten symbols (Label_10_POS, Label_44_ETA, Label_44_IN, Label_44_OFF,
Label_44_ON) which are unused except for generated imports like
Label_10_POS_Generated in lib/MessageDecoder.ts; either remove those legacy
export lines to eliminate dead/ambiguous API or explicitly deprecate them by
re-exporting with JSDoc `@deprecated` annotations and providing a migration note
pointing to the generated names (e.g., Label_10_POS -> Label_10_POS_Generated),
update any internal imports to use the generated symbols instead (verify
MessageDecoder.ts imports), and add a short changelog/migration comment so
consumers know to switch to the generated exports.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 46c233e5-edb4-4fad-baa1-c2b8b6adb4f8

📥 Commits

Reviewing files that changed from the base of the PR and between c3d7008 and 9a65f7a.

⛔ Files ignored due to path filters (6)
  • lib/plugins/generated/Label_10_POS.ts is excluded by !**/generated/**
  • lib/plugins/generated/Label_44_ETA.ts is excluded by !**/generated/**
  • lib/plugins/generated/Label_44_IN.ts is excluded by !**/generated/**
  • lib/plugins/generated/Label_44_OFF.ts is excluded by !**/generated/**
  • lib/plugins/generated/Label_44_ON.ts is excluded by !**/generated/**
  • lib/plugins/generated/Label_44_POS.ts is excluded by !**/generated/**
📒 Files selected for processing (4)
  • jest.config.ts
  • lib/MessageDecoder.ts
  • lib/plugins/escape_hatches/index.ts
  • vendor/airframes-decoder
✅ Files skipped from review due to trivial changes (1)
  • lib/plugins/escape_hatches/index.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • vendor/airframes-decoder

Comment thread jest.config.ts
Comment on lines +91 to +98
moduleNameMapper: {
"^@airframes/ads-runtime-ts$":
"<rootDir>/vendor/airframes-decoder/runtimes/typescript/index.ts",
"^@airframes/ads-runtime-ts/helpers$":
"<rootDir>/vendor/airframes-decoder/runtimes/typescript/helpers.ts",
"^@airframes/ads-runtime-ts/escape_hatches$":
"<rootDir>/vendor/airframes-decoder/runtimes/typescript/escape_hatches/index.ts",
},

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

Fix quote style to match Prettier configuration.

All string literals in moduleNameMapper use double quotes, but Prettier is configured to enforce single quotes. This will cause linting to fail.

🎨 Proposed fix
  moduleNameMapper: {
-    "^`@airframes/ads-runtime-ts`$":
-      "<rootDir>/vendor/airframes-decoder/runtimes/typescript/index.ts",
-    "^`@airframes/ads-runtime-ts/helpers`$":
-      "<rootDir>/vendor/airframes-decoder/runtimes/typescript/helpers.ts",
-    "^`@airframes/ads-runtime-ts/escape_hatches`$":
-      "<rootDir>/vendor/airframes-decoder/runtimes/typescript/escape_hatches/index.ts",
+    '^`@airframes/ads-runtime-ts`$':
+      '<rootDir>/vendor/airframes-decoder/runtimes/typescript/index.ts',
+    '^`@airframes/ads-runtime-ts/helpers`$':
+      '<rootDir>/vendor/airframes-decoder/runtimes/typescript/helpers.ts',
+    '^`@airframes/ads-runtime-ts/escape_hatches`$':
+      '<rootDir>/vendor/airframes-decoder/runtimes/typescript/escape_hatches/index.ts',
  },
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
moduleNameMapper: {
"^@airframes/ads-runtime-ts$":
"<rootDir>/vendor/airframes-decoder/runtimes/typescript/index.ts",
"^@airframes/ads-runtime-ts/helpers$":
"<rootDir>/vendor/airframes-decoder/runtimes/typescript/helpers.ts",
"^@airframes/ads-runtime-ts/escape_hatches$":
"<rootDir>/vendor/airframes-decoder/runtimes/typescript/escape_hatches/index.ts",
},
moduleNameMapper: {
'^`@airframes/ads-runtime-ts`$':
'<rootDir>/vendor/airframes-decoder/runtimes/typescript/index.ts',
'^`@airframes/ads-runtime-ts/helpers`$':
'<rootDir>/vendor/airframes-decoder/runtimes/typescript/helpers.ts',
'^`@airframes/ads-runtime-ts/escape_hatches`$':
'<rootDir>/vendor/airframes-decoder/runtimes/typescript/escape_hatches/index.ts',
},
🧰 Tools
🪛 ESLint

[error] 92-92: Replace "^@airframes/ads-runtime-ts$" with '^@airframes/ads-runtime-ts$'

(prettier/prettier)


[error] 93-93: Replace "<rootDir>/vendor/airframes-decoder/runtimes/typescript/index.ts" with '<rootDir>/vendor/airframes-decoder/runtimes/typescript/index.ts'

(prettier/prettier)


[error] 94-94: Replace "^@airframes/ads-runtime-ts/helpers$" with '^@airframes/ads-runtime-ts/helpers$'

(prettier/prettier)


[error] 95-95: Replace "<rootDir>/vendor/airframes-decoder/runtimes/typescript/helpers.ts" with '<rootDir>/vendor/airframes-decoder/runtimes/typescript/helpers.ts'

(prettier/prettier)


[error] 96-96: Replace "^@airframes/ads-runtime-ts/escape_hatches$" with '^@airframes/ads-runtime-ts/escape_hatches$'

(prettier/prettier)


[error] 97-97: Replace "<rootDir>/vendor/airframes-decoder/runtimes/typescript/escape_hatches/index.ts" with '<rootDir>/vendor/airframes-decoder/runtimes/typescript/escape_hatches/index.ts'

(prettier/prettier)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@jest.config.ts` around lines 91 - 98, Update the string literals in the
moduleNameMapper object to use single quotes to match Prettier settings: change
the keys and mapped path values inside moduleNameMapper (the
"^`@airframes/ads-runtime-ts`$", "^`@airframes/ads-runtime-ts/helpers`$",
"^`@airframes/ads-runtime-ts/escape_hatches`$" entries) from double-quoted to
single-quoted strings so linting/formatting passes.

Comment thread lib/MessageDecoder.ts Outdated
… fixes)

Pulls in three fixes that unblock the Label_44_{IN,ON,OFF,ETA} pilot:

1. Emitter: when-gated field declarations are now hoisted (let X;
   outside the if), so downstream formatters see the variable
   (undefined when the guard fails) instead of crashing with
   ReferenceError. (80095f5)

2. Emitter: formatter type 'fuel' now maps to ResultFormatter.currentFuel
   (the actual runtime method name). Was emitting .fuel which threw
   'is not a function'. (ef58ee3)

3. Runtime: ResultFormatter.currentFuel tolerates undefined/NaN input
   so it can be called unconditionally from generated plugins even
   when the upstream when-gated value never got assigned. Matches the
   original hand-written guard pattern. (ce7d385)

Verified: 407/407 full suite passes.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

import { DecoderPlugin } from "@airframes/ads-runtime-ts";
import type { DecodeResult, Message, Options } from "@airframes/ads-runtime-ts";
import { ResultFormatter } from "@airframes/ads-runtime-ts";
Comment thread lib/plugins/generated/Label_12_N_Space.ts Fixed
Backed by ~60 escape-hatch implementations under lib/plugins/escape_hatches/
that port each hand-written plugin's decode() body into a free function
the generated wrapper invokes via 'import * as hatches from
../escape_hatches'. Implementations authored by 4 parallel agents in a
single pass.

What's now generated:
  CBand, ARINC_702, Label_ColonComma, Label_5Z_Slash,
  Label_10_LDR, Label_10_POS, Label_10_Slash, Label_12_N_Space,
  Label_12_POS, Label_13Through18_Slash, Label_15, Label_15_FST,
  Label_16_AUTPOS, Label_16_Honeywell, Label_16_N_Space, Label_16_POSA1,
  Label_16_TOD, Label_1L_3Line, Label_1L_070, Label_1L_660, Label_1L_Slash,
  Label_20_CFB01, Label_20_POS, Label_21_POS, Label_22_OFF, Label_22_POS,
  Label_24_Slash, Label_2P_FM3, Label_2P_FM4, Label_2P_FM5,
  Label_30_Slash_EA, Label_44_ETA, Label_44_IN, Label_44_OFF, Label_44_ON,
  Label_44_Slash, Label_4A_01, Label_4A_DIS, Label_4A_DOOR, Label_4A_Slash_01,
  Label_4N, Label_4T_AGFSR, Label_4T_ETA, Label_B6_Forwardslash, Label_H2_02E,
  Label_H1_ATIS, Label_H1_EZF, Label_H1_FLR, Label_H1_M_POS, Label_H1_OHMA,
  Label_H1_OFP, Label_H1_Paren, Label_H1_WRN, Label_H1_StarPOS, Label_HX,
  Label_58, Label_80, Label_83, Label_8E, Label_1M_Slash, Label_MA, Label_SQ,
  Label_QP, Label_QQ, Label_QR, Label_QS.

What's still hand-written (separate follow-up):
  - Plugins.Label_4A      — agent stubbed; variant-2/variant-3 field hatches
                            need design (formatter ownership of items list).
  - Plugins.Label_44_POS  — spec uses field-level customs
                            (parse_flight_level_or_ground +
                            flight_level_to_altitude_feet) not implemented
                            in this bulk pass.

Submodule bumped to airframes-decoder@c037de4 to pick up:
  - runtime: DecoderPlugin helpers made public (initResult/setDecodeLevel/
    failUnknown/debug) so escape hatches can delegate
  - runtime: re-export Arinc702Helper, FlightPlanUtils, RouteUtils,
    parseIcaoFpl, MIAMCoreUtils, base64ToUint8Array, inflateData,
    ascii85Decode from the package index
  - emitter: smart slugger handles camelCase boundaries (CBand→c-band,
    StarPOS→star-pos, 3Line→3-line, 4A stays 4a) so generated plugin
    names match the legacy ones byte-for-byte

Verified: 407/407 tests pass. No regressions.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
import type { DecodeResult, Message, Options } from "@airframes/ads-runtime-ts";
import { ResultFormatter } from "@airframes/ads-runtime-ts";
import * as helpers from "@airframes/ads-runtime-ts/helpers";
import * as hatches from "../escape_hatches";
kevinelliott and others added 2 commits May 28, 2026 01:14
Two final swaps:

- Label_4A: spec changed (airframesio/acars-decoder@de6137f) from
  variants+field-customs to whole-plugin parse-custom. Hatch
  implementation in escape_hatches/Label_4A.ts mirrors the original
  decode() body byte-for-byte (3 variants by field count + first-char
  inspection, inline ResultFormatter calls).

- Label_44_POS: spec stays declarative; added the two field-level hatches
  it referenced — parse_flight_level_or_ground and
  flight_level_to_altitude_feet — in escape_hatches/Label_44_POS.ts.
  Both are 1-line ports of inline TS code:
    'GRD'/'***' → 0, else Number(value)
    multiply by 100

Submodule bumped to airframes-decoder@de6137f (carries the Label_4A spec
change).

Verified: 407/407 tests pass. End-to-end vertical for every TS plugin:
  spec/*.yaml → ads-gen → lib/plugins/generated/*.ts → escape_hatches/* →
  ResultFormatter / helpers → MessageDecoder dispatcher → original tests.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…merged)

PR #1 in airframes-decoder merged as e7c66e1. Submodule now tracks main
instead of the init/ads-v1 PR branch. All 407 tests still pass; generated
tree regenerated against the merged-main spec (no diff vs prior bump).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

import { DecoderPlugin } from "@airframes/ads-runtime-ts";
import type { DecodeResult, Message, Options } from "@airframes/ads-runtime-ts";
import { ResultFormatter } from "@airframes/ads-runtime-ts";
import { DecoderPlugin } from "@airframes/ads-runtime-ts";
import type { DecodeResult, Message, Options } from "@airframes/ads-runtime-ts";
import { ResultFormatter } from "@airframes/ads-runtime-ts";
import * as helpers from "@airframes/ads-runtime-ts/helpers";

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

Note

Due to the large number of review comments, Critical severity comments were prioritized as inline comments.

🟠 Major comments (22)
lib/plugins/escape_hatches/Label_2P_FM4.ts-34-48 (1)

34-48: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Fix the unreachable FM4 header guard before using header[1].

header.length == 0 is never true for split, so messages missing FM4 can still flow through and use header[1] as undefined while reporting a successful decode.

Suggested patch
-    const header = parts[0].split('FM4');
-    if (header.length == 0) {
+    const header = parts[0].split('FM4');
+    if (header.length < 2 || !header[1]) {
       // can't use preambles, as there can be info before `FM4`
       // so let's check if we want to decode it here
       ResultFormatter.unknown(result, message.text);
       result.decoded = false;
       result.decoder.decodeLevel = 'none';
       return result;
     }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lib/plugins/escape_hatches/Label_2P_FM4.ts` around lines 34 - 48, The current
guard uses header.length == 0 which is impossible for String.split; change it to
check header.length < 2 before accessing header[1]. Specifically, in the block
where header is created via const header = parts[0].split('FM4'), replace the
unreachable check with if (header.length < 2) { ResultFormatter.unknown(result,
message.text); result.decoded = false; result.decoder.decodeLevel = 'none';
return result; } so you don't access header[1] when FM4 is absent; keep the
existing handling of header[0] and the subsequent
ResultFormatter.departureAirport(result, header[1]) /
ResultFormatter.arrivalAirport(result, parts[1]) unchanged.
lib/plugins/escape_hatches/Label_2P_FM5.ts-33-44 (1)

33-44: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Use a real FM5 presence check before reading header[1].

The current header.length == 0 check can’t fire, so malformed headers can be treated as decoded and pass undefined into formatter fields.

Suggested patch
-    const header = parts[0].split('FM5 ');
-    if (header.length == 0) {
+    const header = parts[0].split('FM5 ');
+    if (header.length < 2 || !header[1]) {
       // can't use preambles, as there can be info before `FM4`
       // so let's check if we want to decode it here
       ResultFormatter.unknown(result, message.text);
       result.decoded = false;
       result.decoder.decodeLevel = 'none';
       return result;
     }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lib/plugins/escape_hatches/Label_2P_FM5.ts` around lines 33 - 44, The code
reads header[1] without verifying the presence of "FM5 ", so malformed headers
can pass undefined into ResultFormatter.departureAirport/arrivalAirport; change
the guard to a real presence check (e.g., verify parts[0] contains "FM5 " and
header.length > 1 or parts[0].startsWith('FM5 ')) and on failure call
ResultFormatter.unknown(result, message.text), set result.decoded = false and
result.decoder.decodeLevel = 'none' before returning so departure/arrival
formatters are never called with undefined.
lib/plugins/escape_hatches/Label_4T_AGFSR.ts-42-47 (1)

42-47: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Apply direction sign to the full coordinate, not just degrees.

Current arithmetic makes S/W values too large (less negative) because minutes are always added as positive.

Suggested fix
   ResultFormatter.position(result, {
     latitude:
-      CoordinateUtils.getDirection(lat[6]) * Number(lat.substring(0, 2)) +
-      Number(lat.substring(2, 6)) / 60,
+      CoordinateUtils.getDirection(lat[6]) *
+      (Number(lat.substring(0, 2)) + Number(lat.substring(2, 6)) / 60),
     longitude:
-      CoordinateUtils.getDirection(lon[7]) * Number(lon.substring(0, 3)) +
-      Number(lon.substring(3, 7)) / 60,
+      CoordinateUtils.getDirection(lon[7]) *
+      (Number(lon.substring(0, 3)) + Number(lon.substring(3, 7)) / 60),
   });
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lib/plugins/escape_hatches/Label_4T_AGFSR.ts` around lines 42 - 47, The
latitude/longitude calculations in Label_4T_AGFSR.ts apply
CoordinateUtils.getDirection(...) only to the degrees portion, causing minutes
to always be added positively; change both expressions to compute the full
coordinate first (degrees + minutes/60) and then multiply that sum by
CoordinateUtils.getDirection(...) so the sign applies to the entire coordinate
(refer to the latitude and longitude object fields and
CoordinateUtils.getDirection in this file).
lib/plugins/escape_hatches/Label_4T_ETA.ts-27-33 (1)

27-33: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Validate ETA tokenization before accessing etaData[2].

data.length === 3 is not enough; malformed third section can still crash on etaData[2].substring(0, 4).

Suggested fix
   ResultFormatter.flightNumber(result, data[0].trim());
   ResultFormatter.departureDay(result, Number(data[1]));
   const etaData = data[2].split(' ');
+  if (etaData.length < 3 || !etaData[2]) {
+    ResultFormatter.unknown(result, message.text);
+    result.decoded = false;
+    result.decoder.decodeLevel = 'none';
+    return result;
+  }
   ResultFormatter.arrivalDay(result, Number(etaData[0]));
   ResultFormatter.arrivalAirport(result, etaData[1], 'IATA');
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lib/plugins/escape_hatches/Label_4T_ETA.ts` around lines 27 - 33, The code
assumes etaData[2] exists and has at least 4 chars before calling substring,
which can crash; in the block where etaData is derived from data[2], validate
etaData length and content (e.g., ensure etaData.length >= 3 and etaData[2] is a
string of expected length/format or matches a HHMMSS pattern) before calling
ResultFormatter.eta with
DateTimeUtils.convertHHMMSSToTod(etaData[2].substring(0,4)); if validation
fails, handle gracefully (skip ETA formatting or provide a default/nullable
value) so ResultFormatter.arrivalDay/arrivalAirport still run safely. Ensure
checks reference etaData and the call to ResultFormatter.eta /
DateTimeUtils.convertHHMMSSToTod so the fix is easy to locate.
lib/plugins/escape_hatches/Label_13Through18_Slash.ts-74-97 (1)

74-97: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Validate /LOC coordinate tokens before fixed-position indexing.

The parser assumes both coordinate tokens and expected lengths are present; malformed /LOC lines can throw on location[1][0]/substring access.

Suggested fix
     if (lines[i].startsWith('/LOC')) {
       const location = lines[i].substring(5).split(',');
+      if (location.length !== 2 || !location[0] || !location[1]) {
+        ResultFormatter.unknown(result, lines[i], '\r\n');
+        continue;
+      }
       let position;
       if (location[0].startsWith('+') || location[0].startsWith('-')) {
         position = {
           latitude: Number(location[0]),
           longitude: Number(location[1]),
         };
       } else {
+        if (location[0].length < 7 || location[1].length < 8) {
+          ResultFormatter.unknown(result, lines[i], '\r\n');
+          continue;
+        }
         position = {
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lib/plugins/escape_hatches/Label_13Through18_Slash.ts` around lines 74 - 97,
The code assumes location[0] and location[1] exist and have fixed substrings
when parsing /LOC lines; add validation in the block that computes position (in
Label_13Through18_Slash.ts) to first check that location.length >= 2 and that
the tokens have the expected formats/lengths (e.g., for signed decimal tokens
ensure they parse as numbers, and for DMS tokens ensure substring indices are
safe and token lengths meet the minimum before calling substring and accessing
[0]). If validation fails, handle gracefully (skip the malformed line, set
position to null/undefined, or log an error) instead of indexing into undefined;
update all uses of location[0], location[1], location[0][0], location[1][0], and
substring calls and keep CoordinateUtils.getDirection and dmsToDecimalDegrees
calls only after validation passes.
lib/plugins/escape_hatches/Label_13Through18_Slash.ts-35-37 (1)

35-37: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Guard first-line/header shape before reading parts[1].

parts[1].substring(0, 2) runs before any shape check, so messages without / crash instead of being marked unknown.

Suggested fix
   const lines = message.text.split('\r\n');
-  const parts = lines[0].split('/');
-  const labelNumber = Number(parts[1].substring(0, 2));
+  const firstLine = lines[0] ?? '';
+  const parts = firstLine.split('/');
+  if (parts.length < 3 || !parts[1] || parts[1].length < 2) {
+    ResultFormatter.unknown(result, message.text);
+    result.decoded = false;
+    result.decoder.decodeLevel = 'none';
+    return result;
+  }
+  const labelNumber = Number(parts[1].substring(0, 2));
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lib/plugins/escape_hatches/Label_13Through18_Slash.ts` around lines 35 - 37,
The code reads parts[1].substring(0,2) without validating the header shape which
crashes on messages missing '/' — before computing labelNumber, validate that
lines (from message.text.split) has at least one element, that parts (from
lines[0].split('/')) has length > 1 and parts[1] has at least 2 characters; if
any check fails, bail out and mark the message as unknown. Update the logic
around const lines, const parts and const labelNumber to perform these guards
and only parse Number(parts[1].substring(0,2)) when the checks pass.
lib/plugins/escape_hatches/Label_4A_DIS.ts-30-36 (1)

30-36: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Add field-count validation before using fields[1]/fields[2].

Malformed comma payloads with fewer than 3 fields currently throw at fields[1].substring(2) instead of cleanly returning unknown.

Suggested fix
   result.decoded = true;
   const fields = message.text.split(',');
+  if (fields.length < 3) {
+    result.decoded = false;
+    ResultFormatter.unknown(result, message.text);
+    plugin.setDecodeLevel(result, result.decoded);
+    return result;
+  }
   ResultFormatter.timestamp(
     result,
     DateTimeUtils.convertHHMMSSToTod(fields[1].substring(2) + '00'),
   );
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lib/plugins/escape_hatches/Label_4A_DIS.ts` around lines 30 - 36, The code
assumes message.text.split(',') yields at least 3 parts but will throw when
fields[1] or fields[2] are missing; add a guard that checks fields.length >= 3
before using fields[1]. If the check fails, call
ResultFormatter.timestamp(result, 'unknown') and
ResultFormatter.callsign(result, 'unknown') (and ResultFormatter.text(result,
'') or similar) and return early. Update the block around the fields variable
and the calls to ResultFormatter.timestamp / ResultFormatter.callsign /
ResultFormatter.text so they only use fields[1], fields[2], and fields.slice(3)
after the length check.
lib/plugins/escape_hatches/Label_5Z_Slash.ts-50-53 (1)

50-53: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Guard split-array indexing before accessing parsed fields.

This decoder assumes required tokens always exist (data[1], data[2], header[n], info[n], airports[n], estimates[n]). Malformed 5Z payloads will throw before reaching the unknown fallback path.

Suggested hardening
   const data = lines[0].split('/');
-  const header = data[1].split(' '); //data[0] is blank
+  if (data.length < 2) {
+    ResultFormatter.unknown(result, message.text);
+    result.decoded = false;
+    result.decoder.decodeLevel = 'none';
+    return result;
+  }
+  const header = data[1].trim().split(/\s+/); // data[0] is blank
   const type = header[0];
   const typeDescription = descriptions[type];
@@
-    if (type === 'B3' && data[1] === 'B3 TO DATA REQ    ') {
-      const info = data[2].split(' ');
+    if (type === 'B3' && data[1] === 'B3 TO DATA REQ    ') {
+      if (!data[2]) {
+        ResultFormatter.unknown(result, message.text);
+        result.decoded = false;
+        result.decoder.decodeLevel = 'none';
+        return result;
+      }
+      const info = data[2].trim().split(/\s+/);
+      if (info.length < 6) {
+        ResultFormatter.unknown(result, message.text);
+        result.decoded = false;
+        result.decoder.decodeLevel = 'none';
+        return result;
+      }
@@
-    } else if (type === 'ET') {
-      const airports = data[2].split(' ');
+    } else if (type === 'ET') {
+      if (!data[2] || !data[3]) {
+        ResultFormatter.unknown(result, message.text);
+        result.decoded = false;
+        result.decoder.decodeLevel = 'none';
+        return result;
+      }
+      const airports = data[2].trim().split(/\s+/);
+      if (airports.length < 5) {
+        ResultFormatter.unknown(result, message.text);
+        result.decoded = false;
+        result.decoder.decodeLevel = 'none';
+        return result;
+      }
@@
-      const estimates = data[3].split(' ');
+      const estimates = data[3].trim().split(/\s+/);
+      if (estimates.length < 3) {
+        ResultFormatter.unknown(result, message.text);
+        result.decoded = false;
+        result.decoder.decodeLevel = 'none';
+        return result;
+      }

Also applies to: 71-129

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lib/plugins/escape_hatches/Label_5Z_Slash.ts` around lines 50 - 53, The
parsing code in Label_5Z_Slash.ts assumes tokens exist (variables data, header,
info, airports, estimates and the lookup descriptions[type]) and will throw on
malformed payloads; update the decoder to defensively check array lengths and
existence before indexing (e.g., verify data.length>1 before using data[1],
header.length>0 before header[0], and similar guards for info[], airports[],
estimates[]) and guard the descriptions lookup (use descriptions[type] ??
fallback). If any required token is missing, return or route to the existing
"unknown" fallback path (or throw a controlled parse error) instead of allowing
uncaught exceptions; apply the same pattern to the other parsing blocks in this
file (the region covering lines ~71-129).
lib/plugins/escape_hatches/Label_83.ts-23-35 (1)

23-35: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Add field-count guards for the 4DH3 ETAT2 variant.

This branch dereferences multiple split tokens without checking array length. A truncated but prefix-matching payload can throw and crash decode.

Suggested fix
   if (text.substring(0, 10) === '4DH3 ETAT2') {
     // variant 2
     const fields = text.split(/\s+/);
+    if (fields.length < 7) {
+      result.decoded = false;
+      ResultFormatter.unknown(result, text);
+      result.decoder.decodeLevel = 'none';
+      return result;
+    }
     if (fields[2].length > 5) {
       result.raw.day = fields[2].substring(5);
     }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lib/plugins/escape_hatches/Label_83.ts` around lines 23 - 35, The code in
Label_83.ts assumes fields and subfields exist and have enough elements, causing
crashes for truncated "4DH3 ETAT2" payloads; add defensive checks before
dereferencing: verify fields.length >= 7 (or the exact number you need) and that
fields[2], fields[3], fields[4], and fields[6] are present, and that subfields =
fields[3].split('/') yields at least 2 parts before calling
ResultFormatter.departureAirport/arrivalAirport; also guard fields[2].length > 5
before substring and only call DateTimeUtils.convertHHMMSSToTod when fields[6]
exists (or default to a safe value/early return), returning or marking result as
unknown when required tokens are missing. Ensure you reference
ResultFormatter.unknown, ResultFormatter.departureAirport,
ResultFormatter.arrivalAirport, ResultFormatter.tail, ResultFormatter.eta, and
DateTimeUtils.convertHHMMSSToTod while adding these checks.
lib/plugins/escape_hatches/Label_8E.ts-18-33 (1)

18-33: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Do not mark unmatched 8E payloads as fully decoded.

When regex matching fails, this function still sets decoded=true and decodeLevel='full', which misclassifies invalid messages as successful parses.

Suggested fix
   const results = message.text.match(regex);
   if (results?.groups) {
@@
     ResultFormatter.eta(
       result,
       DateTimeUtils.convertHHMMSSToTod(results.groups.arrival_eta),
     );
     ResultFormatter.arrivalAirport(result, results.groups.arrival_icao);
+    result.decoded = true;
+    result.decoder.decodeLevel = 'full';
+  } else {
+    ResultFormatter.unknown(result, message.text);
+    result.decoded = false;
+    result.decoder.decodeLevel = 'none';
   }
-
-  result.decoded = true;
-  result.decoder.decodeLevel = 'full';
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lib/plugins/escape_hatches/Label_8E.ts` around lines 18 - 33, The code
currently sets result.decoded = true and result.decoder.decodeLevel = 'full'
regardless of whether the regex matched; update the control flow in Label_8E.ts
so these flags are only set when results?.groups is truthy (i.e., the parse
succeeded). Move the result.decoded and result.decoder.decodeLevel assignments
into the same block that calls ResultFormatter.eta and
ResultFormatter.arrivalAirport, or add an explicit early return when
results?.groups is falsy to avoid marking unmatched 8E payloads as fully
decoded.
lib/plugins/escape_hatches/Label_15_FST.ts-33-40 (1)

33-40: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Validate parsed coordinate/altitude numbers before marking decode success.

This path accepts cardinal markers but does not validate numeric substrings, so malformed payloads can emit NaN/invalid fields and still end as decoded = true.

Suggested fix
   if (
     (firstChar === 'N' || firstChar === 'S') &&
     (middleChar === 'W' || middleChar === 'E')
   ) {
-    const lat =
+    const lat =
       (Number(stringCoords.substring(1, 7)) / 10000) *
       (firstChar === 'S' ? -1 : 1);
-    const lon =
+    const lon =
       (Number(stringCoords.substring(8, 15)) / 10000) *
       (middleChar === 'W' ? -1 : 1);
-    ResultFormatter.position(result, { latitude: lat, longitude: lon });
-    ResultFormatter.altitude(result, Number(stringCoords.substring(15)) * 100);
+    const alt = Number(stringCoords.substring(15)) * 100;
+    if (!Number.isFinite(lat) || !Number.isFinite(lon) || !Number.isFinite(alt)) {
+      ResultFormatter.unknown(result, message.text);
+      result.decoded = false;
+      result.decoder.decodeLevel = 'none';
+      return result;
+    }
+    ResultFormatter.position(result, { latitude: lat, longitude: lon });
+    ResultFormatter.altitude(result, alt);
   } else {

Also applies to: 56-58

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lib/plugins/escape_hatches/Label_15_FST.ts` around lines 33 - 40, The decode
path extracts numeric substrings from stringCoords to compute lat, lon and
altitude but never validates them, allowing NaN/invalid values while still
marking decoded=true; update the logic around the parsing that computes lat, lon
and altitude (references: stringCoords, firstChar, middleChar, lat, lon and
ResultFormatter.position/altitude) to explicitly parse the substrings to
numbers, verify they are finite (e.g., !isNaN and isFinite) and within expected
ranges before calling ResultFormatter.position and ResultFormatter.altitude, and
if any check fails, do not mark the message decoded (return or set
decoded=false) and handle as a parse error so malformed payloads do not emit
invalid coordinates; apply the same validation for the alternate block at lines
56-58.
lib/plugins/escape_hatches/Label_H1_M_POS.ts-47-57 (1)

47-57: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Reject malformed numeric position/altitude/heading values.

parseFloat/Number results are not validated, so NaN values can be emitted with decoded = true.

Suggested fix
   const lat = parseFloat(fields[3].replace(/\s/g, ''));
   const lon = parseFloat(fields[4].replace(/\s/g, ''));
-  ResultFormatter.position(result, { latitude: lat, longitude: lon });
-
-  // Altitude
   const alt = Number(fields[5]);
-  ResultFormatter.altitude(result, alt);
-
-  // Heading
   const hdg = Number(fields[6]);
+  if (
+    !Number.isFinite(lat) ||
+    !Number.isFinite(lon) ||
+    !Number.isFinite(alt) ||
+    !Number.isFinite(hdg)
+  ) {
+    return failUnknown(result, message.text, options);
+  }
+  ResultFormatter.position(result, { latitude: lat, longitude: lon });
+  ResultFormatter.altitude(result, alt);
   ResultFormatter.heading(result, hdg);

Also applies to: 65-67

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lib/plugins/escape_hatches/Label_H1_M_POS.ts` around lines 47 - 57, Validate
parsed numeric values from fields before calling ResultFormatter: after
computing lat = parseFloat(fields[3].replace(/\s/g,'')) and lon =
parseFloat(fields[4].replace(/\s/g,'')) check Number.isFinite(lat) &&
Number.isFinite(lon) and if not, mark the decode as failed (e.g., set
decoded=false or return an error) and do not call ResultFormatter.position;
likewise validate alt = Number(fields[5]) and hdg = Number(fields[6]) with
Number.isFinite(alt)/Number.isFinite(hdg) before calling
ResultFormatter.altitude and ResultFormatter.heading and reject the message if
any are invalid.
lib/plugins/escape_hatches/Label_H1_FLR.ts-13-37 (1)

13-37: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Harden /FR payload extraction before setting decoded state.

Current logic can silently accept malformed payloads (short/non-date payload) and can lose content after additional /FR tokens.

Suggested fix
-  const parts = message.text.split('/FR');
-
-  if (parts.length > 1) {
+  const frIndex = message.text.indexOf('/FR');
+  if (frIndex >= 0) {
     // decode header
-    const fields = parts[0].split('/');
+    const fields = message.text.slice(0, frIndex).split('/');
     // 0 is the msg type
     for (let i = 1; i < fields.length; i++) {
       const field = fields[i];
       ResultFormatter.unknown(result, field, '/');
     }
 
-    const data = parts[1].substring(0, 20);
-    const msg = parts[1].substring(20);
+    const payload = message.text.slice(frIndex + 3);
+    if (payload.length < 20 || !/^\d{12}/.test(payload)) {
+      ResultFormatter.unknown(result, message.text);
+      result.decoded = false;
+      result.decoder.decodeLevel = 'none';
+      return result;
+    }
+    const data = payload.substring(0, 20);
+    const msg = payload.substring(20);

Also applies to: 47-49

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lib/plugins/escape_hatches/Label_H1_FLR.ts` around lines 13 - 37, The current
extraction around parts, data, msg, and datetime trusts
message.text.split('/FR') and slices (data = parts[1].substring(0,20), datetime
= data.substring(0,12)) which can throw or silently lose content for malformed
or multiple /FR tokens; update the logic in Label_H1_FLR.ts to: safely locate
the first "/FR" (use indexOf or split with limit) and ensure parts[1] exists and
has the minimum length before substringing, guard that datetime has at least 12
chars before calling DateTimeUtils.convertDateTimeToEpoch, preserve any
additional "/FR" occurrences by treating the remainder as msg (do not discard
extra tokens), and only set result.raw.message_timestamp and mark decoded when
timestamp parsing succeeds; keep using ResultFormatter.unknown for
trailing/unknown slices so malformed data is logged rather than ignored.
lib/plugins/escape_hatches/Label_H1_Paren.ts-56-63 (1)

56-63: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Anchor latitude parsing to prevent partial matches.

parseLat can partially match malformed 5-digit latitude strings and return incorrect coordinates instead of NaN.

Suggested fix
 function parseLat(latStr: string): number {
-  const match = latStr.match(/(-?)(\d{2})(\d{2})([NS])/);
+  const match = latStr.match(/^(-?)(\d{2,3})(\d{2})([NS])$/);
   if (!match) return NaN;
-  const deg = parseInt(match[2]);
-  const min = parseInt(match[3]);
-  const sign = match[4] === 'S' ? -1 : 1;
+  const deg = parseInt(match[2], 10);
+  const min = parseInt(match[3], 10);
+  if (deg > 90 || min > 59) return NaN;
+  const sign = match[4] === 'S' || match[1] === '-' ? -1 : 1;
   return sign * (deg + min / 60);
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lib/plugins/escape_hatches/Label_H1_Paren.ts` around lines 56 - 63, The
parseLat function currently uses a regex that can partially match malformed
strings; update its pattern to anchor the whole string and add validation for
bounds: replace the regex in parseLat with /^(-?)(\d{2})(\d{2})([NS])$/ to
require full-string matches, then after parsing check that deg is between 0 and
90 and min is between 0 and 59 (return NaN if out of range) before computing the
signed decimal degrees using the existing sign logic.
lib/plugins/escape_hatches/Label_H1_Paren.ts-33-54 (1)

33-54: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Set explicit failure state when regex does not match.

For inputs beginning with ( but failing the full pattern, decode status is left implicit. This should explicitly mark failure for deterministic downstream behavior.

Suggested fix
   const match = message.text.match(regex);
   if (match && match.groups) {
@@
     ResultFormatter.mach(result, parseFloat(match.groups.mach));
     ResultFormatter.unknown(result, 'RMK');
+  } else {
+    result.decoded = false;
+    result.decoder.decodeLevel = 'none';
   }
   return result;
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lib/plugins/escape_hatches/Label_H1_Paren.ts` around lines 33 - 54, When the
regex match fails in the function that computes "match" against "regex",
explicitly set the decode failure state instead of leaving it implicit: in the
else branch after "const match = message.text.match(regex)" set result.decoded =
false and set a clear decode level (e.g. result.decoder.decodeLevel = 'failed'
or 'none'), and clear or set any minimal safe values on
result.formatted.description (or other fields) so downstream consumers have a
deterministic failure state; update the block that currently handles the truthy
match (using ResultFormatter.*) to add this explicit else branch referencing
match, regex, result, and result.decoder.decodeLevel.
lib/plugins/escape_hatches/Label_16_AUTPOS.ts-24-25 (1)

24-25: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Anchor the AUTPOS regex to the end of the message.

Line 24 is start-anchored but not end-anchored, so trailing bytes can be accepted while the decoder reports success. That causes a false “fully decoded” outcome and drops unparsed data.

Suggested fix
-  const regex =
-    /^(\d{6})\/AUTPOS\/LLD (N|S)(\d{2})(\d{2})(\d{2}) (E|W)(\d{3})(\d{2})(\d{2})\s*\r?\n\/ALT (\d+)\/SAT ([*\-\d]{4})\r?\n\/WND ([*\d]{3})([\*\d]{3})\/TAT ([*\-\d]{4})\/TAS ([*\d]{3,4})\/CRZ ([*\d]{3,4})\r?\n\/FOB (\d{6})\r?\n\/DAT (\d{6})\/TIM (\d{6})/;
+  const regex =
+    /^(\d{6})\/AUTPOS\/LLD (N|S)(\d{2})(\d{2})(\d{2}) (E|W)(\d{3})(\d{2})(\d{2})\s*\r?\n\/ALT (\d+)\/SAT ([*\-\d]{4})\r?\n\/WND ([*\d]{3})([\*\d]{3})\/TAT ([*\-\d]{4})\/TAS ([*\d]{3,4})\/CRZ ([*\d]{3,4})\r?\n\/FOB (\d{6})\r?\n\/DAT (\d{6})\/TIM (\d{6})\s*$/;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lib/plugins/escape_hatches/Label_16_AUTPOS.ts` around lines 24 - 25, The
AUTPOS regex in Label_16_AUTPOS.ts is anchored at the start but not the end,
allowing trailing bytes to be ignored; update the regex (the variable named
regex used where match = regex.exec(message.text)) to anchor the pattern to the
end of the message by adding an end anchor (e.g., append $ or \s*$ after the
final group) so the whole message must match and trailing/unparsed data will not
be silently accepted.
lib/plugins/escape_hatches/Label_H2_02E.ts-42-55 (1)

42-55: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Don’t silently drop malformed header chunks.

When the header length is not 45 (Line 43), the code currently skips it entirely. That can still end in decoded=true and even decodeLevel='full' with lost input.

Suggested fix
   const header = parts[0];
   if (header.length === 45) {
@@
     } else {
       result.remaining.text +=
         (result.remaining.text ? ' ' : '') + header.substring(13);
     }
+  } else {
+    result.remaining.text +=
+      (result.remaining.text ? ' ' : '') + header;
   }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lib/plugins/escape_hatches/Label_H2_02E.ts` around lines 42 - 55, The header
chunk handling currently ignores headers whose length !== 45 (variable header
from parts[0]), which can drop input; update the block around header/parts
handling so that when header.length !== 45 you append the entire header text to
result.remaining.text (preserving spacing as done elsewhere) and mark the
message as not fully decoded — e.g., set decoded = false and/or set decodeLevel
= 'partial' (or lower the level used elsewhere) so callers know decoding was
incomplete; keep existing parsing for the 45-byte case (ResultFormatter.day,
ResultFormatter.departureAirport, ResultFormatter.arrivalAirport,
parseWeatherReport, windData) unchanged.
lib/plugins/escape_hatches/Label_MA.ts-53-70 (1)

53-70: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Compute decodeLevel after recursive decode/fallback outcome.

Line 53 sets 'full' too early. If inner decode fails (Lines 67–70) or returns remaining text, the result can still be marked full incorrectly.

Suggested fix
-      result.decoder.decodeLevel = 'full';
       const decoded = plugin.decoder.decode(
@@
       if (decoded.decoded) {
         result.raw = { ...result.raw, ...decoded.raw };
         result.formatted.items.push(...decoded.formatted.items);
         result.remaining = decoded.remaining;
       } else {
         ResultFormatter.text(result, messageText);
         result.remaining = { text: messageText };
       }
+      result.decoder.decodeLevel = result.remaining.text ? 'partial' : 'full';
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lib/plugins/escape_hatches/Label_MA.ts` around lines 53 - 70, The code sets
result.decoder.decodeLevel = 'full' before calling plugin.decoder.decode, which
can mislabel results when the inner decode fails or leaves remaining text;
remove the early assignment and instead set decodeLevel after the
decode/fallback outcome: call plugin.decoder.decode(...) as-is, then if
decoded.decoded is true and decoded.remaining is empty set
result.decoder.decodeLevel = 'full', else set it to 'partial' (or leave unset)
and ensure ResultFormatter.text(result, messageText) and result.remaining = {
text: messageText } paths also set decodeLevel appropriately; update references
around plugin.decoder.decode, decoded, ResultFormatter.text, and
result.remaining accordingly.
lib/plugins/escape_hatches/Label_H1_WRN.ts-25-33 (1)

25-33: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Split '/WN' only once to avoid truncating warning text.

Line 25 uses split('/WN'), but later logic only reads parts[1]. If the warning body itself contains '/WN', content after the second marker is lost.

Suggested fix
-  const parts = message.text.split('/WN');
-
-  if (parts.length > 1) {
-    const fields = parts[0].split('/');
+  const wnIndex = message.text.indexOf('/WN');
+
+  if (wnIndex !== -1) {
+    const header = message.text.slice(0, wnIndex);
+    const body = message.text.slice(wnIndex + 3);
+    const fields = header.split('/');
     ResultFormatter.unknownArr(result, fields.slice(1), '/');
 
-    const data = parts[1].substring(0, 20);
-    const msg = parts[1].substring(20);
+    const data = body.substring(0, 20);
+    const msg = body.substring(20);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lib/plugins/escape_hatches/Label_H1_WRN.ts` around lines 25 - 33, The current
split on message.text using '/WN' can produce multiple parts and truncate
warning text; change the logic so you only split at the first '/WN' (e.g., use a
split with limit of 2 or indexOf to find the first marker) so parts[1] contains
the full warning body. Update the handling around parts, data, msg, datetime and
keep ResultFormatter.unknownArr(result, fields.slice(1), '/') intact; ensure you
derive data/msg/datetime from the single post-marker substring rather than
assuming no further '/WN' occurrences.
lib/plugins/escape_hatches/Label_16_N_Space.ts-19-24 (1)

19-24: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Reject NaN numeric parses before marking the message decoded.

Current flow can set decoded=true with NaN latitude/longitude/altitude. Add finite-number validation and fall back to unknown when parsing fails.

💡 Proposed fix
   if (results?.groups) {
@@
-    const pos = {
-      latitude:
-        Number(results.groups.lat_coord) *
-        (results.groups.lat == 'N' ? 1 : -1),
-      longitude:
-        Number(results.groups.long_coord) *
-        (results.groups.long == 'E' ? 1 : -1),
-    };
+    const lat = Number(results.groups.lat_coord);
+    const lon = Number(results.groups.long_coord);
+    if (!Number.isFinite(lat) || !Number.isFinite(lon)) {
+      ResultFormatter.unknown(result, message.text);
+      result.decoded = false;
+      result.decoder.decodeLevel = 'none';
+      return result;
+    }
+    const pos = {
+      latitude: lat * (results.groups.lat === 'N' ? 1 : -1),
+      longitude: lon * (results.groups.long === 'E' ? 1 : -1),
+    };
     const altitude =
-      results.groups.alt == 'GRD' || results.groups.alt == '***'
+      results.groups.alt === 'GRD' || results.groups.alt === '***'
         ? 0
         : Number(results.groups.alt);
+    if (!Number.isFinite(altitude)) {
+      ResultFormatter.unknown(result, message.text);
+      result.decoded = false;
+      result.decoder.decodeLevel = 'none';
+      return result;
+    }
@@
   if (results?.groups) {
@@
-    const pos = {
-      latitude:
-        Number(results.groups.lat_coord) *
-        (results.groups.lat == 'N' ? 1 : -1),
-      longitude:
-        Number(results.groups.long_coord) *
-        (results.groups.long == 'E' ? 1 : -1),
-    };
+    const lat = Number(results.groups.lat_coord);
+    const lon = Number(results.groups.long_coord);
+    if (!Number.isFinite(lat) || !Number.isFinite(lon)) {
+      ResultFormatter.unknown(result, message.text);
+      result.decoded = false;
+      result.decoder.decodeLevel = 'none';
+      return result;
+    }
+    const pos = {
+      latitude: lat * (results.groups.lat === 'N' ? 1 : -1),
+      longitude: lon * (results.groups.long === 'E' ? 1 : -1),
+    };

Also applies to: 33-44, 67-74

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lib/plugins/escape_hatches/Label_16_N_Space.ts` around lines 19 - 24, When
parsing matches from the regex variants (variant1Regex and variant2Regex) make
sure to validate numeric parses for lat_coord, long_coord and alt (where
present) using a finite-number check (e.g., Number.isFinite after Number(...)
parse) and treat any NaN/Infinity as a parse failure—do not set decoded = true
unless all required numeric fields are finite; on failure, set the corresponding
fields to the unknown/fallback values (the unkwn* fields) and leave decoded
false so the message is not marked decoded. Ensure this validation is applied in
each parsing block that reads lat_coord/long_coord/alt (the same logic used for
the other regex branches) so decoded is only true for fully valid numeric
coordinates.
lib/plugins/escape_hatches/Label_16_Honeywell.ts-34-53 (1)

34-53: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Avoid overlapping waypoint2 with the trailing unknown suffix.

waypoint2 is read from a fixed slice before the 2-char trailer is removed, so short waypoint payloads can incorrectly classify the trailer as a second waypoint.

💡 Proposed fix
-    if (between.charAt(17) === '-') {
+    if (between.charAt(17) === '-') {
       // Waypoint mode
-      const waypoint1 = between.substring(18, 23).trim();
-      const waypoint2 = between.substring(23, 28).trim();
+      const waypointPayload = between.slice(18, -2);
+      const waypoint1 = waypointPayload.substring(0, 5).trim();
+      const waypoint2 = waypointPayload.substring(5, 10).trim();
       if (waypoint2) {
         ResultFormatter.route(result, {
           waypoints: [{ name: waypoint1 }, { name: waypoint2 }],
         });
       } else {
         ResultFormatter.route(result, {
           waypoints: [{ name: waypoint1 }],
         });
       }
@@
-    ResultFormatter.unknown(result, between.substring(between.length - 2), '');
+    ResultFormatter.unknown(result, between.slice(-2), '');
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lib/plugins/escape_hatches/Label_16_Honeywell.ts` around lines 34 - 53, The
waypoint parsing can include the 2-char trailing unknown suffix in waypoint2
because substrings use fixed end indexes; update the slice bounds so waypoint1
and waypoint2 are taken before the trailing suffix (use between.substring(18,
23).trim() for waypoint1 and between.substring(23, between.length - 2).trim()
for waypoint2) and keep the existing logic that only calls ResultFormatter.route
with two waypoints when waypoint2 is non-empty; leave the final
ResultFormatter.unknown using between.substring(between.length - 2) as-is.
lib/plugins/escape_hatches/Label_21_POS.ts-64-70 (1)

64-70: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Fix the validation predicate in processPosition.

The current && chain only rejects input when all checks fail, so many malformed position strings still get parsed and can produce wrong coordinates. This should reject when any required condition fails.

Suggested fix
-  if (
-    value.length !== 16 &&
-    value[0] !== 'N' &&
-    value[0] !== 'S' &&
-    value[8] !== 'W' &&
-    value[8] !== 'E'
-  ) {
+  if (
+    value.length !== 16 ||
+    (value[0] !== 'N' && value[0] !== 'S') ||
+    (value[8] !== 'W' && value[8] !== 'E')
+  ) {
     return;
   }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lib/plugins/escape_hatches/Label_21_POS.ts` around lines 64 - 70, The
validation in processPosition uses && so it only rejects when all checks fail;
change it to reject if any required condition fails by using || between the main
checks and grouping alternatives for each character test, e.g. test length with
value.length !== 16 || (value[0] !== 'N' && value[0] !== 'S') || (value[8] !==
'W' && value[8] !== 'E') so the function (processPosition) correctly returns on
any invalid part of value.
🟡 Minor comments (6)
lib/plugins/escape_hatches/Label_SQ.ts-107-113 (1)

107-113: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Preserve valid zero-valued numeric fields in formatter conditions.

Line 107 and Line 125 use truthy checks, so 0 values are treated as absent and omitted from formatted output. Use explicit numeric/undefined checks instead.

💡 Suggested patch
-    if (gs.coordinates.latitude) {
+    if (
+      typeof gs.coordinates.latitude === 'number' &&
+      typeof gs.coordinates.longitude === 'number'
+    ) {
       result.formatted.items.push({
         type: 'coordinates',
         code: 'COORD',
         label: 'Ground Station Location',
         value: `${gs.coordinates.latitude}, ${gs.coordinates.longitude}`,
       });
     }
@@
-  if (result.raw.vdlFrequency) {
+  if (typeof result.raw.vdlFrequency === 'number') {
     result.formatted.items.push({
       type: 'vdlFrequency',
       code: 'VDLFRQ',
       label: 'VDL Frequency',
       value: `${result.raw.vdlFrequency} MHz`,
     });
   }

Also applies to: 125-131

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lib/plugins/escape_hatches/Label_SQ.ts` around lines 107 - 113, The formatter
currently uses truthy checks (e.g., if (gs.coordinates.latitude)) that drop
valid zero values; update the conditional logic in the Label_SQ formatter where
it inspects gs.coordinates.latitude and gs.coordinates.longitude (and the
similar checks in the block covering the 125-131 range) to explicitly test for
numeric presence (e.g., typeof x === 'number' or Number.isFinite(x') or x !==
undefined && x !== null) before pushing the coordinates item into
result.formatted.items so 0 values are preserved.
lib/plugins/escape_hatches/Label_ColonComma.ts-12-23 (1)

12-23: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Validate frequency before writing formatted output and decode status.

This path currently accepts non-numeric input, emits NaN MHz, and still reports a full decode.

Suggested fix
-  result.raw.frequency = Number(message.text) / 1000;
+  const frequency = Number(message.text) / 1000;
+  if (!Number.isFinite(frequency)) {
+    result.decoded = false;
+    result.decoder.decodeLevel = 'none';
+    return result;
+  }
+  result.raw.frequency = frequency;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lib/plugins/escape_hatches/Label_ColonComma.ts` around lines 12 - 23, The
code assigns result.raw.frequency = Number(message.text) / 1000 without
validating the input, which lets non-numeric values produce NaN and still marks
the message as fully decoded; modify the logic around the result.raw.frequency
assignment and the block that pushes into result.formatted.items and sets
result.decoded/result.decoder.decodeLevel so that you first parse and validate
the numeric value (e.g., const freq = Number(message.text); check
Number.isFinite(freq)), only calculate freq/1000 and push the frequency item
when valid, and only set result.decoded = true and result.decoder.decodeLevel =
'full' in that valid branch; for invalid input, avoid pushing the 'frequency'
formatted item and either leave result.decoded false or push an
error/invalid-item to result.formatted.items.
lib/plugins/escape_hatches/Label_80.ts-100-113 (1)

100-113: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Validate POS regex extraction before emitting coordinates.

When val doesn't match the POS format, this path emits NaN lat/lon and still contributes to a successful decode.

Suggested fix
     case 'POS': {
       // don't use decodeStringCoordinates because of different position format
       const posRegex = /^(?<latd>[NS])(?<lat>.+)(?<lngd>[EW])(?<lng>.+)/;
       const posResult = val.match(posRegex);
+      if (!posResult?.groups) {
+        ResultFormatter.unknown(results, part, '/');
+        break;
+      }
       const lat =
-        Number(posResult?.groups?.lat) *
-        (posResult?.groups?.latd === 'S' ? -1 : 1);
+        Number(posResult.groups.lat) * (posResult.groups.latd === 'S' ? -1 : 1);
       const lon =
-        Number(posResult?.groups?.lng) *
-        (posResult?.groups?.lngd === 'W' ? -1 : 1);
+        Number(posResult.groups.lng) * (posResult.groups.lngd === 'W' ? -1 : 1);
+      if (!Number.isFinite(lat) || !Number.isFinite(lon)) {
+        ResultFormatter.unknown(results, part, '/');
+        break;
+      }
       const position = {
         latitude: Number.isInteger(lat) ? lat / 1000 : lat / 100,
         longitude: Number.isInteger(lon) ? lon / 1000 : lon / 100,
       };
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lib/plugins/escape_hatches/Label_80.ts` around lines 100 - 113, The POS
parsing code uses posResult without validating the regex match, so when val
doesn't match posRegex you end up emitting NaN coordinates; update the block
that builds posResult (using posRegex and val) to check that posResult and
posResult.groups exist and that the expected groups (latd, lat, lngd, lng) are
present and valid before computing lat/lon and calling
ResultFormatter.position—if the match fails, skip emitting coordinates (or mark
the decode as invalid/return early) to avoid adding NaN values. Ensure you
reference the existing symbols posRegex, posResult, val, position, and
ResultFormatter.position when making the change.
lib/plugins/escape_hatches/Label_B6_Forwardslash.ts-14-16 (1)

14-16: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Log message text instead of object coercion in debug output.

Current output is effectively CPDLC: [object Object], which obscures the payload during debugging.

Suggested fix
   if (options.debug) {
-    console.log('CPDLC: ' + message);
+    console.log('CPDLC: ' + message.text);
   }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lib/plugins/escape_hatches/Label_B6_Forwardslash.ts` around lines 14 - 16,
The debug log currently concatenates the object (options.debug block) causing
"[object Object]" output; update the debug logging in Label_B6_Forwardslash.ts
(the options.debug branch that logs 'CPDLC: ' + message) to serialize the
payload instead of coercing it—use JSON.stringify(message, null, 2) or
util.inspect(message) and log that (e.g., prefix with "CPDLC: " and append the
serialized string) so the actual message content is visible during debugging.
lib/plugins/escape_hatches/Label_H1_OHMA.ts-12-15 (1)

12-15: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Avoid serializing missing OHMA payload as "undefined"/"null".

When the hatch receives a nullish value, current conversion emits literal strings that look like real payload.

Suggested fix
 export function ohma_unwrap_message(value: unknown, _args: Record<string, unknown>): unknown {
   // Preserve original behavior: raw.ohma is the raw inflated JSON text.
-  return typeof value === 'string' ? value : String(value);
+  return typeof value === 'string' ? value : String(value ?? '');
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lib/plugins/escape_hatches/Label_H1_OHMA.ts` around lines 12 - 15, The
ohma_unwrap_message function currently converts nullish values into the literal
strings "undefined"/"null"; update it to first check for value === null || value
=== undefined and return the value as-is (preserving actual null/undefined for
raw.ohma), otherwise keep the existing behavior (if typeof value === 'string'
return it, else return String(value)); change only the branching in
ohma_unwrap_message to avoid serializing missing OHMA payloads.
lib/plugins/escape_hatches/Label_1L_3Line.ts-22-33 (1)

22-33: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Handle both \r\n and \n line endings.

The current gate only accepts CRLF, so LF-only 3-line messages are treated as unknown.

💡 Proposed fix
-  const lines = message.text.split('\r\n');
+  const normalized = message.text.replace(/\r?\n/g, '\n');
+  const lines = normalized.split('\n');
@@
-  const parts = message.text.replaceAll('\r\n', '/').split('/');
+  const parts = normalized.replaceAll('\n', '/').split('/');
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lib/plugins/escape_hatches/Label_1L_3Line.ts` around lines 22 - 33, The check
for three lines currently only splits on '\r\n' so LF-only messages fail; update
the logic in the block that sets lines, parts and data to normalize or split on
both CRLF and LF (e.g., normalize message.text by replacing '\r\n' with '\n'
then split on '\n', and when building parts use the same normalized text to join
with '/'), keeping existing behavior for options.debug logging and setting
result.remaining.text, result.decoded, and result.decoder.decodeLevel when the
length is not 3; ensure you update the variables referenced (lines, parts,
message.text) so both CRLF and LF 3-line messages are accepted.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: fa7fafc7-26bc-4531-9952-1533ba9c64f3

📥 Commits

Reviewing files that changed from the base of the PR and between 9a65f7a and cefb6d4.

⛔ Files ignored due to path filters (11)
  • lib/plugins/generated/CBand.ts is excluded by !**/generated/**
  • lib/plugins/generated/Label_13Through18_Slash.ts is excluded by !**/generated/**
  • lib/plugins/generated/Label_1L_3Line.ts is excluded by !**/generated/**
  • lib/plugins/generated/Label_44_ETA.ts is excluded by !**/generated/**
  • lib/plugins/generated/Label_44_IN.ts is excluded by !**/generated/**
  • lib/plugins/generated/Label_44_OFF.ts is excluded by !**/generated/**
  • lib/plugins/generated/Label_44_ON.ts is excluded by !**/generated/**
  • lib/plugins/generated/Label_44_POS.ts is excluded by !**/generated/**
  • lib/plugins/generated/Label_4A.ts is excluded by !**/generated/**
  • lib/plugins/generated/Label_ColonComma.ts is excluded by !**/generated/**
  • lib/plugins/generated/Label_H1_StarPOS.ts is excluded by !**/generated/**
📒 Files selected for processing (66)
  • lib/MessageDecoder.ts
  • lib/plugins/escape_hatches/ARINC_702.ts
  • lib/plugins/escape_hatches/CBand.ts
  • lib/plugins/escape_hatches/Label_10_LDR.ts
  • lib/plugins/escape_hatches/Label_10_Slash.ts
  • lib/plugins/escape_hatches/Label_12_N_Space.ts
  • lib/plugins/escape_hatches/Label_12_POS.ts
  • lib/plugins/escape_hatches/Label_13Through18_Slash.ts
  • lib/plugins/escape_hatches/Label_15.ts
  • lib/plugins/escape_hatches/Label_15_FST.ts
  • lib/plugins/escape_hatches/Label_16_AUTPOS.ts
  • lib/plugins/escape_hatches/Label_16_Honeywell.ts
  • lib/plugins/escape_hatches/Label_16_N_Space.ts
  • lib/plugins/escape_hatches/Label_16_POSA1.ts
  • lib/plugins/escape_hatches/Label_16_TOD.ts
  • lib/plugins/escape_hatches/Label_1L_070.ts
  • lib/plugins/escape_hatches/Label_1L_3Line.ts
  • lib/plugins/escape_hatches/Label_1L_660.ts
  • lib/plugins/escape_hatches/Label_1L_Slash.ts
  • lib/plugins/escape_hatches/Label_1M_Slash.ts
  • lib/plugins/escape_hatches/Label_20_CFB01.ts
  • lib/plugins/escape_hatches/Label_20_POS.ts
  • lib/plugins/escape_hatches/Label_21_POS.ts
  • lib/plugins/escape_hatches/Label_22_OFF.ts
  • lib/plugins/escape_hatches/Label_22_POS.ts
  • lib/plugins/escape_hatches/Label_24_Slash.ts
  • lib/plugins/escape_hatches/Label_2P_FM3.ts
  • lib/plugins/escape_hatches/Label_2P_FM4.ts
  • lib/plugins/escape_hatches/Label_2P_FM5.ts
  • lib/plugins/escape_hatches/Label_30_Slash_EA.ts
  • lib/plugins/escape_hatches/Label_44_POS.ts
  • lib/plugins/escape_hatches/Label_44_Slash.ts
  • lib/plugins/escape_hatches/Label_4A.ts
  • lib/plugins/escape_hatches/Label_4A_01.ts
  • lib/plugins/escape_hatches/Label_4A_DIS.ts
  • lib/plugins/escape_hatches/Label_4A_DOOR.ts
  • lib/plugins/escape_hatches/Label_4A_Slash_01.ts
  • lib/plugins/escape_hatches/Label_4N.ts
  • lib/plugins/escape_hatches/Label_4T_AGFSR.ts
  • lib/plugins/escape_hatches/Label_4T_ETA.ts
  • lib/plugins/escape_hatches/Label_58.ts
  • lib/plugins/escape_hatches/Label_5Z_Slash.ts
  • lib/plugins/escape_hatches/Label_80.ts
  • lib/plugins/escape_hatches/Label_83.ts
  • lib/plugins/escape_hatches/Label_8E.ts
  • lib/plugins/escape_hatches/Label_B6_Forwardslash.ts
  • lib/plugins/escape_hatches/Label_ColonComma.ts
  • lib/plugins/escape_hatches/Label_H1_ATIS.ts
  • lib/plugins/escape_hatches/Label_H1_EZF.ts
  • lib/plugins/escape_hatches/Label_H1_FLR.ts
  • lib/plugins/escape_hatches/Label_H1_M_POS.ts
  • lib/plugins/escape_hatches/Label_H1_OFP.ts
  • lib/plugins/escape_hatches/Label_H1_OHMA.ts
  • lib/plugins/escape_hatches/Label_H1_Paren.ts
  • lib/plugins/escape_hatches/Label_H1_StarPOS.ts
  • lib/plugins/escape_hatches/Label_H1_WRN.ts
  • lib/plugins/escape_hatches/Label_H2_02E.ts
  • lib/plugins/escape_hatches/Label_HX.ts
  • lib/plugins/escape_hatches/Label_MA.ts
  • lib/plugins/escape_hatches/Label_QP.ts
  • lib/plugins/escape_hatches/Label_QQ.ts
  • lib/plugins/escape_hatches/Label_QR.ts
  • lib/plugins/escape_hatches/Label_QS.ts
  • lib/plugins/escape_hatches/Label_SQ.ts
  • lib/plugins/escape_hatches/index.ts
  • vendor/airframes-decoder
✅ Files skipped from review due to trivial changes (1)
  • lib/plugins/escape_hatches/Label_4N.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • vendor/airframes-decoder

Comment on lines +21 to +52
if (results) {
if (options.debug) {
console.log('Label 1M ETA: results');
console.log(results);
}

result.raw.flight_number = results[0];
// results[1]: ETA01 (???)
// results[2]: 230822 - UTC date of eta
ResultFormatter.departureAirport(result, results[3]);
ResultFormatter.arrivalAirport(result, results[4]);
ResultFormatter.alternateAirport(result, results[5]);
// results[6]: 2JK0 (???)
// results[7] 1940 - UTC eta
ResultFormatter.arrivalRunway(result, results[8].replace(results[4], '')); // results[8] EGLL27L
// results[9]: 10(space) (???)

const yymmdd = results[2];
ResultFormatter.eta(
result,
DateTimeUtils.convertDateTimeToEpoch(
results[7] + '00',
yymmdd.substring(4, 6) +
yymmdd.substring(2, 4) +
yymmdd.substring(0, 2),
),
);
}

result.decoded = true;
result.decoder.decodeLevel = 'partial';

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

Add a required-field guard before indexed access and success marking.

if (results) is always true, so malformed payloads can hit results[8].replace(...) and throw. The decoder also marks success even when parsing is incomplete.

Suggested fix
-  if (results) {
+  if (results.length >= 9 && results[2] && results[4] && results[7] && results[8]) {
@@
-  }
-
-  result.decoded = true;
-  result.decoder.decodeLevel = 'partial';
+    result.decoded = true;
+    result.decoder.decodeLevel = 'partial';
+  } else {
+    plugin.setDecodeLevel(result, false);
+  }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (results) {
if (options.debug) {
console.log('Label 1M ETA: results');
console.log(results);
}
result.raw.flight_number = results[0];
// results[1]: ETA01 (???)
// results[2]: 230822 - UTC date of eta
ResultFormatter.departureAirport(result, results[3]);
ResultFormatter.arrivalAirport(result, results[4]);
ResultFormatter.alternateAirport(result, results[5]);
// results[6]: 2JK0 (???)
// results[7] 1940 - UTC eta
ResultFormatter.arrivalRunway(result, results[8].replace(results[4], '')); // results[8] EGLL27L
// results[9]: 10(space) (???)
const yymmdd = results[2];
ResultFormatter.eta(
result,
DateTimeUtils.convertDateTimeToEpoch(
results[7] + '00',
yymmdd.substring(4, 6) +
yymmdd.substring(2, 4) +
yymmdd.substring(0, 2),
),
);
}
result.decoded = true;
result.decoder.decodeLevel = 'partial';
if (results.length >= 9 && results[2] && results[4] && results[7] && results[8]) {
if (options.debug) {
console.log('Label 1M ETA: results');
console.log(results);
}
result.raw.flight_number = results[0];
// results[1]: ETA01 (???)
// results[2]: 230822 - UTC date of eta
ResultFormatter.departureAirport(result, results[3]);
ResultFormatter.arrivalAirport(result, results[4]);
ResultFormatter.alternateAirport(result, results[5]);
// results[6]: 2JK0 (???)
// results[7] 1940 - UTC eta
ResultFormatter.arrivalRunway(result, results[8].replace(results[4], '')); // results[8] EGLL27L
// results[9]: 10(space) (???)
const yymmdd = results[2];
ResultFormatter.eta(
result,
DateTimeUtils.convertDateTimeToEpoch(
results[7] + '00',
yymmdd.substring(4, 6) +
yymmdd.substring(2, 4) +
yymmdd.substring(0, 2),
),
);
result.decoded = true;
result.decoder.decodeLevel = 'partial';
} else {
plugin.setDecodeLevel(result, false);
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lib/plugins/escape_hatches/Label_1M_Slash.ts` around lines 21 - 52, The code
accesses indexed fields on results (e.g., results[8].replace(...), results[7],
results[2]) without verifying they exist and then unconditionally marks parsing
as successful; add a guard that ensures results is an array and has the required
indexes (at least 0,2,3,4,5,7,8) and that results[8] is a string before calling
ResultFormatter.arrivalRunway and DateTimeUtils.convertDateTimeToEpoch, only
call ResultFormatter.* methods and set result.decoded/result.decoder.decodeLevel
= 'partial' after those validations succeed, and if any required field is
missing bail out or set an appropriate failure state instead of marking success;
use the existing identifiers (results, ResultFormatter.arrivalRunway,
ResultFormatter.eta, DateTimeUtils.convertDateTimeToEpoch, result.decoded,
result.decoder.decodeLevel) to locate and update the logic.

Comment on lines +36 to +52
const header = parts[0].split('FM3 ');
if (header.length == 0) {
// can't use preambles, as there can be info before `FM4`
// so let's check if we want to decode it here
ResultFormatter.unknown(result, message.text);
result.decoded = false;
result.decoder.decodeLevel = 'none';
return result;
}

if (header[0].length > 0) {
ResultFormatter.unknown(result, header[0].substring(0, 4));
ResultFormatter.flightNumber(result, header[0].substring(4));
}

if (header[1].length === 4) {
ResultFormatter.timestamp(

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

Fix impossible header guard to prevent undefined dereference.

split('FM3 ') never yields length == 0. When FM3 is absent, header[1] is undefined and header[1].length throws.

Suggested fix
   const header = parts[0].split('FM3 ');
-  if (header.length == 0) {
+  if (header.length < 2) {
     // can't use preambles, as there can be info before `FM4`
     // so let's check if we want to decode it here
     ResultFormatter.unknown(result, message.text);
     result.decoded = false;
     result.decoder.decodeLevel = 'none';
     return result;
   }
@@
-  if (header[1].length === 4) {
+  if (header[1].length === 4) {
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lib/plugins/escape_hatches/Label_2P_FM3.ts` around lines 36 - 52, The current
guard header.length == 0 is impossible after split('FM3 ') and leads to
header[1] being undefined; update the logic in the block handling header
(variable name header created from parts[0].split('FM3 ')) to first check
whether header.length < 2 or header[1] is undefined and treat that case like the
absent-FM3 path (call ResultFormatter.unknown(result, message.text), set
result.decoded = false and result.decoder.decodeLevel = 'none' and return). Only
after confirming header.length >= 2 proceed to use header[0] (for
ResultFormatter.unknown and ResultFormatter.flightNumber) and safely test
header[1].length before calling ResultFormatter.timestamp.

Comment on lines +40 to +52
ResultFormatter.eta(
result,
DateTimeUtils.convertHHMMSSToTod(results[0].substr(2, 4)),
);

if (results[1].substring(0, 2) === 'DS') {
ResultFormatter.arrivalAirport(result, results[1].substring(2, 6));
ResultFormatter.unknown(result, '/'.concat(results[2]));
} else {
ResultFormatter.unknown(
result,
'/'.concat(results[1], '/', results[2]),
);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

Add segment-count validation before indexing results[0..2].

This code can throw on malformed /EA payloads because results[1]/results[2] are accessed unguarded.

Suggested patch
   const results = message.text.split(/\n|\//).slice(1); // Split by / and new line
+  if (results.length < 3 || !results[0]) {
+    ResultFormatter.unknown(result, message.text);
+    result.decoded = false;
+    result.decoder.decodeLevel = 'none';
+    return result;
+  }

   ResultFormatter.eta(
     result,
     DateTimeUtils.convertHHMMSSToTod(results[0].substr(2, 4)),
   );
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lib/plugins/escape_hatches/Label_30_Slash_EA.ts` around lines 40 - 52,
Validate that the parsed `results` array has at least 3 segments before
accessing results[0], results[1], or results[2]; if the count is insufficient,
handle gracefully (e.g., log/mark unknown or return early) instead of indexing
into `results`. In the block around ResultFormatter.eta /
DateTimeUtils.convertHHMMSSToTod and the conditional that calls
ResultFormatter.arrivalAirport or ResultFormatter.unknown, add a guard checking
results.length >= 3 and only perform the existing calls (ResultFormatter.eta,
ResultFormatter.arrivalAirport, ResultFormatter.unknown) when that check passes;
otherwise invoke a safe fallback (e.g., ResultFormatter.unknown with the raw
payload or skip ETA conversion) to avoid throws on malformed `/EA` payloads.

Comment on lines +29 to +35
if (parts[2] === 'LOCATION') {
const latdir = parts[3].substring(0, 1);
const latdeg = Number(parts[3].substring(1, 3));
const latmin = Number(parts[3].substring(3, 7));
const londir = parts[4].substring(0, 1);
const londeg = Number(parts[4].substring(1, 4));
const lonmin = Number(parts[4].substring(4, 8));

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

Guard LOCATION token access before substring calls.

Lines 30–35 assume parts[3] and parts[4] exist. Short/malformed LOCATION messages will throw and abort decoding.

Suggested fix
-  if (parts[2] === 'LOCATION') {
+  if (parts[2] === 'LOCATION' && parts.length >= 5) {
@@
-  } else if (parts[2] === '43') {
+  } else if (parts[2] === '43' && parts.length >= 4) {
     ResultFormatter.departureAirport(result, parts[3]);
     ResultFormatter.unknownArr(result, parts.slice(4), ' ');
   } else {
     result.decoded = false;
   }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lib/plugins/escape_hatches/Label_HX.ts` around lines 29 - 35, The LOCATION
parsing block in Label_HX.ts assumes parts[3] and parts[4] exist and have
sufficient length before calling substring; update the guard in the function
handling LOCATION (the code that sets latdir/latdeg/latmin and
londir/londeg/lonmin) to first verify parts[3] and parts[4] are defined and have
the expected minimum length (e.g., at least 8 chars for lat and lon) and
gracefully handle malformed tokens (return an error, skip parsing, or default
values) so substring() is never called on undefined/short strings.

@makrsmark

Copy link
Copy Markdown
Collaborator

I guess i'm ok with the submodule approach. My big thing is generated code should stay out of source contro

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants