From 15a9d1066670a5c5e32fc21f8ea82ee26697861e Mon Sep 17 00:00:00 2001 From: Logan Cusano Date: Mon, 7 Sep 2026 23:57:20 -0400 Subject: [PATCH] correlator: keep the tiebreak for typed / reassignment calls in the orphan gate (#115) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit _call_is_substanceless mirrored has_event_substance but not the creation gate's type-resolved short-circuit, so a routine-severity fire/medical call with no coords/tags/vehicles — or a reassignment (unit pulled to a new job) — could be gated to orphan where rules would open an incident. Bail out of the gate on incident_type or reassignment. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01Tbknwttzou4s46PAykmtix --- drb-c2-core/app/routers/upload.py | 8 ++++++++ drb-c2-core/tests/test_consensus_gate.py | 19 +++++++++++++++++++ 2 files changed, 27 insertions(+) diff --git a/drb-c2-core/app/routers/upload.py b/drb-c2-core/app/routers/upload.py index 34f468c..7188881 100644 --- a/drb-c2-core/app/routers/upload.py +++ b/drb-c2-core/app/routers/upload.py @@ -141,6 +141,14 @@ def _call_is_substanceless(ctx: dict) -> bool: """ from app.internal import incident_correlator + # The incident-creation gate skips the has_event_substance check entirely + # when a type resolved (incident_correlator._run_decision ~:1397), so a + # typed call — fire/medical/etc. — opens an incident on substance we do not + # re-check here. reassignment=True is dispatch pulling a unit onto a NEW + # job (units are blanked at :296 for exactly that reason): the strongest + # new-incident signal in the pipeline. Either one means "keep the tiebreak". + if ctx.get("incident_type") or ctx.get("reassignment"): + return False if (ctx.get("call_severity") or "routine") in ("moderate", "major"): return False if incident_correlator.has_event_substance(ctx): diff --git a/drb-c2-core/tests/test_consensus_gate.py b/drb-c2-core/tests/test_consensus_gate.py index 36b6bb6..d3483a5 100644 --- a/drb-c2-core/tests/test_consensus_gate.py +++ b/drb-c2-core/tests/test_consensus_gate.py @@ -133,6 +133,25 @@ async def test_call_with_vehicles_is_not_gated(): m_tiebreak.assert_called_once() +async def test_call_with_resolved_incident_type_is_not_gated(): + # The creation gate skips has_event_substance when a type resolved, so a + # typed call (fire/medical/…) opens an incident on substance the gate does + # not re-check — it must keep the tiebreak, not be dropped. + m_apply, m_tiebreak = await _run_consensus( + _preview("new", {}, ctx={"incident_type": "fire"}), _llm("orphan"), + ) + m_tiebreak.assert_called_once() + + +async def test_reassignment_call_is_not_gated(): + # reassignment=True is dispatch pulling a unit onto a NEW job (units are + # blanked for exactly that reason) — the strongest new-incident signal. + m_apply, m_tiebreak = await _run_consensus( + _preview("new", {}, ctx={"reassignment": True}), _llm("orphan"), + ) + m_tiebreak.assert_called_once() + + async def test_recent_incident_on_same_talkgroup_is_not_gated(): ctx = { "system_id": "sys-1",