← back to Designerwallcoverings

scripts/c34-osborne-gaston/DRAFT-worker-blank-reverify-guard.patch.js

104 lines

/* ============================================================================
 * DRAFT ONLY — DO NOT APPLY TO THE KAMATERA WORKER WITHOUT STEVE'S GO.
 *
 * Drain-time live-blank re-verify guard for metafield writes.
 * Target file (on Kamatera, READ-ONLY here):
 *   /root/DW-Agents/shopify-queue-worker/worker.js
 *
 * WHY: The officer's REQUIRED REVISION on the c34 Osborne/Gaston specs push
 * was to confirm the drain worker re-reads the live product metafield and
 * SKIPS if already non-null (idempotent blank-only) before each write. The
 * audit found the worker does NOT do this — it is a generic executor that
 * pushes whatever payload was enqueued (no per-job live blank check exists
 * anywhere in processJob). In the c34 run, blank-only safety was enforced at
 * ENQUEUE time by enqueue-specs.py. This patch moves the guarantee into the
 * worker so ANY future enqueuer (which may not live-verify) is also safe.
 *
 * DESIGN:
 *  - Scope: ONLY metafield-create writes (POST /products/{id}/metafields.json)
 *    targeting namespace 'custom'. All other jobs pass through untouched.
 *  - Fail-OPEN: any guard error (network, parse, etc.) falls through to the
 *    normal write — exactly the pattern the existing dedupGuardForCreate uses
 *    (lines ~675-688). A guard that fail-CLOSED could silently stall a real
 *    push; fail-open keeps current behavior on error.
 *  - Idempotent blank-only: if the live product already has a non-empty value
 *    for that custom.<key>, mark the job 'done' with a skip marker and return
 *    BEFORE the Shopify write. A stale-mirror false-blank therefore can never
 *    overwrite a real live value.
 *  - Read cost: ONE extra GraphQL read per metafield job (cheap; far under the
 *    bucket). Only runs for the metafield-write job shape, not the ~950/day
 *    general traffic.
 *
 * INTEGRATION POINT: inside processJob(job), AFTER the existing
 * "PRE-PUSH DEDUP GUARD (2026-06-12)" try/catch block (worker.js ~line 688)
 * and BEFORE `try { const res = await shopifyRequest(...) }` (~line 690).
 * It reuses the in-scope `token`, `shopifyRequest`, and `pool` already
 * defined above that point.
 * ========================================================================== */


/* ---- ADD near the other top-level helpers (e.g. after collectUserErrors) ---- */

// Parse a metafield-create job into { productId, namespace, key } or null if it
// is not a single custom.* metafield POST. Returns null for anything else so the
// guard is a strict no-op outside its scope.
function parseMetafieldCreateJob(job) {
    if (job.method !== 'POST') return null;
    const m = /^\/products\/(\d+)\/metafields\.json$/.exec(job.endpoint || '');
    if (!m) return null;
    const payload = typeof job.payload === 'string' ? JSON.parse(job.payload) : job.payload;
    const mf = payload && payload.metafield;
    if (!mf || mf.namespace !== 'custom' || !mf.key) return null;
    return { productId: m[1], namespace: mf.namespace, key: mf.key };
}

// Live blank re-verify: returns true if the live product ALREADY has a
// non-empty value for custom.<key> (=> the write should be skipped). Throws on
// any transport/parse error so the caller can fail-OPEN.
async function liveMetafieldAlreadyFilled(productId, key, token) {
    const res = await shopifyRequest(
        'GET',
        `/products/${productId}/metafields.json?namespace=custom&key=${encodeURIComponent(key)}`,
        null,
        token
    );
    const list = (res.body && res.body.metafields) || [];
    const hit = list.find(x => x.namespace === 'custom' && x.key === key);
    const val = hit && hit.value;
    return typeof val === 'string' ? val.trim().length > 0 : (val != null);
}


/* ---- INSERT in processJob(job), right after the dedupGuardForCreate block ---- */
/* ---- and immediately before `try { const res = await shopifyRequest(...) }` -- */

    // --- DRAIN-TIME LIVE BLANK RE-VERIFY (officer-required for c34 specs push) ---
    // Blank-fill-only invariant enforced at the drain: re-read the live metafield
    // and SKIP if it already carries a value, so a stale-mirror false-blank can
    // never overwrite a real live value. Fail-OPEN on any guard error.
    const mfJob = (() => { try { return parseMetafieldCreateJob(job); } catch { return null; } })();
    if (mfJob) {
        try {
            const filled = await liveMetafieldAlreadyFilled(mfJob.productId, mfJob.key, token);
            if (filled) {
                await pool.query(
                    `UPDATE shopify_api_queue
                       SET status = 'done',
                           response_body = $2,
                           error = $3,
                           completed_at = NOW()
                     WHERE id = $1`,
                    [job.id,
                     JSON.stringify({ blank_reverify_skip: true, product_id: mfJob.productId, key: mfJob.key }),
                     `BLANK-REVERIFY SKIP: custom.${mfJob.key} already non-null live`]
                );
                await pool.query(`SELECT pg_notify('shopify_queue_done', $1)`, [String(job.id)]);
                console.log(`[blank-reverify] #${job.id} custom.${mfJob.key} already filled on product ${mfJob.productId} — skipped write`);
                return;
            }
        } catch (rvErr) {
            // Fail-OPEN: proceed with the write, same posture as dedupGuardForCreate.
            console.error(`[blank-reverify] #${job.id} guard error (proceeding with write): ${rvErr.message}`);
        }
    }