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
@@ -21,14 +21,24 @@ from app.internal import transcript_correction as tc
|
||||
SYSTEM = {
|
||||
"vocabulary": ["Croton-Harmon", "Metro-North"],
|
||||
"ten_codes": {"10-4": "acknowledged", "10-13": "officer needs assistance"},
|
||||
"area_context": {"county": "Westchester", "roads": ["Route 9", "Saw Mill Parkway"]},
|
||||
"area_context": {
|
||||
"county": "Westchester",
|
||||
"state": "New York",
|
||||
"local_knowledge": [
|
||||
{"term": "Route 9", "meaning": "north-south state highway"},
|
||||
{"term": "Saw Mill Parkway"},
|
||||
],
|
||||
},
|
||||
"config": {
|
||||
"talkgroups": [
|
||||
{
|
||||
"id": 9048,
|
||||
"name": "Ossining - Police Dispatch",
|
||||
"vocabulary": ["Snowden Avenue", "Croton-Harmon"],
|
||||
"area_context": {"municipality": "Ossining", "landmarks": ["Sing Sing"]},
|
||||
"area_context": {
|
||||
"municipality": "Ossining",
|
||||
"local_knowledge": [{"term": "Sing Sing", "meaning": "state prison"}],
|
||||
},
|
||||
},
|
||||
{"id": 9600, "name": "Harrison - Police/EMS Dispatch"},
|
||||
{"id": 9563, "ten_codes": {"10-4": "on scene"}},
|
||||
@@ -83,6 +93,7 @@ async def test_talkgroup_without_own_data_inherits_system():
|
||||
ctx = await tc.resolve_context("sys-1", 9600)
|
||||
assert ctx["vocabulary"] == ["Croton-Harmon", "Metro-North"]
|
||||
assert any("Westchester" in line for line in ctx["area_lines"])
|
||||
assert ctx["area"].get("municipality") is None, "inherits, invents nothing"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@@ -105,7 +116,10 @@ async def test_missing_scope_is_not_an_error(system_id, tgid):
|
||||
async def test_unconfigured_system_yields_empty_context():
|
||||
with _system(doc=None):
|
||||
ctx = await tc.resolve_context("sys-1", 9048)
|
||||
assert ctx == {"vocabulary": [], "ten_codes": {}, "area_lines": []}
|
||||
assert ctx == {
|
||||
"vocabulary": [], "ten_codes": {}, "area_lines": [],
|
||||
"area": {}, "system_area": {}, "tg_area": {},
|
||||
}
|
||||
|
||||
|
||||
# ── Correction behaviour ────────────────────────────────────────────────────
|
||||
@@ -207,5 +221,7 @@ async def test_reference_data_reaches_the_prompt():
|
||||
await tc.correct("c1", "x y z w", None, system_id="sys-1",
|
||||
talkgroup_id=9048, talkgroup_name="Ossining - Police Dispatch")
|
||||
p = seen["prompt"]
|
||||
assert "Snowden Avenue" in p and "Sing Sing" in p and "Ossining - Police Dispatch" in p
|
||||
assert "Snowden Avenue" in p and "Ossining - Police Dispatch" in p
|
||||
assert "Sing Sing — state prison" in p, "a term without its meaning is half the information"
|
||||
assert "Ossining, Westchester, New York" in p, "state must reach the prompt (server-26#36)"
|
||||
assert "10-13=officer needs assistance" in p
|
||||
|
||||
Reference in New Issue
Block a user