Skip to content

Bootstrap Airframes Decoder Spec (ADS) v1: spec, codegen, runtimes, 68 plugin specs#1

Open
kevinelliott wants to merge 23 commits into
mainfrom
init/ads-v1
Open

Bootstrap Airframes Decoder Spec (ADS) v1: spec, codegen, runtimes, 68 plugin specs#1
kevinelliott wants to merge 23 commits into
mainfrom
init/ads-v1

Conversation

@kevinelliott
Copy link
Copy Markdown
Contributor

@kevinelliott kevinelliott commented May 28, 2026

What is ADS?

The Airframes Decoder Spec (ADS) is a portable, declarative DSL that captures ACARS decoding semantics in a language-neutral form. It is consumed via build-time codegen by acars-decoder-typescript, acars-decoder-rust, and acars-decoder-c — and is designed so future targets (WASM, Go, Python, …) can be added at low marginal cost.

acars-decoder-typescript is in production and is the authoritative reference; behavior must match TS byte-for-byte. The shared corpus (under corpus/) enforces this across implementations.

What's in this PR (6 commits)

# Commit Summary
1 1512d3d Bootstrap: spec format, JSON Schema, codegen TS CLI, 5 reference specs, runtime/corpus layout, DSL/ADOPTION/ESCAPE_HATCHES docs, central CI + reusable codegen-check.yml workflow.
2 3b8d975 Implement Rust + C emitters; fix snake-case filename for digit-letter labels (Label_4A → label_4a).
3 04f536a Populate runtimes/typescript/ — the canonical TS helper library (DecoderPlugin, ResultFormatter, CoordinateUtils, ASCII85, compression, MIAM, ARINC 702, ICAO FPL, types). Surfaced + fixed two Buffer.from('ascii') calls the Web APIs migration missed in arinc_702_helper.ts (shipped separately as airframesio/acars-decoder-typescript#444).
4 0b312e4 Author runtimes/rust/ — the ads-runtime crate scaffold (Plugin trait, helpers, ResultFormatter, coordinate, ASCII85, CRC, types). Builds clean with cargo build.
5 f38aee7 Author runtimes/c/ — the ads_runtime_c static library scaffold (CMake, Plugin descriptor, cJSON-backed raw bag, all decode-fn helpers, ASCII85 + base64 + hex + zlib inflate, CRC, POSIX-regex). Builds clean with cmake --build ..
6 765a390 Bulk-port 63 ACARS plugins from acars-decoder-typescript into ADS YAML, bringing the spec coverage from 5 reference plugins to 68 total.

Verification

# Spec validates
cd codegen && npm install && npm run build
node dist/cli.js validate --spec ../spec
# → OK — 68 spec(s) validated.

# All three targets emit cleanly
node dist/cli.js generate --target ts   --spec ../spec --out /tmp/ads-out-ts
node dist/cli.js generate --target rust --spec ../spec --out /tmp/ads-out-rust
node dist/cli.js generate --target c    --spec ../spec --out /tmp/ads-out-c
# → Emitted 68 plugin(s) per target.

# Runtimes build clean
cd runtimes/typescript && npm install && npm run lint
cd runtimes/rust && cargo build
cd runtimes/c && mkdir build && cd build && cmake .. && cmake --build .

CI workflows: .github/workflows/ci.yml (this repo) + .github/workflows/codegen-check.yml (reusable, security-hardened, called by each language repo to verify their generated/ tree is up-to-date).

What's NOT in this PR (intentional follow-ups)

  • Shared test corpus beyond 2 sample entries — #13 extracts the ~80 test files into JSON corpus via a Jest reporter. Each language repo's CI loads the corpus and asserts deep-equality vs expected to catch divergence.
  • Stage 2 adoption in each language repo — separate PRs per repo (acars-decoder-typescript#N, acars-decoder-rust#N, acars-decoder-c#N) add the submodule, wire codegen into build, replace hand-written plugins with generated ones.
  • Stage 3 cross-impl reconciliation — runs the corpus through TS+Rust+C, diffs results, reconciles divergence.

Known v1.1 follow-ups (file as issues against this repo after merge)

  1. formatter_call.type enum is too narrow. Real TS plugins use ~40 ResultFormatter methods beyond what v1 enumerates (route, eta, currentFuel, temperature, flightNumber, runways, windData, mach, airspeed, state_change, door_event, frequency, atis, fault, warning, …). Drives the high escape-hatch ratio (59 of 63 newly-ported specs use whole-plugin escape hatches because their formatters aren't in the enum). Extending the enum lets most plugins become declarative.

  2. No DSL primitive for addRemainingFields. Plugins like Label_44_IN/ON/OFF/ETA push trailing CSV fields into remaining.text — the 4 declarative ports drop this. Add a trailing_fields_into_unknown parse step, or convert those 4 to escape hatches.

  3. POSIX regex doesn't support PCRE named groupsruntimes/c/src/regex.c resolves numeric indices only. Stage 2 should swap in PCRE2 to support (?<name>...) properly for spec like Label_44_POS.

  4. CRC tables emission. spec/shared/crc_tables.yaml declares the polynomial tables but codegen doesn't emit them yet (the bitwise impls in each runtime are correct and table-free). A v1.1 pass should generate identical lookup tables into each runtime.

  5. Typed-args codegen. Args currently flow as JSON strings (JSON.stringify(args) in emit, parse at runtime). Typed-args codegen is a perf win for hot paths.

Test plan

  • All 68 specs validate against schema/ads-v1.schema.json
  • All three emitters produce non-empty output for every spec
  • runtimes/typescript/ type-checks under strict mode (npm run lint)
  • runtimes/rust/ builds clean (cargo build)
  • runtimes/c/ builds clean as libads_runtime_c.a (cmake --build)
  • CI green (this PR triggers .github/workflows/ci.yml)
  • Reviewer spot-check: a handful of generated .ts files match the original TS plugin shape (Label_10_POS.ts, Label_44_POS.ts, ARINC_702.ts)

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Code generation tool for creating ACARS decoder plugins from YAML specifications in TypeScript, Rust, and C.
    • Multi-language runtime libraries with comprehensive helper functions for decoding, parsing, and formatting message data.
    • 50+ ACARS message decoder specifications covering diverse label types and preamble variants.
  • Documentation

    • Complete adoption guide, DSL reference, and escape hatch documentation for integrating the decoder framework.

Review Change Stack

kevinelliott and others added 6 commits May 27, 2026 21:22
Introduces the Airframes Decoder Spec (ADS) as the language-neutral source
of truth for ACARS plugin behavior, consumed via build-time codegen by
the TypeScript, Rust, and C decoder implementations (and any future target).

What's here:
- schema/ads-v1.schema.json — JSON Schema (Draft 2020-12) validating spec
  YAML files. Covers plugin metadata, qualifiers, parse steps (split,
  regex, substring, bitfield, ascii85, deflate, base64, text_decode,
  hex_decode, concat_bits, custom), fields, variants with conditional
  dispatch, checksums, formatters, and escape hatches.

- spec/labels/{10/POS,4A,44/POS,H1/OHMA}.yaml + spec/wildcards/arinc_702.yaml
  Five reference plugin specs covering the full pattern range: simple
  positional, variant dispatch, regex with named groups, binary
  encoding chain (base64→deflate→utf-8), full escape hatch.

- spec/shared/{crc_tables,decode_fns}.yaml — cross-cutting data that
  codegen emits identically into each runtime, avoiding per-language
  duplication of CRC tables and decoder semantics.

- codegen/ — TypeScript CLI (@airframes/ads-codegen) with parse, validate,
  IR, and emitters for ts/rust/c. CLI: `ads-gen generate --target X
  --spec ... --out ...`. TS emitter is functional for the five reference
  specs; Rust and C emitters are stubs filled in next.

- runtimes/{typescript,rust,c}/ — placeholder dirs for per-language
  runtime helper libraries. Stage 2 moves the existing TS lib/utils/ here
  and authors equivalents for Rust and C.

- corpus/ — shared golden test vectors (sample from Label_10_POS test)
  consumed by every language repo's CI. Source of truth for cross-impl
  parity.

- docs/{DSL,ADOPTION,ESCAPE_HATCHES}.md — spec format reference,
  per-language adoption guide, and escape-hatch conventions.

- .github/workflows/{ci,codegen-check}.yml — central CI plus a reusable
  workflow language repos call to verify their committed generated/
  trees are up-to-date with the spec.

Source of truth: acars-decoder-typescript (production). Behavior must
match TS byte-for-byte. acars-message-documentation fills documentation
gaps. See docs/DSL.md.

Plan: /Users/kevin/.claude/plans/we-want-to-take-quiet-kahan.md

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

Rust emitter targets an `ads_runtime` crate API parallel to the TS shape:
  use ads_runtime::{Plugin, Message, Options, DecodeResult, Qualifiers,
                    ResultFormatter, helpers};
  pub struct LabelXyz;
  impl Plugin for LabelXyz { fn qualifiers(...) ... fn decode(...) ... }

C emitter generates per-plugin .c + .h pairs targeting an `ads_runtime.h`
API:
  ads_decode_result_t *<snake>_decode(const ads_message_t *msg,
                                      const ads_options_t *opts);
  ads_qualifiers_t <snake>_qualifiers(void);
  extern const ads_plugin_descriptor_t <snake>_descriptor;

Both emitters cover all parse steps (split, regex, substring,
require_length, bitfield, ascii85, deflate, base64, text_decode,
hex_decode, concat_bits, custom), fields with optional `when` gating,
variant dispatch with conditional matching, formatter calls (typed + custom),
and escape-hatch delegation at parse/field/formatter levels.

Stage 2 fills in the actual runtime libraries under runtimes/{rust,c}/
matching these emitted call sites.

Filename fix in cli.ts: the previous camelCase→snake_case regex inserted
an unwanted underscore between digit and capital (Label_4A → label_4_a).
Plugin names are already snake-style with caps so a plain lowercase is
correct (Label_4A → label_4a, Label_10_POS → label_10_pos).

Verified end-to-end:
  ads-gen generate --target {ts,rust,c} produces clean, readable output
  for all 5 reference specs.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Copies the canonical TS source for: DecoderPlugin, DecoderPluginInterface,
DateTimeUtils, ResultFormatter, CoordinateUtils, ASCII85 decoder,
compression (pako wrapper + base64), MIAM PDU parser, ARINC 702 helper
with CRC tables, ICAO FPL parser, flight plan / route utils, and the
Route/Waypoint/Wind type modules.

Adds:
- index.ts — public exports: DecoderPlugin, types, ResultFormatter, utils
- helpers.ts — codegen-call adapters (coordinate, integer, float, string,
  timestampHhmmss, callsign, tailNumber, airport, base64ToUint8Array,
  inflate, textDecode, decodeAscii85, hexDecode, bitslice, concatBits).
  Behavior matches the inline plugin logic in acars-decoder-typescript
  byte-for-byte; the shared corpus enforces this.
- MessageDecoder.ts — interface for the dispatcher (decouples
  DecoderPlugin from the concrete dispatcher, which stays in the
  language repo as the public entry point).
- escape_hatches/ — placeholder index for per-plugin custom functions.
- package.json, tsconfig.json, README.md.

Latent bug fixes uncovered during type-checking under the runtime's
stricter tsconfig:

- arinc_702_helper.ts: two surviving `Buffer.from(data, 'ascii')` calls
  the Web APIs migration missed. Replaced with a hand-rolled
  asciiStringToBytes() so CRC helpers run in browser / Deno / Bun.

- miam.ts: strict-null narrowing failed after `body = decoded`
  reassignment; switched to a single-expression assignment that
  preserves narrowing.

- result_formatter.ts: sequence-number / sequence-response items
  pushed numeric values into a string-typed `value` field. Wrapped in
  String(...) to match the interface.

- flight_plan_utils.ts: two unknown-narrowing casts for plugin-specific
  raw fields (procedures, company_route).

These changes preserve runtime behavior exactly; Stage 2 will adopt this
runtime in acars-decoder-typescript via tsconfig path alias and remove
the duplicated lib/utils/ files from that repo.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Rust counterpart of runtimes/typescript/. The scaffold targets the
import shape the codegen emits:
  use ads_runtime::{Plugin, Message, Options, DecodeResult, Qualifiers,
                    ResultFormatter, helpers};

Modules:
- plugin.rs       — Plugin trait, Message/Options/Qualifiers,
                    DecodeResult (with HashMap<&str, serde_json::Value>
                    raw bag), DecodeLevel, fail_unknown()
- helpers.rs      — all decode-fn helpers the codegen calls (coordinate,
                    coordinate_decimal_minutes, integer, float, string,
                    timestamp_hhmmss, callsign, tail_number, airport,
                    base64_decode, inflate, text_decode, decode_ascii85,
                    hex_decode, bitslice, concat_bits, regex)
- coordinate.rs   — combined NS/EW DDDDD parser + DDMM.M decimal-minute
                    parser, mirroring CoordinateUtils
- ascii85.rs      — pure-Rust ASCII85 decoder (Adobe delimiters + 'z'
                    shorthand)
- crc.rs          — CRC-16/IBM-SDLC reversed-nibble + CRC-16/GENIBUS,
                    plus an match_arinc_702_crc convenience that mirrors
                    the TS algorithm-selection logic
- result_formatter.rs — ResultFormatter associated fns (position,
                    altitude, speed, heading, timestamp, callsign,
                    flight_number, tail, departure/arrival_airport, fuel,
                    unknown_arr, unknown) — output strings match TS
                    formatting
- types.rs        — Route, Waypoint, Wind structs (serde Serialize)
- escape_hatches/mod.rs — placeholder index for per-plugin custom fns

Cargo deps: base64, flate2, regex, serde, serde_json, once_cell.
Builds clean with `cargo build`.

CRC tables themselves still TODO — emitted from spec/shared/crc_tables.yaml
in a follow-up; current bitwise impl is correct and table-free.

Args passed as JSON strings (codegen emits `JSON.stringify(args)`). v1.1
should switch to typed-args codegen for efficiency.

Updates .gitignore to skip target/ and build/.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Pure-C runtime mirror of runtimes/typescript/ and runtimes/rust/. Targets
the API surface the codegen emits:
  #include "ads_runtime.h"
  #include "ads_helpers.h"
  #include "ads_escape_hatches.h"
  ads_decode_result_t *<snake>_decode(...)
  ads_qualifiers_t      <snake>_qualifiers(void)
  extern const ads_plugin_descriptor_t <snake>_descriptor;

Files:
- CMakeLists.txt           — static lib (optional shared variant), Asan
                             config, zlib detection, cjson link
- include/ads_runtime.h    — core types: Message, Options, Qualifiers,
                             DecodeResult, value/string_list/bytes, plugin
                             descriptor; result + value lifecycle API
- include/ads_helpers.h    — all decode-fn helpers, formatter helpers,
                             split/regex/substring/in primitives
- include/ads_escape_hatches.h — placeholder header
- src/plugin.c             — DecodeResult lifecycle + ads_value_*,
                             backed by cJSON for the raw bag
- src/helpers.c            — numerics, strings, identifiers, timestamps,
                             bitslice, concat_bits
- src/coordinate.c         — combined NS/EW + decimal-minutes parsers
- src/ascii85.c            — ASCII85 + base64 + hex + zlib inflate
- src/crc.c                — CRC-16 IBM-SDLC reversed-nibble + GENIBUS
- src/result_formatter.c   — ads_fmt_* item-push helpers
- src/string_list.c        — ads_split / ads_substring / ads_str_in
- src/regex.c              — POSIX-regex backed ads_regex_* (Stage 2:
                             upgrade to PCRE2 for named-capture support)

Deps: cjson (raw bag + JSON args parsing), zlib (optional, gates
ads_inflate behind ADS_HAVE_ZLIB), POSIX regex (libc).

Builds clean as libads_runtime_c.a via:
  mkdir build && cd build && cmake .. && cmake --build .

Caveats:
- POSIX regex doesn't support PCRE-style named groups. `ads_regex_group`
  resolves numeric indices only for v1; Stage 2 plans PCRE2 swap.
- Args still flow as JSON strings; typed-args codegen pending for v1.1.
- CRC lookup tables emitted from spec/shared/crc_tables.yaml are pending;
  current bitwise impl is correct and table-free.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Brings the spec coverage from 5 reference plugins to 68 — the full
TS plugin catalog minus the official.ts barrel file. Authored in parallel
by four agents, each working a distinct label-range batch.

Coverage:
- Batch 1 (Label_10-22): 22 specs — mostly whole-plugin escape hatches
- Batch 2 (Label_24-4N): 15 specs — 4 declarative + 11 escape hatches
- Batch 3 (Label_4T-H1):  16 specs — all whole-plugin escape hatches
- Batch 4 (Label_H2-official): 9 specs — all whole-plugin escape hatches
- ColonComma: 1 spec (after schema relaxation, see below)

Of the 63 new specs, 4 are declarative ports (Label_44_IN/ON/OFF/ETA —
clean CSV + coordinate_decimal_minutes + airport + timestamp shape) and
59 use whole-plugin escape hatches via `parse: { custom: <name> }` and
`formatted: { custom: <name> }`.

Why the heavy escape-hatch ratio: the v1 schema's `formatter_call.type`
enum covers only the most common formatters (position, altitude,
timestamp, etc.). Real TS plugins lean on ~40 more ResultFormatter
methods (route, eta, currentFuel, temperature, flightNumber,
departureDay/arrivalDay, out/off/on/in, runway types, windData, mach,
airspeed, totalAirTemp, state_change, door_event, frequency, atis,
loadsheet, fault, warning, route, routeNumber, cg, engineStart/Stop,
groundspeed, day, month, airline, message_type, tail, unknownArr…).
Generating those declaratively would silently drop fields. Per the
"under-port-with-escape-hatches is better than over-port-with-wrong-
semantics" rule, conservative bias is correct.

Two real-life follow-ups for ADS v1.1:
1. Extend `formatter_call.type` to cover the full TS ResultFormatter API
   (or add an `aliases` form that lets specs declare the formatter name
   inline).
2. Add a `trailing_fields_into_unknown` parse step so plugins like
   Label_44_IN/ON/OFF/ETA preserve the addRemainingFields behavior that
   the declarative path currently drops.

Schema relaxation: the `qualifiers.labels` pattern previously rejected
punctuation, blocking Label_ColonComma (label literal ":;"). Changed
to allow any non-empty string — ACARS labels are byte sequences, not
identifiers. The schema now matches the real ACARS spec.

Escape-hatch inventory (~118 functions, ~2 per plugin):
- All listed in /docs/ESCAPE_HATCHES.md after stage 2 wire-up
- Each = the original TS plugin's decode() and formatted-item-push code,
  ported 1:1 into runtimes/<lang>/escape_hatches/

Verified end-to-end:
- All 68 specs validate against schema/ads-v1.schema.json
- `ads-gen --target ts` emits 68 .ts files cleanly
- `ads-gen --target rust` emits 68 .rs files cleanly
- `ads-gen --target c` emits 68 .c+.h pairs cleanly

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

coderabbitai Bot commented May 28, 2026

Important

Review skipped

Too many files!

This PR contains 299 files, which is 149 over the limit of 150.

To get a review, narrow the scope:
• coderabbit review --type committed # exclude uncommitted changes
• coderabbit review --dir # limit to a subdirectory
• coderabbit review --base # compare against a closer base

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 664fe845-8a05-47cb-b7cc-7c1885418932

📥 Commits

Reviewing files that changed from the base of the PR and between 5df58a6 and 520f7b2.

⛔ Files ignored due to path filters (1)
  • codegen/package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (299)
  • .github/workflows/.gitkeep
  • .github/workflows/ci.yml
  • .github/workflows/codegen-check.yml
  • .github/workflows/corpus-test.yml
  • .gitignore
  • codegen/README.md
  • codegen/package.json
  • codegen/src/cli.ts
  • codegen/src/emit-c.ts
  • codegen/src/emit-rust.ts
  • codegen/src/emit-typescript.ts
  • codegen/src/index.ts
  • codegen/src/ir.ts
  • codegen/src/parse-spec.ts
  • codegen/src/validate.ts
  • codegen/tsconfig.json
  • codegen/vitest.config.ts
  • corpus/.gitkeep
  • corpus/README.md
  • corpus/labels/10/LDR/sample-001.json
  • corpus/labels/10/LDR/sample-002.json
  • corpus/labels/10/LDR/sample-003.json
  • corpus/labels/10/POS/sample-001.json
  • corpus/labels/10/POS/sample-002.json
  • corpus/labels/10/Slash/sample-001.json
  • corpus/labels/10/Slash/sample-002.json
  • corpus/labels/10/Slash/sample-003.json
  • corpus/labels/12/N_Space/sample-001.json
  • corpus/labels/12/N_Space/sample-002.json
  • corpus/labels/12/N_Space/sample-003.json
  • corpus/labels/12/POS/sample-001.json
  • corpus/labels/12/POS/sample-002.json
  • corpus/labels/15/FST/sample-001.json
  • corpus/labels/15/FST/sample-002.json
  • corpus/labels/15/Paren2/sample-001.json
  • corpus/labels/15/Paren2/sample-002.json
  • corpus/labels/15/Paren2/sample-003.json
  • corpus/labels/15/Paren2/sample-004.json
  • corpus/labels/15/Paren2/sample-005.json
  • corpus/labels/15/Paren2/sample-006.json
  • corpus/labels/15/Paren2/sample-007.json
  • corpus/labels/16/AUTPOS/sample-001.json
  • corpus/labels/16/AUTPOS/sample-002.json
  • corpus/labels/16/AUTPOS/sample-003.json
  • corpus/labels/16/Honeywell/sample-001.json
  • corpus/labels/16/Honeywell/sample-002.json
  • corpus/labels/16/Honeywell/sample-003.json
  • corpus/labels/16/Honeywell/sample-004.json
  • corpus/labels/16/N_Space/sample-001.json
  • corpus/labels/16/N_Space/sample-002.json
  • corpus/labels/16/N_Space/sample-003.json
  • corpus/labels/16/N_Space/sample-004.json
  • corpus/labels/16/POSA1/sample-001.json
  • corpus/labels/16/POSA1/sample-002.json
  • corpus/labels/16/POSA1/sample-003.json
  • corpus/labels/16/TOD/sample-001.json
  • corpus/labels/16/TOD/sample-002.json
  • corpus/labels/16/TOD/sample-003.json
  • corpus/labels/16/TOD/sample-004.json
  • corpus/labels/16/TOD/sample-005.json
  • corpus/labels/1L/070/sample-001.json
  • corpus/labels/1L/070/sample-002.json
  • corpus/labels/1L/660/sample-001.json
  • corpus/labels/1L/660/sample-002.json
  • corpus/labels/1M/Slash/sample-001.json
  • corpus/labels/20/POS/sample-001.json
  • corpus/labels/20/POS/sample-002.json
  • corpus/labels/20/POS/sample-003.json
  • corpus/labels/21/POS/sample-001.json
  • corpus/labels/21/POS/sample-002.json
  • corpus/labels/22/OFF/sample-001.json
  • corpus/labels/22/OFF/sample-002.json
  • corpus/labels/22/OFF/sample-003.json
  • corpus/labels/22/OFF/sample-004.json
  • corpus/labels/22/POS/sample-001.json
  • corpus/labels/22/POS/sample-002.json
  • corpus/labels/24/Slash/sample-001.json
  • corpus/labels/24/Slash/sample-002.json
  • corpus/labels/2P/FM3/sample-001.json
  • corpus/labels/2P/FM3/sample-002.json
  • corpus/labels/2P/FM3/sample-003.json
  • corpus/labels/2P/FM3/sample-004.json
  • corpus/labels/2P/FM4/sample-001.json
  • corpus/labels/2P/FM4/sample-002.json
  • corpus/labels/2P/FM4/sample-003.json
  • corpus/labels/2P/FM5/sample-001.json
  • corpus/labels/2P/FM5/sample-002.json
  • corpus/labels/30/Slash_EA/sample-001.json
  • corpus/labels/44/ETA/sample-001.json
  • corpus/labels/44/ETA/sample-002.json
  • corpus/labels/44/IN/sample-001.json
  • corpus/labels/44/IN/sample-002.json
  • corpus/labels/44/IN/sample-003.json
  • corpus/labels/44/OFF/sample-001.json
  • corpus/labels/44/OFF/sample-002.json
  • corpus/labels/44/ON/sample-001.json
  • corpus/labels/44/ON/sample-002.json
  • corpus/labels/44/ON/sample-003.json
  • corpus/labels/44/ON/sample-004.json
  • corpus/labels/44/POS/sample-001.json
  • corpus/labels/44/POS/sample-002.json
  • corpus/labels/44/Slash/sample-001.json
  • corpus/labels/44/Slash/sample-002.json
  • corpus/labels/44/Slash/sample-003.json
  • corpus/labels/4A/01/sample-001.json
  • corpus/labels/4A/DIS/sample-001.json
  • corpus/labels/4A/DOOR/sample-001.json
  • corpus/labels/4A/Slash_01/sample-001.json
  • corpus/labels/4A/sample-001.json
  • corpus/labels/4A/sample-002.json
  • corpus/labels/4A/sample-003.json
  • corpus/labels/4A/sample-004.json
  • corpus/labels/4A/sample-005.json
  • corpus/labels/4A/sample-006.json
  • corpus/labels/4N/sample-001.json
  • corpus/labels/4N/sample-002.json
  • corpus/labels/4N/sample-003.json
  • corpus/labels/4N/sample-004.json
  • corpus/labels/4N/sample-005.json
  • corpus/labels/4N/sample-006.json
  • corpus/labels/4N/sample-007.json
  • corpus/labels/4T/AGFSR/sample-001.json
  • corpus/labels/4T/AGFSR/sample-002.json
  • corpus/labels/4T/AGFSR/sample-003.json
  • corpus/labels/4T/ETA/sample-001.json
  • corpus/labels/4T/ETA/sample-002.json
  • corpus/labels/58/sample-001.json
  • corpus/labels/58/sample-002.json
  • corpus/labels/5Z/Slash/sample-001.json
  • corpus/labels/5Z/Slash/sample-002.json
  • corpus/labels/5Z/Slash/sample-003.json
  • corpus/labels/5Z/Slash/sample-004.json
  • corpus/labels/5Z/Slash/sample-005.json
  • corpus/labels/5Z/Slash/sample-006.json
  • corpus/labels/5Z/Slash/sample-007.json
  • corpus/labels/5Z/Slash/sample-008.json
  • corpus/labels/80/sample-001.json
  • corpus/labels/80/sample-002.json
  • corpus/labels/80/sample-003.json
  • corpus/labels/80/sample-004.json
  • corpus/labels/80/sample-005.json
  • corpus/labels/80/sample-006.json
  • corpus/labels/80/sample-007.json
  • corpus/labels/80/sample-008.json
  • corpus/labels/80/sample-009.json
  • corpus/labels/83/sample-001.json
  • corpus/labels/83/sample-002.json
  • corpus/labels/83/sample-003.json
  • corpus/labels/83/sample-004.json
  • corpus/labels/83/sample-005.json
  • corpus/labels/83/sample-006.json
  • corpus/labels/8E/sample-001.json
  • corpus/labels/H1/ATIS/sample-001.json
  • corpus/labels/H1/ATIS/sample-002.json
  • corpus/labels/H1/ATIS/sample-003.json
  • corpus/labels/H1/ATIS/sample-004.json
  • corpus/labels/H1/ATIS/sample-005.json
  • corpus/labels/H1/ATIS/sample-006.json
  • corpus/labels/H1/EZF/sample-001.json
  • corpus/labels/H1/EZF/sample-002.json
  • corpus/labels/H1/EZF/sample-003.json
  • corpus/labels/H1/EZF/sample-004.json
  • corpus/labels/H1/EZF/sample-005.json
  • corpus/labels/H1/FLR/sample-001.json
  • corpus/labels/H1/FLR/sample-002.json
  • corpus/labels/H1/FLR/sample-003.json
  • corpus/labels/H1/FLR/sample-004.json
  • corpus/labels/H1/FLR/sample-005.json
  • corpus/labels/H1/FLR/sample-006.json
  • corpus/labels/H1/M_POS/sample-001.json
  • corpus/labels/H1/M_POS/sample-002.json
  • corpus/labels/H1/M_POS/sample-003.json
  • corpus/labels/H1/M_POS/sample-004.json
  • corpus/labels/H1/M_POS/sample-005.json
  • corpus/labels/H1/M_POS/sample-006.json
  • corpus/labels/H1/M_POS/sample-007.json
  • corpus/labels/H1/OFP/sample-001.json
  • corpus/labels/H1/OFP/sample-002.json
  • corpus/labels/H1/OFP/sample-003.json
  • corpus/labels/H1/OFP/sample-004.json
  • corpus/labels/H1/OFP/sample-005.json
  • corpus/labels/H1/OFP/sample-006.json
  • corpus/labels/H1/OHMA/sample-001.json
  • corpus/labels/H1/OHMA/sample-002.json
  • corpus/labels/H1/OHMA/sample-003.json
  • corpus/labels/H1/OHMA/sample-004.json
  • corpus/labels/H1/Paren/sample-001.json
  • corpus/labels/H1/Paren/sample-002.json
  • corpus/labels/H1/Paren/sample-003.json
  • corpus/labels/H1/WRN/sample-001.json
  • corpus/labels/H1/WRN/sample-002.json
  • corpus/labels/H1/WRN/sample-003.json
  • corpus/labels/H1/WRN/sample-004.json
  • corpus/labels/H1/WRN/sample-005.json
  • corpus/labels/H1/WRN/sample-006.json
  • corpus/labels/H2/02E/sample-001.json
  • corpus/labels/H2/02E/sample-002.json
  • corpus/labels/H2/02E/sample-003.json
  • corpus/labels/H2/02E/sample-004.json
  • corpus/labels/HX/sample-001.json
  • corpus/labels/HX/sample-002.json
  • corpus/labels/HX/sample-003.json
  • corpus/labels/MA/sample-001.json
  • corpus/labels/MA/sample-002.json
  • corpus/labels/MA/sample-003.json
  • corpus/labels/MA/sample-004.json
  • corpus/labels/QQ/sample-001.json
  • corpus/labels/QQ/sample-002.json
  • corpus/labels/QQ/sample-003.json
  • corpus/labels/SQ/sample-001.json
  • corpus/labels/SQ/sample-002.json
  • corpus/labels/SQ/sample-003.json
  • corpus/wildcards/arinc_702/sample-001.json
  • corpus/wildcards/arinc_702/sample-002.json
  • corpus/wildcards/arinc_702/sample-003.json
  • corpus/wildcards/arinc_702/sample-004.json
  • corpus/wildcards/arinc_702/sample-005.json
  • corpus/wildcards/arinc_702/sample-006.json
  • corpus/wildcards/arinc_702/sample-007.json
  • corpus/wildcards/arinc_702/sample-008.json
  • corpus/wildcards/arinc_702/sample-009.json
  • corpus/wildcards/arinc_702/sample-010.json
  • corpus/wildcards/arinc_702/sample-011.json
  • corpus/wildcards/arinc_702/sample-012.json
  • corpus/wildcards/arinc_702/sample-013.json
  • corpus/wildcards/arinc_702/sample-014.json
  • corpus/wildcards/arinc_702/sample-015.json
  • corpus/wildcards/arinc_702/sample-016.json
  • corpus/wildcards/arinc_702/sample-017.json
  • corpus/wildcards/arinc_702/sample-018.json
  • corpus/wildcards/arinc_702/sample-019.json
  • corpus/wildcards/arinc_702/sample-020.json
  • corpus/wildcards/arinc_702/sample-021.json
  • corpus/wildcards/arinc_702/sample-022.json
  • corpus/wildcards/arinc_702/sample-023.json
  • corpus/wildcards/arinc_702/sample-024.json
  • corpus/wildcards/arinc_702/sample-025.json
  • corpus/wildcards/arinc_702/sample-026.json
  • corpus/wildcards/arinc_702/sample-027.json
  • corpus/wildcards/arinc_702/sample-028.json
  • corpus/wildcards/arinc_702/sample-029.json
  • corpus/wildcards/arinc_702/sample-030.json
  • corpus/wildcards/arinc_702/sample-031.json
  • corpus/wildcards/arinc_702/sample-032.json
  • corpus/wildcards/arinc_702/sample-033.json
  • corpus/wildcards/arinc_702/sample-034.json
  • corpus/wildcards/arinc_702/sample-035.json
  • corpus/wildcards/arinc_702/sample-036.json
  • corpus/wildcards/arinc_702/sample-037.json
  • corpus/wildcards/arinc_702/sample-038.json
  • corpus/wildcards/arinc_702/sample-039.json
  • corpus/wildcards/arinc_702/sample-040.json
  • corpus/wildcards/arinc_702/sample-041.json
  • corpus/wildcards/arinc_702/sample-042.json
  • corpus/wildcards/arinc_702/sample-043.json
  • corpus/wildcards/arinc_702/sample-044.json
  • corpus/wildcards/arinc_702/sample-045.json
  • corpus/wildcards/arinc_702/sample-046.json
  • corpus/wildcards/arinc_702/sample-047.json
  • corpus/wildcards/arinc_702/sample-048.json
  • corpus/wildcards/arinc_702/sample-049.json
  • corpus/wildcards/arinc_702/sample-050.json
  • corpus/wildcards/arinc_702/sample-051.json
  • corpus/wildcards/arinc_702/sample-052.json
  • corpus/wildcards/arinc_702/sample-053.json
  • corpus/wildcards/arinc_702/sample-054.json
  • corpus/wildcards/arinc_702/sample-055.json
  • corpus/wildcards/arinc_702/sample-056.json
  • corpus/wildcards/arinc_702/sample-057.json
  • corpus/wildcards/arinc_702/sample-058.json
  • corpus/wildcards/arinc_702/sample-059.json
  • corpus/wildcards/arinc_702/sample-060.json
  • corpus/wildcards/arinc_702/sample-061.json
  • corpus/wildcards/arinc_702/sample-062.json
  • corpus/wildcards/arinc_702/sample-063.json
  • corpus/wildcards/arinc_702/sample-064.json
  • corpus/wildcards/arinc_702/sample-065.json
  • corpus/wildcards/arinc_702/sample-066.json
  • corpus/wildcards/arinc_702/sample-067.json
  • corpus/wildcards/arinc_702/sample-068.json
  • corpus/wildcards/arinc_702/sample-069.json
  • corpus/wildcards/arinc_702/sample-070.json
  • corpus/wildcards/arinc_702/sample-071.json
  • corpus/wildcards/arinc_702/sample-072.json
  • corpus/wildcards/arinc_702/sample-073.json
  • corpus/wildcards/arinc_702/sample-074.json
  • corpus/wildcards/arinc_702/sample-075.json
  • corpus/wildcards/arinc_702/sample-076.json
  • corpus/wildcards/arinc_702/sample-077.json
  • corpus/wildcards/arinc_702/sample-078.json
  • corpus/wildcards/arinc_702/sample-079.json
  • corpus/wildcards/arinc_702/sample-080.json
  • corpus/wildcards/arinc_702/sample-081.json
  • corpus/wildcards/arinc_702/sample-082.json
  • corpus/wildcards/arinc_702/sample-083.json
  • corpus/wildcards/arinc_702/sample-084.json
  • corpus/wildcards/arinc_702/sample-085.json
  • corpus/wildcards/arinc_702/sample-086.json
  • corpus/wildcards/arinc_702/sample-087.json

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds ADS v1 schema/docs, a codegen tool with IR/validation/emitters, TypeScript/Rust/C runtimes and utilities, extensive label and wildcard specs, shared corpus fixtures, and CI workflows to validate, generate, and smoke-test outputs.

Changes

ADS v1 Bootstrap

Layer / File(s) Summary
Schema, codegen, runtimes, specs/corpus, and CI
schema/*, codegen/*, runtimes/**, spec/**, corpus/**, .github/workflows/*, .gitignore, docs/*
Defines ADS DSL/schema; implements multi-target codegen (CLI, IR, validators, TS/Rust/C emitters); adds TS/Rust/C runtimes and utilities; introduces many label/wildcard specs and shared data/corpus; wires CI generation/validation.

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

Poem

A rabbit taps its tiny keys—
Specs sprout fields like forest trees.
Codegen hums in triple tongues,
CI dances, beating drums.
Runtimes hop in C and Rust,
TypeScript sails in tidy gust—
Decode the skies, in code we trust! 🐇✨

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch init/ads-v1

Copy link
Copy Markdown

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

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 765a3903ce

ℹ️ 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".

Comment thread codegen/src/emit-typescript.ts Outdated
: renderExpr(field.from);
if (field.when) {
out.push(`${indent}if (${renderCondition(field.when)}) {`);
out.push(`${indent} const ${field.name} = ${decodeExpr};`);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Hoist conditional field values before formatting

For specs that mark a field with when and later format it with when_present (for example spec/labels/44/ON.yaml formats $fuel_remaining), this emits const fuel_remaining inside the if block. The formatter is emitted after the fields and references fuel_remaining outside that block, so the generated TypeScript plugin cannot compile (or would reference an out-of-scope value). Emit a variable in the outer decode scope and only call the formatter when it is present.

Useful? React with 👍 / 👎.

Comment thread codegen/src/emit-c.ts Outdated
);
out.push(`${indent}}`);
} else {
out.push(`${indent}ads_value_t ${field.name} = ${decodeExpr};`);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Emit C field values as pointers

Every C decode helper in runtimes/c/include/ads_helpers.h returns ads_value_t *, and ads_result_raw_set also expects an ads_value_t *, but this generator declares generated field locals as ads_value_t. For any generated C plugin with fields, the emitted code has incompatible pointer/object types before it can be linked; the field local should be emitted as ads_value_t * (including the conditional-field branch).

Useful? React with 👍 / 👎.

Comment thread codegen/src/emit-rust.ts Outdated
}
const fn = call.fn;
if (Object.keys(call.args).length > 0) {
return `helpers::${fn}(${valueExpr}, ${rustString(JSON.stringify(call.args))})`;
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Route Rust helpers with args to their args-aware functions

When a spec supplies decode args for helpers such as integer or timestamp_hhmmss (several label 44 specs do this), this emits calls like helpers::integer(value, "{...}"). The Rust runtime exposes one-argument integer/timestamp_hhmmss plus separate integer_with_args/timestamp_hhmmss_with_args, so those generated Rust plugins fail to compile for the args-bearing fields; map these helper names to their _with_args variants when call.args is non-empty.

Useful? React with 👍 / 👎.

Replaces the 2 manual reference samples with an auto-extracted corpus
captured by running the full acars-decoder-typescript test suite (88
suites, 407 passing tests, 9 skipped) through a Jest setup hook that
wraps every plugin's prototype.decode and records (input → DecodeResult)
pairs to JSONL. A post-processing script groups records by plugin slug
and writes per-spec sample files at
corpus/<spec_path>/sample-NNN.json.

Coverage:
- 288 samples across 57 of the 68 plugin specs
- Sample-001 typically the canonical "successful decode" case; later
  samples cover variants and failure modes
- The extracted "expected" field is the *actual* DecodeResult shape
  the TS production code returns, so cross-implementation parity is
  enforced against real production behavior, not assertion fragments

Unmatched (need v1.1 follow-up — 6 plugin slugs):
- c-band (spec slug: cband — TS hyphenates)
- label-1l-3-line (spec slug: label-1l-3line — slug function doesn't
  split digit→lowercase boundaries)
- label-h1-star-pos (spec slug: label-h1-starpos — uppercase-run
  boundary)
- label-13-18-slash (spec slug match issue)
- label-1l-1-line, test-label-44 (no matching spec; possibly stale TS
  test fixtures)

Recommended v1.1 fix: add an explicit `slug` field on `plugin` in the
spec for the divergent cases (escape hatch), or use a camelCase-aware
slugger (with care not to regress Label_4A → label-4a).

Tooling (intentionally NOT committed):
- /tmp/corpus-extractor.js — Jest setupFilesAfterEnv hook that monkey-
  patches each plugin class's prototype.decode to log records
- /tmp/corpus-postprocess.js — reads JSONL, groups by slug, writes
  sample files

Rerun:
  cd acars-decoder-typescript
  rm -f /tmp/extracted-corpus.jsonl && touch /tmp/extracted-corpus.jsonl
  npm test -- --setupFilesAfterEnv=/tmp/corpus-extractor.js --runInBand --silent
  node /tmp/corpus-postprocess.js

(These belong as committed tooling in codegen/scripts/ in a follow-up.)

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

Copilot AI left a comment

Choose a reason for hiding this comment

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

Copilot wasn't able to review this pull request because it exceeds the maximum number of files (300). Try reducing the number of changed files and requesting a review from Copilot again.

kevinelliott and others added 5 commits May 27, 2026 23:59
Companion to codegen-check.yml. Language repos call this from their CI
to run the shared corpus through their decoder and fail on any
divergence vs the expected output in each corpus sample file.

Caller example:

  jobs:
    corpus:
      uses: airframesio/acars-decoder/.github/workflows/corpus-test.yml@main
      with:
        language: ts
        run-cmd: npm run test:corpus
        setup-cmd: npm ci

Handles checkout-with-submodules and toolchain installation per language
(Node for ts/rust/c, plus Rust toolchain for rust, plus CMake + cjson +
zlib for c). The caller specifies the actual corpus-test run-cmd.

Security hardening matches codegen-check.yml: inputs flow through env
vars (not direct ${{ }} interpolation in shell), and an input-validation
step rejects newlines, backticks, and $(...) in command strings.

Sets up Stage 2.5 / Stage 3: each language repo's `test:corpus`
implementation (TS first, then Rust, then C) will land in follow-up
PRs; this workflow is the CI glue that runs them.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Fixes the double-bookkeeping issue identified in airframes-decoder#1's
known-follow-ups: previously every field unconditionally wrote
`result.raw.<name> = <name>` even when a downstream formatter call also
wrote raw under the formatter's canonical key (position, altitude,
callsign, …). The original hand-written plugins only have the formatter
write. The generated code's extra raw entries caused divergence from the
existing tests and blocked the Stage 2 behavioral swap.

Fix: pre-scan FormattedIR.items for `$varname` references and skip the
auto-emit for fields whose name is consumed by a formatter. Custom
formatters and free-text formatters are conservatively treated as
non-consuming (so the auto-emit still happens), since we can't statically
tell what the hatch will do with raw.

Applied identically in emit-typescript.ts, emit-rust.ts, and emit-c.ts
so the three targets produce equivalent shapes.

Verified: regenerating from `spec/labels/10/POS.yaml` now produces a
Label_10_POS.ts whose raw output matches the original
acars-decoder-typescript/lib/plugins/Label_10_POS.ts byte-for-byte
(raw has {position, altitude} only, not {latitude, longitude, altitude,
position}).

Unblocks the Stage 2 behavioral swap for the ~4 declarative ports
(Label_10_POS, Label_44_POS, Label_44_IN/ON/OFF/ETA). Whole-plugin
escape-hatch specs are unaffected.

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

Previously the generated TS emitter wrote `if (cond) { const X = ...; }`
which made X block-scoped — a downstream `ResultFormatter.X(result, X)`
outside the if would throw ReferenceError when the guard was false. The
original hand-written plugins implicitly relied on X being undefined in
that case (the formatter handles undefined gracefully).

Fix: declare `let X;` outside the if, assign inside. Variable is now
in scope after the if, undefined when the guard fails — matching the
original behavior.

Surfaced by the Stage 2.5 pilot extension to Label_44_ON, whose
fuel_remaining field is `when`-gated.

Same shape of fix queued for Rust + C emitters (Option<T> outside / NULL
outside respectively) — pushed separately once verified in their
respective build pipelines.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The TS runtime's ResultFormatter exposes the method as currentFuel, not
fuel. Generated plugins were calling .fuel(...) which threw
'is not a function' at runtime — surfaced by the Label_44_ON labelindex
test under the Stage 2.5 pilot.

Wider audit of formatter method-name mismatches between the emitter map
and the runtime is queued; this commit fixes the one that's currently
exercised by tests.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
When a spec has a when-gated fuel field whose guard fails, the variable
becomes undefined. Generated plugins still call ResultFormatter.currentFuel
unconditionally, which previously crashed on .toString() of undefined.

Original hand-written plugins guarded with 'if (value !== sentinel)
ResultFormatter.X(...)' — i.e. they didn't call the formatter at all
when the value was missing. The defensive guard inside currentFuel
matches that semantic without requiring formatter-level when-clauses
in the DSL.

Same defensive pattern queued for the other ResultFormatter methods
that may receive missing values; this commit fixes the one currently
exercised by tests (Label_44_ON via the labelindex test).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
kevinelliott and others added 11 commits May 28, 2026 01:00
…er, FlightPlanUtils, parseIcaoFpl, MIAMCoreUtils, RouteUtils, ascii85Decode, base64ToUint8Array, inflateData

Unblocks Stage 2.5 bulk escape-hatch implementations: hatches are free
functions that need to call plugin.failUnknown / plugin.setDecodeLevel /
plugin.debug / plugin.initResult, but those were protected. Now public.

Also exports the helpers used by ARINC 702 / MIAM / OFP / route plugins
that previously were only accessible via deep submodule paths.

Documented inline why each was promoted (so a future reviewer doesn't
silently re-protect them).

Verified runtimes/typescript type-checks clean.

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

Three boundary rules now insert hyphens to match legacy hand-written plugin
names byte-for-byte:
  - lowercase→Uppercase                       (CBand → c-band)
  - digit→Uppercase if next is lowercase      (3Line → 3-line; 4A stays 4a)
  - Uppercase→Uppercase if next is lowercase  (StarPOS → star-pos)

Fixes the Stage 2.5 bulk-swap labelindex test failure where CBand's
generated name 'cband' didn't match the legacy 'c-band' that test
assertions check for. Also future-proofs Label_1L_3Line and
Label_H1_StarPOS.

Verified slugs:
  Label_10_POS       → label-10-pos
  Label_4A           → label-4a
  ARINC_702          → arinc-702
  CBand              → c-band
  Label_1L_3Line     → label-1l-3-line
  Label_H1_StarPOS   → label-h1-star-pos

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

The variant+field+formatter-custom shape worked in theory but had no
clean contract for who owns the formatted.items list when the formatter
is custom. Other complex plugins (CBand, ARINC_702, MIAM, etc.) use the
whole-plugin parse-custom + format-description shape; Label_4A follows
suit. Implementation lives in lib/plugins/escape_hatches/Label_4A.ts in
each language repo.

Validates clean: 68 specs OK.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Mirrors the TS emitter fixes (init/ads-v1@80095f5 and @c037de4) so all
three targets produce consistent generated code:

  - when-gated fields: declare outside the if, assign inside. Rust uses
    Option<serde_json::Value>, C uses NULL-initialized pointer.
  - smart slug: insert hyphens at camelCase boundaries (CBand→c-band,
    StarPOS→star-pos, 3Line→3-line, Label_4A stays label-4a).

Verified: rust + c emitters produce 68 plugins cleanly; CBand slug is
now 'c-band' in both targets.

Required prep before Stage 2.5 Rust + C escape-hatch bulk implementation.

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

A prior edit to emit-rust.ts's emitField when-branch failed silently
due to a 'File has not been read yet' error in the harness; the smart-
slug change landed but the when-hoist did not. Surfaced when Stage 2.5
Rust wiring produced 'cannot find value fuel_remaining' errors.

Now matches the TS emitter's pattern: when-gated fields hoist to
'let X: Option<JsonValue> = if cond { ... Some(v.into()) } else { None };'
so downstream formatter calls see the variable.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Three coordinated changes that bring the Rust emitter and runtime into
agreement:

  1. Emitter: always emits 2-arg helper calls (value + args_json), even
     when args is empty. Removes the special-case 1-arg dispatch that
     diverged from the runtime.
  2. Emitter: stops appending .as_str() on regex Captures index access
     (the indexed access already returns &str; the .as_str() relied on
     the unstable str_as_str library feature).
  3. Emitter: unknown_arr arg coercion uses .to_string() instead of
     .clone() so the Vec<String> param matches.
  4. Runtime helpers.rs: every decode-fn helper now accepts (value,
     args_json) uniformly. Args-free helpers ignore the second arg.
  5. Runtime result_formatter.rs: every formatter method accepts
     impl Into<Option<JsonValue>> so when-gated fields (which now
     hoist to Option<JsonValue>) flow through cleanly. None / Null /
     NaN inputs no-op, matching the TS pattern of guarding before
     calling the formatter.

Expected to take Rust crate compile errors from 33 down to a small
remainder for follow-up.

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

Closes the remaining 4 type-mismatch errors from the convergence pass:

  - regex captures: pass &onExpr so String values (message.text) coerce
    to &str.
  - deflate: inflate takes &[u8]; src is Vec<u8>, so always borrow.
  - text_decode: same — borrow the Vec<u8> source.
  - renderDecodeCall: borrow the value expr by default (skip when expr
    already starts with & / * / numeric literal). Lets owned values
    (String from text_decode, Vec<u8>) flow through to helpers/hatches
    that take references.

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

Stage 2.5 C convergence prep. C compiler needs forward declarations; the
generated plugins call ads_hatch_<name>(...) and would otherwise produce
implicit-function-declaration errors.

Three signatures:
  - parse hatches (61): (msg, result, opts) -> result
  - field decode (3):   (value, args_json) -> value
  - formatter (1):      (result) -> void

Implementations live in each language repo's src/escape_hatches/ (the
central runtime can't implement plugin-specific logic).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Every decode-fn helper now accepts (value, args_json), where args_json
is '{}' when the spec specifies no args. Args-free helpers (decode_float,
decode_string, etc.) just ignore the second arg.

Removes the *_args / *_with_args duplicate functions; they had unified
implementations behind separate names which confused the emitter
dispatch. Now the emitter always emits the same 2-arg form regardless
of whether args is empty.

Same fix shape as the Rust convergence pass (init/ads-v1@6b475ff). Both
runtimes are now consistent.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
ads_regex_match_t is opaque (forward-declared in ads_runtime.h), so the
emitted code can't declare a value of it. Switch to the heap-allocated
ads_regex_match_new() and pass the pointer to ads_regex_match_ok /
ads_regex_group.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants