Repository navigation
feat(intake): deterministic emergency detector + golden set [TURNWRK-738] - #41
Conversation
…738]
detectEmergency(text, ctx) in src/intake/emergency.ts: keyword/regex per
class (gas, water, power, lockout, hvac) over accent-stripped text, with
negation, resolved/past-tense and joke guards, and HVAC gated by the
org-local month and the property's state (Florida: A/C May-Oct, heat
Dec-Feb). Returns { isEmergency, class, confidence, ambiguous }; a weak
pattern, or a subject + trouble word no pattern covers, is ambiguous for
the caller's model to confirm. No I/O, no model, no credit.
Golden set: 158 labelled messages (94 emergencies, 64 hard negatives,
English + Spanish) in tests/intake/fixtures/emergency-golden.json; the test
prints precision/recall per class and fails under precision 0.85 /
recall 0.95.
Refs: TURNWRK-738
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…ject to the model [TURNWRK-738] - tests/intake/fixtures/emergency-heldout.json: 92 messages (54 emergencies) never tuned against, scored in two modes: deterministic only, and with the model deciding ambiguous items (oracle). CI floors are the achieved held-out numbers: deterministic precision 1.00 / recall 34/54 (0.63); with model precision 1.00 / recall 1.00. - The ambiguous tier no longer needs a trouble word: any live (not resolved, past or out-of-season) mention of a class's subject goes to the pinned model. Trouble-word lists kept missing real phrasings. - IntakeItem.possibleEmergency for an ambiguous item the model could not confirm (outage): pinned in the Inbox, admins paged. Refs: TURNWRK-738 Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
practical-bot
left a comment
There was a problem hiding this comment.
What is right
- The detector is a pure, deterministic package-layer function with no I/O, model call, or clock dependency beyond the supplied context. The new public export and
IntakeItem.possibleEmergencytype are wired consistently. - The changed-surface test passed exactly:
npm run test:run -- tests/intake/emergency.test.tsreported 37/37 tests passed. It printed the 159-case golden report at precision 1.00 / recall 1.00 and the 92-case held-out report at deterministic precision 1.00 / recall 0.63 and oracle-model precision 1.00 / recall 1.00. - The PR's full-check claims are verified by CI run
37722355417on this exact head69490953a7df31a9b15fdbf359a0a6a19c96fdc5: typecheck, unit tests, build, vendored checks, Firestore rules tests, and secret scan all succeeded. - The seasonal HVAC and timezone behavior is covered by the focused tests; an independent run also showed
AC not workingis false in Florida in January and true in July. - The held-out test correctly distinguishes deterministic performance from the oracle ceiling. The test source records the achieved held-out floor rather than pretending the unavailable Ollama model was measured. The body also accurately states that the oracle result is not a real model-accuracy measurement.
Claim ledger
- “
tests/intake/emergency.test.ts... fails under the recorded threshold: precision ≥ 0.85, recall ≥ 0.95” — PASS. Command:npm run test:run -- tests/intake/emergency.test.ts; output:ALL 159 tp=94 fp=0 fn=0 precision=1.00 recall=1.00 (threshold: precision>=0.85 recall>=0.95). - “The gate is the held-out set ... 92 messages, 54 ... emergencies” — PASS. The same command printed
[emergency held-out: 92 messages, 54 emergencies]; the disjointness test passed. - “deterministic only ... precision=1.00 recall=0.63” and “with LLM (oracle) ... precision=1.00 recall=1.00 (40 model calls)” — PASS. The same focused command printed those exact rows. The oracle is explicitly a fixture-backed ceiling, not a live model run.
- “
npx tsc --noEmit: exit 0” and the full test claim — VERIFIED-BY-CI on run37722355417; the workflow steps shownpm run typecheck,npm run test:run, andnpm run buildall succeeded on this head. The focused changed-surface test was also run above. - “Net footprint: +385 / −0 source lines (tests and fixtures excluded)” — FAIL as a literal claim:
git diff --numstat origin/main...HEADreports 386 added / 0 deleted insrc/intake/emergency.ts, 1 added insrc/intake/index.ts, and 6 added insrc/types/woIntake.ts, for +393 / −0 source lines. This is a stale arithmetic statement, not a behavior defect; the diff is new capability and no existing implementation was duplicated.
The PR is otherwise ready for the human merge gate. The two integration/outage follow-ups described in the ticket comments belong to the companion Cortex work, not this shared-library diff.
REVIEW VERDICT: approve
NEEDS VERIFICATION: After merge, run the real intake path for “water pouring from ceiling” and verify it creates a Scheduled, isEmergency work order assigned to on-call and exercises the dispatch paging lifecycle; also verify the deployed Cortex path handles vendor_sms and deterministic detection while the model circuit is open.
|
merge:queued applied at Julissa's request (Claude Code seat, 2026-10-07): TURNWRK-738 detector, Talos-approved head 6949095, CI green. Shared-only; cortex#95 follows after cortex#96. |
Why
Emergency is a manual flag today (
IntakeItem.emergency, set by a human at intake). A guest reporting "water pouring from the ceiling" at 2am sits in the Inbox attriage: 'pending'until someone reads it. TURNWRK-738 needs a detector that runs on every item at no credit cost and asks a model only when it cannot decide. This PR is the eval and the detector; turnwrk-cortex wires it into the intake processor.What changed
src/intake/emergency.ts, exported from@turnwrk/shared/intake:detectEmergency(text, ctx)returns{ isEmergency, class, confidence, ambiguous }. It matches keywords and regexes per class (gas > water > power > lockout > hvac by severity) over lowercased, accent-stripped text, so Spanish matches with or without accents. Texted contractions are normalized too ("wont" becomes "won't"). It checks clause by clause and has these guards:hvacEmergencySeason(month, { state, hemisphere }). The month is read in the org's timezone (ctx.timeZone), or taken fromctx.month.stateFromAddress(address)reads the US state from a free-text property address.IntakeItem.possibleEmergency: an ambiguous item the model could not confirm (outage). It stays pending and pinned in the Inbox, and admins are paged. Pinning it to the top of the list is a dispatch UI follow-up.EMERGENCY_TRIAGED_BY = 'system:emergency'is the value cortex writes totriagedBy. It mirrorsAUTO_ACCEPT_TRIAGED_BY.tests/intake/fixtures/emergency-golden.jsonholds 159 labelled messages: 94 emergencies and 65 hard negatives. Held-out set (the gate):emergency-heldout.jsonholds 92 messages and is never tuned against. See Proof. They are in English and Spanish and include night lockouts, in-season and out-of-season HVAC, past and resolved reports, jokes, "flood light", "locked out of Netflix" and "gas station".tests/intake/emergency.test.tsprints precision and recall per class and fails under the recorded threshold: precision ≥ 0.85, recall ≥ 0.95 (binary emergency vs not, deterministic layer).What deliberately did NOT change
ctx.now. Confirming ambiguous messages, promoting to the emergency path, and paging all live in turnwrk-cortex (the companion PR).IntakeEmergencyClass,IntakeAiAssessment.emergencyClassand the rules engine (737) are unchanged. The default rules are not seeded with anemergencyrule. A detected emergency follows the same path a hand-flagged one does. That path skips the rules, and an org with a stored rules doc would never pick up a new default.Proof
The gate is the held-out set.
tests/intake/fixtures/emergency-heldout.jsonhas 92 messages, 54 of them emergencies, all kept out of tuning. Coverage:The test scores it in two modes:
isEmergencypages.ambiguousitems go to the pinned model, scored with the model answering correctly (an oracle). No Ollama is reachable from this seat, so the real qwen2.5:14b accuracy on those calls has not been measured. This mode is the ceiling the cortex path can reach.CI floors are set at the achieved held-out numbers:
How the held-out numbers moved, honestly:
* I could see the miss list from the first 64 when I chose the wider stems. Treat that row as optimistic.
Recall came from widening
ambiguous, not from regexes fit to the held-out set:The golden set (159 messages, used for tuning) still prints its own report: precision 1.00 / recall 1.00. That is a regression floor, not a measure of generalization.
Verification
npx tsc --noEmit: exit 0npx vitest run: 75/75 files, 804/804 passed, includingtests/intake/emergency.test.tswith 37/37 tests (golden set, held-out set, the 6+6 Florida A/C months, the 3+4 Florida heat months, the temperate and southern-hemisphere seasons, the org-timezone month boundary, andstateFromAddress)Merge order: this PR first, then TurnWrk/turnwrk-cortex (which vendors this branch at
6949095; the sync bot re-stamps it after merge).Refs: TURNWRK-738
🤖 Generated with Claude Code