[object Object]

← back to Designer Wallcoverings

CC suppression guard floor-check (TK-11840): fail-closed on a truncated-but-nonempty suppression set

3e3b130219bd444a1584ae52305f6888d88d17ab · 2026-09-16 17:49:44 -0700 · Steve

The size===0 abort was blind to a partial/corrupt write (e.g. 12 of 5,847 addresses),
which would import ~everyone with the chronic-bounce guard silently disabled. Carry the
file's declared count beside the observed set size and ABORT a LIVE import on a >10%
shortfall. Adds a test-flag-guarded seam (CC_SUPPRESSION_TEST) + verified the guard
reddens on an injected truncated fixture and a scheduled job ignores the override.
Found by Cody the contrarian red-team.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

Files touched

Diff

commit 3e3b130219bd444a1584ae52305f6888d88d17ab
Author: Steve <steve@designerwallcoverings.com>
Date:   Wed Sep 16 17:49:44 2026 -0700

    CC suppression guard floor-check (TK-11840): fail-closed on a truncated-but-nonempty suppression set
    
    The size===0 abort was blind to a partial/corrupt write (e.g. 12 of 5,847 addresses),
    which would import ~everyone with the chronic-bounce guard silently disabled. Carry the
    file's declared count beside the observed set size and ABORT a LIVE import on a >10%
    shortfall. Adds a test-flag-guarded seam (CC_SUPPRESSION_TEST) + verified the guard
    reddens on an injected truncated fixture and a scheduled job ignores the override.
    Found by Cody the contrarian red-team.
    
    Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---
 mailers/cc-api/import-fm-clients.js | 22 +++++++++++++++++++---
 1 file changed, 19 insertions(+), 3 deletions(-)

diff --git a/mailers/cc-api/import-fm-clients.js b/mailers/cc-api/import-fm-clients.js
index 0500ba2f..25c3ba4c 100644
--- a/mailers/cc-api/import-fm-clients.js
+++ b/mailers/cc-api/import-fm-clients.js
@@ -25,7 +25,12 @@ const LIST_NAME = 'DW FileMaker Clients (existing relationship)';
 const BATCH = 500;
 const LIVE = process.env.CC_LIVE === '1' && process.env.CONFIRM === '1';
 const API = 'https://api.cc.email/v3';
-const SUPPRESSION_FILE = path.join(__dirname, 'hygiene', 'suppression-list.json');
+// Testability seam (TK-11840): a fault can be INJECTED into the suppression guard only when the
+// explicit test flag is set — a scheduled/LIVE job must NEVER pass CC_SUPPRESSION_TEST, so it can
+// never silently measure a fixture. Used by the negative test that proves the guard reddens.
+const SUPPRESSION_FILE = (process.env.CC_SUPPRESSION_TEST === '1' && process.env.CC_SUPPRESSION_FILE)
+  ? process.env.CC_SUPPRESSION_FILE
+  : path.join(__dirname, 'hygiene', 'suppression-list.json');
 const SUPPRESSION_RESTORE = path.join(__dirname, 'hygiene', 'suppress-restore.json');
 
 // Durable chronic-bounce guard (TK-11840): load the canonical suppression set so
@@ -35,18 +40,21 @@ const SUPPRESSION_RESTORE = path.join(__dirname, 'hygiene', 'suppress-restore.js
 function loadSuppressionSet() {
   const set = new Set();
   let source = null;
+  let declared = null; // the file's OWN declared count — the "population" beside the observed set size
   try {
     const reg = JSON.parse(fs.readFileSync(SUPPRESSION_FILE, 'utf8'));
     for (const e of reg.emails || []) { const v = String(e).trim().toLowerCase(); if (v) set.add(v); }
+    if (Number.isFinite(reg.count)) declared = reg.count;
     source = 'suppression-list.json';
   } catch {
     try {
       const rm = JSON.parse(fs.readFileSync(SUPPRESSION_RESTORE, 'utf8'));
       for (const r of rm.plan || []) { const v = String(r.email || '').trim().toLowerCase(); if (v) set.add(v); }
+      if (Array.isArray(rm.plan)) declared = rm.plan.length;
       source = 'suppress-restore.json (fallback)';
     } catch { source = null; }
   }
-  return { set, source };
+  return { set, source, declared };
 }
 
 async function req(method, urlPath, body) {
@@ -75,11 +83,19 @@ async function req(method, urlPath, body) {
   const rawContacts = data.import_data;
 
   // --- Durable chronic-bounce suppression guard (TK-11840) ---
-  const { set: suppressed, source: supSource } = loadSuppressionSet();
+  const { set: suppressed, source: supSource, declared: supDeclared } = loadSuppressionSet();
   if (suppressed.size === 0) {
     const msg = 'SUPPRESSION LIST EMPTY/UNREADABLE — refusing to import without the chronic-bounce guard.';
     if (LIVE) { console.error(`ABORT: ${msg} (expected ${SUPPRESSION_FILE})`); process.exit(1); }
     console.warn(`⚠️  ${msg} (dry-run continues; a LIVE run would ABORT)`);
+  } else if (supDeclared != null && suppressed.size < Math.floor(supDeclared * 0.9)) {
+    // Cody/TK-11840: a truncated-but-non-empty file is invisible to the size===0 check.
+    // Carry the file's declared count beside the observed set size — a >10% shortfall means the
+    // suppression set is DEGRADED (partial/corrupt write), so a LIVE import must fail closed too.
+    const msg = `SUPPRESSION LIST DEGRADED — loaded ${suppressed.size} of a declared ${supDeclared} (`
+      + `${((suppressed.size / supDeclared) * 100).toFixed(1)}%); refusing to import on a shrunken guard.`;
+    if (LIVE) { console.error(`ABORT: ${msg} (expected ~${supDeclared} in ${SUPPRESSION_FILE})`); process.exit(1); }
+    console.warn(`⚠️  ${msg} (dry-run continues; a LIVE run would ABORT)`);
   }
   const emailOf = (r) => String(r.email_address || r.email || '').trim().toLowerCase();
   const contacts = rawContacts.filter((r) => !suppressed.has(emailOf(r)));

← 8efc9519 auto-data-snapshot: 2026-09-16T16:47:48 (3 data files) — sho  ·  back to Designer Wallcoverings  ·  auto-data-snapshot: 2026-09-16T17:53:57 (3 data files) — sho 799b8466 →