Wire up ADS submodule, codegen, runtime path aliases (no behavioral swap yet)#445
Wire up ADS submodule, codegen, runtime path aliases (no behavioral swap yet)#445kevinelliott wants to merge 7 commits into
Conversation
…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>
WalkthroughAdds 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. ChangesADS Decoder Submodule Integration
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Possibly related PRs
Suggested reviewers
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
|
| 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"; |
There was a problem hiding this comment.
💡 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"; |
There was a problem hiding this comment.
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 👍 / 👎.
| "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)" |
There was a problem hiding this comment.
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 👍 / 👎.
There was a problem hiding this comment.
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-decoderas 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.
| "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)" |
| jobs: | ||
| ads-generated-up-to-date: | ||
| uses: airframesio/acars-decoder/.github/workflows/codegen-check.yml@init/ads-v1 | ||
| with: |
There was a problem hiding this comment.
Actionable comments posted: 4
🧹 Nitpick comments (1)
package.json (1)
19-19: ⚡ Quick winPrefer
npm cifor reproducible codegen builds.Using
npm installmay produce non-deterministic builds ifpackage-lock.jsonis out of sync. Since this installs dependencies for the vendored codegen tool,npm ciwill 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
⛔ Files ignored due to path filters (68)
lib/plugins/generated/ARINC_702.tsis excluded by!**/generated/**lib/plugins/generated/CBand.tsis excluded by!**/generated/**lib/plugins/generated/Label_10_LDR.tsis excluded by!**/generated/**lib/plugins/generated/Label_10_POS.tsis excluded by!**/generated/**lib/plugins/generated/Label_10_Slash.tsis excluded by!**/generated/**lib/plugins/generated/Label_12_N_Space.tsis excluded by!**/generated/**lib/plugins/generated/Label_12_POS.tsis excluded by!**/generated/**lib/plugins/generated/Label_13Through18_Slash.tsis excluded by!**/generated/**lib/plugins/generated/Label_15.tsis excluded by!**/generated/**lib/plugins/generated/Label_15_FST.tsis excluded by!**/generated/**lib/plugins/generated/Label_16_AUTPOS.tsis excluded by!**/generated/**lib/plugins/generated/Label_16_Honeywell.tsis excluded by!**/generated/**lib/plugins/generated/Label_16_N_Space.tsis excluded by!**/generated/**lib/plugins/generated/Label_16_POSA1.tsis excluded by!**/generated/**lib/plugins/generated/Label_16_TOD.tsis excluded by!**/generated/**lib/plugins/generated/Label_1L_070.tsis excluded by!**/generated/**lib/plugins/generated/Label_1L_3Line.tsis excluded by!**/generated/**lib/plugins/generated/Label_1L_660.tsis excluded by!**/generated/**lib/plugins/generated/Label_1L_Slash.tsis excluded by!**/generated/**lib/plugins/generated/Label_1M_Slash.tsis excluded by!**/generated/**lib/plugins/generated/Label_20_CFB01.tsis excluded by!**/generated/**lib/plugins/generated/Label_20_POS.tsis excluded by!**/generated/**lib/plugins/generated/Label_21_POS.tsis excluded by!**/generated/**lib/plugins/generated/Label_22_OFF.tsis excluded by!**/generated/**lib/plugins/generated/Label_22_POS.tsis excluded by!**/generated/**lib/plugins/generated/Label_24_Slash.tsis excluded by!**/generated/**lib/plugins/generated/Label_2P_FM3.tsis excluded by!**/generated/**lib/plugins/generated/Label_2P_FM4.tsis excluded by!**/generated/**lib/plugins/generated/Label_2P_FM5.tsis excluded by!**/generated/**lib/plugins/generated/Label_30_Slash_EA.tsis excluded by!**/generated/**lib/plugins/generated/Label_44_ETA.tsis excluded by!**/generated/**lib/plugins/generated/Label_44_IN.tsis excluded by!**/generated/**lib/plugins/generated/Label_44_OFF.tsis excluded by!**/generated/**lib/plugins/generated/Label_44_ON.tsis excluded by!**/generated/**lib/plugins/generated/Label_44_POS.tsis excluded by!**/generated/**lib/plugins/generated/Label_44_Slash.tsis excluded by!**/generated/**lib/plugins/generated/Label_4A.tsis excluded by!**/generated/**lib/plugins/generated/Label_4A_01.tsis excluded by!**/generated/**lib/plugins/generated/Label_4A_DIS.tsis excluded by!**/generated/**lib/plugins/generated/Label_4A_DOOR.tsis excluded by!**/generated/**lib/plugins/generated/Label_4A_Slash_01.tsis excluded by!**/generated/**lib/plugins/generated/Label_4N.tsis excluded by!**/generated/**lib/plugins/generated/Label_4T_AGFSR.tsis excluded by!**/generated/**lib/plugins/generated/Label_4T_ETA.tsis excluded by!**/generated/**lib/plugins/generated/Label_58.tsis excluded by!**/generated/**lib/plugins/generated/Label_5Z_Slash.tsis excluded by!**/generated/**lib/plugins/generated/Label_80.tsis excluded by!**/generated/**lib/plugins/generated/Label_83.tsis excluded by!**/generated/**lib/plugins/generated/Label_8E.tsis excluded by!**/generated/**lib/plugins/generated/Label_B6_Forwardslash.tsis excluded by!**/generated/**lib/plugins/generated/Label_ColonComma.tsis excluded by!**/generated/**lib/plugins/generated/Label_H1_ATIS.tsis excluded by!**/generated/**lib/plugins/generated/Label_H1_EZF.tsis excluded by!**/generated/**lib/plugins/generated/Label_H1_FLR.tsis excluded by!**/generated/**lib/plugins/generated/Label_H1_M_POS.tsis excluded by!**/generated/**lib/plugins/generated/Label_H1_OFP.tsis excluded by!**/generated/**lib/plugins/generated/Label_H1_OHMA.tsis excluded by!**/generated/**lib/plugins/generated/Label_H1_Paren.tsis excluded by!**/generated/**lib/plugins/generated/Label_H1_StarPOS.tsis excluded by!**/generated/**lib/plugins/generated/Label_H1_WRN.tsis excluded by!**/generated/**lib/plugins/generated/Label_H2_02E.tsis excluded by!**/generated/**lib/plugins/generated/Label_HX.tsis excluded by!**/generated/**lib/plugins/generated/Label_MA.tsis excluded by!**/generated/**lib/plugins/generated/Label_QP.tsis excluded by!**/generated/**lib/plugins/generated/Label_QQ.tsis excluded by!**/generated/**lib/plugins/generated/Label_QR.tsis excluded by!**/generated/**lib/plugins/generated/Label_QS.tsis excluded by!**/generated/**lib/plugins/generated/Label_SQ.tsis excluded by!**/generated/**
📒 Files selected for processing (7)
.github/workflows/ads-check.yml.gitmoduleseslint.config.mtsjest.config.tspackage.jsontsconfig.jsonvendor/airframes-decoder
| 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 |
There was a problem hiding this comment.
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-v1Adjust 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.
| 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 |
There was a problem hiding this comment.
🧩 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 || trueRepository: 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.ymlRepository: airframesio/acars-decoder-typescript
Length of output: 1299
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "=== Full file ==="
cat -n .github/workflows/ads-check.ymlRepository: 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@6da198f085a6ae56eb0291fe938c239aef8faa93Also 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.
| testPathIgnorePatterns: [ | ||
| "/node_modules/", | ||
| "/vendor/", // skip vendored airframes-decoder submodule's tests | ||
| ], |
There was a problem hiding this comment.
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.
| 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.
| "compilerOptions": { | ||
| "target": "esNext", | ||
| "module": "commonjs", | ||
| "moduleResolution": "node", |
There was a problem hiding this comment.
🧩 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
fiRepository: 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.
There was a problem hiding this comment.
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 cifailed with an existing@typescript-eslintpeer-resolution conflict, so I installed withnpm ci --legacy-peer-depsto continue review.npm run buildpassed.npm test -- --runInBandpassed.npm run ads:codegen-buildpassed.npm run ads:generaterewrote every generated file header from the author’s local absolute path to the checkout path, andnpm run ads:checkthen failed.npx tsc --noEmitfails; many failures are pre-existing test/source strictness issues, but the newlib/plugins/generated/**files also add errors such as missing../escape_hatches, invalidResultFormatter.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.
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. | |||
There was a problem hiding this comment.
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"; |
There was a problem hiding this comment.
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.
| ResultFormatter.timestamp(result, day); | ||
| ResultFormatter.timestamp(result, timestamp); | ||
| ResultFormatter.timestamp(result, eta); | ||
| ResultFormatter.fuel(result, fuel_in_tons); |
There was a problem hiding this comment.
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.
| "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)" |
There was a problem hiding this comment.
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.
| "strict": true, | ||
| "baseUrl": ".", | ||
| "paths": { | ||
| "@airframes/ads-runtime-ts": ["vendor/airframes-decoder/runtimes/typescript/index.ts"], |
There was a problem hiding this comment.
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.
…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>
There was a problem hiding this comment.
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
⛔ Files ignored due to path filters (6)
lib/plugins/generated/Label_10_POS.tsis excluded by!**/generated/**lib/plugins/generated/Label_44_ETA.tsis excluded by!**/generated/**lib/plugins/generated/Label_44_IN.tsis excluded by!**/generated/**lib/plugins/generated/Label_44_OFF.tsis excluded by!**/generated/**lib/plugins/generated/Label_44_ON.tsis excluded by!**/generated/**lib/plugins/generated/Label_44_POS.tsis excluded by!**/generated/**
📒 Files selected for processing (4)
jest.config.tslib/MessageDecoder.tslib/plugins/escape_hatches/index.tsvendor/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
| 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", | ||
| }, |
There was a problem hiding this comment.
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.
| 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.
… 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"; |
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"; |
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"; |
There was a problem hiding this comment.
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 winFix the unreachable
FM4header guard before usingheader[1].
header.length == 0is never true forsplit, so messages missingFM4can still flow through and useheader[1]asundefinedwhile 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 winUse a real
FM5presence check before readingheader[1].The current
header.length == 0check can’t fire, so malformed headers can be treated as decoded and passundefinedinto 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 winApply 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 winValidate ETA tokenization before accessing
etaData[2].
data.length === 3is not enough; malformed third section can still crash onetaData[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 winValidate
/LOCcoordinate tokens before fixed-position indexing.The parser assumes both coordinate tokens and expected lengths are present; malformed
/LOClines can throw onlocation[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 winGuard 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 winAdd 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 winGuard 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 winAdd field-count guards for the
4DH3 ETAT2variant.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 winDo not mark unmatched
8Epayloads as fully decoded.When regex matching fails, this function still sets
decoded=trueanddecodeLevel='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 winValidate 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 winReject malformed numeric position/altitude/heading values.
parseFloat/Numberresults are not validated, so NaN values can be emitted withdecoded = 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 winHarden
/FRpayload extraction before setting decoded state.Current logic can silently accept malformed payloads (short/non-date payload) and can lose content after additional
/FRtokens.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 winAnchor latitude parsing to prevent partial matches.
parseLatcan partially match malformed 5-digit latitude strings and return incorrect coordinates instead ofNaN.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 winSet 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 winAnchor 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 winDon’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=trueand evendecodeLevel='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 winCompute
decodeLevelafter 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 winSplit
'/WN'only once to avoid truncating warning text.Line 25 uses
split('/WN'), but later logic only readsparts[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 winReject
NaNnumeric parses before marking the message decoded.Current flow can set
decoded=truewithNaNlatitude/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 winAvoid overlapping
waypoint2with the trailing unknown suffix.
waypoint2is 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 winFix 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 winPreserve valid zero-valued numeric fields in formatter conditions.
Line 107 and Line 125 use truthy checks, so
0values 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 winValidate 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 winValidate POS regex extraction before emitting coordinates.
When
valdoesn't match the POS format, this path emitsNaNlat/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 winLog 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 winAvoid 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 winHandle both
\r\nand\nline 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
⛔ Files ignored due to path filters (11)
lib/plugins/generated/CBand.tsis excluded by!**/generated/**lib/plugins/generated/Label_13Through18_Slash.tsis excluded by!**/generated/**lib/plugins/generated/Label_1L_3Line.tsis excluded by!**/generated/**lib/plugins/generated/Label_44_ETA.tsis excluded by!**/generated/**lib/plugins/generated/Label_44_IN.tsis excluded by!**/generated/**lib/plugins/generated/Label_44_OFF.tsis excluded by!**/generated/**lib/plugins/generated/Label_44_ON.tsis excluded by!**/generated/**lib/plugins/generated/Label_44_POS.tsis excluded by!**/generated/**lib/plugins/generated/Label_4A.tsis excluded by!**/generated/**lib/plugins/generated/Label_ColonComma.tsis excluded by!**/generated/**lib/plugins/generated/Label_H1_StarPOS.tsis excluded by!**/generated/**
📒 Files selected for processing (66)
lib/MessageDecoder.tslib/plugins/escape_hatches/ARINC_702.tslib/plugins/escape_hatches/CBand.tslib/plugins/escape_hatches/Label_10_LDR.tslib/plugins/escape_hatches/Label_10_Slash.tslib/plugins/escape_hatches/Label_12_N_Space.tslib/plugins/escape_hatches/Label_12_POS.tslib/plugins/escape_hatches/Label_13Through18_Slash.tslib/plugins/escape_hatches/Label_15.tslib/plugins/escape_hatches/Label_15_FST.tslib/plugins/escape_hatches/Label_16_AUTPOS.tslib/plugins/escape_hatches/Label_16_Honeywell.tslib/plugins/escape_hatches/Label_16_N_Space.tslib/plugins/escape_hatches/Label_16_POSA1.tslib/plugins/escape_hatches/Label_16_TOD.tslib/plugins/escape_hatches/Label_1L_070.tslib/plugins/escape_hatches/Label_1L_3Line.tslib/plugins/escape_hatches/Label_1L_660.tslib/plugins/escape_hatches/Label_1L_Slash.tslib/plugins/escape_hatches/Label_1M_Slash.tslib/plugins/escape_hatches/Label_20_CFB01.tslib/plugins/escape_hatches/Label_20_POS.tslib/plugins/escape_hatches/Label_21_POS.tslib/plugins/escape_hatches/Label_22_OFF.tslib/plugins/escape_hatches/Label_22_POS.tslib/plugins/escape_hatches/Label_24_Slash.tslib/plugins/escape_hatches/Label_2P_FM3.tslib/plugins/escape_hatches/Label_2P_FM4.tslib/plugins/escape_hatches/Label_2P_FM5.tslib/plugins/escape_hatches/Label_30_Slash_EA.tslib/plugins/escape_hatches/Label_44_POS.tslib/plugins/escape_hatches/Label_44_Slash.tslib/plugins/escape_hatches/Label_4A.tslib/plugins/escape_hatches/Label_4A_01.tslib/plugins/escape_hatches/Label_4A_DIS.tslib/plugins/escape_hatches/Label_4A_DOOR.tslib/plugins/escape_hatches/Label_4A_Slash_01.tslib/plugins/escape_hatches/Label_4N.tslib/plugins/escape_hatches/Label_4T_AGFSR.tslib/plugins/escape_hatches/Label_4T_ETA.tslib/plugins/escape_hatches/Label_58.tslib/plugins/escape_hatches/Label_5Z_Slash.tslib/plugins/escape_hatches/Label_80.tslib/plugins/escape_hatches/Label_83.tslib/plugins/escape_hatches/Label_8E.tslib/plugins/escape_hatches/Label_B6_Forwardslash.tslib/plugins/escape_hatches/Label_ColonComma.tslib/plugins/escape_hatches/Label_H1_ATIS.tslib/plugins/escape_hatches/Label_H1_EZF.tslib/plugins/escape_hatches/Label_H1_FLR.tslib/plugins/escape_hatches/Label_H1_M_POS.tslib/plugins/escape_hatches/Label_H1_OFP.tslib/plugins/escape_hatches/Label_H1_OHMA.tslib/plugins/escape_hatches/Label_H1_Paren.tslib/plugins/escape_hatches/Label_H1_StarPOS.tslib/plugins/escape_hatches/Label_H1_WRN.tslib/plugins/escape_hatches/Label_H2_02E.tslib/plugins/escape_hatches/Label_HX.tslib/plugins/escape_hatches/Label_MA.tslib/plugins/escape_hatches/Label_QP.tslib/plugins/escape_hatches/Label_QQ.tslib/plugins/escape_hatches/Label_QR.tslib/plugins/escape_hatches/Label_QS.tslib/plugins/escape_hatches/Label_SQ.tslib/plugins/escape_hatches/index.tsvendor/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
| 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'; | ||
|
|
There was a problem hiding this comment.
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.
| 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.
| 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( |
There was a problem hiding this comment.
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.
| 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]), | ||
| ); |
There was a problem hiding this comment.
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.
| 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)); |
There was a problem hiding this comment.
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.
|
I guess i'm ok with the submodule approach. My big thing is generated code should stay out of source contro |


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
vendor/airframes-decoderpinned toinit/ads-v1.@airframes/ads-runtime-ts→vendor/airframes-decoder/runtimes/typescript/index.ts@airframes/ads-runtime-ts/helpers→vendor/airframes-decoder/runtimes/typescript/helpers.ts@airframes/ads-runtime-ts/escape_hatches→vendor/airframes-decoder/runtimes/typescript/escape_hatches/index.tsnpm run ads:codegen-build— installs + builds the codegen toolnpm run ads:generate— emitslib/plugins/generated/*.tsfrom spec YAMLnpm run ads:check— fails iflib/plugins/generated/is out-of-datelib/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.vendor/**andlib/plugins/generated/**(avoid linting third-party code and auto-generated source)./vendor/(skip vendored submodule's own test files)..github/workflows/ads-check.ymlcalls the central reusablecodegen-check.ymlfrom 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
rawfields: it auto-emitsresult.raw.<fieldName> = valuefrom the spec'sfieldsblock AND then formatter calls write torawunder formatter-canonical keys (position,altitude, etc.). The original hand-written plugins only do the latter. Adopting the generated plugins as-is would produce extrarawentries 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/*.tshelpers 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-buildsucceedsnpm run ads:generateproduces clean output; no diff vs committedads-checkworkflow on this PR (verifies generated tree stays in sync)Related
🤖 Generated with Claude Code
Summary by CodeRabbit
Chores
Refactor
New Features