Skip to content

Reject non-transponder hexes from the ADS-B world - #252

Merged
jehanazad merged 1 commit into
mainfrom
fix/dark-adsb-lane
Aug 26, 2026
Merged

jehanazad merged 1 commit into
mainfrom
fix/dark-adsb-lane

Conversation

@jehanazad

Copy link
Copy Markdown
Contributor

Summary

Dark simulated aircraft were displayed as ADS-B assisted — universally, not intermittently. The fleet's 1 Hz ADS-B push substitutes the simulator object id (obj-NNNNN) for the missing transponder hex, and both the sim ingest and the solver keying rule accepted any truthy string: every dark solve claimed against its own pseudo-transponder, keyed mn-adsb-obj-*, and the feed marked it adsb_assisted. Measured live 2026-08-26: 15008/15008 multinode samples ADS-B-assisted, the mn-dark-* store (violet lane, anchor honoring, proximity claiming) permanently empty, "MLAT dark 0" on the map's stats panel.

Changes

  • services/id_utils.py — new is_transponder_hex: six hex digits, tar1090's ~ prefix allowed for non-ICAO TIS-B addresses. Defined over normalize_hex_key output.
  • routes/sim_ingest.py/api/sim/adsb/push drops non-transponder hexes, reports rejected_hex in the response, and bumps a new sim_adsb_push_rejected_hex counter so an outdated fleet still pushing dark aircraft is visible.
  • services/tasks/solver.pymultinode_key_decision refuses the mn-adsb-* branch for a non-transponder adsb_hex; the solve falls through to the dark branches (anchor / proximity / mint) instead.
  • core/state.py / vulture_whitelist.py — the counter declaration, its reset, and the string-keyed-bump whitelist entry.

Test coverage

  • New tests/test_id_utils.py pins the hex shape rule (including the ~ TIS-B form and the normalized-input contract).
  • tests/test_sim_ingest.py gains TestAdsbPush: ICAO hex accepted, ~ hex accepted, obj-* rejected without touching state.adsb_aircraft.
  • tests/test_solver_anchor.py gains two key-decision cases: obj-* in adsb_hex falls through to proximity claiming; ~ hex still takes the ADS-B branch.
  • Ran test_id_utils.py, test_solver_anchor.py, test_sim_ingest.py, test_sim_ingest_mount.py (55 passed) and the pre-commit gate (ruff, vulture) clean.

Review notes

🤖 Generated with Claude Code

A dark simulated aircraft has no transponder, but the fleet's 1 Hz ADS-B
push substitutes the simulator object id (obj-NNNNN) for the missing hex,
and both the sim ingest and the solver keying rule took any truthy string.
Every dark solve then claimed against its own pseudo-transponder, keyed
mn-adsb-obj-*, and the feed marked it adsb_assisted — measured live
2026-08-26 as 15008/15008 multinode samples ADS-B-assisted with the
mn-dark-* store (violet lane, anchor honoring, proximity claiming)
permanently empty.

Three layers, one rule (id_utils.is_transponder_hex — six hex digits,
tar1090's ~ prefix allowed for non-ICAO TIS-B):

- /api/sim/adsb/push drops non-transponder hexes, reports rejected_hex in
  the response, and counts them (sim_adsb_push_rejected_hex) so an
  outdated fleet still pushing dark aircraft is visible.
- multinode_key_decision refuses the mn-adsb-* branch for them, so a
  poisoned adsb_hex can no longer put a dark target in the ADS-B lane.

The fleet-side fix (stop pushing dark aircraft at all) lands separately
in retina-simulation; this side must hold regardless of fleet version.

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

claude Bot commented Aug 26, 2026

Copy link
Copy Markdown

Claude finished @jehanazad's task in 2m 55s —— View job


Claude's review

  • Gather context (PR description, diff, CLAUDE.md)
  • Read changed files in full
  • Review is_transponder_hex / id_utils.py logic
  • Review sim_ingest.py push-rejection changes
  • Review solver.py key-decision change
  • Review test coverage
  • Post final review

I traced adsb_hex back through every call site that reaches multinode_key_decision (known_lane.py, frame_processor._view_adsb_hex) to confirm the normalize→gate contract actually holds end to end, not just at the two touched call sites. I don't have a working .venv in this sandbox, so I read/reasoned through the logic rather than re-running pytest/pre-commit myself — the PR body already reports 55 passed and a clean gate.

Summary: this is a well-scoped, well-tested fix. The root cause (a truthy-string check accepting obj-NNNNN as if it were a transponder hex) is real, the new is_transponder_hex gate is applied consistently at both places that mattered (sim_ingest.py's push and solver.py's keying rule), and the regex + fullmatch combination correctly handles the ~-prefixed TIS-B case without becoming overly permissive. I verified normalization is applied before the gate at every call site I could find (sim_ingest.py:127, frame_processor.py:245, known_lane.py:456), so the "input is already normalized" contract documented in id_utils.py isn't violated anywhere today.

One minor nit, nothing blocking:

  • backend/services/tasks/solver.py:630multinode_key_decision's docstring still says "ADS-B-tagged solves key on the transponder hex — unconditional". That's no longer true after this PR's adsb_hex and is_transponder_hex(adsb_hex) gate a few lines below (line 658). The new inline comment explains the gate well, but the higher-level "Order" docstring a few lines up wasn't updated to match, so a future reader skimming just the docstring gets the old, now-incorrect picture. Fix this →

Nothing else stood out:

  • is_transponder_hex's regex (~?[0-9a-f]{6} with fullmatch) correctly rejects obj-*, over/under-length, unnormalized-case, and double-tilde inputs per the new test matrix in test_id_utils.py.
  • The sim_adsb_push_rejected_hex counter follows the existing string-keyed bump_counter pattern exactly (declaration, _reset_for_tests, vulture whitelist entry all present and consistent with neighboring counters like known_claims_made).
  • known_lane.py's _publish path (a second, pre-existing caller of multinode_key_decision not mentioned in the PR description) is unaffected in practice — its hexn is always normalize_hex_key'd from already-claimed ADS-B hexes, so it can't regress here, though it does share the same now-slightly-stale "unconditional" assumption noted above.
  • No contracts/nodes-v1.openapi.yaml update needed — /api/sim/adsb/push isn't a /v1/nodes route.
  • Test coverage directly targets the regression (dark object-id falls through to proximity, ~-hex still takes the ADS-B branch, push endpoint rejects/report correctly).

@jehanazad
jehanazad merged commit 3ca0285 into main Aug 26, 2026
13 of 14 checks passed
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.

1 participant