From 664462819fe1d381edc4b101af2c334ad8ae8e03 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 5 Sep 2026 06:25:08 +0000 Subject: [PATCH] Gate overlap zones on node world, and report the skipped pairs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps libs/retina-analytics to the branch commit that stops register_node and rebuild_zones_for building an overlap zone between two nodes in different worlds (offworldlabs/retina-analytics#25). This fleet is the reason: 50 synthetic nodes and 8 receivers share one footprint over Greenville, so the associator held a zone for all 400 sim/real pairs — 39 of them a live adjacency edge — and a pairing drawn from one of those grids can only ever match a simulated tracklet against a real echo. Real node ids showed up in 48 of 156 synthetic dark solver records over a 7-minute window. node_world is already the single authority for the question and is already injected as node_world_provider, so nothing new is wired here. /api/radar/association/status gains assoc_world_skipped_pairs next to overlap_zones: without it a fleet whose cross-world pairs are being refused reads exactly like a fleet whose pairs never overlapped. Co-Authored-By: Claude Fable 5.1 --- backend/routes/analytics.py | 8 +++++++ backend/tests/test_adsb_seed_backend.py | 29 +++++++++++++++++++++++++ backend/tests/test_analytics_routes.py | 12 ++++++++++ libs/retina-analytics | 2 +- 4 files changed, 50 insertions(+), 1 deletion(-) diff --git a/backend/routes/analytics.py b/backend/routes/analytics.py index 07d83a9c..23225ccf 100644 --- a/backend/routes/analytics.py +++ b/backend/routes/analytics.py @@ -125,6 +125,14 @@ async def association_status(): return { "registered_nodes": len(_a.node_geometries), "overlap_zones": len(_a.overlap_zones), + # Node pairs that got no grid because the two nodes are in different + # worlds (node_world above). Read next to overlap_zones: on a fleet + # of 50 synthetic nodes over the same city as 8 receivers it is the + # 400 sim/real pairs whose grids could only ever have paired a + # simulated echo with a real one. Counted per pair considered, so it + # keeps rising as nodes re-register — zero means the fleet is single- + # world (or untagged), not that the gate is off. + "assoc_world_skipped_pairs": getattr(_a, "assoc_world_skipped_pairs", 0), # Confirmed single-node tracks each node last submitted; these are what # pairings are drawn from. "pending_tracks": {nid: len(tracks) for nid, tracks in list(_a._pending_tracks.items())}, diff --git a/backend/tests/test_adsb_seed_backend.py b/backend/tests/test_adsb_seed_backend.py index cba3bb36..77b3fc86 100644 --- a/backend/tests/test_adsb_seed_backend.py +++ b/backend/tests/test_adsb_seed_backend.py @@ -605,3 +605,32 @@ def test_associator_gets_the_state_world_resolver(self): must consult the same resolver claiming and the auto-tag filter use, or one consumer accepts what another rejects.""" assert state.node_associator.node_world_provider is state.node_world + + def test_a_sim_and_a_real_node_over_one_footprint_get_no_overlap_zone(self): + """The same resolver, one level down: bottom-up pairing must not build + a grid across worlds either. Registering a synthetic node and a + hardware node on overlapping coverage used to leave a zone whose only + possible pairing was a simulated echo against a real one — which is how + real node ids reached the synthetic fleet's dark solves.""" + _a = state.node_associator + try: + _a.register_node("synth-GVL-9001", dict(_NODE_CFG)) + _a.register_node("hw-9001", dict(_NODE_CFG, rx_lat=34.86, rx_lon=-82.36)) + + assert _a.overlap_zones == {} + assert _a._neighbors.get("synth-GVL-9001", set()) == set() + assert _a.assoc_world_skipped_pairs == 1 + finally: + state._reset_for_tests() + + def test_two_synthetic_nodes_over_one_footprint_still_pair(self): + """The gate is the world difference, not the registration.""" + _a = state.node_associator + try: + _a.register_node("synth-GVL-9001", dict(_NODE_CFG)) + _a.register_node("synth-GVL-9002", dict(_NODE_CFG, rx_lat=34.86, rx_lon=-82.36)) + + assert _a.overlap_zones + assert _a.assoc_world_skipped_pairs == 0 + finally: + state._reset_for_tests() diff --git a/backend/tests/test_analytics_routes.py b/backend/tests/test_analytics_routes.py index 22a821e0..73c691d4 100644 --- a/backend/tests/test_analytics_routes.py +++ b/backend/tests/test_analytics_routes.py @@ -129,6 +129,18 @@ def test_status_returns_expected_fields(self, client): assert "overlap_zones" in body assert "overlaps" in body + def test_status_reports_world_skipped_pairs(self, client): + """The world gate on overlap zones is otherwise invisible: a fleet + whose sim/real pairs are being refused looks exactly like a fleet whose + pairs never overlapped, and only this counter separates them.""" + _a = state.node_associator + _a.assoc_world_skipped_pairs += 7 + try: + body = client.get("/api/radar/association/status").json() + assert body["assoc_world_skipped_pairs"] == 7 + finally: + state._reset_for_tests() + def test_status_includes_claiming_block(self, client): """Top-down claiming (ASSOC_CLAIM_MODE) since boot — off by default in tests, so this pins the shape rather than any particular mode.""" diff --git a/libs/retina-analytics b/libs/retina-analytics index 14504176..4439c2c7 160000 --- a/libs/retina-analytics +++ b/libs/retina-analytics @@ -1 +1 @@ -Subproject commit 145041767422723d89d98ec003f843347ddbb880 +Subproject commit 4439c2c7d2d4fb49ca6646000f3bedc25f9701c4