← back to Dwha Harlequin Onboard
Harden DWHA onboarding before gated canary
a4262a48f1f8581d0d106042bfabdaf01376e189 · 2026-08-31 11:20:56 -0700 · Steve
Files touched
M scripts/check-coverage.mjsM scripts/onboard-batch.mjsA verification/e2e-proof.json
Diff
commit a4262a48f1f8581d0d106042bfabdaf01376e189
Author: Steve <steve@designerwallcoverings.com>
Date: Mon Aug 31 11:20:56 2026 -0700
Harden DWHA onboarding before gated canary
---
scripts/check-coverage.mjs | 1 +
scripts/onboard-batch.mjs | 110 ++++++++++++++++++++++++++++++++++++++------
verification/e2e-proof.json | 70 ++++++++++++++++++++++++++++
3 files changed, 166 insertions(+), 15 deletions(-)
diff --git a/scripts/check-coverage.mjs b/scripts/check-coverage.mjs
index 020e435..95efed4 100644
--- a/scripts/check-coverage.mjs
+++ b/scripts/check-coverage.mjs
@@ -54,6 +54,7 @@ async function main() {
width, material, discontinued, on_shopify, shopify_product_id,
image_rejected, image_rejection_reason, design, features
FROM harlequin_catalog
+ WHERE dw_sku LIKE 'DWHA-%' OR dw_sku IS NULL
ORDER BY id ASC
`);
allRows = result.rows;
diff --git a/scripts/onboard-batch.mjs b/scripts/onboard-batch.mjs
index 2de71df..835863c 100644
--- a/scripts/onboard-batch.mjs
+++ b/scripts/onboard-batch.mjs
@@ -47,7 +47,8 @@ const CADENCE_FILE = path.join(DATA_DIR, 'cadence-counter.json');
const LEDGER_FILE = path.join(DATA_DIR, 'onboard-ledger.jsonl');
const DAY_LIMIT = 25; // Steve-approved cadence
-const SHOPIFY_API = `https://${SHOPIFY_STORE}/admin/api/2024-10`;
+const SHOPIFY_VERSION = process.env.SHOPIFY_API_VERSION || '2026-07';
+const SHOPIFY_API = `https://${SHOPIFY_STORE}/admin/api/${SHOPIFY_VERSION}`;
const SHOPIFY_DELAY = 500; // ms between API calls (rate-limit safe)
const IS_DRY_RUN = process.argv.includes('--dry-run');
@@ -250,6 +251,50 @@ async function shopifyPost(endpoint, payload) {
});
}
+async function shopifyGraphql(query, variables = {}) {
+ const response = await shopifyPost('/graphql.json', { query, variables });
+ if (response.errors?.length) {
+ throw new Error(`Shopify GraphQL: ${JSON.stringify(response.errors).slice(0, 500)}`);
+ }
+ return response.data;
+}
+
+async function findLiveVariantBySku(sku) {
+ const data = await shopifyGraphql(`
+ query FindVariantBySku($query: String!) {
+ productVariants(first: 10, query: $query) {
+ nodes {
+ sku
+ product { id legacyResourceId handle status }
+ }
+ }
+ }
+ `, { query: `sku:${JSON.stringify(sku)}` });
+
+ return data.productVariants.nodes.find(node => node.sku === sku) || null;
+}
+
+async function shopifyDeleteProduct(productId) {
+ const url = new URL(`${SHOPIFY_API}/products/${productId}.json`);
+ return new Promise((resolve, reject) => {
+ const req = https.request({
+ hostname: url.hostname,
+ path: url.pathname,
+ method: 'DELETE',
+ headers: { 'X-Shopify-Access-Token': SHOPIFY_TOKEN },
+ }, res => {
+ let data = '';
+ res.on('data', chunk => data += chunk);
+ res.on('end', () => {
+ if ([200, 204, 404].includes(res.statusCode)) resolve(res.statusCode);
+ else reject(new Error(`Shopify DELETE ${res.statusCode}: ${data.slice(0, 300)}`));
+ });
+ });
+ req.on('error', reject);
+ req.end();
+ });
+}
+
async function shopifyPostImage(productId, localPath, dw_sku) {
const imageData = fs.readFileSync(localPath);
const b64 = imageData.toString('base64');
@@ -321,18 +366,17 @@ async function main() {
product_type, width, length, repeat_v, repeat_h, material,
price_trade, price_retail, image_url, product_url,
fire_rating, finish, application, match_type, design, features,
- color_primary, color_secondary
+ color_primary, color_secondary, on_shopify, shopify_product_id
FROM harlequin_catalog
WHERE
- price_trade IS NOT NULL
+ dw_sku LIKE 'DWHA-%'
+ AND price_trade IS NOT NULL
AND image_url IS NOT NULL
AND dw_sku IS NOT NULL
AND (discontinued IS FALSE OR discontinued IS NULL)
AND (image_rejected IS FALSE OR image_rejected IS NULL)
- AND (on_shopify IS FALSE OR on_shopify IS NULL)
ORDER BY id ASC
- LIMIT $1
- `, [IS_DRY_RUN ? 9999 : remaining]);
+ `);
rows = result.rows;
} catch (err) {
console.error('DB query failed:', err.message);
@@ -340,7 +384,7 @@ async function main() {
process.exit(1);
}
- console.log(`\nViable rows to process: ${rows.length}${IS_DRY_RUN ? ' (dry-run shows all)' : ` (capped at ${remaining} today)`}`);
+ console.log(`\nViable catalog rows to inspect: ${rows.length}${IS_DRY_RUN ? ' (dry-run candidate set; live SKU checks run only in approved live mode)' : ` (creates capped at ${remaining} today)`}`);
if (IS_DRY_RUN) {
console.log('\n--- DRY-RUN PREVIEW (first 10 rows) ---');
@@ -360,9 +404,31 @@ async function main() {
let created = 0, skipped = 0, errors = [];
for (const row of rows) {
+ if (created >= remaining) break;
const { dw_sku, image_url } = row;
console.log(`\n[${created + skipped + 1}/${rows.length}] Processing ${dw_sku} ...`);
+ // 0. Live idempotency check. DB on_shopify is only a cache and may be stale.
+ let existingVariant;
+ try {
+ existingVariant = await findLiveVariantBySku(dw_sku);
+ } catch (lookupErr) {
+ console.error(` STOP — live SKU idempotency check failed: ${lookupErr.message}`);
+ errors.push({ dw_sku, stage: 'live-sku-check', error: lookupErr.message });
+ break; // fail closed: never create when duplicate detection is unavailable
+ }
+
+ if (existingVariant) {
+ const existingProductId = String(existingVariant.product.legacyResourceId);
+ console.log(` SKIP — live SKU already exists on ${existingVariant.product.status} product ${existingProductId}`);
+ if (!row.on_shopify || String(row.shopify_product_id || '') !== existingProductId) {
+ await markOnShopify(pool, row.id, existingProductId);
+ console.log(` db: reconciled stale on_shopify flag to live product ${existingProductId}`);
+ }
+ skipped++;
+ continue;
+ }
+
// 1. Download image
let localImagePath;
try {
@@ -385,6 +451,8 @@ async function main() {
productId = response.product?.id;
if (!productId) throw new Error('No product ID returned from Shopify');
console.log(` shopify: created DRAFT product ${productId}`);
+ const mapPath = saveRestoreMap(dw_sku, String(productId), row);
+ console.log(` restore-map: ${path.basename(mapPath)}`);
} catch (shopErr) {
console.warn(` SKIP — Shopify create failed: ${shopErr.message}`);
errors.push({ dw_sku, stage: 'shopify-create', error: shopErr.message });
@@ -398,9 +466,25 @@ async function main() {
await shopifyPostImage(productId, localImagePath, dw_sku);
console.log(` image: attached to product ${productId}`);
} catch (imgAttachErr) {
- // Non-fatal — product is created, image can be added later; log it
- console.warn(` WARN — image attach failed (product still created): ${imgAttachErr.message}`);
+ console.warn(` image attach failed; compensating DELETE required: ${imgAttachErr.message}`);
errors.push({ dw_sku, stage: 'image-attach', error: imgAttachErr.message, shopify_product_id: productId });
+ try {
+ await shopifyDeleteProduct(productId);
+ fs.renameSync(
+ path.join(RESTORE_DIR, `${dw_sku}.json`),
+ path.join(RESTORE_DIR, `${dw_sku}.rolled-back.json`),
+ );
+ console.log(` compensated: deleted incomplete DRAFT product ${productId}; DB unchanged`);
+ skipped++;
+ continue;
+ } catch (deleteErr) {
+ console.error(` FATAL — incomplete DRAFT remains; restore-map retained: ${deleteErr.message}`);
+ errors.push({ dw_sku, stage: 'compensating-delete', error: deleteErr.message, shopify_product_id: productId });
+ created++;
+ cadence.count++;
+ saveCadenceState(cadence);
+ break; // stop the batch; do not compound an unresolved partial failure
+ }
}
// 4. DB: mark on_shopify
@@ -411,11 +495,7 @@ async function main() {
console.warn(` WARN — DB update failed: ${dbErr.message}`);
}
- // 5. Save restore-map
- const mapPath = saveRestoreMap(dw_sku, String(productId), row);
- console.log(` restore-map: ${path.basename(mapPath)}`);
-
- // 6. Ledger
+ // 5. Ledger
appendLedger({
agent: 'vp-dw-commerce',
ticket: 'TK-10882',
@@ -427,7 +507,7 @@ async function main() {
verify: `shopify product get ${productId}`,
});
- // 7. Also log to global executed-reversible ledger
+ // 6. Also log to global executed-reversible ledger
try {
const execLedger = '/Users/macstudio3/.claude/yolo-queue/executed-reversible/ledger.jsonl';
fs.mkdirSync(path.dirname(execLedger), { recursive: true });
diff --git a/verification/e2e-proof.json b/verification/e2e-proof.json
new file mode 100644
index 0000000..78102c9
--- /dev/null
+++ b/verification/e2e-proof.json
@@ -0,0 +1,70 @@
+{
+ "intent": "Safely prepare TK-10882 for a one-product DWHA Shopify DRAFT canary followed by a 25/day LaunchAgent cadence.",
+ "risk_tier": "R4",
+ "environment": "macstudio3 local dw_unified mirror plus read-only Shopify Admin GraphQL preflight",
+ "timestamp": "2026-08-31T18:18:00.000Z",
+ "build_identity": "working tree based on 9d75a2e; final commit recorded after verification",
+ "baseline": {
+ "coverage": "631 READY, 29 settlement-flagged, 602 clean",
+ "launchagent": "not installed",
+ "restore_maps": 0,
+ "cadence_count": 0,
+ "prefix_state": "0 ACTIVE Harlequin DWHQ-* and 39 ACTIVE DWHF-335* in local mirror"
+ },
+ "checks": [
+ {
+ "name": "JavaScript parse and diff hygiene",
+ "command": "node --check scripts/onboard-batch.mjs; node --check scripts/check-coverage.mjs; git diff --check",
+ "verdict": "PASS"
+ },
+ {
+ "name": "DWHA coverage query",
+ "command": "npm run check",
+ "assertions": ["631 READY", "29 settlement-flagged", "602 clean", "no writes"],
+ "verdict": "PASS"
+ },
+ {
+ "name": "Onboarding dry-run",
+ "command": "npm run dry-run",
+ "assertions": ["only DWHA-* candidates", "658 catalog candidates inspected", "0 Shopify calls", "0 DB writes"],
+ "verdict": "PASS"
+ },
+ {
+ "name": "Live SKU lookup negative branch",
+ "artifact": "Shopify GraphQL productVariants exact query for DWHA-570086",
+ "assertions": ["errors=null", "nodes=[]"],
+ "verdict": "PASS"
+ },
+ {
+ "name": "Live SKU lookup positive branch",
+ "artifact": "Shopify GraphQL productVariants exact query for DWHF-335031",
+ "assertions": ["exact SKU returned", "legacy product ID 7787424448563", "status ACTIVE"],
+ "verdict": "PASS"
+ },
+ {
+ "name": "LaunchAgent static validation",
+ "command": "plutil -lint launchd/com.steve.dwha-onboard-daily.plist; test -x /opt/homebrew/bin/node",
+ "assertions": ["plist OK", "configured Node executable exists", "service remains uninstalled"],
+ "verdict": "PASS"
+ },
+ {
+ "name": "One-product external mutation and persisted-state assertion",
+ "reason": "Requires explicit Steve approval for Shopify DRAFT create plus canonical dw_unified update.",
+ "verdict": "SKIP"
+ },
+ {
+ "name": "Compensating-delete failure-path exercise against Shopify",
+ "reason": "Would require deliberately causing and deleting a production-side DRAFT; held under the same R4 approval gate.",
+ "verdict": "SKIP"
+ },
+ {
+ "name": "LaunchAgent install and first scheduled execution",
+ "reason": "Schedule installation is gated and must follow a successful one-product canary.",
+ "verdict": "SKIP"
+ }
+ ],
+ "cleanup": "No Shopify product, canonical row, or LaunchAgent was changed. Read-only preflights left no retained external test state.",
+ "rollback": "For an approved canary, scripts/rollback.mjs DWHA-XXXXXX deletes the created DRAFT, clears the catalog flag, and archives its restore-map.",
+ "verdict": "BLOCKED",
+ "blocker": "Critical R4 canary and schedule-install paths are intentionally skipped pending Steve approval; see pending-approval/2026-08-31-TK-10882-DWHA-canary-then-cadence.md."
+}
← 9e7a440 auto-data-snapshot: 2026-08-31T11:17:15 (3 data files) — .gi
·
back to Dwha Harlequin Onboard
·
Record DWHA verification commit identities 25a836f →