area_context v2 + Maps place verification (server-26#36, #37)
#36 — the correction pass shipped in 58efdbd was right, its reference-data
shape was not. One shape now, at both scopes, every field nullable:
area_context: { municipality?, county?, state?,
center?, radius_km?, resolved_from?, resolved_at?,
local_knowledge?: [{term, meaning}] }
`state` closes the ambiguity that made "Ossining" a national guess.
`local_knowledge` replaces roads[]/landmarks[], which could not hold
intersections, schools or nicknames and carried no meanings — `11-X-ray` is
useless alone, `11-X-ray — MTA PD patrol unit` is what a corrector can act on.
Pre-#36 roads[]/landmarks[] are read forward as bare terms so nothing an
operator already entered is lost.
Nullability is the mechanism: which scope gets filled is the operator's
declaration of how homogeneous the system is. One town — fill it once at system
level. Statewide — leave it blank and fill each talkgroup.
The backend owns the derived anchor. PUT /systems/{id} merges config.talkgroups[]
against what is stored instead of writing the client's blob verbatim, which
would have erased the anchor and the pending queue — the same defect as the
ten_codes wipe.
#37 — Maps as a verifier, not as prompt stuffing. The corrector emits its
location nouns; each is geocoded against the talkgroup's anchor, and on a miss
we look for a sound-alike that does resolve there, correct to it, and propose
{term, meaning} to that talkgroup. Cost scales with location nouns, not calls.
No anchor means SKIP. An area too wide to discriminate stores no anchor at all,
because a statewide radius would confirm anything inside it — verification that
passes everything is worse than none, since it reads as a check in the data.
Also re-anchors _geocode_location, which rejected results >40km from the NODE
(server-26#6). An antenna is not a jurisdiction; distance-from-node was always
a stand-in for the anchor and is now only the fallback.
The induction loop proposes at talkgroup level and never promotes. Blast
radius: a wrong term on a channel misleads that channel, the same term
system-wide misleads one 400km away on a statewide system.
38 new tests; 240 pass. Frontend typechecks clean.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
58efdbd6eb
commit
964343c819
@@ -78,6 +78,49 @@ class CommandPayload(BaseModel):
|
||||
# Systems
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
class LocalKnowledgeEntry(BaseModel):
|
||||
"""A local name and what it is. Both scopes, one shape (server-26#36)."""
|
||||
term: str
|
||||
meaning: Optional[str] = None
|
||||
|
||||
|
||||
class AreaContextBody(BaseModel):
|
||||
"""
|
||||
Ground truth about the area a system or talkgroup covers.
|
||||
|
||||
Every field is nullable on purpose: which SCOPE an operator fills is their
|
||||
declaration of how homogeneous the system is. A single-municipality system
|
||||
is described once at system level and inherited by every talkgroup; a
|
||||
statewide one is left null there and described per talkgroup. See
|
||||
`internal/area_context.py` for the merge rules and the anchor.
|
||||
|
||||
`center`/`radius_km`/`resolved_from`/`resolved_at` are absent here by
|
||||
design — the backend geocodes and writes those. A client that sends them is
|
||||
ignored.
|
||||
"""
|
||||
municipality: Optional[str] = None
|
||||
county: Optional[str] = None
|
||||
state: Optional[str] = None
|
||||
local_knowledge: List[LocalKnowledgeEntry] = []
|
||||
|
||||
|
||||
class TalkgroupEntry(BaseModel):
|
||||
"""
|
||||
One entry in `config.talkgroups[]`.
|
||||
|
||||
Declared so the talkgroup copy of `area_context` stops being unvalidated
|
||||
JSON riding inside the config blob — it is the same shape as the system's
|
||||
and gets the same validator (server-26#36).
|
||||
"""
|
||||
model_config = {"extra": "allow"}
|
||||
|
||||
id: int
|
||||
name: str = ""
|
||||
tag: str = "other"
|
||||
vocabulary: List[str] = []
|
||||
area_context: Optional[AreaContextBody] = None
|
||||
|
||||
|
||||
class SystemRecord(BaseModel):
|
||||
system_id: str
|
||||
org_id: Optional[str] = None
|
||||
@@ -86,9 +129,10 @@ class SystemRecord(BaseModel):
|
||||
config: Dict[str, Any] = {} # OP25-compatible config blob
|
||||
ten_codes: Dict[str, str] = {} # {"10-10": "Commercial Alarm", ...}
|
||||
# Ground truth about the area this system covers, fed to the transcript
|
||||
# corrector (server-26#36): {municipality, county, roads[], landmarks[]}.
|
||||
# Per-talkgroup overrides live inside config.talkgroups[] and rank ABOVE
|
||||
# this, so a multi-county system can narrow per channel.
|
||||
# corrector and the place verifier (server-26#36 / #37). Shape is
|
||||
# AreaContextBody plus the backend-owned anchor. Per-talkgroup overrides
|
||||
# live inside config.talkgroups[] and rank ABOVE this, so a multi-county
|
||||
# system narrows per channel rather than replacing this wholesale.
|
||||
area_context: Dict[str, Any] = {}
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user