[object Object]

← back to Norma

TK-11367: account-scoped Novasuede guard for Norma's daily cadence

ac130fddc011c03ef9fdda918ead3752106a45ea · 2026-09-10 07:56:48 -0700 · Steve Abrams

Norma is a THIRD posting pipeline that TK-11086's 2026-09-01 kill never
touched — that patch only fixed ~/Projects/dw-marketing-reels — so Norma kept
fanning the Novasuede line across the DW network for six more days (35 posts,
09-02..09-07) with nothing watching.

Guard sits at the account/product seam in daily-cadence.js because the block is
account-scoped, and reuses the existing 'skip this candidate' path so no other
selection logic changes. It matches on TEXT (title/caption), NOT on a DWNS- sku
prefix: the leaked posts carried DWCC-* skus, which is exactly why TK-11086's
sku-prefix filter missed them.

Scope per Steve 2026-09-10: block designerwallcoverings, hospitalitywallcoverings
and wallpaperinstallers; @suedewallpaper stays ALLOWED as the suede line's own
account (TK-10866 kept it as a valid Novasuede target).

Verified: 34/34 of Norma's real leaked posts would now be blocked and both
@suedewallpaper posts correctly allowed; against 50 live Novasuede products from
Norma's own search pool the guard blocks on all three target accounts and allows
on suede; non-Novasuede negative controls all pass; --dry cadence run clean.
No pm2 restart needed — launchd spawns a fresh node per run.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LjQgkCC1vKXku5Ep5ftooU

Files touched

Diff

commit ac130fddc011c03ef9fdda918ead3752106a45ea
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Thu Sep 10 07:56:48 2026 -0700

    TK-11367: account-scoped Novasuede guard for Norma's daily cadence
    
    Norma is a THIRD posting pipeline that TK-11086's 2026-09-01 kill never
    touched — that patch only fixed ~/Projects/dw-marketing-reels — so Norma kept
    fanning the Novasuede line across the DW network for six more days (35 posts,
    09-02..09-07) with nothing watching.
    
    Guard sits at the account/product seam in daily-cadence.js because the block is
    account-scoped, and reuses the existing 'skip this candidate' path so no other
    selection logic changes. It matches on TEXT (title/caption), NOT on a DWNS- sku
    prefix: the leaked posts carried DWCC-* skus, which is exactly why TK-11086's
    sku-prefix filter missed them.
    
    Scope per Steve 2026-09-10: block designerwallcoverings, hospitalitywallcoverings
    and wallpaperinstallers; @suedewallpaper stays ALLOWED as the suede line's own
    account (TK-10866 kept it as a valid Novasuede target).
    
    Verified: 34/34 of Norma's real leaked posts would now be blocked and both
    @suedewallpaper posts correctly allowed; against 50 live Novasuede products from
    Norma's own search pool the guard blocks on all three target accounts and allows
    on suede; non-Novasuede negative controls all pass; --dry cadence run clean.
    No pm2 restart needed — launchd spawns a fresh node per run.
    
    Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
    Claude-Session: https://claude.ai/code/session_01LjQgkCC1vKXku5Ep5ftooU
---
 agents/instagram-agent/daily-cadence.js | 27 ++++++++++++++++++++++-----
 1 file changed, 22 insertions(+), 5 deletions(-)

