# Verification Plan — Step 5d

> **What this is:** a tiny manifest, written once between bundle analysis and code generation, that tells every downstream verification stage *which classes of bug it should look for on this specific site*.
>
> **Why this exists:** running every verification on every site is slow and noisy. Skipping verifications by gut feel is how regressions ship. The plan is the third option — explicit, derived from extraction artifacts, auditable.

## Two artifacts live here

| File | Author | Lifetime | Purpose |
|---|---|---|---|
| `verification-plan.json` | auto-generated by `verification-plan.sh` | refreshed any time upstream artifacts change | declares which downstream checks are required, based on detected animation/interaction types |
| `known-artifacts.json` | agent-curated during diagnosis | grows during section-compare iterations | records section/transition FAILs that have been verified as capture-timing / non-deterministic-ref artifacts, with the evidence that justifies the verdict |

Both live at `tmp/ref/<component>/`. Both are JSON. Both are consumed by gates in `ui_clone/gate.py`.

---

## 1. `verification-plan.json` — required-checks manifest

### Producer

`skills/visual-debug/scripts/verification-plan.sh tmp/ref/<component>` reads these inputs:

| Input | Lives at | Signal extracted |
|---|---|---|
| `bundle-map.json` | `tmp/ref/<c>/` | which JS library handles which animation |
| `interactions-detected.json` | `tmp/ref/<c>/` | hover / scroll / IO / click presence + `hasPreloader` |
| `external-sdks.json` | `tmp/ref/<c>/` | GSAP / Lenis / Framer / Motion detection |
| `scroll-engine.json` | `tmp/ref/<c>/` | custom scroll engine (`hasTransformScroll`, library name) |
| `transition-spec.json` | `tmp/ref/<c>/` | `trigger` field for every transition entry |
| `canvas-webgl-detection.json` | `tmp/ref/<c>/` | `hasCanvas` flag |

### Schema

```json
{
  "schemaVersion": 1,
  "generatedAt": "2026-05-11T07:42:00Z",
  "component": "<c>",
  "signals": {
    "hasScrollScrub": true,
    "hasIOReveal": true,
    "hasHover": true,
    "hasSplash": true,
    "hasCanvas": false,
    "hasCustomScroll": false,
    "hasCommercialFont": false
  },
  "requiredChecks": [
    {
      "id": "hydration-check",
      "script": "skills/visual-debug/scripts/hydration-check.sh",
      "produces": "hydration-check.json",
      "reason": "Universal — every HTML page must hydrate cleanly",
      "severity": "block"
    },
    {
      "id": "scroll-end-completion",
      "script": "skills/visual-debug/scripts/scroll-end-completion-check.sh",
      "produces": "scroll-completion.json",
      "reason": "signals.hasScrollScrub=true — scroll-scrub reveals must settle by maxScroll across all supported viewports",
      "severity": "block"
    },
    {
      "id": "reveal-trigger",
      "script": "skills/visual-debug/scripts/reveal-trigger-check.sh",
      "produces": "reveal-trigger.json",
      "reason": "signals.hasIOReveal=true — initially-hidden elements must advance after IO fires",
      "severity": "block"
    },
    {
      "id": "transition-compare",
      "script": "skills/visual-debug/scripts/transition-compare.sh",
      "produces": "transitions/result.txt",
      "reason": "signals.hasHover=true",
      "severity": "block"
    }
  ],
  "viewports": [
    { "w": 375, "h": 812, "label": "mobile" },
    { "w": 1280, "h": 800, "label": "laptop" },
    { "w": 1600, "h": 900, "label": "desktop-mid" },
    { "w": 1920, "h": 1080, "label": "desktop-large" }
  ]
}
```

### Field semantics

