Withhold receiver-relative geometry from the public diagnostic dumps - #355
Conversation
This comment has been minimized.
This comment has been minimized.
Every receiver coordinate the API publishes is displaced
(services/public_location.py), but the unauthenticated /api/test/* payloads
carried quantities measured from the TRUE receiver to a point the same payload
gives the position of: a beam-gate range and bearing, a bistatic range against
a transmitter published untranslated, an FOV read-out at the true bearing off a
curve /api/radar/analytics also publishes, a per-track bistatic delay. Each is
a locus the receiver sits on, and enough of them intersect well inside the
0.5-1 km fuzz, so the displacement was decorative on any node appearing in one
of these dumps.
services/public_geometry.py puts the rule in one place: a per-node constant is
the envelope and may be published, a per-record value that varies with the true
geometry is a measurement and is withheld. The pass is structural over
containers rather than a per-route field list, so a field is withheld at any
nesting depth and a new record shape needs no list widened. It matches on the
leaf key alone, which is its one real limit and is stated in the module.
Applied to each payload at its serialisation edge rather than per record, so a
key added to one of these payloads later cannot ship raw by being one the
author forgot to wrap. /api/test/mlat-verification is the exception: both
readers of its store are unauthenticated, so _publish_mlat_verification strips
once on write rather than on every request, and both writers now go through it.
/api/test/node/{id}/verification is node-scoped, so it also withholds
solver_lat and solver_lon: every track entry is measured from that one node's
receiver, and the aircraft feed publishes the same solve position displaced
under the same hex, so the true frame beside it differences to the offset. A
multinode payload keeps them, since no single receiver is behind that position.
node_detection_range's hand-rolled furthest_detections drop becomes a call to
the same module, so that rule has one owner rather than two. That summary's
keys come from the retina_analytics submodule, so a test pins its overlap
with the withheld set: an upgrade that adds a colliding name fails there
instead of quietly dropping the field from the payload.
Closes 123zgec25me. Leaves 123zgec25a0 (position_error_km, which needs a
decision about what the route is for) and 123zgec25bc (the private-node
opt-out) open, both already tracked.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
cb62151 to
cce9b99
Compare
|
Closed the review's one finding, folded into the existing commit rather than stacked. It was a consequence of consolidating
No collision today. Current keys are Backend 3286 passed, 2 skipped. Lint gate green. One run in between failed |
|
Claude finished @Babissimo's task in 3m 35s —— View job Reviewing PR #355
ReviewTraced all four call sites ( Confirmed correct:
On the concern raised in the prior review pass (leaf-name collision between Minor, non-blocking observation: Could not execute |
Resolves 21 conflicting files against 53 commits on main, including the two pieces already split out of this PR (#351 identity scrub, #355 receiver-geometry withholding), #358 site markers / empirical coverage, #348 location privacy, #350/#353/#354 owner contact and retnode links, and the #343/#346/#347/#357 palette work. Rules applied: main's version is the base for the add/add files (public_geometry.py, test_no_real_identities.py); the PR's ref-keying is layered on top of main's rework everywhere else; substitution still runs last on every publication path and fails closed for unregistered real nodes. Follow-on edits outside the conflicted set, needed to compile or to keep main's tests honest under ref addressing: frontend nodeSites.ts (+test) now reads is_synthetic; backend test_node_ref.py and test_public_geometry.py address routes by ref / synthetic id; analytics.py's owner-private listing merge keys on the owner identity. Verified: backend 3495 passed / 2 skipped, ruff clean; frontend tsc, lint, vite build, vitest 232 passed; dashboard tsc, eslint, vitest 53 passed, build. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The merge with main left /api/test/mlat-history running the geometry pass twice: per record inside _published_records (this branch) and again over the whole payload at the three return sites (#355). The per-record pass is the one that has to stay, because it must precede the identity walk (foreign_node_ids is both a withheld measurement and an identity field the walk renames). The outer pass stripped nothing beyond it: every record list already goes through _published_records and the remaining top-level keys are window parameters and counts. tests/test_public_geometry.py still fetches all three shapes of the route and asserts nothing from the withheld set appears at any depth, so a future field added outside `records` fails there regardless. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
/api/test/*published per-record quantities measured from a node's TRUE receiver to a point the same payload gives the position of.services/public_location.pydisplaces every receiver coordinate the API publishes, and each of these handed that displacement back: a beam-gate range and bearing to a located aircraft, a bistatic range against a transmitter published untranslated, an FOV read-out at the true bearing off a curve/api/radar/analyticsalso publishes, a per-track bistatic delay. Enough of them intersect well inside the fuzz, which is 0.5–1 km, so defeating it resolves an address rather than a neighbourhood.Second of five, split out of #349. Closes 123zgec25me.
The rule, in one place
services/public_geometry.py. A per-node constant is the envelope and may be published; a per-record value that varies with the true geometry is a measurement and is withheld.Each field carries its own reason beside it, because the argument differs per field.
bistatic_kmgoes because the transmitter is published untranslated, so the differential range fixes the receiver on a hyperbola through two known foci.fov_limit_kmgoes because the curve it is read off is itself published asempirical_polygon, so a value inverts to the bearing it was read at. The FOV shadow verdicts go because they are stamped for the nodes that PASSED as well as the ones that failed, so each bounds the receiver to a region where a failure would only exclude one.The pass is structural over containers, so a field is withheld at any nesting depth without a per-route list. It matches on the leaf key name alone, which is its one real limit and is stated in the module: the same quantity under a name the module has not seen stays published.
Where it is applied
At each payload's serialisation edge rather than per record, so a key added to one of these payloads later cannot ship raw by being one the author forgot to wrap.
/api/test/mlat-verificationis the exception. Both readers of its store are unauthenticated, so_publish_mlat_verificationstrips once on write rather than on every request, and both writers go through it — the early-return path guarded nothing before, which is a maintenance trap rather than a live bug, since that literal carries none of the withheld fields today./api/test/node/{id}/verificationis node-scoped, so it also withholdssolver_lat/solver_lon: every track entry is measured from that one node's receiver, and the aircraft feed publishes the same solve position displaced under the same hex, so the true frame beside it differences to the offset. A multinode payload keeps them, since no single receiver is behind that position. Stripped at the route rather than in the store, because it is the route's one-node addressing that makes the solve position receiver-relative.node_detection_rangedroppedfurthest_detectionswith its own dict comprehension. That becomes a call to the same module, so the rule has one owner.public_location.public_node_summarystill hand-rolls the third copy; different module, out of this PR's seam.That summary's keys come from the
retina_analyticssubmodule rather than this repo, and the pass matches on leaf key name, soTestTheDetectionAreaSummarypins the overlap: the intersection is exactly{furthest_detections}today, and an upgrade that adds a colliding name fails there instead of quietly dropping the field.The tests are the deliverable
backend/tests/test_public_geometry.py, 260 lines against 133 of production code.Unit tests on the helper prove nothing about whether a route calls it, so six go through the routes. Verified by mutation rather than by assertion: strip the call from
node_verification, from anymlat_historyreturn, or from_publish_mlat_verification, and exactly those six fail, then pass again on restore.Left deliberately
position_error_kmstays. It inverts against the aircraft feed under same-epoch cross-surface pairing, which needs a decision about what the route is for rather than a field drop. 123zgec25a0.private_node_ids()filter. 123zgec25bc.truth_lat/truth_lonstay. Public ADS-B positions, and removingmeasured_delay_usalready breaks the bistatic ranging equation they sat in.Verification
Backend 3286 passed, 2 skipped.
pre-commit run --all-filesgreen.No client reads a withheld name: the
solver_lat/solver_lonthatLiveAircraftMapuses come from/test/mlat-verification, which is multinode and keeps them.max_range_km,max_bistatic_range_kmandestimated_range_kmare separate keys from the withheld barerange_kmand are untouched.One first run failed
test_known_lane.py::test_interval_gate_holds_between_passes, the flake documented on main since b0bbeb4. Not this branch: it passes alone, passes after this file, and passed on the re-run, and the six newTestClient(app)calls leak zero threads (measured: 1 before, 1 after), so they add none of the solver daemons that fixture's docstring names as the mechanism.🤖 Generated with Claude Code