correlator: remove is_dispatch and the tactical fit path entirely (#134)

Full removal, not a hardcoded flag: _is_dispatch_channel, _DISPATCH_TG_RE, the is_dispatch parameter, and _call_fits_incident's tactical branch are gone. One evaluation path for every channel.

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-13 14:49:55 -04:00
co-authored by Claude Sonnet 5
parent 0473e6a583
commit b1884852d5
5 changed files with 106 additions and 238 deletions
+1
View File
@@ -47,3 +47,4 @@ Thumbs.db
# Out of scope - not a deployed service (server-26#56)
drb-telegram-bot/
.claude/worktrees/
+12 -24
View File
@@ -90,31 +90,19 @@ class Settings(BaseSettings):
unit_continuity_max_idle_minutes: int = 20 # unit-continuity path: skip if incident idle > this
recorrelation_scan_minutes: int = 60 # re-examine orphaned calls ended within this window
tg_fast_path_idle_minutes: int = 90 # fast path: max minutes since incident last updated
# Dispatch channels only: tier-2 thin calls attach to a lone candidate idle < this.
# Was 10, which is long enough for the channel to have moved on to something else:
# on 2026-08-16 a "72 at Holland Station" incident absorbed a Grand Central train
# meet 9.6 min later, and a status check absorbed a records lookup at 9.7 min.
# Across that dump every correct thin attach was <= 3.4 min idle and every wrong
# one was >= 8.2, so 5 separates them with room on both sides. Genuine
# back-and-forth is handled by the 30-second tier-1 path above this.
# Second consumer (server-26#115): routers/upload.py's LLM-orphan-gate
# escape hatch (_recent_incident_on_same_talkgroup) always uses this same
# value now — no dispatch/tactical branch there since #133. Retuning this
# for fast/thin reasons moves that gate's behavior too — check both call
# sites before changing it. (tg_thin_idle_minutes below is now unused in
# production — is_dispatch is hardcoded True at its only real call site,
# incident_correlator.py's fast-path — see server-26#134.)
# Tier-2 thin calls attach to a lone candidate idle < this, on every
# channel (server-26#133/#134 removed the dispatch/tactical split — a
# channel's name doesn't change how much scrutiny it gets). Was 10, which
# is long enough for the channel to have moved on to something else: on
# 2026-08-16 a "72 at Holland Station" incident absorbed a Grand Central
# train meet 9.6 min later, and a status check absorbed a records lookup
# at 9.7 min. Every correct thin attach in that dump was <= 3.4 min idle
# and every wrong one was >= 8.2, so 5 separates them with room on both
# sides. Genuine back-and-forth is handled by the 30-second tier-1 path
# above this. Also the escape hatch in routers/upload.py's LLM-orphan gate
# (_recent_incident_on_same_talkgroup, server-26#115) — check both call
# sites before retuning this.
tg_dispatch_thin_idle_minutes: int = 5
# Every other channel: tier-2 thin calls attach to a lone candidate idle < this.
# Non-dispatch talkgroups previously had NO tier-2 bound at all — they used the
# whole 90-minute tg_fast_path_idle_minutes window with no single-candidate
# requirement and no fit test, which is the widest version of the 2026-08-20
# over-merge. A tactical channel really is dedicated to one scene, so it earns
# a longer window than a dispatch backbone, but not an unbounded one: 15 sits
# inside the 20-minute tactical-default window in _call_fits_incident, so the
# no-evidence thin path is never more permissive than the fit-tested path on
# the same channel.
tg_thin_idle_minutes: int = 15
# ── Hard caps: an incident past either of these stops accepting calls ──────
# Enforced on every correlation path (see _incident_at_capacity). Pairwise fit
+63 -124
View File
@@ -98,13 +98,6 @@ _PURSUIT_PROXIMITY_KM = 20.0 # expanded radius for moving incidents
# overlap with the candidate OR a distance under this tighter bar.
_LOCATION_TIGHT_PROXIMITY_KM = 0.2
_DISPATCH_TG_RE = re.compile(
r"\bdispatch\b|\bdisp\b"
r"|\bpatched\b" # patched channels aggregate multiple call streams
r"|\bprimary\b", # "Primary" channels serve as shared backbones
re.IGNORECASE,
)
# Matches route/road identifiers in location strings for cross-system parent detection.
# Groups: numbered routes (Route 202, NY-9, US-6, I-87, CR-35) and named parkways/highways.
_ROAD_RE = re.compile(
@@ -494,13 +487,6 @@ def _resolve_incident_title(
return {}
def _is_dispatch_channel(talkgroup_name: Optional[str]) -> bool:
"""True when the talkgroup is a shared dispatch backbone (not a tactical/working channel)."""
if not talkgroup_name:
return False
return bool(_DISPATCH_TG_RE.search(talkgroup_name))
def _incident_idle_minutes(inc: dict, now: datetime) -> float:
"""Minutes since the incident was last updated (or started). Returns 9999 on parse error."""
try:
@@ -976,15 +962,10 @@ def _run_decision(ctx: dict) -> dict:
# directly on the Firestore call doc). Fall back to the call doc so that
# dispatch-channel strictness works regardless of how the call arrived.
effective_talkgroup_name = talkgroup_name or call_doc.get("talkgroup_name")
# server-26#134: always dispatch-strict now, not name-guessed — a
# channel labeled "tac"/"tactical" is rare and no less scrutinized in
# practice than any other. _call_fits_incident's tactical branch is
# kept, unreached, in case that's ever wrong.
is_dispatch = True
if effective_talkgroup_name != talkgroup_name:
logger.info(
f"Correlator: talkgroup_name missing from request for call {call_id}, "
f"resolved from call doc: {effective_talkgroup_name!r} → is_dispatch={is_dispatch}"
f"resolved from call doc: {effective_talkgroup_name!r}"
)
tg_matches = [
@@ -1028,10 +1009,7 @@ def _run_decision(ctx: dict) -> dict:
# single-candidate requirement and no fit test of any kind. Four
# hours is not a bound, and neither is ninety minutes.
THIN_CONVERSATIONAL_SECS = 30
thin_window_min = (
settings.tg_dispatch_thin_idle_minutes if is_dispatch
else settings.tg_thin_idle_minutes
)
thin_window_min = settings.tg_dispatch_thin_idle_minutes
very_recent = [
inc for inc in tg_recent
if _idle_gate_minutes(inc, now) * 60 <= THIN_CONVERSATIONAL_SECS
@@ -1053,8 +1031,7 @@ def _run_decision(ctx: dict) -> dict:
if len(thin_pool) > 1:
logger.info(
f"Correlator fast-path thin (tier-2): {len(thin_pool)} active incidents "
f"on {'dispatch' if is_dispatch else 'tactical'} channel — "
f"ambiguous, skipping thin call {call_id}"
f"— ambiguous, skipping thin call {call_id}"
)
thin_pool = []
@@ -1084,14 +1061,14 @@ def _run_decision(ctx: dict) -> dict:
candidate = tg_recent[0]
logger.info(
f"Correlator fast/single: call {call_id} vs incident {candidate['incident_id']} "
f"tg_name={effective_talkgroup_name!r} is_dispatch={is_dispatch} "
f"tg_name={effective_talkgroup_name!r} "
f"idle={round(_incident_idle_minutes(candidate, now), 1)}min "
f"call_units={call_units} inc_units={candidate.get('units')} "
f"call_coords={'yes' if coords else 'no'} inc_coords={'yes' if candidate.get('location_coords') else 'no'}"
)
fit, fit_signal = _call_fits_incident(
candidate, call_units, call_vehicles, coords,
settings.location_proximity_km, is_dispatch=is_dispatch,
settings.location_proximity_km,
call_embedding=call_embedding, now=now,
reassignment=reassignment,
)
@@ -1101,13 +1078,12 @@ def _run_decision(ctx: dict) -> dict:
"corr_path": "fast/single",
"corr_incident_idle_min": round(_incident_idle_minutes(candidate, now), 1),
"corr_fit_signal": fit_signal,
"corr_is_dispatch": is_dispatch,
}
if fit_signal == "unit_overlap" and call_units:
corr_debug["corr_matched_units"] = _matching_units(call_units, candidate.get("units"))
logger.info(
f"Correlator fast-path: call {call_id} → {candidate['incident_id']} "
f"(signal={fit_signal}, is_dispatch={is_dispatch})"
f"(signal={fit_signal})"
)
else:
logger.info(
@@ -1123,14 +1099,14 @@ def _run_decision(ctx: dict) -> dict:
# dispatch channel should create its own incident, not be force-merged.
logger.info(
f"Correlator fast/disambig: call {call_id} vs incident {candidate['incident_id']} "
f"tg_name={effective_talkgroup_name!r} is_dispatch={is_dispatch} "
f"tg_name={effective_talkgroup_name!r} "
f"idle={round(_incident_idle_minutes(candidate, now), 1)}min "
f"call_units={call_units} inc_units={candidate.get('units')} "
f"call_coords={'yes' if coords else 'no'} inc_coords={'yes' if candidate.get('location_coords') else 'no'}"
)
fit, fit_signal = _call_fits_incident(
candidate, call_units, call_vehicles, coords,
settings.location_proximity_km, is_dispatch=is_dispatch,
settings.location_proximity_km,
call_embedding=call_embedding, now=now,
reassignment=reassignment,
)
@@ -1141,7 +1117,6 @@ def _run_decision(ctx: dict) -> dict:
"corr_incident_idle_min": round(_incident_idle_minutes(candidate, now), 1),
"corr_candidates": len(tg_recent),
"corr_fit_signal": fit_signal,
"corr_is_dispatch": is_dispatch,
}
if fit_signal == "unit_overlap" and call_units:
corr_debug["corr_matched_units"] = _matching_units(call_units, candidate.get("units"))
@@ -1720,7 +1695,6 @@ def _call_fits_incident(
call_vehicles: list[str],
call_coords: Optional[dict],
proximity_km: float,
is_dispatch: bool = False,
call_embedding: Optional[list] = None,
now: Optional[datetime] = None,
reassignment: bool = False,
@@ -1730,48 +1704,24 @@ def _call_fits_incident(
the incident; signal names the specific evidence that drove the decision.
fits=True signals: "unit_overlap" | "vehicle_overlap" | "location_proximity"
| "time_fallback" | "tactical_default"
fits=False signals: "unit_loc_conflict" | "content_divergence"
| "location_conflict" | "no_signal" | "tactical_idle"
| "location_conflict" | "no_signal"
Original docstring (logic unchanged):
Return True if this call plausibly belongs to the given incident.
Evaluation order for dispatch channels (is_dispatch=True):
─────────────────────────────────────────────────────────
1. Unit overlap
Same officer = same call. On dispatch channels, also run a location
conflict guard: if both sides carry geocoded coords and they differ
significantly, the officer has moved to a new scene and the unit match
is a false positive.
When the call has NO geocoded coordinates AND the incident is old
(≥ 15 min), use content divergence as a location proxy: an officer at
a genuinely different scene will be talking about clearly different
things. For recent incidents (< 15 min) we skip this proxy — the
officer may simply be giving an update without mentioning the address.
2. Vehicle overlap → True
3. Location proximity
Both sides geocoded and close → True.
Both sides geocoded and far apart (no other positive signal) → False.
4. No positive signals fired → dispatch fallback
a. Conversational continuity: idle < 2 min → True.
A call arriving within 2 minutes of the last incident activity almost
certainly belongs to the same dispatch thread. "Baker, head over
there too" or "copy that" carries no incident-specific vocabulary but
is unambiguously a response to what was just said. We do not require
embedding similarity here — embeddings capture word meaning, not
conversational context, and short operational messages will always
have lower similarity than the incident's accumulated content.
b. Older incident, no positive signals → False.
A shared dispatch channel must not absorb calls by default.
Tactical / working channel (is_dispatch=False):
────────────────────────────────────────────────
Channel is dedicated to one scene. No evidence of separation ≈ same call.
Default → True.
Evaluation order:
1. Unit overlap. Same officer = same call. Also runs a location-conflict
guard: geocoded on both sides and clearly different → the officer has
moved to a new scene, false positive. No geocode on the call (or on
the incident, with a call geocode) AND the incident is old (≥ 15 min)
→ content divergence as a location proxy (embedding similarity < 0.82
→ different scene). Skipped for recent incidents (< 15 min) — an
update without re-stating the address is normal.
2. Vehicle overlap → True.
3. Location proximity. Both geocoded and close → True; far apart with no
other positive signal → False.
4. No positive signal at all → False. A shared channel must not absorb
calls by default (server-26#134 — this used to default True within
20 min on any channel not name-matched as "dispatch"; a talkgroup
named tac/tactical is no less scrutinized in practice than any other).
Thin calls (no units/vehicles/coords) never reach this function —
they are intercepted before it in correlate_call.
@@ -1791,44 +1741,43 @@ def _call_fits_incident(
inc_units = _unit_keys(inc.get("units"))
matched_units = _matching_units(call_units, inc.get("units"))
if matched_units:
if is_dispatch:
if call_coords:
# Hard location conflict: geocoded on both sides and clearly different.
inc_coords_u = inc.get("location_coords")
if inc_coords_u:
dist_km = _haversine_km(
call_coords["lat"], call_coords["lng"],
inc_coords_u["lat"], inc_coords_u["lng"],
)
if dist_km > proximity_km:
logger.info(f" fits[{inc_id}]: unit_overlap({matched_units}) but location_conflict dist={dist_km:.2f}km → unit_loc_conflict")
return False, "unit_loc_conflict"
elif call_embedding and idle_min >= 15:
# Call has geocode but incident doesn't — fall back to content
# divergence as a location proxy. Without this, stale incidents
# that never geocoded absorb unrelated calls purely on unit
# overlap (e.g. a patrol officer working a second scene 70 min
# after the original call).
inc_emb_u = inc.get("embedding")
if inc_emb_u:
sim = _cosine_similarity(call_embedding, inc_emb_u)
if sim < 0.82:
logger.info(f" fits[{inc_id}]: unit_overlap({matched_units}) but content_divergence (has_call_coords/no_inc_coords) sim={sim:.3f} → content_divergence")
return False, "content_divergence"
if call_coords:
# Hard location conflict: geocoded on both sides and clearly different.
inc_coords_u = inc.get("location_coords")
if inc_coords_u:
dist_km = _haversine_km(
call_coords["lat"], call_coords["lng"],
inc_coords_u["lat"], inc_coords_u["lng"],
)
if dist_km > proximity_km:
logger.info(f" fits[{inc_id}]: unit_overlap({matched_units}) but location_conflict dist={dist_km:.2f}km → unit_loc_conflict")
return False, "unit_loc_conflict"
elif call_embedding and idle_min >= 15:
# No geocode available AND old incident: use content divergence as a
# location-proxy veto. After 15+ minutes an officer at a completely
# different scene will be discussing clearly different content.
# Skip this for recent incidents — an officer updating on the same
# scene without re-stating the address is normal and their update
# won't share much vocabulary with the original dispatch.
# Call has geocode but incident doesn't — fall back to content
# divergence as a location proxy. Without this, stale incidents
# that never geocoded absorb unrelated calls purely on unit
# overlap (e.g. a patrol officer working a second scene 70 min
# after the original call).
inc_emb_u = inc.get("embedding")
if inc_emb_u:
sim = _cosine_similarity(call_embedding, inc_emb_u)
if sim < 0.82:
logger.info(f" fits[{inc_id}]: unit_overlap({matched_units}) but content_divergence sim={sim:.3f} → content_divergence")
logger.info(f" fits[{inc_id}]: unit_overlap({matched_units}) but content_divergence (has_call_coords/no_inc_coords) sim={sim:.3f} → content_divergence")
return False, "content_divergence"
logger.info(f" fits[{inc_id}]: unit_overlap matched={matched_units} is_dispatch={is_dispatch} → unit_overlap")
elif call_embedding and idle_min >= 15:
# No geocode available AND old incident: use content divergence as a
# location-proxy veto. After 15+ minutes an officer at a completely
# different scene will be discussing clearly different content.
# Skip this for recent incidents — an officer updating on the same
# scene without re-stating the address is normal and their update
# won't share much vocabulary with the original dispatch.
inc_emb_u = inc.get("embedding")
if inc_emb_u:
sim = _cosine_similarity(call_embedding, inc_emb_u)
if sim < 0.82:
logger.info(f" fits[{inc_id}]: unit_overlap({matched_units}) but content_divergence sim={sim:.3f} → content_divergence")
return False, "content_divergence"
logger.info(f" fits[{inc_id}]: unit_overlap matched={matched_units} → unit_overlap")
return True, "unit_overlap"
# ── 2. Vehicle overlap ────────────────────────────────────────────────────
@@ -1882,29 +1831,19 @@ def _call_fits_incident(
return False, "location_conflict"
# ── 4. No positive signals ────────────────────────────────────────────────
# Requires at least one positive signal (unit, vehicle, or location match).
# A substantive call with no matching signals is more likely a separate
# incident than a follow-up — two dispatches can arrive within seconds of
# each other on a busy channel. Content-free thin calls are handled before
# this function via the thin path in correlate_call, with a tighter
# 30-second recency window.
logger.info(
f" fits[{inc_id}]: no positive signal — is_dispatch={is_dispatch} idle={idle_min:.1f}min "
f" fits[{inc_id}]: no positive signal — idle={idle_min:.1f}min "
f"inc_units={list(inc_units)} call_units={call_units} "
f"inc_vehicles={list(inc_vehicles)} call_vehicles={call_vehicles} "
f"call_coords={call_coords is not None} inc_coords={inc_coords is not None}"
)
if is_dispatch:
# Dispatch channels require at least one positive signal (unit, vehicle,
# or location match). A substantive call with no matching signals is more
# likely a separate incident than a follow-up to the current one — two
# dispatches can arrive within seconds of each other on a busy channel.
# Content-free thin calls are handled before this function via the thin
# path in correlate_call, with a tighter 30-second recency window.
return False, "no_signal"
# Tactical channel: one scene per channel.
# Within 20 min of the last incident activity, link by default — same
# working channel almost certainly means same scene.
# After 20 min of silence, require at least one positive signal; the same
# frequency can be reused for a new unrelated incident later in the shift.
if idle_min < 20.0:
return True, "tactical_default"
return False, "tactical_idle"
return False, "no_signal"
async def _update_incident(
+10 -48
View File
@@ -111,55 +111,17 @@ async def upload_call_audio(
def _recent_incident_on_same_talkgroup(ctx: dict) -> bool:
"""
True when one of the already-loaded recent incidents is running on this
call's own system + talkgroup AND was active within the last
`settings.tg_dispatch_thin_idle_minutes` minutes. Covers the "unit
dispatched on the dispatch channel, thin acknowledgement 10-30s later"
case: the ack carries no substance of its own but plainly belongs to the
job just opened.
True when a recent incident is running on this call's own system +
talkgroup, within `settings.tg_dispatch_thin_idle_minutes` (5 min) —
applied uniformly regardless of the talkgroup's name (server-26#134).
Covers "unit dispatched, thin ack 10-30s later": the ack has no
substance of its own but plainly belongs to the job just opened.
Always uses `settings.tg_dispatch_thin_idle_minutes` (5 min), regardless
of what the talkgroup is named. An earlier version of this branched on
`_is_dispatch_channel` (mirroring incident_correlator.py's fast/thin idle-
window selection) to use a longer 15-minute window on anything not
literally named "dispatch"/"patched"/"primary" — owner correction,
2026-09-13, from direct scanning experience: a talkgroup named "tac"/
"tactical" genuinely does see materially different traffic only during a
real incident, and that's rare; the overwhelming majority of traffic on
ANY monitored channel — including high-risk stops and pursuits — runs on
the main channel regardless of what it's named. `_is_dispatch_channel`'s
string-match is a naming-convention guess, not a detector of actual
channel behavior, and trusting it here meant a busy single-channel
department not literally named "dispatch" would silently get the more
permissive window and reproduce #115's original bug. One constant,
applied uniformly, is the safer default; a real low-volume channel is the
rare case, not the norm, so erring toward the tighter window costs little.
NOT applied to incident_correlator.py's own fast/thin selection (out of
scope for this pass — a bigger, decision-changing surface, worth its own
review rather than changing under this fix).
This used to be a plain "does any recent incident exist on this
talkgroup" check against a 2-hour window (`correlation_window_hours`).
Measured live in production (server-26#115, CORRELATION_REVIEW_0912.md,
window #3): on a busy dispatch channel producing 3-13 incidents per 2h,
that condition is satisfied almost unconditionally, so the surrounding
LLM-orphan gate never fired on exactly the channels it exists to
protect (0/24 target-shaped calls gated in a 4h window). The docstring's
own intent was always "10-30 seconds", not "hours" — a few minutes is
the right shape.
Reads ctx["recent"] — the same window-filtered candidate list the rules
engine already loaded — so this adds no Firestore read.
Known limitation (server-26#115): ctx["recent"] is derived from
`all_active` in `_build_context` — incidents with `status=="active"`
for the call's org, with over-capacity incidents already dropped by
`_drop_capped` — not a full scan of the `incidents` collection. A
same-talkgroup incident that has already auto-resolved (no longer
"active") or hit `incident_max_calls`/`incident_max_duration_minutes`
will NOT appear here even though it is chronologically recent. A proper
fix needs a dedicated Firestore query that is not status/capacity
filtered — a new read, out of scope for this pass.
Reads ctx["recent"] (the rules engine's own candidate list — no extra
Firestore read). That list is status=="active" incidents only, so an
already-resolved or capacity-capped same-talkgroup incident won't be
seen here even if chronologically recent (server-26#115, unresolved —
would need a dedicated non-status-filtered query).
Whether this limitation explains the 2/24 unexplained gate misses in the
window #3 measurement is UNANSWERED, not confirmed either way — a prior
+20 -42
View File
@@ -31,8 +31,10 @@ from app.internal.incident_correlator import (
NOW = datetime(2026, 8, 20, 7, 0, 0, tzinfo=timezone.utc)
# TG 383 from the dump: "Ch 1 (Patched with 155.310)". _DISPATCH_TG_RE matches
# "patched", so this is a shared dispatch backbone carrying the whole department.
# TG 383 from the dump: "Ch 1 (Patched with 155.310)", a shared dispatch
# backbone carrying the whole department. Kept as two distinct fixture names
# for readability even though the channel's name no longer affects behavior
# (server-26#134).
DISPATCH_TG = "Ch 1 (Patched with 155.310)"
TACTICAL_TG = "Fireground 2"
@@ -140,12 +142,9 @@ def test_thin_call_with_no_overlap_does_not_attach_on_a_dispatch_channel():
assert _run_decision(_ctx(all_active=[inc], recent=[inc]))["action"] == "orphan"
def test_thin_call_with_no_overlap_does_not_attach_on_a_tactical_channel():
"""
The widest version of the bug: non-dispatch talkgroups skipped the tiering
entirely and used the whole 90-minute fast-path window with no
single-candidate requirement, so ANY thin call joined whatever was newest.
"""
def test_thin_call_with_no_overlap_does_not_attach_on_a_tactical_named_channel():
"""A channel's name no longer changes anything (server-26#134) — same
assertion as the dispatch-named case above, different fixture name."""
inc = _incident(idle_minutes=40)
decision = _run_decision(_ctx(
all_active=[inc], recent=[inc], talkgroup_name=TACTICAL_TG,
@@ -154,10 +153,9 @@ def test_thin_call_with_no_overlap_does_not_attach_on_a_tactical_channel():
def test_tactical_named_channel_uses_the_dispatch_window_now():
"""server-26#134: is_dispatch is hardcoded True — a channel's name no
longer changes the thin window. 14 min (the old tactical bound minus 1,
inside the old 15-min window) is now past the 5-min dispatch window."""
inc = _incident(idle_minutes=settings.tg_thin_idle_minutes - 1)
"""server-26#134: 14 min was inside the old 15-min tactical window; now
every channel uses the 5-min window regardless of name."""
inc = _incident(idle_minutes=14)
decision = _run_decision(_ctx(
all_active=[inc], recent=[inc], talkgroup_name=TACTICAL_TG,
))
@@ -234,47 +232,27 @@ def test_back_dated_thin_call_does_not_sail_through_the_recency_gate():
def test_back_dated_call_does_not_bypass_the_content_divergence_veto(monkeypatch):
"""
Same `9d376ffe` failure mode, but exercised directly against
`_call_fits_incident` on a dispatch channel: unit overlap plus a
back-dated call (incident updated 45 minutes AFTER the call's own
`started_at`, which the sweep passes as `now`) used to make the signed
idle -45, so `idle_min >= 15` read False and the content-divergence
veto never ran — unit overlap alone forced the merge regardless of
what the call was actually about. With the gate fixed to compare
distance, idle_min is 45 (>= 15), the veto runs, and a divergent
embedding (patched below so the assertion doesn't depend on numpy
being installed in this environment) fails it.
Same `9d376ffe` failure mode, exercised directly against
`_call_fits_incident`: unit overlap plus a back-dated call (incident
updated 45 minutes AFTER the call's own `started_at`, which the sweep
passes as `now`) used to make the signed idle -45, so `idle_min >= 15`
read False and the content-divergence veto never ran — unit overlap
alone forced the merge regardless of what the call was actually about.
With the gate fixed to compare distance, idle_min is 45 (>= 15), the
veto runs, and a divergent embedding (patched below so the assertion
doesn't depend on numpy being installed in this environment) fails it.
"""
monkeypatch.setattr(correlator_mod, "_cosine_similarity", lambda a, b: 0.0)
inc = _incident(idle_minutes=-45, units=["6-Adam"])
inc["embedding"] = [1.0, 0.0]
fits, signal = _call_fits_incident(
inc, call_units=["6-Adam"], call_vehicles=[], call_coords=None,
proximity_km=settings.location_proximity_km, is_dispatch=True,
proximity_km=settings.location_proximity_km,
call_embedding=[0.0, 1.0], now=NOW,
)
assert (fits, signal) == (False, "content_divergence")
def test_back_dated_call_on_tactical_channel_does_not_get_the_default():
"""
Tactical-channel counterpart: no unit/vehicle/location signal, so the
function falls through to step 4's `idle_min < 20.0` default. A
back-dated call (incident updated 45 minutes after the call's own
started_at) used to read idle_min as -45, which is always < 20.0, so
`tactical_default` fired unconditionally no matter how stale the
incident actually was relative to this call. Fixed, idle_min is the
45-minute distance, which is not < 20.0.
"""
inc = _incident(idle_minutes=-45)
fits, signal = _call_fits_incident(
inc, call_units=[], call_vehicles=[], call_coords=None,
proximity_km=settings.location_proximity_km, is_dispatch=False,
call_embedding=None, now=NOW,
)
assert (fits, signal) == (False, "tactical_idle")
# ---------------------------------------------------------------------------
# 4. Hard caps — path-independent, because pairwise fit tests can't see shape
# ---------------------------------------------------------------------------