Skip to content

Calibrate empirical coverage from the claim lane, not the emit loop - #368

Merged
jehanazad merged 7 commits into
mainfrom
feat/calibration-from-claims
Sep 15, 2026
Merged

jehanazad merged 7 commits into
mainfrom
feat/calibration-from-claims

Conversation

@jehanazad

Copy link
Copy Markdown
Contributor

Summary

Empirical-coverage calibration has been effectively dead since #240 (2026-08-25) made KNOWN_LANE_MODE=binding the default. The only recorder was the tracker's emit loop, which needs an ADS-B-tagged detection to reach the tracker, and binding strips every claimed detection before it gets there. Measured on the test deployment's persisted bins on 2026-09-13: every synthetic node's newest calibration point is dated exactly 2026-08-25; radar3/radar3a get a trickle (newest 3–6 days old) made only of detections the claim lane refused but the greedy tagger accepted; the ret* TCP nodes have no calibration file at all. Every coverage polygon on every environment is a frozen pre-#240 picture drawn by the old greedy binder, and that binder was wrong about the aircraft roughly a third of the time (47 % of one 42° synthetic node's points lay outside a wedge the simulator never emits detections outside of).

This moves calibration into the claim lane under a rule much stricter than claiming, and silences the emit-loop path unless the lane is off. Real and synthetic nodes are treated identically here; which polygon is published is #367's business.

Stacked on #367 (base branch = its head); merge order: retina-analytics #28, #32, then #367, then this.

The rule (services/known_claiming.py::_calibration_from_claim)

