- Thread company_id through reports query and CampaignReportSummary
- Mirror the route's Bug 2 (D-05) tenant-resolution block: query
mimecast_tenants for an enabled row, build a tenant-scoped client via
getMimecastClientForTenant, and pass { client, cacheScope } to
getBlastRadius() when one exists
- Preserve global env fallback unchanged when no companyId or no
enabled tenant row is present
- Add USER_AWARENESS to the Verdict union in campaign-classifier.ts
- mapVerdictToActions('USER_AWARENESS') returns ['acknowledge_user']; not added to DESTRUCTIVE_ACTIONS so requires_approval computes false
- classifyCampaign's simulation branch now assigns verdict = 'USER_AWARENESS' directly instead of falling through to evaluateSpamVsUnwanted
- deriveDefaultParams('acknowledge_user') returns {} (no operator-editable params)
- Widen TriageNoteEvidence.verdict to admit 'USER_AWARENESS' (pure type widen, no formatting change)
- Tests: classifier simulation fixtures now assert USER_AWARENESS/acknowledge_user/requiresApproval=false; new mapVerdictToActions/computeRequiresApproval/deriveDefaultParams cases
- 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)