- `signals.*` — booleans inferred from extraction artifacts. `hasScrollScrub` is true iff *any* of: `external-sdks.json` lists Framer Motion `useScroll` / Motion `useScroll`, `scroll-engine.json.libraryName` is set, OR `transition-spec.json` has a `trigger: "scroll-scrub"` entry, OR `interactions-detected.json` has an entry with `engine: "scroll"`.
- `requiredChecks[]` — derived from signals using the dispatch table below. The list is *closed* once written: downstream gates only honor entries from this list. If a signal flips later, regenerate the plan.
- `severity` — `block` (gate FAILs if check artifact is missing or status≠pass) or `warn` (gate prints warning, returns 0).
- `viewports[]` — every applicable check that takes viewport arguments must iterate over this list. Override via the `VIEWPORTS` env var if a site supports something unusual (e.g. ultrawide-only).

### Signal → check dispatch table

| Signal | Check id | Severity | Notes |
|---|---|---|---|
| *always* | `hydration-check` | block | universal — runs on every page |
| *always* | `tailwind-transform-conflict` | block | universal — flags Tailwind v3↔v4 transform shorthand/individual-property stacking (Root Cause I). Safe on non-Tailwind / single-version hosts (script writes `status: pass`). |
| `hasScrollScrub` | `scroll-end-completion` | block | catches viewport-dependent "scroll never completes" bugs |
| `hasIOReveal` | `reveal-trigger` | block | catches "stuck IO behind overflow:hidden" bug class |
| `hasHover` | `transition-compare` | block | predefined hover/transition gate (existing) |
| `hasSplash` | `splash-extraction` artifacts present | warn | existing — splash-extraction.md handles |
| `hasCanvas` | `canvas-webgl-extraction` followed | warn | existing — canvas sub-doc handles |
| `hasCommercialFont` | `font-parity` | block | existing — font-parity.json gate |

### When to (re)generate

- **First time:** at the end of Step 5d (after `transition-spec.json` is written).
- **Re-run when:** any upstream artifact in the producer table changes. The script is fast (no browser work — pure JSON reading).
- **Stale detection:** `gate_post_implement` flags the plan as stale if any source artifact has a newer mtime.

---

## 2. `known-artifacts.json` — capture-noise whitelist

### What it is, what it isn't

A list of **sections or transitions whose FAIL state has been independently verified as not-a-bug**. Capture timing variance, non-deterministic reference (canvas frames, video playback, RNG), shader warm-up — anything that produces FAIL on AE/SSIM but matches the reference *behaviorally*. Per-entry evidence is required.

This is **not** a "muting" mechanism for inconvenient failures. The gate validates each entry; entries without proof are ignored.

### Schema

```json
{
  "schemaVersion": 1,
  "component": "<c>",
  "sections": [
    {
      "name": "masthead",
      "verifiedBy": "manual-frame-cmp",
      "evidence": "tmp/ref/<c>/sections/diff/masthead.png inspected; impl renders identical hero; AE artifact from <video> autoplay frame offset",
      "aeThresholdCeiling": 850,
      "verifiedAt": "2026-05-09T11:42:00Z"
    },
    {
      "name": "page_first__r2OaE",
      "verifiedBy": "readPixels",
      "evidence": "HeroCanvas WebGL readPixels shows identical luminance histogram (see HeroCanvas test in <c>/__tests__)",
      "aeThresholdCeiling": 1200,
      "verifiedAt": "2026-05-09T19:55:00Z"
    }
  ],
  "transitions": [
    {
      "id": "cardBubble",
      "verifiedBy": "bundle-grep",
      "evidence": "transition-spec.json says infinite flap; impl matches bundle parameters; capture-timing artifact only",
      "aeThresholdCeiling": 600,
      "verifiedAt": "2026-05-10T02:14:00Z"
    }
  ]
}
```

### Required fields per entry

- `name` (sections) / `id` (transitions) — must match exactly. No substring or glob.
- `verifiedBy` — one of:
  - `readPixels` — pixel-level proof from a canvas/WebGL `readPixels` invariant
  - `bundle-grep` — bundle source proves the parameters match
  - `manual-frame-cmp` — agent visually compared ref+impl frames and confirmed match
  - `non-deterministic-ref` — reference itself is non-deterministic (RNG without seed, autoplay video) so AE will always vary