A claim records a calibration point only if all five hold:

  1. it has a fresh transponder fix behind it: a node tag (path 1), a cached-fix assignment (path 2), or a hold whose fix was refreshed this frame (path H after _claim_holds' consistency check against a live fix); an unrefreshed hold or a follow claim never records;
  2. that fix is at most CAL_MAX_ADSB_AGE_S (10 s) old at the frame instant;
  3. the residual against the fix's prediction is within CAL_CLAIM_DELAY_US (3 µs) and CAL_CLAIM_DOPPLER_HZ (8 Hz), unscaled by age — about a tenth of the claim gate's area;
  4. the detection is uncontested: no dark projection inside the claim gate, and no other known hex whose prediction lies inside the gate (the candidate list is now built for the whole frame, not just the unclaimed detections, so every claim can be checked for rivals; claiming results are byte-identical);
  5. the link is mature: at least CAL_CLAIM_MIN_CLAIMS (3) claims of this hex by this node with no gap over CAL_CLAIM_STREAK_GAP_S (10 s).

The position recorded is the fix dead-reckoned to the frame instant, so the old fix-vs-detection skew rule does not apply; services/calibration.py::record_claim_calibration documents that as the module's fourth rule. Per-reason counters (calibration_points_recorded, calibration_claims_rejected_{hold,stale_fix,residual,contested,immature}) are in the stats payload beside the known_claims block, and the runbook says how to read them.

Measured offline (backend/scripts/calibration_attribution_bench.py)

Simulator world, 20 nodes, truth = the aircraft that actually produced each detection. points/node-min · wrong-hex · out-of-wedge, seed 7 / 240 s (seeds 42–44 in the commit body agree):

source blind node (path 2 + holds, the hardware case) tagged node (path 1)
old greedy tagger (the literal old recorder) 82.0 · 9.5 % · 9.3 % same
every claim, ungated 70.0 · 0.5 % · 0.1 % 74.5 · 0.4 % · 0.1 %
this rule 63.9 · 0.1 % · 0.0 % 68.2 · 0.1 % · 0.0 %

The first cut of rule 1 refused all holds and starved blind nodes to ~0.1 points/node-min because path H outranks path 2 from the second frame of every link; judging refreshed holds on their fresh fix (commit 2242780) restored the yield without loosening any constant.

Changes

  • services/known_claiming.py: candidate list built per frame for every claim's rival check; _fresh_fix_prediction returns the DR position; refreshed holds carry the fresh-fix prediction and DR position on extra (filtered out of the registry entry, which stays byte-identical); _calibration_from_claim + streak store; per-frame counter flush.
  • services/calibration.py: record_claim_calibration, fourth rule in the docstring.
  • services/track_gates.py: emit-loop recording only when KNOWN_LANE_MODE == "off".
  • config/constants.py: the four CAL_CLAIM_* constants with derivations. core/state.py, routes/test.py, vulture_whitelist.py: counters.
  • scripts/calibration_attribution_bench.py: the measurement above, --both, --hold-gap.
  • docs/pipeline.md §7 rewritten for the claim-lane source; docs/runbook.md funnel entry.

Test coverage

  • tests/test_calibration_from_claims.py (20 new): each rule's reject, the third-claim record, DR position, rival candidate, dark contention, node tags, streak reset, refreshed hold records / unrefreshed does not / refreshed judged on the fresh residual / registry entry carries no calibration keys; lane on ⇒ emit loop records nothing, lane off ⇒ unchanged.
  • Existing claiming, hold, follow, TCP-claim and track-gate tests pass unchanged; test_solver_stats.py updated for the two new keys in the known_claims stats block.
  • Full backend suite exit 0 (3536 passed, 1 skipped); pre-commit gate and identity guard clean.

Review notes

  • Yield is deliberately ~15 % below "every claim": rules 3–5 are what buy the 0.1 % wrong-hex rate. Tune constants only with the bench.
  • After deploy, calibration_points_recorded should climb within minutes on every claiming node, and the newest bin_pos_ts in coverage_data/empirical_<id>.json should move; live check on test owed.
  • Existing persisted bins are still the old greedy picture. They will be diluted, not replaced (200-point FIFO per bin). A one-off reset of coverage_data on deploy would give clean polygons faster; not done here — operator's call.

🤖 Generated with Claude Code

jehanazad and others added 6 commits September 13, 2026 22:09
Every node's published `empirical_coverage.polygon` is evidence-only, built
from calibration points that come from ADS-B hexes bound to tracks. That is
right for real hardware — a real node's declared beam azimuth and width are
unsurveyed configuration, and drawing them would claim coverage nobody
measured — but wrong for the simulator fleet. The simulator emits a detection
only for an aircraft inside the node's declared cone, so for a synthetic node
the cone IS the detection area by definition, and the evidence is the
unreliable half: roughly a third of those hex binds are to the wrong aircraft.

Measured on test 2026-09-13, synth-GVL-SCAT-0032 (42 deg beam) held 3,037
calibration points of which 47% lay outside its wedge (34% ignoring the two
edge bins), 55 out-of-wedge bins had opened, and the published polygon covered
71 of 72 bearings. Every synthetic node on the test map draws as a disc.

So register_node_blocking passes declared_geometry_is_truth=is_synthetic_node
to the analytics registration, and the library publishes
declared_wedge_polygon() for those nodes instead (retina-analytics 6df7c3d,
pinned here). Real nodes are untouched. Both kinds now name their rule in
`empirical_coverage.polygon_source`, so no consumer has to guess which shape
it was served.

is_synthetic_node is imported inside the function: tcp_handler imports this
module at import time, the same cycle analytics_refresh and core.state already
step around the same way.

test_ingest_event_loop's registration stubs take **kwargs — they stand in for
the analytics registry, which now receives one more keyword.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The site popup quotes measured coverage and nothing else — the declared beam
azimuth, width and range were deliberately removed from it, because most
nodes' aim was never surveyed and beside a calibration-point count they read
as measurements.

That stays true for real nodes. A synthetic node is now served its declared
cone as its detection area (the simulator emits a detection only inside it, so
it is the detection area by definition), and the backend says which rule it
used in `empirical_coverage.polygon_source`. NodeInfo carries it through as
`empirical_polygon_source`, and the popup line moves into a pure
`coverageLine(node)` helper in nodeSites.ts so the wording is unit-tested
rather than buried in JSX: the declared beam is quoted only when the source
reads "declared", and "learned" is treated as measured, because it is.

Missing on a payload from an older server, where it defaults to "evidence" —
the conservative reading, and what every node published then.

CoverageLayer needs no change: it draws whatever polygon it is served.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
services/calibration.py has held the one age rule and the one skew rule since
the frame and solve paths disagreed on them. It gains a third call site's
entry point — record_claim_calibration — and a fourth rule in the docstring
saying which call site may record at all.

The claim lane's recorder shares the age rule (at 250 m/s a 10 s fix is 2.5 km
stale against a 5-degree grid) and deliberately does not share the skew rule.
That is not an omission: its caller dead-reckons the fix to the frame instant,
so the fix and the detection it describes are the same instant by construction
and there is no skew left to bound. The test says so explicitly, because a
later reader would otherwise "fix" the missing gate.

