[object Object]

← back to Settlement Prescan

settlement pre-scan: /bin/zsh-local vision gate over fromental/gracie/zuber catalogs

b4e4776b273d95095ec059d59c307a74a85917dd · 2026-07-09 19:13:29 -0700 · Steve Abrams

Files touched

Diff

commit b4e4776b273d95095ec059d59c307a74a85917dd
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Thu Jul 9 19:13:29 2026 -0700

    settlement pre-scan: /bin/zsh-local vision gate over fromental/gracie/zuber catalogs
---
 .gitignore |   8 +++
 scan.mjs   | 184 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
 2 files changed, 192 insertions(+)

diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..1924158
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,8 @@
+node_modules/
+.env*
+tmp/
+*.log
+.DS_Store
+dist/
+build/
+.next/
diff --git a/scan.mjs b/scan.mjs
new file mode 100644
index 0000000..4580f8b
--- /dev/null
+++ b/scan.mjs
@@ -0,0 +1,184 @@
+#!/usr/bin/env node
+/**
+ * DW Settlement Gate — reversible PRE-SCAN over the 3 luxury wallcovering catalogs.
+ *
+ * Runs the settlement IMAGE-SIDE gate (settlement-post-gen-vision path) on each
+ * SKU's primary image using LOCAL qwen2.5vl vision on Mac1 Ollama ($0), then
+ * applies the settlement-verdict combinator logic and writes:
+ *   - <table>.settlement_verdict  (OK | NEEDS_REVIEW | BLOCK)
+ *   - <table>.settlement_reason   (one-line rationale + detector booleans)
+ *
+ * The five detectors mirror the SHA-locked settlement sub-skills:
+ *   A1 settlement-part-a1-directional-leaves : directional variation amongst leaves/fronds
+ *   A2 settlement-part-a2-open-space         : visible negative space between foliage motifs
+ *   A3 settlement-part-a3-multiple-colors    : >1 ink color in the leaf/foliage layer
+ *   B  settlement-part-b-prohibited-elements : bananas / banana pods / grapes / birds / butterflies
+ *   AC settlement-acceptable-elements        : tree trunks / branches / non-prohibited fruit-or-animal
+ *
+ * Verdict (settlement-verdict binding, defendant-favorable, fail-closed):
+ *   BLOCK        iff  A1 && A2 && A3 && B         (full Part A + Part B; Acceptable can't rescue when B present)
+ *   NEEDS_REVIEW iff  any detector is "unknown"   (fail-closed — pre-scan staging, human eyes required)
+ *                OR   image could not be fetched/analyzed
+ *   OK           otherwise (Part A not fully satisfied, or Part B absent)
+ *
+ * NOTE ON NEEDS_REVIEW vs the verdict table: the live settlement-verdict table
+ * collapses every "unknown" straight to BLOCK (hard publish gate). This is a
+ * REVERSIBLE staging PRE-SCAN, not a publish gate — so "unknown"/unreadable is
+ * surfaced as NEEDS_REVIEW (human review) rather than a hard BLOCK, matching the
+ * OK/NEEDS_REVIEW/BLOCK tri-state Steve asked for. The settlement-clean subset =
+ * OK only. No BLOCK/NEEDS_REVIEW row is ever eligible for publish from this scan.
+ *
+ * Zero npm deps: DB access via `psql` child_process (dw_unified is LOCAL).
+ */
+
+import { spawnSync } from 'node:child_process';
+import fs from 'node:fs';
+import path from 'node:path';
+
+const OLLAMA = process.env.OLLAMA_HOST || 'http://192.168.1.133:11434';
+const MODEL = process.env.SETTLEMENT_VLM || 'qwen2.5vl:7b';
+const DB = process.env.DATABASE_URL || 'postgresql://localhost/dw_unified';
+const TMP = path.join(process.cwd(), 'tmp');
+fs.mkdirSync(TMP, { recursive: true });
+
+const ALL_TABLES = ['fromental_catalog', 'gracie_catalog', 'zuber_catalog'];
+
+const args = process.argv.slice(2);
+const opt = (k, d) => { const i = args.indexOf(k); return i >= 0 ? args[i + 1] : d; };
+const LIMIT = parseInt(opt('--limit', '0'), 10) || 0;   // 0 = all NULL rows
+const ONLY = opt('--table', null);
+const tables = ONLY ? [ONLY] : ALL_TABLES;
+
+function log(...a) { console.log(new Date().toISOString(), ...a); }
+
+// ---- psql helpers ---------------------------------------------------------
+function psql(sql) {
+  const r = spawnSync('psql', [DB, '-t', '-A', '-F', '\t', '-c', sql],
+    { encoding: 'utf8', maxBuffer: 64 * 1024 * 1024 });
+  if (r.status !== 0) throw new Error('psql: ' + (r.stderr || r.stdout || 'unknown'));
+  return r.stdout;
+}
+function sqlLit(s) { return "'" + String(s).replace(/'/g, "''") + "'"; }
+
+function fetchRows(table) {
+  const out = psql(
+    `SELECT mfr_sku, image_url FROM ${table}
+     WHERE settlement_verdict IS NULL AND image_url IS NOT NULL AND image_url <> ''
+     ORDER BY mfr_sku ${LIMIT ? `LIMIT ${LIMIT}` : ''};`
+  );
+  return out.split('\n').filter(Boolean).map(line => {
+    const tab = line.indexOf('\t');
+    return { mfr_sku: line.slice(0, tab), image_url: line.slice(tab + 1) };
+  });
+}
+function writeVerdict(table, sku, v, reason) {
+  psql(`UPDATE ${table} SET settlement_verdict=${sqlLit(v)}, settlement_reason=${sqlLit(reason.slice(0, 1000))} WHERE mfr_sku=${sqlLit(sku)};`);
+}
+
+// ---- download image to base64 ---------------------------------------------
+async function fetchImage(url) {
+  try {
+    const res = await fetch(url, { signal: AbortSignal.timeout(45000) });
+    if (!res.ok) return null;
+    const buf = Buffer.from(await res.arrayBuffer());
+    if (buf.length < 500) return null;
+    return buf.toString('base64');
+  } catch { return null; }
+}
+
+// ---- one vision call to qwen2.5vl on Mac1 Ollama ($0 local) ---------------
+const VISION_PROMPT = `You are a legal image-compliance detector for a wallpaper Settlement Agreement. Look ONLY at what is visually present in this wallpaper/wallcovering image. Answer each of the five questions with exactly true, false, or unknown (use unknown ONLY if the image is unreadable/blank/too abstract to tell).
+
+A1 (directional_leaves): Does the design contain a REPEATING pattern of leaves, palm fronds, or similar foliage where the leaves point in MORE THAN ONE direction/orientation (directional variation)? true only if foliage is clearly present AND directionally varied.
+A2 (open_space): Is there visible OPEN NEGATIVE SPACE (background showing) BETWEEN the foliage/leaf motifs, rather than edge-to-edge dense coverage? Answer false if there is no foliage at all.
+A3 (multiple_colors): Does the LEAF/FOLIAGE layer itself use MORE THAN ONE ink color (excluding the background)? Answer false if there is no foliage at all.
+B (prohibited_elements): Does the image contain ANY of these specific elements: bananas, banana pods, grapes, birds, or butterflies? true if any are present.
+AC (acceptable_elements): Does the image contain tree trunks, clearly-represented branches, OR fruit/animal elements OTHER than bananas/grapes/birds/butterflies (e.g. flowers alone do NOT count; but a deer, a fish, a pomegranate, a monkey would count)?
+
+Respond with ONLY a compact JSON object, no prose:
+{"A1":true|false|"unknown","A2":true|false|"unknown","A3":true|false|"unknown","B":true|false|"unknown","AC":true|false|"unknown","note":"<=12 word visual summary"}`;
+
+async function runVision(b64) {
+  const body = {
+    model: MODEL, prompt: VISION_PROMPT, images: [b64],
+    stream: false, format: 'json', keep_alive: '30m', options: { temperature: 0 },
+  };
+  const res = await fetch(`${OLLAMA}/api/generate`, {
+    method: 'POST', headers: { 'Content-Type': 'application/json' },
+    body: JSON.stringify(body), signal: AbortSignal.timeout(180000),
+  });
+  if (!res.ok) throw new Error(`ollama ${res.status}`);
+  const j = await res.json();
+  let parsed;
+  try { parsed = JSON.parse(j.response); }
+  catch { throw new Error('bad-json:' + String(j.response).slice(0, 120)); }
+  return parsed;
+}
+
+function norm(v) {
+  if (v === true) return true;
+  if (v === false) return false;
+  const s = String(v).toLowerCase().trim();
+  if (s === 'true' || s === 'yes') return true;
+  if (s === 'false' || s === 'no') return false;
+  return 'unknown';
+}
+
+// ---- settlement-verdict combinator (tri-state staging variant) ------------
+function verdict(d) {
+  const { A1, A2, A3, B, AC } = d;
+  const anyUnknown = [A1, A2, A3, B, AC].some(x => x === 'unknown');
+  const partA = A1 === true && A2 === true && A3 === true;
+  const partB = B === true;
+  if (partA && partB) {
+    return { v: 'BLOCK', why: 'full settlement violation — Part A (directional leaves + open space + multiple colors) all satisfied AND Part B prohibited element present; Acceptable carve-out cannot apply when Part B is present' };
+  }
+  if (anyUnknown) {
+    return { v: 'NEEDS_REVIEW', why: 'fail-closed — one or more detectors returned unknown (unreadable/ambiguous); requires human review before any publish' };
+  }
+  if (partA && !partB) {
+    return { v: 'OK', why: 'Part A fully satisfied but Part B absent — no prohibited banana/grape/bird/butterfly element, permitted' };
+  }
+  return { v: 'OK', why: 'Part A not fully satisfied (foliage not directional/spaced/multicolor as required) — permitted regardless of other elements' };
+}
+
+async function scanTable(table) {
+  const rows = fetchRows(table);
+  log(`[${table}] ${rows.length} rows to scan${LIMIT ? ` (limit ${LIMIT})` : ''}`);
+  let ok = 0, review = 0, block = 0;
+  for (const row of rows) {
+    const sku = row.mfr_sku;
+    let res, reason;
+    const b64 = await fetchImage(row.image_url);
+    if (!b64) {
+      res = { v: 'NEEDS_REVIEW', why: 'image could not be fetched/decoded — human review required' };
+      reason = res.why;
+    } else {
+      try {
+        const raw = await runVision(b64);
+        const d = { A1: norm(raw.A1), A2: norm(raw.A2), A3: norm(raw.A3), B: norm(raw.B), AC: norm(raw.AC) };
+        res = verdict(d);
+        reason = `${res.why} [A1=${d.A1} A2=${d.A2} A3=${d.A3} B=${d.B} AC=${d.AC}]` +
+                 (raw.note ? ` | ${String(raw.note).slice(0, 80)}` : '');
+      } catch (e) {
+        res = { v: 'NEEDS_REVIEW', why: 'vision analysis failed: ' + String(e.message).slice(0, 80) };
+        reason = res.why;
+      }
+    }
+    writeVerdict(table, sku, res.v, reason);
+    if (res.v === 'OK') ok++; else if (res.v === 'NEEDS_REVIEW') review++; else block++;
+    const done = ok + review + block;
+    if (done % 20 === 0) log(`  [${table}] progress ${done}/${rows.length} ok=${ok} review=${review} block=${block}`);
+  }
+  log(`[${table}] DONE ok=${ok} needs_review=${review} block=${block}`);
+  return { table, ok, review, block, scanned: rows.length };
+}
+
+async function main() {
+  log(`settlement pre-scan start | model=${MODEL} @ ${OLLAMA} | tables=${tables.join(',')}`);
+  const summary = [];
+  for (const t of tables) summary.push(await scanTable(t));
+  log('SUMMARY ' + JSON.stringify(summary));
+}
+
+main().catch(e => { log('FATAL', e); process.exit(1); });

(oldest)  ·  back to Settlement Prescan  ·  num_ctx=8192 fix (context-overflow on large images) + detach ad49317 →