Skip to content

Carry the node's ADS-B correlation with its position in v1 frames (contract 1.5.0) - #514

Merged
Babissimo merged 4 commits into
mainfrom
feat/v1-adsb-position-tags
Sep 22, 2026
Merged

Babissimo merged 4 commits into
mainfrom
feat/v1-adsb-position-tags

Conversation

@jehanazad

Copy link
Copy Markdown
Contributor

Summary

A node that correlates a detection with an ADS-B aircraft also knows where that aircraft was, but the v1 contract only had room for the hex. Without a position source of its own the server can do nothing with the hex, and on any environment that sees the node second-hand through the detection mirror that means no claim, no coverage calibration point, and an untagged detection at the tracker. This is why real nodes on test recorded zero calibration points after #513 landed: mirrored frames arrive hex-only and test has no fresh real-world position cache. Contract 1.5.0 adds an optional adsb column carrying the node's own correlation with its position.

Node side: offworldlabs/retina-telemetry#19.

Changes

  • backend/routes/node_schemas.py: new AdsbTag (hex, lat, lon required; alt ft, gs kt, track deg and the node's expected delay/Doppler and residuals optional; extra keys forbidden). DetectionFrame.adsb: list[AdsbTag | None] | None, parallel to the four arrays and required to agree with adsb_hex entry for entry. A hex-only node omits it and is unchanged.
  • backend/services/node_pipeline.py: pipeline_frame files the tags as the per-detection adsb records the TCP ingest has always produced (alt under alt_baro, null fields omitted because the geolocator branches on key presence). From there nothing else changes: frame_processor stores the position in the aircraft cache under the node's world, the known lane claims through path 1, the tracker reads the tag per detection, and the mirror, which uses the same conversion, forwards the position to every target.
  • backend/routes/nodes.py: NODE_API_VERSION 1.4.0 → 1.5.0 with the reasoning comment.
  • contracts/nodes-v1.openapi.yaml: regenerated.
  • backend/services/frame_processor.py: the comment claiming v1 frames never carry an adsb list is updated.

Test coverage

  • test_node_schemas.py::TestAdsbTags: round trip, absent is None, minimal tag, length mismatch, hex disagreement, tag on an unassociated detection, unknown keys, out-of-range and non-numeric values.
  • test_node_pipeline.py: conversion shape, null fields omitted, and end to end: a wire tag is a path-1 claim with no aircraft cache involved.
  • test_detection_mirror.py: the batch carries the tags in the pipeline shape; a hex-only frame is byte-identical.
  • Full backend suite 4225 passed, 2 skipped; pre-commit passes; generate_openapi --check passes.

Review notes

  • Rollout order. A server below 1.5.0 refuses a frame carrying adsb outright, and the telemetry client cannot learn the server's contract version. This must reach every environment nodes post to (prod) before retina-telemetry#19 ships to nodes. The mirror path needs it on both the sending and receiving environment.
  • Trust surface. Node-supplied tags were already authoritative on the TCP path (path 1 is not re-gated); this extends the same trust to v1 nodes. The residual, exclusivity and maturity rules still decide what calibrates.
  • Deferred: the tracker could resolve adsb_hex for hex-only nodes from the aircraft cache; untouched here.

🤖 Generated with Claude Code

…5.0)

A node that correlates a detection with an ADS-B aircraft knows where that
aircraft was — blah2-api's enrichment reports hex, lat, lon, altitude, ground
speed, track and its own delay/Doppler residuals — but the v1 contract only had
room for the hex.  The server then needs a position source of its own to make
anything of it, and where it has none, which is any environment that only sees
the node second-hand through the detection mirror, the correlation is inert:
no claim, no coverage calibration point, and the tracker never sees the tag.

Contract 1.5.0 adds an optional `adsb` column to DetectionFrame: an AdsbTag or
null per detection, parallel to `adsb_hex` and required to agree with it entry
for entry, so one correlation cannot be said twice differently.  A hex-only
node omits it and is byte-for-byte unchanged.

pipeline_frame files the tags as the per-detection `adsb` records the TCP
ingest has always produced (`alt` under `alt_baro`, null fields omitted since
the geolocator branches on key presence), so frame_processor stores the
position in the aircraft cache under the node's world, the known lane claims
through path 1, the tracker tags the detection, and the mirror — which uses
the same conversion — forwards the position to every target.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@claude

This comment has been minimized.

Babissimo and others added 2 commits September 22, 2026 16:55
A node sending 1.5.0's `adsb` tags said each correlation twice, once in
`adsb_hex` and again as the tag's `hex`, with a validator whose only job
was to check the two copies agreed. A node with association switched off
still had to send a list of nulls. Nothing on the server reads the
frame's `adsb_hex` beyond copying it onto the queue: the tracker takes
its hex from the per-detection `adsb` record, and path 1 claiming reads
`frame["adsb"]`.