The four constants are the gates the caller applies. 3 µs / 8 Hz against the
claim gate's 10 µs / 25 Hz: the simulator's measurement noise is sigma 0.1-0.2
µs and 2-4 Hz, so this is still over 5 sigma at the noisy end while shrinking
the delay x Doppler area a wrong aircraft can land in by ~10x. Three claims
with no gap over 10 s is the maturity bar, the counterpart of the emit path's
n_detections >= 3 — a wrong bind can be a coincidence once, not frame after
frame at a consistent (delay, Doppler).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Empirical-coverage calibration has been dead since 2026-08-25 and nobody
noticed, because the thing that killed it was a default flip. KNOWN_LANE_MODE
went to "binding" in #240, and binding mode strips every detection the claim
lane binds out of the frame before the tracker sees it. The emit loop records
only when track.last_detection_adsb_hex is set, and that is set only from a
tagged detection the tracker actually saw — so for a synthetic node, whose
every detection is claimed, it is never set at all.

Measured on the test deployment's persisted bins 2026-09-13: every synthetic
node's newest calibration point is dated exactly 2026-08-25, 19 days old. Real
nodes get a trickle (3-6 days) because a few tagged detections survive
claiming — but those are precisely the binds claiming refused, which is
adverse selection, and it shows: across eight nodes 5-41 % of the points sit
outside the node's own declared wedge, and the simulator only ever emits a
detection inside it. synth-GVL-SCAT-0032, a 42-degree beam, held 3,037 points
of which 47 % were out of wedge.

So the claim lane becomes the one source whenever it runs, and the emit-loop
block is gated on mode "off" (where it is unchanged, rules and all). In shadow
mode the reason is different and just as decisive: nothing is stripped, so
both paths would record the same detection twice and the bin-count gates would
be reading a doubled denominator.

A claim is not automatically a calibration point. Claiming binds what it can
explain; calibration records what it could not have explained any other way,
and _calibration_from_claim charges every claim to exactly one of six
outcomes: recorded, or one of the five rules, in order. No hold or follow
claim (neither has a fresh transponder fix behind it). Fresh fix at the frame
instant. Tight residual, unscaled by fix age. Uncontested — no dark projection
inside the claim gate, and no OTHER known hex's prediction inside it either.
Mature link — three claims, no gap over 10 s.

Rule 4's second half is what the path-2 restructure is for. The candidate list
is now built for every frame carrying detections, and without skipping the
hexes paths 1 and H already took; the claimed_hexes skip moved to the
assignment's column build, so the cost matrix and every claim are exactly what
they were, while a path-1 tag's detection can finally be judged against the
rest of the cache. The published reject tallies are pinned to what the
ASSIGNMENT rejected, so widening the population does not inflate them.

The streak store is deliberately not the hold store: KNOWN_HOLD_MAX_GAP_S <= 0
is a supported rollback and calibration must not switch off with it. Every
non-hold claim advances the streak, including ones that fail rules 3-4 — those
are still evidence of the link, just not clean samples, and withholding them
would make a node's first clean sample its third, which on a noisy link may
never arrive.

The recorded position is the fix dead-reckoned to the frame instant, carried
from the assignment that computed it rather than re-derived, on a private
`extra` key filtered back out before the claim record is written — so
state.known_claims is byte-identical.

KNOWN GAP, pinned by a test rather than left to be rediscovered: path H
outranks path 2 and every claim creates a hold, so a node that sends no
frame["adsb"] is claiming through path H from the second frame of each link
onwards, and rule 1 refuses those. Such a link never matures. The whole
synthetic fleet sends tags (path 1 outranks path H) so it is unaffected;
KNOWN_HOLD_MAX_GAP_S=0 isolates it. See docs/pipeline.md for the numbers and
the fix.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The five rules are only worth their cost if they buy attribution, so this
measures it rather than asserting it. Truth is exact, not inferred: the
simulator staples the producing aircraft's hex to each detection index, so the
frame read BEFORE the tags are stripped says which aircraft every detection
belongs to, and None means no transponder produced it — clutter or a dark
target — against which any hex is wrong by construction.

