Stage 3 reference solution
Three parts: add a belief-strength field, implement deterministic reconciliation, and swap the runner’s naive one-incident-per-report loop for it.
Belief strength
Add a confidence field (belief strength in [0, 1]) to the Incident dataclass - the starter ships none.
confidence: float = 0.5 # belief strength in [0, 1]
A fire opens at 0.6, an unknown at 0.4; corroboration adds 0.15, a nearby denial subtracts 0.25 (never below 0, never auto-closing), and truthful truck vision pins it to 1.0 or closes the incident.
Thresholds
MATCH_RADIUS: float = 60.0
TIME_WINDOW: int = 12
OPEN_CONFIDENCE = {"fire": 0.6, "unknown": 0.4}
CORROBORATE = 0.15
FALSE_ALARM_PENALTY = 0.25
These are a tuning choice, not a given: any values that keep the deterministic fixtures separate work for the tests, but the live stage3-partial@londone map spaces buildings hundreds to thousands of world units apart, so pick a radius sane for the map and let the vision override catch a wrong merge rather than a radius that always does.
reconcile_reports
Per the decision table in reconcile.py’s docstring. A denial never opens an incident; if one sits near an open belief it links as a possible false alarm and demotes confidence, never overwriting a live belief. A fire/unknown report opens a new incident or merges into the nearest compatible one (nearest-first, within radius and time window).
def _reconcile_one(r, incidents, tick):
if r.coords is None:
return Outcome(IGNORED, None, r.contact_id, "no resolvable location")
if r.incident_type == "none":
match = _nearest_open(r.coords, incidents, tick)
if match is None:
return Outcome(IGNORED, None, r.contact_id, "no fire, no incident")
incidents.link(match.incident_id, r.contact_id, tick)
incidents.update(match.incident_id, tick=tick,
confidence=max(0.0, match.confidence - FALSE_ALARM_PENALTY))
return Outcome(FALSE_ALARM, match.incident_id, r.contact_id, "denial near a suspected incident")
match = _best_match(r, incidents, tick)
if match is None:
inc = incidents.open(incident_type=r.incident_type, coords=r.coords, tick=tick,
building_id=_building_id(r), severity=_sev_value(r),
headcount=_head_value(r), headcount_qualifier=_head_qualifier(r),
confidence=OPEN_CONFIDENCE.get(r.incident_type, 0.4))
incidents.link(inc.incident_id, r.contact_id, tick)
return Outcome(OPEN, inc.incident_id, r.contact_id)
incidents.link(match.incident_id, r.contact_id, tick)
return _merge(match, r, incidents, tick)
_merge upgrades an unknown belief to fire, keeps the max severity band, and on an all_out-vs-trapped headcount clash flags a contradiction while retaining the MORE cautious belief (never silently overwriting). It also adopts a headcount claim when the incident holds none yet, and nudges confidence up by CORROBORATE.
reconcile_vision (ground truth)
A building seen normal closes any co-located incident (RESOLVED_VISION); burning confirms or opens one at confidence 1.0 (CONFIRMED_VISION). collapsed is terminal - the building is lost, nothing left to serve - and the reference takes no reconcile action on it.
Runner
Replace the naive stage-1 loop: reconcile_mod.reconcile_reports(new_reports, ep.incidents, ep.reports, tick), then resolve each observation building to a point and call reconcile_mod.reconcile_vision(...) so reconciliation stays pure.
Verified: REC-1..8 green against the solution (cd starter && uv run --project ../solution behave features/reconcile.feature); ruff/pyright clean; reconciliation is deterministic (identical inputs -> identical links). A live stage3-partial@londone episode resolves at casualties 0 / buildings_lost 0, improving on the greedy baseline of 26 / 1.