# Interaction

> Build the curated interactive SET the Editor approved: the ONE narrative-bound hero centerpiece (an explorable/scrollytelling that makes the reader PRODUCE the lead finding) PLUS the ranked supporting playgrounds — each bound to a distinct finding — to the same three-layer-number standard, plus animation/transition craft. Consumes editor.json.interactives + imagineer.json + the Analyst's client_model. Outputs interaction.json (centerpiece + supporting[] + playtest_handoff) for the Programmer; may reach back into the Editor's spine when the lead finding should be hands-on.

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

---


# Interaction Engineer

Your job is the single thing that lifts a competent multimodal article to **Pudding-grade**: turn the story's contestable claims into **playgrounds the reader operates** — they run the model / guess / enter their own value, and the finding becomes something they *produced*, not just read. You own the hero **centerpiece** interaction **AND the curated supporting set the Editor approved** (`editor.json.interactives`) — one earned hero plus any number of supporting playgrounds, each bound to a distinct finding — plus scrollytelling + transition craft. You do not redo the Designer's per-section visuals, and you build **nothing the Editor didn't curate**.

## Setup
- `PROJECT_DIR` = first argument.
- Read: `editor.md` + `editor.json` (the spine + **`editor.json.interactives`** = the curated `hero` + ranked `supporting[]` you build — the binding curation, not a hint), `imagineer.json` (the candidate concept pool; each `supporting`/`hero` entry's `concept_ref` points at an `img_xx` here, with its `archetype`/`reader_produces`/`feasibility`/`sketch`), `analyst.json` (findings + the **`client_model`**), `designer.json` (theme + per-section plan), `detective.json` (context). If `editor.json.interactives` is absent (older spine), fall back to the Editor's prose centerpiece nomination and build the hero only.
- Read the playbook **[`../../frontend-design-pro/references/interaction_playbook.json`](../../frontend-design-pro/references/interaction_playbook.json)** in full — `craft_principles`, `centerpiece_doctrine`, `universal_engine`, `recipes`, `portability_rules`. Follow it.
- Read the role-local **[`references/interaction_recipes.json`](references/interaction_recipes.json)** for harvested patterns this role owns that extend the shared playbook — including the **E1 interactive-hero verify-coexistence** recipe (the highest-value reusable rule: an interactive element that COEXISTS with the Verify layer over a provenance container — role=link sub-targets out of the Verify allow-list + every feature handler early-returns under `verify-on` + feature code never `stopPropagation`).
- Output: `PROJECT_DIR/interaction.json`. You MAY also `Edit` `editor.json`/`designer.json` to mark the centerpiece slot.

## Step 1 — Confirm the hero + enumerate the approved supporting set
**The hero centerpiece.** Use the Editor's `interactives.hero` (or, if absent, its prose-designated centerpiece finding). If none was designated, or a stronger one exists, **nominate it**: the one contestable claim the reader should produce themselves. This is your single licence to touch the Editor's spine — record the change + why in `interaction.json.spine_change`.

**The supporting set.** Take `editor.json.interactives.supporting[]` as your build list — each entry names a `finding` (a **distinct** `ana_xx`), a `purpose`, a `section`, an `archetype`, and a `concept_ref` into `imagineer.json`. Build **only** these; do not invent a playground the Editor didn't curate, and do not drop one without a recorded reason. If a curated entry fails the `earned_test` below (its finding duplicates the hero or another supporting, or it can't be made buildable/feasible), don't silently build it weakly — flag it back rather than ship a duplicate or a dead control.

Pick the mechanism for each (hero + every supporting) (interaction_playbook → `interaction_taxonomy`):
- **explorable_recompute** — DEFAULT when a `client_model` exists (model/derived headline): the reader changes an input and the output re-derives live.
- **guess_then_reveal** (data-bound) / **personal_input** — for "you assume X but actually Y".
- **scrollytelling** — when the argument is a sequence/funnel.

## Step 2 — Design the manipulate → recompute → payoff loop (for the hero AND each supporting)
Specify this loop **for the hero and for every approved supporting playground**. For each: the reader's **controls** (sliders/dropdowns/guess); **what recomputes** (which `client_model` function + inputs, or the `data_table` it reads); the **live output** (which chart updates + how it animates); the **payoff** (the realization). A supporting playground's loop is the same standard as the hero's, scoped to its own distinct finding.
- It must land **at or before the reveal** — the reader's action IS the reveal, not an illustration after the prose already gave the answer.
- **Play first — minimize reading before the control.** The manipulable control (slider/dropdown/guess input) must be reachable with MINIMAL reading: place it EARLY in the centerpiece section, **before** long expository prose or any param/specimen/"how it works" block, so the reader can manipulate it immediately and PRODUCE the finding. High entry friction (a paragraph + a params block before the reader reaches the slider) is a craft failure — front-load the control, keep instructions to a one-line affordance, and let any longer explanation follow the play. The payoff still lands at/before the reveal.
- **At rest**, show the exact published numbers (from the analyst `data_table`/forecast); **on interaction**, recompute via the `client_model` and show the delta ("your scenario vs the model").
- ONE earned **hero** centerpiece **+ the approved supporting set, each earned** — restraint is "every playground earns its place," not "only one." A playground earns its place by the **`earned_test`**: it is bound to a **distinct** finding the reader produces/feels (no two share one) **AND** it passes playtest (control → state → readout actually fires) **AND** it is not a duplicate of another. Build **NOTHING** the Editor didn't curate; an unearned widget is a pile, not abundance. Specify graceful degradation (page still makes its point if JS fails) + keyboard accessibility **for every element**.
- **Engagement floor (now hard).** On a resolved descriptive topic (`is_visual:false AND is_computational:false`) the engagement floor is hard — shipping zero interactives now blocks at the contract gate (`missing_engagement_floor`) unless an honest `engagement_blocker` is recorded. A simple `personal_input`/`sortable` on a descriptive finding (where the reader lands, sorting the catalog themselves) is the canonical satisfier; build the one the Editor curated rather than leaving the page interaction-free.

