Skip to content
// Turn the AI output (or a template / supplied draft) into final copy, then run the guardrails.
const s = $('Settings (edit me)').first().json;
const esc = (t) => String(t ?? '').replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
const cut = (t, n) => { t = String(t ?? '').replace(/\s+/g, ' ').trim(); if (t.length <= n) return t; const c = t.slice(0, n + 1); return c.slice(0, c.lastIndexOf(' ')).replace(/[,;:.\-–]+$/, '') ; };
const cap = (t) => t.charAt(0).toUpperCase() + t.slice(1);

function template(p) {
  const a = p.attributes;
  const bits = [a.material && `made from ${a.material}`, a.color && `in ${a.color}`].filter(Boolean).join(' ');
  const lead = `${p.title}${bits ? ', ' + bits : ''}.`;
  const use = p.product_type ? ` A simple, well-made ${p.product_type.toLowerCase()} from ${s.brand_name}.` : ` From ${s.brand_name}.`;
  return {
    description: lead + use,
    bullets: Object.entries(a).filter(([, v]) => v).map(([k, v]) => `${cap(k)}: ${v}`).slice(0, 5),
    seo_title: `${p.title} | ${s.brand_name}`,
    meta_description: `${lead} Shop ${p.title.toLowerCase()} at ${s.brand_name}.`,
    tags: [p.product_type, a.material, a.color].filter((t) => t && !/\d/.test(t) && String(t).length <= 25).map((t) => String(t).toLowerCase()),
  };
}

return $input.all().map((item, i) => {
  if (item.json.nothing_to_do) return item;
  const fromAi = !item.json.product_id; // the AI chain outputs only { output } or { error }
  const p = fromAi ? $('Pick Products').itemMatching(i).json : item.json;
  const ai = fromAi ? item.json.output : null;
  let c, source, ai_error;
  if (p.draft) { c = p.draft; source = 'supplied'; }
  else if (ai?.description) { c = ai; source = 'ai'; }
  else { c = template(p); source = 'template'; if (fromAi) ai_error = String(item.json.error?.message ?? item.json.error ?? 'no AI output').slice(0, 200); }

  const description = String(c.description ?? '').trim();
  const bullets = (c.bullets ?? []).map((b) => String(b).trim()).filter(Boolean).slice(0, 6);
  const issues = [];

  // 1. Banned claims (legal risk / off-brand)
  const fullText = [description, ...bullets, c.seo_title, c.meta_description].join(' ');
  for (const phrase of s.banned_phrases) if (new RegExp(`\\b${phrase}\\b`, 'i').test(fullText)) issues.push(`banned phrase: "${phrase}"`);
  // 2. Numbers that aren't in the product data (invented specs)
  const facts = JSON.stringify([p.title, p.attributes, p.price, p.existing_text]).toLowerCase();
  for (const n of new Set(fullText.match(/\d+(?:[.,]\d+)?/g) ?? [])) if (!facts.includes(n.toLowerCase())) issues.push(`number not in product data: ${n}`);
  // 3. Length
  const wc = [description, ...bullets].join(' ').split(/\s+/).filter(Boolean).length;
  if (wc > s.max_words) issues.push(`too long: ${wc} words (max ${s.max_words})`);
  if (wc < 15) issues.push(`too short: ${wc} words`);
  if (Object.keys(p.attributes).length === 0 && !p.existing_text) issues.push('thin product data: add material, size or other attributes');

  const seo_title = cut(c.seo_title || p.title, 60);
  const meta_description = cut(c.meta_description || description, 155);
  const tags = [...new Set([...p.tags, ...(c.tags ?? [])].map((t) => String(t).trim().toLowerCase()).filter(Boolean))].slice(0, s.max_tags);
  const body_html = `<p>${esc(description)}</p>` + (bullets.length ? `\n<ul>\n${bullets.map((b) => `  <li>${esc(b)}</li>`).join('\n')}\n</ul>` : '');

  return {
    json: {
      product_id: p.product_id, title: p.title, source, ai_error,
      status: issues.length ? 'needs_review' : 'ready', issues,
      copy: { description, bullets, body_html, seo_title, meta_description, tags, word_count: wc },
      original_html: p.existing_text,
      ...(p._run ? { _run: p._run } : {}),
    },
  };
});