Three rules are scored side by side on the same frames. "greedy" is the
literal old source: associate_detections_to_adsb's tags, which is what set
track.last_detection_adsb_hex, recorded at the reported position, with no
one-to-one, no world gate and no visibility gate. "claims" is every claim the
lane makes recorded ungated — the fairer baseline, since the new rule is a
filter on exactly that population. "new" is the five rules, scored through
_calibration_from_claim itself so the bench reads the hex and the position the
rule actually accepted rather than re-deriving either.

20 synthetic nodes, 3 seeds, 5 simulated minutes each, ~26-30 aircraft:

                     wrong-hex    out-of-wedge   points/node-min
  greedy             8.9-11.0 %    8.1-10.0 %      69-100
  claims             0.9-3.5  %    0-1.3    %      57-92
  new                0.0-0.1  %    0.0      %      51-82

...except blind with path H enabled, where the yield collapses to 0.11-0.13
points per node-minute with 97 % of claims charged to rejected_hold. That is
the gap the previous commit documents, and --hold-gap is the lever that
isolates it; the 51-82 figures above are the tagged legs and the blind legs
with KNOWN_HOLD_MAX_GAP_S=0.

The bench swaps in its own in-memory NodeAnalyticsManager: the module-level
one is wired to backend/coverage_data, so registering there would load a
deployment's persisted bins into the run and write the run's points back out.

docs/pipeline.md section 7 is rewritten around the new source — the five
rules, the counters, the dead-since-#240 finding and the path-H gap — keeping
the emit-loop rules for mode off. The runbook gains the funnel and how to read
a flat `recorded`.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The previous commit shipped the claim-lane calibration rule with a measured
hole in it: a node that sends no frame["adsb"] yielded 0.11-0.39 calibration
points per node-minute against the 51-68 a tagged node gets, with ~97% of its
claims charged to rejected_hold.  That is every hardware receiver without its
own ADS-B correlation, which is the population the empirical polygon exists
for.

The cause was an ordering, not a judgement.  Path H runs before path 2 and
every claim creates a hold, so from the second frame of a link onwards the
claim is a hold claim; rule 1 refused it AND returned before the maturity
streak was touched, so the link could never reach CAL_CLAIM_MIN_CLAIMS.  Rule
1 was written for a hold with nothing behind it, and it was catching holds
with a live transponder behind them.

A hold carrying extra["fix_refreshed"] is not that hold.  The consistency rule
in _claim_holds has already compared this very detection against the LIVE
cached fix inside path 2's own age-scaled gate, and the claim carries that fix
rather than the hold's frozen one.  It is a path-2 claim in everything but
which path found the detection, so calibration now judges it as one: rule 2
against the fresh fix's fix_ts_ms (which is what the claim already carries),
rule 3 against the FRESH FIX's prediction rather than the hold's propagated
one, rule 4 against the same candidate population as any other claim, rule 5
advancing on it, and the position recorded is that fresh fix dead-reckoned to
the frame instant.

Rule 3's reference is the part that is not cosmetic.  A hold predicts this
node's next measurement from this node's last measurement, so its residual is
near zero whatever aircraft is actually out there — judging a calibration
point on it would be circular, and the 3 us gate would be measuring the
propagator rather than the attribution.  _fresh_fix_prediction therefore hands
back the dead-reckoned position it already computes alongside the prediction,
for the same reason path 2's candidate loop does: re-deriving it at the
recording site would be a second offset_latlon_m free to disagree with the one
the prediction was built from.  Both ride on the claim's private `extra` and
are filtered back out before the registry record is written, now through
_CAL_EXTRA_KEYS rather than a single-key comparison, so state.known_claims is
byte-identical and a test pins its exact key set.

A hold whose fix is gone, or has aged past KNOWN_CLAIM_MAX_FIX_AGE_S so
_fresh_fix_prediction stops answering, carries no fix_refreshed and is still
refused by rule 1 — both halves are tested, the second through the window
where the fix is merely stale (rejected_stale_fix) before it ages out
entirely (rejected_hold).

Re-measured with calibration_attribution_bench.py, 20 synthetic nodes, seeds
42/43/44, 5 simulated minutes, same seeds old and new (the old module run from
a shadow tree so both legs see identical traffic):

                          old rule (new)      this commit
  blind, holds ON      0.11-0.39 /node-min   54.5-61.3 /node-min
  blind, holds OFF     51.2-60.6             51.2-60.6  (unchanged: no holds)
  tagged (path 1)      57.6-67.5             57.6-67.5  (+0.1, 39-78 hold
                                                        rejects become
                                                        residual rejects)

