Files
gstack/design/src/check.ts
T
Garry Tan 52288947ec feat(design): receipted fetch for OpenAI calls
design/src/receipted-fetch.ts wraps every api.openai.com call: a
content-free egress receipt (sink design-openai, sha256 of the JSON
body — hash only, never the body) is written BEFORE the send. Polarity
is FAIL-OPEN: user-facing generation must not die because an audit log
hiccuped, so a receipt failure warns on stderr and the call proceeds.
Streams pass through untouched (response bodies returned as-is;
non-string request bodies receipted as sha256:null rather than drained
to hash).

All ten call sites converted with per-command payload classes:
generate, variants (injected fetchFn passes through), iterate (both
threaded and fresh paths), evolve (image + screenshot analysis), check,
diff, design-to-code, memory.

Unit-tested with injected fetch: receipt-before-send ordering, stream
passthrough, and fail-open on an unwritable ledger.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
(cherry picked from commit c0e5ff6639414ac2fd98e8ac3affb51401746b55)
2026-08-12 15:31:48 -07:00

98 lines
3.4 KiB
TypeScript

/**
* Vision-based quality gate for generated mockups.
* Uses GPT-4o vision to verify text readability, layout completeness, and visual coherence.
*/
import fs from "fs";
import { requireApiKey } from "./auth";
import { receiptedFetch } from "./receipted-fetch";
export interface CheckResult {
pass: boolean;
issues: string;
}
/**
* Check a generated mockup against the original brief.
*/
export async function checkMockup(imagePath: string, brief: string): Promise<CheckResult> {
const apiKey = requireApiKey();
const imageData = fs.readFileSync(imagePath).toString("base64");
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), 60_000);
try {
const response = await receiptedFetch("check-screenshot-request", "https://api.openai.com/v1/chat/completions", {
method: "POST",
headers: {
"Authorization": `Bearer ${apiKey}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
model: "gpt-4o",
messages: [{
role: "user",
content: [
{
type: "image_url",
image_url: { url: `data:image/png;base64,${imageData}` },
},
{
type: "text",
text: [
"You are a UI quality checker. Evaluate this mockup against the design brief.",
"",
`Brief: ${brief}`,
"",
"Check these 3 things:",
"1. TEXT READABILITY: Are all labels, headings, and body text legible? Any misspellings?",
"2. LAYOUT COMPLETENESS: Are all requested elements present? Anything missing?",
"3. VISUAL COHERENCE: Does it look like a real production UI, not AI art or a collage?",
"",
"Respond with exactly one line:",
"PASS — if all 3 checks pass",
"FAIL: [list specific issues] — if any check fails",
].join("\n"),
},
],
}],
max_tokens: 200,
}),
signal: controller.signal,
});
if (!response.ok) {
const error = await response.text();
if (response.status === 403 && error.includes("organization must be verified")) {
console.error("OpenAI organization verification required. Go to https://platform.openai.com/settings/organization to verify.");
return { pass: true, issues: "OpenAI org not verified — vision check skipped" };
}
// Non-blocking: if vision check fails, default to PASS with warning
console.error(`Vision check API error (${response.status}): ${error}`);
return { pass: true, issues: "Vision check unavailable — skipped" };
}
const data = await response.json() as any;
const content = data.choices?.[0]?.message?.content?.trim() || "";
if (content.startsWith("PASS")) {
return { pass: true, issues: "" };
}
// Extract issues after "FAIL:"
const issues = content.replace(/^FAIL:\s*/i, "").trim();
return { pass: false, issues: issues || content };
} finally {
clearTimeout(timeout);
}
}
/**
* Standalone check command: check an existing image against a brief.
*/
export async function checkCommand(imagePath: string, brief: string): Promise<void> {
const result = await checkMockup(imagePath, brief);
console.log(JSON.stringify(result, null, 2));
}