← 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
M mailers/cc-api/import-fm-clients.js
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 →