diff --git a/sellers/ui-ux-designer/.gitignore b/sellers/ui-ux-designer/.gitignore new file mode 100644 index 00000000..a9f8e5a8 --- /dev/null +++ b/sellers/ui-ux-designer/.gitignore @@ -0,0 +1,5 @@ +node_modules/ +evidence/tmp/ +runs/*/workspace/node_modules +runs/generative-smoke/ +runs/generative-sabotage.*/ diff --git a/sellers/ui-ux-designer/README.md b/sellers/ui-ux-designer/README.md new file mode 100644 index 00000000..2d685f05 --- /dev/null +++ b/sellers/ui-ux-designer/README.md @@ -0,0 +1,137 @@ +# UI/UX designer seller + +A **runnable** maxplayer seller package for UI/UX design work. It is not a persona file: a real +model harness, driven through maxplayer's own local driver, is given a natural-language brief and +**authors the design itself** — then renders it, inspects its own screenshots, and audits its own +accessibility. Around that sit a pinned browser harness, accessibility checks, visual comparison, +image handling, and one smoke gate that proves the whole path works — or fails. + +## Read this first: what this does NOT do + +- **It has never been run against a live maxplayer relay.** No wallet, no sats, no seat, no mint. + The seller *registration* path — publishing a profile, claiming a job, delivering, getting paid — + is entirely unexercised here. Everything below concerns the agent and its tools. +- **The generative path costs real model tokens** on this host's own account, takes minutes, and + produces **different output every run**. The gate is deliberately not deterministic. +- **It rests on one model harness.** `codex-acp` is what answers here; `claude-agent-acp` is + installed but refused by a standing org spend limit. If the working account hits its limit the + generative check fails loudly — it never falls back to faking the designer. +- **No human designer has judged the output.** Zero automated accessibility violations is a floor, + not taste. +- **It has not been run inside the docker sandbox** (`[sandbox] mode = "docker"`). +- Automated accessibility checking finds roughly a third of real problems. See + [`design-identity/IDENTITY.md`](design-identity/IDENTITY.md) §6 rule 12. +- The default design identity is **proposed, not adopted**. + +## Prerequisites + +- **Node ≥ 20** (developed and verified on v26.5.0). +- **A `maxplayer` binary built with the `acp` feature**, for the generative check: + ```bash + cargo build -p maxplayer --features acp --release + ``` + A stock release build refuses the path with `maxplayer run requires rebuilding with the acp + feature`. The gate locates `target/release/maxplayer` (or `target/debug`), verifies it really has + the feature, and fails with that message rather than substituting anything. +- **An authorized ACP harness on PATH** — `codex-acp` is what this was verified against. Override + with `--agent-command `. +- A Playwright-managed Chromium. The lockfile pins `playwright@1.62.0`, whose Chromium is + **revision 1234 / Google Chrome for Testing 151.0.7922.34**. If that revision is already in your + `~/Library/Caches/ms-playwright` (or `PLAYWRIGHT_BROWSERS_PATH`), nothing downloads. Otherwise: + `npx playwright install chromium`. +- No paid service, API key, or subscription is required or accepted. + +## Setup and run + +```bash +cd sellers/ui-ux-designer +npm ci +bash scripts/smoke.sh # the gate — exit 0 means everything below is true +``` + +**Expect this to take several minutes**: the gate spends a real model turn letting the agent design +something. Last verified run: **9 checks run, 0 failed**. + +The gate is the documentation that cannot go stale. It launches Chromium and prints its version +(rather than checking a binary exists), regenerates the contrast ledger, checks libvips, **hands a +natural-language brief with no candidate supplied to a real model through the maxplayer local +driver and requires it to author, render and audit its own design**, then drives the deterministic +review pipeline as tooling, re-verifies every produced artifact from outside the agent, and finally +runs four negative controls that must each fail. Any failure exits nonzero. + +Run the generative path directly: + +```bash +node agent/design-run.mjs --brief briefs/nova-status-brief.md --out runs/mine +node agent/design-run.mjs --brief briefs/nova-status-brief.md --out runs/mine --verify-only +``` + +`--verify-only` re-checks a finished run's evidence without spending another model turn. + +Run the review tooling directly: + +```bash +node tools/drive-agent.mjs --brief jobs/sample-review.json +``` + +## What is in here + +| Path | What it is | +|---|---| +| `agent/designer-agent.mjs` | the ACP stdio agent a seller spawns; implements `review-redesign` | +| `tools/drive-agent.mjs` | minimal ACP client — spawns the agent and speaks the handshake | +| `tools/tokens.mjs` | generates `tokens.css` + `contrast.md`; nonzero exit on a contrast miss | +| `tools/preview.mjs` | loopback-only static server rooted at this package | +| `tools/shoot.mjs` | desktop + mobile screenshots that must prove they contain a rendered page | +| `tools/a11y.mjs` | axe-core run guarded by load proof | +| `tools/vdiff.mjs` | pixelmatch comparison with required-change / max-change thresholds | +| `tools/svg.mjs` | sharp SVG rasterising and pipeline check | +| `tools/verify-manifest.mjs` | re-reads and re-digests every artifact after the run | +| `design-identity/` | the Plainsong identity, tokens, measured contrast, licensed corpus | +| `knowledge/INDEX.md` | what the agent reads before working, and what each tool refuses to do | +| `samples/redesign/` | before/after sample plus `RATIONALE.md` | +| `evidence/` | screenshots, axe JSON, diffs, and the verified `MANIFEST.md` | +| `config/seller.config.toml.example` | separate seller home, least privilege, no wallet | + +## Pointing a real seller at it + +```bash +mkdir -p ~/.maxplayer-ui-ux-designer +cp config/seller.config.toml.example ~/.maxplayer-ui-ux-designer/config.toml +# edit: set the ABSOLUTE path to agent/designer-agent.mjs +MAXPLAYER_HOME=~/.maxplayer-ui-ux-designer "$MAXPLAYER_BIN" seller +``` + +`MAXPLAYER_HOME` gives this seat its own home so its key and state never sit beside another seat's. +The config file carries **no** mint, wallet, key or rate — those are human decisions, and this +package was built under a hold on real sats. + +Note on discovery: `ui-ux-designer` is a *custom* preset, so it publishes **no wire harness +family** — the closed vocabulary is `claude-code | codex | cursor | goose` +(`crates/maxplayer-core/src/agent_presets.rs`). Buyers filtering by family will not match this +seat. They reach it by naming the agent. + +## Job brief format + +```json +{ + "job": "review-redesign", + "name": "meridian-invoices", + "baseline": "/samples/redesign/before/", + "candidate": "/samples/redesign/after/", + "expectBaselineViolations": 4, + "maxCandidateViolations": 0, + "minChangeRatio": 0.02 +} +``` + +`expectBaselineViolations` is the important one: it asserts the baseline really is as bad as +expected. A baseline that suddenly reports zero problems almost always means the check broke, not +that the page improved — so that case fails the run. + +Unknown job types and free-text prompts are refused rather than guessed at. + +## Reports + +Full write-up, including everything verified first-hand and every limitation: +[`reports/ui-ux-designer-seller-report.md`](reports/ui-ux-designer-seller-report.md). diff --git a/sellers/ui-ux-designer/agent/design-run.mjs b/sellers/ui-ux-designer/agent/design-run.mjs new file mode 100644 index 00000000..9927bd10 --- /dev/null +++ b/sellers/ui-ux-designer/agent/design-run.mjs @@ -0,0 +1,406 @@ +#!/usr/bin/env node +/** + * design-run.mjs — hand a natural-language brief to a REAL model harness through the + * ACTUAL maxplayer local driver, and prove the model did the design work. + * + * This is the correction to the e72e9bd verdict. Previously `agent/designer-agent.mjs` + * only accepted a supplied candidate and rewrote it deterministically — it could not + * create a design. Here: + * + * - the brief is natural language, and NO candidate implementation is supplied; + * - the agent authors `status.html` itself; + * - the agent runs the browser/accessibility tools ITSELF and reads their output, + * iterating until the accessibility gate is clean at BOTH viewports; + * - the review pipeline is TOOLING the agent uses, never the designer. + * + * Nothing here writes a single line of the deliverable. If the model does not produce + * it, this exits nonzero. Success is never reported without evidence: + * + * 1. the driver's event log must end `turn_ended: completed`; + * 2. `status.html` must exist, be non-trivial, and NOT be a copy of any file we shipped; + * 3. the event log must show the agent INVOKING shoot.mjs and a11y.mjs — screenshots + * that appear without a recorded tool call are not accepted as the agent's work; + * 4. the accessibility JSON the agent produced must show a real page load and zero + * violations at desktop AND mobile. + * + * Usage: + * node agent/design-run.mjs --brief briefs/nova-status-brief.md \ + * [--out runs/generative-] [--agent-command ] [--idle-timeout 900] + * + * No relay, no wallet, no sats, no deployment: `maxplayer run` is the local driver only. + */ +import { spawnSync } from "node:child_process"; +import { createHash } from "node:crypto"; +import fs from "node:fs"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; + +const HERE = path.dirname(fileURLToPath(import.meta.url)); +const ROOT = path.resolve(HERE, ".."); + +const arg = (k, d) => { + const i = process.argv.indexOf(k); + return i > -1 ? process.argv[i + 1] : d; +}; + +const sha256 = (buf) => createHash("sha256").update(buf).digest("hex"); + +/** The maxplayer binary must be one WE built with the acp feature. */ +export function resolveDriver() { + const explicit = arg("--maxplayer"); + const candidates = explicit + ? [explicit] + : [ + path.resolve(ROOT, "../../target/release/maxplayer"), + path.resolve(ROOT, "../../target/debug/maxplayer"), + ]; + for (const c of candidates) { + if (!fs.existsSync(c)) continue; + const probe = spawnSync(c, ["run"], { encoding: "utf8" }); + const text = `${probe.stdout || ""}${probe.stderr || ""}`; + if (/requires rebuilding with the acp feature/.test(text)) continue; // built without acp + return c; + } + throw new Error( + "no maxplayer binary with the `acp` feature found — build it with:\n" + + " cargo build -p maxplayer --features acp --release", + ); +} + +/** + * Probe an ACP adapter for a model that actually answers. Presence on PATH is not + * availability: claude-agent-acp handshakes fine here but is refused at prompt time by a + * standing org spend limit, so it must not be chosen. + */ +export function pickHarness(explicit) { + const candidates = explicit ? [explicit] : ["/opt/homebrew/bin/codex-acp"]; + for (const c of candidates) if (fs.existsSync(c)) return c; + throw new Error( + `no authorized ACP harness available (looked for: ${candidates.join(", ")}).\n` + + "This is a BLOCKED condition, not something to substitute deterministic execution for.", + ); +} + +/** Copy the tools + tokens the agent needs, and link the pinned node_modules. */ +function stageWorkspace(ws) { + fs.mkdirSync(ws, { recursive: true }); + fs.mkdirSync(path.join(ws, "tools"), { recursive: true }); + for (const f of ["shoot.mjs", "a11y.mjs", "vdiff.mjs", "preview.mjs"]) + fs.copyFileSync(path.join(ROOT, "tools", f), path.join(ws, "tools", f)); + + fs.mkdirSync(path.join(ws, "design-identity"), { recursive: true }); + for (const f of ["tokens.css", "tokens.json", "contrast.md"]) { + const src = path.join(ROOT, "design-identity", f); + if (fs.existsSync(src)) fs.copyFileSync(src, path.join(ws, "design-identity", f)); + } + + // Pinned dependencies, single copy — reuse, never re-resolve. + const link = path.join(ws, "node_modules"); + if (!fs.existsSync(link)) fs.symlinkSync(path.join(ROOT, "node_modules"), link, "dir"); + return ws; +} + +function buildTask(briefText) { + return `You are a UI/UX designer. Design and BUILD the page described in the brief below. + +There is no existing implementation and no candidate to edit. You are authoring this from +nothing. Write the source yourself. + +=== BRIEF (verbatim from the buyer) === +${briefText} +=== END BRIEF === + +Your working directory already contains everything you need: + + design-identity/tokens.css the design tokens — link or inline these; do not invent + a second colour system. Read them before you design. + design-identity/contrast.md measured contrast ratios for the token palette + tools/shoot.mjs screenshots, desktop AND mobile in one run + tools/a11y.mjs axe-core accessibility audit + node_modules/ pinned Chromium/Playwright/axe — already installed + +Do this, in order: + +1. Read design-identity/tokens.css so you know the real token names and values. +2. Write status.html in the working directory: ONE self-contained file, styles inline, + no framework, no build step, no external network requests. +3. Screenshot it and LOOK at what you produced: + node tools/shoot.mjs --url file://$PWD/status.html --name status --out shots --full + The tool prints the pixel dimensions, distinct colour count and ink ratio of each + capture, and it fails if the image is blank. Read that output. +4. Audit accessibility at BOTH viewports, and read every violation: + node tools/a11y.mjs --url file://$PWD/status.html --name status --viewport desktop --out a11y + node tools/a11y.mjs --url file://$PWD/status.html --name status --viewport mobile --out a11y +5. FIX what the tools report and repeat steps 3-4 until BOTH audits report + violations=0. Do not stop while a violation remains. Do not silence a violation by + deleting the content it was about. +6. Finish by writing DESIGN-NOTES.md in the working directory: what you designed and why, + which token you used for what, how you met the colour-blindness and screen-reader + constraints in the brief, and the final violation counts and screenshot dimensions you + observed from the tool output. + +Constraints that matter as much as the visual result: the status must be readable without +relying on colour alone, the page must make sense to a screen reader in source order, and +it must hold up at 390px wide. Contrast must meet WCAG AA. + +Run the tools yourself and act on what they say. Do not claim a result you have not seen +in tool output.`; +} + +/** + * Parse what the agent actually did. The driver writes TWO streams and they carry + * different things — an earlier version of this gate read only the first and wrongly + * accused a passing run of never rendering anything: + * + * events.jsonl the durable job log. Envelope is {v,seq,ts,payload:{type,data}} — + * the type is NESTED under `payload`. Carries driver.ready, + * job.execution_changed and agent.message ONLY. Completion here is + * job.execution_changed with status "completed"; there is no + * `turn_ended` record in this file. + * driver-stdout.jsonl the forwarded ACP session/update notifications. THIS is where the + * agent's tool calls and the `turn_ended` stop reason appear. + * + * Absence of a tool call in events.jsonl means nothing. Only stdout can answer it. + */ +function readEvents(logPath, stdoutPath) { + const out = { turnEnded: null, jobStatus: null, toolText: [], lines: 0, stdoutLines: 0 }; + + if (fs.existsSync(logPath)) + for (const line of fs.readFileSync(logPath, "utf8").split("\n")) { + const t = line.trim(); + if (!t) continue; + out.lines += 1; + let ev; + try { + ev = JSON.parse(t); + } catch { + continue; + } + const p = ev.payload ?? ev; + if (p.type === "job.execution_changed" && p.data?.status) out.jobStatus = p.data.status; + } + + if (stdoutPath && fs.existsSync(stdoutPath)) + for (const line of fs.readFileSync(stdoutPath, "utf8").split("\n")) { + const t = line.trim(); + if (!t) continue; + out.stdoutLines += 1; + let ev; + try { + ev = JSON.parse(t); + } catch { + continue; + } + if (ev.type === "turn_ended") out.turnEnded = ev.data; + const s = JSON.stringify(ev); + if (/turn_ended/.test(s) && out.turnEnded === null) { + const m = s.match(/"turn_ended"\s*,\s*"data"\s*:\s*"([a-z_]+)"/); + if (m) out.turnEnded = m[1]; + } + if (/tool_call|shoot\.mjs|a11y\.mjs|exec_command|shell/.test(s)) out.toolText.push(s); + } + + return out; +} + +export async function main() { + const briefRel = arg("--brief", "briefs/nova-status-brief.md"); + const briefPath = path.resolve(ROOT, briefRel); + if (!fs.existsSync(briefPath)) throw new Error(`brief not found: ${briefPath}`); + const briefText = fs.readFileSync(briefPath, "utf8"); + const briefDigest = sha256(fs.readFileSync(briefPath)); + + // --verify-only re-reads a finished run's evidence without spending another model turn. + // The checks below are the SAME ones the live gate applies, so this cannot be used to + // wave a run through: it re-reads artifacts from disk rather than trusting a summary. + const verifyOnly = process.argv.includes("--verify-only"); + + const stamp = new Date().toISOString().replace(/[-:]/g, "").replace(/\..+/, "Z"); + const outDir = path.resolve(ROOT, arg("--out", `runs/generative-${stamp}`)); + if (verifyOnly && !fs.existsSync(outDir)) + throw new Error(`--verify-only needs an existing run directory: ${outDir}`); + const ws = verifyOnly ? path.join(outDir, "workspace") : stageWorkspace(path.join(outDir, "workspace")); + + const driver = verifyOnly ? "(verify-only)" : resolveDriver(); + const harness = verifyOnly ? "(verify-only)" : pickHarness(arg("--agent-command")); + const logPath = path.join(outDir, "events.jsonl"); + const task = buildTask(briefText); + if (!verifyOnly) fs.writeFileSync(path.join(outDir, "task.txt"), task); + + // Guard: the deliverable must not exist before the model runs. + const deliverable = path.join(ws, "status.html"); + if (!verifyOnly && fs.existsSync(deliverable)) + throw new Error("status.html already exists in a fresh workspace — refusing to claim authorship"); + + const argv = [ + "run", + "--agent-command", + harness, + "--task", + task, + "--cwd", + ws, + "--log", + logPath, + "--job-id", + `design-${stamp}`, + "--permission-policy", + "allow", + "--idle-timeout", + arg("--idle-timeout", "900"), + ]; + + console.log(`driver : ${driver}`); + console.log(`harness : ${harness}`); + console.log(`workspace: ${ws}`); + console.log(`brief : ${briefRel} (sha256 ${briefDigest.slice(0, 16)}…)`); + console.log("running the model — this is a real agent turn, it takes minutes…"); + + let res; + let elapsed; + if (verifyOnly) { + res = { status: 0, stdout: "", stderr: "" }; + elapsed = "0.0"; + console.log(`re-verifying existing run evidence in ${outDir}`); + } else { + const started = Date.now(); + res = spawnSync(driver, argv, { + encoding: "utf8", + maxBuffer: 256 * 1024 * 1024, + stdio: ["ignore", "pipe", "pipe"], + }); + elapsed = ((Date.now() - started) / 1000).toFixed(1); + fs.writeFileSync(path.join(outDir, "driver-stdout.jsonl"), res.stdout || ""); + fs.writeFileSync(path.join(outDir, "driver-stderr.log"), res.stderr || ""); + console.log(`driver exit=${res.status} in ${elapsed}s`); + } + + const failures = []; + const ev = readEvents(logPath, path.join(outDir, "driver-stdout.jsonl")); + + if (res.status !== 0) + failures.push(`driver exited ${res.status}: ${(res.stderr || "").trim().split("\n").pop()}`); + // The job must have finished, by either stream's account. + const finished = ev.jobStatus === "completed" || ev.turnEnded === "end_turn" || ev.turnEnded === "completed"; + if (!finished) + failures.push( + `the run did not finish (job status ${JSON.stringify(ev.jobStatus)}, ` + + `stop reason ${JSON.stringify(ev.turnEnded)})`, + ); + + // 2. The deliverable must exist, be real, and be the model's own work. + let html = null; + if (!fs.existsSync(deliverable)) { + failures.push("the agent did not create status.html — the model produced no deliverable"); + } else { + html = fs.readFileSync(deliverable, "utf8"); + if (html.length < 800) failures.push(`status.html is only ${html.length} chars — not a real page`); + const shippedDigests = new Set(); + const sampleDir = path.join(ROOT, "samples", "redesign"); + if (fs.existsSync(sampleDir)) + for (const d of ["before", "after"]) { + const f = path.join(sampleDir, d, "index.html"); + if (fs.existsSync(f)) shippedDigests.add(sha256(fs.readFileSync(f))); + } + if (shippedDigests.has(sha256(Buffer.from(html)))) + failures.push("status.html is byte-identical to a file this repository shipped — not authored"); + } + + // 3. The AGENT must have invoked the tools itself. + const ranShoot = ev.toolText.some((t) => t.includes("shoot.mjs")); + const ranA11y = ev.toolText.some((t) => t.includes("a11y.mjs")); + if (!ranShoot) failures.push("no tool call invoking shoot.mjs — the agent never rendered its design"); + if (!ranA11y) failures.push("no tool call invoking a11y.mjs — the agent never checked accessibility"); + + // 4. The agent's own accessibility results, re-read from disk, both viewports. + const a11yDir = path.join(ws, "a11y"); + const a11y = {}; + if (fs.existsSync(a11yDir)) + for (const f of fs.readdirSync(a11yDir).filter((f) => f.endsWith(".json"))) { + let r; + try { + r = JSON.parse(fs.readFileSync(path.join(a11yDir, f), "utf8")); + } catch { + failures.push(`unparseable a11y report ${f}`); + continue; + } + const vp = r.viewport || f; + a11y[vp] = r; + const proof = r.loadProof || {}; + if (!proof.title || (proof.elements ?? 0) < 20) + failures.push(`${vp}: audit has no load proof — a zero-violation run on a blank page is void`); + const v = r.counts?.violations; + if (v !== 0) failures.push(`${vp}: ${v} accessibility violations remain`); + } + for (const vp of ["desktop", "mobile"]) + if (!a11y[vp]) failures.push(`no accessibility report for ${vp} viewport`); + + // Screenshots the agent produced. + const shotsDir = path.join(ws, "shots"); + const shots = fs.existsSync(shotsDir) + ? fs.readdirSync(shotsDir).filter((f) => f.endsWith(".png")) + : []; + if (shots.length < 2) failures.push(`expected desktop and mobile screenshots, found ${shots.length}`); + + const notes = path.join(ws, "DESIGN-NOTES.md"); + if (!fs.existsSync(notes)) failures.push("the agent did not write DESIGN-NOTES.md"); + + const summary = { + generatedAt: new Date().toISOString(), + driver: { path: driver, exit: res.status, elapsedSeconds: Number(elapsed) }, + harness, + brief: { path: briefRel, sha256: briefDigest, candidateSupplied: false }, + events: { + lines: ev.lines, + stdoutLines: ev.stdoutLines, + jobStatus: ev.jobStatus, + turnEnded: ev.turnEnded, + toolCallsObserved: ev.toolText.length, + }, + agentInvokedTools: { shoot: ranShoot, a11y: ranA11y }, + deliverable: html + ? { file: "workspace/status.html", bytes: Buffer.byteLength(html), sha256: sha256(Buffer.from(html)) } + : null, + screenshots: shots.map((f) => { + const p = path.join(shotsDir, f); + return { file: `workspace/shots/${f}`, bytes: fs.statSync(p).size, sha256: sha256(fs.readFileSync(p)) }; + }), + accessibility: Object.fromEntries( + Object.entries(a11y).map(([vp, r]) => [ + vp, + { + engine: r.engine, + violations: r.counts?.violations, + passes: r.counts?.passes, + rulesConsidered: r.counts?.rulesConsidered, + loadProof: r.loadProof, + }, + ]), + ), + failures, + verdict: failures.length === 0 ? "PASS" : "FAIL", + }; + fs.writeFileSync(path.join(outDir, "summary.json"), `${JSON.stringify(summary, null, 2)}\n`); + + console.log(`\n--- generative design run: ${summary.verdict} ---`); + console.log( + `events=${ev.lines} job=${ev.jobStatus} stop=${ev.turnEnded} ` + + `toolcalls=${ev.toolText.length} shoot=${ranShoot} a11y=${ranA11y}`, + ); + for (const [vp, r] of Object.entries(summary.accessibility)) + console.log(` ${vp}: ${r.violations} violations, ${r.passes} passes (${r.engine})`); + for (const s of summary.screenshots) console.log(` shot ${s.file} ${s.bytes}B`); + if (failures.length) { + for (const f of failures) console.error(` FAIL ${f}`); + console.error(`\nGENERATIVE RUN FAILED — ${failures.length} problem(s)`); + process.exit(1); + } + console.log(`\nOK — the model authored, rendered and audited its own design. ${outDir}`); +} + +if (process.argv[1] && process.argv[1].endsWith("design-run.mjs")) { + main().catch((e) => { + console.error(`GENERATIVE RUN FAILED — ${e.message}`); + process.exit(1); + }); +} diff --git a/sellers/ui-ux-designer/agent/designer-agent.mjs b/sellers/ui-ux-designer/agent/designer-agent.mjs new file mode 100644 index 00000000..62a3ffa4 --- /dev/null +++ b/sellers/ui-ux-designer/agent/designer-agent.mjs @@ -0,0 +1,320 @@ +#!/usr/bin/env node +/** + * designer-agent.mjs — the UI/UX designer seller's agent, spoken to over ACP stdio. + * + * This is the process a maxplayer seller spawns via `[agents.ui-ux-designer] argv = [...]`. It + * speaks line-delimited JSON-RPC 2.0 on stdin/stdout and answers exactly the three requests the + * repo's ACP driver sends for one engine run (`crates/maxplayer-core/src/driver/acp_driver.rs`): + * + * initialize -> { protocolVersion: 2 } + * session/new -> { sessionId } (params carry the job cwd) + * session/prompt -> { stopReason: "end_turn" } after the work is done + * + * SCOPE, stated plainly: this agent does design review and token-driven redesign verification. It + * is NOT a general coding harness and it calls no language model. The prompt turn carries a JSON + * job brief; anything else is answered with a refusal recorded in the report, not a guess. + * + * Everything it claims, it proves — it reuses the same tools the CLI does, each of which refuses to + * report success without evidence (see tools/shoot.mjs, tools/a11y.mjs, tools/vdiff.mjs). + * + * Logs go to STDERR only. Anything on stdout that is not a JSON-RPC frame corrupts the protocol. + */ +import { createInterface } from "node:readline"; +import { mkdir, writeFile, readFile } from "node:fs/promises"; +import { join, resolve } from "node:path"; +import { createHash } from "node:crypto"; +import { chromium } from "playwright"; +import { startPreview } from "../tools/preview.mjs"; +import { shoot } from "../tools/shoot.mjs"; +import { audit } from "../tools/a11y.mjs"; +import { vdiff } from "../tools/vdiff.mjs"; +import { rasterise, checkSharp } from "../tools/svg.mjs"; +import { PKG, TOKENS } from "../tools/tokens.mjs"; + +const PROTOCOL_VERSION = 2; +const log = (...a) => process.stderr.write(`[designer-agent] ${a.join(" ")}\n`); + +async function sha256(file) { + return createHash("sha256").update(await readFile(file)).digest("hex"); +} + +/** + * The actual design work for one job. + * + * brief: { + * job: "review-redesign", + * baseline: "/samples/redesign/before/", path served by the preview server + * candidate: "/samples/redesign/after/", + * name: "meridian-invoices", + * expectBaselineViolations: 1, baseline must be at least this bad, or the check is not running + * maxCandidateViolations: 0, + * minChangeRatio: 0.02, + * outDir: "" defaults to /evidence + * } + */ +export async function runJob(brief, cwd) { + if (brief.job !== "review-redesign") + throw new Error( + `unsupported job "${brief.job}". This agent implements "review-redesign" only; it does not ` + + `improvise other work.`, + ); + const outDir = brief.outDir ? resolve(brief.outDir) : join(cwd || PKG, "evidence"); + const name = brief.name || "sample"; + await mkdir(outDir, { recursive: true }); + + const started = new Date(); + const steps = []; + const record = (step, detail) => { + steps.push({ step, ...detail }); + log(`${step}: ${JSON.stringify(detail)}`); + }; + + // The image pipeline is checked before anything depends on it, so a missing native library is a + // named blocker instead of a confusing failure three steps later. + const sharpInfo = checkSharp(); + record("image-pipeline", sharpInfo); + + const preview = await startPreview({ port: 0 }); + const browser = await chromium.launch(); + try { + record("preview", { origin: preview.origin, root: PKG }); + + const shots = {}; + for (const [role, path] of [ + ["baseline", brief.baseline], + ["candidate", brief.candidate], + ]) { + shots[role] = await shoot({ + url: preview.url(path), + name: `${name}-${role}`, + outDir: join(outDir, "screenshots"), + browser, + }); + record(`screenshot:${role}`, { + captures: shots[role].map((s) => ({ + viewport: s.label, + size: `${s.analysis.width}x${s.analysis.height}`, + bytes: s.bytes, + distinctColors: s.analysis.distinctColors, + inkRatio: Number(s.analysis.inkRatio.toFixed(4)), + file: s.file, + })), + }); + } + + const audits = {}; + for (const [role, path] of [ + ["baseline", brief.baseline], + ["candidate", brief.candidate], + ]) { + audits[role] = {}; + for (const viewport of Object.keys(TOKENS.viewport)) { + const r = await audit({ + url: preview.url(path), + name: `${name}-${role}`, + viewport, + outDir: join(outDir, "a11y"), + browser, + }); + audits[role][viewport] = r; + record(`axe:${role}:${viewport}`, { + engine: r.engine, + violations: r.counts.violations, + nodesFlagged: r.counts.nodesFlagged, + passes: r.counts.passes, + rulesConsidered: r.counts.rulesConsidered, + loadProof: `${r.loadProof.elements} elements / ${r.loadProof.textLength} chars`, + file: r.file, + }); + } + } + + // Gate the audit numbers against the brief's expectations. A baseline that is suddenly clean + // means the check stopped working far more often than it means the page got better. + const baseWorst = Math.max( + ...Object.values(audits.baseline).map((r) => r.counts.violations), + ); + const candWorst = Math.max( + ...Object.values(audits.candidate).map((r) => r.counts.violations), + ); + const expectBase = brief.expectBaselineViolations ?? 1; + const maxCand = brief.maxCandidateViolations ?? 0; + if (baseWorst < expectBase) + throw new Error( + `baseline reported only ${baseWorst} violations, expected at least ${expectBase} — ` + + `the accessibility check is probably not exercising the page`, + ); + if (candWorst > maxCand) + throw new Error(`candidate reported ${candWorst} violations, over the limit of ${maxCand}`); + + const diffs = {}; + for (const viewport of Object.keys(TOKENS.viewport)) { + const a = shots.baseline.find((s) => s.label === viewport).file; + const b = shots.candidate.find((s) => s.label === viewport).file; + const d = await vdiff({ + a, + b, + out: join(outDir, "diff", `${name}-${viewport}.png`), + }); + const minRatio = brief.minChangeRatio ?? 0.02; + if (d.ratio < minRatio) + throw new Error( + `${viewport}: only ${(d.ratio * 100).toFixed(2)}% of pixels changed (need ` + + `${(minRatio * 100).toFixed(2)}%) — the redesign did not take effect`, + ); + diffs[viewport] = d; + record(`vdiff:${viewport}`, { + changed: d.changed, + total: d.total, + ratio: Number(d.ratio.toFixed(4)), + file: d.diffFile, + }); + } + + const mark = await rasterise({ + input: join(PKG, "design-identity", "mark.svg"), + output: join(outDir, "images", `${name}-mark.png`), + width: 480, + }); + record("raster", { output: mark.output, bytes: mark.bytes, size: `${mark.analysis.width}x${mark.analysis.height}` }); + + // Evidence manifest: every artifact, its size and its digest, so a reviewer can check that the + // files described are the files present. + const artifacts = [ + ...Object.values(shots).flat().map((s) => s.file), + ...Object.values(audits).flatMap((byVp) => Object.values(byVp).map((r) => r.file)), + ...Object.values(diffs).map((d) => d.diffFile), + mark.output, + ]; + const manifest = []; + for (const file of artifacts) { + const buf = await readFile(file); + manifest.push({ file, bytes: buf.length, sha256: createHash("sha256").update(buf).digest("hex") }); + } + + const summary = { + agent: "ui-ux-designer", + identity: `${TOKENS.$identity} (${TOKENS.$status})`, + job: brief.job, + name, + startedAt: started.toISOString(), + finishedAt: new Date().toISOString(), + environment: { + node: process.version, + chromium: browser.version(), + axe: audits.candidate.desktop.engine, + sharp: `${sharpInfo.sharp} / libvips ${sharpInfo.vips}`, + }, + accessibility: { + baselineViolations: Object.fromEntries( + Object.entries(audits.baseline).map(([v, r]) => [v, r.counts.violations]), + ), + candidateViolations: Object.fromEntries( + Object.entries(audits.candidate).map(([v, r]) => [v, r.counts.violations]), + ), + baselineFindings: audits.baseline.desktop.violations.map((v) => `${v.id} (${v.nodeCount})`), + }, + visualChange: Object.fromEntries( + Object.entries(diffs).map(([v, d]) => [v, `${(d.ratio * 100).toFixed(2)}%`]), + ), + steps, + manifest, + }; + + const reportFile = join(outDir, `${name}-agent-result.json`); + await writeFile(reportFile, JSON.stringify(summary, null, 2) + "\n"); + summary.reportFile = reportFile; + summary.reportSha256 = await sha256(reportFile); + log(`done — report ${reportFile}`); + return summary; + } finally { + await browser.close(); + await preview.close(); + } +} + +/** Extract the brief from an ACP prompt turn: the first text block that parses as JSON. */ +export function briefFromPrompt(params) { + const blocks = params?.prompt || params?.input || []; + const texts = (Array.isArray(blocks) ? blocks : [blocks]) + .map((b) => (typeof b === "string" ? b : b?.text)) + .filter(Boolean); + for (const t of texts) { + const trimmed = t.trim(); + if (trimmed.startsWith("{")) { + try { + return JSON.parse(trimmed); + } catch { + /* fall through to the error below */ + } + } + } + throw new Error( + "prompt carried no JSON job brief. This agent takes a JSON brief " + + '({"job":"review-redesign","baseline":"...","candidate":"..."}); it does not interpret free text.', + ); +} + +function send(msg) { + process.stdout.write(JSON.stringify(msg) + "\n"); +} + +async function main() { + const state = { cwd: process.cwd(), lastResult: null }; + const rl = createInterface({ input: process.stdin, crlfDelay: Infinity }); + log(`ready — node ${process.version}, package ${PKG}`); + for await (const line of rl) { + const raw = line.trim(); + if (!raw) continue; + let msg; + try { + msg = JSON.parse(raw); + } catch { + log(`ignoring non-JSON line (${raw.length} bytes)`); + continue; + } + if (msg.id === undefined) continue; // notification: nothing to answer + try { + switch (msg.method) { + case "initialize": + send({ + jsonrpc: "2.0", + id: msg.id, + result: { + protocolVersion: PROTOCOL_VERSION, + agentInfo: { name: "maxplayer-ui-ux-designer", version: "0.1.0" }, + }, + }); + break; + case "session/new": { + const cwd = msg.params?.cwd || msg.params?.session_config?.cwd; + if (cwd) state.cwd = cwd; + send({ jsonrpc: "2.0", id: msg.id, result: { sessionId: `designer-${process.pid}` } }); + break; + } + case "session/prompt": { + const brief = briefFromPrompt(msg.params); + state.lastResult = await runJob(brief, state.cwd); + send({ jsonrpc: "2.0", id: msg.id, result: { stopReason: "end_turn" } }); + break; + } + case "session/cancel": + send({ jsonrpc: "2.0", id: msg.id, result: {} }); + break; + default: + send({ + jsonrpc: "2.0", + id: msg.id, + error: { code: -32601, message: `method not implemented: ${msg.method}` }, + }); + } + } catch (e) { + log(`ERROR ${e.message}`); + send({ jsonrpc: "2.0", id: msg.id, error: { code: -32000, message: String(e.message || e) } }); + } + } +} + +if (process.argv[1] && process.argv[1].endsWith("designer-agent.mjs")) { + await main(); +} diff --git a/sellers/ui-ux-designer/briefs/nova-status-brief.md b/sellers/ui-ux-designer/briefs/nova-status-brief.md new file mode 100644 index 00000000..ffceda25 --- /dev/null +++ b/sellers/ui-ux-designer/briefs/nova-status-brief.md @@ -0,0 +1,27 @@ +# Design brief — Nova status page + +Natural language, as a buyer would write it. **No candidate implementation is supplied**, and none +exists in this repository: the agent authors the source itself. + +--- + +We run Nova, a small hosted service. We need a **public status page** — one screen, no login, +the thing people open when they think we are down. + +It should show, at a glance: + +- whether everything is currently working, in plain words, at the top +- the state of our four components: API, Dashboard, Webhooks, Scheduled Jobs +- uptime over the last 90 days for each, as a small bar strip +- the two most recent incidents, each with a title, when it started, how long it lasted, and a + one-line summary of what happened +- when the page last refreshed + +People land here angry and in a hurry, often on a phone, sometimes on a train with one bar of +signal. Make it fast to read and impossible to misread. It has to work for someone who cannot +distinguish red from green, and for someone using a screen reader. + +Build it as a single self-contained HTML file called `status.html`. No build step, no framework, no +external network requests — inline the styles. + +Use our design tokens; do not invent a second colour system. diff --git a/sellers/ui-ux-designer/config/seller.config.toml.example b/sellers/ui-ux-designer/config/seller.config.toml.example new file mode 100644 index 00000000..8d4b967a --- /dev/null +++ b/sellers/ui-ux-designer/config/seller.config.toml.example @@ -0,0 +1,53 @@ +# Seller home for the UI/UX designer agent — EXAMPLE, copy before use. +# +# mkdir -p ~/.maxplayer-ui-ux-designer +# cp config/seller.config.toml.example ~/.maxplayer-ui-ux-designer/config.toml +# $EDITOR ~/.maxplayer-ui-ux-designer/config.toml # set the absolute agent path +# MAXPLAYER_HOME=~/.maxplayer-ui-ux-designer "$MAXPLAYER_BIN" seller +# +# SEPARATE HOME, ON PURPOSE. `MAXPLAYER_HOME` points at a directory used by this seller and nothing +# else, so its key, config and state never sit beside another seat's. Do not point this at a home +# that already holds a funded wallet. +# +# WHAT THIS FILE DELIBERATELY DOES NOT CONTAIN: no mint, no wallet path, no nsec, no relay +# credential, no rate. Those are decisions for whoever actually runs a paid seat, and this package +# was built under a hold on real sats. Adding them is a human act. + +[seller] +# The preset this node serves. Exact-or-nothing dispatch: a buyer asking for "ui-ux-designer" +# reaches this agent; a buyer asking for anything else does not. +agents = ["ui-ux-designer"] + +# NOTE ON DISCOVERABILITY — read before expecting buyers to find this seat. +# `ui-ux-designer` is a CUSTOM preset, and custom presets publish NO wire harness family. The +# closed vocabulary is claude-code | codex | cursor | goose +# (crates/maxplayer-core/src/agent_presets.rs, HARNESS_FAMILIES). A buyer filtering by harness +# family will therefore not match this seat, by design rather than by defect: an unstated family +# never satisfies a buyer that named one. Buyers reach it by naming the agent. + +# Custom agent preset. A config entry named after a built-in would override that built-in; this +# name is not a built-in, so nothing is shadowed. +[agents.ui-ux-designer] +# ABSOLUTE PATH REQUIRED — replace with your checkout location. The agent resolves its own package +# files relative to this script, so it does not care about the process cwd, but the argv entry +# itself is not resolved against anything. +argv = [ + "node", + "/ABSOLUTE/PATH/TO/sellers/ui-ux-designer/agent/designer-agent.mjs", +] + +# Least privilege, in the three places it is actually decidable here: +# 1. The agent needs PATH, HOME, and — only if your Playwright browsers live outside the default +# cache — PLAYWRIGHT_BROWSERS_PATH. It reads no API key and no token, because it calls no +# hosted service. Do not add credentials to this table. +# 2. The preview server binds 127.0.0.1 on an ephemeral port and serves the package directory +# read-only. It is up for the duration of one prompt turn. +# 3. Chromium is launched headless with no persistent profile; each capture gets a fresh context. +# +# [env] +# PLAYWRIGHT_BROWSERS_PATH = "/absolute/path/to/ms-playwright" + +# Sandboxing: if you run this seat under `[sandbox] mode = "docker"`, argv[0] stays bare and is +# resolved against the IMAGE's PATH, and the image must contain node, the package, and a Chromium +# Playwright can drive. This package has NOT been exercised inside the docker sandbox — see the +# limitations section of the report before assuming it works there. diff --git a/sellers/ui-ux-designer/design-identity/IDENTITY.md b/sellers/ui-ux-designer/design-identity/IDENTITY.md new file mode 100644 index 00000000..8b4d0db0 --- /dev/null +++ b/sellers/ui-ux-designer/design-identity/IDENTITY.md @@ -0,0 +1,128 @@ +# Plainsong — the default design identity + +## 0. Status: PROPOSED, not adopted + +This is **one explicit proposal**, written down so the seller has a real default instead of +improvising a new look per job. It has not been adopted by any product, has not been reviewed by a +human designer, and carries no brand authority. A buyer may override any token; when a buyer +supplies their own system, theirs wins and this file is inert. + +Marked proposed in three places on purpose: here, in `tokens.json` (`$status`), and in the +generated `tokens.css` header. If you find it presented as an adopted house style anywhere, that is +a bug. + +Source of truth is `design-identity/tokens.json`. `tokens.css` and `contrast.md` are **generated** +from it by `node tools/tokens.mjs`; never hand-edit those two. + +## 1. The idea in one paragraph + +Plainsong is a quiet, text-first identity for tools people use all day: near-black ink on near-white +paper, exactly one accent that means "you can act here", generous line height, a 4px grid, and +motion short enough that nobody waits for it. It is deliberately unfashionable — no gradients on +text, no glass, no hero video. The taste it expresses is that legibility outranks novelty, and that +an interface which is boring on the tenth day is better than one that is delightful on the first. + +## 2. Typography + +Sans: `"Inter var", Inter, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", +Arial, sans-serif`. Mono: `"IBM Plex Mono", ui-monospace, SFMono-Regular, Menlo, Consolas, +monospace`. **No font binaries are bundled** — both named faces are SIL OFL 1.1 (see +`REFERENCES.md`) and the stack degrades to the system UI face, so the package ships no font +licensing surface at all. + +Scale is a 1.25 major third off a 16px base, in `rem` so browser zoom and user font-size settings +work: + +| step | rem | ≈px | line-height | tracking | used for | +|---|---|---|---|---|---| +| xs | 0.75 | 12 | 1.5 | +0.01em | legal, timestamps, table meta — never body copy | +| sm | 0.875 | 14 | 1.5 | +0.005em | secondary text, form help, dense tables | +| base | 1 | 16 | 1.6 | 0 | **body default; never go below this for prose** | +| lg | 1.25 | 20 | 1.5 | −0.005em | lead paragraph, card title | +| xl | 1.5625 | 25 | 1.4 | −0.01em | section heading (h3) | +| 2xl | 1.953 | 31 | 1.3 | −0.015em | page subsection (h2) | +| 3xl | 2.441 | 39 | 1.2 | −0.02em | page title (h1) | +| 4xl | 3.052 | 49 | 1.1 | −0.025em | marketing hero only | + +Weights: 400 body, 500 UI labels and buttons, 600 headings, 700 sparingly. Line length **45–80 +characters, 68 ideal** (`--measure`). Tracking tightens as size grows because optical spacing that +suits 16px looks loose at 39px; that is why the table carries a tracking column at all. + +## 3. Color + +Full measured table with real ratios: **[`contrast.md`](contrast.md)** — generated, 13 contracted +pairs, all passing at the time of writing. Highlights: body ink `ink-900` on `paper-000` is +**17.82:1**; the primary button (`paper-000` on `accent-600`) is **5.82:1**; the link color +`accent-700` on paper is **8.46:1**; the focus ring is **5.82:1** against the page. + +Roles, not decoration: + +- **ink-900/700/600** — text. `ink-500` is the floor for *large* text and non-text UI only; it is + 5.93:1 here, so it is safe for body too, but the role stays "de-emphasised". +- **ink-300** — non-text boundaries (input borders, dividers that carry meaning). It is exactly the + token that failed the first time this ledger was computed at 2.59:1 and was darkened from + `#98A2AE` to `#868F9C` to clear WCAG 2.2 SC 1.4.11 at **3.27:1**. That failure is the reason the + contrast script exits nonzero rather than printing a table nobody reads. +- **paper-000/100/200/300** — surfaces, lightest at the back. +- **accent-600/700** — the *only* interactive color. If everything is accented, nothing is. +- **success / warn / danger** — status, always paired with an icon and a word (see Rule 3). + +## 4. Spacing, radius, elevation + +4px base unit. Allowed steps: 0, 2, 4, 8, 12, 16, 20, 24, 32, 40, 48, 64, 80, 96, 128. Anything off +the grid is a mistake, not a nuance. Rhythm: 8px inside a control, 16px between related controls, +24–32px between groups, 48–64px between page sections. + +Radius: 2 (chips) / 4 (inputs, buttons) / 8 (cards) / 16 (sheets) / pill (badges only). Pick one per +component and stay there; nested mismatched radii look broken. + +Elevation is three shadows, all low-contrast and neutral-tinted. Shadow is never the only signal for +an interactive boundary — a border carries it, because shadows vanish in forced-colors mode. + +## 5. Motion + +Durations: instant 0 / fast **120ms** (hover, focus, color) / base **180ms** (most enter-exit) / +slow **260ms** (sheets, expanding panels) / deliberate **400ms** (page-level, rare). +Easings: standard `cubic-bezier(0.2,0,0,1)`, enter `cubic-bezier(0,0,0,1)`, exit +`cubic-bezier(0.3,0,1,1)`. Max travel **24px** — things fade and shift, they do not fly. + +Under `prefers-reduced-motion: reduce`, every duration token collapses to 1ms and transforms are +dropped. This is compiled into `tokens.css` by the generator, so honouring it is the default rather +than a thing someone remembers to add. + +## 6. Accessibility rules (non-negotiable in this identity) + +1. Contrast: **4.5:1** body text, **3:1** large text (≥24px, or ≥19px bold) and non-text UI. Every + documented pair is machine-checked; see Rule 12. +2. Focus is **always visible**: 2px solid ring, 2px offset, ≥3:1 against its background. + `outline: none` without a replacement indicator is a defect. +3. Never color alone: status uses color **+** icon **+** text. +4. Targets ≥ **24×24px** (WCAG 2.2 SC 2.5.8), 44px recommended for touch, ≥8px between adjacent + targets. +5. Every control has a programmatic name. Placeholders are not labels. +6. One `h1`; heading levels never skip. Landmarks on every region. A skip link first in tab order. +7. Text reflows and remains readable at 200% zoom and at 320px width — no horizontal scroll. +8. `prefers-reduced-motion` respected; nothing flashes more than 3×/second. +9. `lang` on ``; page `` is unique and describes the page. +10. Keyboard reaches everything, in DOM order, and can get back out of every trap (Esc closes). +11. Images: meaningful ones get real alt text, decorative ones get `alt=""`. Icon-only buttons get + an accessible name. +12. **Automated checks are a floor, not a ceiling.** axe-core finds roughly a third of real issues; + the remaining rules above are reviewed by hand and stated as reviewed, never as "axe passed". + +## 7. Anti-patterns (named, so they can be called out) + +Ghost buttons at 2.8:1 · placeholder-as-label · `outline: none` with nothing in its place · +`<div onclick>` instead of `<button>` · icon-only navigation with no accessible name · tap targets +under 24px · disabled controls with no explanation of what would enable them · color-only status +dots · autoplaying carousels · parallax with no reduced-motion fallback · modals without focus trap +or Esc · justified body text and all-caps sentences · text over photography with no scrim · +`px` font sizes that defeat browser zoom · infinite scroll over a footer people need · +"click here" links · toast-only error reporting for a form field · 12px body copy. + +## 8. How the seller uses this + +The agent reads this file and `knowledge/INDEX.md` before proposing a redesign, applies +`tokens.css`, then **proves** the result: renders desktop and mobile, screenshots both, runs axe, +and diffs before/after. A redesign that cannot be screenshotted and checked is not delivered. The +rules in §6 that a machine cannot check are reported as hand-reviewed, by name, in the rationale. diff --git a/sellers/ui-ux-designer/design-identity/REFERENCES.md b/sellers/ui-ux-designer/design-identity/REFERENCES.md new file mode 100644 index 00000000..d3eed948 --- /dev/null +++ b/sellers/ui-ux-designer/design-identity/REFERENCES.md @@ -0,0 +1,73 @@ +# Reference corpus — source, version, licensing + +Everything the Plainsong identity and this seller's judgement lean on, with where it came from and +what its licence permits. **Nothing in this package is scraped, and no third-party asset is +redistributed here.** Design guidance below is *consulted*, not copied: the tokens in +`tokens.json` are authored for this package. + +## A. Code actually installed in this package + +Versions and SPDX identifiers below were read out of `node_modules/<pkg>/package.json` on this +machine, not recalled from memory. + +| Package | Version | Licence | Role | +|---|---|---|---| +| `playwright` | 1.62.0 | Apache-2.0 | browser automation, screenshots | +| `playwright-core` | 1.62.0 | Apache-2.0 | pinned by `overrides` to a single copy | +| `axe-core` | 4.11.2 | MPL-2.0 | accessibility rule engine | +| `@axe-core/playwright` | 4.11.2 | MPL-2.0 | axe injection into a Playwright page | +| `pixelmatch` | 6.0.0 | ISC | visual comparison | +| `pngjs` | 7.0.0 | MIT | PNG decode for pixel assertions | +| `sharp` | 0.34.5 | Apache-2.0 (libvips 8.17.3, LGPL-3.0) | SVG → raster, image resizing | + +Browser binary: **Google Chrome for Testing 151.0.7922.34**, Playwright revision **1234**, from the +shared `~/Library/Caches/ms-playwright` cache. Chrome for Testing is Google's binary under its own +terms; it is *used*, never redistributed by this package, and nothing here vendors it. + +MPL-2.0 (axe-core) is file-level copyleft: we call it as an unmodified dependency, which imposes no +obligation on this package's own source. Nothing here is statically linked into axe. + +## B. Normative accessibility sources + +| Source | Version / date | Licence | How used | +|---|---|---|---| +| WCAG 2.2 (W3C Recommendation) | 5 Oct 2023, latest errata 12 Dec 2024 | W3C Document Licence | the success criteria named in IDENTITY.md §6 (1.4.3, 1.4.11, 2.4.7, 2.5.8, 1.4.10, 1.4.4, 2.3.1) | +| WAI-ARIA Authoring Practices Guide (APG) | 1.2 patterns | W3C Software & Document Notice (BSD-3-Clause for code) | keyboard interaction expectations per widget | +| HTML Living Standard | WHATWG, continuously updated | CC-BY 4.0 | semantic element choice | +| MDN Web Docs | continuously updated | prose CC-BY-SA 2.5, code CC0 | API and CSS behaviour details | + +W3C Document Licence permits reference and quotation, **not** derivative redistribution — so this +package cites criteria by number and never reproduces WCAG text wholesale. + +## C. Design-system guidance consulted (ideas, not assets) + +| Source | Version | Licence | What was taken | +|---|---|---|---| +| GOV.UK Design System | v5.x | code MIT, docs Open Government Licence v3 | plain-language error patterns; one-thing-per-page forms | +| U.S. Web Design System (USWDS) | v3.x | public domain (US Gov work) / CC0 | target-size and focus-visibility discipline | +| Material Design 3 — motion | M3, 2024+ | docs CC-BY 4.0, code Apache-2.0 | the idea of short standard easing curves; **our durations and beziers are our own numbers** | +| Refactoring UI (Wathan & Schoger) | 1st ed. 2019 | commercial book, no reuse | influence only: spacing rhythm, de-emphasis by weight not size. No text or asset reused. | + +## D. Typefaces — named, never bundled + +| Face | Version | Licence | Status here | +|---|---|---|---| +| Inter | 4.x | SIL Open Font Licence 1.1 | named first in the sans stack; **no font file ships in this package** | +| IBM Plex Mono | 6.x | SIL Open Font Licence 1.1 | named first in the mono stack; not bundled | + +Both degrade to the platform UI face, so a machine without them renders correctly with system +fonts. If a buyer wants Inter actually served, they self-host it under OFL 1.1 with its licence +file — that is their deployment decision, not ours, and it is outside this package's scope. + +## E. Sample content + +`samples/redesign/` is written from scratch for this package: invented product name, invented copy, +no customer data, no third-party logo, no stock photography. Icons are inline SVG authored here. +Public-domain / self-authored throughout, so the before/after screenshots can be shared without any +clearance. + +## F. What is deliberately absent + +No icon-font, no UI kit, no Figma export, no scraped screenshot corpus, no paid asset library, no +model weights. If a job needs one of those, it is a buyer-supplied input or a blocker to report — +not something this seller acquires. (The fold forbids paid services and new subscriptions.) diff --git a/sellers/ui-ux-designer/design-identity/contrast.md b/sellers/ui-ux-designer/design-identity/contrast.md new file mode 100644 index 00000000..02d0cbee --- /dev/null +++ b/sellers/ui-ux-designer/design-identity/contrast.md @@ -0,0 +1,27 @@ +# Contrast ledger — GENERATED + +Every number below is computed by `node tools/tokens.mjs --contrast` using the WCAG 2.x +relative-luminance formula over `design-identity/tokens.json`. Nothing here is typed by hand. +Regenerate after any color change; `scripts/smoke.sh` reruns it and fails the gate on a miss. + +Identity: **Plainsong** (PROPOSED — not adopted. See design-identity/IDENTITY.md §0.) +Generated by tools/tokens.mjs from tokens.json v0.1.0. + +``` +foreground background hex fg hex bg ratio min verdict use +ink-900 paper-000 #14181D #FFFFFF 17.82 4.5 PASS body text on page +ink-900 paper-100 #14181D #F7F8FA 16.77 4.5 PASS body text on subtle surface +ink-700 paper-000 #333B45 #FFFFFF 11.34 4.5 PASS secondary body text +ink-600 paper-000 #465162 #FFFFFF 8.03 4.5 PASS muted body text (floor case) +ink-500 paper-000 #5A6572 #FFFFFF 5.93 3 PASS large text / non-text UI only +paper-000 accent-600 #FFFFFF #1F5FD0 5.82 4.5 PASS text on primary button +accent-700 paper-000 #17499E #FFFFFF 8.46 4.5 PASS link text on page +accent-700 accent-100 #17499E #E4EDFC 7.18 4.5 PASS link text on accent surface +paper-000 success-600 #FFFFFF #14714A 6.02 4.5 PASS text on success fill +paper-000 warn-600 #FFFFFF #8A5A00 5.93 4.5 PASS text on warning fill +paper-000 danger-600 #FFFFFF #B3251E 6.56 4.5 PASS text on danger fill +focus-ring paper-000 #1F5FD0 #FFFFFF 5.82 3 PASS focus indicator vs page (WCAG 2.2 SC 1.4.11) +ink-300 paper-000 #868F9C #FFFFFF 3.27 3 PASS input border / non-text boundary +``` + +All 13 contracted pairs meet their stated minimum. diff --git a/sellers/ui-ux-designer/design-identity/mark.svg b/sellers/ui-ux-designer/design-identity/mark.svg new file mode 100644 index 00000000..66e4b76c --- /dev/null +++ b/sellers/ui-ux-designer/design-identity/mark.svg @@ -0,0 +1,25 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- Authored for this package. Plainsong mark: four staff lines and a rising figure, drawn on the + 4px grid, in accent-600 / ink-900 / ink-300 from tokens.json. No third-party artwork. --> +<svg xmlns="http://www.w3.org/2000/svg" width="240" height="96" viewBox="0 0 240 96" role="img" + aria-labelledby="mark-title"> + <title id="mark-title">Plainsong + + + + + + + + + + + + + + + + + + diff --git a/sellers/ui-ux-designer/design-identity/tokens.css b/sellers/ui-ux-designer/design-identity/tokens.css new file mode 100644 index 00000000..fdba7b44 --- /dev/null +++ b/sellers/ui-ux-designer/design-identity/tokens.css @@ -0,0 +1,105 @@ +/* GENERATED by tools/tokens.mjs --css from design-identity/tokens.json. Do not hand-edit. */ +/* Identity: Plainsong — PROPOSED — not adopted. See design-identity/IDENTITY.md §0. */ +:root { + --color-ink-900: #14181D; + --color-ink-700: #333B45; + --color-ink-600: #465162; + --color-ink-500: #5A6572; + --color-ink-300: #868F9C; + --color-ink-200: #C2C9D2; + --color-ink-100: #D9DEE4; + --color-paper-000: #FFFFFF; + --color-paper-100: #F7F8FA; + --color-paper-200: #EDEFF3; + --color-paper-300: #E1E5EB; + --color-accent-700: #17499E; + --color-accent-600: #1F5FD0; + --color-accent-300: #8FB3EE; + --color-accent-100: #E4EDFC; + --color-success-700: #0F5A3B; + --color-success-600: #14714A; + --color-warn-700: #6E4700; + --color-warn-600: #8A5A00; + --color-danger-700: #8E1D17; + --color-danger-600: #B3251E; + --color-focus-ring: #1F5FD0; + --font-sans: "Inter var", Inter, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; + --font-mono: "IBM Plex Mono", ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; + --text-xs: 0.75rem; + --leading-xs: 1.5; + --tracking-xs: 0.01em; + --text-sm: 0.875rem; + --leading-sm: 1.5; + --tracking-sm: 0.005em; + --text-base: 1rem; + --leading-base: 1.6; + --tracking-base: 0; + --text-lg: 1.25rem; + --leading-lg: 1.5; + --tracking-lg: -0.005em; + --text-xl: 1.5625rem; + --leading-xl: 1.4; + --tracking-xl: -0.01em; + --text-2xl: 1.953rem; + --leading-2xl: 1.3; + --tracking-2xl: -0.015em; + --text-3xl: 2.441rem; + --leading-3xl: 1.2; + --tracking-3xl: -0.02em; + --text-4xl: 3.052rem; + --leading-4xl: 1.1; + --tracking-4xl: -0.025em; + --weight-regular: 400; + --weight-medium: 500; + --weight-semibold: 600; + --weight-bold: 700; + --space-0: 0px; + --space-1: 4px; + --space-2: 8px; + --space-3: 12px; + --space-4: 16px; + --space-5: 20px; + --space-6: 24px; + --space-8: 32px; + --space-10: 40px; + --space-12: 48px; + --space-16: 64px; + --space-20: 80px; + --space-24: 96px; + --space-32: 128px; + --space-0_5: 2px; + --radius-none: 0px; + --radius-sm: 2px; + --radius-md: 4px; + --radius-lg: 8px; + --radius-xl: 16px; + --radius-pill: 999px; + --shadow-sm: 0 1px 2px rgba(20,24,29,0.06), 0 1px 1px rgba(20,24,29,0.04); + --shadow-md: 0 2px 6px rgba(20,24,29,0.08), 0 1px 2px rgba(20,24,29,0.06); + --shadow-lg: 0 8px 24px rgba(20,24,29,0.12), 0 2px 6px rgba(20,24,29,0.06); + --duration-instant: 0ms; + --duration-fast: 120ms; + --duration-base: 180ms; + --duration-slow: 260ms; + --duration-deliberate: 400ms; + --ease-standard: cubic-bezier(0.2, 0, 0, 1); + --ease-enter: cubic-bezier(0, 0, 0, 1); + --ease-exit: cubic-bezier(0.3, 0, 1, 1); + --ease-linear: linear; + --target-min: 24px; + --target-recommended: 44px; + --focus-ring-width: 2px; + --focus-ring-offset: 2px; + --measure: 68ch; +} + +/* Rule 8 of IDENTITY.md is a token, not a suggestion. */ +@media (prefers-reduced-motion: reduce) { + :root { + --duration-instant: 1ms; + --duration-fast: 1ms; + --duration-base: 1ms; + --duration-slow: 1ms; + --duration-deliberate: 1ms; + } +} diff --git a/sellers/ui-ux-designer/design-identity/tokens.json b/sellers/ui-ux-designer/design-identity/tokens.json new file mode 100644 index 00000000..20a8d2cb --- /dev/null +++ b/sellers/ui-ux-designer/design-identity/tokens.json @@ -0,0 +1,90 @@ +{ + "$identity": "Plainsong", + "$status": "PROPOSED — not adopted. See design-identity/IDENTITY.md §0.", + "$version": "0.1.0", + "color": { + "ink-900": "#14181D", + "ink-700": "#333B45", + "ink-600": "#465162", + "ink-500": "#5A6572", + "ink-300": "#868F9C", + "ink-200": "#C2C9D2", + "ink-100": "#D9DEE4", + "paper-000": "#FFFFFF", + "paper-100": "#F7F8FA", + "paper-200": "#EDEFF3", + "paper-300": "#E1E5EB", + "accent-700": "#17499E", + "accent-600": "#1F5FD0", + "accent-300": "#8FB3EE", + "accent-100": "#E4EDFC", + "success-700": "#0F5A3B", + "success-600": "#14714A", + "warn-700": "#6E4700", + "warn-600": "#8A5A00", + "danger-700": "#8E1D17", + "danger-600": "#B3251E", + "focus-ring": "#1F5FD0" + }, + "contrastContract": [ + { "pair": ["ink-900", "paper-000"], "min": 4.5, "use": "body text on page" }, + { "pair": ["ink-900", "paper-100"], "min": 4.5, "use": "body text on subtle surface" }, + { "pair": ["ink-700", "paper-000"], "min": 4.5, "use": "secondary body text" }, + { "pair": ["ink-600", "paper-000"], "min": 4.5, "use": "muted body text (floor case)" }, + { "pair": ["ink-500", "paper-000"], "min": 3.0, "use": "large text / non-text UI only" }, + { "pair": ["paper-000", "accent-600"], "min": 4.5, "use": "text on primary button" }, + { "pair": ["accent-700", "paper-000"], "min": 4.5, "use": "link text on page" }, + { "pair": ["accent-700", "accent-100"], "min": 4.5, "use": "link text on accent surface" }, + { "pair": ["paper-000", "success-600"], "min": 4.5, "use": "text on success fill" }, + { "pair": ["paper-000", "warn-600"], "min": 4.5, "use": "text on warning fill" }, + { "pair": ["paper-000", "danger-600"], "min": 4.5, "use": "text on danger fill" }, + { "pair": ["focus-ring", "paper-000"], "min": 3.0, "use": "focus indicator vs page (WCAG 2.2 SC 1.4.11)" }, + { "pair": ["ink-300", "paper-000"], "min": 3.0, "use": "input border / non-text boundary" } + ], + "type": { + "familySans": "\"Inter var\", Inter, -apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, \"Helvetica Neue\", Arial, sans-serif", + "familyMono": "\"IBM Plex Mono\", ui-monospace, SFMono-Regular, Menlo, Consolas, monospace", + "baseSizePx": 16, + "ratio": 1.25, + "scale": { + "xs": { "rem": 0.75, "px": 12, "lineHeight": 1.5, "tracking": "0.01em" }, + "sm": { "rem": 0.875, "px": 14, "lineHeight": 1.5, "tracking": "0.005em" }, + "base": { "rem": 1, "px": 16, "lineHeight": 1.6, "tracking": "0" }, + "lg": { "rem": 1.25, "px": 20, "lineHeight": 1.5, "tracking": "-0.005em" }, + "xl": { "rem": 1.5625, "px": 25, "lineHeight": 1.4, "tracking": "-0.01em" }, + "2xl": { "rem": 1.953, "px": 31, "lineHeight": 1.3, "tracking": "-0.015em" }, + "3xl": { "rem": 2.441, "px": 39, "lineHeight": 1.2, "tracking": "-0.02em" }, + "4xl": { "rem": 3.052, "px": 49, "lineHeight": 1.1, "tracking": "-0.025em" } + }, + "weight": { "regular": 400, "medium": 500, "semibold": 600, "bold": 700 }, + "measureCh": { "min": 45, "ideal": 68, "max": 80 } + }, + "space": { + "unitPx": 4, + "scale": { "0": 0, "0.5": 2, "1": 4, "2": 8, "3": 12, "4": 16, "5": 20, "6": 24, "8": 32, "10": 40, "12": 48, "16": 64, "20": 80, "24": 96, "32": 128 } + }, + "radius": { "none": 0, "sm": 2, "md": 4, "lg": 8, "xl": 16, "pill": 999 }, + "shadow": { + "sm": "0 1px 2px rgba(20,24,29,0.06), 0 1px 1px rgba(20,24,29,0.04)", + "md": "0 2px 6px rgba(20,24,29,0.08), 0 1px 2px rgba(20,24,29,0.06)", + "lg": "0 8px 24px rgba(20,24,29,0.12), 0 2px 6px rgba(20,24,29,0.06)" + }, + "motion": { + "durationMs": { "instant": 0, "fast": 120, "base": 180, "slow": 260, "deliberate": 400 }, + "easing": { + "standard": "cubic-bezier(0.2, 0, 0, 1)", + "enter": "cubic-bezier(0, 0, 0, 1)", + "exit": "cubic-bezier(0.3, 0, 1, 1)", + "linear": "linear" + }, + "reducedMotionDurationMs": 1, + "maxTravelPx": 24 + }, + "target": { "minPx": 24, "recommendedPx": 44, "minGapPx": 8 }, + "focus": { "ringWidthPx": 2, "ringOffsetPx": 2, "style": "solid" }, + "breakpointPx": { "sm": 480, "md": 768, "lg": 1024, "xl": 1280 }, + "viewport": { + "desktop": { "width": 1280, "height": 900, "deviceScaleFactor": 1 }, + "mobile": { "width": 390, "height": 844, "deviceScaleFactor": 2 } + } +} diff --git a/sellers/ui-ux-designer/evidence/MANIFEST.md b/sellers/ui-ux-designer/evidence/MANIFEST.md new file mode 100644 index 00000000..24f3306b --- /dev/null +++ b/sellers/ui-ux-designer/evidence/MANIFEST.md @@ -0,0 +1,36 @@ +# Evidence manifest — GENERATED + +Written by `tools/verify-manifest.mjs`, which re-reads every file from disk after the agent +run finished. Sizes and digests below were recomputed here, not copied from the agent's own +report. + +- agent: `ui-ux-designer` · identity Plainsong (PROPOSED — not adopted. See design-identity/IDENTITY.md §0.) +- job: `review-redesign` (meridian-invoices) +- run: 2026-09-09T18:02:01.202Z → 2026-09-09T18:02:07.230Z +- node v26.5.0 · Chromium 151.0.7922.34 · axe-core 4.11.4 · sharp 0.34.5 / libvips 8.17.3 + +## Results + +- baseline violations: {"desktop":4,"mobile":4} +- candidate violations: {"desktop":0,"mobile":0} +- baseline findings: button-name (1), color-contrast (34), html-has-lang (1), image-alt (1) +- visual change: {"desktop":"8.51%","mobile":"8.25%"} + +## Artifacts + +``` +status bytes sha256 file +OK 60634 5d40fc37979617c2872825b9526af59aee9d065f55d14cb9505ca3f9b7f31a73 evidence/screenshots/meridian-invoices-baseline-desktop.png +OK 48998 8d5d3acf13bcce74b051d07a4c2e92c89e2190a32682438a84b5deef1d17f5e1 evidence/screenshots/meridian-invoices-baseline-mobile.png +OK 93810 93d705a22651530527357a995039199518819fd487e634a3d06f0cc87be9b8b1 evidence/screenshots/meridian-invoices-candidate-desktop.png +OK 125764 a15b9d35add1c328d21a042ef4e27397bf3614f6ef4cc6f841322e13241e47ad evidence/screenshots/meridian-invoices-candidate-mobile.png +OK 2239 fee78452372c394cd6a5d2d6a7366af846636b79cdf5297d1b09b3c832cabe73 evidence/a11y/meridian-invoices-baseline-desktop.json +OK 2238 3f2fb5cad655b8028cbaca2cfa5769e1ce57cf4bfd74533f754f505f92466ae8 evidence/a11y/meridian-invoices-baseline-mobile.json +OK 728 64d168666ff484b803a050ddce0c7bab0430b446587f5fb6465a457fffafb9cc evidence/a11y/meridian-invoices-candidate-desktop.json +OK 751 d383cac8d43860e12db9669877563ce599425570b14adf72c110660652ba4fbb evidence/a11y/meridian-invoices-candidate-mobile.json +OK 94671 96fd4dffb38b9a2ef8c4800be1d97de9dd9bcb36a2ce0de7479d3403c4b9eb89 evidence/diff/meridian-invoices-desktop.png +OK 95776 c69b99570211b4efca5ce0d05c437288d9799f6dc57ecca674fd0a1c0524b6fd evidence/diff/meridian-invoices-mobile.png +OK 4475 dfd2cf14ca7231b4bfede0457fddfe5d8ffcdf1ecf60410903b616ae64d3d72c evidence/images/meridian-invoices-mark.png +``` + +All 11 artifacts verified on disk. diff --git a/sellers/ui-ux-designer/evidence/RUNTIME.md b/sellers/ui-ux-designer/evidence/RUNTIME.md new file mode 100644 index 00000000..ce42b41b --- /dev/null +++ b/sellers/ui-ux-designer/evidence/RUNTIME.md @@ -0,0 +1,80 @@ +# Authorized runtime — what was probed, and what answered + +The verdict against `e72e9bd` required integration with a supported coding/model harness using the +**existing authorized runtime only**, and a concrete BLOCKED if none was available. This file +records what was actually probed, first-hand, rather than what was found on `PATH`. + +Presence on `PATH` is not availability. Each adapter below was **driven** over ACP stdio +(`initialize` → `session/new` → `session/prompt`) and judged on whether a model answered. + +## Adapters probed + +| Adapter | On PATH | Handshake | Session | Model answered | Verdict | +|---|---|---|---|---|---| +| `/opt/homebrew/bin/claude-agent-acp` | yes | protocol 1, OK | OK | **no** | refused — org spend limit | +| `/opt/homebrew/bin/codex-acp` | yes | protocol 1, OK | OK | **yes** | **usable** | +| `claude-code-acp`, `cursor-agent`, `cursor-agent-acp`, `goose` | no | — | — | — | absent | + +### claude-agent-acp — refused, twice + +Handshake and session creation both succeed; the refusal is at prompt time: + +``` +{"code":-32603,"data":{"errorKind":"rate_limit"}, + "message":"Internal error: You've hit your org's monthly spend limit ..."} +``` + +Probed twice, roughly an hour apart, with the same result — a standing account condition, not a +transient blip. An earlier probe in this same lane *did* return `PONG` with `stopReason: end_turn`, +so the adapter itself is sound; the credit ceiling is what closed. This adapter is therefore **not** +the harness the gate depends on. + +### codex-acp — usable + +`agentInfo: @agentclientprotocol/codex-acp 1.1.7`. Session offers `gpt-5.6-sol` at low/medium +reasoning. The prompt returned: + +``` +stopReason: end_turn +usage: {inputTokens: 3463, cachedReadTokens: 11264, outputTokens: 6, totalTokens: 14733} +``` + +Non-zero output tokens against a real quota — a model produced that text. + +## The maxplayer local driver path + +The harness is not spoken to directly by the gate. It is driven **through the actual maxplayer local +driver**, built from this very commit: + +``` +maxplayer 0.5.8 (e72e9bd7114fe86412b62ff68a08c28c5602d396) +``` + +The stock `/opt/homebrew/bin/maxplayer` (0.1.0-rc.3) refuses this path outright — +`maxplayer run requires rebuilding with the acp feature` — so the binary is built from source: + +``` +cargo build -p maxplayer --features acp --release +``` + +Proof the driver reaches a model and the model acts on the filesystem: + +``` +maxplayer run --agent-command /opt/homebrew/bin/codex-acp \ + --task "Create a file named hello.txt ... containing exactly the word PONG." \ + --cwd --log /events.jsonl --job-id probe-codex-1 \ + --permission-policy allow --idle-timeout 240 +``` + +- exit `0` +- `hello.txt` on disk containing `PONG` — written by the model, not by this repository +- 18 events in the JSONL log, ending `{"type":"turn_ended","data":"completed"}` + +No relay, no wallet, no sats, no deployment: `maxplayer run` is the local driver path only. + +## What this means for the correction + +The deterministic reviewer that drew the verdict is retained, but **demoted to tooling** — it +inspects, it does not design. The designer is now the model harness above, reached through +`maxplayer run`. Substituting deterministic execution here is precisely the failure being +corrected, so it is not done. diff --git a/sellers/ui-ux-designer/evidence/a11y/after-desktop.json b/sellers/ui-ux-designer/evidence/a11y/after-desktop.json new file mode 100644 index 00000000..1a84b4a2 --- /dev/null +++ b/sellers/ui-ux-designer/evidence/a11y/after-desktop.json @@ -0,0 +1,33 @@ +{ + "name": "after", + "url": "http://127.0.0.1:8791/samples/redesign/after/", + "viewport": "desktop", + "engine": "axe-core 4.11.4", + "tags": [ + "wcag2a", + "wcag2aa", + "wcag21a", + "wcag21aa", + "wcag22aa" + ], + "generatedAt": "2026-09-09T04:10:04.038Z", + "loadProof": { + "title": "Invoices — Meridian", + "elements": 103, + "textLength": 1045, + "stylesheets": 3, + "url": "http://127.0.0.1:8791/samples/redesign/after/", + "rulesConsidered": 62, + "httpStatus": 200 + }, + "counts": { + "violations": 0, + "passes": 29, + "incomplete": 0, + "inapplicable": 33, + "rulesConsidered": 62, + "nodesFlagged": 0 + }, + "violations": [], + "incompleteIds": [] +} diff --git a/sellers/ui-ux-designer/evidence/a11y/before-desktop.json b/sellers/ui-ux-designer/evidence/a11y/before-desktop.json new file mode 100644 index 00000000..a5833121 --- /dev/null +++ b/sellers/ui-ux-designer/evidence/a11y/before-desktop.json @@ -0,0 +1,93 @@ +{ + "name": "before", + "url": "http://127.0.0.1:8791/samples/redesign/before/", + "viewport": "desktop", + "engine": "axe-core 4.11.4", + "tags": [ + "wcag2a", + "wcag2aa", + "wcag21a", + "wcag21aa", + "wcag22aa" + ], + "generatedAt": "2026-09-09T04:10:03.063Z", + "loadProof": { + "title": "Meridian", + "elements": 70, + "textLength": 698, + "stylesheets": 2, + "url": "http://127.0.0.1:8791/samples/redesign/before/", + "rulesConsidered": 64, + "httpStatus": 200 + }, + "counts": { + "violations": 4, + "passes": 13, + "incomplete": 0, + "inapplicable": 47, + "rulesConsidered": 64, + "nodesFlagged": 37 + }, + "violations": [ + { + "id": "button-name", + "impact": "critical", + "help": "Buttons must have discernible text", + "helpUrl": "https://dequeuniversity.com/rules/axe/4.11/button-name?application=playwright", + "tags": [ + "wcag2a", + "wcag412" + ], + "nodeCount": 1, + "sampleTargets": [ + ".icobtn" + ] + }, + { + "id": "color-contrast", + "impact": "serious", + "help": "Elements must meet minimum color contrast ratio thresholds", + "helpUrl": "https://dequeuniversity.com/rules/axe/4.11/color-contrast?application=playwright", + "tags": [ + "wcag2aa", + "wcag143" + ], + "nodeCount": 34, + "sampleTargets": [ + "a:nth-child(1)", + "a:nth-child(2)", + "a:nth-child(3)", + "a:nth-child(4)" + ] + }, + { + "id": "html-has-lang", + "impact": "serious", + "help": " element must have a lang attribute", + "helpUrl": "https://dequeuniversity.com/rules/axe/4.11/html-has-lang?application=playwright", + "tags": [ + "wcag2a", + "wcag311" + ], + "nodeCount": 1, + "sampleTargets": [ + "html" + ] + }, + { + "id": "image-alt", + "impact": "critical", + "help": "Images must have alternative text", + "helpUrl": "https://dequeuniversity.com/rules/axe/4.11/image-alt?application=playwright", + "tags": [ + "wcag2a", + "wcag111" + ], + "nodeCount": 1, + "sampleTargets": [ + "img" + ] + } + ], + "incompleteIds": [] +} diff --git a/sellers/ui-ux-designer/evidence/a11y/meridian-invoices-baseline-desktop.json b/sellers/ui-ux-designer/evidence/a11y/meridian-invoices-baseline-desktop.json new file mode 100644 index 00000000..eb14e169 --- /dev/null +++ b/sellers/ui-ux-designer/evidence/a11y/meridian-invoices-baseline-desktop.json @@ -0,0 +1,93 @@ +{ + "name": "meridian-invoices-baseline", + "url": "http://127.0.0.1:49613/samples/redesign/before/", + "viewport": "desktop", + "engine": "axe-core 4.11.4", + "tags": [ + "wcag2a", + "wcag2aa", + "wcag21a", + "wcag21aa", + "wcag22aa" + ], + "generatedAt": "2026-09-09T18:02:04.702Z", + "loadProof": { + "title": "Meridian", + "elements": 70, + "textLength": 698, + "stylesheets": 2, + "url": "http://127.0.0.1:49613/samples/redesign/before/", + "rulesConsidered": 64, + "httpStatus": 200 + }, + "counts": { + "violations": 4, + "passes": 13, + "incomplete": 0, + "inapplicable": 47, + "rulesConsidered": 64, + "nodesFlagged": 37 + }, + "violations": [ + { + "id": "button-name", + "impact": "critical", + "help": "Buttons must have discernible text", + "helpUrl": "https://dequeuniversity.com/rules/axe/4.11/button-name?application=playwright", + "tags": [ + "wcag2a", + "wcag412" + ], + "nodeCount": 1, + "sampleTargets": [ + ".icobtn" + ] + }, + { + "id": "color-contrast", + "impact": "serious", + "help": "Elements must meet minimum color contrast ratio thresholds", + "helpUrl": "https://dequeuniversity.com/rules/axe/4.11/color-contrast?application=playwright", + "tags": [ + "wcag2aa", + "wcag143" + ], + "nodeCount": 34, + "sampleTargets": [ + "a:nth-child(1)", + "a:nth-child(2)", + "a:nth-child(3)", + "a:nth-child(4)" + ] + }, + { + "id": "html-has-lang", + "impact": "serious", + "help": " element must have a lang attribute", + "helpUrl": "https://dequeuniversity.com/rules/axe/4.11/html-has-lang?application=playwright", + "tags": [ + "wcag2a", + "wcag311" + ], + "nodeCount": 1, + "sampleTargets": [ + "html" + ] + }, + { + "id": "image-alt", + "impact": "critical", + "help": "Images must have alternative text", + "helpUrl": "https://dequeuniversity.com/rules/axe/4.11/image-alt?application=playwright", + "tags": [ + "wcag2a", + "wcag111" + ], + "nodeCount": 1, + "sampleTargets": [ + "img" + ] + } + ], + "incompleteIds": [] +} diff --git a/sellers/ui-ux-designer/evidence/a11y/meridian-invoices-baseline-mobile.json b/sellers/ui-ux-designer/evidence/a11y/meridian-invoices-baseline-mobile.json new file mode 100644 index 00000000..aa81e172 --- /dev/null +++ b/sellers/ui-ux-designer/evidence/a11y/meridian-invoices-baseline-mobile.json @@ -0,0 +1,93 @@ +{ + "name": "meridian-invoices-baseline", + "url": "http://127.0.0.1:49613/samples/redesign/before/", + "viewport": "mobile", + "engine": "axe-core 4.11.4", + "tags": [ + "wcag2a", + "wcag2aa", + "wcag21a", + "wcag21aa", + "wcag22aa" + ], + "generatedAt": "2026-09-09T18:02:05.434Z", + "loadProof": { + "title": "Meridian", + "elements": 70, + "textLength": 698, + "stylesheets": 2, + "url": "http://127.0.0.1:49613/samples/redesign/before/", + "rulesConsidered": 64, + "httpStatus": 200 + }, + "counts": { + "violations": 4, + "passes": 13, + "incomplete": 0, + "inapplicable": 47, + "rulesConsidered": 64, + "nodesFlagged": 37 + }, + "violations": [ + { + "id": "button-name", + "impact": "critical", + "help": "Buttons must have discernible text", + "helpUrl": "https://dequeuniversity.com/rules/axe/4.11/button-name?application=playwright", + "tags": [ + "wcag2a", + "wcag412" + ], + "nodeCount": 1, + "sampleTargets": [ + ".icobtn" + ] + }, + { + "id": "color-contrast", + "impact": "serious", + "help": "Elements must meet minimum color contrast ratio thresholds", + "helpUrl": "https://dequeuniversity.com/rules/axe/4.11/color-contrast?application=playwright", + "tags": [ + "wcag2aa", + "wcag143" + ], + "nodeCount": 34, + "sampleTargets": [ + "a:nth-child(1)", + "a:nth-child(2)", + "a:nth-child(3)", + "a:nth-child(4)" + ] + }, + { + "id": "html-has-lang", + "impact": "serious", + "help": " element must have a lang attribute", + "helpUrl": "https://dequeuniversity.com/rules/axe/4.11/html-has-lang?application=playwright", + "tags": [ + "wcag2a", + "wcag311" + ], + "nodeCount": 1, + "sampleTargets": [ + "html" + ] + }, + { + "id": "image-alt", + "impact": "critical", + "help": "Images must have alternative text", + "helpUrl": "https://dequeuniversity.com/rules/axe/4.11/image-alt?application=playwright", + "tags": [ + "wcag2a", + "wcag111" + ], + "nodeCount": 1, + "sampleTargets": [ + "img" + ] + } + ], + "incompleteIds": [] +} diff --git a/sellers/ui-ux-designer/evidence/a11y/meridian-invoices-candidate-desktop.json b/sellers/ui-ux-designer/evidence/a11y/meridian-invoices-candidate-desktop.json new file mode 100644 index 00000000..44e13907 --- /dev/null +++ b/sellers/ui-ux-designer/evidence/a11y/meridian-invoices-candidate-desktop.json @@ -0,0 +1,33 @@ +{ + "name": "meridian-invoices-candidate", + "url": "http://127.0.0.1:49613/samples/redesign/after/", + "viewport": "desktop", + "engine": "axe-core 4.11.4", + "tags": [ + "wcag2a", + "wcag2aa", + "wcag21a", + "wcag21aa", + "wcag22aa" + ], + "generatedAt": "2026-09-09T18:02:06.187Z", + "loadProof": { + "title": "Invoices — Meridian", + "elements": 103, + "textLength": 1045, + "stylesheets": 3, + "url": "http://127.0.0.1:49613/samples/redesign/after/", + "rulesConsidered": 62, + "httpStatus": 200 + }, + "counts": { + "violations": 0, + "passes": 29, + "incomplete": 0, + "inapplicable": 33, + "rulesConsidered": 62, + "nodesFlagged": 0 + }, + "violations": [], + "incompleteIds": [] +} diff --git a/sellers/ui-ux-designer/evidence/a11y/meridian-invoices-candidate-mobile.json b/sellers/ui-ux-designer/evidence/a11y/meridian-invoices-candidate-mobile.json new file mode 100644 index 00000000..ed4b6b21 --- /dev/null +++ b/sellers/ui-ux-designer/evidence/a11y/meridian-invoices-candidate-mobile.json @@ -0,0 +1,35 @@ +{ + "name": "meridian-invoices-candidate", + "url": "http://127.0.0.1:49613/samples/redesign/after/", + "viewport": "mobile", + "engine": "axe-core 4.11.4", + "tags": [ + "wcag2a", + "wcag2aa", + "wcag21a", + "wcag21aa", + "wcag22aa" + ], + "generatedAt": "2026-09-09T18:02:06.937Z", + "loadProof": { + "title": "Invoices — Meridian", + "elements": 103, + "textLength": 1045, + "stylesheets": 3, + "url": "http://127.0.0.1:49613/samples/redesign/after/", + "rulesConsidered": 63, + "httpStatus": 200 + }, + "counts": { + "violations": 0, + "passes": 30, + "incomplete": 1, + "inapplicable": 32, + "rulesConsidered": 63, + "nodesFlagged": 0 + }, + "violations": [], + "incompleteIds": [ + "color-contrast" + ] +} diff --git a/sellers/ui-ux-designer/evidence/diff/desktop-before-after.png b/sellers/ui-ux-designer/evidence/diff/desktop-before-after.png new file mode 100644 index 00000000..964b4bdd Binary files /dev/null and b/sellers/ui-ux-designer/evidence/diff/desktop-before-after.png differ diff --git a/sellers/ui-ux-designer/evidence/diff/meridian-invoices-desktop.png b/sellers/ui-ux-designer/evidence/diff/meridian-invoices-desktop.png new file mode 100644 index 00000000..964b4bdd Binary files /dev/null and b/sellers/ui-ux-designer/evidence/diff/meridian-invoices-desktop.png differ diff --git a/sellers/ui-ux-designer/evidence/diff/meridian-invoices-mobile.png b/sellers/ui-ux-designer/evidence/diff/meridian-invoices-mobile.png new file mode 100644 index 00000000..c110118c Binary files /dev/null and b/sellers/ui-ux-designer/evidence/diff/meridian-invoices-mobile.png differ diff --git a/sellers/ui-ux-designer/evidence/images/mark-480.png b/sellers/ui-ux-designer/evidence/images/mark-480.png new file mode 100644 index 00000000..6c42a95e Binary files /dev/null and b/sellers/ui-ux-designer/evidence/images/mark-480.png differ diff --git a/sellers/ui-ux-designer/evidence/images/meridian-invoices-mark.png b/sellers/ui-ux-designer/evidence/images/meridian-invoices-mark.png new file mode 100644 index 00000000..6c42a95e Binary files /dev/null and b/sellers/ui-ux-designer/evidence/images/meridian-invoices-mark.png differ diff --git a/sellers/ui-ux-designer/evidence/meridian-invoices-agent-result.json b/sellers/ui-ux-designer/evidence/meridian-invoices-agent-result.json new file mode 100644 index 00000000..7daf7819 --- /dev/null +++ b/sellers/ui-ux-designer/evidence/meridian-invoices-agent-result.json @@ -0,0 +1,209 @@ +{ + "agent": "ui-ux-designer", + "identity": "Plainsong (PROPOSED — not adopted. See design-identity/IDENTITY.md §0.)", + "job": "review-redesign", + "name": "meridian-invoices", + "startedAt": "2026-09-09T18:02:01.202Z", + "finishedAt": "2026-09-09T18:02:07.230Z", + "environment": { + "node": "v26.5.0", + "chromium": "151.0.7922.34", + "axe": "axe-core 4.11.4", + "sharp": "0.34.5 / libvips 8.17.3" + }, + "accessibility": { + "baselineViolations": { + "desktop": 4, + "mobile": 4 + }, + "candidateViolations": { + "desktop": 0, + "mobile": 0 + }, + "baselineFindings": [ + "button-name (1)", + "color-contrast (34)", + "html-has-lang (1)", + "image-alt (1)" + ] + }, + "visualChange": { + "desktop": "8.51%", + "mobile": "8.25%" + }, + "steps": [ + { + "step": "image-pipeline", + "sharp": "0.34.5", + "vips": "8.17.3", + "svgInput": true, + "pngOutput": true, + "webpOutput": true, + "concurrency": 14 + }, + { + "step": "preview", + "origin": "http://127.0.0.1:49613", + "root": "/Users/forge/forge/v2/wt/w-ui-ux-designer-seller/sellers/ui-ux-designer" + }, + { + "step": "screenshot:baseline", + "captures": [ + { + "viewport": "desktop", + "size": "1280x900", + "bytes": 60634, + "distinctColors": 1374, + "inkRatio": 0.046, + "file": "/Users/forge/forge/v2/wt/w-ui-ux-designer-seller/sellers/ui-ux-designer/evidence/screenshots/meridian-invoices-baseline-desktop.png" + }, + { + "viewport": "mobile", + "size": "780x1687", + "bytes": 48998, + "distinctColors": 1489, + "inkRatio": 0.0202, + "file": "/Users/forge/forge/v2/wt/w-ui-ux-designer-seller/sellers/ui-ux-designer/evidence/screenshots/meridian-invoices-baseline-mobile.png" + } + ] + }, + { + "step": "screenshot:candidate", + "captures": [ + { + "viewport": "desktop", + "size": "1280x900", + "bytes": 93810, + "distinctColors": 1744, + "inkRatio": 0.0383, + "file": "/Users/forge/forge/v2/wt/w-ui-ux-designer-seller/sellers/ui-ux-designer/evidence/screenshots/meridian-invoices-candidate-desktop.png" + }, + { + "viewport": "mobile", + "size": "780x1688", + "bytes": 125764, + "distinctColors": 1698, + "inkRatio": 0.0707, + "file": "/Users/forge/forge/v2/wt/w-ui-ux-designer-seller/sellers/ui-ux-designer/evidence/screenshots/meridian-invoices-candidate-mobile.png" + } + ] + }, + { + "step": "axe:baseline:desktop", + "engine": "axe-core 4.11.4", + "violations": 4, + "nodesFlagged": 37, + "passes": 13, + "rulesConsidered": 64, + "loadProof": "70 elements / 698 chars", + "file": "/Users/forge/forge/v2/wt/w-ui-ux-designer-seller/sellers/ui-ux-designer/evidence/a11y/meridian-invoices-baseline-desktop.json" + }, + { + "step": "axe:baseline:mobile", + "engine": "axe-core 4.11.4", + "violations": 4, + "nodesFlagged": 37, + "passes": 13, + "rulesConsidered": 64, + "loadProof": "70 elements / 698 chars", + "file": "/Users/forge/forge/v2/wt/w-ui-ux-designer-seller/sellers/ui-ux-designer/evidence/a11y/meridian-invoices-baseline-mobile.json" + }, + { + "step": "axe:candidate:desktop", + "engine": "axe-core 4.11.4", + "violations": 0, + "nodesFlagged": 0, + "passes": 29, + "rulesConsidered": 62, + "loadProof": "103 elements / 1045 chars", + "file": "/Users/forge/forge/v2/wt/w-ui-ux-designer-seller/sellers/ui-ux-designer/evidence/a11y/meridian-invoices-candidate-desktop.json" + }, + { + "step": "axe:candidate:mobile", + "engine": "axe-core 4.11.4", + "violations": 0, + "nodesFlagged": 0, + "passes": 30, + "rulesConsidered": 63, + "loadProof": "103 elements / 1045 chars", + "file": "/Users/forge/forge/v2/wt/w-ui-ux-designer-seller/sellers/ui-ux-designer/evidence/a11y/meridian-invoices-candidate-mobile.json" + }, + { + "step": "vdiff:desktop", + "changed": 98008, + "total": 1152000, + "ratio": 0.0851, + "file": "/Users/forge/forge/v2/wt/w-ui-ux-designer-seller/sellers/ui-ux-designer/evidence/diff/meridian-invoices-desktop.png" + }, + { + "step": "vdiff:mobile", + "changed": 108516, + "total": 1315860, + "ratio": 0.0825, + "file": "/Users/forge/forge/v2/wt/w-ui-ux-designer-seller/sellers/ui-ux-designer/evidence/diff/meridian-invoices-mobile.png" + }, + { + "step": "raster", + "output": "/Users/forge/forge/v2/wt/w-ui-ux-designer-seller/sellers/ui-ux-designer/evidence/images/meridian-invoices-mark.png", + "bytes": 4475, + "size": "480x192" + } + ], + "manifest": [ + { + "file": "/Users/forge/forge/v2/wt/w-ui-ux-designer-seller/sellers/ui-ux-designer/evidence/screenshots/meridian-invoices-baseline-desktop.png", + "bytes": 60634, + "sha256": "5d40fc37979617c2872825b9526af59aee9d065f55d14cb9505ca3f9b7f31a73" + }, + { + "file": "/Users/forge/forge/v2/wt/w-ui-ux-designer-seller/sellers/ui-ux-designer/evidence/screenshots/meridian-invoices-baseline-mobile.png", + "bytes": 48998, + "sha256": "8d5d3acf13bcce74b051d07a4c2e92c89e2190a32682438a84b5deef1d17f5e1" + }, + { + "file": "/Users/forge/forge/v2/wt/w-ui-ux-designer-seller/sellers/ui-ux-designer/evidence/screenshots/meridian-invoices-candidate-desktop.png", + "bytes": 93810, + "sha256": "93d705a22651530527357a995039199518819fd487e634a3d06f0cc87be9b8b1" + }, + { + "file": "/Users/forge/forge/v2/wt/w-ui-ux-designer-seller/sellers/ui-ux-designer/evidence/screenshots/meridian-invoices-candidate-mobile.png", + "bytes": 125764, + "sha256": "a15b9d35add1c328d21a042ef4e27397bf3614f6ef4cc6f841322e13241e47ad" + }, + { + "file": "/Users/forge/forge/v2/wt/w-ui-ux-designer-seller/sellers/ui-ux-designer/evidence/a11y/meridian-invoices-baseline-desktop.json", + "bytes": 2239, + "sha256": "fee78452372c394cd6a5d2d6a7366af846636b79cdf5297d1b09b3c832cabe73" + }, + { + "file": "/Users/forge/forge/v2/wt/w-ui-ux-designer-seller/sellers/ui-ux-designer/evidence/a11y/meridian-invoices-baseline-mobile.json", + "bytes": 2238, + "sha256": "3f2fb5cad655b8028cbaca2cfa5769e1ce57cf4bfd74533f754f505f92466ae8" + }, + { + "file": "/Users/forge/forge/v2/wt/w-ui-ux-designer-seller/sellers/ui-ux-designer/evidence/a11y/meridian-invoices-candidate-desktop.json", + "bytes": 728, + "sha256": "64d168666ff484b803a050ddce0c7bab0430b446587f5fb6465a457fffafb9cc" + }, + { + "file": "/Users/forge/forge/v2/wt/w-ui-ux-designer-seller/sellers/ui-ux-designer/evidence/a11y/meridian-invoices-candidate-mobile.json", + "bytes": 751, + "sha256": "d383cac8d43860e12db9669877563ce599425570b14adf72c110660652ba4fbb" + }, + { + "file": "/Users/forge/forge/v2/wt/w-ui-ux-designer-seller/sellers/ui-ux-designer/evidence/diff/meridian-invoices-desktop.png", + "bytes": 94671, + "sha256": "96fd4dffb38b9a2ef8c4800be1d97de9dd9bcb36a2ce0de7479d3403c4b9eb89" + }, + { + "file": "/Users/forge/forge/v2/wt/w-ui-ux-designer-seller/sellers/ui-ux-designer/evidence/diff/meridian-invoices-mobile.png", + "bytes": 95776, + "sha256": "c69b99570211b4efca5ce0d05c437288d9799f6dc57ecca674fd0a1c0524b6fd" + }, + { + "file": "/Users/forge/forge/v2/wt/w-ui-ux-designer-seller/sellers/ui-ux-designer/evidence/images/meridian-invoices-mark.png", + "bytes": 4475, + "sha256": "dfd2cf14ca7231b4bfede0457fddfe5d8ffcdf1ecf60410903b616ae64d3d72c" + } + ] +} diff --git a/sellers/ui-ux-designer/evidence/screenshots/after-desktop.png b/sellers/ui-ux-designer/evidence/screenshots/after-desktop.png new file mode 100644 index 00000000..b886352d Binary files /dev/null and b/sellers/ui-ux-designer/evidence/screenshots/after-desktop.png differ diff --git a/sellers/ui-ux-designer/evidence/screenshots/after-mobile.png b/sellers/ui-ux-designer/evidence/screenshots/after-mobile.png new file mode 100644 index 00000000..dec0de54 Binary files /dev/null and b/sellers/ui-ux-designer/evidence/screenshots/after-mobile.png differ diff --git a/sellers/ui-ux-designer/evidence/screenshots/before-desktop.png b/sellers/ui-ux-designer/evidence/screenshots/before-desktop.png new file mode 100644 index 00000000..ec7e0fbd Binary files /dev/null and b/sellers/ui-ux-designer/evidence/screenshots/before-desktop.png differ diff --git a/sellers/ui-ux-designer/evidence/screenshots/before-mobile.png b/sellers/ui-ux-designer/evidence/screenshots/before-mobile.png new file mode 100644 index 00000000..1926b7f1 Binary files /dev/null and b/sellers/ui-ux-designer/evidence/screenshots/before-mobile.png differ diff --git a/sellers/ui-ux-designer/evidence/screenshots/meridian-invoices-baseline-desktop.png b/sellers/ui-ux-designer/evidence/screenshots/meridian-invoices-baseline-desktop.png new file mode 100644 index 00000000..ec7e0fbd Binary files /dev/null and b/sellers/ui-ux-designer/evidence/screenshots/meridian-invoices-baseline-desktop.png differ diff --git a/sellers/ui-ux-designer/evidence/screenshots/meridian-invoices-baseline-mobile.png b/sellers/ui-ux-designer/evidence/screenshots/meridian-invoices-baseline-mobile.png new file mode 100644 index 00000000..1926b7f1 Binary files /dev/null and b/sellers/ui-ux-designer/evidence/screenshots/meridian-invoices-baseline-mobile.png differ diff --git a/sellers/ui-ux-designer/evidence/screenshots/meridian-invoices-candidate-desktop.png b/sellers/ui-ux-designer/evidence/screenshots/meridian-invoices-candidate-desktop.png new file mode 100644 index 00000000..b886352d Binary files /dev/null and b/sellers/ui-ux-designer/evidence/screenshots/meridian-invoices-candidate-desktop.png differ diff --git a/sellers/ui-ux-designer/evidence/screenshots/meridian-invoices-candidate-mobile.png b/sellers/ui-ux-designer/evidence/screenshots/meridian-invoices-candidate-mobile.png new file mode 100644 index 00000000..dec0de54 Binary files /dev/null and b/sellers/ui-ux-designer/evidence/screenshots/meridian-invoices-candidate-mobile.png differ diff --git a/sellers/ui-ux-designer/jobs/sample-review.json b/sellers/ui-ux-designer/jobs/sample-review.json new file mode 100644 index 00000000..dfff35fa --- /dev/null +++ b/sellers/ui-ux-designer/jobs/sample-review.json @@ -0,0 +1,9 @@ +{ + "job": "review-redesign", + "name": "meridian-invoices", + "baseline": "/samples/redesign/before/", + "candidate": "/samples/redesign/after/", + "expectBaselineViolations": 4, + "maxCandidateViolations": 0, + "minChangeRatio": 0.02 +} diff --git a/sellers/ui-ux-designer/knowledge/INDEX.md b/sellers/ui-ux-designer/knowledge/INDEX.md new file mode 100644 index 00000000..a643abc2 --- /dev/null +++ b/sellers/ui-ux-designer/knowledge/INDEX.md @@ -0,0 +1,51 @@ +# Agent knowledge index + +Read in this order. Everything else in the package is generated, evidence, or sample. + +## 1. What to design like +- **[`../design-identity/IDENTITY.md`](../design-identity/IDENTITY.md)** — the Plainsong identity. + PROPOSED, not adopted. Type scale, colour roles, 4px spacing, motion, 12 accessibility rules, + named anti-patterns. A buyer's own system always overrides it. +- **[`../design-identity/tokens.json`](../design-identity/tokens.json)** — the only hand-edited + source of design values. `tokens.css` and `contrast.md` are generated from it; never edit those. +- **[`../design-identity/contrast.md`](../design-identity/contrast.md)** — measured contrast for + every contracted colour pair. Regenerated, never typed. +- **[`../design-identity/REFERENCES.md`](../design-identity/REFERENCES.md)** — every source with + version and licence. No fonts bundled, no scraped assets. + +## 2. What each tool refuses to do +Each tool is written to fail rather than produce a result that merely looks successful. + +| Tool | Does | Refuses | +|---|---|---| +| `tools/tokens.mjs` | generates `tokens.css` + `contrast.md` | exits nonzero if any contracted pair misses its minimum | +| `tools/preview.mjs` | loopback static server rooted at the package | serves nothing above the root; no write or upload path | +| `tools/shoot.mjs` | desktop + mobile screenshots | a non-2xx page, a wrong viewport, a blank or single-colour PNG, or one with too few dark pixels to be legible | +| `tools/a11y.mjs` | axe-core over wcag2a…wcag22aa | **any** verdict from a page that fails load proof — empty title, ≤20 elements, ≤200 chars of text, no stylesheet, or ≤50 axe rules considered | +| `tools/vdiff.mjs` | pixelmatch comparison + diff PNG | comparing two blank images; a 0% change when a change was required; resizing mismatched inputs (it crops to the overlap instead) | +| `tools/svg.mjs` | sharp/libvips SVG → PNG | a raster that is blank, zero-byte, or the wrong width; a libvips missing SVG/PNG support | +| `tools/drive-agent.mjs` | spawns the agent and speaks ACP | a failed handshake, an agent error, or any stopReason other than `end_turn` | + +## 3. How a job runs +`agent/designer-agent.mjs` is the ACP stdio agent the seller spawns. One job type: +**`review-redesign`**, taking a JSON brief (see `jobs/sample-review.json`). It refuses free-text +prompts and refuses unknown job types rather than improvising. + +Turn sequence: check the image pipeline → start the preview server → screenshot baseline and +candidate at both viewports → axe both at both viewports → assert the baseline is *at least* as bad +as the brief expects (a suddenly-clean baseline means the check broke, not that the page improved) +→ assert the candidate is within its violation limit → visual-diff each viewport and require real +change → raster the mark → write a JSON result with a per-artifact sha256 manifest. + +## 4. Standing rules for the agent +1. Automated checks are a floor. axe finds roughly a third of real accessibility problems; state + which of IDENTITY.md §6 you reviewed **by hand** and never let "axe passed" stand in for it. +2. Report the anti-patterns axe cannot see — placeholder-as-label passes axe, because a placeholder + supplies an accessible name. +3. Never claim a capability you have not exercised in this run. A binary on disk is not access. +4. Every number in a deliverable comes from a file in the manifest. +5. No purchases, no subscriptions, no deployment, no wallet. Missing tools are blockers to report. + +## 5. Where evidence lands +`evidence/screenshots/`, `evidence/a11y/`, `evidence/diff/`, `evidence/images/`, and +`evidence/-agent-result.json`. The gate re-verifies every manifest entry's size and digest. diff --git a/sellers/ui-ux-designer/package-lock.json b/sellers/ui-ux-designer/package-lock.json new file mode 100644 index 00000000..3e4173ab --- /dev/null +++ b/sellers/ui-ux-designer/package-lock.json @@ -0,0 +1,704 @@ +{ + "name": "@maxplayer/ui-ux-designer-seller", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "@maxplayer/ui-ux-designer-seller", + "version": "0.1.0", + "dependencies": { + "@axe-core/playwright": "4.11.2", + "axe-core": "4.11.4", + "pixelmatch": "6.0.0", + "playwright": "1.62.0", + "pngjs": "7.0.0", + "sharp": "0.34.5" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/@axe-core/playwright": { + "version": "4.11.2", + "resolved": "https://registry.npmjs.org/@axe-core/playwright/-/playwright-4.11.2.tgz", + "integrity": "sha512-iP6hfNl9G0j/SEUSo8M7D80RbcDo9KRAAfDP4IT5OHB+Wm6zUHIrm8Y51BKI+Oyqduvipf9u1hcRy57zCBKzWQ==", + "license": "MPL-2.0", + "dependencies": { + "axe-core": "~4.11.3" + }, + "peerDependencies": { + "playwright-core": ">= 1.0.0" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.11.3", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.3.tgz", + "integrity": "sha512-Xz4Tpyki7XyrpbUK1jR1AhdAdaXyhhY4lZ3neLodmhpuWfy2PAQN5B46sAiU4liOXGLkHypn/qU+jvfWSCYYLA==", + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@img/colour": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.1.0.tgz", + "integrity": "sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@img/sharp-darwin-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.34.5.tgz", + "integrity": "sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-arm64": "1.2.4" + } + }, + "node_modules/@img/sharp-darwin-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.34.5.tgz", + "integrity": "sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-x64": "1.2.4" + } + }, + "node_modules/@img/sharp-libvips-darwin-arm64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.2.4.tgz", + "integrity": "sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-darwin-x64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.2.4.tgz", + "integrity": "sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.2.4.tgz", + "integrity": "sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==", + "cpu": [ + "arm" + ], + "libc": [ + "glibc" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.2.4.tgz", + "integrity": "sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==", + "cpu": [ + "arm64" + ], + "libc": [ + "glibc" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-ppc64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.2.4.tgz", + "integrity": "sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==", + "cpu": [ + "ppc64" + ], + "libc": [ + "glibc" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-riscv64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.2.4.tgz", + "integrity": "sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==", + "cpu": [ + "riscv64" + ], + "libc": [ + "glibc" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-s390x": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.2.4.tgz", + "integrity": "sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==", + "cpu": [ + "s390x" + ], + "libc": [ + "glibc" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-x64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.2.4.tgz", + "integrity": "sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==", + "cpu": [ + "x64" + ], + "libc": [ + "glibc" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-arm64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.2.4.tgz", + "integrity": "sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==", + "cpu": [ + "arm64" + ], + "libc": [ + "musl" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-x64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.2.4.tgz", + "integrity": "sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==", + "cpu": [ + "x64" + ], + "libc": [ + "musl" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-linux-arm": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.34.5.tgz", + "integrity": "sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==", + "cpu": [ + "arm" + ], + "libc": [ + "glibc" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.34.5.tgz", + "integrity": "sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==", + "cpu": [ + "arm64" + ], + "libc": [ + "glibc" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm64": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-ppc64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.34.5.tgz", + "integrity": "sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==", + "cpu": [ + "ppc64" + ], + "libc": [ + "glibc" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-ppc64": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-riscv64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.34.5.tgz", + "integrity": "sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==", + "cpu": [ + "riscv64" + ], + "libc": [ + "glibc" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-riscv64": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-s390x": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.34.5.tgz", + "integrity": "sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==", + "cpu": [ + "s390x" + ], + "libc": [ + "glibc" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-s390x": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.34.5.tgz", + "integrity": "sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==", + "cpu": [ + "x64" + ], + "libc": [ + "glibc" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-x64": "1.2.4" + } + }, + "node_modules/@img/sharp-linuxmusl-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.34.5.tgz", + "integrity": "sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==", + "cpu": [ + "arm64" + ], + "libc": [ + "musl" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-arm64": "1.2.4" + } + }, + "node_modules/@img/sharp-linuxmusl-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.34.5.tgz", + "integrity": "sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==", + "cpu": [ + "x64" + ], + "libc": [ + "musl" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-x64": "1.2.4" + } + }, + "node_modules/@img/sharp-wasm32": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.34.5.tgz", + "integrity": "sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==", + "cpu": [ + "wasm32" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", + "optional": true, + "dependencies": { + "@emnapi/runtime": "^1.7.0" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.34.5.tgz", + "integrity": "sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-ia32": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.34.5.tgz", + "integrity": "sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg==", + "cpu": [ + "ia32" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.34.5.tgz", + "integrity": "sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/axe-core": { + "version": "4.11.4", + "resolved": "https://registry.npmjs.org/axe-core/-/axe-core-4.11.4.tgz", + "integrity": "sha512-KunSNx+TVpkAw/6ULfhnx+HWRecjqZGTOyquAoWHYLRSdK1tB5Ihce1ZW+UY3fj33bYAFWPu7W/GRSmmrCGuxA==", + "license": "MPL-2.0", + "engines": { + "node": ">=4" + } + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/pixelmatch": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/pixelmatch/-/pixelmatch-6.0.0.tgz", + "integrity": "sha512-FYpL4XiIWakTnIqLqvt3uN4L9B3TsuHIvhLILzTiJZMJUsGvmKNeL4H3b6I99LRyerK9W4IuOXw+N28AtRgK2g==", + "license": "ISC", + "dependencies": { + "pngjs": "^7.0.0" + }, + "bin": { + "pixelmatch": "bin/pixelmatch" + } + }, + "node_modules/playwright": { + "version": "1.62.0", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.62.0.tgz", + "integrity": "sha512-Z14dG305dgaLu6foB1TXQagFiW8JfSUIUaUuPaKQ6NtBPKF1P/qXcqfh6c6K/icPqdy37JmjbiBXf6JNg6Sylw==", + "license": "Apache-2.0", + "dependencies": { + "playwright-core": "1.62.0" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=20" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, + "node_modules/playwright-core": { + "version": "1.62.0", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.62.0.tgz", + "integrity": "sha512-nsNRyq0r2zsG8AcRHWknc9QRA5XCueC7gWMrs+Gx2tlZn9hcl8zudfh00lhJPY1DE7NmZ6bDsT9g2yey8mXljA==", + "license": "Apache-2.0", + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/pngjs": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/pngjs/-/pngjs-7.0.0.tgz", + "integrity": "sha512-LKWqWJRhstyYo9pGvgor/ivk2w94eSjE3RGVuzLGlr3NmD8bf7RcYGze1mNdEHRP6TRP6rMuDHk5t44hnTRyow==", + "license": "MIT", + "engines": { + "node": ">=14.19.0" + } + }, + "node_modules/semver": { + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/sharp": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.5.tgz", + "integrity": "sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==", + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "@img/colour": "^1.0.0", + "detect-libc": "^2.1.2", + "semver": "^7.7.3" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-darwin-arm64": "0.34.5", + "@img/sharp-darwin-x64": "0.34.5", + "@img/sharp-libvips-darwin-arm64": "1.2.4", + "@img/sharp-libvips-darwin-x64": "1.2.4", + "@img/sharp-libvips-linux-arm": "1.2.4", + "@img/sharp-libvips-linux-arm64": "1.2.4", + "@img/sharp-libvips-linux-ppc64": "1.2.4", + "@img/sharp-libvips-linux-riscv64": "1.2.4", + "@img/sharp-libvips-linux-s390x": "1.2.4", + "@img/sharp-libvips-linux-x64": "1.2.4", + "@img/sharp-libvips-linuxmusl-arm64": "1.2.4", + "@img/sharp-libvips-linuxmusl-x64": "1.2.4", + "@img/sharp-linux-arm": "0.34.5", + "@img/sharp-linux-arm64": "0.34.5", + "@img/sharp-linux-ppc64": "0.34.5", + "@img/sharp-linux-riscv64": "0.34.5", + "@img/sharp-linux-s390x": "0.34.5", + "@img/sharp-linux-x64": "0.34.5", + "@img/sharp-linuxmusl-arm64": "0.34.5", + "@img/sharp-linuxmusl-x64": "0.34.5", + "@img/sharp-wasm32": "0.34.5", + "@img/sharp-win32-arm64": "0.34.5", + "@img/sharp-win32-ia32": "0.34.5", + "@img/sharp-win32-x64": "0.34.5" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD", + "optional": true + } + } +} diff --git a/sellers/ui-ux-designer/package.json b/sellers/ui-ux-designer/package.json new file mode 100644 index 00000000..a7beea19 --- /dev/null +++ b/sellers/ui-ux-designer/package.json @@ -0,0 +1,26 @@ +{ + "name": "@maxplayer/ui-ux-designer-seller", + "version": "0.1.0", + "private": true, + "type": "module", + "description": "Runnable UI/UX designer seller package for maxplayer: pinned Chromium/Playwright harness, axe accessibility checks, visual comparison, SVG/image handling.", + "engines": { + "node": ">=20" + }, + "scripts": { + "smoke": "bash scripts/smoke.sh", + "harness:verify": "node tools/verify-harness.mjs" + }, + "dependencies": { + "playwright": "1.62.0", + "@axe-core/playwright": "4.11.2", + "axe-core": "4.11.4", + "pixelmatch": "6.0.0", + "pngjs": "7.0.0", + "sharp": "0.34.5" + }, + "overrides": { + "playwright-core": "1.62.0", + "axe-core": "4.11.4" + } +} diff --git a/sellers/ui-ux-designer/reports/ui-ux-designer-seller-report.md b/sellers/ui-ux-designer/reports/ui-ux-designer-seller-report.md new file mode 100644 index 00000000..f8c92540 --- /dev/null +++ b/sellers/ui-ux-designer/reports/ui-ux-designer-seller-report.md @@ -0,0 +1,239 @@ +# UI/UX designer seller — build report + +- **Worker:** `w-ui-ux-designer-seller` (forge worker, channel `#ui-ux-designer-seller`) +- **Ordering seat:** maxie, on Damian's authorization. Folded by hearth. +- **Worktree:** `~/forge/v2/wt/w-ui-ux-designer-seller` +- **Branch:** `w/ui-ux-designer-seller` +- **Base:** `d7b94db2dbb7aeeefdcbb087edd0c90df56a8bdb` ("release: cut v0.5.8 (#984)"), resolved **by + URL** from `https://github.com/MakePrisms/maxplayerai.git`. On the Studio checkout + (`/Users/forge/forge/work/maxplayerai`) that URL is the remote labelled `upstream`; `origin` + there is the fork `maxy-player/maxplayerai`, whose local `main` was stale at `91e5946`. I fetched + and confirmed the live head rather than trusting the label. +- **Package:** `sellers/ui-ux-designer/` +- **Date:** 9 September 2026 (UTC) + +## 1. The gate + +```bash +cd sellers/ui-ux-designer +npm ci +bash scripts/smoke.sh +``` + +The generative check spends a **real model turn** and takes several minutes. That is deliberate: +a gate that only checks a deterministic rewrite is exactly the defect this package was corrected +for. It also needs a `maxplayer` binary built with the `acp` feature (see §2.1). + +**Verified both directions, first-hand:** + +- full gate — `exit 0`, **"9 checks run, 0 failed"**, `SMOKE GATE PASSED` +- sabotaged job (baseline and candidate pointed at the same page) — `exit 1`, "7 checks run, + 2 failed", failing with: + `baseline reported only 0 violations, expected at least 4 — the accessibility check is probably + not exercising the page` + +A gate that cannot fail proves nothing, so the sabotage run is part of the evidence, not an aside. + +What the gate does, in order: + +1. **Environment.** Node floor check, then it *launches* Chromium and prints the version the + running browser reports. Not `which`, not a file-exists test. +2. **Contrast.** Regenerates `design-identity/contrast.md` from `tokens.json` with the WCAG + relative-luminance formula. 13 contracted pairs, all passing; a miss exits nonzero. +3. **Image pipeline.** Loads sharp and asserts libvips has SVG and PNG support compiled in. +4. **The generative path — the heart of the gate.** Hands `briefs/nova-status-brief.md`, a + natural-language brief with **no candidate implementation supplied**, to a real model harness + through the **actual maxplayer local driver**. The model must author the page itself, render it, + read its own screenshots and audit output, and iterate to zero accessibility violations at both + viewports. See §2.1. +5. **The review pipeline, as tooling only.** Spawns `agent/designer-agent.mjs` over ACP stdio — + `initialize` → `session/new` → `session/prompt`, requiring `stopReason: "end_turn"`. It is + retained and explicitly demoted: it inspects a supplied candidate and is never allowed to stand + in for a designer. +6. **External verification.** `tools/verify-manifest.mjs` re-reads all 11 artifacts from disk and + recomputes each sha256. The agent's own digests are not trusted; a manifest self-certified by + the process that wrote the files is a receipt, not evidence. +7. **Four negative controls, each of which must exit nonzero** (and did): + - an empty page that axe scores as **0 violations** is rejected by the load-proof guard; + - a 404 URL yields no screenshot; + - two identical images fail a required-change diff; + - a generative run whose `status.html` has been deleted is **not** reported as a pass — proving + the generative check reads artifacts off disk rather than trusting its own summary. + +## 2. Harness access — verified, not assumed + +### 2.1 The model harness and the maxplayer driver + +The correction required integrating a supported coding/model harness using the **existing +authorized runtime only**, with a concrete BLOCKED if none was available. Presence on `PATH` is not +availability, so each adapter was *driven* over ACP stdio and judged on whether a model answered: + +| Adapter | Handshake | Session | Model answered | Verdict | +|---|---|---|---|---| +| `claude-agent-acp` | protocol 1 OK | OK | **no** | refused: `errorKind: rate_limit`, org monthly spend limit — reproduced twice, an hour apart | +| `codex-acp` | protocol 1 OK | OK | **yes** | **used** — `gpt-5.6-sol`, `stopReason: end_turn`, non-zero output tokens against a real quota | +| `claude-code-acp`, `cursor-agent`, `cursor-agent-acp`, `goose` | — | — | — | absent from `PATH` | + +So no BLOCKED: an authorized harness is available, and the gate depends on `codex-acp`, not on the +spend-limited one. Full detail in `evidence/RUNTIME.md`. + +The harness is not spoken to directly. It is driven through the **actual maxplayer local driver**, +built from this branch — the stock `/opt/homebrew/bin/maxplayer` (0.1.0-rc.3) refuses this path +outright with `maxplayer run requires rebuilding with the acp feature`: + +```bash +cargo build -p maxplayer --features acp --release +# -> maxplayer 0.5.8 (e72e9bd7114fe86412b62ff68a08c28c5602d396) +``` + +`maxplayer run` is the local driver only: no relay, no wallet, no sats, no deployment. + +**What the model actually produced**, from the passing gate run (`runs/generative-smoke/summary.json`, +verdict `PASS`, `failures: []`): + +| Evidence | Result | +|---|---| +| Driver | exit 0, `job.execution_changed → completed` | +| Deliverable | `status.html` authored by the model, ~14 KB, not byte-identical to anything shipped here | +| Agent-invoked tools | `shoot.mjs` **true**, `a11y.mjs` **true** — read from the driver's own session updates | +| Accessibility, desktop | **0 violations**, 15 passes, 63 rules considered, axe-core 4.11.4 | +| Accessibility, mobile | **0 violations**, 15 passes, 63 rules considered, axe-core 4.11.4 | +| Load proof | HTTP 200, title "Nova service status", 225 elements | +| Screenshots | desktop 122,207 B and mobile 291,881 B, both content-asserted | +| Design notes | `DESIGN-NOTES.md` written by the model | + +**A detector bug worth recording.** The first generative run passed on the merits yet the gate +failed it, claiming the agent "never rendered its design". The driver writes *two* streams and they +carry different things: `events.jsonl` nests its type under `.payload` and holds only +`driver.ready` / `job.execution_changed` / `agent.message`, while the ACP tool calls and the +`turn_ended` stop reason arrive on driver **stdout**. Reading only the first stream produced a +confident false accusation against work that was in fact correct. The gate now reads both, and +`--verify-only` re-checks finished evidence without spending another model turn. + +The load-bearing line in the order was "verify actual harness access, not installed-binary +presence". Concretely: + +| Capability | How it was proven | Result | +|---|---|---| +| Chromium drivable from the package | `chromium.launch()`, then `browser.version()` | **151.0.7922.34** (Playwright revision 1234) | +| Screenshot readback | captured, decoded with pngjs, pixel statistics asserted | desktop 1280×900, 93,810 B, 1744 distinct colours, 3.83% ink | +| Node/frontend preview | loopback server started, page fetched, HTTP status checked | ephemeral port, 200 | +| axe accessibility | run at both viewports with load proof recorded | baseline 4 violations / 37 nodes; candidate 0 with 29–30 passes | +| Visual comparison | pixelmatch over real captures | desktop 8.51%, mobile 8.25% changed | +| SVG / image handling | sharp rasterised an authored SVG, dimensions and content asserted | 480×192, 4,475 B, 246 colours | + +Pinned: `playwright@1.62.0`, `axe-core@4.11.4`, `@axe-core/playwright@4.11.2`, `pixelmatch@6.0.0`, +`pngjs@7.0.0`, `sharp@0.34.5`, with `npm ci` from a committed lockfile. Playwright 1.62.0 was +chosen *because* its Chromium revision (1234) is the one already in this host's cache, so a fresh +`npm ci` downloads no browser. + +**Two real pinning defects found and fixed during the build**, both invisible without checking: + +1. `@axe-core/playwright`'s loose `playwright-core: ">= 1.0.0"` peer caused npm to hoist a **second + playwright-core at 1.63.0** beside the pinned 1.62.0. An `overrides` entry now forces one copy. +2. Its `axe-core: "~4.11.3"` dependency installed a **nested axe-core 4.11.4** next to my declared + 4.11.2 — so the engine that actually ran was not the one I had pinned. Both the dependency and + the override are now 4.11.4, and there is a single copy. This was only visible because the tool + prints the engine version it used. + +## 3. Deliverables + +| Item ordered | Where | +|---|---| +| Runnable seller package | `sellers/ui-ux-designer/` | +| One explicit proposed design identity | `design-identity/IDENTITY.md` — "Plainsong", marked PROPOSED in the doc, the token file and the generated CSS | +| Concrete typography / colour / spacing / motion / a11y rules / anti-patterns | IDENTITY.md §§2–7; 12 numbered a11y rules; 18 named anti-patterns | +| Reference corpus with source, version, licensing | `design-identity/REFERENCES.md` — dependency licences read off disk from `node_modules`; WCAG 2.2, APG, GOV.UK, USWDS, M3, Inter/IBM Plex OFL. No fonts bundled, no scraped assets | +| Reproducible pinned tooling | `package.json` + `package-lock.json`, `npm ci` verified | +| Setup / run docs | `README.md`, limitations stated first | +| Concise agent knowledge index | `knowledge/INDEX.md`, including a table of what each tool *refuses* to do | +| Sample redesign source | `samples/redesign/before/index.html`, `samples/redesign/after/index.html` | +| Before/after desktop and mobile screenshots | `evidence/screenshots/` — four captures, all in the manifest | +| Rationale | `samples/redesign/RATIONALE.md` | +| Local smoke command | `scripts/smoke.sh` | +| Evidence manifest | `evidence/MANIFEST.md`, regenerated and externally verified by the gate | + +## 4. Design identity summary + +**Plainsong** — proposed, not adopted, and inert the moment a buyer supplies their own system. +Near-black ink on near-white paper, one accent, 1.25 type scale off a 16px base, 4px spacing grid, +motion capped at 260ms with 24px maximum travel, `prefers-reduced-motion` compiled into the +generated token block rather than left to memory. + +The contrast ledger is generated, never typed. It earned its place immediately: the first border +ink, `#98A2AE`, measured **2.59:1** against paper — under the 3:1 floor for non-text UI in WCAG 2.2 +SC 1.4.11. It was darkened to `#868F9C` = **3.27:1**. That failure is documented in IDENTITY.md §3 +rather than quietly corrected. + +## 5. Sample redesign result + +axe-core 4.11.4, tags `wcag2a wcag2aa wcag21a wcag21aa wcag22aa`, both viewports: + +- **before:** 4 violations across 37 nodes — `color-contrast` (34 nodes), `html-has-lang`, + `image-alt`, `button-name` +- **after:** 0 violations, 29 passes desktop / 30 mobile, load proof 103 elements and 1,045 + characters of rendered text + +`RATIONALE.md` additionally lists **ten defects axe did not and could not find**, each explained — +most importantly placeholder-as-label, which axe *passes* because a placeholder does supply an +accessible name, and `
` controls, which axe cannot flag because a div with no +interactive role is just a div. Those are recorded as hand-reviewed, never folded into the machine +result. + +## 6. Evidence manifest + +`sellers/ui-ux-designer/evidence/MANIFEST.md` — 11 artifacts, all verified on disk after the run: +4 screenshots, 4 axe JSON reports, 2 diff images, 1 raster. Run recorded as node v26.5.0 / +Chromium 151.0.7922.34 / axe-core 4.11.4 / sharp 0.34.5 with libvips 8.17.3. + +`evidence/` also holds artifacts from the manual tool verification I did while building +(`before-*`, `after-*`, `desktop-before-after.png`, `mark-480.png`). Those are genuine outputs but +they are **not** covered by the manifest, which describes only the agent's own run. + +## 7. Limitations — stated plainly + +1. **The maxplayer binary IS now run, but only its local driver path.** `maxplayer run` is built + from this branch with `--features acp` and drives the model harness for real. No live relay was + touched: no wallet, no sats, no mint, no seat, no key. The seller registration, job-claim, + delivery and payment path remains **entirely unexercised**. This package proves the *agent and + its tools*; it does not prove the seat earns. That is a real gap, not a rounding error, and it + follows directly from the hold on real sats in my fold. +1b. **The gate depends on a binary you must build.** `cargo build -p maxplayer --features acp + --release` is a prerequisite, and the release build is not cheap. The gate fails with a clear + message rather than silently substituting a fake driver. +2. **Docker sandbox mode unexercised.** Under `[sandbox] mode = "docker"` argv[0] resolves against + the image's PATH, and the image would need node, this package, and a drivable Chromium. Not + tested. +3. **The designer is a real model; the deterministic pipeline is now only tooling.** The + generative path calls `codex-acp` (`gpt-5.6-sol`) through the maxplayer driver and the model + authors the deliverable. Consequences a buyer should know: output **varies between runs**, the + gate is not deterministic, each run costs real model tokens on the host's own account, and the + run takes minutes rather than seconds. `agent/designer-agent.mjs` still only accepts a + `review-redesign` JSON brief — it is a tool, not the designer. +3b. **The harness is single-supplier in practice.** `claude-agent-acp` is present but refused by a + standing org spend limit, and cursor/goose adapters are absent, so the gate currently rests on + `codex-acp` alone. If that account hits its own limit, the generative check cannot run — and it + will fail loudly rather than fall back to deterministic execution. +3c. **One generative brief, and no human design review of its output.** The model's status page + passed axe at both viewports and its screenshots were content-asserted, but no human has judged + whether it is *good*. Zero automated violations is a floor, not taste. +4. **sharp's install script is blocked** by this host's npm allow-scripts policy. The library loads + and works (libvips 8.17.3, verified), but on a host that needs the build step, `npm ci` alone + may not be enough. +5. **Automated accessibility checking is a floor**, roughly a third of real issues. Everything else + in IDENTITY.md §6 is hand review, and is reported as such. +6. **Plainsong is proposed**, unreviewed by any human designer, with no brand authority. +7. **The sample is static HTML** — no framework, no build step, no data layer. A real product + integration would need work this package does not contain. +8. **Two paths, each narrow**: one generative brief (`briefs/nova-status-brief.md`) and one + deterministic job type (`review-redesign`). +9. The two negative-control fixtures (`tools/fixtures/blank.html`) and the sabotage job exist to + test the gate. The sabotage job was run from `/tmp` and its artifacts were removed; only the + real run's evidence is committed. + +## 8. Holds observed + +No wallet touched, no sats, no paid service, no new subscription, no production deployment, no +merge, no tag, no push to any branch. Delivery is a local branch in an isolated worktree. Git +identity was set per-command (`git -c user.name=… -c user.email=…`); `git config` was never run, so +no other lane's committer was disturbed. diff --git a/sellers/ui-ux-designer/runs/generative-current/driver-stderr.log b/sellers/ui-ux-designer/runs/generative-current/driver-stderr.log new file mode 100644 index 00000000..e69de29b diff --git a/sellers/ui-ux-designer/runs/generative-current/summary.json b/sellers/ui-ux-designer/runs/generative-current/summary.json new file mode 100644 index 00000000..ccbaeefa --- /dev/null +++ b/sellers/ui-ux-designer/runs/generative-current/summary.json @@ -0,0 +1,76 @@ +{ + "generatedAt": "2026-09-09T17:54:47.422Z", + "driver": { + "path": "(verify-only)", + "exit": 0, + "elapsedSeconds": 0 + }, + "harness": "(verify-only)", + "brief": { + "path": "briefs/nova-status-brief.md", + "sha256": "aab962e18c4a05ad6a7176f7bbae0f78929bdfeee5b808a75ad3db2daa54b66f", + "candidateSupplied": false + }, + "events": { + "lines": 444, + "stdoutLines": 568, + "jobStatus": "completed", + "turnEnded": "completed", + "toolCallsObserved": 47 + }, + "agentInvokedTools": { + "shoot": true, + "a11y": true + }, + "deliverable": { + "file": "workspace/status.html", + "bytes": 14942, + "sha256": "a443bb3901127282ac595930afd010cd8d0bb7a0c772793dcefc824b39b5d790" + }, + "screenshots": [ + { + "file": "workspace/shots/status-desktop.png", + "bytes": 122207, + "sha256": "4f64cae3c6983f08a1838e0e650d3c04bb583a3a8ccc9998e6751e41bfba0996" + }, + { + "file": "workspace/shots/status-mobile.png", + "bytes": 291881, + "sha256": "9796062cc8c4e970123ffbd6126263f4e70f20dc21f40906783c476b90a35a55" + } + ], + "accessibility": { + "desktop": { + "engine": "axe-core 4.11.4", + "violations": 0, + "passes": 15, + "rulesConsidered": 63, + "loadProof": { + "title": "Nova service status", + "elements": 225, + "textLength": 1281, + "stylesheets": 2, + "url": "file:///Users/forge/forge/v2/wt/w-ui-ux-designer-seller/sellers/ui-ux-designer/runs/generative-current/workspace/status.html", + "rulesConsidered": 63, + "httpStatus": 200 + } + }, + "mobile": { + "engine": "axe-core 4.11.4", + "violations": 0, + "passes": 15, + "rulesConsidered": 63, + "loadProof": { + "title": "Nova service status", + "elements": 225, + "textLength": 1281, + "stylesheets": 2, + "url": "file:///Users/forge/forge/v2/wt/w-ui-ux-designer-seller/sellers/ui-ux-designer/runs/generative-current/workspace/status.html", + "rulesConsidered": 63, + "httpStatus": 200 + } + } + }, + "failures": [], + "verdict": "PASS" +} diff --git a/sellers/ui-ux-designer/runs/generative-current/task.txt b/sellers/ui-ux-designer/runs/generative-current/task.txt new file mode 100644 index 00000000..f9ec6f50 --- /dev/null +++ b/sellers/ui-ux-designer/runs/generative-current/task.txt @@ -0,0 +1,71 @@ +You are a UI/UX designer. Design and BUILD the page described in the brief below. + +There is no existing implementation and no candidate to edit. You are authoring this from +nothing. Write the source yourself. + +=== BRIEF (verbatim from the buyer) === +# Design brief — Nova status page + +Natural language, as a buyer would write it. **No candidate implementation is supplied**, and none +exists in this repository: the agent authors the source itself. + +--- + +We run Nova, a small hosted service. We need a **public status page** — one screen, no login, +the thing people open when they think we are down. + +It should show, at a glance: + +- whether everything is currently working, in plain words, at the top +- the state of our four components: API, Dashboard, Webhooks, Scheduled Jobs +- uptime over the last 90 days for each, as a small bar strip +- the two most recent incidents, each with a title, when it started, how long it lasted, and a + one-line summary of what happened +- when the page last refreshed + +People land here angry and in a hurry, often on a phone, sometimes on a train with one bar of +signal. Make it fast to read and impossible to misread. It has to work for someone who cannot +distinguish red from green, and for someone using a screen reader. + +Build it as a single self-contained HTML file called `status.html`. No build step, no framework, no +external network requests — inline the styles. + +Use our design tokens; do not invent a second colour system. + +=== END BRIEF === + +Your working directory already contains everything you need: + + design-identity/tokens.css the design tokens — link or inline these; do not invent + a second colour system. Read them before you design. + design-identity/contrast.md measured contrast ratios for the token palette + tools/shoot.mjs screenshots, desktop AND mobile in one run + tools/a11y.mjs axe-core accessibility audit + node_modules/ pinned Chromium/Playwright/axe — already installed + +Do this, in order: + +1. Read design-identity/tokens.css so you know the real token names and values. +2. Write status.html in the working directory: ONE self-contained file, styles inline, + no framework, no build step, no external network requests. +3. Screenshot it and LOOK at what you produced: + node tools/shoot.mjs --url file://$PWD/status.html --name status --out shots --full + The tool prints the pixel dimensions, distinct colour count and ink ratio of each + capture, and it fails if the image is blank. Read that output. +4. Audit accessibility at BOTH viewports, and read every violation: + node tools/a11y.mjs --url file://$PWD/status.html --name status --viewport desktop --out a11y + node tools/a11y.mjs --url file://$PWD/status.html --name status --viewport mobile --out a11y +5. FIX what the tools report and repeat steps 3-4 until BOTH audits report + violations=0. Do not stop while a violation remains. Do not silence a violation by + deleting the content it was about. +6. Finish by writing DESIGN-NOTES.md in the working directory: what you designed and why, + which token you used for what, how you met the colour-blindness and screen-reader + constraints in the brief, and the final violation counts and screenshot dimensions you + observed from the tool output. + +Constraints that matter as much as the visual result: the status must be readable without +relying on colour alone, the page must make sense to a screen reader in source order, and +it must hold up at 390px wide. Contrast must meet WCAG AA. + +Run the tools yourself and act on what they say. Do not claim a result you have not seen +in tool output. \ No newline at end of file diff --git a/sellers/ui-ux-designer/runs/generative-current/workspace/DESIGN-NOTES.md b/sellers/ui-ux-designer/runs/generative-current/workspace/DESIGN-NOTES.md new file mode 100644 index 00000000..66c3bc80 --- /dev/null +++ b/sellers/ui-ux-designer/runs/generative-current/workspace/DESIGN-NOTES.md @@ -0,0 +1,47 @@ +# Nova status page — design notes + +## What I designed + +The page is a single, offline `status.html` with its CSS and tiny bar-generation script inline. It has no dependencies, build step, framework, font download, or external network request. + +The reading order follows the urgency of a status visit: + +1. A plain-language current verdict: “Everything is working normally.” +2. Four component cards with explicit status text, uptime percentage, and a compact 90-day strip. +3. The two latest resolved incidents, with title, summary, start time, and duration. +4. A final refreshed timestamp. + +The wide layout uses two columns for component comparison and a single incident list. At 390 CSS pixels it becomes one column, preserves comfortable side margins, and places incident timing beneath the summary. The final mobile capture shows no horizontal overflow or clipped content. + +## Token use + +Only values from `design-identity/tokens.css` are used for the visual system. + +- `paper-100` is the page ground; `paper-000` is the high-clarity card surface; `paper-300` and `ink-100` provide separators and boundaries. +- `ink-900` is primary copy. `ink-600` is supporting text, using the palette’s measured AA-safe body-text pairing on white. +- `success-600` / `success-700` reinforce “Operational” and “Resolved.” +- `warn-600` and `danger-600` mark the exceptional uptime bars that correspond to the incident history. +- `accent-100` supplies the restrained Nova ring motif, and `accent-700` marks the refresh symbol. +- The supplied type scale, spacing scale, radii, monospace family, and `shadow-sm` establish hierarchy without introducing parallel values or effects. + +## Accessibility decisions + +Status never depends on color. Every live component includes a check symbol and the word “Operational”; the top-level state includes a check and the full phrase “All systems operational”; incidents are explicitly labeled “Resolved.” Uptime history is also stated as a numeric percentage and summarized in screen-reader-only prose. Red and amber bars are supplementary history cues, not the only carriers of meaning. + +The source order matches the visual order. Semantic `header`, `main`, `section`, `article`, headings, lists, and `time` elements make the page navigable and understandable with a screen reader. Decorative branding, the ring motif, and visual bar strips are hidden from the accessibility tree. Each uptime strip instead has a concise spoken summary, avoiding 120 repetitive bar announcements. + +All foreground/background pairings follow the supplied contrast ledger. Axe’s one inconclusive `color-contrast` check was manually inspected: it targets only the `aria-hidden` check glyph inside the status icon. That glyph uses the ledger’s `paper-000` on `success-600` pair, measured at 6.02:1. It is also redundant with visible and accessible status text. + +## Final verification + +Final screenshot run: + +- Desktop: **1280 × 1351 px**, 1,471 distinct colors, 4.68% ink. +- Mobile: **780 × 3886 px** (390 CSS px at 2×), 1,928 distinct colors, 7.59% ink. + +Final axe-core 4.11.4 audits: + +- Desktop: **0 violations**, 0 flagged nodes (63 rules considered). +- Mobile: **0 violations**, 0 flagged nodes (63 rules considered). + +Both runs passed the audit tool’s document-title, DOM-size, text-length, stylesheet, HTTP-status, and rules-considered load proof. diff --git a/sellers/ui-ux-designer/runs/generative-current/workspace/a11y/status-desktop.json b/sellers/ui-ux-designer/runs/generative-current/workspace/a11y/status-desktop.json new file mode 100644 index 00000000..d2145e01 --- /dev/null +++ b/sellers/ui-ux-designer/runs/generative-current/workspace/a11y/status-desktop.json @@ -0,0 +1,35 @@ +{ + "name": "status", + "url": "file:///Users/forge/forge/v2/wt/w-ui-ux-designer-seller/sellers/ui-ux-designer/runs/generative-current/workspace/status.html", + "viewport": "desktop", + "engine": "axe-core 4.11.4", + "tags": [ + "wcag2a", + "wcag2aa", + "wcag21a", + "wcag21aa", + "wcag22aa" + ], + "generatedAt": "2026-09-09T17:50:48.460Z", + "loadProof": { + "title": "Nova service status", + "elements": 225, + "textLength": 1281, + "stylesheets": 2, + "url": "file:///Users/forge/forge/v2/wt/w-ui-ux-designer-seller/sellers/ui-ux-designer/runs/generative-current/workspace/status.html", + "rulesConsidered": 63, + "httpStatus": 200 + }, + "counts": { + "violations": 0, + "passes": 15, + "incomplete": 1, + "inapplicable": 47, + "rulesConsidered": 63, + "nodesFlagged": 0 + }, + "violations": [], + "incompleteIds": [ + "color-contrast" + ] +} diff --git a/sellers/ui-ux-designer/runs/generative-current/workspace/a11y/status-mobile.json b/sellers/ui-ux-designer/runs/generative-current/workspace/a11y/status-mobile.json new file mode 100644 index 00000000..278a14d8 --- /dev/null +++ b/sellers/ui-ux-designer/runs/generative-current/workspace/a11y/status-mobile.json @@ -0,0 +1,35 @@ +{ + "name": "status", + "url": "file:///Users/forge/forge/v2/wt/w-ui-ux-designer-seller/sellers/ui-ux-designer/runs/generative-current/workspace/status.html", + "viewport": "mobile", + "engine": "axe-core 4.11.4", + "tags": [ + "wcag2a", + "wcag2aa", + "wcag21a", + "wcag21aa", + "wcag22aa" + ], + "generatedAt": "2026-09-09T17:50:48.460Z", + "loadProof": { + "title": "Nova service status", + "elements": 225, + "textLength": 1281, + "stylesheets": 2, + "url": "file:///Users/forge/forge/v2/wt/w-ui-ux-designer-seller/sellers/ui-ux-designer/runs/generative-current/workspace/status.html", + "rulesConsidered": 63, + "httpStatus": 200 + }, + "counts": { + "violations": 0, + "passes": 15, + "incomplete": 1, + "inapplicable": 47, + "rulesConsidered": 63, + "nodesFlagged": 0 + }, + "violations": [], + "incompleteIds": [ + "color-contrast" + ] +} diff --git a/sellers/ui-ux-designer/runs/generative-current/workspace/design-identity/contrast.md b/sellers/ui-ux-designer/runs/generative-current/workspace/design-identity/contrast.md new file mode 100644 index 00000000..02d0cbee --- /dev/null +++ b/sellers/ui-ux-designer/runs/generative-current/workspace/design-identity/contrast.md @@ -0,0 +1,27 @@ +# Contrast ledger — GENERATED + +Every number below is computed by `node tools/tokens.mjs --contrast` using the WCAG 2.x +relative-luminance formula over `design-identity/tokens.json`. Nothing here is typed by hand. +Regenerate after any color change; `scripts/smoke.sh` reruns it and fails the gate on a miss. + +Identity: **Plainsong** (PROPOSED — not adopted. See design-identity/IDENTITY.md §0.) +Generated by tools/tokens.mjs from tokens.json v0.1.0. + +``` +foreground background hex fg hex bg ratio min verdict use +ink-900 paper-000 #14181D #FFFFFF 17.82 4.5 PASS body text on page +ink-900 paper-100 #14181D #F7F8FA 16.77 4.5 PASS body text on subtle surface +ink-700 paper-000 #333B45 #FFFFFF 11.34 4.5 PASS secondary body text +ink-600 paper-000 #465162 #FFFFFF 8.03 4.5 PASS muted body text (floor case) +ink-500 paper-000 #5A6572 #FFFFFF 5.93 3 PASS large text / non-text UI only +paper-000 accent-600 #FFFFFF #1F5FD0 5.82 4.5 PASS text on primary button +accent-700 paper-000 #17499E #FFFFFF 8.46 4.5 PASS link text on page +accent-700 accent-100 #17499E #E4EDFC 7.18 4.5 PASS link text on accent surface +paper-000 success-600 #FFFFFF #14714A 6.02 4.5 PASS text on success fill +paper-000 warn-600 #FFFFFF #8A5A00 5.93 4.5 PASS text on warning fill +paper-000 danger-600 #FFFFFF #B3251E 6.56 4.5 PASS text on danger fill +focus-ring paper-000 #1F5FD0 #FFFFFF 5.82 3 PASS focus indicator vs page (WCAG 2.2 SC 1.4.11) +ink-300 paper-000 #868F9C #FFFFFF 3.27 3 PASS input border / non-text boundary +``` + +All 13 contracted pairs meet their stated minimum. diff --git a/sellers/ui-ux-designer/runs/generative-current/workspace/design-identity/tokens.css b/sellers/ui-ux-designer/runs/generative-current/workspace/design-identity/tokens.css new file mode 100644 index 00000000..fdba7b44 --- /dev/null +++ b/sellers/ui-ux-designer/runs/generative-current/workspace/design-identity/tokens.css @@ -0,0 +1,105 @@ +/* GENERATED by tools/tokens.mjs --css from design-identity/tokens.json. Do not hand-edit. */ +/* Identity: Plainsong — PROPOSED — not adopted. See design-identity/IDENTITY.md §0. */ +:root { + --color-ink-900: #14181D; + --color-ink-700: #333B45; + --color-ink-600: #465162; + --color-ink-500: #5A6572; + --color-ink-300: #868F9C; + --color-ink-200: #C2C9D2; + --color-ink-100: #D9DEE4; + --color-paper-000: #FFFFFF; + --color-paper-100: #F7F8FA; + --color-paper-200: #EDEFF3; + --color-paper-300: #E1E5EB; + --color-accent-700: #17499E; + --color-accent-600: #1F5FD0; + --color-accent-300: #8FB3EE; + --color-accent-100: #E4EDFC; + --color-success-700: #0F5A3B; + --color-success-600: #14714A; + --color-warn-700: #6E4700; + --color-warn-600: #8A5A00; + --color-danger-700: #8E1D17; + --color-danger-600: #B3251E; + --color-focus-ring: #1F5FD0; + --font-sans: "Inter var", Inter, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; + --font-mono: "IBM Plex Mono", ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; + --text-xs: 0.75rem; + --leading-xs: 1.5; + --tracking-xs: 0.01em; + --text-sm: 0.875rem; + --leading-sm: 1.5; + --tracking-sm: 0.005em; + --text-base: 1rem; + --leading-base: 1.6; + --tracking-base: 0; + --text-lg: 1.25rem; + --leading-lg: 1.5; + --tracking-lg: -0.005em; + --text-xl: 1.5625rem; + --leading-xl: 1.4; + --tracking-xl: -0.01em; + --text-2xl: 1.953rem; + --leading-2xl: 1.3; + --tracking-2xl: -0.015em; + --text-3xl: 2.441rem; + --leading-3xl: 1.2; + --tracking-3xl: -0.02em; + --text-4xl: 3.052rem; + --leading-4xl: 1.1; + --tracking-4xl: -0.025em; + --weight-regular: 400; + --weight-medium: 500; + --weight-semibold: 600; + --weight-bold: 700; + --space-0: 0px; + --space-1: 4px; + --space-2: 8px; + --space-3: 12px; + --space-4: 16px; + --space-5: 20px; + --space-6: 24px; + --space-8: 32px; + --space-10: 40px; + --space-12: 48px; + --space-16: 64px; + --space-20: 80px; + --space-24: 96px; + --space-32: 128px; + --space-0_5: 2px; + --radius-none: 0px; + --radius-sm: 2px; + --radius-md: 4px; + --radius-lg: 8px; + --radius-xl: 16px; + --radius-pill: 999px; + --shadow-sm: 0 1px 2px rgba(20,24,29,0.06), 0 1px 1px rgba(20,24,29,0.04); + --shadow-md: 0 2px 6px rgba(20,24,29,0.08), 0 1px 2px rgba(20,24,29,0.06); + --shadow-lg: 0 8px 24px rgba(20,24,29,0.12), 0 2px 6px rgba(20,24,29,0.06); + --duration-instant: 0ms; + --duration-fast: 120ms; + --duration-base: 180ms; + --duration-slow: 260ms; + --duration-deliberate: 400ms; + --ease-standard: cubic-bezier(0.2, 0, 0, 1); + --ease-enter: cubic-bezier(0, 0, 0, 1); + --ease-exit: cubic-bezier(0.3, 0, 1, 1); + --ease-linear: linear; + --target-min: 24px; + --target-recommended: 44px; + --focus-ring-width: 2px; + --focus-ring-offset: 2px; + --measure: 68ch; +} + +/* Rule 8 of IDENTITY.md is a token, not a suggestion. */ +@media (prefers-reduced-motion: reduce) { + :root { + --duration-instant: 1ms; + --duration-fast: 1ms; + --duration-base: 1ms; + --duration-slow: 1ms; + --duration-deliberate: 1ms; + } +} diff --git a/sellers/ui-ux-designer/runs/generative-current/workspace/design-identity/tokens.json b/sellers/ui-ux-designer/runs/generative-current/workspace/design-identity/tokens.json new file mode 100644 index 00000000..20a8d2cb --- /dev/null +++ b/sellers/ui-ux-designer/runs/generative-current/workspace/design-identity/tokens.json @@ -0,0 +1,90 @@ +{ + "$identity": "Plainsong", + "$status": "PROPOSED — not adopted. See design-identity/IDENTITY.md §0.", + "$version": "0.1.0", + "color": { + "ink-900": "#14181D", + "ink-700": "#333B45", + "ink-600": "#465162", + "ink-500": "#5A6572", + "ink-300": "#868F9C", + "ink-200": "#C2C9D2", + "ink-100": "#D9DEE4", + "paper-000": "#FFFFFF", + "paper-100": "#F7F8FA", + "paper-200": "#EDEFF3", + "paper-300": "#E1E5EB", + "accent-700": "#17499E", + "accent-600": "#1F5FD0", + "accent-300": "#8FB3EE", + "accent-100": "#E4EDFC", + "success-700": "#0F5A3B", + "success-600": "#14714A", + "warn-700": "#6E4700", + "warn-600": "#8A5A00", + "danger-700": "#8E1D17", + "danger-600": "#B3251E", + "focus-ring": "#1F5FD0" + }, + "contrastContract": [ + { "pair": ["ink-900", "paper-000"], "min": 4.5, "use": "body text on page" }, + { "pair": ["ink-900", "paper-100"], "min": 4.5, "use": "body text on subtle surface" }, + { "pair": ["ink-700", "paper-000"], "min": 4.5, "use": "secondary body text" }, + { "pair": ["ink-600", "paper-000"], "min": 4.5, "use": "muted body text (floor case)" }, + { "pair": ["ink-500", "paper-000"], "min": 3.0, "use": "large text / non-text UI only" }, + { "pair": ["paper-000", "accent-600"], "min": 4.5, "use": "text on primary button" }, + { "pair": ["accent-700", "paper-000"], "min": 4.5, "use": "link text on page" }, + { "pair": ["accent-700", "accent-100"], "min": 4.5, "use": "link text on accent surface" }, + { "pair": ["paper-000", "success-600"], "min": 4.5, "use": "text on success fill" }, + { "pair": ["paper-000", "warn-600"], "min": 4.5, "use": "text on warning fill" }, + { "pair": ["paper-000", "danger-600"], "min": 4.5, "use": "text on danger fill" }, + { "pair": ["focus-ring", "paper-000"], "min": 3.0, "use": "focus indicator vs page (WCAG 2.2 SC 1.4.11)" }, + { "pair": ["ink-300", "paper-000"], "min": 3.0, "use": "input border / non-text boundary" } + ], + "type": { + "familySans": "\"Inter var\", Inter, -apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, \"Helvetica Neue\", Arial, sans-serif", + "familyMono": "\"IBM Plex Mono\", ui-monospace, SFMono-Regular, Menlo, Consolas, monospace", + "baseSizePx": 16, + "ratio": 1.25, + "scale": { + "xs": { "rem": 0.75, "px": 12, "lineHeight": 1.5, "tracking": "0.01em" }, + "sm": { "rem": 0.875, "px": 14, "lineHeight": 1.5, "tracking": "0.005em" }, + "base": { "rem": 1, "px": 16, "lineHeight": 1.6, "tracking": "0" }, + "lg": { "rem": 1.25, "px": 20, "lineHeight": 1.5, "tracking": "-0.005em" }, + "xl": { "rem": 1.5625, "px": 25, "lineHeight": 1.4, "tracking": "-0.01em" }, + "2xl": { "rem": 1.953, "px": 31, "lineHeight": 1.3, "tracking": "-0.015em" }, + "3xl": { "rem": 2.441, "px": 39, "lineHeight": 1.2, "tracking": "-0.02em" }, + "4xl": { "rem": 3.052, "px": 49, "lineHeight": 1.1, "tracking": "-0.025em" } + }, + "weight": { "regular": 400, "medium": 500, "semibold": 600, "bold": 700 }, + "measureCh": { "min": 45, "ideal": 68, "max": 80 } + }, + "space": { + "unitPx": 4, + "scale": { "0": 0, "0.5": 2, "1": 4, "2": 8, "3": 12, "4": 16, "5": 20, "6": 24, "8": 32, "10": 40, "12": 48, "16": 64, "20": 80, "24": 96, "32": 128 } + }, + "radius": { "none": 0, "sm": 2, "md": 4, "lg": 8, "xl": 16, "pill": 999 }, + "shadow": { + "sm": "0 1px 2px rgba(20,24,29,0.06), 0 1px 1px rgba(20,24,29,0.04)", + "md": "0 2px 6px rgba(20,24,29,0.08), 0 1px 2px rgba(20,24,29,0.06)", + "lg": "0 8px 24px rgba(20,24,29,0.12), 0 2px 6px rgba(20,24,29,0.06)" + }, + "motion": { + "durationMs": { "instant": 0, "fast": 120, "base": 180, "slow": 260, "deliberate": 400 }, + "easing": { + "standard": "cubic-bezier(0.2, 0, 0, 1)", + "enter": "cubic-bezier(0, 0, 0, 1)", + "exit": "cubic-bezier(0.3, 0, 1, 1)", + "linear": "linear" + }, + "reducedMotionDurationMs": 1, + "maxTravelPx": 24 + }, + "target": { "minPx": 24, "recommendedPx": 44, "minGapPx": 8 }, + "focus": { "ringWidthPx": 2, "ringOffsetPx": 2, "style": "solid" }, + "breakpointPx": { "sm": 480, "md": 768, "lg": 1024, "xl": 1280 }, + "viewport": { + "desktop": { "width": 1280, "height": 900, "deviceScaleFactor": 1 }, + "mobile": { "width": 390, "height": 844, "deviceScaleFactor": 2 } + } +} diff --git a/sellers/ui-ux-designer/runs/generative-current/workspace/shots/status-desktop.png b/sellers/ui-ux-designer/runs/generative-current/workspace/shots/status-desktop.png new file mode 100644 index 00000000..45fb0aa3 Binary files /dev/null and b/sellers/ui-ux-designer/runs/generative-current/workspace/shots/status-desktop.png differ diff --git a/sellers/ui-ux-designer/runs/generative-current/workspace/shots/status-mobile.png b/sellers/ui-ux-designer/runs/generative-current/workspace/shots/status-mobile.png new file mode 100644 index 00000000..2a640ece Binary files /dev/null and b/sellers/ui-ux-designer/runs/generative-current/workspace/shots/status-mobile.png differ diff --git a/sellers/ui-ux-designer/runs/generative-current/workspace/status.html b/sellers/ui-ux-designer/runs/generative-current/workspace/status.html new file mode 100644 index 00000000..8ed561a9 --- /dev/null +++ b/sellers/ui-ux-designer/runs/generative-current/workspace/status.html @@ -0,0 +1,247 @@ + + + + + + + Nova service status + + + +
+
+
Nova
+ Service status +
+
+ +
+
+