- `evidence` — human-readable explanation, ideally with a file/line pointer or test name.
- `aeThresholdCeiling` — the observed AE value at the time of verification. If the current AE *exceeds this by more than 50%* (`AE > ceiling × 1.5`), the gate **rejects** the entry — implying the bug got worse and the artifact verdict is no longer valid.
- `verifiedAt` — ISO 8601 timestamp.

Entries missing any field, or with an unknown `verifiedBy` value, are **silently ignored by the gate** (and the underlying FAIL stays a FAIL).

### Coverage cap

If `len(sections) + len(transitions)` exceeds **30%** of the total section/transition count in the project, `gate_section_compare` prints a warning:

```
⚠️  known-artifacts.json marks 12/30 (40%) of sections as artifacts.
   This is above the 30% threshold and likely indicates a real bug being
   silenced. Re-verify entries with `verifiedBy: manual-frame-cmp`.
```

The gate still honors valid entries — the warning is advisory. The intent is to make over-marking *visible*, not to block it (sometimes a site really does have lots of canvas noise).

### Adding an entry

1. The relevant FAIL must already exist in `tmp/ref/<c>/sections/result.txt` or `transitions/result.txt`.
2. Gather evidence — the right method depends on what's varying:
   - Canvas / WebGL: write a `readPixels` invariant test
   - Bundle-driven animation: grep the bundle for the parameter, copy the relevant line into `evidence`
   - Capture timing: take 3–5 ref captures, compare AE pairwise to establish noise floor
3. Set `aeThresholdCeiling` to **the observed AE at verification time** (not a guess — read it from `result.txt`).
4. Commit the file. The gate picks it up on the next run.

### Worked example — when to NOT add an entry

A masthead is reporting AE=2200 (`FAIL`). Tempting to mark as artifact ("HeroCanvas timing variance"). But:
- The reference site has the same canvas — capture 3 refs of the **reference**, get the noise floor. If reference-vs-reference AE is ~250, the noise floor is 250. The impl at 2200 is ~9× the floor — that's a real bug, not noise.
- Only when reference-vs-reference AE approaches the impl-vs-ref AE is the "capture artifact" claim defensible.

The gate cannot enforce this discipline — it can only validate the *shape* of the entry. The agent must do the homework before writing one.

---

## How the two artifacts interact at gate time

```
gate_post_implement:
  existing checks (extracted.json, transition-spec.json, static/ref)
  ↓
  read verification-plan.json (if present)
    for each requiredChecks[]:
      check `produces` artifact exists at tmp/ref/<c>/
      if file has `status: "pass"` → PASS
      else → FAIL (with severity respected)
  if plan absent → behave as today

gate_section_compare:
  existing result.txt parsing
  ↓
  count ❌ FAIL lines
  read known-artifacts.json (if present)
    for each entry:
      validate required fields present
      validate aeThresholdCeiling not exceeded
      if valid → downgrade matching ❌ to PASS in gate output
      else → ignore entry (FAIL stays FAIL)
  emit warning if marked > 30% of sections
  result.txt itself is never modified
```

`verification-plan.json` is *prescriptive* (what should be checked), `known-artifacts.json` is *exculpatory* (what failures are not real). They don't overlap.

---

## Backwards compatibility

- Pre-existing projects without either file: gates behave exactly as before.
- New checks listed in `verification-plan.json` but whose `produces` artifact is missing: gate reports as a clear FAIL with the script path needed to produce it. No silent passes.
- Schema bumps: `schemaVersion` is checked; mismatched versions cause a one-line WARN and the file is ignored (preserves forward compatibility while signaling a tooling refresh is due).

---

After this step, return to `SKILL.md` Step 5e (capture verification). The `verification-plan.json` you just wrote is consumed by `gate_spec` (refuses to pass without it) and by `gate_post_implement` (enforces each `requiredChecks[]` entry).
