← back to Dw Settlement Audit
settlement audit stage 2: Gemini vision verifier (resumable, no auto-delete)
e577f6fe91e29a9c0b14f16339b50f91c700e4fd · 2026-05-18 17:30:00 -0700 · SteveStudio2
Files touched
Diff
commit e577f6fe91e29a9c0b14f16339b50f91c700e4fd
Author: SteveStudio2 <steve@designerwallcoverings.com>
Date: Mon May 18 17:30:00 2026 -0700
settlement audit stage 2: Gemini vision verifier (resumable, no auto-delete)
---
verify-candidates.js | 117 +++++++++++++++++++++++++++++++++++++++++++++++++++
1 file changed, 117 insertions(+)
diff --git a/verify-candidates.js b/verify-candidates.js
new file mode 100644
index 0000000..bc8a4d6
--- /dev/null
+++ b/verify-candidates.js
@@ -0,0 +1,117 @@
+#!/usr/bin/env node
+/* DW settlement audit — stage 2: vision verify.
+ *
+ * Reads candidates.jsonl (stage-1 keyword hits), fetches each product's primary
+ * image, and runs it through Gemini vision against the verbatim settlement gate.
+ * Writes verdicts.jsonl. Resumable — skips products already in verdicts.jsonl.
+ *
+ * NEVER deletes or unpublishes anything. Output is a report for Steve's sign-off.
+ *
+ * SHOPIFY_ADMIN_TOKEN=... GEMINI_API_KEY=... node verify-candidates.js
+ */
+'use strict';
+const fs = require('fs');
+
+const SHOP = 'designer-laboratory-sandbox.myshopify.com';
+const STOKEN = process.env.SHOPIFY_ADMIN_TOKEN;
+const GKEY = process.env.GEMINI_API_KEY;
+if (!STOKEN || !GKEY) { console.error('need SHOPIFY_ADMIN_TOKEN + GEMINI_API_KEY'); process.exit(1); }
+
+const CAND = 'candidates.jsonl';
+const VERD = 'verdicts.jsonl';
+const PROG = 'progress2.txt';
+
+// Verbatim settlement gate — combined Part A + Part B test.
+const GATE = `You are auditing a wallcovering product image against a binding legal settlement.
+
+PART A — a design satisfies Part A only if ALL THREE are present together:
+ A1. Repeating pattern with directional variation amongst leaves, palm fronds, or similar foliage.
+ A2. Open space between the leaves.
+ A3. More than one ink/color.
+
+PART B — a design satisfies Part B if ANY ONE appears:
+ Bananas or banana pods; grapes; birds (any species); butterflies (any species).
+
+The design is PROHIBITED if and only if BOTH Part A is fully satisfied AND Part B is satisfied.
+A design satisfying Part A but with NO Part B element is PERMITTED.
+A Part B element on a non-leaf substrate (single-color trellis, cane lattice, geometric,
+mural) is PERMITTED because Part A is not fully satisfied.
+Use the defendant-favorable reading. If genuinely borderline, return "BORDERLINE".
+
+Reply with ONLY a compact JSON object, no prose, no markdown fence:
+{"a1":bool,"a2":bool,"a3":bool,"partA":bool,"partB":bool,"verdict":"PROHIBITED|PERMITTED|BORDERLINE","reason":"<=25 words"}`;
+
+const sleep = ms => new Promise(r => setTimeout(r, ms));
+const readLines = f => { try { return fs.readFileSync(f, 'utf8').split('\n').filter(Boolean); } catch { return []; } };
+
+async function shopify(path) {
+ for (let a = 0; a < 5; a++) {
+ const r = await fetch(`https://${SHOP}/admin/api/2024-01/${path}`,
+ { headers: { 'X-Shopify-Access-Token': STOKEN } });
+ if (r.status === 429) { await sleep(2000); continue; }
+ if (!r.ok) return null;
+ return r.json();
+ }
+ return null;
+}
+
+async function geminiVerify(imgB64, mime, ctx) {
+ const body = {
+ contents: [{ parts: [
+ { text: GATE + '\n\nProduct title: ' + ctx },
+ { inline_data: { mime_type: mime, data: imgB64 } },
+ ] }],
+ generationConfig: { temperature: 0, maxOutputTokens: 300 },
+ };
+ for (let a = 0; a < 4; a++) {
+ const r = await fetch(
+ 'https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash:generateContent?key=' + GKEY,
+ { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) });
+ if (r.status === 429 || r.status >= 500) { await sleep(3000); continue; }
+ const j = await r.json();
+ const txt = j?.candidates?.[0]?.content?.parts?.[0]?.text || '';
+ const m = txt.match(/\{[\s\S]*\}/);
+ if (m) { try { return JSON.parse(m[0]); } catch {} }
+ return { verdict: 'ERROR', reason: 'unparseable: ' + txt.slice(0, 80) };
+ }
+ return { verdict: 'ERROR', reason: 'gemini unavailable' };
+}
+
+(async () => {
+ const cands = readLines(CAND).map(l => JSON.parse(l));
+ const done = new Set(readLines(VERD).map(l => { try { return JSON.parse(l).id; } catch { return null; } }));
+ const todo = cands.filter(c => !done.has(c.id));
+ let n = 0, prohibited = 0, borderline = 0, err = 0;
+ const out = fs.createWriteStream(VERD, { flags: 'a' });
+
+ for (const c of todo) {
+ n++;
+ let rec = { id: c.id, handle: c.handle, title: c.title, status: c.status,
+ published: c.published, match: c.match };
+ try {
+ const imgs = await shopify(`products/${c.id}/images.json`);
+ const src = imgs?.images?.[0]?.src;
+ if (!src) { rec.verdict = 'NO_IMAGE'; rec.reason = 'product has no image'; }
+ else {
+ const ir = await fetch(src);
+ const buf = Buffer.from(await ir.arrayBuffer());
+ const mime = ir.headers.get('content-type') || 'image/jpeg';
+ const v = await geminiVerify(buf.toString('base64'), mime.split(';')[0], c.title || '');
+ rec = Object.assign(rec, v, { imageUrl: src });
+ }
+ } catch (e) { rec.verdict = 'ERROR'; rec.reason = e.message; }
+ if (rec.verdict === 'PROHIBITED') prohibited++;
+ if (rec.verdict === 'BORDERLINE') borderline++;
+ if (rec.verdict === 'ERROR') err++;
+ out.write(JSON.stringify(rec) + '\n');
+ if (n % 10 === 0 || n === todo.length) {
+ fs.writeFileSync(PROG, `verified ${n}/${todo.length} (of ${cands.length} total) | ` +
+ `PROHIBITED ${prohibited} | BORDERLINE ${borderline} | ERROR ${err} | ${new Date().toISOString()}\n`);
+ }
+ await sleep(700); // pace Shopify + Gemini
+ }
+ out.end();
+ fs.writeFileSync(PROG, `DONE | verified ${n}/${todo.length} | PROHIBITED ${prohibited} | ` +
+ `BORDERLINE ${borderline} | ERROR ${err} | ${new Date().toISOString()}\n`);
+ console.log(`stage 2 done — ${n} verified, ${prohibited} prohibited, ${borderline} borderline, ${err} errors`);
+})();
← 2fca7eb initial scaffold — DW settlement catalog audit, stage 1 text
·
back to Dw Settlement Audit
·
stage 2: disable Gemini thinking budget — was truncating ver a3e513d →