All systems operational

+

Everything is working normally.

+

All Nova services are available. We’re not aware of any current interruptions.

+
+ +
+
+

Components

+

Uptime over the past 90 days

+
+
+
+

API

Operational
+
90-day uptime99.99%
+

API uptime was 99.99 percent over the last 90 days, with one brief interruption.

+ + +
+
+

Dashboard

Operational
+
90-day uptime100%
+

Dashboard uptime was 100 percent over the last 90 days, with no interruptions.

+ + +
+
+

Webhooks

Operational
+
90-day uptime99.96%
+

Webhooks uptime was 99.96 percent over the last 90 days, including one degraded period.

+ + +
+
+

Scheduled Jobs

Operational
+
90-day uptime99.98%
+

Scheduled Jobs uptime was 99.98 percent over the last 90 days, including one delayed period.

+ + +
+
+
+ +
+
+

Recent incidents

+

Last 30 days

+
+
+
+
+

Resolved

+

Delayed webhook deliveries

+

A queue backlog delayed some webhook deliveries; all events were delivered automatically after capacity was restored.

+
+
    +
  • Started
  • +
  • Duration18 minutes
  • +
+
+
+
+

Resolved

+

Scheduled jobs started late

+

A database maintenance task caused a small number of jobs to start late; no scheduled work was lost.