Attribution did not pay for the yield: wrong-hex 0.0-0.3% and out-of-wedge
0.0% on the blind+holds leg, against 7.3-10.1% / 5.5-8.6% for the greedy tags
that fed the old emit path and 1.1-3.5% / 0-1.3% for every claim recorded
ungated, on the same frames.  The blind+holds leg's residual rejects rise
(363-394 vs 186-296 with holds off), which is exactly the fresh-fix residual
doing the work the hold's own residual could not.

docs/pipeline.md section 7 gains the refreshed-hold rule and loses the "known
gap" note; the runbook's funnel entry for a flat `recorded` with a large
`hold` now points at the transponder rather than at path H.

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

This comment has been minimized.

@jehanazad
jehanazad changed the base branch from feat/synthetic-nodes-declared-wedge to main September 15, 2026 20:18
Resolve docs/pipeline.md against the squash-merged #367 and #401 by taking the branch as rebased onto main.

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

claude Bot commented Sep 15, 2026 •

Copy link
Copy Markdown

Claude finished @jehanazad's task in 9m 47s —— View job


Claude finished reviewing PR #368

Todo list

  • Gather context (PR description, changed files, repo instructions)
  • Diff this sync against the previous review (origin/main...HEAD)
  • Re-confirm the previous review's two findings against current HEAD
  • Deep multi-angle pass (line-by-line diff, removed-behavior audit, cross-file trace, reuse/simplification/efficiency)
  • Verify the strongest new findings directly against the code
  • Post review findings

What changed since the last review

The only new commit since the prior review round is 6dde80f, a merge of main to resolve docs/pipeline.md against the squash-merged #367/#401. The calibration logic itself is unchanged from what was already reviewed. This pass re-confirms the earlier findings and adds a deeper sweep that surfaced one new, higher-confidence issue.

New finding — funnel invariant has an uncounted exit (backend/services/known_claiming.py:381–393)

pos = extra.get(_CAL_DR_KEY)
if not pos:
    return False
...
return record_claim_calibration(node_id, pos[0], pos[1], fix_age_s=fix_age_s, detection_ts=time.time())

called as:

if _calibration_from_claim(...):
    cal_recorded += 1

(known_claiming.py:1319–1334)

By this point the claim has already survived all five named rules (hold/stale_fix/residual/contested/immature) — none of cal_rejects gets bumped here. But record_claim_calibration (calibration.py:141–149) can still return False: valid_latlon(lat, lon) rejects (0.0, 0.0) (its documented broken-config sentinel) and, separately, bool(float('nan')) is True in Python so valid_latlon does not catch NaN — meaning a NaN pair instead sails through into state.node_analytics.record_calibration_point. Either way, nothing increments any of the five reject counters, so calibration_points_recorded + Σ(calibration_claims_rejected_*) can fall short of the claim count — directly contradicting the invariant docs/pipeline.md and routes/test.py's own comment assert ("exactly one counter moves per claim... the six sum to the claim count").

Not live today: every current claim shape (path 1, path 2, refreshed hold) unconditionally populates _CAL_DR_KEY with real floats before reaching this point, and path 1 already checks math.isfinite on its tag lat/lon (known_claiming.py:1059) — but path 2's dr_lat/dr_lon (offset_latlon_m on cached ADS-B velocity, known_claiming.py:1190–1195) and the refreshed-hold's fresh-fix DR position carry no equivalent finiteness/sentinel check before landing in _CAL_DR_KEY. A corrupted vel_east/vel_north in the ADS-B cache, or a dead-reckoned position that lands exactly on (0, 0), would be silently dropped from the funnel (or, in the NaN case, silently written into the coverage bin store) with zero diagnostic trail — exactly the kind of gap the runbook's new funnel-reading guidance assumes can't happen. No test in test_calibration_from_claims.py asserts recorded + Σrejects == claims, nor exercises a valid_latlon-rejecting or non-finite DR position.

Fix this →

Previous findings — still open, re-confirmed at current HEAD

