← back to Secrets Manager

coordonne-collab-audit/settlement-gate.js

72 lines

#!/usr/bin/env node
// Settlement post-gen-vision gate for the 54 Coco Dávez Colortherapills images.
// Runs the 5 detectors as ONE structured Gemini 2.0-flash vision call per image,
// then applies the settlement-verdict combinator (BLOCK iff A1&A2&A3&B, else OK;
// any "unknown" fails closed to BLOCK).
// Gemini vision only (Anthropic vision API is banned, per CLAUDE.md 2026-06-10).
const fs = require('fs');
const path = require('path');
const { execSync } = require('child_process');

const ENV = fs.readFileSync(path.join(__dirname, '..', '.env'), 'utf8');
const GKEY = (ENV.match(/^GEMINI_API_KEY=(.+)$/m) || [])[1];
if (!GKEY) { console.error('no GEMINI_API_KEY'); process.exit(1); }

const rows = JSON.parse(fs.readFileSync(path.join(__dirname, process.env.INFILE || 'cotswolds-rows.json'), 'utf8'));

const PROMPT = `You are a strict legal image-compliance detector for a wallpaper settlement gate. Look ONLY at the pixels in this image. Answer these 5 questions about the artwork as a JSON object with boolean values (use "unknown" only if the image is unreadable):
- a1_directional_leaves: Does the design contain repeating leaves / palm fronds / similar FOLIAGE with directional variation (leaves pointing in more than one orientation across the repeat)? true/false
- a2_open_space: Is there visible open negative space BETWEEN foliage/leaf motifs (as opposed to edge-to-edge coverage, OR the design simply has no foliage)? Answer true ONLY if there IS foliage AND there is open space between the leaves; if there is no foliage at all, answer false.
- a3_multiple_colors: Does the LEAF/FOLIAGE layer specifically use more than one ink color? If there is no foliage, answer false.
- b_prohibited: Does the artwork depict any of these SPECIFIC elements: a banana, banana pod, grapes, a BIRD, or a BUTTERFLY? true/false
- acceptable: Does it contain tree trunks, clearly-represented branches, or a fruit/animal element OTHER than banana/grape/bird/butterfly? true/false
Respond with ONLY the JSON object, no prose. Example: {"a1_directional_leaves":false,"a2_open_space":false,"a3_multiple_colors":false,"b_prohibited":false,"acceptable":false}`;

function verdict(d) {
  const vals = [d.a1_directional_leaves, d.a2_open_space, d.a3_multiple_colors, d.b_prohibited];
  if (vals.some(v => v === 'unknown' || v === null || v === undefined)) return { v: 'BLOCK', reason: 'unknown detector input — fail closed' };
  const partA = d.a1_directional_leaves === true && d.a2_open_space === true && d.a3_multiple_colors === true;
  if (partA && d.b_prohibited === true) return { v: 'BLOCK', reason: 'Part A fully satisfied AND Part B present' };
  return { v: 'OK', reason: partA ? 'Part A only, no prohibited element' : 'Part A not fully satisfied' };
}

async function gemini(imgB64, mime) {
  const url = `https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash:generateContent?key=${GKEY}`;
  const body = {
    contents: [{ parts: [{ text: PROMPT }, { inline_data: { mime_type: mime, data: imgB64 } }] }],
    generationConfig: { temperature: 0, response_mime_type: 'application/json' },
  };
  const r = await fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) });
  const j = await r.json();
  const txt = j?.candidates?.[0]?.content?.parts?.[0]?.text;
  if (!txt) throw new Error('no gemini text: ' + JSON.stringify(j).slice(0, 300));
  return JSON.parse(txt);
}

(async () => {
  const out = [];
  let calls = 0;
  for (const row of rows) {
    let det, verd, err = null;
    try {
      // fetch image bytes
      const ir = await fetch(row.image_url);
      if (!ir.ok) throw new Error('img fetch ' + ir.status);
      const buf = Buffer.from(await ir.arrayBuffer());
      const mime = ir.headers.get('content-type') || 'image/jpeg';
      det = await gemini(buf.toString('base64'), mime);
      calls++;
      verd = verdict(det);
    } catch (e) {
      det = null; verd = { v: 'BLOCK', reason: 'error: ' + e.message }; err = e.message;
    }
    out.push({ dw_sku: row.dw_sku, pattern: row.pattern_name, verdict: verd.v, reason: verd.reason, detectors: det, error: err });
    process.stderr.write(`${row.dw_sku} ${row.pattern_name} -> ${verd.v}\n`);
    await new Promise(r => setTimeout(r, 300));
  }
  const blocks = out.filter(o => o.verdict === 'BLOCK');
  const summary = { total: out.length, ok: out.filter(o => o.verdict === 'OK').length, block: blocks.length, gemini_calls: calls, results: out };
  fs.writeFileSync(path.join(__dirname, process.env.OUTFILE || 'cotswolds-settlement-result.json'), JSON.stringify(summary, null, 2));
  console.log(JSON.stringify({ total: summary.total, ok: summary.ok, block: summary.block, gemini_calls: calls, blocked: blocks.map(b => ({ sku: b.dw_sku, pattern: b.pattern, reason: b.reason })) }, null, 2));
})();