Skip to content

feat(app): mvp-visual-verify — OCR + palette + pixel-diff + expectation layer over audit:app#14437

Merged
lalalune merged 5 commits into
developfrom
feat/mvp-visual-verify-harness
Jul 5, 2026
Merged

feat(app): mvp-visual-verify — OCR + palette + pixel-diff + expectation layer over audit:app#14437
lalalune merged 5 commits into
developfrom
feat/mvp-visual-verify-harness

Conversation

@lalalune

@lalalune lalalune commented Jul 5, 2026

Copy link
Copy Markdown
Member

mvp-visual-verify — a higher-fidelity visual-verification layer over audit:app

Evidence tooling (not a new CI gate) that post-processes the screenshots audit:app already captures (packages/app/aesthetic-audit-output/<viewport>/<slug>.png) into a per-state readout a reviewer can trust without re-eyeballing 200 PNGs. For every captured state it produces OCR text, a dominant-color palette, a pixel-diff vs a committed baseline, and a declarative pass/fail over per-state expectations — then writes report.json + an annotated contact-sheet.html.

What it adds

  • packages/app/scripts/mvp-visual-verify.mjs — the post-processor. Discovers viewport dirs under the audit output, runs OCR + palette + diff + expectations per <viewport>/<slug>.png, writes a separate mvp-verify/ tree (report.json, contact-sheet.html, diffs/, baseline/) so it never collides with the audit's own report.
  • scripts/mvp-visual-verify/ocr.mjs — on-screen text via the system tesseract binary, spawned through child_process. Honest { available:false, reason } degrade when the binary is absent (the OCR column renders "N/A", never fabricated text).
  • scripts/mvp-visual-verify/dominant-color.mjs + color-bucket.mjs — sharp decodes to raw RGBA; quantizePalette (pure) buckets to a coarse grid and returns the top-K swatches with brand-bucket labels. The classifier is a faithful ESM port of the audit's own bucket() (test/ui-smoke/aesthetic-audit-rules.ts) and is parity-tested against it across an RGB-cube sweep so the no-blue rule cannot drift between capture and post-process.
  • scripts/mvp-visual-verify/diff.mjs — pixel diff with zero new deps: sharp decodes both PNGs, comparePixels (pure) counts changed pixels + per-channel mean-abs delta and paints a magenta-on-dimmed-gray highlight buffer, summarizeDiff (pure) reduces to %. A missing baseline records status:"new" — never a fabricated 0%.
  • scripts/mvp-visual-verify/expectation-eval.mjs + expectations.json — declarative per-slug specs (expected OCR substrings present/absent, brand-orange accent, no blue, no horizontal overflow) → pass/fail with reasons. A genuinely-unavailable input (OCR engine absent, report lacks the overflow field) resolves to skip, never a silent pass.
  • scripts/mvp-visual-verify/overflow-invariant-demo.mjs — a real headless-chromium demonstration that the horizontal-overflow invariant catches the WS5 transcript bug.
  • test/audit/mvp-visual-verify.test.ts — 20 unit tests over the pure functions (parity sweep, quantizer, diff summarizer/comparator, expectation evaluator).
  • test/ui-smoke/all-views-aesthetic-audit.spec.ts (modified) — records a document-level horizontal-overflow invariant (documentElement.scrollWidth − innerWidth) into report.json for every view; hard-gated only under ELIZA_AUDIT_APP_STRICT. A measurement failure throws (no fabricated "0 = healthy").
  • package.jsonmvp:visual-verify, audit:app:verify (= audit:app && mvp:visual-verify), mvp:visual-verify:overflow-demo.

Engines used

