# Programmer

> 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.

- Skill: `qinghonglin/programmer-2` (Agent Skill, multi-file: 5 files)
- Install (CLI): `npx skillmds@latest add qinghonglin/programmer-2`
- Raw SKILL.md: https://api.skillmd.com/api/skills/qinghonglin/programmer-2/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Web & Frontend
- Author: QinghongLin (https://skillmd.com/u/qinghonglin)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/qinghonglin/programmer-2

---


# 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_table`s
  - `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`](../../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`](../../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`](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:

```html
<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:**
1. **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.
2. **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`.
3. **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`](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`](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`](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`](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`](../../frontend-design-pro/references/design_tokens.json)**, follow **[`../../frontend-design-pro/references/layout.json`](../../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`](../../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`](../../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`](../../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`](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`](references/component_implementations.json)**. For the **BGM now-playing card**, build the enriched **`signature_media_card`** from **[`../../frontend-design-pro/references/components.json`](../../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`](../../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`](../../dataviz-craft/references/vega_recipes.json)** for the Vega-Lite spec, **[`../../dataviz-craft/references/annotation_layers.json`](../../dataviz-craft/references/annotation_layers.json)** to add the annotation layer, and **[`../../dataviz-craft/references/axis_label_polish.json`](../../dataviz-craft/references/axis_label_polish.json)** for axis/label polish. Reach for **[`../../dataviz-craft/references/d3_fallback_recipes.json`](../../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.

1. **Copy the panel shell into `index.html` verbatim.** Paste **[`../inspector/references/inspector_panel_reference.html`](../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).

2. **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`](../inspector/references/verify_map.schema.json)** — and copy the field shape from the filled example **[`../inspector/references/examples/verify_map.example.json`](../inspector/references/examples/verify_map.example.json)** (one entry of each kind), replacing its toy transit content with yours.

3. **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`](../inspector/references/run_cells.schema.json)**, and copy the filled example **[`../inspector/references/examples/run_cells.example.json`](../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.

4. **Author the reproducible notebook** *(premium profile only — skip in fast)* `PROJECT_DIR/verify/<topic>_forecast.ipynb` — re-runs the pipeline from raw data and `assert`s the reproduced headline numbers match the published ones. Follow the recipe in **[`../inspector/references/reproducible_notebook.md`](../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`](../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`](../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/verify_map.example.json)** + **[`../inspector/references/examples/run_cells.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`](../inspector/references/examples/cell_registry.example.json)**. Conform to the schemas in **[`../inspector/references/`](../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.

