← 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}`);
}
}