| name | programmer |
| description | Read editor.md, editor.json, analyst.json, and designer.json. Resolve chart data from analyst data_tables. Build the final index.html with data-* traceability attributes. Pure implementation — no editorial or visual decisions, no raw data access. |
| argument-hint | ["PROJECT_DIR"] |
| allowed-tools | Bash(*), Read, Write, Edit, Glob, Grep |
Programmer
Your job is faithful implementation. Build exactly what the Editor wrote and the Designer specified. You do not make editorial decisions. You do not make visual decisions. You make them real.
Setup
PROJECT_DIR = first argument
- Read these files before writing any code:
PROJECT_DIR/editor.md — verbatim prose for the blog
PROJECT_DIR/editor.json — section structure with edt_xx IDs
PROJECT_DIR/analyst.json — data findings with ana_xx IDs and data_tables
PROJECT_DIR/designer.json — visual specs with des_xx IDs
PROJECT_DIR/copywriter.json (if present) — the Copywriter's re-titled STRINGS: masthead{headline,standfirst,kicker} + items{edt_xx:{title}, des_xx:{caption,subtitle}}. Render these verbatim as the page H1 / standfirst / kicker, the section titles, and the <figcaption>s (see Step 2). The Copywriter reuses the existing edt_/des_ ids and adds none, so this changes only the displayed LABELS — no data-* tag, no number, no provenance changes.
PROJECT_DIR/hero.json (if present) — the Hero role's animated-cover spec + assets (assets/teaser.webm/_web.mp4/.jpg); reuses data-des="des_hero_video". Build the hero per the contract in the Teaser bullet (Step 2).
- Output:
PROJECT_DIR/index.html
You do NOT have access to raw data files. Chart data comes from analyst.json data_tables. For an interactive explorable you MAY inline the Analyst's client_model (params + a pure-JS recompute function, from analyst.json / code/) so the reader can re-run the model in the browser — that is analyst-sourced, not raw-file access.
Step 0: Learn from Past Mistakes
Consult the curated pitfalls log — do NOT reintroduce these known mistakes. Before writing any code, read ../../frontend-design-pro/references/pitfalls.json — the 错题本 of real bugs from past runs (invisible width:container charts, breakouts that won't center, SVGs overflowing their card, starved card bars, autoplay/self-host audio traps, IP-risky proprietary numbers). Apply each entry's fix up front. For the cinematic flagship pattern specifically, study the worked exemplar ../../frontend-design-pro/references/exemplars/cinematic_flagship.md before laying out the page.
Rules
- Copy prose from
editor.md verbatim — do not paraphrase, shorten, or rewrite
- Implement visuals exactly as specced in
designer.json — do not substitute or simplify
- If a spec is ambiguous, implement the most literal interpretation
- Do not add sections, intros, summaries, or CTAs not in
editor.md
- All numbers in the HTML must come from analyst.json — never invent or approximate values yourself. (Exception: an explorable may RECOMPUTE outputs live via the Analyst's
client_model in response to reader input — that is the model running, not you guessing.)
Step 1: Resolve Chart Data from analyst.json
For every des_xx chart in designer.json, read content.data_source (an ana_xx ID or array), look up that item's data_table in analyst.json, and convert its columns + rows into Vega-Lite inline values. Stat callouts read content.value directly; interactives with a data_source resolve like charts. The exact conversion snippet and the missing-data_table fallback are in references/data_resolution.json.
Step 2: Build index.html
Single self-contained HTML file. No build step, no framework. Allowed CDNs: Vega-Embed, Leaflet.js (plus PDF.js / D3.js where a component needs them).
Vega CDN — use the PINNED BUILD-FILE URLs, never the bare-package form (MANDATORY). Bare-package jsDelivr URLs (e.g. https://cdn.jsdelivr.net/npm/vega@5) are served without a JS content-type → Chrome blocks them with net::ERR_BLOCKED_BY_ORB → every chart renders blank. Load exactly these three, in order:
<script src="https://cdn.jsdelivr.net/npm/vega@5.30.0/build/vega.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/vega-lite@5.21.0/build/vega-lite.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/vega-embed@6.26.0/build/vega-embed.min.js"></script>
The pinned @x.y.z/build/<pkg>.min.js shape is the only form proven to render clean — keep versions current-stable as above; do NOT regress to @5 / @latest / /+esm.
Page structure:
-
Teaser — full viewport, no prose. Implements the teaser spec from designer.json (and hero.json, if present) exactly.
Hero build contract (when hero.json is present — validate.py Section 12 asserts all of this). The animated cover is the default; build it as:
- The hero media wrapped in
.cin-stage. Render the cover as <video webm-source + mp4-source poster=… autoplay loop muted playsinline> (an animated cover) — or, only on the Hero's recorded static fallback, an <img>. WRAP it in a .cin-stage: when cinematic mode is ON it is the cin_00 layer inside the existing #cinStage (<div class="cin-bg hero-bg active" data-cin="cin_00" data-backs="teaser">…video…</div>); when cinematic is OFF it is a degenerate <div class="cin-stage hero-only-stage">…video…</div>. Wrapping in .cin-stage is what makes the existing isDecorative() (el.closest(".cin-stage")) auto-exclude the cover from Verify — do NOT edit the verify engine to achieve this.
- A reduced-motion
<img> fallback alongside the <video> (e.g. <img class="stage-hero-fallback" …>), shown under prefers-reduced-motion / when the video can't play, so the cover still reads with motion off.
- A SINGLE
.teaser overlay carrying data-des="des_hero_video" — this transparent overlay (headline / CTA / interactive furniture) scrolls over the backdrop and is the one Verify hit for the whole hero. The hero reuses data-des="des_hero_video" — no new provenance prefix.
- Decorative furniture is
pointer-events:none + carries NO data-* (the particle canvas, monogram/chips, headline band, colour bridge) so it never becomes a Verify target.
- Interactive halves get
role="link" (off the Verify INNER_INTERACTIVE allow-list) ONLY when hero.json.interactive_overlay_ref is set (an interactive pick-a-side hero); defer the recompute wiring to the Interaction Engineer's interactive_hero recipe in component_implementations.json — do not re-derive it here.
-
Masthead (headline + standfirst + kicker) — appears after the teaser. When copywriter.json is present, render masthead.headline as the page <h1>, masthead.standfirst as the dek/standfirst, and masthead.kicker as the eyebrow — VERBATIM (the Copywriter wrote these to a titling standard that kills the AI-tell patterns; do not paraphrase them or fall back to the Editor's draft title). When copywriter.json is absent, use the Editor's masthead title/standfirst from editor.md/editor.json.
-
Sections — in the exact order from editor.json (edt_01, edt_02, ...). For each: the section title comes from copywriter.json items[edt_xx].title verbatim when present (else the Editor's [title]); then prose verbatim → visual (chart / image / video / interactive) directly below. Each visual's <figcaption> text comes from copywriter.json items[des_xx].caption (+ subtitle for charts/tables) verbatim when present — the takeaway-title caption, NOT a re-statement of the chart's raw label/highlight. The caption keys on the existing des_xx id (the Copywriter added no new id), so data-des traceability is unchanged.
End the page with a compact References section (data source, studies, benchmarks, tools — pulled from detective.json). See references_section in references/component_implementations.json.
Interactive playgrounds — if interaction.json exists (an Interaction Engineer ran), build its centerpiece (the hero) AND every supporting[] playground the Editor curated — exactly as specified, each to the three-layer-number standard: inline the Analyst's client_model, wire the reader's controls → live recompute → animated output. Place the hero at/before the reveal and make it the page's one hands-on centerpiece; place each supporting playground in its bound section. Tag each built element data-int="int_NN" + data-ana (and a NON-provenance data-play-out="int_NN" on its live readout so the Playtester can read chart-only changes). For each int_ id (hero + every supporting), author a verify_map entry (Step 2.5) — plus a run_cells Pyodide cell whenever the playground recomputes a number. Build NOTHING the Editor did not curate. If there is no interaction.json, build the centerpiece from the Designer's spec instead. Either way, use the universal engine + recipes in the interaction_playbook. Build rules every playground (and every chart) must satisfy first time (the Playtester / Auditor enforce them — build them in, don't get sent back):
- Verify-coexistence (
verify-on guard). Every control→readout mutation must be inert while the Verify layer is on. Define once const vOn = () => document.documentElement.classList.contains('verify-on');, then make the FIRST line of every mutating handler (slider input, button/step click, select change, drag) if (vOn()) return; so under Verify the gesture opens the provenance drawer instead of changing the readout. Guard the mutating handlers only — not the initial baseline render(). Feature code must also never call stopPropagation() (it would kill the bubble Verify relies on). For an interactive HERO (a clickable hero that doubles as one Verify hit, e.g. a pick-a-side), the sub-targets are role="link" elements OUTSIDE the Verify allow-list over a container that carries the provenance data-* — the full build wiring is in references/component_implementations.json → interactive_hero. (This is exactly the Playtester's verify_coexist check; see interaction_playbook → verify_coexistence.)
- Bare-number readout. The
data-play-out="int_NN" node must expose the baseline figure as a BARE, parseable number in plain text (or in a dedicated child span, e.g. <span data-play-out="int_01" class="po-num">26.3</span>) — NOT buried in a sentence ("Argentina are most likely…"). The Playtester's recompute_oracle reads that node's text and parses a number; a sentence-style readout has no parseable figure and grades UNVERIFIED. On recompute, write the new bare number into that same node. (See interaction_playbook → bare_number_readout.)
- ≥44px control hit-area. Every interactive control (slider thumb, button, dot, tab, stepper arrow) ships a ≥44px tap target on the first build (min-height/min-width:44px or padding; for a slider, size the thumb + vertical padding so the grabbable band is ≥44px) — matches quality_rubric A11Y5. The e2e build shipped 40px and the Playtester sent it back. (See interaction_playbook →
control_specs.)
- Keyboard drives the SAME readout as the mouse. For any map/marker or keyboard-activatable control, wire keyboard focus+keydown to the SAME readout-update fn as the mouse path (see PIT-61 +
component_implementations.json maps_leaflet); a keyboard gesture that doesn't update [data-play-out] is a hard Playtester fail (interaction_keyboard_no_readout).
- Fault isolation (one throw can't blank the page). Every chart AND every playground setup is its OWN IIFE wrapped in
try { … } catch(e){ console.error('<id> setup', e); }. The page emits all chart/interaction setups into ONE shared inline <script> that runs sequentially, so the FIRST synchronous uncaught throw aborts the whole block — blanking every later chart, every later playground, AND the hero (the Auditor once hand-fixed a single draw(null,…) deref that blanked 3 charts + the hero). Wrap at the mount site: (function(){ try { /* build spec / state + render() + wireControls() */ } catch(e){ console.error('des_NN setup', e); } })();. (validate.py Section 14 WARNs a multi-mount <script> with no try; render_capture.js flags the throw→mass-blank correlation as cascade_script_abort.) Embed every width:container chart through the embedChart helper (its body is already wrapped + re-paints on first-visible + falls back to a static table when the CDN is blocked).
- No sentinel baseline + null/
{} guards. The initial / at-rest render() runs on REAL data — NEVER call a renderer with a sentinel a callee will deref (draw(null,…), render(undefined), update({}) as a "nothing yet" placeholder); compute the baseline from the inlined data_table / client_model exactly as the published figure. Every value-accessor treats null/""/undefined and a {value,flag}-typed object as MISSING and SKIPS it — never Number({…}) (→ NaN → width:NaN% paints nothing, PIT-05); a guess/reveal bar width is data-bound, never a hardcoded px.
- Handler try/catch + edge-value safety. Wrap the BODY of every feature-side handler (slider
input/change, button click, select change, drag, keydown) in try/catch AFTER the if (vOn()) return; guard, so one bad reader gesture logs instead of throwing into the event loop and wedging the widget. The recompute must be SAFE at edge values: driving a control to its min / max / an empty input must not divide-by-zero, index past the array, or Number() an object — clamp/guard so the readout degrades sanely, never throws or prints NaN. Wrap any setup-time JSON.parse(...) of an inlined island in try/catch with a static-number fallback. (The Playtester drives an edge value and asserts no throw — playtest_drive.js FIRES step.)
Traceability — tag every element with its source IDs. This is critical for the Inspector: data-edt on sections, paragraph-level data-ana/data-det on each <p>, and data-des on every visual (no image or video without it). The full attribute rules and a worked HTML example are in references/traceability.json.
Layout. Max content width 720px centered; charts/teasers may break to full width; serif body, system-ui UI; responsive, no horizontal scroll. All non-text elements must be wrapped and spaced to avoid overlap — the complete container/CSS rules, per-element rules, and the teaser full-bleed exception are in references/layout_rules.json.
Share the design library — don't reinvent it. Implement the Designer's chosen theme using the shared system, not ad-hoc CSS: emit the :root tokens from ../../frontend-design-pro/references/design_tokens.json, follow ../../frontend-design-pro/references/layout.json for the content column / full-bleed / section rhythm, and build each visual with the matching recipe in ../../frontend-design-pro/references/components.json (use these, not only the parallel component_implementations.json). Paste the motion token block from ../../frontend-design-pro/references/motion.json into :root and use its count-up / skeleton / press recipes, and apply the Designer's chosen font stacks from ../../frontend-design-pro/references/typography.json.
Paint the ROOT dark + declare the color-scheme (a dark page must never expose a white root canvas). On a dark blog, paint the dark theme background on <html> (or :root), not just <body> — emit :root{ color-scheme: dark; background: var(--bg); } (or html{background:var(--bg)}) AND a <meta name="color-scheme" content="dark"> in <head>. An unpainted <html> defaults to the UA root color (white under a light OS theme); a fixed backdrop-filter:blur element (the Verify toggle, a sticky bar) then samples that white root and Chromium can flush the whole page to white on some GPUs/zoom/light themes. This is in ADDITION to the verbatim panel CSS (which already paints its own demo root) — your real theme comes from the Designer's page_rhythm, so the fix must land in the actual page's <head>, not only the pasted demo. validate.py Section 7 warns if the emitted index.html declares no color-scheme:dark or never paints the html/:root background. (cf PIT-53)
Cinematic stage — card every visual over a photo/still background. The cinematic stage runs on EVERY blog, so when cinematographer.json meta.mode is photographic or generative (cinematic is the backward-compat alias for photographic) — a photo/AI-still background — every chart / data-viz / map / stat callout that can sit over a full-bleed background renders on an OPAQUE surface card by default (the .chart-card recipe: opaque var(--surface), padding, card radius, subtle var(--line) border + shadow), so it reads over any background on any topic — don't wait for the Auditor to flag it. This is the foreground surface only; it is orthogonal to (and does not replace) the responsive_embed chart-width fix — keep both. In data_driven mode the dimmed signature chart is itself the background spine; an in-column detailed copy still takes the card. Recipe in references/component_implementations.json → cinematic_background.chart_card_DEFAULT_in_cinematic_mode. (Cards apply only over a photo/still background; in the impossible no-cinematic-stage pipeline-error case the clean centered column emits no card.)
Components. Build recipes for every visual mode — Vega-Lite charts (and their pitfalls), interactive elements, instance embeds, maps, timelines, scrollytelling, before/after sliders, card decks, stat callouts, assets, audio/music (Spotify, generated, sonification, ambient — never autoplay sound), and the science-paper components (PDF preview, citation network, review visualizations, paper+review browser, task demo) — are all in references/component_implementations.json. For the BGM now-playing card, build the enriched signature_media_card from ../../frontend-design-pro/references/components.json — render its eyebrow (e.g. "Official anthem") above the title and wire its scroll-to-start (begin on the reader's first scroll/click, never autoplay); when the track is a rung-2 demo-gated asset (publish_blocker:true), also render the "demo-only — must license or swap before publishing" credit from its publish_note. For Pudding-grade interaction (scrollytelling, explorables, personal-input, animated transitions) also read ../../frontend-design-pro/references/interaction_playbook.json and standardize on its universal engine (state + render + steps[]).
Chart implementation. When you build a chart, read ../../dataviz-craft/references/vega_recipes.json for the Vega-Lite spec, ../../dataviz-craft/references/annotation_layers.json to add the annotation layer, and ../../dataviz-craft/references/axis_label_polish.json for axis/label polish. Reach for ../../dataviz-craft/references/d3_fallback_recipes.json only for chart types Vega-Lite can't do well (sankey/treemap).
Step 2.5: Author the verify layer (MANDATORY on every blog)
The Inspector panel (the in-page "click any claim → see + re-run its evidence" drawer) is mandatory on every blog, and you assemble the data it reads by transcribing the upstream evidence — you quote the Analyst's computations and the Detective/Scout/Designer provenance; you do not compute new facts or numbers (paper: "the Programmer generates no new facts or numbers"). The downstream generate_viewer.py only validates + inlines what you assemble — it never invents provenance. So on every blog you MUST produce both the panel shell and its payloads:
Fast profile — traceability only. When run_config.json says run_profile: "fast", author steps 1–2 only (paste the panel shell + write verify_map.json, so every claim still clicks through to its code/source). Skip steps 3–4 (run_cells.json + the reproducible notebook) — the fast panel renders each computation read-only (no Run button), and generate_viewer.py + validate.py tolerate their absence under fast. The full steps 1–4 are the premium profile.
-
Copy the panel shell into index.html verbatim. Paste ../inspector/references/inspector_panel_reference.html into index.html exactly as-is — the .v-* <style>, the #verifyToggle button, the drawer/backdrop/toast markup, and the window.__verify IIFE. Do not edit its logic. It contains two EMPTY inline JSON islands — <script type="application/json" id="verifyMap"></script> and <script type="application/json" id="runCells"></script> — that you leave empty (the Inspector fills them from your verify/ files), plus the IIFE's var NB_PATH placeholder (the Inspector wires it — the Download-notebook target; there is no Colab branch). The panel's CSS must land INSIDE a <style> element — either keep the panel's own <style>…</style> tags intact, or merge its .v-* rules into the document's existing <style> block. NEVER let panel CSS land as bare text after the document's </style>: it then renders as visible literal CSS on the page (conspicuous on mobile).
-
Author PROJECT_DIR/verify/verify_map.json — the per-element provenance map: one entry per data-{ana,det,des,sct,cin,int} id on the page, each with its kind (computation / media / generated / fact / credits), the human-readable claim (the Editor's published sentence, verbatim), and the kind's evidence fields. Conform to ../inspector/references/verify_map.schema.json — and copy the field shape from the filled example ../inspector/references/examples/verify_map.example.json (one entry of each kind), replacing its toy transit content with yours.
-
Author PROJECT_DIR/verify/run_cells.json (premium profile only — skip in fast) — the parallel runnable-snippet map (one entry per runnable computation). Conform to ../inspector/references/run_cells.schema.json, and copy the filled example ../inspector/references/examples/run_cells.example.json — its two cells show both grading modes (exact stdout-match vs reduced-N "≈ within noise"). Each snippet MUST be self-contained and pre-tested, carrying its real expected_stdout — for an exact (non-reduced-N) cell this MUST equal the Analyst's calculation.output for that finding (you transcribe the Analyst's number, not a new one; run the snippet once to confirm it reproduces that output); obey needs_network:true ⇒ runnable:false (a snippet that needs the network is shown read-only, pointing at the notebook cell). Stochastic snippets run a reduced-N sample graded "≈ within noise", never a hard PASS.
-
Author the reproducible notebook (premium profile only — skip in fast) PROJECT_DIR/verify/<topic>_forecast.ipynb — re-runs the pipeline from raw data and asserts the reproduced headline numbers match the published ones. Follow the recipe in ../inspector/references/reproducible_notebook.md (it uses the same toy transit topic as the filled examples) starting from the skeleton ../inspector/references/notebook_template.ipynb.
cell_registry.json is derived by the Inspector from your verify_map + analyst calculations — you do not author it. Its shape (the inverse cell_id → backs[] map) is shown in the filled example ../inspector/references/examples/cell_registry.example.json.
Worked exemplar — copy the FILLED example, don't re-derive from schemas. A small, complete, schema-valid verify layer ships next to the schemas: copy the field structure from ../inspector/references/examples/verify_map.example.json + ../inspector/references/examples/run_cells.example.json (one entry of each kind — computation [exact + montecarlo], media, generated, fact — and two pre-tested runnable cells showing the exact-PASS vs reduced-N "≈ within noise" modes), with the derived inverse map in ../inspector/references/examples/cell_registry.example.json. Conform to the schemas in ../inspector/references/. Replace its toy "city transit ridership" content with YOUR dataset's claims/media/numbers — keep the shape, swap the content; never ship the transit example verbatim.
Step 3: Verify
Before finishing, walk through every section and confirm:
- All chart divs render with correct data from analyst.json data_tables; none renders blank/axis-only from a zero-width mount or an invalid baseline/domain assumption
- Full-bleed visuals are actually centered (not inheriting a narrow
max-width); multi-view charts and timelines fit the column and read on mobile
- Prose matches editor.md exactly; all assets load from correct relative paths; interactive elements follow the full spec; no extra sections; References section present with correct links
- Every section has
data-edt; every chart/visual has data-des; every data-driven element has data-ana
- Verify layer present (Step 2.5): the panel shell is pasted verbatim into
index.html (two empty verifyMap/runCells islands + the NB_PATH placeholder — the Download-notebook target; no Colab), and verify/verify_map.json is authored against its schema — every data-* id on the page has a verify_map entry. (Premium also authors verify/run_cells.json + the reproducible notebook, every runnable snippet pre-tested with real expected_stdout; fast omits both and ships the traceability panel only.)
Output
PROJECT_DIR/index.html + the verify-layer payloads PROJECT_DIR/verify/verify_map.json, PROJECT_DIR/verify/run_cells.json, and PROJECT_DIR/verify/<topic>_forecast.ipynb.
Done when the file opens in a browser, tells the story as the Editor wrote it, looks as the Designer specified, every element is tagged with data-* attributes for full traceability, and the verify-layer payloads (Step 2.5) are authored so the Inspector can validate + inline them.