- gatherCampaignEvidence: bulk-fetches reports (earliest-first, joined to
contacts for requester email) -> messages (report_id = ANY) -> indicators
(message_id = ANY), parses messages.headers JSONB into bounded
ParsedMessage fields, and runs one getBlastRadius() lookup keyed off the
earliest report's sender/subject/±24h window (research A6); synthesizes
unavailable/not_configured with no Mimecast call when no report is linked
- evaluateThreatTier (D-03): blastRadius.status==='ok' AND
(delivered>0 OR clicked>0) AND (hasHardAuthFail via effectiveAuthResults
OR hasKnownBadIndicatorMatch — same attachment_hash/url value spanning
>=2 distinct messages, cross-report correlation only, no external
reputation lookup per research A4)
- evaluateSpamVsUnwanted (D-04): UNWANTED when any attachment/url indicator
matches or delivery is contained to the reporter(s) only; SPAM otherwise
- classifyCampaign: D-06 simulation short-circuit -> D-03 -> D-04 ->
computeConfidence -> mapVerdictToActions -> computeRequiresApproval ->
append-only INSERT into classifications (D-02, no ON CONFLICT), wrapped
in try/catch logging [CAMPAIGN-CLASSIFIER] + err.message and rethrowing
- isKnownSimulationSender relaxed to a narrower SenderIdentity shape so both
the full NormalizedMessage fixtures and the bounded ParsedMessage type
can share it
- All 39 tests green; tsc clean; full `npm test` suite green except 2
pre-existing, unrelated itglue-search.test.ts failures (see
deferred-items.md)