So `adsb_hex` becomes optional and is marked deprecated in the contract.
`adsb` alone carries the association, neither column means the node
correlated nothing, and a frame that sends both must still have them
agree. `pipeline_frame` fills the queue's `adsb_hex` from whichever
column arrived, so nothing downstream, the detection mirror included,
sees the option.

It adds nothing to the rollout. Relaxing a required request field breaks
no client, and a server below 1.5.0 already refuses a frame carrying
`adsb`, so the server-first order this PR needs covers it. It stays
within 1.5.0, which has not shipped.

retina-telemetry#19 copies the 1.5.0 DetectionFrame and needs this
revision of it; it can then stop sending `adsb_hex` beside the tags.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Brings the branch up to date with main so CI can run on it; a conflicting PR queues no pull_request workflows. The one conflict was the ADS-B extraction comment in frame_processor.py, which this branch rewrites, beside the probation guard #525 added to the line below it. Both are kept.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@claude

This comment has been minimized.

The agreement check between adsb and adsb_hex is one-directional on purpose: a tag's hex must match the hex beside it, but a hex beside a null tag passes. That is how a node says it matched an aircraft and had no usable position for it, which retina-telemetry#19 sends when lat/lon are not finite. The docstring's 'agree entry for entry' read as symmetric, and the review bot proposed making the check so, which would refuse those frames. The docstring now says what the check allows, and a test pins it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@claude

claude Bot commented Sep 22, 2026 •

Copy link
Copy Markdown

Claude finished @Babissimo's task in 1m 36s —— View job


Review of PR #514 (latest push)

  • Gather context (diff vs origin/main, CLAUDE.md, prior review comment)
  • Check whether the prior finding (_arrays_are_parallel one-directional check) was addressed
  • Trace new/changed code in this revision
  • Check contract regeneration and CLAUDE.md compliance
  • Post final review

Summary

This push (1c49f97, f904fb4) responds to the prior review's finding about _arrays_are_parallel in backend/routes/node_schemas.py — and the response is correct, not a bug fix I'd ask for again.

Prior finding resolved by clarification, not code change

The earlier review flagged that a hex in adsb_hex[i] beside adsb[i] = None validated cleanly, calling it asymmetric versus the docstring's "agree entry for entry." f904fb4 explains why that's intentional: a hex beside a null tag means the node matched the aircraft but had no usable position for it (retina-telemetry#19 sends this when lat/lon aren't finite) — that's a correlation without a fix, not a disagreement. Refusing it would reject a real, valid frame shape.

The fix here is documentation + a pinning test rather than a logic change:

  • backend/routes/node_schemas.py:253-259 — docstring now says explicitly what the check allows ("a hex beside a null tag is one correlation without a position").
  • backend/tests/test_node_schemas.py::TestAdsbTags::test_a_hex_beside_a_null_tag_is_accepted — pins the behavior so a future change can't silently tighten it back.

I traced known_claiming.claim_known_targets and passive_radar.process_frame again to confirm they still key exclusively off frame["adsb"], never adsb_hex — so this asymmetry has no operational effect on claiming/tracking, only on what a hex-only entry means in the schema. That matches what the prior review already verified.

Rest of the diff

  • backend/services/node_pipeline.py — _hexes()/_tag_record() conversion is correct: alt→alt_baro, null fields omitted (geolocator branches on key presence), hex list built from adsb_hex when present, else derived from tags, else all-None.
  • backend/services/frame_processor.py — comment update accurately reflects that v1 frames can now carry adsb.
  • backend/routes/nodes.py — version bump to 1.5.0 with a reasoning comment consistent with CLAUDE.md's contract rule.
  • contracts/nodes-v1.openapi.yaml — regenerated correctly: version bump, new AdsbTag component, adsb_hex moved to optional+deprecated: true, adsb added, required list updated to drop adsb_hex. Matches the schema changes exactly.
  • Test coverage (test_node_schemas.py, test_node_pipeline.py, test_detection_mirror.py, test_node_streaming.py) covers round-trip, length mismatches, both agreement directions, tag-field omission, mirror byte-identity for hex-only frames, and an end-to-end path-1 claim — all consistent with what the diff actually does.
  • No secrets, addresses, or .env.example gaps introduced.

I could not run pre-commit or the test suite myself (no backend/.venv in this sandbox) — same limitation as the previous review pass.

Verdict

No outstanding findings. The one issue from the prior review is resolved appropriately (explained + tested rather than "fixed" into a stricter check that would reject valid frames).
(branch: feat/v1-adsb-position-tags)

@Babissimo
Babissimo merged commit 4e54fd1 into main Sep 22, 2026
16 checks passed
@Babissimo
Babissimo deleted the feat/v1-adsb-position-tags branch September 22, 2026 16:20
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