← back to Dw Contact Us Pages

scripts/verify.mjs

180 lines

#!/usr/bin/env node
// verify.mjs — READ ONLY post-apply verification. TK-11925.
//   node scripts/verify.mjs [--n 5] [--storefront https://www.designerwallcoverings.com]
//
// Per CLAUDE.md TK-11431: an UNMEASURED input is never PASS. Every check reports
// MEASURED-GOOD / MEASURED-BAD / NOT-MEASURED, carries population beside observed,
// and 0-of-0 is WARN, never green.
import { writeFileSync, mkdirSync } from 'node:fs';
import { DATA_DIR, parseArgs, gql, loadTargets, VENDORS, TARGET_PUBLICATIONS, isSampleVariant } from './lib.mjs';

const a = parseArgs();
const N = Number(a.n || 5);
const STORE = String(a.storefront || 'https://www.designerwallcoverings.com').replace(/\/$/, '');

// Narrow the HTML to the product-details region. Everything price/cart/sample shaped
// must be asserted INSIDE this window — see the note at the call site.
function scopeDetails(html) {
  const i = html.indexOf('product-details-wrapper');
  if (i < 0) return null;
  let j = html.length;
  for (const e of ['static-product-recommendations', 'id="dw-similar"', 'You may also like', 'END sections: footer', '<footer']) {
    const k = html.indexOf(e, i);
    if (k > -1 && k < j) j = k;
  }
  const out = html.slice(i, j);
  // A truncated extraction would score 0 on every token and fake a PASS.
  return out.length < 1000 ? null : out;
}

const targets = loadTargets();
const sample = [];
for (const v of VENDORS) sample.push(...targets.filter((p) => p.vendor === v).slice(0, N));
if (!sample.length) { console.error('WARN: population 0 — nothing sampled. NOT-MEASURED, not PASS.'); process.exit(1); }

const Q = `query V($ids: [ID!]!) {
  nodes(ids: $ids) { ... on Product {
    id handle vendor templateSuffix
    resourcePublicationsV2(first: 30) { nodes { isPublished publication { id name } } }
    variants(first: 100) { nodes { id sku title inventoryPolicy inventoryQuantity inventoryItem { tracked } } }
  } } }`;

const state = new Map();
for (let i = 0; i < sample.length; i += 20) {
  const d = await gql(Q, { ids: sample.slice(i, i + 20).map((p) => p.id) });
  for (const n of d.nodes) if (n) state.set(n.id, n);
}

const TARGET_PUB_IDS = new Set(TARGET_PUBLICATIONS.map((p) => p.id));
const rows = [];
let anyFail = 0, anyUnmeasured = 0;

