← back to Dw Activation Debug TK11314
scripts/replay-readiness.js
50 lines
#!/usr/bin/env node
'use strict';
const fs = require('node:fs');
const path = require('node:path');
const { canonicalProduct, widthFromLive } = require('../lib/live-product-fields');
const { fiveFieldExtra } = require('../lib/five-field-extra');
const [snapshotPath, beforeModule, afterModule, output] = process.argv.slice(2);
if (!output) throw new Error('Usage: replay-readiness.js <snapshot> <old-validator> <new-validator> <output>');
const snapshot = JSON.parse(fs.readFileSync(snapshotPath));
const beforeValidator = require(path.resolve(beforeModule));
const afterValidator = require(path.resolve(afterModule));
const counts = { products: snapshot.products.length, width_rejections_removed: 0,
description_rejections_removed: 0, old_canonical_and_extra_pass: 0, new_canonical_and_extra_pass: 0,
variant_pagination_needed: 0, metafield_pagination_needed: 0 };
const changed = [];
const vendors = {};
for (const product of snapshot.products) {
if (!product || product.variants?.pageInfo?.hasNextPage || product.metafields?.pageInfo?.hasNextPage)
throw new Error('Snapshot contains missing or incomplete products');
const shape = canonicalProduct(product, product.variants.nodes[0]?.sku, product.vendor);
const first = { ...product, variants: { nodes: product.variants.nodes.slice(0, 20) },
metafields: { nodes: product.metafields.nodes.slice(0, 80) } };
const oldShape = canonicalProduct(first, shape.dwSku, product.vendor);
const get = ns => first.metafields.nodes.find(m => m.namespace === ns && m.key === 'width')?.value || '';
oldShape.specs.width = get('global') || get('custom');
oldShape.variants = first.variants.nodes.map(v => ({ sku: v.sku }));
const before = beforeValidator.validateBeforeActivate(oldShape);
const after = afterValidator.validateBeforeActivate(shape);
const oldExtra = fiveFieldExtra(first), extra = fiveFieldExtra(product);
const widthRemoved = before.reasons.includes('missing spec(s): width') && !after.reasons.includes('missing spec(s): width');
const descRemoved = before.reasons.some(r => /legal\/disclaimer/.test(r)) && !after.reasons.some(r => /legal\/disclaimer/.test(r));
if (widthRemoved) counts.width_rejections_removed++;
if (descRemoved) counts.description_rejections_removed++;
if (before.ok && oldExtra.ok) counts.old_canonical_and_extra_pass++;
if (after.ok && extra.ok) counts.new_canonical_and_extra_pass++;
if (product.variants.nodes.length > 20) counts.variant_pagination_needed++;
if (product.metafields.nodes.length > 80) counts.metafield_pagination_needed++;
if (JSON.stringify(before.reasons) !== JSON.stringify(after.reasons) || JSON.stringify(oldExtra.reasons) !== JSON.stringify(extra.reasons)) {
vendors[product.vendor] = (vendors[product.vendor] || 0) + 1;
changed.push({ id: product.id, vendor: product.vendor, title: product.title,
width_source: widthFromLive(product).source, before: before.reasons, after: after.reasons,
extra_before: oldExtra.reasons, extra_after: extra.reasons });
}
}
const report = { captured_at: snapshot.captured_at,
scope: 'Canonical readiness and price/sample checks only. Manufacturer provenance, private-label, showroom, settlement and publication gates still apply.',
...counts, vendors, changed_products: changed };
fs.writeFileSync(output, JSON.stringify(report, null, 2) + '\n');
console.log(JSON.stringify({ ...counts, changed_products: changed.length, vendors, output }));