+
+
    +
  • Started
  • +
  • Duration11 minutes
  • +
+
+
+
+
+ +
+

Last refreshed

+

Nova status

+
+ + + + diff --git a/sellers/ui-ux-designer/runs/generative-current/workspace/tools/a11y.mjs b/sellers/ui-ux-designer/runs/generative-current/workspace/tools/a11y.mjs new file mode 100644 index 00000000..84506a5e --- /dev/null +++ b/sellers/ui-ux-designer/runs/generative-current/workspace/tools/a11y.mjs @@ -0,0 +1,171 @@ +#!/usr/bin/env node +/** + * a11y.mjs — axe-core accessibility run that cannot report a false clean bill of health. + * + * The failure this file exists to prevent: axe returns `violations: []` for a page that never + * loaded. A 404 body, a blank document, a navigation that timed out before paint — all produce + * zero violations, and a report that says "0 issues" is indistinguishable from a genuinely + * accessible page unless you check that axe had a page to look at. + * + * So before any result is believed, the page must clear LOAD PROOF: + * - HTTP response was ok() + * - document.title is non-empty + * - > MIN_ELEMENTS elements in the DOM + * - > MIN_TEXT characters of rendered body text + * - axe's own rule accounting (passes + violations + incomplete + inapplicable) > MIN_RULES, + * which proves the engine actually executed its ruleset rather than bailing early + * A zero-violation result that fails load proof is reported as FAILED, never as clean. + * + * Usage: + * node tools/a11y.mjs --url --name [--max N] [--expect-min N] [--out DIR] + * --max N exit nonzero if violations > N (default 0) + * --expect-min N exit nonzero if violations < N — for asserting a known-bad page really is bad + */ +import { mkdir, writeFile } from "node:fs/promises"; +import { join } from "node:path"; +import { chromium } from "playwright"; +import AxeBuilder from "@axe-core/playwright"; +import { PKG, TOKENS } from "./tokens.mjs"; + +export const TAGS = ["wcag2a", "wcag2aa", "wcag21a", "wcag21aa", "wcag22aa"]; +export const MIN_ELEMENTS = 20; +export const MIN_TEXT = 200; +export const MIN_RULES = 50; + +/** + * Run axe against one URL at one viewport. Returns the full axe result plus the load proof. + * Throws if load proof fails — the caller never gets the chance to read a hollow zero. + */ +export async function audit({ url, name, viewport = "desktop", outDir, browser: given } = {}) { + const vp = TOKENS.viewport[viewport]; + if (!vp) throw new Error(`unknown viewport ${viewport}`); + const out = outDir || join(PKG, "evidence", "a11y"); + await mkdir(out, { recursive: true }); + const browser = given || (await chromium.launch()); + try { + const context = await browser.newContext({ + viewport: { width: vp.width, height: vp.height }, + deviceScaleFactor: vp.deviceScaleFactor, + isMobile: viewport === "mobile", + reducedMotion: "reduce", + }); + const page = await context.newPage(); + const resp = await page.goto(url, { waitUntil: "load", timeout: 30_000 }); + if (!resp) throw new Error(`${name}: no response for ${url}`); + if (!resp.ok()) throw new Error(`${name}: HTTP ${resp.status()} for ${url}`); + await page.waitForLoadState("networkidle", { timeout: 30_000 }); + + const proof = await page.evaluate(() => ({ + title: document.title, + elements: document.querySelectorAll("*").length, + textLength: (document.body?.innerText || "").trim().length, + stylesheets: document.styleSheets.length + document.querySelectorAll("style").length, + url: location.href, + })); + + const results = await new AxeBuilder({ page }).withTags(TAGS).analyze(); + + const rulesConsidered = + results.passes.length + + results.violations.length + + results.incomplete.length + + results.inapplicable.length; + + const loadProof = { ...proof, rulesConsidered, httpStatus: resp.status() }; + const problems = []; + if (!proof.title || !proof.title.trim()) problems.push("document.title is empty"); + if (proof.elements <= MIN_ELEMENTS) + problems.push(`only ${proof.elements} DOM elements (need > ${MIN_ELEMENTS})`); + if (proof.textLength <= MIN_TEXT) + problems.push(`only ${proof.textLength} chars of rendered text (need > ${MIN_TEXT})`); + if (proof.stylesheets === 0) problems.push("zero stylesheets"); + if (rulesConsidered <= MIN_RULES) + problems.push(`axe considered only ${rulesConsidered} rules (need > ${MIN_RULES})`); + if (problems.length) + throw new Error( + `${name}: LOAD PROOF FAILED — ${problems.join("; ")}. ` + + `Any accessibility verdict from this run is void, including "${results.violations.length} violations".`, + ); + + const summary = { + name, + url, + viewport, + engine: `axe-core ${results.testEngine.version}`, + tags: TAGS, + generatedAt: new Date().toISOString(), + loadProof, + counts: { + violations: results.violations.length, + passes: results.passes.length, + incomplete: results.incomplete.length, + inapplicable: results.inapplicable.length, + rulesConsidered, + nodesFlagged: results.violations.reduce((n, v) => n + v.nodes.length, 0), + }, + violations: results.violations.map((v) => ({ + id: v.id, + impact: v.impact, + help: v.help, + helpUrl: v.helpUrl, + tags: v.tags.filter((t) => t.startsWith("wcag")), + nodeCount: v.nodes.length, + sampleTargets: v.nodes.slice(0, 4).map((n) => n.target.join(" ")), + })), + incompleteIds: results.incomplete.map((i) => i.id), + }; + + const file = join(out, `${name}-${viewport}.json`); + await writeFile(file, JSON.stringify(summary, null, 2) + "\n"); + await context.close(); + return { ...summary, file }; + } finally { + if (!given) await browser.close(); + } +} + +if (process.argv[1] && process.argv[1].endsWith("a11y.mjs")) { + const arg = (k, d) => { + const i = process.argv.indexOf(k); + return i > -1 ? process.argv[i + 1] : d; + }; + const url = arg("--url"); + const name = arg("--name", "page"); + const max = Number(arg("--max", "0")); + const expectMin = Number(arg("--expect-min", "0")); + if (!url) { + console.error( + "usage: node tools/a11y.mjs --url --name [--max N] [--expect-min N] [--out DIR]", + ); + process.exit(2); + } + try { + const r = await audit({ url, name, viewport: arg("--viewport", "desktop"), outDir: arg("--out") }); + console.log( + `${r.name}/${r.viewport} — ${r.engine}, load proof OK ` + + `(title="${r.loadProof.title}", ${r.loadProof.elements} elements, ` + + `${r.loadProof.textLength} chars text, ${r.counts.rulesConsidered} rules considered)`, + ); + console.log( + ` violations=${r.counts.violations} (${r.counts.nodesFlagged} nodes) ` + + `passes=${r.counts.passes} incomplete=${r.counts.incomplete}`, + ); + for (const v of r.violations) + console.log(` - [${v.impact}] ${v.id}: ${v.help} (${v.nodeCount} nodes)`); + console.log(` -> ${r.file}`); + if (r.counts.violations > max) { + console.error(`A11Y GATE FAILED — ${r.counts.violations} violations exceeds --max ${max}`); + process.exit(1); + } + if (r.counts.violations < expectMin) { + console.error( + `A11Y GATE FAILED — expected at least ${expectMin} violations on this page but found ` + + `${r.counts.violations}. Either the page changed or the check is not really running.`, + ); + process.exit(1); + } + } catch (e) { + console.error(`A11Y RUN FAILED — ${e.message}`); + process.exit(1); + } +} diff --git a/sellers/ui-ux-designer/runs/generative-current/workspace/tools/preview.mjs b/sellers/ui-ux-designer/runs/generative-current/workspace/tools/preview.mjs new file mode 100644 index 00000000..4f2a0e5e --- /dev/null +++ b/sellers/ui-ux-designer/runs/generative-current/workspace/tools/preview.mjs @@ -0,0 +1,94 @@ +#!/usr/bin/env node +/** + * preview.mjs — a dependency-free static file server rooted at the PACKAGE directory, so a sample + * page can link `/design-identity/tokens.css` exactly as a real app would link a token bundle. + * + * Binds 127.0.0.1 on an ephemeral port. Loopback only, no directory traversal above the root, no + * upload path, no write path — that is the whole "least privilege" story for the preview leg. + * + * Used as a module (`startPreview()`), or standalone: `node tools/preview.mjs [--port N]`. + */ +import { createServer } from "node:http"; +import { createReadStream } from "node:fs"; +import { stat } from "node:fs/promises"; +import { join, extname, normalize, sep } from "node:path"; +import { fileURLToPath } from "node:url"; +import { dirname } from "node:path"; + +const ROOT = join(dirname(fileURLToPath(import.meta.url)), ".."); + +const TYPES = { + ".html": "text/html; charset=utf-8", + ".css": "text/css; charset=utf-8", + ".js": "text/javascript; charset=utf-8", + ".mjs": "text/javascript; charset=utf-8", + ".json": "application/json; charset=utf-8", + ".svg": "image/svg+xml", + ".png": "image/png", + ".jpg": "image/jpeg", + ".webp": "image/webp", + ".woff2": "font/woff2", +}; + +/** Resolve a URL path inside ROOT, or null if it escapes. */ +function resolveInRoot(urlPath) { + const decoded = decodeURIComponent(urlPath.split("?")[0]); + const rel = normalize(decoded).replace(/^(\.\.[/\\])+/, "").replace(/^[/\\]+/, ""); + const abs = join(ROOT, rel); + if (abs !== ROOT && !abs.startsWith(ROOT + sep)) return null; + return abs; +} + +export function startPreview({ port = 0 } = {}) { + const server = createServer(async (req, res) => { + if (req.method !== "GET" && req.method !== "HEAD") { + res.writeHead(405, { allow: "GET, HEAD" }); + return res.end("method not allowed"); + } + let abs = resolveInRoot(req.url || "/"); + if (!abs) { + res.writeHead(403); + return res.end("forbidden"); + } + try { + let info = await stat(abs); + if (info.isDirectory()) { + abs = join(abs, "index.html"); + info = await stat(abs); + } + res.writeHead(200, { + "content-type": TYPES[extname(abs).toLowerCase()] || "application/octet-stream", + "content-length": info.size, + "cache-control": "no-store", + }); + if (req.method === "HEAD") return res.end(); + createReadStream(abs).pipe(res); + } catch { + res.writeHead(404, { "content-type": "text/plain; charset=utf-8" }); + res.end("not found"); + } + }); + + return new Promise((resolve, reject) => { + server.once("error", reject); + server.listen(port, "127.0.0.1", () => { + const actual = server.address().port; + resolve({ + port: actual, + origin: `http://127.0.0.1:${actual}`, + url: (p) => `http://127.0.0.1:${actual}${p.startsWith("/") ? p : "/" + p}`, + close: () => new Promise((r) => server.close(r)), + }); + }); + }); +} + +if (process.argv[1] && process.argv[1].endsWith("preview.mjs")) { + const i = process.argv.indexOf("--port"); + const port = i > -1 ? Number(process.argv[i + 1]) : 0; + const s = await startPreview({ port }); + console.log(`preview serving ${ROOT}`); + console.log(`origin ${s.origin}`); + console.log(`before ${s.url("/samples/redesign/before/")}`); + console.log(`after ${s.url("/samples/redesign/after/")}`); +} diff --git a/sellers/ui-ux-designer/runs/generative-current/workspace/tools/shoot.mjs b/sellers/ui-ux-designer/runs/generative-current/workspace/tools/shoot.mjs new file mode 100644 index 00000000..7dba11c3 --- /dev/null +++ b/sellers/ui-ux-designer/runs/generative-current/workspace/tools/shoot.mjs @@ -0,0 +1,166 @@ +#!/usr/bin/env node +/** + * shoot.mjs — desktop + mobile screenshots that PROVE they contain a rendered page. + * + * The failure this file exists to prevent: a screenshot step that "succeeds" while producing a + * blank white PNG, because the URL 404'd, the CSS never loaded, or the page was captured before + * paint. A nonzero byte count does not rule any of that out — an all-white 1280×900 PNG is a + * perfectly valid, perfectly worthless ~6KB file. + * + * So every capture is decoded with pngjs and must clear ALL of: + * - dimensions match the requested viewport (× deviceScaleFactor) + * - byte length >= MIN_BYTES + * - >= MIN_COLORS distinct RGB colors + * - the most common color covers <= MAX_DOMINANCE of pixels (catches near-blank) + * - >= MIN_INK_RATIO of pixels are meaningfully darker than the page background (catches + * "styled background painted, no text rendered") + * Any miss throws, and the CLI exits nonzero. + * + * Usage: node tools/shoot.mjs --url --name [--out DIR] [--full] + */ +import { mkdir, writeFile, stat } from "node:fs/promises"; +import { join } from "node:path"; +import { chromium } from "playwright"; +import { PNG } from "pngjs"; +import { PKG, TOKENS } from "./tokens.mjs"; + +export const MIN_BYTES = 3000; +export const MIN_COLORS = 12; +export const MAX_DOMINANCE = 0.985; +export const MIN_INK_RATIO = 0.002; + +/** + * Decode a PNG buffer and describe what is actually in it. + * Pure measurement — no verdict, so the thresholds live in one visible place below. + */ +export function analyzePng(buffer) { + const png = PNG.sync.read(buffer); + const counts = new Map(); + const total = png.width * png.height; + let dark = 0; + for (let i = 0; i < png.data.length; i += 4) { + const r = png.data[i]; + const g = png.data[i + 1]; + const b = png.data[i + 2]; + const key = (r << 16) | (g << 8) | b; + counts.set(key, (counts.get(key) || 0) + 1); + // Rec.601 luma; "ink" = anything clearly darker than a light surface. + if (0.299 * r + 0.587 * g + 0.114 * b < 170) dark += 1; + } + let top = 0; + for (const n of counts.values()) if (n > top) top = n; + return { + width: png.width, + height: png.height, + pixels: total, + distinctColors: counts.size, + dominance: top / total, + inkRatio: dark / total, + }; +} + +/** Throw unless the image demonstrably contains a rendered page. */ +export function assertRendered(buffer, label, expect) { + const a = analyzePng(buffer); + const fail = (why) => + new Error( + `${label}: ${why} (bytes=${buffer.length} ${a.width}x${a.height} colors=${a.distinctColors} ` + + `dominance=${a.dominance.toFixed(4)} ink=${a.inkRatio.toFixed(4)})`, + ); + if (buffer.length < MIN_BYTES) throw fail(`screenshot is only ${buffer.length} bytes`); + if (expect) { + // ±2px: Chromium rounds emulated dimensions by a pixel at deviceScaleFactor 2 (observed on the + // `before` sample, which has no viewport meta). The tolerance still catches a wrong viewport, + // which is what this check is for; it is not a pixel-accuracy assertion. + if (Math.abs(a.width - expect.width) > 2) + throw fail(`width ${a.width} != expected ${expect.width}`); + if (!expect.full && Math.abs(a.height - expect.height) > 2) + throw fail(`height ${a.height} != expected ${expect.height}`); + } + if (a.distinctColors < MIN_COLORS) throw fail(`only ${a.distinctColors} distinct colors — blank?`); + if (a.dominance > MAX_DOMINANCE) + throw fail(`${(a.dominance * 100).toFixed(2)}% of pixels are one color — effectively blank`); + if (a.inkRatio < MIN_INK_RATIO) + throw fail(`only ${(a.inkRatio * 100).toFixed(3)}% dark pixels — nothing legible rendered`); + return a; +} + +/** + * Capture one URL at both viewports. Returns [{ label, file, bytes, analysis }]. + * `page.goto` is checked for HTTP status too: a 404 body renders fine and screenshots fine. + */ +export async function shoot({ url, name, outDir, full = false, browser: given } = {}) { + const out = outDir || join(PKG, "evidence", "screenshots"); + await mkdir(out, { recursive: true }); + const browser = given || (await chromium.launch()); + const results = []; + try { + for (const [label, vp] of Object.entries(TOKENS.viewport)) { + const context = await browser.newContext({ + viewport: { width: vp.width, height: vp.height }, + deviceScaleFactor: vp.deviceScaleFactor, + isMobile: label === "mobile", + hasTouch: label === "mobile", + reducedMotion: "reduce", + }); + const page = await context.newPage(); + const errors = []; + page.on("pageerror", (e) => errors.push(String(e))); + const resp = await page.goto(url, { waitUntil: "load", timeout: 30_000 }); + if (!resp) throw new Error(`${name}/${label}: no response object for ${url}`); + if (!resp.ok()) throw new Error(`${name}/${label}: HTTP ${resp.status()} for ${url}`); + await page.waitForLoadState("networkidle", { timeout: 30_000 }); + // A stylesheet that 404s leaves the DOM intact and the design gone. Catch it here. + const sheets = await page.evaluate( + () => document.styleSheets.length + document.querySelectorAll("style").length, + ); + if (sheets === 0) throw new Error(`${name}/${label}: page loaded with zero stylesheets`); + const buffer = await page.screenshot({ fullPage: full }); + const analysis = assertRendered(buffer, `${name}/${label}`, { + width: vp.width * vp.deviceScaleFactor, + height: vp.height * vp.deviceScaleFactor, + full, + }); + const file = join(out, `${name}-${label}.png`); + await writeFile(file, buffer); + const onDisk = await stat(file); + if (onDisk.size !== buffer.length) + throw new Error(`${name}/${label}: wrote ${buffer.length} bytes, disk has ${onDisk.size}`); + if (errors.length) console.warn(` ! ${name}/${label} page errors: ${errors.join(" | ")}`); + results.push({ label, file, bytes: buffer.length, analysis, viewport: vp }); + await context.close(); + } + } finally { + if (!given) await browser.close(); + } + return results; +} + +if (process.argv[1] && process.argv[1].endsWith("shoot.mjs")) { + const arg = (k, d) => { + const i = process.argv.indexOf(k); + return i > -1 ? process.argv[i + 1] : d; + }; + const url = arg("--url"); + const name = arg("--name", "page"); + if (!url) { + console.error("usage: node tools/shoot.mjs --url --name [--out DIR] [--full]"); + process.exit(2); + } + try { + const res = await shoot({ + url, + name, + outDir: arg("--out"), + full: process.argv.includes("--full"), + }); + for (const r of res) + console.log( + `OK ${name}/${r.label} ${r.analysis.width}x${r.analysis.height} ${r.bytes}B ` + + `colors=${r.analysis.distinctColors} ink=${(r.analysis.inkRatio * 100).toFixed(2)}% -> ${r.file}`, + ); + } catch (e) { + console.error(`SCREENSHOT FAILED — ${e.message}`); + process.exit(1); + } +} diff --git a/sellers/ui-ux-designer/runs/generative-current/workspace/tools/tokens.mjs b/sellers/ui-ux-designer/runs/generative-current/workspace/tools/tokens.mjs new file mode 100644 index 00000000..de8f957e --- /dev/null +++ b/sellers/ui-ux-designer/runs/generative-current/workspace/tools/tokens.mjs @@ -0,0 +1,8 @@ +import { readFile } from "node:fs/promises"; +import { dirname, resolve } from "node:path"; +import { fileURLToPath } from "node:url"; + +export const PKG = resolve(dirname(fileURLToPath(import.meta.url)), ".."); +export const TOKENS = JSON.parse( + await readFile(resolve(PKG, "design-identity/tokens.json"), "utf8"), +); diff --git a/sellers/ui-ux-designer/runs/generative-current/workspace/tools/vdiff.mjs b/sellers/ui-ux-designer/runs/generative-current/workspace/tools/vdiff.mjs new file mode 100644 index 00000000..f18aa268 --- /dev/null +++ b/sellers/ui-ux-designer/runs/generative-current/workspace/tools/vdiff.mjs @@ -0,0 +1,120 @@ +#!/usr/bin/env node +/** + * vdiff.mjs — pixel comparison between two PNGs, used two ways: + * 1. before-vs-after: prove the redesign actually changed the rendering (--min-ratio). + * 2. regression: prove a rerender did NOT change (--max-ratio), for catching drift. + * + * The failure this file exists to prevent: a "visual comparison" step that reports 0.00% changed + * and is read as "matches", when in fact both inputs were the same blank image, or one was never + * regenerated. So a zero ratio is a FAILURE by default in before/after mode, and both inputs are + * checked for actual content before they are compared at all. + * + * Images of differing size are compared over their overlapping region, with the size mismatch + * itself reported — never silently resized, which would invent pixels. + * + * Usage: node tools/vdiff.mjs --a --b --out [--min-ratio 0.02] [--max-ratio 1] + */ +import { readFile, writeFile, mkdir } from "node:fs/promises"; +import { dirname, join } from "node:path"; +import { PNG } from "pngjs"; +import pixelmatch from "pixelmatch"; +import { PKG } from "./tokens.mjs"; +import { analyzePng, MIN_COLORS, MAX_DOMINANCE } from "./shoot.mjs"; + +/** Compare two PNG files. Returns { width, height, changed, total, ratio, sizeMismatch, diffFile }. */ +export async function vdiff({ a, b, out, threshold = 0.1 } = {}) { + const [bufA, bufB] = await Promise.all([readFile(a), readFile(b)]); + + // Both inputs must contain something. Comparing two blanks is not a comparison. + for (const [label, buf] of [ + ["a", bufA], + ["b", bufB], + ]) { + const an = analyzePng(buf); + if (an.distinctColors < MIN_COLORS || an.dominance > MAX_DOMINANCE) + throw new Error( + `input ${label} looks blank (colors=${an.distinctColors}, dominance=${an.dominance.toFixed(4)}) — ` + + `refusing to compare blank images`, + ); + } + + const pngA = PNG.sync.read(bufA); + const pngB = PNG.sync.read(bufB); + const width = Math.min(pngA.width, pngB.width); + const height = Math.min(pngA.height, pngB.height); + const sizeMismatch = + pngA.width !== pngB.width || pngA.height !== pngB.height + ? { a: `${pngA.width}x${pngA.height}`, b: `${pngB.width}x${pngB.height}` } + : null; + + // Crop both to the overlap rather than scaling: scaling would fabricate pixel values. + const crop = (png) => { + const c = new PNG({ width, height }); + for (let y = 0; y < height; y++) { + const from = (png.width * y) << 2; + png.data.copy(c.data, (width * y) << 2, from, from + (width << 2)); + } + return c; + }; + const cA = crop(pngA); + const cB = crop(pngB); + const diff = new PNG({ width, height }); + const changed = pixelmatch(cA.data, cB.data, diff.data, width, height, { + threshold, + includeAA: false, + alpha: 0.25, + diffColor: [211, 37, 30], + }); + + const diffFile = out || join(PKG, "evidence", "diff", "diff.png"); + await mkdir(dirname(diffFile), { recursive: true }); + await writeFile(diffFile, PNG.sync.write(diff)); + + const total = width * height; + return { width, height, changed, total, ratio: changed / total, sizeMismatch, diffFile }; +} + +if (process.argv[1] && process.argv[1].endsWith("vdiff.mjs")) { + const arg = (k, d) => { + const i = process.argv.indexOf(k); + return i > -1 ? process.argv[i + 1] : d; + }; + const a = arg("--a"); + const b = arg("--b"); + if (!a || !b) { + console.error( + "usage: node tools/vdiff.mjs --a --b [--out ] [--min-ratio R] [--max-ratio R]", + ); + process.exit(2); + } + const minRatio = Number(arg("--min-ratio", "0")); + const maxRatio = Number(arg("--max-ratio", "1")); + try { + const r = await vdiff({ a, b, out: arg("--out"), threshold: Number(arg("--threshold", "0.1")) }); + if (r.sizeMismatch) + console.log( + ` note: sizes differ (a=${r.sizeMismatch.a} b=${r.sizeMismatch.b}); compared the ` + + `${r.width}x${r.height} overlap`, + ); + console.log( + `changed ${r.changed} of ${r.total} px = ${(r.ratio * 100).toFixed(2)}% -> ${r.diffFile}`, + ); + if (r.ratio < minRatio) { + console.error( + `VISUAL DIFF FAILED — ${(r.ratio * 100).toFixed(2)}% changed is below --min-ratio ` + + `${(minRatio * 100).toFixed(2)}%. Nothing meaningfully changed, so the redesign did not apply.`, + ); + process.exit(1); + } + if (r.ratio > maxRatio) { + console.error( + `VISUAL DIFF FAILED — ${(r.ratio * 100).toFixed(2)}% changed exceeds --max-ratio ` + + `${(maxRatio * 100).toFixed(2)}%`, + ); + process.exit(1); + } + } catch (e) { + console.error(`VISUAL DIFF FAILED — ${e.message}`); + process.exit(1); + } +} diff --git a/sellers/ui-ux-designer/samples/redesign/RATIONALE.md b/sellers/ui-ux-designer/samples/redesign/RATIONALE.md new file mode 100644 index 00000000..fd1aa7f3 --- /dev/null +++ b/sellers/ui-ux-designer/samples/redesign/RATIONALE.md @@ -0,0 +1,86 @@ +# Sample redesign — Meridian invoices screen + +Invented product, invented copy, authored here. `before/index.html` is deliberately flawed; +`after/index.html` is the same screen rebuilt on the Plainsong tokens. + +Screenshots (regenerated by the gate, digests in `evidence/MANIFEST.md`): + +| | desktop 1280×900 | mobile 390×844 @2× | +|---|---|---| +| before | `evidence/screenshots/meridian-invoices-baseline-desktop.png` | `…-baseline-mobile.png` | +| after | `evidence/screenshots/meridian-invoices-candidate-desktop.png` | `…-candidate-mobile.png` | +| diff | `evidence/diff/meridian-invoices-desktop.png` (8.51% of pixels) | `…-mobile.png` (8.25%) | + +## What the machine found + +axe-core 4.11.4, tags `wcag2a wcag2aa wcag21a wcag21aa wcag22aa`, at both viewports. + +**Before — 4 violations across 37 nodes:** + +- `color-contrast` (serious, **34 nodes**) — the whole page. Body text `#8b93a1` on white is + 2.72:1; nav links `#b9d2ee` on `#2b6cb0` and the ghost buttons at `#aab6c8` are worse. +- `html-has-lang` (serious, 1) — no `lang`, so screen readers guess pronunciation. +- `image-alt` (critical, 1) — the payments badge has no alt text. +- `button-name` (critical, 1) — the empty icon button in the header has no accessible name at all. + +**After — 0 violations, 29 passes desktop / 30 mobile**, with load proof recorded (103 elements, +1045 characters of rendered text, 62 rules considered). The zero means axe ran on a real page. + +## What the machine could not find + +This is the part that matters, and it is why the identity says automated checks are a floor. +Each of these is a genuine defect in `before/` that **axe reported nothing about**: + +- **Placeholder-as-label.** Both filter inputs are labelled only by `placeholder`. axe's `label` + rule *passes*, because a placeholder does supply an accessible name — but the name vanishes the + moment the user types, and it never has a visible persistent label. Fixed with real `