← back to Designerwallcoverings

scripts/bannedword-body-leak/bannedword-body-leak.mjs

86 lines

// bannedword-body-leak — pull the live body_html for the AI-preamble-leaking
// products (from the 2026-06-15 banned-word audit), classify the leak, and PREVIEW
// the strip transform. READ-ONLY (GraphQL GET of descriptionHtml). No writes. $0.
//
// Leak classes:
//   md_fence    — markdown code fences wrapping the body (```html ... ```)
//   ai_preamble — leading "Sure, here is..." / "Certainly!" / "As an AI..." etc.
//   ai_trailing — trailing "Let me know if...", "I hope this helps"
// Strip transform (previewed, not applied): drop fences + leading/trailing AI lines.
import { execFileSync } from 'node:child_process';
import fs from 'node:fs';

const TOKEN = (fs.readFileSync(`${process.env.HOME}/Projects/secrets-manager/.env`, 'utf8').match(/^SHOPIFY_ADMIN_TOKEN=(.+)$/m) || [])[1]?.trim();
const HOST = 'designer-laboratory-sandbox.myshopify.com', API = '2024-10';
const AUDIT = `${process.env.HOME}/.claude/yolo-queue/banned-word-live-audit-2026-06-15.json`;
const OUT = `${process.env.HOME}/.claude/yolo-queue/bannedword-body-leak-2026-06-16.json`;
const MD = `${process.env.HOME}/.claude/yolo-queue/bannedword-body-leak-2026-06-16.md`;

const ids = JSON.parse(fs.readFileSync(AUDIT, 'utf8')).samples.preamble.map(p => `gid://shopify/Product/${p.id}`);

async function gql(query) {
  const res = await fetch(`https://${HOST}/admin/api/${API}/graphql.json`, {
    method: 'POST', headers: { 'X-Shopify-Access-Token': TOKEN, 'Content-Type': 'application/json' },
    body: JSON.stringify({ query }),
  });
  return (await res.json()).data;
}

// strip transform
const FENCE = /^\s*```(?:html|json|markdown)?\s*$|```/gim;
const PREAMBLE = /^\s*(?:sure|certainly|absolutely|of course|here(?:'s| is)(?: the| a)?|below is|i'd be happy to|i would be happy to|as an ai|as a language model|i cannot|i can't|i apologize|okay|ok)\b[^.\n]*[.:!]?\s*/i;
const TRAILING = /\s*(?:let me know if[^.\n]*\.?|i hope this helps[^.\n]*\.?|feel free to[^.\n]*\.?|hope (?:this|that) helps[^.\n]*\.?)\s*$/i;

function classify(body) {
  const cls = [];
  if (/```/.test(body)) cls.push('md_fence');
  if (PREAMBLE.test(body.replace(/<[^>]+>/g, '').trimStart())) cls.push('ai_preamble');
  if (TRAILING.test(body.replace(/<[^>]+>/g, '').trimEnd())) cls.push('ai_trailing');
  return cls;
}
function strip(body) {
  let s = body.replace(FENCE, '');
  // strip leading preamble from the visible text (after any opening tag)
  s = s.replace(/^(\s*(?:<p>|<div>|<span>)?\s*)/i, (m) => m).replace(PREAMBLE, '');
  s = s.replace(TRAILING, '');
  return s.trim();
}

(async () => {
  const out = [];
  for (let i = 0; i < ids.length; i += 40) {
    const chunk = ids.slice(i, i + 40);
    const d = await gql(`{ nodes(ids: [${chunk.map(x => `"${x}"`).join(',')}]) { ... on Product { id title vendor descriptionHtml } } }`);
    for (const n of (d?.nodes || [])) {
      if (!n) continue;
      const body = n.descriptionHtml || '';
      const cls = classify(body);
      const after = strip(body);
      out.push({ id: n.id.split('/').pop(), vendor: n.vendor, title: n.title, classes: cls,
        before_len: body.length, after_len: after.length, removed_chars: body.length - after.length,
        before_head: body.slice(0, 160), after_head: after.slice(0, 160) });
    }
  }
  const byClass = out.reduce((m, r) => { for (const c of (r.classes.length ? r.classes : ['none'])) m[c] = (m[c]||0)+1; return m; }, {});
  const byVendor = out.reduce((m, r) => (m[r.vendor] = (m[r.vendor]||0)+1, m), {});
  const report = { generated_at: new Date().toISOString(), source: 'live Shopify descriptionHtml (READ-ONLY)', total: out.length, by_class: byClass, by_vendor: byVendor, products: out };
  fs.writeFileSync(OUT, JSON.stringify(report, null, 2));

  let md = `# Banned-word body-leak (AI preamble) — live cleanup list + strip preview\n\n`;
  md += `**$0, READ-ONLY** (pulled live descriptionHtml for the ${out.length} flagged products). No writes.\n\n`;
  md += `## Leak classes\n${Object.entries(byClass).map(([k,v])=>`- **${k}**: ${v}`).join('\n')}\n\n`;
  md += `## By vendor\n${Object.entries(byVendor).sort((a,b)=>b[1]-a[1]).map(([k,v])=>`- ${k}: ${v}`).join('\n')}\n\n`;
  md += `## Strip transform (previewed, GATED to apply)\n`;
  md += `1. remove markdown code fences (\\\`\\\`\\\`html … \\\`\\\`\\\`)\n2. strip leading AI preamble ("Sure, here is…", "Certainly!", "As an AI…")\n3. strip trailing AI notes ("Let me know if…", "I hope this helps")\n\n`;
  md += `## Before → after (sample)\n`;
  for (const r of out.filter(r => r.classes.length).slice(0, 15)) {
    md += `\n**${r.vendor} · ${r.title}** (id ${r.id}, classes: ${r.classes.join(',')}, −${r.removed_chars} chars)\n`;
    md += `- before: \`${r.before_head.replace(/\n/g,' ')}\`\n`;
    md += `- after:  \`${r.after_head.replace(/\n/g,' ')}\`\n`;
  }
  md += `\n_Full list + per-product before/after in the JSON. Apply via shopify_api_queue {id, body_html:<stripped>} — GATED, Steve approves._\n`;
  fs.writeFileSync(MD, md);
  console.log(`[body-leak] ${out.length} products | classes: ${JSON.stringify(byClass)}`);
  console.log(`Report: ${MD}`);
})().catch(e => { console.error('FATAL', e.message); process.exit(1); });