diff --git a/agents/instagram-agent/daily-cadence.js b/agents/instagram-agent/daily-cadence.js
index ae69bb0..f7b4db5 100644
--- a/agents/instagram-agent/daily-cadence.js
+++ b/agents/instagram-agent/daily-cadence.js
@@ -81,11 +81,28 @@ function themeFor(h) {
 const BOOST_SHOP = 'designer-laboratory-sandbox.myshopify.com';
 const CANDIDATE_PAGES = 3; // up to 150 guard-gated candidates before giving up
 
-async function tryCandidates(list) {
+// ── Novasuede account guard (TK-11367; implements Steve's TK-11086 order 2026-09-01:
+// "kill any posts of novasuede to designerwallcoverings websites"). Norma is a THIRD posting
+// pipeline that TK-11086's patch never touched — it only fixed ~/Projects/dw-marketing-reels —
+// so Norma kept fanning the Novasuede line across the DW network for six more days (35 posts,
+// 09-02..09-07). Guarded HERE, at the account/product seam, because the block is account-scoped.
+// Matches on TEXT (title/caption), NOT on a `DWNS-` sku prefix: the leaked posts carried DWCC-*
+// skus, which is precisely why TK-11086's sku-prefix filter did not catch them.
+// @suedewallpaper is deliberately NOT blocked — it is the suede line's own account and TK-10866
+// kept it as a valid Novasuede target. Steve confirmed this exact scope 2026-09-10.
+const NOVASUEDE_RE = /nova\s*suede/i;
+const NOVASUEDE_BLOCKED = new Set(['designerwallcoverings', 'hospitalitywallcoverings', 'wallpaperinstallers']);
+const novasuedeBlocked = (acct, ...texts) =>
+  NOVASUEDE_BLOCKED.has(String(acct || '').toLowerCase()) && texts.some((t) => NOVASUEDE_RE.test(String(t || '')));
+
+async function tryCandidates(list, acct) {
   for (const p of list) {
     if (!p.handle || everPosted.has(p.handle)) continue; // don't repost a product
+    if (novasuedeBlocked(acct, p.title, p.handle)) continue;   // cheap pre-filter, before the fetch
     try {
       const r = await content.resolveProduct(p.handle); // throws on leak/no-image (guard) — UNCHANGED
+      // Authoritative re-check on the real product payload (the search list's title can be stale/absent).
+      if (novasuedeBlocked(acct, r.title, r.caption)) continue;
       // Skip if ANY slide (room OR swatch) was already posted — not just the swatch.
       if ((r.images || [r.image_url]).some((u) => everPostedImg.has(imgKey(u)))) continue;
       return { handle: p.handle, title: r.title, image_url: r.image_url };
@@ -94,7 +111,7 @@ async function tryCandidates(list) {
   return null;
 }
 
-async function findProduct(keyword) {
+async function findProduct(keyword, acct) {
   for (let page = 1; page <= CANDIDATE_PAGES; page++) {
     let list = [];
     try {
@@ -105,14 +122,14 @@ async function findProduct(keyword) {
       list = j.products || [];
     } catch { break; }
     if (!list.length) break;
-    const hit = await tryCandidates(list);
+    const hit = await tryCandidates(list, acct);
     if (hit) return hit;
   }
   // Fallback: original 10-capped suggest.json if the Boost API is unreachable.
   try {
     const u = `${STORE}/search/suggest.json?q=${encodeURIComponent(keyword)}&resources%5Btype%5D=product&resources%5Blimit%5D=10`;
     const j = await (await fetch(u)).json();
-    return await tryCandidates((j.resources?.results?.products) || []);
+    return await tryCandidates((j.resources?.results?.products) || [], acct);
   } catch { return null; }
 }
 
@@ -148,7 +165,7 @@ async function findProduct(keyword) {
     if (batch.length >= BATCH) break;
     const t = themeFor(h);
     if (t.sensitive) { needsCody.push(h); continue; }
-    const prod = await findProduct(t.keyword);
+    const prod = await findProduct(t.keyword, h);
     if (!prod) { console.log(`  ⚠ @${h} — no clean product for "${t.keyword}" (all leaked/reused); will retry`); continue; }
     batch.push({ account: h, product: prod.handle, title: prod.title });
   }

← 2c9013b IG fleet: re-enroll @grassclothwallpaper (34->35) — link res  ·  back to Norma  ·  auto-data-snapshot: 2026-09-10T08:12:39 (1 data files) — age ff18124 →