← 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
M agents/instagram-agent/daily-cadence.js
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 →