1. Detection-range recording silently dies fleet-wide under the new default (backend/services/track_gates.py:758) — record_verified_detection is still gated behind state.KNOWN_LANE_MODE == "off" alongside the calibration recorder, so it no longer fires under binding (default since #240) or shadow, and nothing in the claim lane replaces it. Still undocumented as a side effect in docs/pipeline.md/docs/runbook.md.

2. Path-1 (node-tagged) calibration points can never fail the freshness rule, and now provably get zero skew protection either (backend/services/known_claiming.py:1062–1077) — path 1 sets fix_ts_ms = ts_ms (the frame's own time) and stores the raw, un-dead-reckoned tag position as _CAL_DR_KEY. record_claim_calibration's docstring justifies skipping CAL_FIX_DETECTION_SKEW_S on the grounds that "dead-reckoning the fix to the frame instant makes the skew zero by construction" — but path 1 explicitly does not dead-reckon ("there is nothing to dead-reckon it across"), so that justification doesn't hold for it specifically. A node whose onboard ADS-B correlation updates slower than its detection frame rate would re-stamp a stale tag position as current, every frame, with neither the freshness rule (vacuous — fix_age_s is always ~0) nor the skew rule (doesn't apply to this path) catching it. No test exercises a moving aircraft via path 1 (fixtures use gs=0).

Efficiency — unconditional per-frame candidate build, now with more detail

The path-2 candidate loop (world/visibility gate, offset_latlon_m, _point_in_beam, predict_observation) lost its if free: early-out and now runs every frame regardless of whether anything is actually unclaimed, solely to give calibration's exclusivity check (_has_rival_candidate) a population to test against. For a steady-state fleet where most nodes hold already-linked tracks (little/no free detections per frame), this reintroduces a full cache-sized scan every frame that previously cost nothing. Compounding it, state._adsb_for_seeding() (a full rebuild of the ADS-B cache dict) is now called up to 2 + (matched holds) times per node per frame within one claim_known_targets pass — once directly, once inside _follow_states, and once per successful path-H match inside _fresh_fix_prediction. No benchmark or perf test covers this; worth a sanity check against production frame-rate budgets before merge, especially for real, fully-tagged nodes with large caches.

Minor / optional

  • The dead-reckon-then-predict pattern (offset_latlon_m → predict_observation, exposing the DR position alongside) is now written out independently at two sites this PR touches (_fresh_fix_prediction and the path-2 candidate loop), plus two pre-existing copies in _dark_global_projections and _claim_dark_follow — four total. A shared helper would remove the "second offset_latlon_m that could silently disagree" risk the code's own comments already flag.
  • _claim_streaks is a third hand-rolled keyed-streak-with-pruning store, structurally similar to dark_follow.py's reject/cooldown dicts and tasks/known_lane.py's _reanchor_mem. Each has different reset semantics, so not a drop-in unification, but a candidate for a shared "TTL'd keyed streak" utility going forward.
  • _CAL_DR_KEY/_CAL_PRED_KEY smuggled through the mutable extra dict and filtered back out via _CAL_EXTRA_KEYS before the registry write — works, but a future _CAL_* key that's added without updating _CAL_EXTRA_KEYS would leak silently into state.known_claims, which the aircraft feed reads directly.
  • _touch_claim_streak's gap check (known_claiming.py:239–262) uses 0.0 <= rather than a strict > bound, so a claim replayed at an identical ts_ms would double-advance the maturity streak. Unreachable within a single well-formed frame (each hex claimed once via claimed_hexes), and the same 0.0 <= idiom is already used elsewhere in the file for hold-gap expiry, so likely accepted house style rather than a live bug — flagging only since calibration correctness is this PR's whole point.

Checked, no issue found

  • _has_rival_candidate's self-match guard, the counts_as_reject boolean keeping known_claims_visibility_rejects/world_rejects byte-identical despite the widened candidate population, shadow-mode coverage (claim lane runs and records in shadow too, per TestKnownLaneSilencesThisPath), _fresh_fix_prediction's 4→6-tuple change (single caller, correctly unpacked), and all the new/updated test and doc cross-references (vulture_whitelist.py, test_solver_stats.py, routes/test.py) — all verified consistent.
  • The merge conflict resolution in docs/pipeline.md matches the current implementation exactly (five rules, refreshed-hold carve-out, bench numbers); no factual drift from the rebase.
    · branch feat/calibration-from-claims

@jehanazad
jehanazad merged commit 22507df into main Sep 15, 2026
14 checks passed
@Babissimo
Babissimo deleted the feat/calibration-from-claims branch September 21, 2026 08:42
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