diff --git a/papers/FIGURE-TABLE-QA-CHECK.md b/papers/FIGURE-TABLE-QA-CHECK.md new file mode 100644 index 0000000000..6ad7e1aeff --- /dev/null +++ b/papers/FIGURE-TABLE-QA-CHECK.md @@ -0,0 +1,74 @@ +# Figure-Table-QA-Check + +`Figure-Table-QA-Check` is the visual acceptance gate for every paper in the +Aesthetic Computer paper stack. A successful TeX build, a clean log, and a +whole-document thumbnail are necessary but do not constitute a pass. + +## What counts + +Review every visual surface: + +- generated illustrations, photographs, screenshots, plots, and diagrams; +- tables, matrices, scorecards, and comparison grids; +- code, query, telemetry, and reproduction cards; +- framed callouts or other embedded raster/vector panels. + +An embedding counts as a figure for this process even when the source does not +use a LaTeX `figure` environment. + +## Required loop + +1. Build the paper with `paper_build`. +2. Run `paper_figure_table_qa_check` without `page` for an all-page overview. +3. Use OpenAI visual inference to inspect the overview for hierarchy, placement, + float order, density, and unfinished pages. +4. Run the check with `page: N` for every page containing an inventoried figure, + table, or embedded card. Inspect each at full resolution. +5. Reject and revise every visual failure. Rebuild and repeat the overview and + detail checks until the entire paper passes. +6. Open the accepted PDF with `paper_open`. + +The QA tool deliberately reports `VISUAL INFERENCE REQUIRED`; it never +self-certifies a pass. + +## Acceptance rubric + +### Intent and placement + +- A requested lead illustration actually leads the paper. +- Visuals appear near the claims they support, in narrative order. +- Captions and figure/table numbers match references in the prose. +- Floats do not strand headings, reorder evidence, or create an unfinished page. + +### Reading size and typography + +- All text is legible at normal PDF reading size without exceptional zoom. +- Body, captions, legends, axes, diagram labels, code, and table values have a + deliberate hierarchy. +- Data-bearing text must not be smaller than the paper's footnote size. +- `\scriptsize` and `\tiny` are not acceptable for figures, tables, or cards. + +### Tables + +- Use intentional column widths and eliminate avoidable line wrapping. +- Use clean outlined cells and restrained semantic color coding. +- Align numeric values and comparable units consistently. +- Keep headers, rules, padding, and caption placement consistent. +- Reject clipping, broken rules, crowded cells, ambiguous colors, and decorative + fills that do not encode meaning. + +### Figures, diagrams, and embedded cards + +- Flow direction and hierarchy are immediately unambiguous. +- Labels do not collide with nodes, arrows, art, borders, or watermarks. +- Contrast survives print and normal-size screen reading. +- Code/query cards preserve readable type, indentation, and line length. +- Generated imagery is inspected for both content fidelity and page composition. + +## House style + +Paper tables should default to readable full-width layouts when a single column +would force ugly wrapping. Prefer a dark semantic header, lightly tinted row or +status cells, thin colored outlines, aligned values, and concise cell copy. Color +must explain categories, time windows, status, risk, or recommendation—not merely +decorate the grid. diff --git a/slab/bin/paper-mcp.mjs b/slab/bin/paper-mcp.mjs index b71e0c89b6..6b033125d6 100644 --- a/slab/bin/paper-mcp.mjs +++ b/slab/bin/paper-mcp.mjs @@ -8,9 +8,9 @@ // paths, and XeLaTeX runs without shell escape. import { execFile } from "node:child_process"; -import { access, readFile, readdir, realpath, stat } from "node:fs/promises"; +import { access, mkdtemp, readFile, readdir, realpath, stat, writeFile } from "node:fs/promises"; import { constants as fsConstants } from "node:fs"; -import { homedir } from "node:os"; +import { homedir, tmpdir } from "node:os"; import { basename, dirname, extname, join, relative, resolve, sep } from "node:path"; import { fileURLToPath } from "node:url"; import { promisify } from "node:util"; @@ -294,6 +294,47 @@ async function texEngine() { throw new Error("no TeX engine found (install XeLaTeX/Tectonic or set XELATEX/TECTONIC)."); } +async function firstExecutable(candidates, label) { + for (const path of candidates.filter(Boolean)) if (await exists(path)) return path; + throw new Error(`${label} is unavailable (${candidates.filter(Boolean).join(", ")}).`); +} + +function visualInventory(source) { + if (!source || extname(source.path || "").toLowerCase() !== ".tex") { + return { figures: [], tables: [], embeds: [] }; + } + const lines = String(source.text || "").split("\n"); + const figures = []; + const tables = []; + const embeds = []; + let figureDepth = 0; + let tableDepth = 0; + for (let i = 0; i < lines.length; i++) { + const line = lines[i]; + const lineNo = i + 1; + if (/\\begin\{figure\*?\}/.test(line)) { + figureDepth += 1; + figures.push({ line: lineNo, kind: "figure environment" }); + } + if (/\\begin\{table\*?\}/.test(line)) { + tableDepth += 1; + tables.push({ line: lineNo, kind: "table environment" }); + } + if (/\\captionof\{figure\}/.test(line)) figures.push({ line: lineNo, kind: "captioned embedded figure" }); + if (/\\captionof\{table\}/.test(line)) tables.push({ line: lineNo, kind: "captioned embedded table" }); + if (!figureDepth && /\\includegraphics(?:\[[^\]]*\])?\{/.test(line)) { + const hasNearbyCaption = lines.slice(i, i + 8).some((candidate) => /\\captionof\{figure\}/.test(candidate)); + if (!hasNearbyCaption) embeds.push({ line: lineNo, kind: "standalone image" }); + } + if (!figureDepth && /\\begin\{(?:tikzpicture|lstlisting)\}/.test(line)) { + embeds.push({ line: lineNo, kind: /tikzpicture/.test(line) ? "standalone diagram" : "standalone code card" }); + } + if (/\\end\{figure\*?\}/.test(line)) figureDepth = Math.max(0, figureDepth - 1); + if (/\\end\{table\*?\}/.test(line)) tableDepth = Math.max(0, tableDepth - 1); + } + return { figures, tables, embeds }; +} + async function buildRecord(rec, passes = 2) { if (!rec.sourcePath || extname(rec.sourcePath).toLowerCase() !== ".tex") { throw new Error(`${rec.id} has no TeX source.`); @@ -369,6 +410,141 @@ async function toolOpen({ paper, build = false } = {}) { return [{ type: "text", text: `opened ${rec.title} [${rec.id}]\n${pdfPath}` }]; } +async function toolFigureTableQaCheck({ paper, build = false, dpi = 180, page } = {}) { + const rec = await resolvePaper(paper); + let pdfPath = rec.pdfPath; + if ((!pdfPath || !(await exists(pdfPath))) && build) pdfPath = (await buildRecord(rec, 2)).pdfPath; + if (!pdfPath || !(await exists(pdfPath))) throw new Error(`${rec.id} has no PDF. Build it before Figure-Table-QA-Check.`); + + const pdftoppm = await firstExecutable([ + process.env.PDFTOPPM, + "/opt/homebrew/bin/pdftoppm", + "/usr/local/bin/pdftoppm", + "/usr/bin/pdftoppm", + ], "pdftoppm"); + const pdfinfo = await firstExecutable([ + process.env.PDFINFO, + "/opt/homebrew/bin/pdfinfo", + "/usr/local/bin/pdfinfo", + "/usr/bin/pdfinfo", + ], "pdfinfo"); + const montage = await firstExecutable([ + process.env.MONTAGE, + "/opt/homebrew/bin/montage", + "/usr/local/bin/montage", + "/usr/bin/montage", + ], "ImageMagick montage"); + + const requestedDpi = Math.max(120, Math.min(300, Number(dpi) || 180)); + const qaDir = await mkdtemp(join(tmpdir(), "figure-table-qa-")); + const prefix = join(qaDir, "page"); + const info = await pexec(pdfinfo, [pdfPath], { timeout: 15_000, maxBuffer: 2 * 1024 * 1024 }); + const pageCount = Number(String(info.stdout || "").match(/^Pages:\s+(\d+)/m)?.[1] || 0); + await pexec(pdftoppm, ["-png", "-r", String(requestedDpi), pdfPath, prefix], { + timeout: 180_000, + maxBuffer: 4 * 1024 * 1024, + }); + const pageFiles = (await readdir(qaDir)) + .filter((name) => /^page-\d+\.png$/.test(name)) + .sort((a, b) => Number(a.match(/\d+/)?.[0]) - Number(b.match(/\d+/)?.[0])) + .map((name) => join(qaDir, name)); + if (!pageFiles.length) throw new Error(`Figure-Table-QA-Check could not rasterize ${pdfPath}.`); + + const sourceText = rec.sourcePath && await exists(rec.sourcePath) + ? await readFile(rec.sourcePath, "utf8") + : ""; + const inventory = visualInventory({ path: rec.sourcePath, text: sourceText }); + const inventoryLines = [ + ...inventory.figures.map((item, i) => `- Figure ${i + 1}: ${item.kind}, source line ${item.line}`), + ...inventory.tables.map((item, i) => `- Table ${i + 1}: ${item.kind}, source line ${item.line}`), + ...inventory.embeds.map((item, i) => `- Embedded visual ${i + 1}: ${item.kind}, source line ${item.line}`), + ]; + const rubric = [ + "Intent and placement: the visual appears where the narrative requires it; lead art actually leads.", + "Reading size: body, labels, captions, code, and table text remain legible at normal PDF size; no data display below footnote size.", + "Geometry: no clipping, collision, accidental wrap, broken rule, orphaned caption, or misleading whitespace.", + "Tables: intentional widths; aligned values; semantic color; clean outlined cells; consistent headers; no avoidable line wrapping.", + "Figures and embeddings: unambiguous hierarchy and flow; sufficient contrast; caption and numbering match the prose.", + "Page composition: floats do not strand headings, reorder evidence, or create visibly unfinished pages.", + ]; + const manifestPath = join(qaDir, "Figure-Table-QA-Check.md"); + const manifest = [ + `# Figure-Table-QA-Check: ${rec.title}`, + "", + `- Paper: ${pdfPath}`, + `- Pages: ${pageCount || pageFiles.length}`, + `- Raster: ${requestedDpi} DPI`, + `- Status: VISUAL INFERENCE REQUIRED`, + "", + "## Inventory", + "", + ...(inventoryLines.length ? inventoryLines : ["- No source inventory available; inspect every rendered page."]), + "", + "## OpenAI visual-inference rubric", + "", + ...rubric.map((item) => `- ${item}`), + "", + "A successful TeX build is not a pass. Inspect the overview and every page containing an inventoried item at full resolution, revise every failure, rebuild, and rerun this check.", + ].join("\n"); + await writeFile(manifestPath, `${manifest}\n`, "utf8"); + + let imagePath; + let imageLabel; + if (page != null) { + const selected = Math.trunc(Number(page)); + if (!Number.isFinite(selected) || selected < 1 || selected > pageFiles.length) { + throw new Error(`page must be between 1 and ${pageFiles.length}.`); + } + imagePath = pageFiles[selected - 1]; + imageLabel = `full-resolution page ${selected}`; + } else { + imagePath = join(qaDir, "overview.png"); + const fontCandidates = [ + "/System/Library/Fonts/Supplemental/Verdana.ttf", + "/System/Library/Fonts/Helvetica.ttc", + "/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf", + "/usr/share/fonts/dejavu/DejaVuSans.ttf", + ]; + let montageFont = null; + for (const candidate of fontCandidates) { + if (await exists(candidate)) { montageFont = candidate; break; } + } + const montageArgs = [ + ...(montageFont ? ["-font", montageFont] : []), + "-label", "", + "-background", "white", + ...pageFiles, + "-thumbnail", "420x544", + "-tile", "3x", + "-geometry", "+12+12", + imagePath, + ]; + try { + await pexec(montage, montageArgs, { timeout: 120_000, maxBuffer: 4 * 1024 * 1024 }); + } catch (error) { + // Some ImageMagick builds return a font warning after successfully + // writing the montage. Accept the artifact only if it really exists. + if (!(await exists(imagePath))) throw error; + } + imageLabel = "all-page overview"; + } + const imageData = await readFile(imagePath); + const textResult = [ + `Figure-Table-QA-Check prepared for ${rec.title} [${rec.id}]`, + `STATUS: VISUAL INFERENCE REQUIRED — this renderer does not self-certify a pass.`, + `Review image: ${imageLabel}`, + `Bundle: ${qaDir}`, + `Manifest: ${manifestPath}`, + `Pages: ${pageFiles.join(", ")}`, + "", + manifest, + ].join("\n"); + return [ + { type: "text", text: textResult }, + { type: "image", data: imageData.toString("base64"), mimeType: "image/png", _meta: { "codex/imageDetail": "original" } }, + ]; +} + const TOOLS = [ { name: "paper_list", @@ -403,7 +579,7 @@ const TOOLS = [ }, { name: "paper_build", - description: "Build a resolved .tex paper in place with XeLaTeX (no shell escape), normally two passes. SIDE EFFECT: writes the PDF and normal TeX auxiliary files beside the source.", + description: "Build a resolved .tex paper in place with XeLaTeX (no shell escape), normally two passes. A successful build is not visual acceptance: call paper_figure_table_qa_check afterward. SIDE EFFECT: writes the PDF and normal TeX auxiliary files beside the source.", inputSchema: { type: "object", properties: { @@ -414,6 +590,20 @@ const TOOLS = [ required: ["paper"], }, }, + { + name: "paper_figure_table_qa_check", + description: "Run Figure-Table-QA-Check: rasterize a paper, inventory figures/tables/embedded visual cards, and return an overview or full-resolution page for OpenAI visual inference. This tool never self-certifies a pass; the model must inspect every inventoried item, iterate failures, and rerun the check.", + inputSchema: { + type: "object", + properties: { + paper: { type: "string" }, + build: { type: "boolean", default: false, description: "Build missing TeX output first." }, + dpi: { type: "integer", minimum: 120, maximum: 300, default: 180 }, + page: { type: "integer", minimum: 1, description: "Optional one-based page number for full-resolution visual inference; omit for the all-page overview." }, + }, + required: ["paper"], + }, + }, { name: "paper_open", description: "Open a resolved paper PDF in Preview on macOS (or the available native viewer elsewhere). Pass build:true to build missing TeX output first. SIDE EFFECT: opens a local viewer window.", @@ -434,6 +624,7 @@ async function callTool(name, args) { case "paper_find": return toolFind(args || {}); case "paper_read": return toolRead(args || {}); case "paper_build": return toolBuild(args || {}); + case "paper_figure_table_qa_check": return toolFigureTableQaCheck(args || {}); case "paper_open": return toolOpen(args || {}); default: throw new Error(`Unknown tool: ${name}`); } @@ -472,4 +663,4 @@ async function handleMessage(message) { const port = httpPort(process.argv, 7777); if (port) serveHttp({ handleMessage, port, banner: "📄 paper-mcp shared daemon" }); -else serveStdio({ handleMessage, banner: "📄 paper-mcp started (paper_list, paper_find, paper_read, paper_build, paper_open)" }); +else serveStdio({ handleMessage, banner: "📄 paper-mcp started (paper_list, paper_find, paper_read, paper_build, paper_figure_table_qa_check, paper_open)" }); -- 2.51.2 From e3986f224f92400a069128050746cb584d83b823 Mon Sep 17 00:00:00 2001 From: "prompt.ac/@jeffrey" Date: Wed, 22 Jul 2026 13:46:12 -0700 Subject: [PATCH 2/2] Add Fuser internal paper style --- papers/FIGURE-TABLE-QA-CHECK.md | 5 +++ papers/FUSER-PAPER-STYLE.md | 64 +++++++++++++++++++++++++++++++++ 2 files changed, 69 insertions(+) create mode 100644 papers/FUSER-PAPER-STYLE.md diff --git a/papers/FIGURE-TABLE-QA-CHECK.md b/papers/FIGURE-TABLE-QA-CHECK.md index 6ad7e1aeff..0e0d44f733 100644 --- a/papers/FIGURE-TABLE-QA-CHECK.md +++ b/papers/FIGURE-TABLE-QA-CHECK.md @@ -72,3 +72,8 @@ would force ugly wrapping. Prefer a dark semantic header, lightly tinted row or status cells, thin colored outlines, aligned values, and concise cell copy. Color must explain categories, time windows, status, risk, or recommendation—not merely decorate the grid. + +For Fuser-facing internal documents, also apply +[`FUSER-PAPER-STYLE.md`](FUSER-PAPER-STYLE.md). Its exact-logo, cover-image, +palette, diagram-flow, and brand-fidelity checks are part of visual acceptance, +not optional decoration. diff --git a/papers/FUSER-PAPER-STYLE.md b/papers/FUSER-PAPER-STYLE.md new file mode 100644 index 0000000000..3363160f08 --- /dev/null +++ b/papers/FUSER-PAPER-STYLE.md @@ -0,0 +1,64 @@ +# Fuser Internal Paper Style + +Use this profile for Fuser-facing papers in the shared paper stack. It extends +`Figure-Table-QA-Check`; it does not replace that acceptance gate. + +## Identity source + +- Use the production Fuser lockup from + `/Users/jas/Developer/fuser/apps/app/public/fuser-thumbnail-logo.svg`. +- Use the icon path from that same SVG when a mark-only treatment is needed. + `captutor/bin/captutor-wallpaper.swift` is the reference implementation: it + extracts the production path verbatim and never invents a replacement mark. +- Never ask an image model to typeset or reconstruct the Fuser wordmark. For a + generated cover, pass the production logo as an image input for visual + conditioning, then layer the exact SVG or a deterministic rasterization into + the document. + +## Cover + +- The lead illustration is Figure 1 and visibly leads page 1. +- Prefer one strong cover field over a small decorative image below a title. +- Keep a calm region for title typography and an exact Fuser lockup. +- Use the Captutor stage as a tonal reference: near-black or off-white ground, + sparse modular marks, restrained motion/connection language, and no generic + cloud, robot, or fantasy-server imagery. +- Generated covers must be checked both as standalone images and in the final + page crop. A good source image can still fail as a cover. + +## Color and type + +- Base: near-black `#171717`; paper white `#FAFAFA`. +- Brand energy: violet around `#662DD6`, electric blue around `#4259FF`, and + cyan/mint around `#00ADBB` / `#009970`. +- Reserve magenta/red for pressure, failure, or risk; reserve amber for warning. +- Use semantic color in tables. The same color means the same category or + verdict throughout one paper. +- Headers and section labels may use a clean sans face. Long-form body copy must + remain quiet and highly readable. No data-bearing text below footnote size. + +## Tables and figures + +- Default to thin violet outlines, a dark violet header, light semantic row + tints, aligned values, and concise cell copy. +- Avoid decorative fills, avoidable wrapping, cramped columns, and ambiguous + legends. +- Architecture diagrams should choose a direction that survives the final + column width. If a wide float would separate a diagram from its heading or + reorder the argument, redesign it as an in-flow vertical figure. +- Telemetry, code, query, and reproduction cards count as figures and receive + the same font-size and visual-inference checks. + +## Acceptance + +Run `paper_figure_table_qa_check` after the final build. Inspect the all-page +overview, then every page containing a figure, table, diagram, or embedded card +at full resolution. The check must explicitly verify: + +- exact-logo fidelity and cover crop; +- title/logo contrast and minimum reading size; +- figure and table order relative to the prose; +- all table wrapping, cell rules, semantic colors, and numeric alignment; +- diagram flow, arrow direction, label collision, and caption order; +- code/query-card type at normal PDF reading size. +