for (const p of sample) {
  const r = { handle: p.handle, vendor: p.vendor, checks: {} };
  const set = (k, verdict, detail) => { r.checks[k] = { verdict, detail }; if (verdict === 'FAIL') anyFail++; if (verdict === 'NOT-MEASURED') anyUnmeasured++; };

  // -- 1. admin state
  const s = state.get(p.id);
  if (!s) set('templateSuffix', 'NOT-MEASURED', 'product not returned by admin API');
  else {
    set('templateSuffix', s.templateSuffix === 'contact-us' ? 'PASS' : 'FAIL', `suffix=${JSON.stringify(s.templateSuffix)}`);
    const ns = s.variants.nodes.filter((v) => !isSampleVariant(v));
    const bad = ns.filter((v) => !(v.inventoryPolicy === 'DENY' && v.inventoryItem?.tracked === true && (v.inventoryQuantity || 0) <= 0));
    set('variants_hardened', ns.length === 0 ? 'NOT-MEASURED' : (bad.length ? 'FAIL' : 'PASS'),
        `${ns.length - bad.length}/${ns.length} non-sample DENY+tracked+qty<=0` + (ns.length === 0 ? ' (sample-only product: 0 of 0)' : ''));
    const smp = s.variants.nodes.filter((v) => isSampleVariant(v));
    set('sample_untouched', smp.length === 0 ? 'NOT-MEASURED' : 'PASS', `${smp.length} sample variant(s) present`);
    const stillOn = s.resourcePublicationsV2.nodes.filter((x) => TARGET_PUB_IDS.has(x.publication.id) && x.isPublished);
    set('channels_unpublished', stillOn.length ? 'FAIL' : 'PASS',
        stillOn.length ? 'still on: ' + stillOn.map((x) => x.publication.name).join(', ') : 'off G&YT / Shop / Buy Button');
    const os = s.resourcePublicationsV2.nodes.find((x) => x.publication.name === 'Online Store');
    set('online_store_kept', !os ? 'NOT-MEASURED' : (os.isPublished ? 'PASS' : 'FAIL'), os ? `published=${os.isPublished}` : 'no Online Store publication row');
  }

  // -- 2. live PDP HTML
  let html = null;
  try {
    const res = await fetch(`${STORE}/products/${p.handle}`, { headers: { 'User-Agent': 'dw-contact-us-verify/1.0' } });
    if (res.ok) html = await res.text(); else set('pdp_fetch', 'NOT-MEASURED', `HTTP ${res.status}`);
  } catch (e) { set('pdp_fetch', 'NOT-MEASURED', String(e).slice(0, 80)); }

  if (html === null) {
    for (const k of ['contact_block', 'no_add_to_cart', 'no_price_markup', 'no_sample_button', 'body_class'])
      set(k, 'NOT-MEASURED', 'PDP HTML unavailable');
  } else {
    set('contact_block', /class="dw-cu"|dw-cu__h/.test(html) ? 'PASS' : 'FAIL', 'dw-cu block in HTML');
    // SCOPE MATTERS. Measured on a known no-buy-box PDP (a Newmor showroom product):
    // the literal strings "Add to cart" (x4), "dl-sample-btn" (x4) and "Complimentary
    // Sample" (x1) appear elsewhere in the document — locale JSON, quick-shop and
    // recommendation templates — so a whole-document match reports FAIL on a page that
    // is already correct. Scoped to the product-details region, that same page scores 0
    // on every token and a buy-box page scores non-zero: a real discriminator.
    const details = scopeDetails(html);
    if (details === null) {
      for (const k of ['no_add_to_cart', 'no_price_markup', 'no_sample_button'])
        set(k, 'NOT-MEASURED', 'product-details region not found in HTML');
    } else {
      const cart = /class="add-to-cart|Add to cart/i.test(details);
      set('no_add_to_cart', cart ? 'FAIL' : 'PASS', cart ? 'add-to-cart markup in product details' : 'none in product details');
      const priced = /class="product__price"|class="product__form"|class="money"|<span class="money">/.test(details);
      set('no_price_markup', priced ? 'FAIL' : 'PASS', priced ? 'price/form markup in product details' : 'none in product details');
      // Match a RENDERED sample button via its class attribute, NOT the bare token: theme.liquid's
      // `.dl-sample-btn` querySelector JS (lines 731/764) + locale/quick-shop templates put the bare
      // string inside the scope window even on a clean page (verified live 2026-09-20:
      // real_sample_btn_elements=0 on DG/RL/CL samples). This mirrors no_add_to_cart / no_price_markup
      // above, which already require class="…", and still FAILs on a real <button class="dl-sample-btn">.
      const smpBtn = /class="[^"]*\bdl-sample-btn\b|class="[^"]*\bdl-second-sample-btn\b/.test(details);
      set('no_sample_button', smpBtn ? 'FAIL' : 'PASS', smpBtn ? 'sample UI in product details' : 'none in product details');
    }
    set('body_class', /template-suffix-contact-us/.test(html) ? 'PASS' : 'FAIL', 'body.template-suffix-contact-us hook');
    // Review fixes (contrarian #3/#4): structured data must carry NO Offer/price, and the
    // out-of-loop RL "Minimum order" notice must not render on a page with nothing to order.
    const ld = [...html.matchAll(/<script type="application\/ld\+json">([\s\S]*?)<\/script>/g)].map((m) => m[1]).join('\n');
    const hasOffer = /"@type":\s*"Offer"|"offers"\s*:/.test(ld);
    set('no_jsonld_offer', ld ? (hasOffer ? 'FAIL' : 'PASS') : 'NOT-MEASURED', ld ? (hasOffer ? 'Offer/price present in JSON-LD' : 'no Offer in JSON-LD') : 'no JSON-LD block found');
    // Require ATTRIBUTE syntax, not a bare token: the bare `data-min-order-notice` matched the
    // page's own defensive CSS selector `[data-min-order-notice]` inside <style>, firing WARN on
    // 100% of products regardless of truth (Cody red-team 2026-09-20). Same fix already applied to
    // no_sample_button above; a real rendered notice still carries class="…"/data-…="".
    const minOrder = /class="[^"]*\bmin-order-notice\b|data-min-order-notice="/.test(html);
    set('min_order_notice_hidden', minOrder ? 'WARN' : 'PASS', minOrder ? 'min-order markup present (hidden by CSS only)' : 'no min-order markup');
  }

  // -- 3. public product json
  try {
    const res = await fetch(`${STORE}/products/${p.handle}.json`);
    if (!res.ok) set('json_unavailable', 'NOT-MEASURED', `HTTP ${res.status}`);
    else {
      const j = await res.json();
      const ns = (j.product?.variants || []).filter((v) => !isSampleVariant(v));
      // This store's public /products/<handle>.json OMITS `available` (verified null on target AND
      // control products, Cody red-team 2026-09-20), so a truthiness test could never FAIL — a false
      // PASS. Report NOT-MEASURED unless `available` is a real boolean (CLAUDE.md TK-11431 #1). The
      // authoritative unbuyable signal is `variants_hardened` above (admin API: DENY+tracked+qty<=0).
      const measurable = ns.filter((v) => typeof v.available === 'boolean');
      const buyable = measurable.filter((v) => v.available === true);
      if (ns.length === 0) set('json_rolls_unavailable', 'NOT-MEASURED', 'sample-only product: 0 of 0');
      else if (measurable.length === 0) set('json_rolls_unavailable', 'NOT-MEASURED', `public .json omits available for ${ns.length} variant(s); authoritative check = variants_hardened (admin)`);
      else set('json_rolls_unavailable', buyable.length ? 'FAIL' : 'PASS', `${measurable.length - buyable.length}/${measurable.length} non-sample available:false (public .json)`);
    }
  } catch (e) { set('json_unavailable', 'NOT-MEASURED', String(e).slice(0, 80)); }

  rows.push(r);
}