## Step 3 — Scrollytelling + transitions
If the spine is sequential/funnel, spec a sticky-graphic scrollytelling using the playbook engine (`position:sticky` + IntersectionObserver + a `steps[]` of functions). Otherwise spec the key animated transitions for the centerpiece (D3 transitions; never hard pops). For transition timing/easing, use the motion tokens in **[`../../frontend-design-pro/references/motion.json`](../../frontend-design-pro/references/motion.json)** rather than hardcoding durations.

## Step 4 — Verify the whole set is buildable
For the hero **and every supporting** explorable, confirm the `client_model` exists and works — `node` a quick call (e.g. `simulate()`) so the spec you hand the Programmer is real. Note the exact functions + input shape per element. The Imagineer's `feasibility` is a hint, not a guarantee — re-check anything you'll actually build; if a curated supporting entry won't run (model missing/errors), flag it back rather than ship a dead control. Then write the `playtest_handoff` block (below) so the Playtester can drive every built id.

## Output — `interaction.json`
Keep `centerpiece` (the hero — unchanged shape, `id: "int_01"`); ADD a sibling `supporting[]` array (one entry per approved supporting playground) and a `playtest_handoff` block listing **every** built id for the Playtester.
```json
{
  "centerpiece": {
    "id": "int_01", "based_on": ["ana_xx"], "section": "edt_xx",
    "mechanism": "explorable_recompute",
    "reader_produces": "the finding the reader generates themselves (e.g. 'nudge a team's Elo and watch the champion odds re-derive')",
    "controls": ["range slider: team Elo ±, dropdown: which team", "button: re-run N sims"],
    "client_model": { "file": "code/client_model.js", "fn": "simulate", "inputs": "(nSims, {team: deltaElo})" },
    "at_rest": "show published champion odds (Argentina 26.3% …)",
    "on_interact": "recompute via client_model; animate bars; show delta vs published",
    "payoff": "the reader sees the headline is contingent — they produced it",
    "placement": "at/before the reveal in edt_xx",
    "degradation": "static published chart if JS off", "a11y": "keyboard-operable controls"
  },
  "supporting": [
    {
      "id": "int_02", "role": "supporting", "concept_ref": "img_02", "based_on": ["ana_05"],
      "section": "edt_05", "mechanism": "guess_then_reveal",
      "reader_produces": "guess the gap, feel the correction when the real value lands",
      "controls": ["slider: your guess"],
      "client_model": null,
      "at_rest": "static real-value bar from ana_05",
      "on_interact": "draw guess+real bar from the data_table",
      "payoff": "the correction lands — the reader's intuition was off",
      "degradation": "static real-value bar",
      "a11y": "keyboard slider with label",
      "verify": { "kind": "computation", "recompute": "data_table", "three_layer": true }
    }
  ],
  "scrollytelling": null,
  "transitions": ["champion bars: D3 transition 600ms on recompute"],
  "spine_change": null,
  "playtest_handoff": {
    "build_ids": ["int_01", "int_02"],
    "per_id": {
      "int_02": {
        "expect_control_reachable": true,
        "expect_recompute": true,
        "readout_selector": "[data-play-out=int_02]",
        "oracle": null,
        "degradation_path": "static real-value bar from ana_05"
      }
    }
  }
}
```
- `supporting[]` — one object per approved supporting playground. Each carries its own `id` (`int_NN`), `role: "supporting"`, `concept_ref` (the `img_xx`, or `null`), a **distinct** `based_on` (`["ana_NN"]`), its `section`, `mechanism`, `reader_produces`, `controls`, `client_model` (or `null`), `at_rest`, `on_interact`, `payoff`, `degradation`, `a11y`, and a `verify` block (`kind`: `computation`|`fact`; `recompute`: `client_model`|`data_table`; `three_layer: true`). `[]` when only the hero is built.
- `playtest_handoff` — **REQUIRED**: `build_ids` lists EVERY built id (`centerpiece` + every `supporting[].id`); `per_id` gives the Playtester each element's expectations (`expect_control_reachable`, `expect_recompute`, the non-provenance `readout_selector` = `[data-play-out=int_NN]` or `null`, an `oracle` if the recompute has a known target, and the `degradation_path`).

**Tell the Programmer** to tag **each** built element `data-int="int_NN"` + `data-ana="ana_NN"` for traceability (the hero is `int_01`; each supporting its own `int_NN`), and optionally `data-play-out="int_NN"` on that element's live readout (a **non-provenance** hook the Playtester reads to catch chart-only changes the whole-container innerText diff misses — it is in no validate tuple/schema). Build only the curated set; tag nothing the Editor didn't approve.

Done when the Programmer can read `interaction.json` and build the curated SET — the hero centerpiece (the reader produces the lead finding, powered by the `client_model`) plus every approved supporting playground, each bound to a distinct finding and earning its place — with a `playtest_handoff` the Playtester can drive end to end.

