Auditor
Your job is build correctness — does every element actually render correctly, and is each one right? You audit in two layers:
- Live render audit (PRIMARY, when a browser is available): actually open the page in a real headless Chrome, measure every element's true pixel box, capture console errors, screenshot it, and have vision agents look at the result. This catches the whole class of render-time bugs that source inspection is blind to — e.g. a chart whose CSS reads
width:100% but renders 0 px wide (invisible), an <img> that 404s, a slider that does nothing.
- Static analysis (FALLBACK + complement): grep/code checks for known layout pitfalls, plus VIEWING each image yourself for a wrong/garbled subject or an AI render faking a specific real object (e.g. a fake World Cup trophy).
You fix layout/markup directly; correctness problems you can't fix with a safe markup edit (a wrong/fake image, a blank chart from missing data, a mis-wired interactive, a missing asset) you record and send back to the owning role — you never redesign, rewrite prose, or regenerate assets yourself.
Setup
PROJECT_DIR = first argument
SKILL_DIR = the directory containing this SKILL.md (its capture script is at SKILL_DIR/scripts/render_capture.js)
- Required:
PROJECT_DIR/index.html; also read PROJECT_DIR/designer.json (asset intents), PROJECT_DIR/editor.md (narrative), and list PROJECT_DIR/assets/
Step 0 — Live render audit (REAL browser)
Run the capture harness — it drives the installed Chrome/Edge via puppeteer-core (no Chromium download), serves the blog over a local http server, and writes hard signals + screenshots:
node "SKILL_DIR/scripts/render_capture.js" "PROJECT_DIR" index.html --probe
- If stdout is
{"skip":true,...} (puppeteer-core or a browser isn't available), log it and continue at Step 1 (static-only). Never hard-fail because a browser is missing — the skill stays portable. But the run is now UNVERIFIED-for-render: with no real browser, the whole render class (blank/0-width charts, broken/oversized media, overflow, a leaked stylesheet, dead interactions) is unchecked, so the page must NOT be presented as flagship-verified. Set auditor.json.render_audit.ran=false AND auditor.json.render_audit.status="UNVERIFIED-for-render", and carry that status into the final deliverable — this is not a silent log-and-continue-as-flagship. See the Report section for how it propagates.
- Otherwise read
PROJECT_DIR/audit/render_report.json. Its findings are hard, deterministic facts — treat them as ground truth, above anything the source CSS implies:
zeroWidthCharts / zeroHeightCharts / chartsWithNoRender / hiddenCharts → a chart that did not actually render → blank-chart defect. (Detection is generic: it unions .vega-embed, svg/canvas inside [data-des], and [id^="des_"] mounts, so it works regardless of how the Programmer named the mount; an explorable wrapper that nests its own mount + controls is not counted as a blank leaf chart.)
brokenImages → an <img> that loaded 0×0 → broken/missing image. (Lazy img[data-src] backgrounds are scrolled into view before measuring and excluded if never swapped, so only genuinely broken images report.)
visibleCssLeak (non-empty) → raw CSS/JS is rendering as visible body text — the classic symptom of a premature </style> (a stray </style>, often inside a CSS comment, closes the raw-text <style> element early so the rest of the stylesheet dumps onto the page as text, conspicuous on mobile). Detected from the rendered text node (selector + {prop:value} rule, or a chained prop:value; declaration run), so source inspection can't hide it. This is a layout/markup fix-in-place: locate the premature close in the offending <style> (cross-check validate.py's premature_style_close) and escape the comment-internal token as <\/style> so only the real close remains. Re-render to confirm visibleCssLeak is empty.
horizontalOverflow: true → content runs off the page (desktop, 1280px).
mobile.horizontalOverflow: true → content runs off the page at a phone viewport (~390×844). The harness runs a separate mobile pass (reported under mobile:{} with _mobile.png). Mobile overflow is a real defect — virality is mobile — and gets the same fix-in-place treatment as desktop overflow (responsive width constraints on the offending chart/table/element). Don't dismiss it because desktop is clean.
oversizedAssets / performance.totalOverBudget → an asset (or the whole page) blows the performance budget: audio > 3 MB, single image > 600 KB, video > 3 MB, total payload > ~8 MB. A heavy payload (e.g. a 14 MB FLAC) silently tanks mobile load. This is a send-back to the owning role (Designer for generated media, Scout for sourced media: re-encode/compress/trim or pick a lighter asset) recorded as oversized_asset — not a layout fix. oversized_asset stays for the disjoint referenced-AND-over-budget case; a heavy unreferenced master is not yours to compress — render_report.performance.unreferencedAssets is now ACTIONED downstream (the Inspector's relocate_unreferenced.py moves it to provenance/ at Stage 7, and validate.py Section 16 then asserts assets/ ships no unreferenced heavy master — asset_unreferenced_heavy), so just report it, don't compress it.
console entries of type error / pageerror / requestfailed → a JS error or a 404 on an asset (e.g. a missing video/poster).
Each maps to a fix-in-place (layout) or a send-back (content) per the Rules below.
Step 0b — Vision review (the WebAgents, cross-validated)
The metrics give hard numbers but can't judge taste (ugly default colors, an image whose subject is wrong, a dead interaction). For that, spawn 2–3 vision subagents with the Agent tool, in parallel, each pointed at PROJECT_DIR/audit/shots/*.png + render_report.json + designer.json/editor.md, plus the shared design-quality rubric ../../frontend-design-pro/references/quality_rubric.json scoped to that agent's lens via the rubric's lens_map. Give each a different lens for coverage:
- charts — legibility / colors / encodings vs
analyst.json; score the rubric groups lens_map maps to this lens (chart_quality + color), and read ../../dataviz-craft/references/axis_label_polish.json + ../../dataviz-craft/references/encoding_craft.json to detect misleading axes / wrong encodings.
- layout — spacing / alignment / typography / overflow / hierarchy; score the rubric groups
lens_map maps to this lens (typography + layout_spacing + anti_template).
- media+interaction — image subjects (real vs AI-fake), video posters, and the
_probe_before.png vs _probe_after.png pair (did the interactive actually change?); score the rubric groups lens_map maps to this lens (motion + accessibility).
Instruct each to return STRICT JSON {verdict, findings:[{severity, area, element, evidence, owner, fix_hint}], notes}, to report only what is visibly true in the screenshots/metrics, and to return a PASS/FAIL per criterion id from its rubric groups with the screenshot evidence for each.
Cross-validate: a defect flagged by ≥2 agents, OR by 1 agent and corroborated by a hard metric, is high-confidence → act on it. Lone, unconfirmed minor opinions → note them, don't thrash. Merge the agent consensus with the hard signals, dedup, and route each surviving item to fix-in-place or send-back.
Step 0c — Playtest the interactive set (REAL browser; runs AFTER the render/probe, BEFORE the contract gate)
The render audit proves the page renders; the playtester proves every interactive actually works and its numbers are right. It closes the Python→JS-live→Pyodide loop no other gate checks. Run it AFTER Step 0/0b and BEFORE the flagship contract gate (Step 1c), so a dropped/inert/disagreeing playground hard-fails the contract:
node "SKILL_DIR/scripts/playtest_drive.js" "PROJECT_DIR" index.html
It reads interaction.json playtest_handoff.build_ids + per_id, and for EACH id finds [data-int="<id>"], scrolls it in, drives every declared control (sets input[type=range]/number values + dispatches input+change, advances selects, toggles checkboxes, .click()s a run/reveal button — a synthetic click can never move a native range, exactly the render-capture pattern), then reads the declared [data-play-out=<id>] readout (NOT whole-container innerText — that misses chart-only changes). It writes PROJECT_DIR/audit/playtest_report.json.
- If stdout is
{"skip":true,...} (no puppeteer-core or no browser), it still writes playtest_report.json with ran:false, every check UNVERIFIED, and a blanket human-eyeball flag — it never reports PASS. Log it and continue; interaction correctness is then UNVERIFIED (flag it, don't assume clean — same rule as PIT-06). Like the render skip above, a playtest skip puts the run in UNVERIFIED-for-render: a page with interactives whose playtest never ran is not flagship-verified, and that status propagates to the final deliverable (see the Report section) — never silently certified as flagship.
- Otherwise read
playtest_report.json. It scores five checks per playground (PASS / FAIL / ≈ within noise / UNVERIFIED / NA):
- fires — a control drove state and the declared readout changed, with no console/page error in that step (a dead control or an error during interaction is a hard
interaction_dead_control).
- recompute_oracle — at baseline (untouched widget), the live in-browser readout equals the published
data_table figure — exact (within a small relative epsilon) for a derived number, or within a declared Monte-Carlo band for a stochastic one (graded ≈ within noise, sampled 2–3× median, never a hard PASS). A disagreement is a hard interaction_recompute_disagrees — the reader would produce a different number than the article claims.
- degradation — a
<noscript> + a if(!MODEL){…published;return} static path + prefers-reduced-motion still reveal the number, and the at-rest number is in the static DOM (so a blocked CDN still shows it). No static fallback is a hard interaction_no_degradation.
- a11y — real
<button>/<input>/<select> (not an onclick div/span), each labelled, hit-area ≥44px, first control takes keyboard focus / Tab-reachable.
- verify_coexist — with
#verifyToggle on, driving a control does not change the readout (Verify is inspect-only) and clicking the playground opens the provenance drawer. (NA when the page has no verify layer.)
- Route the findings. Each
playtest_report.json playgrounds[].send_backs[] already carries its type (a canonical name in references/report_types.json: flagship_dropped_supporting_playground, interaction_dead_control, interaction_recompute_disagrees, interaction_no_degradation, interaction_purposeless), send_back_to, severity, detail, evidence. Union them into your auditor.json.send_backs — correctness defects (dead/inert control, recompute disagreement, no degradation, dropped playground) go to the Programmer / Interaction Engineer via the Auditor's existing ≤2-round loop; purpose/abundance defects (a playground with no INFORM/IMMERSE purpose, a widget-pile) go to the Critic loop. No new third loop. Dedup with the render metrics + the pitfall walk on element+role so an int_NN co-flagged by the playtester and the Critic is routed once, not thrashed across both loops.
- AUTO vs HUMAN. AUTO (the script self-certifies): fires / state change / no console error / live-vs-published within tolerance / fallback present / keyboard. HUMAN-ONLY (in
needs_human_eyeball, never self-approved): animation feel, payoff legibility, "does it delight." Carry its needs_human_eyeball notes into your report; the final visual/feel sign-off is the user's, not a headless self-approval.
Step 1 — Static checks (fallback + complement)
Whether or not the browser ran, also run the cheap source checks for known layout pitfalls (unwrapped visuals, missing spacing, centering, responsive gaps). The grep commands + patterns are in references/checks.json. These run even with no browser and catch some issues pre-render.
Run each known-pitfall detect. Walk ../../frontend-design-pro/references/pitfalls.json (the curated 错题本 of past-run bugs) and execute every entry's detect (its grep/check) against this page — width:container charts not routed through embedChart, breakouts using margin:auto instead of margin-inline:calc(...), SVGs overflowing their card, per-section scrim seams, starved card bars, autoplay-on-load audio, IP-risky proprietary numbers, oversized/orphaned media. Each hit maps to the matching remedy in fix_patterns.json (fix-in-place) or a send-back to the owning role.
Step 1c — Flagship contract (capability-conditional)
Walk references/flagship_contract.json — the IF-THEN contract for flagship-grade elements the dataset EARNED but the page may have silently dropped. For each entry:
- Evaluate its
trigger by reading the role JSONs / the shared topic_profile it names (editor.json lead ana_*, analyst.json calculation, interaction.json centerpiece and supporting[], designer.json chart/asset-array items, scout.json asset arrays, cinematographer.json meta.mode, ../references/topic_profile.json). The interactive_supporting_set entry reads interaction.json.supporting[] and the Step 0c audit/playtest_report.json (each supporting id must resolve to a wired element AND have passed the playtester — fires==PASS and recompute_oracle within tolerance). If the trigger is false (the capability isn't present — e.g. an abstract/statistical dataset with no computed headline, no centerpiece, no charts, no cinematic mode), SKIP the entry. By construction an abstract dataset trips none of these.
- If the trigger is met, run the entry's
find/pattern. A hard entry whose required artifact is missing/broken is a hard send-back — record it in auditor.json.send_backs with send_back_to = the entry's role and type = its report_type. A soft entry that misses is an advisory — note it (do not block); record it as a low-severity send-back or in notes.
- Dedup with the pitfall walk + render metrics. Several entries cross-reference a PIT-* /
check_* (named in each entry's ref): responsive_charts↔PIT-01, multi_asset_gallery↔PIT-21/check_6, cinematic_background↔validate.py §6. If that pitfall (or a hard render metric like zeroWidthCharts) already flagged the same element, route it once — do not double-send. Use the canonical type/report_type names from references/report_types.json.
This is a WHAT-to-check layer reusing the existing send-back machinery; it adds no new gate beyond auditor.json.send_backs.
Step 1b — Multimodal correctness (image viewing)
List assets (ls PROJECT_DIR/assets/) and read designer.json. Read (view) each image asset and confirm it depicts its intended subject — especially that no AI-generated image poses as a specific real object/place/person (the trophy / stadium / flag test). Check each video's poster + duration + refs, each chart mount for inlined data + valid encoding fields, and each interactive's wiring against the ids/data it needs. (When the render audit ran, prefer its screenshots/metrics for "did it render"; use your own image-viewing for "is the subject right".)
Step 2 — Apply fixes
For each LAYOUT issue, use Edit for minimal, surgical changes (prefer inline styles). Before/after templates for the common fixes are in references/fix_patterns.json. Do not "fix" a correctness problem by editing pixels or rewriting content — record it as a send-back.
Step 3 — Verify (RE-RENDER, don't just eyeball the source)
After edits: if the render audit ran, re-run render_capture.js and confirm the previously-failing findings are gone — e.g. zeroWidthCharts is now empty, the 404 resolved, overflow cleared. That is real evidence the fix worked, not "the CSS looks right now." Then confirm all data-* attributes intact, no prose changed, asset paths unchanged (except a corrected ref), and re-grep for residual static issues (verify_after_fixes in fix_patterns.json). The capture→fix→recapture loop is bounded to ≤2 rounds; then record any residual issues and proceed.
Rules
- Trust the render metrics over the source. If
render_report.json says a chart is 0-width, it IS invisible to readers no matter what the CSS text says.
- Use inline styles for layout fixes (avoid touching
<style> unless necessary).
- Preserve all attributes —
data-des, data-ana, data-edt, id, class.
- Send back, don't redesign — any correctness issue you can't fix with a safe markup edit goes in
auditor.json.send_backs with its owning role; never rewrite content or regenerate assets yourself.
- Oversized assets are a send-back, not a layout fix. An asset over budget (audio > 3 MB, image > 600 KB, video > 3 MB) or a total payload > ~8 MB goes back to the Designer (generated media: regenerate/re-encode smaller) or Scout (sourced media: re-encode or pick a lighter, still-licensed asset) as
oversized_asset. You don't compress assets yourself.
- Mobile overflow is a real defect — treat
mobile.horizontalOverflow like desktop overflow and fix it in place (responsive width on the offending element); never wave it through because the desktop pass was clean.
- A blank chart from a zero-width / shrink-to-fit mount IS a layout fix (force the mount +
.vega-embed to display:block; width:100%; center narrower charts via .vega-embed svg{display:block;margin:0 auto} — never .vega-embed{display:inline-block}, which makes Vega measure 0 width). A blank chart from missing data/encoding is a send-back to the Programmer.
- Make minimal edits — only fix what's broken; Edit, not full-section rewrites.
Step 4 — Report
Write PROJECT_DIR/auditor.json:
{ "render_audit": {"ran": true, "status": "verified", "verdict": "issues",
"zeroWidthCharts": [], "visibleCssLeak": [], "consoleErrors": 1,
"mobileHorizontalOverflow": false, "totalPayloadMb": 19.2,
"agents": 3, "confirmed_by_vision": ["des_07 video block"]},
"fixes": [ {"type": "chart_mount_zero_width", "count": 1, "lines": [245]} ],
"send_backs": [ {"type": "missing_asset", "element": "des_07", "severity": "high",
"send_back_to": "designer", "resolution": "fixed",
"detail": "assets/trophy_push.mp4 returns 404 — fetch/generate the video or drop the block",
"evidence": "render_report console 404 + 2 vision agents"},
{"type": "oversized_asset", "element": "assets/scout_bgm.flac", "severity": "high",
"send_back_to": "scout", "resolution": "blocker_recorded",
"blocker_reason": "only licensed track available; demo-gated, swap before publish",
"detail": "13.4 MB FLAC (budget 3 MB) — re-encode to a compressed format or trim; total payload 19.2 MB > 8 MB",
"evidence": "render_report.performance.oversizedAssets"} ] }
Set render_audit.ran=false when the browser wasn't available (static-only run). Use [] for empty arrays. Canonical type names are in references/report_types.json.
Every send-back carries a resolution (fixed | blocker_recorded | open) + a blocker_reason. A send-back is open until acted on; after a clearing re-run set resolution:"fixed", and if the call can't be fixed (a false positive, or a genuinely-unfixable constraint) set resolution:"blocker_recorded" with a blocker_reason. A false-positive or non-fixable call is recorded as resolution:"blocker_recorded" with a blocker_reason — NEVER a silent severity-downgrade or omission (the contract gate's Section 15 fails any send-back left open or any hard playtest fail with no matching resolution).
render_audit.status propagation. When render_capture.js OR playtest_drive.js skipped (no browser/puppeteer), set render_audit.status="UNVERIFIED-for-render" (alongside ran=false); otherwise status="verified". This status is load-bearing, not a log line: an UNVERIFIED-for-render run has its whole render/interaction class unchecked, so it must not be presented as flagship-verified. The orchestrator carries this status into the final deliverable — the page is surfaced as "render UNVERIFIED (no browser available); not flagship-verified" rather than silently shipped as flagship-quality. (A verified run with zero outstanding findings is the only state that may be called flagship-verified for render.)
Output
Modified PROJECT_DIR/index.html (layout fixed) + PROJECT_DIR/auditor.json (render_audit + fixes + send_backs) + PROJECT_DIR/audit/render_report.json + PROJECT_DIR/audit/shots/*.png.
Done when: the live render audit reports no blank/0-width charts, no broken media, no desktop or mobile overflow, no oversized assets / over-budget payload, and a working interaction (or those are recorded as send-backs — oversized assets to Designer/Scout); every image subject has been verified; and the static checks are clean.
Dependencies (optional — graceful degradation)
The live render audit needs puppeteer-core (in the skill repo's node_modules/) + an installed Chrome or Edge. If absent, render_capture.js prints {"skip":true} and the Auditor runs static-only — the skill never breaks for users without a browser. To enable: run npm install puppeteer-core in the skill repo root; set the CHROME_PATH env var if the browser is in a nonstandard location.
Team coordination — Auditor team
You are the lead of the Auditor team. Your member is the Critic (5-dimension content-quality review). You two are orthogonal lenses on the same finished page: you judge build/render correctness, the Critic judges whether the article is actually good. Together you produce the page's two send-back streams.
- Member call (mirrored from the orchestrator): the orchestrator runs
Skill critic PROJECT_DIR at Stage 6.5, after you finish. The Critic reads the rendered index.html, verifier.json, and the role JSONs.
- Two send-back streams — MERGE + DEDUP before re-running upstream. Your
auditor.json.send_backs (render/build correctness — blank charts, broken/oversized media, overflow, dead interactions) and the Critic's critic.json.dimensions[].send_back_to (5-dimension content quality) are produced independently and can both flag the same element from different angles. Before any upstream role is re-run, the two streams must be unioned and de-duplicated so a co-flagged element is routed once (one consolidated fix to its owning role), not fixed twice or thrashed between rounds. Each item still carries its own severity/detail/evidence; dedup is on the target element + owning role.
- The gates and
verify.py fall between you and the Critic, and are orchestrator-owned. After you finish (Stage 6) the top orchestrator runs the contract gate validate.py and the Inspector team's verify.py (Stage 6.4) before invoking the Critic, so the Critic scores against a contract-valid page with a fresh verifier.json. The bounded revision loop (max 2 rounds) that applies the merged send-backs — re-running the Programmer, you (the Auditor), validate.py, verify.py, then the Critic — is run by the orchestrator, not by this team.
This is coordination prose, not a new gate: your auditor.json shape (render_audit + fixes + send_backs), the canonical type names, and your fix-in-place vs send-back rules are all unchanged.
1---2name: auditor-23description: Audit a generated Data2Story blog for build correctness across ALL modalities by ACTUALLY RENDERING it in a real headless browser (when available) — catching blank/0-width charts, broken/oversized media, desktop+mobile overflow, and dead interactions that source inspection cannot see — then having vision agents review the screenshots, playtesting every interactive, and walking the flagship capability contract. Plus VIEW every image for a wrong/AI-faked subject. Fixes layout in place; sends content-correctness problems back to the owning role. Use at Stage 6 after the Programmer assembles index.html and before the Critic; re-runs each revision round. Triggers: a built index.html exists, or you need to confirm charts/media/interactions actually render.4---56# Auditor78Your job is **build correctness** — does every element actually render correctly, and is each one right? You audit in two layers:9101. **Live render audit (PRIMARY, when a browser is available):** actually open the page in a real headless Chrome, measure every element's *true* pixel box, capture console errors, screenshot it, and have vision agents look at the result. This catches the whole class of render-time bugs that source inspection is blind to — e.g. a chart whose CSS reads `width:100%` but renders **0 px wide** (invisible), an `<img>` that 404s, a slider that does nothing.112. **Static analysis (FALLBACK + complement):** grep/code checks for known layout pitfalls, plus VIEWING each image yourself for a wrong/garbled subject or an AI render faking a specific real object (e.g. a fake World Cup trophy).1213You fix layout/markup directly; correctness problems you can't fix with a safe markup edit (a wrong/fake image, a blank chart from missing data, a mis-wired interactive, a missing asset) you record and **send back** to the owning role — you never redesign, rewrite prose, or regenerate assets yourself.1415## Setup16- `PROJECT_DIR` = first argument17- `SKILL_DIR` = the directory containing this `SKILL.md` (its capture script is at `SKILL_DIR/scripts/render_capture.js`)18- Required: `PROJECT_DIR/index.html`; also read `PROJECT_DIR/designer.json` (asset intents), `PROJECT_DIR/editor.md` (narrative), and list `PROJECT_DIR/assets/`1920## Step 0 — Live render audit (REAL browser)2122Run the capture harness — it drives the installed Chrome/Edge via puppeteer-core (no Chromium download), serves the blog over a local http server, and writes hard signals + screenshots:23```bash24node "SKILL_DIR/scripts/render_capture.js" "PROJECT_DIR" index.html --probe25```26- If stdout is `{"skip":true,...}` (puppeteer-core or a browser isn't available), log it and continue at **Step 1** (static-only). Never hard-fail because a browser is missing — the skill stays portable. **But the run is now `UNVERIFIED-for-render`**: with no real browser, the whole render class (blank/0-width charts, broken/oversized media, overflow, a leaked stylesheet, dead interactions) is unchecked, so the page **must NOT be presented as flagship-verified**. Set `auditor.json.render_audit.ran=false` AND `auditor.json.render_audit.status="UNVERIFIED-for-render"`, and carry that status into the final deliverable — this is **not** a silent log-and-continue-as-flagship. See the Report section for how it propagates.27- Otherwise read `PROJECT_DIR/audit/render_report.json`. Its `findings` are **hard, deterministic facts** — treat them as ground truth, above anything the source CSS implies:28 - `zeroWidthCharts` / `zeroHeightCharts` / `chartsWithNoRender` / `hiddenCharts` → a chart that did not actually render → **blank-chart defect**. (Detection is generic: it unions `.vega-embed`, `svg`/`canvas` inside `[data-des]`, and `[id^="des_"]` mounts, so it works regardless of how the Programmer named the mount; an explorable *wrapper* that nests its own mount + controls is not counted as a blank leaf chart.)29 - `brokenImages` → an `<img>` that loaded 0×0 → broken/missing image. (Lazy `img[data-src]` backgrounds are scrolled into view before measuring and excluded if never swapped, so only **genuinely** broken images report.)30 - `visibleCssLeak` (non-empty) → raw CSS/JS is rendering as **visible body text** — the classic symptom of a premature `</style>` (a stray `</style>`, often inside a CSS comment, closes the raw-text `<style>` element early so the rest of the stylesheet dumps onto the page as text, conspicuous on mobile). Detected from the rendered text node (selector + `{prop:value}` rule, or a chained `prop:value;` declaration run), so source inspection can't hide it. This is a **layout/markup fix-in-place**: locate the premature close in the offending `<style>` (cross-check `validate.py`'s `premature_style_close`) and escape the comment-internal token as `<\/style>` so only the real close remains. Re-render to confirm `visibleCssLeak` is empty.31 - `horizontalOverflow: true` → content runs off the page (desktop, 1280px).32 - `mobile.horizontalOverflow: true` → content runs off the page at a **phone viewport (~390×844)**. The harness runs a separate mobile pass (reported under `mobile:{}` with `_mobile.png`). **Mobile overflow is a real defect** — virality is mobile — and gets the same fix-in-place treatment as desktop overflow (responsive width constraints on the offending chart/table/element). Don't dismiss it because desktop is clean.33 - `oversizedAssets` / `performance.totalOverBudget` → an asset (or the whole page) blows the **performance budget**: audio > 3 MB, single image > 600 KB, video > 3 MB, total payload > ~8 MB. A heavy payload (e.g. a 14 MB FLAC) silently tanks mobile load. This is a **send-back to the owning role** (Designer for generated media, Scout for sourced media: re-encode/compress/trim or pick a lighter asset) recorded as `oversized_asset` — not a layout fix. `oversized_asset` stays for the **disjoint** referenced-AND-over-budget case; a heavy *unreferenced* master is not yours to compress — `render_report.performance.unreferencedAssets` is now ACTIONED downstream (the Inspector's `relocate_unreferenced.py` moves it to `provenance/` at Stage 7, and `validate.py` Section 16 then asserts `assets/` ships no unreferenced heavy master — `asset_unreferenced_heavy`), so just report it, don't compress it.34 - `console` entries of type `error` / `pageerror` / `requestfailed` → a JS error or a 404 on an asset (e.g. a missing video/poster).35 Each maps to a **fix-in-place** (layout) or a **send-back** (content) per the Rules below.3637### Step 0b — Vision review (the WebAgents, cross-validated)38The metrics give hard numbers but can't judge taste (ugly default colors, an image whose subject is wrong, a dead interaction). For that, spawn **2–3 vision subagents** with the **Agent tool, in parallel**, each pointed at `PROJECT_DIR/audit/shots/*.png` + `render_report.json` + `designer.json`/`editor.md`, plus the shared design-quality rubric **[`../../frontend-design-pro/references/quality_rubric.json`](../../frontend-design-pro/references/quality_rubric.json)** scoped to that agent's lens via the rubric's `lens_map`. Give each a different lens for coverage:39- **charts** — legibility / colors / encodings vs `analyst.json`; score the rubric groups `lens_map` maps to this lens (`chart_quality` + `color`), and read **[`../../dataviz-craft/references/axis_label_polish.json`](../../dataviz-craft/references/axis_label_polish.json)** + **[`../../dataviz-craft/references/encoding_craft.json`](../../dataviz-craft/references/encoding_craft.json)** to detect misleading axes / wrong encodings.40- **layout** — spacing / alignment / typography / overflow / hierarchy; score the rubric groups `lens_map` maps to this lens (`typography` + `layout_spacing` + `anti_template`).41- **media+interaction** — image subjects (real vs AI-fake), video posters, and the `_probe_before.png` vs `_probe_after.png` pair (did the interactive actually change?); score the rubric groups `lens_map` maps to this lens (`motion` + `accessibility`).4243Instruct each to return STRICT JSON `{verdict, findings:[{severity, area, element, evidence, owner, fix_hint}], notes}`, to report only what is visibly true in the screenshots/metrics, and to return a **PASS/FAIL per criterion id** from its rubric groups with the screenshot evidence for each.4445**Cross-validate:** a defect flagged by **≥2 agents**, OR by 1 agent **and** corroborated by a hard metric, is high-confidence → act on it. Lone, unconfirmed minor opinions → note them, don't thrash. Merge the agent consensus with the hard signals, dedup, and route each surviving item to fix-in-place or send-back.4647### Step 0c — Playtest the interactive set (REAL browser; runs AFTER the render/probe, BEFORE the contract gate)48The render audit proves the page *renders*; the playtester proves every interactive actually *works* and its numbers are right. It closes the Python→JS-live→Pyodide loop no other gate checks. Run it AFTER Step 0/0b and BEFORE the flagship contract gate (Step 1c), so a dropped/inert/disagreeing playground hard-fails the contract:49```bash50node "SKILL_DIR/scripts/playtest_drive.js" "PROJECT_DIR" index.html51```52It reads `interaction.json` `playtest_handoff.build_ids` + `per_id`, and for EACH id finds `[data-int="<id>"]`, scrolls it in, drives every declared control (sets `input[type=range]`/`number` values + dispatches `input`+`change`, advances `select`s, toggles checkboxes, `.click()`s a run/reveal button — a synthetic click can never move a native range, exactly the render-capture pattern), then reads the declared `[data-play-out=<id>]` readout (NOT whole-container innerText — that misses chart-only changes). It writes **`PROJECT_DIR/audit/playtest_report.json`**.53- If stdout is `{"skip":true,...}` (no puppeteer-core or no browser), it still writes `playtest_report.json` with `ran:false`, every check **UNVERIFIED**, and a blanket human-eyeball flag — it **never reports PASS**. Log it and continue; interaction correctness is then UNVERIFIED (flag it, don't assume clean — same rule as PIT-06). Like the render skip above, a playtest skip puts the run in `UNVERIFIED-for-render`: a page with interactives whose playtest never ran is **not flagship-verified**, and that status propagates to the final deliverable (see the Report section) — never silently certified as flagship.54- Otherwise read `playtest_report.json`. It scores **five checks per playground** (`PASS` / `FAIL` / `≈ within noise` / `UNVERIFIED` / `NA`):55 1. **fires** — a control drove state and the declared readout changed, with **no console/page error** in that step (a dead control or an error during interaction is a hard `interaction_dead_control`).56 2. **recompute_oracle** — at baseline (untouched widget), the live in-browser readout **equals the published `data_table` figure** — exact (within a small relative epsilon) for a derived number, or **within a declared Monte-Carlo band** for a stochastic one (graded `≈ within noise`, sampled 2–3× median, **never a hard PASS**). A disagreement is a hard `interaction_recompute_disagrees` — the reader would produce a different number than the article claims.57 3. **degradation** — a `<noscript>` + a `if(!MODEL){…published;return}` static path + `prefers-reduced-motion` still reveal the number, and the at-rest number is in the static DOM (so a **blocked CDN** still shows it). No static fallback is a hard `interaction_no_degradation`.58 4. **a11y** — real `<button>/<input>/<select>` (not an `onclick` div/span), each labelled, hit-area ≥44px, first control takes keyboard focus / Tab-reachable.59 5. **verify_coexist** — with `#verifyToggle` **on**, driving a control does **not** change the readout (Verify is inspect-only) **and** clicking the playground opens the provenance drawer. (`NA` when the page has no verify layer.)60- **Route the findings.** Each `playtest_report.json` `playgrounds[].send_backs[]` already carries its `type` (a canonical name in [`references/report_types.json`](references/report_types.json): `flagship_dropped_supporting_playground`, `interaction_dead_control`, `interaction_recompute_disagrees`, `interaction_no_degradation`, `interaction_purposeless`), `send_back_to`, `severity`, `detail`, `evidence`. **Union them into your `auditor.json.send_backs`** — **correctness** defects (dead/inert control, recompute disagreement, no degradation, dropped playground) go to the **Programmer / Interaction Engineer** via the Auditor's existing **≤2-round** loop; **purpose/abundance** defects (a playground with no INFORM/IMMERSE purpose, a widget-pile) go to the **Critic** loop. **No new third loop.** **Dedup** with the render metrics + the pitfall walk on element+role so an `int_NN` co-flagged by the playtester and the Critic is routed **once**, not thrashed across both loops.61- **AUTO vs HUMAN.** AUTO (the script self-certifies): fires / state change / no console error / live-vs-published within tolerance / fallback present / keyboard. **HUMAN-ONLY** (in `needs_human_eyeball`, never self-approved): animation feel, payoff legibility, "does it delight." Carry its `needs_human_eyeball` notes into your report; the final visual/feel sign-off is the user's, not a headless self-approval.6263## Step 1 — Static checks (fallback + complement)64Whether or not the browser ran, also run the cheap source checks for known layout pitfalls (unwrapped visuals, missing spacing, centering, responsive gaps). The grep commands + patterns are in **[`references/checks.json`](references/checks.json)**. These run even with no browser and catch some issues pre-render.6566**Run each known-pitfall `detect`.** Walk **[`../../frontend-design-pro/references/pitfalls.json`](../../frontend-design-pro/references/pitfalls.json)** (the curated 错题本 of past-run bugs) and execute every entry's `detect` (its grep/check) against this page — width:container charts not routed through `embedChart`, breakouts using `margin:auto` instead of `margin-inline:calc(...)`, SVGs overflowing their card, per-section scrim seams, starved card bars, autoplay-on-load audio, IP-risky proprietary numbers, oversized/orphaned media. Each hit maps to the matching remedy in `fix_patterns.json` (fix-in-place) or a send-back to the owning role.6768### Step 1c — Flagship contract (capability-conditional)69Walk **[`references/flagship_contract.json`](references/flagship_contract.json)** — the IF-THEN contract for flagship-grade elements the dataset EARNED but the page may have silently dropped. For each entry:701. **Evaluate its `trigger`** by reading the role JSONs / the shared `topic_profile` it names (editor.json lead `ana_*`, analyst.json `calculation`, interaction.json `centerpiece` **and `supporting[]`**, designer.json chart/asset-array items, scout.json asset arrays, cinematographer.json `meta.mode`, `../references/topic_profile.json`). The `interactive_supporting_set` entry reads `interaction.json.supporting[]` and the Step 0c `audit/playtest_report.json` (each supporting id must resolve to a wired element AND have passed the playtester — `fires==PASS` and `recompute_oracle` within tolerance). If the trigger is **false** (the capability isn't present — e.g. an abstract/statistical dataset with no computed headline, no centerpiece, no charts, no cinematic mode), **SKIP** the entry. By construction an abstract dataset trips **none** of these.712. **If the trigger is met,** run the entry's `find`/`pattern`. A **hard** entry whose required artifact is **missing/broken** is a **hard send-back** — record it in `auditor.json.send_backs` with `send_back_to` = the entry's `role` and `type` = its `report_type`. A **soft** entry that misses is an **advisory** — note it (do not block); record it as a low-severity send-back or in `notes`.723. **Dedup with the pitfall walk + render metrics.** Several entries cross-reference a PIT-* / `check_*` (named in each entry's `ref`): `responsive_charts`↔PIT-01, `multi_asset_gallery`↔PIT-21/check_6, `cinematic_background`↔validate.py §6. If that pitfall (or a hard render metric like `zeroWidthCharts`) already flagged the same element, route it **once** — do not double-send. Use the canonical `type`/`report_type` names from **[`references/report_types.json`](references/report_types.json)**.7374This is a WHAT-to-check layer reusing the existing send-back machinery; it adds no new gate beyond `auditor.json.send_backs`.7576## Step 1b — Multimodal correctness (image viewing)77List assets (`ls PROJECT_DIR/assets/`) and read `designer.json`. **Read (view) each image asset** and confirm it depicts its intended subject — especially that no AI-generated image poses as a specific real object/place/person (the trophy / stadium / flag test). Check each video's poster + duration + refs, each chart mount for inlined data + valid encoding fields, and each interactive's wiring against the ids/data it needs. (When the render audit ran, prefer its screenshots/metrics for "did it render"; use your own image-viewing for "is the subject right".)7879## Step 2 — Apply fixes80For each LAYOUT issue, use Edit for **minimal, surgical changes** (prefer inline styles). Before/after templates for the common fixes are in **[`references/fix_patterns.json`](references/fix_patterns.json)**. Do not "fix" a correctness problem by editing pixels or rewriting content — record it as a send-back.8182## Step 3 — Verify (RE-RENDER, don't just eyeball the source)83After edits: if the render audit ran, **re-run `render_capture.js`** and confirm the previously-failing findings are gone — e.g. `zeroWidthCharts` is now empty, the 404 resolved, overflow cleared. That is real evidence the fix worked, not "the CSS looks right now." Then confirm all `data-*` attributes intact, no prose changed, asset paths unchanged (except a corrected ref), and re-grep for residual static issues (`verify_after_fixes` in fix_patterns.json). The capture→fix→recapture loop is **bounded to ≤2 rounds**; then record any residual issues and proceed.8485## Rules86- **Trust the render metrics over the source.** If `render_report.json` says a chart is 0-width, it IS invisible to readers no matter what the CSS text says.87- **Use inline styles** for layout fixes (avoid touching `<style>` unless necessary).88- **Preserve all attributes** — `data-des`, `data-ana`, `data-edt`, `id`, `class`.89- **Send back, don't redesign** — any correctness issue you can't fix with a safe markup edit goes in `auditor.json.send_backs` with its owning role; never rewrite content or regenerate assets yourself.90- **Oversized assets are a send-back, not a layout fix.** An asset over budget (audio > 3 MB, image > 600 KB, video > 3 MB) or a total payload > ~8 MB goes back to the **Designer** (generated media: regenerate/re-encode smaller) or **Scout** (sourced media: re-encode or pick a lighter, still-licensed asset) as `oversized_asset`. You don't compress assets yourself.91- **Mobile overflow is a real defect** — treat `mobile.horizontalOverflow` like desktop overflow and fix it in place (responsive width on the offending element); never wave it through because the desktop pass was clean.92- A blank chart from a **zero-width / shrink-to-fit mount** IS a layout fix (force the mount + `.vega-embed` to `display:block; width:100%`; center narrower charts via `.vega-embed svg{display:block;margin:0 auto}` — **never** `.vega-embed{display:inline-block}`, which makes Vega measure 0 width). A blank chart from **missing data/encoding** is a send-back to the Programmer.93- **Make minimal edits** — only fix what's broken; Edit, not full-section rewrites.9495## Step 4 — Report96Write `PROJECT_DIR/auditor.json`:97```json98{ "render_audit": {"ran": true, "status": "verified", "verdict": "issues",99 "zeroWidthCharts": [], "visibleCssLeak": [], "consoleErrors": 1,100 "mobileHorizontalOverflow": false, "totalPayloadMb": 19.2,101 "agents": 3, "confirmed_by_vision": ["des_07 video block"]},102 "fixes": [ {"type": "chart_mount_zero_width", "count": 1, "lines": [245]} ],103 "send_backs": [ {"type": "missing_asset", "element": "des_07", "severity": "high",104 "send_back_to": "designer", "resolution": "fixed",105 "detail": "assets/trophy_push.mp4 returns 404 — fetch/generate the video or drop the block",106 "evidence": "render_report console 404 + 2 vision agents"},107 {"type": "oversized_asset", "element": "assets/scout_bgm.flac", "severity": "high",108 "send_back_to": "scout", "resolution": "blocker_recorded",109 "blocker_reason": "only licensed track available; demo-gated, swap before publish",110 "detail": "13.4 MB FLAC (budget 3 MB) — re-encode to a compressed format or trim; total payload 19.2 MB > 8 MB",111 "evidence": "render_report.performance.oversizedAssets"} ] }112```113Set `render_audit.ran=false` when the browser wasn't available (static-only run). Use `[]` for empty arrays. Canonical `type` names are in **[`references/report_types.json`](references/report_types.json)**.114115**Every send-back carries a `resolution` (`fixed | blocker_recorded | open`) + a `blocker_reason`.** A send-back is `open` until acted on; after a clearing re-run set `resolution:"fixed"`, and if the call can't be fixed (a false positive, or a genuinely-unfixable constraint) set `resolution:"blocker_recorded"` with a `blocker_reason`. **A false-positive or non-fixable call is recorded as `resolution:"blocker_recorded"` with a `blocker_reason` — NEVER a silent severity-downgrade or omission** (the contract gate's Section 15 fails any send-back left `open` or any hard playtest fail with no matching resolution).116117**`render_audit.status` propagation.** When `render_capture.js` OR `playtest_drive.js` skipped (no browser/puppeteer), set `render_audit.status="UNVERIFIED-for-render"` (alongside `ran=false`); otherwise `status="verified"`. This status is **load-bearing, not a log line**: an `UNVERIFIED-for-render` run has its whole render/interaction class unchecked, so it **must not be presented as flagship-verified**. The orchestrator carries this status into the **final deliverable** — the page is surfaced as "render UNVERIFIED (no browser available); not flagship-verified" rather than silently shipped as flagship-quality. (A `verified` run with zero outstanding findings is the only state that may be called flagship-verified for render.)118119## Output120Modified `PROJECT_DIR/index.html` (layout fixed) + `PROJECT_DIR/auditor.json` (`render_audit` + `fixes` + `send_backs`) + `PROJECT_DIR/audit/render_report.json` + `PROJECT_DIR/audit/shots/*.png`.121122Done when: the live render audit reports no blank/0-width charts, no broken media, no desktop **or mobile** overflow, no oversized assets / over-budget payload, and a working interaction (or those are recorded as send-backs — oversized assets to Designer/Scout); every image subject has been verified; and the static checks are clean.123124## Dependencies (optional — graceful degradation)125The live render audit needs `puppeteer-core` (in the skill repo's `node_modules/`) + an installed Chrome or Edge. If absent, `render_capture.js` prints `{"skip":true}` and the Auditor runs static-only — the skill never breaks for users without a browser. To enable: run `npm install puppeteer-core` in the skill repo root; set the `CHROME_PATH` env var if the browser is in a nonstandard location.126127## Team coordination — Auditor team128129You are the **lead of the Auditor team**. Your member is the **Critic** (5-dimension content-quality review). You two are **orthogonal lenses on the same finished page**: you judge build/render correctness, the Critic judges whether the article is actually *good*. Together you produce the page's two send-back streams.130131- **Member call (mirrored from the orchestrator):** the orchestrator runs `Skill critic PROJECT_DIR` at **Stage 6.5**, after you finish. The Critic reads the rendered `index.html`, `verifier.json`, and the role JSONs.132- **Two send-back streams — MERGE + DEDUP before re-running upstream.** Your `auditor.json.send_backs` (render/build correctness — blank charts, broken/oversized media, overflow, dead interactions) and the Critic's `critic.json.dimensions[].send_back_to` (5-dimension content quality) are produced independently and can both flag the **same element** from different angles. Before any upstream role is re-run, the two streams must be **unioned and de-duplicated** so a co-flagged element is **routed once** (one consolidated fix to its owning role), not fixed twice or thrashed between rounds. Each item still carries its own `severity`/`detail`/`evidence`; dedup is on the target element + owning role.133- **The gates and `verify.py` fall between you and the Critic, and are orchestrator-owned.** After you finish (Stage 6) the top orchestrator runs the **contract gate `validate.py`** and the **Inspector team's `verify.py` (Stage 6.4)** before invoking the Critic, so the Critic scores against a contract-valid page with a fresh `verifier.json`. The **bounded revision loop (max 2 rounds)** that applies the merged send-backs — re-running the Programmer, you (the Auditor), `validate.py`, `verify.py`, then the Critic — is **run by the orchestrator**, not by this team.134135This is coordination prose, not a new gate: your `auditor.json` shape (`render_audit` + `fixes` + `send_backs`), the canonical `type` names, and your fix-in-place vs send-back rules are all unchanged.