From d18b2dcb71ead3434523d7d242e0227eb6baf158 Mon Sep 17 00:00:00 2001 From: "prompt.ac/@jeffrey" Date: Wed, 9 Sep 2026 17:10:31 -0400 Subject: [PATCH] papers: gate evidence figure scale in Aesthetic Eye --- papers/AESTHETIC-EYE.md | 58 +++++++++++--- papers/FIGURE-TABLE-QA-CHECK.md | 5 +- papers/QUALITY-RUBRIC.md | 14 ++-- papers/SCORE.md | 4 +- papers/aesthetic-eye.mjs | 117 +++++++++++++++++++++++------ papers/test/aesthetic-eye.test.mjs | 64 ++++++++++++++++ slab/bin/paper-mcp.mjs | 8 +- 7 files changed, 222 insertions(+), 48 deletions(-) create mode 100644 papers/test/aesthetic-eye.test.mjs diff --git a/papers/AESTHETIC-EYE.md b/papers/AESTHETIC-EYE.md index 2198fc25b..3886ba28e 100644 --- a/papers/AESTHETIC-EYE.md +++ b/papers/AESTHETIC-EYE.md @@ -1,9 +1,10 @@ # Aesthetic Eye -`aesthetic-eye` is the render-first design gate for papers and their diagrams. A -successful TeX build is not visual approval. The final PDF must receive a -paper-wide brand verdict, and every diagram must receive the literal verdict -`design: pass` or `design: fail` in an `aesthetic-eye.json` beside the paper. +`aesthetic-eye` is the render-first design gate for papers and their visual +evidence. A successful TeX build is not visual approval. The final PDF must +receive a paper-wide brand verdict, and every evidence figure and diagram must +receive the literal verdict `design: pass` or `design: fail` in an +`aesthetic-eye.json` beside the paper. ## Brand rule @@ -63,6 +64,23 @@ These are visual judgments, not geometry-only lint. The agent must open and inspect the prepared crops. A manifest copied from an older render fails because it records the SHA-256 of the reviewed PDF. +## Evidence-figure checks + +Every photographic, screenshot, plotted, or generated figure receives five +checks. The checker inventories figure environments containing +`\includegraphics` in the matching TeX source, so omitting their manifest +records is a failure. + +- `scale` — the evidence is large enough to inspect at normal PDF size; +- `legibility` — essential marks, labels, and comparisons can be read without + zooming beyond normal reading size; +- `evidenceDominance` — the evidence, not its caption or surrounding prose, + owns the figure's visual area; +- `crop` — empty margins and irrelevant interface chrome do not miniaturize the + useful content; +- `captionFit` — the caption identifies what is shown and what it proves without + visually overpowering it. + ## Manifest Place `aesthetic-eye.json` beside the paper source: @@ -72,6 +90,7 @@ Place `aesthetic-eye.json` beside the paper source: "schema": 1, "paper": "Example Paper", "pdf": "example.pdf", + "expectedFigures": 1, "expectedDiagrams": 1, "visualInference": true, "pdfSha256": "sha256-of-reviewed-pdf", @@ -86,6 +105,21 @@ Place `aesthetic-eye.json` beside the paper source: "dotColor": "pass" } }, + "figures": [ + { + "id": "observed-result", + "page": 1, + "crop": [0.08, 0.22, 0.84, 0.32], + "design": "pass", + "checks": { + "scale": "pass", + "legibility": "pass", + "evidenceDominance": "pass", + "crop": "pass", + "captionFit": "pass" + } + } + ], "diagrams": [ { "id": "system-map", @@ -107,20 +141,22 @@ Place `aesthetic-eye.json` beside the paper source: ``` `crop` is `[x, y, width, height]` in normalized page coordinates, measured from -the upper-left. `expectedDiagrams` is the explicit inventory: every diagram in -the paper must appear once, including title illustrations that explain the -system rather than merely decorate it. +the upper-left. `expectedFigures` must match every TeX figure containing +`\includegraphics`; `expectedDiagrams` is the explicit diagram inventory, +including title illustrations that explain the system rather than merely +decorate it. ## Pass ```bash node papers/aesthetic-eye.mjs prepare papers/arxiv-example -# Open .aesthetic-eye/pages-contact.png, diagrams-contact.png, and every crop. +# Open pages-contact.png, figures-contact.png, diagrams-contact.png, and every crop. # Record the visual-inference verdicts and the printed pdfSha256. node papers/aesthetic-eye.mjs check papers/arxiv-example ``` The check fails when a visible brand name omits its period, the brand review is -absent, the dot-color check fails, a diagram is missing, a verdict is absent, a -diagram fails, or the PDF changed after review. `.aesthetic-eye/` is disposable -rendered evidence; the manifest is the durable review record. +absent, the dot-color check fails, a source evidence figure or diagram is +missing from the manifest, a verdict is absent or failed, or the PDF changed +after review. `.aesthetic-eye/` is disposable rendered evidence; the manifest +is the durable review record. diff --git a/papers/FIGURE-TABLE-QA-CHECK.md b/papers/FIGURE-TABLE-QA-CHECK.md index 9ebf00d44..1dba37e83 100644 --- a/papers/FIGURE-TABLE-QA-CHECK.md +++ b/papers/FIGURE-TABLE-QA-CHECK.md @@ -28,8 +28,9 @@ use a LaTeX `figure` environment. 5. Reject and revise every visual failure. Rebuild and repeat the overview and detail checks until the entire paper passes. 6. Run `paper_aesthetic_eye_prepare`, inspect its all-page contact sheet and - every declared diagram crop, and record the six diagram verdicts plus the - brand verdict in `aesthetic-eye.json` against the current PDF hash. + every declared evidence-figure and diagram crop, and record the figure, + diagram, and brand verdicts in `aesthetic-eye.json` against the current PDF + hash. 7. Run `paper_aesthetic_eye_check`. A missing, stale, malformed, or failing manifest is a hard failure and must be reported as such. 8. Open the accepted PDF with `paper_open` only after both rendered gates pass. diff --git a/papers/QUALITY-RUBRIC.md b/papers/QUALITY-RUBRIC.md index efe421005..a1fe5dc86 100644 --- a/papers/QUALITY-RUBRIC.md +++ b/papers/QUALITY-RUBRIC.md @@ -80,15 +80,17 @@ flagship paper is expected to show its argument, not just state it. - **D6** Screenshots are legitimate evidence: the coarse UI being criticized, the AC piece being praised, the user comment proving demand. Caption them like exhibits — say what the reader is looking at and why it is in the paper. -- **D7 [HARD] Aesthetic Eye.** Every diagram is inventoried in the paper's - `aesthetic-eye.json` and receives an explicit visual-inference verdict of - `design: pass|fail`. A pass requires clear tangents, accessible type, balanced - composition, intentional use of available space, legible hierarchy, and - unambiguous edge routing. The paper-wide brand verdict also requires every +- **D7 [HARD] Aesthetic Eye.** Every evidence figure and diagram is inventoried + in the paper's `aesthetic-eye.json` and receives an explicit visual-inference + verdict of `design: pass|fail`. Evidence figures must pass scale, legibility, + evidence dominance, crop, and caption-fit checks. Diagrams must pass clear + tangents, accessible type, balanced composition, intentional use of available + space, legible hierarchy, and unambiguous edge routing. The paper-wide brand + verdict also requires every visible wordmark to read `Aesthetic.Computer`, with its period in AC pink (`#B44887`). The manifest's PDF hash must match the built PDF. Run `node papers/aesthetic-eye.mjs prepare `, inspect the page contact - sheet and every crop, record the verdicts, then run + sheet and every evidence-figure and diagram crop, record the verdicts, then run `node papers/aesthetic-eye.mjs check `. ## E — Cover (`bin/gen-cover.mjs`) diff --git a/papers/SCORE.md b/papers/SCORE.md index 66a5a11c1..a7332ec58 100644 --- a/papers/SCORE.md +++ b/papers/SCORE.md @@ -26,7 +26,7 @@ Treating a mill request as mere PDF generation is the failure this contract exis Before outlining or designing it, consult the Platter: this score, the rendered public Platter index, relevant sub-platter manifests, prior AC papers and their bibliographies, and the code, data, images, or other primary evidence behind the subject. Record what was consulted in the source, notes, or bibliography so the result remains reproducible. The Platter supplies context and evidence; it is not permission to inflate the scope or silently publish private vault material. -An arXiv-lane default should include a title, `@jeffrey` byline and date, abstract, introduction/problem, related work or project context, system/method, implementation, evaluation or evidence, ethics/privacy/limitations, conclusion, references, and numbered, captioned figures/tables where they aid the argument. Source, bibliography, figures, and data provenance live beside the paper. Building is followed by Figure-Table-QA-Check, visual inspection, an `aesthetic-eye.json` tied to the current PDF hash, and a passing Aesthetic Eye check. Missing, stale, malformed, or failed Aesthetic Eye review is a hard failure; a compiling PDF alone is not finished. +An arXiv-lane default should include a title, `@jeffrey` byline and date, abstract, introduction/problem, related work or project context, system/method, implementation, evaluation or evidence, ethics/privacy/limitations, conclusion, references, and numbered, captioned figures/tables where they aid the argument. Source, bibliography, figures, and data provenance live beside the paper. Building is followed by Figure-Table-QA-Check, visual inspection, an `aesthetic-eye.json` tied to the current PDF hash, and a passing Aesthetic Eye check covering evidence scale as well as diagram craft. Missing, stale, malformed, or failed Aesthetic Eye review is a hard failure; a compiling PDF alone is not finished. Every new mill-built paper ends with one paperclip linking one embedded ZIP source bundle. The small disclaimer follows the attachment and names both its purpose and exclusions. `cli.mjs`, `papermill.mjs`, and `bin/build-dossier.mjs` create deterministic bundles before LaTeX runs; the shared layout, essay, and cards styles render the tail through `ac-source-bundle.sty`. By default the ZIP contains the primary `.tex`, matching Markdown, bibliography, local styles/classes, and a plain-text inventory. A paper may add specific text sources with `source-bundle.json` (`{"include":["notes.md"]}`), but paths must remain inside its directory. Never include private evidence, credentials, raw datasets, generated PDFs, or whole directories by implication. @@ -222,7 +222,7 @@ The mill's code lives at the top of `papers/` and in [`bin/`](bin/). All scripts | [`bin/crunch.mjs`](bin/crunch.mjs) | Build a paper on the **oven** instead of locally — for a machine with no TeX, or a document that must stay private. Tars the directory, POSTs it to `/paper-crunch`, streams the log, writes the PDF. Never commits and never publishes: the crunched PDF comes back to you and goes nowhere near `papers.aesthetic.computer` (that is `cli.mjs publish`, which builds from `main`). House `.sty` + webfonts resolve inside the oven's sandbox. | | [`bin/gen-cover.mjs`](bin/gen-cover.mjs) | Generate the colored-pencil vignette cover illustration for a dossier from `figures/cover-prompt.txt` via OpenAI gpt-image-2 (1024×1024 square, faded edges). | | [`bin/gen-qrs.mjs`](bin/gen-qrs.mjs) | Generate per-paper QR-code PNG pointing to the deployed permalink at `papers.aesthetic.computer/.pdf`. Uses `qrencode` CLI. | -| [`aesthetic-eye.mjs`](aesthetic-eye.mjs) | Render final-PDF page contacts and diagram crops; enforce the colored `Aesthetic.Computer` period and current visual-inference verdicts. See [`AESTHETIC-EYE.md`](AESTHETIC-EYE.md). | +| [`aesthetic-eye.mjs`](aesthetic-eye.mjs) | Render final-PDF page contacts plus evidence-figure and diagram crops; enforce useful evidence scale, the colored `Aesthetic.Computer` period, and current visual-inference verdicts. See [`AESTHETIC-EYE.md`](AESTHETIC-EYE.md). | | [`bin/migrate-cover.mjs`](bin/migrate-cover.mjs) | One-shot migration: rewrite an old-style cover block (4em pals + 15em hero) into the new vignette layout (pals top-left + QR top-right + TikZ-overlaid title floating over faded illustration). Idempotent. | | [`bin/fix-people-tables.mjs`](bin/fix-people-tables.mjs) | One-shot: convert `tabularx{lXl}` people tables to `tabularx{lXX}` so the third column wraps and stops overflowing the column width. | diff --git a/papers/aesthetic-eye.mjs b/papers/aesthetic-eye.mjs index 5577b571c..31e9ce897 100644 --- a/papers/aesthetic-eye.mjs +++ b/papers/aesthetic-eye.mjs @@ -1,9 +1,9 @@ #!/usr/bin/env node -// aesthetic-eye — render-first visual QA for papers and their diagrams. +// aesthetic-eye — render-first visual QA for papers and their visual evidence. // -// A TeX build proves syntax, not design. This tool prepares diagram crops for +// A TeX build proves syntax, not design. This tool prepares evidence crops for // visual inference and enforces a current-PDF manifest whose verdict is the -// literal `design: pass|fail` for every diagram. +// literal `design: pass|fail` for every evidence figure and diagram. import { createHash } from "node:crypto"; import { execFile } from "node:child_process"; @@ -14,6 +14,7 @@ import { fileURLToPath } from "node:url"; const exec = promisify(execFile); const REQUIRED_CHECKS = ["tangents", "type", "balance", "spaceUse", "hierarchy", "edgeRouting"]; +const FIGURE_CHECKS = ["scale", "legibility", "evidenceDominance", "crop", "captionFit"]; const PRESENTATION_CHECKS = ["oneClaim", "distanceType", "evidenceDominance", "voteClarity", "routeOrientation", "qrLegibility"]; const BRAND_NAME = "Aesthetic.Computer"; const BRAND_DOT_COLOR = "#B44887"; @@ -30,6 +31,12 @@ async function sha256(path) { return createHash("sha256").update(await readFile(path)).digest("hex"); } +export function countEvidenceFigures(sourceText) { + return [...String(sourceText || "").matchAll(/\\begin\{figure\*?\}([\s\S]*?)\\end\{figure\*?\}/g)] + .filter((match) => /\\includegraphics(?:\[[^\]]*\])?\{/.test(match[1])) + .length; +} + function resolveInputs(input, manifestArg) { const absolute = resolve(input); const paperDir = extname(absolute).toLowerCase() === ".pdf" ? dirname(absolute) : absolute; @@ -47,12 +54,17 @@ async function loadReview(input, manifestArg) { ? manifest.pdf : join(paperDir, manifest.pdf || ""); if (!manifest.pdf || !(await exists(pdfPath))) throw new Error(`manifest PDF is missing: ${pdfPath}`); - return { paperDir, manifestPath, manifest, pdfPath }; + const sourcePath = join(paperDir, `${basename(pdfPath, extname(pdfPath))}.tex`); + const sourceFigureCount = await exists(sourcePath) + ? countEvidenceFigures(await readFile(sourcePath, "utf8")) + : null; + return { paperDir, manifestPath, manifest, pdfPath, sourcePath, sourceFigureCount }; } -export function validateManifest(manifest, currentPdfSha256) { +export function validateManifest(manifest, currentPdfSha256, sourceFigureCount = null) { const errors = []; const diagrams = Array.isArray(manifest?.diagrams) ? manifest.diagrams : []; + const figures = Array.isArray(manifest?.figures) ? manifest.figures : []; const brand = manifest?.brand; const presentation = manifest?.presentation; if (manifest?.schema !== 1) errors.push("schema must be 1"); @@ -105,6 +117,46 @@ export function validateManifest(manifest, currentPdfSha256) { const allDesignPass = diagrams.length === manifest?.expectedDiagrams && diagrams.every((diagram) => diagram.design === "pass"); + + const figuresRequired = (Number.isInteger(sourceFigureCount) && sourceFigureCount > 0) + || manifest?.expectedFigures != null + || figures.length > 0; + if (figuresRequired) { + if (!Number.isInteger(manifest?.expectedFigures) || manifest.expectedFigures < 0) { + errors.push("expectedFigures must be a non-negative integer when the source contains evidence figures"); + } else { + if (figures.length !== manifest.expectedFigures) { + errors.push(`expected ${manifest.expectedFigures} evidence figure(s), found ${figures.length}`); + } + if (Number.isInteger(sourceFigureCount) && manifest.expectedFigures !== sourceFigureCount) { + errors.push(`source contains ${sourceFigureCount} evidence figure(s), manifest expects ${manifest.expectedFigures}`); + } + } + const ids = new Set(); + for (const [index, figure] of figures.entries()) { + const prefix = `figure ${figure?.id || index + 1}`; + if (!figure?.id) errors.push(`${prefix}: id is required`); + else if (ids.has(figure.id)) errors.push(`${prefix}: id must be unique`); + else ids.add(figure.id); + if (!Number.isInteger(figure?.page) || figure.page < 1) errors.push(`${prefix}: page must be >= 1`); + if (!Array.isArray(figure?.crop) || figure.crop.length !== 4 || figure.crop.some((n) => !Number.isFinite(n) || n < 0 || n > 1)) { + errors.push(`${prefix}: crop must be four normalized values between 0 and 1`); + } else if (figure.crop[0] + figure.crop[2] > 1 || figure.crop[1] + figure.crop[3] > 1) { + errors.push(`${prefix}: crop extends beyond the rendered page`); + } + if (!["pass", "fail"].includes(figure?.design)) errors.push(`${prefix}: design must be pass or fail`); + for (const check of FIGURE_CHECKS) { + if (!["pass", "fail"].includes(figure?.checks?.[check])) errors.push(`${prefix}: checks.${check} must be pass or fail`); + } + const failedChecks = FIGURE_CHECKS.filter((check) => figure?.checks?.[check] === "fail"); + if (figure?.design === "pass" && failedChecks.length) { + errors.push(`${prefix}: design cannot pass while ${failedChecks.join(", ")} fail`); + } + } + } + const allFigureDesignsPass = !figuresRequired + || (figures.length === manifest?.expectedFigures + && figures.every((figure) => figure.design === "pass")); if (presentation) { const slides = Array.isArray(presentation.slides) ? presentation.slides : []; if (!Number.isInteger(presentation.expectedSlides) || presentation.expectedSlides < 1) { @@ -131,7 +183,14 @@ export function validateManifest(manifest, currentPdfSha256) { const presentationPass = !presentation || (presentation.slides?.length === presentation.expectedSlides && presentation.slides.every((slide) => slide.design === "pass")); - return { pass: errors.length === 0 && allDesignPass && brandPass && presentationPass, errors, diagrams, brand, presentation }; + return { + pass: errors.length === 0 && allDesignPass && allFigureDesignsPass && brandPass && presentationPass, + errors, + diagrams, + figures, + brand, + presentation, + }; } export async function findVisibleBrandViolations(pdfPath) { @@ -142,7 +201,7 @@ export async function findVisibleBrandViolations(pdfPath) { async function check(input, manifestArg) { const review = await loadReview(input, manifestArg); const digest = await sha256(review.pdfPath); - const verdict = validateManifest(review.manifest, digest); + const verdict = validateManifest(review.manifest, digest, review.sourceFigureCount); const brandViolations = await findVisibleBrandViolations(review.pdfPath); const errors = [...verdict.errors]; if (brandViolations.length) { @@ -151,6 +210,7 @@ async function check(input, manifestArg) { const pass = verdict.pass && errors.length === 0; console.log(`brand: ${verdict.brand?.design || "missing"} ${BRAND_NAME} dot ${BRAND_DOT_COLOR}`); for (const diagram of verdict.diagrams) console.log(`design: ${diagram.design} ${diagram.id} page ${diagram.page}`); + for (const figure of verdict.figures) console.log(`figure: ${figure.design} ${figure.id} page ${figure.page}`); for (const slide of verdict.presentation?.slides || []) console.log(`slide: ${slide.design} page ${slide.page}`); if (errors.length) for (const error of errors) console.error(`FAIL: ${error}`); if (!pass && !errors.length) console.error("FAIL: one or more visual verdicts have design: fail"); @@ -180,29 +240,40 @@ async function prepare(input, manifestArg, outputArg) { } } - const crops = []; - for (const diagram of review.manifest.diagrams || []) { - const pagePath = `${pagePrefix}-${diagram.page}.png`; - if (!(await exists(pagePath))) throw new Error(`${diagram.id}: rendered page ${diagram.page} is missing`); - const { stdout } = await exec("magick", ["identify", "-format", "%w %h", pagePath]); - const [pageWidth, pageHeight] = stdout.trim().split(/\s+/).map(Number); - const [x, y, width, height] = diagram.crop; - const geometry = `${Math.round(pageWidth * width)}x${Math.round(pageHeight * height)}+${Math.round(pageWidth * x)}+${Math.round(pageHeight * y)}`; - const cropPath = join(outputDir, `diagram-${diagram.id}.png`); - await exec("magick", [pagePath, "-crop", geometry, "+repage", cropPath]); - crops.push(cropPath); - console.log(`${diagram.id}: ${cropPath}`); + async function renderCrops(items, kind) { + const crops = []; + for (const item of items || []) { + const pagePath = `${pagePrefix}-${item.page}.png`; + if (!(await exists(pagePath))) throw new Error(`${item.id}: rendered page ${item.page} is missing`); + const { stdout } = await exec("magick", ["identify", "-format", "%w %h", pagePath]); + const [pageWidth, pageHeight] = stdout.trim().split(/\s+/).map(Number); + const [x, y, width, height] = item.crop; + const geometry = `${Math.round(pageWidth * width)}x${Math.round(pageHeight * height)}+${Math.round(pageWidth * x)}+${Math.round(pageHeight * y)}`; + const cropPath = join(outputDir, `${kind}-${item.id}.png`); + await exec("magick", [pagePath, "-crop", geometry, "+repage", cropPath]); + crops.push(cropPath); + console.log(`${item.id}: ${cropPath}`); + } + return crops; + } + + const figureCrops = await renderCrops(review.manifest.figures, "figure"); + if (figureCrops.length) { + const contactPath = join(outputDir, "figures-contact.png"); + await exec("magick", ["montage", "-font", CONTACT_FONT, ...figureCrops, "-thumbnail", "1000x700", "-tile", "2x", "-geometry", "+24+24", contactPath]); + console.log(`figures: ${contactPath}`); } - if (crops.length) { + const diagramCrops = await renderCrops(review.manifest.diagrams, "diagram"); + if (diagramCrops.length) { const contactPath = join(outputDir, "diagrams-contact.png"); - await exec("magick", ["montage", "-font", CONTACT_FONT, ...crops, "-thumbnail", "1000x700", "-tile", "2x", "-geometry", "+24+24", contactPath]); - console.log(`contact: ${contactPath}`); + await exec("magick", ["montage", "-font", CONTACT_FONT, ...diagramCrops, "-thumbnail", "1000x700", "-tile", "2x", "-geometry", "+24+24", contactPath]); + console.log(`diagrams: ${contactPath}`); } const brandViolations = await findVisibleBrandViolations(review.pdfPath); console.log(`brand text: ${brandViolations.length ? `FAIL (${brandViolations.length} missing period)` : "PASS"}`); console.log(`pdfSha256: ${await sha256(review.pdfPath)}`); - console.log("Next: inspect pages-contact.png for the colored Aesthetic.Computer period, inspect every diagram crop, record the verdicts, then run `aesthetic-eye check`."); + console.log("Next: inspect pages-contact.png for the colored Aesthetic.Computer period, inspect every evidence-figure and diagram crop, record the verdicts, then run `aesthetic-eye check`."); } function option(args, name) { diff --git a/papers/test/aesthetic-eye.test.mjs b/papers/test/aesthetic-eye.test.mjs new file mode 100644 index 000000000..ba406c401 --- /dev/null +++ b/papers/test/aesthetic-eye.test.mjs @@ -0,0 +1,64 @@ +import test from "node:test"; +import assert from "node:assert/strict"; + +import { countEvidenceFigures, validateManifest } from "../aesthetic-eye.mjs"; + +const baseManifest = { + schema: 1, + pdf: "paper.pdf", + pdfSha256: "current-hash", + visualInference: true, + reviewedAt: "2026-09-09T00:00:00Z", + reviewer: { kind: "visual-inference", agent: "test" }, + brand: { + canonicalName: "Aesthetic.Computer", + dotColor: "#B44887", + design: "pass", + checks: { period: "pass", dotColor: "pass" }, + }, + expectedDiagrams: 0, + diagrams: [], +}; + +const passingFigure = { + id: "result", + page: 1, + crop: [0.1, 0.2, 0.8, 0.4], + design: "pass", + checks: { + scale: "pass", + legibility: "pass", + evidenceDominance: "pass", + crop: "pass", + captionFit: "pass", + }, +}; + +test("counts image-backed evidence figures but not a TikZ diagram", () => { + const source = String.raw` + \begin{figure}\includegraphics{evidence}\end{figure} + \begin{figure*}\begin{tikzpicture}\end{tikzpicture}\end{figure*}`; + assert.equal(countEvidenceFigures(source), 1); +}); + +test("fails when source evidence is absent from the Aesthetic Eye manifest", () => { + const verdict = validateManifest(baseManifest, "current-hash", 1); + assert.equal(verdict.pass, false); + assert.match(verdict.errors.join("\n"), /expectedFigures/); +}); + +test("passes a fully reviewed evidence figure", () => { + const manifest = { ...baseManifest, expectedFigures: 1, figures: [passingFigure] }; + assert.equal(validateManifest(manifest, "current-hash", 1).pass, true); +}); + +test("fails a too-small evidence verdict", () => { + const figure = { + ...passingFigure, + checks: { ...passingFigure.checks, scale: "fail" }, + }; + const manifest = { ...baseManifest, expectedFigures: 1, figures: [figure] }; + const verdict = validateManifest(manifest, "current-hash", 1); + assert.equal(verdict.pass, false); + assert.match(verdict.errors.join("\n"), /scale/); +}); diff --git a/slab/bin/paper-mcp.mjs b/slab/bin/paper-mcp.mjs index 4849ee70f..cd364f21b 100755 --- a/slab/bin/paper-mcp.mjs +++ b/slab/bin/paper-mcp.mjs @@ -38,7 +38,7 @@ const PAPERS_STACK_INSTRUCTIONS = [ "The papers stack is the Aesthetic Computer scholarly publishing workflow, not a generic PDF renderer.", "For requests such as 'use the papers stack' or 'use /papers', first consult papers/SCORE.md, the public Platter index, relevant sub-platters, prior papers and bibliographies, and primary code/data/evidence.", "Unless the user explicitly requests another mill lane, default to an archival/arXiv-style LaTeX paper with abstract, context/related work, method/system, implementation, evidence/evaluation, ethics/privacy/limitations, conclusion, references, and captioned figures/tables.", - "Briefings, dossiers, essays, decks, cards, and visual reports are distinct outputs. Build, run Figure-Table-QA-Check, visually inspect every page and diagram crop, record the current render in aesthetic-eye.json, and run Aesthetic Eye check. Compilation alone is not completion; a missing, stale, or failing Aesthetic Eye review is a hard failure.", + "Briefings, dossiers, essays, decks, cards, and visual reports are distinct outputs. Build, run Figure-Table-QA-Check, visually inspect every page plus every evidence-figure and diagram crop, record the current render in aesthetic-eye.json, and run Aesthetic Eye check. Compilation alone is not completion; a missing, stale, or failing Aesthetic Eye review is a hard failure.", ].join(" "); async function exists(path) { @@ -443,7 +443,7 @@ async function toolAestheticEyePrepare({ paper } = {}) { if (!rec.pdfPath || !(await exists(rec.pdfPath))) throw new Error(`${rec.id} has no PDF. Call paper_build first.`); const result = await runAestheticEye("prepare", rec); if (!result.pass) throw new Error(`Aesthetic Eye prepare failed for ${rec.id}:\n${result.output}`); - return [{ type: "text", text: `Aesthetic Eye review prepared for ${rec.title} [${rec.id}]\n${result.output}\nInspect pages-contact.png and every diagram crop, then record verdicts and call paper_aesthetic_eye_check.` }]; + return [{ type: "text", text: `Aesthetic Eye review prepared for ${rec.title} [${rec.id}]\n${result.output}\nInspect pages-contact.png and every evidence-figure and diagram crop, then record verdicts and call paper_aesthetic_eye_check.` }]; } async function toolAestheticEyeCheck({ paper } = {}) { @@ -670,7 +670,7 @@ const TOOLS = [ }, { name: "paper_aesthetic_eye_prepare", - description: "Prepare Aesthetic Eye review artifacts from an existing aesthetic-eye.json: an all-page contact sheet and one crop per declared diagram. Inspect every rendered artifact and update the manifest with the current PDF hash and literal visual verdicts. SIDE EFFECT: writes .aesthetic-eye/ raster files beside the paper.", + description: "Prepare Aesthetic Eye review artifacts from an existing aesthetic-eye.json: an all-page contact sheet and one crop per declared evidence figure and diagram. Inspect every rendered artifact and update the manifest with the current PDF hash and literal visual verdicts. SIDE EFFECT: writes .aesthetic-eye/ raster files beside the paper.", inputSchema: { type: "object", properties: { paper: { type: "string" } }, @@ -724,7 +724,7 @@ async function handleMessage(message) { result: { protocolVersion: "2024-11-05", capabilities: { tools: {} }, - serverInfo: { name: "paper-mcp", version: "1.2.0" }, + serverInfo: { name: "paper-mcp", version: "1.3.0" }, instructions: PAPERS_STACK_INSTRUCTIONS, }, }; -- 2.51.2