# JG2400/F0 phase-body decoder — integration handoff Branch prepared for integration: `experiment/jg25-f0-cpp-head`, based on `claude/jg25_fm` at `500ceb2`. ## What is implemented `modem/jg25/src/jg25_phase_body_decode.{hpp,cpp}` is a complete C++ body-decoder head for **JG2400/F0 only**. It is intentionally not called by the runtime yet. The detector is the wire-compatible CPFSK phase trellis proven offline on the real-radio corpus: - 48 carrier-phase states; - four JG2400 tone transitions from each state; - bounded `{-1,0,+1}` phase correction with fixed penalty `0.5`; - decision-directed estimate of the residual body frequency error before Viterbi; - real-waveform coherent matched correlations — **no FFT/Hilbert dependency**; - the production body's existing timing ladder: top ten legal-tone hypotheses plus zero-ppm offsets; - CRC-32C remains the only success/admission gate. The C++ arithmetic itself was checked against the full RF corpus before handoff: every complete JG2400/F0 fixture was CRC-valid and byte-exact (37/37), including all 9 held-out August body-CRC failures and all 28 existing successes, all on the first timing hypothesis. The remaining corpus `no_capture` fixture has a clipped preamble; on its untouched continuous source the frame synchronizer finds it and the phase decoder is byte-exact. ## API ```cpp bool phase_body_applicable(const Header& h) noexcept; PhaseBodyDecodeResult decode_jg2400_f0_phase_once( const float* x, std::size_t n, const Header& h, const Acq& acq, double ppm, long sample_offset); PhaseBodyDecodeResult decode_jg2400_f0_phase( const float* x, std::size_t n, const Header& h, const Acq& acq); ``` `x` must be the **already-conditioned** branch audio. Do not run the emphasis FIR again inside this head. `Header::bodyStart` and `Acq` use exactly the same coordinate system as `decode_body()`. `insufficient_audio` is deliberately separate from `crc_failed`. A recovery worker that has not buffered enough body samples must retry later, not record a decoder failure. ## Recommended first integration Do not replace the existing body decoder globally in the first patch. Keep the change additive and cheap on the shared path: ```cpp auto body = decode_body(conditioned.data(), conditioned.size(), h, acq); if (!body.crc && phase_body_applicable(h)) { // Prefer doing this on the existing low-priority recovery/body worker. auto phase = decode_jg2400_f0_phase( conditioned.data(), conditioned.size(), h, acq); if (phase.crc_valid()) { body.crc = true; body.raw = std::move(phase.raw); body.ppm = phase.ppm; body.retries = phase.hypotheses_tested > 0 ? phase.hypotheses_tested - 1 : 0; // Record detector="phase_trellis", sample_offset, // residual_hz and hypotheses_tested in diagnostics. } } ``` The recovery worker is the preferred first home because a 348-byte trellis is intentionally more work than the energy detector, and Stage 3 already moved body recovery off the shared scan/pump worker. Once runtime cost is measured and bounded, using the same fallback in the primary body path is a separate decision. ## Do not change these while integrating 1. **No wire change.** No pilot, whitening, tone/baud change, or new header field. 2. **No DCD coupling.** The phase head receives a candidate that already has acquisition/header geometry; it does not decide whether audio is a frame. 3. **No JG1200 use.** At JG1200, 600-Hz spacing equals 600 baud, so all tones have the same symbol-boundary phase advance and this trellis is degenerate. `phase_body_applicable()` enforces the bypass. 4. **No F1/F2/F3 use.** This is an F0 body detector. Coded profiles retain the existing Viterbi/FEC path. 5. **No alternate CRC policy.** Header CRC-16 only grants permission to spend body work. Body CRC-32C is still the delivery gate. 6. **No re-tuning `0.5` on the September pair.** It was chosen before the independent August corpus and that holdout passed. Treat it as frozen unless a broader held-out corpus disproves it. 7. **Do not duplicate body geometry.** Continue using the shared `Header::bodyStart`, `transmitted_bits()`, and existing recovery `body_end_sample` scheduling. ## Diagnostics to add during integration For every phase attempt, retain: - attempt source (`primary_capture` or `stream_recovery`); - absolute candidate sample already carried by the recovery path; - selected emphasis slope; - `detector=phase_trellis`; - body length; - phase `ppm` and `sample_offset`; - `residual_body_cfo_hz`; - `phase_hypotheses_tested`; - body CRC verdict; - worker decode time. Keep decoder attempts separate from unique RF frames exactly as Stage 3 already does. ## Required integration gates Before Bench B: 1. `test_jg25_phase_body_decode` green under GCC, strict Clang, ASan/UBSan. 2. Existing full test suite unchanged for JG1200 and F1/F2/F3. 3. Full JG25 RF corpus through the integrated C++ path: - all existing JG2400/F0 successes preserved; - 9/9 held-out August JG2400/F0 body failures CRC-valid and exact; - zero CRC-valid wrong payloads; - continuous-source `GT0785` found by frame sync and decoded exact. 4. Re-run the 2026-09-02 recording: - 18/18 historical JG2400/F0 body failures recovered; - 21/21 prior JG2400/F0 deliveries unchanged; - block-equivalent 40/40 including the accidental JG2000 frame. 5. Runtime benchmark on 30-, 51-, ~137- and 348-byte bodies. Report median/p95/max body-worker time and hypotheses tried. The scan/pump worker must never run the trellis. 6. Noise / false-header stress: zero body CRC-32C admissions. Only after those gates should Bench B test JG2400/F0 as the shipping default.