correlator: keep the tiebreak for typed / reassignment calls in the orphan gate (#115)

_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 <noreply@anthropic.com>

Claude-Session: https://claude.ai/code/session_01Tbknwttzou4s46PAykmtix
This commit is contained in:
Logan Cusano
2026-09-07 23:57:20 -04:00
co-authored by Claude Sonnet 5
parent dd426572fc
commit 15a9d10666
2 changed files with 27 additions and 0 deletions
+8
View File
@@ -141,6 +141,14 @@ def _call_is_substanceless(ctx: dict) -> bool:
""" """
from app.internal import incident_correlator 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"): if (ctx.get("call_severity") or "routine") in ("moderate", "major"):
return False return False
if incident_correlator.has_event_substance(ctx): if incident_correlator.has_event_substance(ctx):
+19
View File
@@ -133,6 +133,25 @@ async def test_call_with_vehicles_is_not_gated():
m_tiebreak.assert_called_once() 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(): async def test_recent_incident_on_same_talkgroup_is_not_gated():
ctx = { ctx = {
"system_id": "sys-1", "system_id": "sys-1",