// ---- positive control: a handle that SHOULD already be clean (no buy box) proves the
// scoped extractor can still find markup when it is there, rather than always scoring 0.
if (a.control) {
  try {
    const html = await (await fetch(`${STORE}/products/${a.control}`)).text();
    const d = scopeDetails(html);
    const hit = d === null ? null : /class="add-to-cart|class="product__price"|dl-sample-btn/.test(d);
    console.log(`\ncontrol ${a.control}: scope=${d === null ? 'NOT-FOUND' : d.length + 'B'} buybox_markup=${hit}`);
  } catch (e) { console.log(`control ${a.control}: NOT-MEASURED (${String(e).slice(0, 60)})`); }
}

// ---- table
const cols = [...new Set(rows.flatMap((r) => Object.keys(r.checks)))];
const pad = (s, n) => String(s).padEnd(n);
console.log('\nTK-11925 verify · sampled ' + rows.length + ' products (' + N + ' per vendor) · storefront ' + STORE + '\n');
console.log(pad('handle', 42) + cols.map((c) => pad(c.slice(0, 13), 15)).join(''));
for (const r of rows) {
  console.log(pad(r.handle.slice(0, 40), 42) + cols.map((c) => {
    const v = r.checks[c]?.verdict || '-';
    const mark = v === 'PASS' ? 'PASS' : v === 'FAIL' ? 'FAIL' : v === '-' ? '-' : 'NOT-MEAS';
    return pad(mark, 15);
  }).join(''));
}
for (const r of rows) for (const [k, v] of Object.entries(r.checks)) if (v.verdict !== 'PASS') console.log(`  ! ${r.handle} ${k}: ${v.verdict} — ${v.detail}`);

const verdict = anyFail ? 'FAIL' : anyUnmeasured ? 'WARN' : 'PASS';
console.log(`\nVERDICT: ${verdict}  (fail=${anyFail}, not-measured=${anyUnmeasured}, population=${targets.length}, observed=${rows.length})`);
console.log('NOT MEASURED BY THIS SCRIPT: the Boost grid "Contact us for pricing" swap is client-side JS —');
console.log('  it cannot be asserted from raw HTML. Check a collection page in a browser.');
mkdirSync(DATA_DIR, { recursive: true });
writeFileSync(`${DATA_DIR}/verify-latest.json`, JSON.stringify({
  ts: new Date().toISOString(), verdict, fail: anyFail, not_measured: anyUnmeasured,
  population: targets.length, observed: rows.length, storefront: STORE, rows,
}, null, 2));
process.exit(verdict === 'FAIL' ? 1 : 0);