← back to Marketing Command Center
lib/settlement-gate.js
160 lines
'use strict';
// Server-side Settlement gate for the #vendors "Make it ours" creative system.
//
// This is a faithful JS port of the CANONICAL image-side settlement checker
// ~/Projects/Designer-Wallcoverings/DW-Programming/ImportNewSkufromURL/lib/settlement-checker.ts
// (checkSettlementCompliance) — the reference implementation the
// `settlement-post-gen-vision` skill defers to. We do NOT reinvent the rule;
// we run the SAME post-gen VISION check the skill prescribes, so any
// AI-generated OR composited DW-original asset is verified against the executed
// Settlement Agreement (Part A all-three AND Part B, no acceptable carve-out)
// BEFORE it can be staged/shown as final.
//
// MODEL RE-ROUTE (2026-08-25, TK-10845): the vision call now runs on a LOCAL,
// $0 vision model (ollama qwen2.5vl:7b on 127.0.0.1:11434) instead of a paid
// API. History: it was Gemini, then re-routed to Claude vision per Steve's
// "use other model unless for vision" — but BOTH paid keys (Gemini image AND
// the box's ANTHROPIC_API_KEY) turned out to be out of credits, which blocked
// this legal gate (so even the $0 branded card couldn't SAVE) and the room
// render, and was also killing the sub-agents. Steve's standing directive is
// $0 / no more paying, so the genuine vision task now runs on the local model
// Steve already hosts — zero credits, zero rate-limits. Overridable via env
// (SETTLEMENT_VISION_URL / SETTLEMENT_VISION_MODEL) so Steve can repoint it at
// a funded frontier vision API later for maximum legal-gate accuracy.
//
// HARD RULES honored from settlement-post-gen-vision/SKILL.md (UNCHANGED — only
// the model TRANSPORT changed; the prompt text, combination logic, return shape,
// and fail-CLOSED behavior are IDENTICAL to the prior Gemini/Claude ports):
// - Vision is GROUND TRUTH (text anti-prompts don't bind a generator).
// - FAIL-SAFE / fail-CLOSED: any fetch/parse error or uncertainty returns
// NEEDS_REVIEW, never an auto-pass. This is a LEGAL compliance gate.
// - Verdict vocabulary is BLOCK / NEEDS_REVIEW / OK.
//
// COST: $0 (local ollama vision). A local 7B vision model is weaker than a
// frontier model at nuanced motif detection, but the gate stays fail-CLOSED
// (uncertainty -> NEEDS_REVIEW), so the conservative bias is preserved. A plain
// branded-card composite (Treatment 1) is not AI-generated, but we still
// post-gen-vision-check it per the task spec ("run it past settlement post-gen
// vision to be safe").
// Local vision endpoint (ollama /api/generate) + model. Both env-overridable so
// the gate can be repointed at Mac1's ollama or a funded frontier API later.
const VISION_URL = process.env.SETTLEMENT_VISION_URL || 'http://127.0.0.1:11434/api/generate';
const VISION_MODEL = process.env.SETTLEMENT_VISION_MODEL || 'qwen2.5vl:7b';
const VISION_COST = 0; // local, $0
// Back-compat aliases (some callers reference the old names for cost labels).
const CLAUDE_VISION_MODEL = VISION_MODEL;
const CLAUDE_VISION_COST = VISION_COST;
// Retained for back-compat export; no longer required by the local path.
const ANTHROPIC_KEY_NAMES = ['ANTHROPIC_API_KEY'];
function anthropicKey() {
for (const n of ANTHROPIC_KEY_NAMES) if (process.env[n]) return process.env[n];
try {
const fs = require('fs');
const os = require('os');
const path = require('path');
const p = path.join(os.homedir(), 'Projects', 'secrets-manager', '.env');
const s = fs.readFileSync(p, 'utf8');
for (const n of ANTHROPIC_KEY_NAMES) {
const m = s.match(new RegExp('^' + n + '=(.*)$', 'm'));
if (m) return m[1].replace(/^["']|["']$/g, '').trim();
}
} catch { /* ignore */ }
return '';
}
// Run the post-gen vision gate on a raw image buffer.
// @param {Buffer} buf the produced image bytes (branded card PNG, or room JPEG)
// @param {string} mime image mime (image/png | image/jpeg) [kept for signature compat]
// @param {string} title short label for the prompt (vendor/pattern name)
// @returns {Promise<{verdict:'OK'|'NEEDS_REVIEW'|'BLOCK', reason:string, cost:number, detail:object}>}
async function settlementGateBuffer(buf, mime, title) {
const failSafe = (why) => ({
verdict: 'NEEDS_REVIEW',
reason: `Vision check unavailable (${why}) — flagged for review rather than auto-passed.`,
cost: 0,
detail: { failSafe: true },
});
if (!Buffer.isBuffer(buf) || !buf.length) return failSafe('empty image buffer');
const b64 = buf.toString('base64');
// CANONICAL PROMPT TEXT — preserved VERBATIM across every re-route so the legal
// gate's semantics are byte-identical; only the transport (local model) changed.
const prompt = `You are performing a strict LEGAL compliance check on a wallcovering design image for "${String(title || '').slice(0, 120)}".
Answer ONLY with valid JSON, no markdown:
{
"a1_directional_leaves": <true if the design shows repeating patterns with DIRECTIONAL VARIATION amongst leaves, palm fronds, or similar foliage>,
"a2_open_space": <true if there is visible OPEN/NEGATIVE SPACE between the leaves (not edge-to-edge foliage coverage)>,
"a3_multiple_colors": <true if the leaf/foliage layer uses MORE THAN ONE ink color (ignore the background color)>,
"b_prohibited_elements": <true if the design contains ANY of: bananas, banana pods, grapes, birds, or butterflies>,
"b_elements_found": [<list which of banana/banana pod/grape/bird/butterfly appear, empty if none>],
"acceptable_carveouts": <true if the design clearly shows tree trunks, OR clearly represented branches, OR fruit/animal elements OTHER than bananas/grapes/birds/butterflies>,
"is_tropical_foliage_design": <true if this is a tropical/botanical foliage design at all>,
"visual_summary": "<one sentence describing what the design depicts>"
}
Be precise and conservative: only answer true when the visual evidence is clear.`;
let json;
try {
// ollama /api/generate with an image + forced JSON output. format:'json'
// makes the local model return a single JSON object we can parse reliably.
const res = await fetch(VISION_URL, {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({
model: VISION_MODEL,
prompt,
images: [b64],
stream: false,
format: 'json',
options: { temperature: 0 },
}),
});
json = await res.json();
} catch (e) {
return failSafe('local vision fetch ' + String((e && e.message) || e).slice(0, 100));
}
if (!json || json.error) {
return failSafe('local vision: ' + String((json && json.error) || 'no response').slice(0, 120));
}
// ollama /api/generate returns { response: "<the JSON text>" }.
const text = String(json.response || '');
let v;
try {
v = JSON.parse(text.replace(/```json\n?|```\n?/g, '').trim());
} catch {
return failSafe('unparseable vision verdict');
}
// Defendant-favorable combination (IDENTICAL to the canonical checker):
// VIOLATION requires FULL Part A (all three) AND Part B, no acceptable carve-out.
const partA = !!(v.a1_directional_leaves && v.a2_open_space && v.a3_multiple_colors);
const partB = !!v.b_prohibited_elements;
const carveout = !!v.acceptable_carveouts;
const violates = partA && partB && !carveout;
const needsReview = !violates && ((partA && !carveout) || (partB && !!v.is_tropical_foliage_design));
const reason =
`Local vision (${VISION_MODEL}): A1=${!!v.a1_directional_leaves}, A2=${!!v.a2_open_space}, A3=${!!v.a3_multiple_colors} ` +
`(Part A ${partA ? 'MET' : 'not met'}); Part B=${partB}` +
`${(v.b_elements_found || []).length ? ` [${(v.b_elements_found || []).join(', ')}]` : ''}; ` +
`carve-out=${carveout}. ${v.visual_summary || ''}`.trim();
const verdict = violates ? 'BLOCK' : (needsReview ? 'NEEDS_REVIEW' : 'OK');
return { verdict, reason, cost: VISION_COST, detail: { partA, partB, carveout, raw: v } };
}
module.exports = {
settlementGateBuffer,
anthropicKey,
VISION_MODEL,
VISION_COST,
VISION_URL,
// back-compat aliases
CLAUDE_VISION_MODEL,
CLAUDE_VISION_COST,
};