Column Engine Honest degrade
OCR system tesseract v5.5.2 (/opt/homebrew/bin/tesseract), spawned tesseract <png> stdout -l eng. tesseract.js is NOT used (not installed). {available:false} → "N/A (tesseract not found)"
Palette sharp raw RGBA → hue-based bucketing (ported + parity-pinned to the audit's bucket()) throws on malformed buffer
Diff sharp raw decode + hand-rolled compare (pixelmatch deliberately not installed) missing baseline → status:"new"
Overflow Playwright page.evaluate document-level probe measurement failure throws

Proof (real screenshots, opened by hand)

Ran against the checkout's real audit:app output — 195 states in ~68s, 3 real findings caught:

[mvp-visual-verify] OCR engine: /opt/homebrew/bin/tesseract
[mvp-visual-verify] viewports: desktop(100), mobile-portrait(95)
[mvp-visual-verify] wrote 195 states → mvp-verify/report.json + contact-sheet.html
[mvp-visual-verify] expectation failures: 3 | overflow states: 0 | new baselines: 195
  FAIL plugin-clawville-tui @ desktop: no-blue: palette blue coverage 91.4% > 5.0%
  FAIL plugin-polymarket-gui @ desktop: ocr-text: forbidden present: undefined
  FAIL plugin-polymarket-tui @ desktop: ocr-text: forbidden present: undefined

Adversarial self-review — each column verified against the actual pixels:

  • OCR is real. plugin-polymarket-gui.png OCR read "Cannot read properties of undefined (reading 'ready')"; opening the PNG shows that exact crash string in red on screen. The undefined junk-token expectation fired correctly (whole-word matched, so undefined in finances would not false-fire).
  • Palette is real. plugin-clawville-tui palette reported blue 91.4% (dominant swatch #080818 @ 85.3%); the PNG is a dark-navy TUI — a genuine no-blue violation, caught.
  • Diff moves with content. Same image vs itself = 0%; builtin-character vs builtin-settings baseline = 93.5% (meanDelta 44.7) with a magenta diff PNG; resized mobile-vs-desktop settings = 36.1% (resized:true). Diff PNG opened: magenta on changed pixels, dimmed-gray on the structurally-shared bands.
  • Expectations actually FAIL. 3 of 195 states fail (above) — the evaluator is not a pass-everything rubber stamp. Unit tests additionally assert fail-on-DOM-blue, fail-on-missing-orange, fail-over-overflow-tolerance, and honest skip when OCR is unavailable.
  • No larp columns. OCR degrades to explicit N/A without tesseract; diff records new without a baseline; overflow skips when the report predates the field. Nothing is fabricated.

Caught-regression demo (real chromium)

bun run --cwd packages/app mvp:visual-verify:overflow-demo:

[overflow-demo] viewport 390x844, tolerance 2px
[overflow-demo] BUGGY (un-contained token):     2886px  -> INVARIANT FIRES ✓
[overflow-demo] FIXED (overflow-x:hidden+wrap): 0px  -> passes ✓
[overflow-demo] PASS — the invariant catches the regression and clears the fix

It renders the WS5 transcript bug (an un-contained wide token, no overflow-x containment) in headless chromium → documentElement.scrollWidth − innerWidth = 2886px, invariant fires; the fix (overflow-x:hidden + overflow-wrap:anywhere) → 0px, passes. It uses the exact probe now wired into the capture spec, so the demo doubles as a self-test of the capture-side gate. (Note the CSS subtlety it is built around: overflow-y:auto alone auto-promotes overflow-x to auto, self-containing the token; the real user-visible leak is an un-contained wide descendant, which is what BUGGY reproduces.)

How the visual MVP workstreams use it for evidence

  • WS3 (launcher). audit:app:verify after any launcher-tile change gives per-viewport palette + OCR + diff-vs-baseline for the launcher states; the accent-orange / no-blue expectations enforce the brand rule automatically, and the committed baseline flags unintended pixel drift (e.g. the label-clip regressions) instead of relying on eyeballing.
  • WS5 (chat-ux). The horizontal-overflow invariant is the WS5 transcript-overflow signature (overflow-y:auto without overflow-x:hidden). The capture spec records horizontalOverflowPx per view; mvp:visual-verify surfaces it and the strict gate can hard-fail it — a long unbreakable token in a bubble can no longer ship a horizontally-scrolling chat. The overflow-demo is the reusable proof that the gate bites.
  • WS6 (views). OCR present/absent expectations assert each redesigned view actually painted its expected chrome (Models/Voice, Personality/Knowledge, Automations/Workflows) and carries no render-crash junk (undefined, NaN, [object Object]) — the polymarket crash above is exactly the class of regression a views sweep must not reintroduce. The contact-sheet is the single artifact a reviewer scans to sign off all views at once.

Verification

  • CI=true npx vitest run test/audit/mvp-visual-verify.test.ts20 passed.
  • mvp:visual-verify:overflow-demo → exit 0 (2886px fires, 0px passes).
  • mvp:visual-verify on 195 real states → 3 real findings, report + contact-sheet written.
  • biome lint clean on all touched files; rebased onto origin/develop.

Evidence tooling only — no required CI check added. report.json / contact-sheet.html / diff+baseline PNGs are generated into the gitignored aesthetic-audit-output/mvp-verify/.

🤖 Generated with Claude Code

@lalalune

lalalune commented Jul 5, 2026

Copy link
Copy Markdown
Member Author

Verification update after c2939bca4ba:

  • Hardened mvp-visual-verify --strict so it exits non-zero when the report has expectation failures, skipped checks, or first-run baselines. Non-strict mode still writes the full report/contact sheet for manual review.
  • OCR failures are now recorded per screenshot as explicit unavailable reasons instead of aborting the entire run, so a reviewer still gets palette/diff/expectation output for the rest of the capture set.
  • Contact sheet screenshots and diff thumbnails now link to the full-size artifacts.

Local verification:

bunx @biomejs/biome check packages/app/scripts/mvp-visual-verify.mjs packages/app/test/audit/mvp-visual-verify.test.ts
Checked 2 files in 32ms. No fixes applied.
bun run --cwd packages/app test -- test/audit/mvp-visual-verify.test.ts
Test Files  1 passed (1)
Tests       21 passed (21)

Attempted real screenshot capture:

bun run --cwd packages/app audit:app:verify

Blocked before screenshots by existing environment/dependency issues:

  • View build phase failed resolving @elizaos/capacitor-messages, @elizaos/capacitor-phone, and @xterm/addon-fit.
  • Retried with ELIZA_UI_SMOKE_SKIP_VIEW_BUILD=1; renderer build then failed because the current install is missing @tailwindcss/vite even though it is declared in packages/app/package.json and bun.lock.
  • bun install --frozen-lockfile could not be used to repair the install because the current dirty manifest state would update bun.lock.
  • tesseract is not installed in this environment, so OCR would honestly report N/A (tesseract not found) here until the binary is installed.

No app screenshots were produced in this run because Playwright never reached the capture phase.

@lalalune lalalune force-pushed the feat/mvp-visual-verify-harness branch from c2939bc to e376ccd Compare July 5, 2026 22:52
@lalalune

lalalune commented Jul 5, 2026

Copy link
Copy Markdown
Member Author

Proof — run on REAL captured screenshots + caught-regression demo

All five columns exercised against the real audit:app output already on disk (packages/app/aesthetic-audit-output/), not fabricated. I opened the report and multiple contact-sheet states by hand.

Real run: 195 states (desktop + mobile-portrait) in ~68s

[mvp-visual-verify] OCR engine: /opt/homebrew/bin/tesseract
[mvp-visual-verify] viewports: desktop(100), mobile-portrait(95)
[mvp-visual-verify] wrote 195 states → mvp-verify/report.json + contact-sheet.html
[mvp-visual-verify] expectation failures: 3 | overflow states: 0 | new baselines: 195
  FAIL plugin-clawville-tui @ desktop: no-blue: palette blue coverage 91.4% > 5.0%
  FAIL plugin-polymarket-gui @ desktop: ocr-text: forbidden present: undefined
  FAIL plugin-polymarket-tui @ desktop: ocr-text: forbidden present: undefined

These 3 are genuine regressions the harness caught, hand-verified:

  • plugin-clawville-tui — palette is 91.4% blue (#080818, #081828 deep navy). A real no-blue brand violation on a real screenshot.
  • plugin-polymarket-{gui,tui} — OCR captured Cannot read properties of undefined (reading 'ready') — a real JS crash rendered on screen. Confirmed visually: the red error string is painted in the view. My whole-word absent match caught the standalone undefined (and does NOT false-fire on undefined inside a real word — an earlier substring bug that fired on "fiNANces" was found and fixed during bring-up).

OCR / palette / diff are real (hand-checked)

  • OCR (builtin-settings desktop): 60 words — Settings … Models & Providers … Voice … Appearance … Wallet … Billing & Credits.
  • Palette (builtin-chat desktop): orange 99.9% top swatch #e85818:orange; zero blue.
  • Pixel-diff on real pixels (zero new deps, sharp raw buffers):
    IDENTICAL (settings vs itself):        0.000% changed
    DIFFERENT (settings vs character):    93.535% changed, meanAbsDelta 44.658, diff PNG written (magenta)
    RESIZED   (mobile chat vs desktop):   34.100% changed, resized:true   ← resize path exercised
    NO BASELINE:                          status "new" (honest, not a false 0% pass)
    
    The magenta diff PNG renders correctly (changed pixels magenta, unchanged regions dimmed gray).

Caught-regression demo (real headless chromium)

bun run --cwd packages/app mvp:visual-verify:overflow-demo renders the WS5 transcript bug and its fix and drives the exact probe the capture spec records into report.json:

[overflow-demo] viewport 390x844, tolerance 2px
[overflow-demo] BUGGY (un-contained token):     2886px  -> INVARIANT FIRES ✓
[overflow-demo] FIXED (overflow-x:hidden+wrap): 0px  -> passes ✓
[overflow-demo] PASS — the invariant catches the regression and clears the fix

The capture spec (all-views-aesthetic-audit.spec.ts) now records horizontalOverflowPx for every view and hard-gates it under ELIZA_AUDIT_APP_STRICT; expectation-eval also validates it from the report (skips honestly when a stale report lacks the field).

Unit tests

test/audit/mvp-visual-verify.test.ts20 passing, including a full RGB-cube parity sweep asserting the ported bucket() is byte-for-byte identical to the audit's own bucket() (so the no-blue guarantee can't drift between capture and post-process).

Notes

  • Baselines co-locate under aesthetic-audit-output/mvp-verify/baseline/ (that tree is gitignored), so 793 baseline PNGs are not committed; the diff mechanism is proven by the run-to-run demo above. A team wanting a committed baseline can point --input at a tracked dir.
  • Evidence tooling only — no blocking CI gate added.

@lalalune lalalune marked this pull request as ready for review July 5, 2026 22:53
Shaw and others added 2 commits July 5, 2026 18:55
…on layer over audit:app

Post-processes the screenshots audit:app already captures into a higher-fidelity
visual-verification report. For each <viewport>/<slug>.png:
- OCR text readout via the system tesseract binary (honest N/A when absent)
- dominant-color palette (sharp raw RGBA quantized) with brand-bucket labels
- pixel-diff vs a committed baseline (zero new deps: sharp raw diff, magenta PNG)
- declarative expectation validation (OCR present/absent, brand-orange accent,
  no blue, no horizontal overflow) → pass/fail with reasons

Also wires a document-level horizontal-overflow invariant
(documentElement.scrollWidth − innerWidth) into the capture spec — recorded into
report.json for every view, hard-gated under ELIZA_AUDIT_APP_STRICT. This is the
WS5 transcript-overflow signature (overflow-y:auto without overflow-x:hidden).

Pure functions (color-bucket parity-tested against the audit's own bucket(),
quantizePalette, summarizeDiff/comparePixels, evaluateExpectations/resolveSpec)
are unit-tested in test/audit/mvp-visual-verify.test.ts (20 tests).

Scripts: mvp:visual-verify + audit:app:verify (audit:app && mvp:visual-verify).
Evidence tooling only — no CI gate added.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…verify

- Add scripts/mvp-visual-verify/overflow-invariant-demo.mjs: a real headless-
  chromium demonstration that the horizontal-overflow invariant catches the WS5
  transcript bug (un-contained wide token → 2886px document overflow → invariant
  FIRES) and clears the fix (overflow-x:hidden + wrap → 0px). Doubles as a
  self-test of the capture-side gate; exposed as mvp:visual-verify:overflow-demo.
- Biome format pass on the mvp-visual-verify modules; drop an unused counter.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@lalalune lalalune force-pushed the feat/mvp-visual-verify-harness branch from e376ccd to 5df9343 Compare July 5, 2026 22:59
Shaw and others added 3 commits July 5, 2026 19:01
biome noNonNullAssertion warns on the highlight! accesses; assert not-null
then read through a nullish-coalesced buffer instead.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@lalalune lalalune merged commit d5a1aa8 into develop Jul 5, 2026
12 of 17 checks passed
@lalalune lalalune deleted the feat/mvp-visual-verify-harness branch July 5, 2026 23:04

@lalalune lalalune left a comment

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed together with the other visual-verification PRs landing this session (#14433 merged, #14437, #14451, #14452). No file collisions or forks: none of these edits or imports the merged packages/app/scripts/lib/visual-qa.mjs; the only shared file across the three is packages/app/package.json (each appends a DIFFERENT, non-overlapping script key — a trivial additive scripts hunk merge for whichever lands 2nd/3rd). All additive, disjoint output artifacts, low blast radius (test tooling, no product/runtime code). #14437 mvp-visual-verify operates on the whole aesthetic-audit-output/ dir and adds a genuinely new capability — the document-level horizontal-overflow invariant. It reimplements OCR (tesseract.js)/palette/pixel-diff/expectation-eval in its own mvp-visual-verify/*.mjs rather than importing #14433's lib. Safe to merge; will need a one-hunk package.json scripts rebase if it lands after #14451/#14452. Duplication of the OCR/palette/pixel primitives with #14433 (and #14451/#14452) is real tech-debt — filing a consolidation follow-up. LGTM (additive).

@lalalune

lalalune commented Jul 5, 2026

Copy link
Copy Markdown
Member Author

Follow-up opened: #14472.

Reason: #14437 merged the strict visual verifier, but audit:app:verify still invoked non-strict reporter mode. #14472 changes that package script to run mvp:visual-verify -- --strict so skipped expectation checks and first-run baselines fail the verification command instead of remaining advisory.

Focused evidence on #14472:

  • bun run --cwd packages/app test -- test/audit/mvp-visual-verify.test.ts → 21 passed
  • bun run --cwd packages/app mvp:visual-verify:overflow-demo → regression fires at 2886px and fixed case passes at 0px
  • bunx @biomejs/biome check packages/app/package.json → clean
  • git diff --check origin/develop...HEAD → clean

@lalalune

lalalune commented Jul 5, 2026

Copy link
Copy Markdown
Member Author

Update pushed in f0a1ee2432.

What changed:

  • Added a committed visual baseline for the current audit:app capture set: 368 PNG states across desktop-landscape, ipad-portrait, mobile-landscape, and mobile-portrait.
  • Added mvp:visual-verify:strict and wired audit:app:verify to run the strict verifier after audit:app.
  • Recalibrated expectations from the real OCR/manual screenshot review:
    • builtin-automations: desktop expectation now keys on Workflows instead of OCR-fragile Automations/Tasks.
    • builtin-character: desktop expectation matches the visible Personality + Skills tabs.
    • builtin-settings: no longer requires orange accent on the neutral settings screen.
  • Kept OCR required under strict mode, so screenshots without Tesseract coverage fail instead of being silently skipped.

Local verification:

  • bunx @biomejs/biome check packages/app/package.json packages/app/scripts/mvp-visual-verify.mjs packages/app/scripts/mvp-visual-verify/color-bucket.mjs packages/app/scripts/mvp-visual-verify/diff.mjs packages/app/scripts/mvp-visual-verify/dominant-color.mjs packages/app/scripts/mvp-visual-verify/expectation-eval.mjs packages/app/scripts/mvp-visual-verify/ocr.mjs packages/app/scripts/mvp-visual-verify/overflow-invariant-demo.mjs packages/app/scripts/mvp-visual-verify/expectations.json packages/app/test/audit/mvp-visual-verify.test.ts passed.
  • bun run --cwd packages/app test -- test/audit/mvp-visual-verify.test.ts passed: 21 tests.
  • bun run --cwd packages/app mvp:visual-verify:strict passed against the captured audit output:
    • 368 states processed.
    • OCR engine: system Tesseract at /opt/homebrew/bin/tesseract.
    • expectation failures: 0.
    • expectation skips: 0.
    • overflow states: 0.
    • new baselines: 0.

Manual screenshot review performed for the states that originally failed expectations:

  • builtin-automations desktop: screen is valid; OCR/title visibility made the old Automations/Tasks expectation too brittle.
  • builtin-character desktop: visible tabs are Personality, Relationships, Skills, Experience; the prior Knowledge expectation was wrong for this view.
  • builtin-settings desktop: neutral settings layout is valid; requiring orange accent there was over-specific.

Separate blocker still visible from the full app audit, not introduced by this verifier commit:

  • audit:app captured 368 passing screenshots before the final audit summary failed on existing minimalism ratchet regressions: plugin-hyperliquid-gui @ mobile-portrait, plugin-polymarket-gui @ mobile-portrait, and plugin-polymarket-gui @ mobile-landscape.
  • The summary from that run was: 368 findings — broken=0 needs-work=9 needs-eyeball=21 good=338 minimalism-budget-failures=0 minimalism-ratchet-failures=3 hover-probe-failures=69 density-probe-failures=0.

Environment notes:

  • Local default node is 23.3.0; repo requires Node 24. I used a temporary Node 24.11.1 via ELIZA_NODE_PATH for the full audit:app capture path.
  • I had to build packages/import-conversations first because the app audit build could not resolve @elizaos/import-conversations/browser from an unbuilt workspace package.

@claude

claude Bot commented Jul 6, 2026

Copy link
Copy Markdown
Contributor

Claude encountered an error —— View job


I'll analyze this and get back to you.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants