From 3424812d606824d4d37e2761154b4f1366bd8566 Mon Sep 17 00:00:00 2001 From: lorentz Date: Wed, 15 Jul 2026 13:47:48 -0400 Subject: [PATCH] docs(17): fix plan-checker warnings (merge formula clarity + resolved open questions) --- .../phases/17-mimecast-blast-radius-lookup/17-01-PLAN.md | 2 +- .../phases/17-mimecast-blast-radius-lookup/17-RESEARCH.md | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.planning/phases/17-mimecast-blast-radius-lookup/17-01-PLAN.md b/.planning/phases/17-mimecast-blast-radius-lookup/17-01-PLAN.md index f304b3f..56e62be 100644 --- a/.planning/phases/17-mimecast-blast-radius-lookup/17-01-PLAN.md +++ b/.planning/phases/17-mimecast-blast-radius-lookup/17-01-PLAN.md @@ -179,7 +179,7 @@ Factory-convention analogs to mirror: 2. Build the cache key `::` per addigy-devices precedent: with messageId → `mimecast:blast-radius:msgid:${input.messageId}`; else → `mimecast:blast-radius:composite:${input.sender}:${input.subject}:${input.dateWindow.start.toISOString()}:${input.dateWindow.end.toISOString()}`. 3. `const cached = await getCachedData(cacheKey); if (cached) return cached;` — a cache hit must short-circuit BEFORE any MimecastClient call (D-04). 4. Inside a `try`: `const client = getMimecastClient();`. If `input.messageId` is present, optionally `await client.getMessageInfo(input.messageId)` for body/header evidence ONLY — never use its return to derive counts and never let it gate the fan-out (Pitfall 1). Then ALWAYS run the fan-out via `Promise.all([...])` (D-01 corrected — fan-out is unconditional, not a fallback): `searchDeliveredMessages({ to: input.recipient, from: input.sender, subject: input.subject, start, end })` where `start`/`end` are `input.dateWindow.start/end.toISOString().replace(/\.\d{3}Z$/, '+0000')`; `getHeldMessages({ recipient: input.recipient })`; `getThreatEvents()`. - 5. Merge per 17-RESEARCH Pattern 3: build `perRecipient[]` by grouping delivered + held rows on their single-string `to` field (delivered → 'delivered', held → 'held', neither → 'unknown'); `delivered` = count of delivered rows, `held` = count of held rows, `rejected` = delivered rows whose `.status` string indicates rejection, `matched` = total delivered + held rows. Because the exact `.status` rejection enum is UNCONFIRMED (17-RESEARCH A3), add a code comment noting this and treat unrecognized status strings conservatively as 'delivered' rather than hardcoding a guessed rejected value as confirmed; log the raw status values seen at debug level for first-real-tenant validation. Derive `clicked` best-effort by counting threat-event items whose `analysis[]` includes a click-type value, else `0` (comment the best-effort limitation). + 5. Merge per 17-RESEARCH Pattern 3: build `perRecipient[]` by grouping delivered + held rows on their single-string `to` field (delivered → 'delivered', held → 'held', neither → 'unknown'); first partition `searchDeliveredMessages` rows into non-rejected vs. rejected using `.status` (rejected = row's `.status` string indicates rejection); `delivered` = count of NON-rejected delivered rows (mutually exclusive with `rejected`), `rejected` = count of rejected delivered rows, `held` = count of held rows, `matched` = `delivered + held` (does not include `rejected` — a rejected recipient was never actually delivered). Because the exact `.status` rejection enum is UNCONFIRMED (17-RESEARCH A3), add a code comment noting this and treat unrecognized status strings conservatively as 'delivered' (i.e. not rejected) rather than hardcoding a guessed rejected value as confirmed; log the raw status values seen at debug level for first-real-tenant validation. Derive `clicked` best-effort by counting threat-event items whose `analysis[]` includes a click-type value, else `0` (comment the best-effort limitation). 6. `await setCachedData(cacheKey, result, 300);` then return the result. 7. `catch (err)`: `console.error('[MIMECAST-BLAST-RADIUS] lookup failed', err instanceof Error ? err.message : err)` — log err.message ONLY, never full Mimecast response bodies which may contain other recipients' subjects/content (T-17-02) — and return `{ status:'unavailable', reason:'lookup_failed', error: err instanceof Error ? err.message : String(err) }`. This satisfies BLAST-02: the public entry point never throws. diff --git a/.planning/phases/17-mimecast-blast-radius-lookup/17-RESEARCH.md b/.planning/phases/17-mimecast-blast-radius-lookup/17-RESEARCH.md index 0dcb585..5bc6710 100644 --- a/.planning/phases/17-mimecast-blast-radius-lookup/17-RESEARCH.md +++ b/.planning/phases/17-mimecast-blast-radius-lookup/17-RESEARCH.md @@ -535,7 +535,7 @@ actually call, not `docs/mimecast-api-guide.md`. ## Open Questions -1. **Should this phase's abstraction resolve a per-company Mimecast tenant, or only use the +1. **(RESOLVED: see CONTEXT.md D-05 — single global client for v1, multi-tenant gap documented in code)** Should this phase's abstraction resolve a per-company Mimecast tenant, or only use the single global env-var client?** - What we know: `mimecast_tenants` (migration 062) exists and is actively used by `/api/mimecast/held` and `/api/mimecast/delivered` (the two existing routes closest in @@ -552,7 +552,7 @@ actually call, not `docs/mimecast-api-guide.md`. addition later (swap `getMimecastClient()` for a tenant lookup + `getMimecastClientForTenant()`, using the same fan-out logic). -2. **What are the real string values Mimecast returns for `status` in message-tracking search +2. **(RESOLVED: deferred to runtime observation — 17-01-PLAN.md Task 2 treats unrecognized status strings conservatively as non-rejected and logs raw values at debug level rather than hardcoding a guessed enum)** What are the real string values Mimecast returns for `status` in message-tracking search results (delivered vs. rejected classification)?** - What we know: The TypeScript field exists (`MimecastDeliveredMessage.status: string`) and is populated from `e.status` in the raw API response, per `searchDeliveredMessages()`'s