# INFRA-F41 Step 2 — Full-tree deterministic render verifier — Implementation Plan

> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.

**Goal:** Eliminate the render-verifier's coverage blind spot (currently 62/91 styled selectors uncovered, per `audit:render-coverage-gaps`) by verifying EVERY styled sub-element against Figma — deterministically, with zero heuristic element↔node matching.

**Architecture:** Establish *explicit* Figma↔DOM correspondence via `data-figma-node-id` attributes baked into canonical components. The extract pipeline (already recurses child nodes with `fills`, EXTRACT-007) emits a per-variant expected-node list `{figmaNodeId, fillHex, borderHex, textColorHex, ...}`. A new tree-walk verifier looks up each expected node by `[data-figma-node-id="…"]` and compares all visual props. Rollout is **incremental and FP-safe**: the verifier only checks annotated nodes; unannotated styled elements are *reported as coverage gaps, never failures*, so the false-positive rate stays 0 throughout. `audit:render-coverage-gaps` is the progress tracker (gap count → 0).

**Tech Stack:** Node ESM scripts (`figma-sync/extract.mjs`, `scripts/generate-render-verification-manifest.mjs`), Playwright + `getComputedStyle` (`tests/visual-verify/manifest-verifier.spec.ts`), Vue SFC canonical components (`src/canonical/*.vue`).

**Why not heuristic / why incremental:** 2026-05-20 proved heuristic element↔variant matching = ≥40% false positives (the origin of FIGMA_AS_SOURCE_OF_TRUTH). Correspondence MUST be explicit. Data-driven elements (`v-for` chips) have no fixed per-instance node-id → annotate the *template* with the representative Figma node-id, verify the first rendered instance. Exported-SVG icons (the popup-close ambiguity) are resolved here: the extract records the *resolved* fill hex, which is exactly what renders.

---

## Pre-flight (read before Task 1)

- [ ] Read `figma-sync/extract.mjs` lines ~250–290 (child node extraction: each child already carries `fills`, `subnodeCount`, recursive `children`).
- [ ] Read `scripts/generate-render-verification-manifest.mjs`: `flattenGeometryChildren` (~752), `compositePaintHexForTheme` (~721), `paintHexForTheme` (~716), `resolveSelector` (~667), and where each manifest entry object is assembled (search `rootTargetSelector`).
- [ ] Read `tests/visual-verify/manifest-verifier.spec.ts` lines ~157–360 (how root/text targets are located + compared; `colorToHex`, `pushExact`, `pushNumber`).
- [ ] Run baseline: `pnpm audit:render-coverage-gaps` — record the current gap list (62) as the burn-down target.

---

## Task 1: Extract emits per-child visual props (extend EXTRACT-007)

**Files:**
- Modify: `figma-sync/extract.mjs` (child extraction block ~250–290)
- Verify against: `figma-data/normalized/components-tokenized/select_box_filled__1436_32816.json`

The child recursion already stores `fills`. Add per-child `strokes` (border color), `cornerRadius`, `opacity`, and ensure `id` (Figma node-id) is preserved on every child node so the manifest can key on it. For exported-SVG icon children, record the resolved paint hex (already what `extractPaint` yields).

- [ ] **Step 1: Add a failing assertion** in a new `tests/extract-child-visual-props.test.ts`: re-read the committed `select_box_filled` tokenized JSON, assert a known chip-close child node has `{ id, fills }` AND newly `strokes`/`opacity` fields present (will fail until extract emits them).
- [ ] **Step 2: Run** `pnpm vitest run tests/extract-child-visual-props.test.ts` → expect FAIL (fields missing).
- [ ] **Step 3: Implement** — in the child-node mapper, add `strokes: (child.strokes ?? []).map(extractPaint).filter(Boolean)`, `opacity: child.opacity ?? 1`, `cornerRadius: child.cornerRadius`. Keep `id`.
- [ ] **Step 4:** `pnpm sync:figma-library --with-extract --skip-icons --skip-sot-audit` to re-extract (or a scoped `--component-node-id=` for one set), then re-run the test → PASS.
- [ ] **Step 5: Commit** `feat(extract): per-child visual props (strokes/opacity/radius) for full-tree verify [INFRA-F41]`

## Task 2: Manifest emits an `expectedNodes[]` list per variant

**Files:**
- Modify: `scripts/generate-render-verification-manifest.mjs` (entry assembly + reuse `flattenGeometryChildren`/`compositePaintHexForTheme`)
- Output shape: each manifest entry gains `expectedNodes: [{ figmaNodeId, name, fillHex, borderHex, textColorHex, radius, opacity }]`

- [ ] **Step 1: Failing test** `tests/manifest-expected-nodes.test.ts`: generate manifest in-memory (or read the written file after a gen run), assert a SelectBoxFilled multi-select entry has `expectedNodes` containing the chip-close node id (`1436:36578`-family) with `fillHex` ≈ `#9e9e9e`.
- [ ] **Step 2: Run** → FAIL (`expectedNodes` undefined).
- [ ] **Step 3: Implement** — add `buildExpectedNodes(variantChildren, theme)`: walk children (reuse `flattenGeometryChildren`), for each with paints compute `compositePaintHexForTheme(fills, theme)` / strokes → push `{figmaNodeId: child.id, name: child.name, fillHex, borderHex, ...}`. Attach to each entry.
- [ ] **Step 4:** `pnpm test:render-verification` regenerates the manifest; re-run test → PASS.
- [ ] **Step 5: Commit** `feat(manifest): expectedNodes[] per variant for full-tree verify [INFRA-F41]`

## Task 3: Tree-walk verifier (additive, FP-safe)

**Files:**
- Modify: `tests/visual-verify/manifest-verifier.spec.ts` (add a pass that iterates `entry.expectedNodes`)
- The existing root/text checks stay unchanged.

- [ ] **Step 1:** For each `expectedNode`, `page.locator('[data-figma-node-id="' + id + '"]')`. If `count === 0` → push a `coverage-gap` record (NOT a fail). If found → compare `getComputedStyle` color/backgroundColor/borderColor via existing `colorToHex` against the node's expected hexes using `pushExact`.
- [ ] **Step 2:** Add the gap/compared counts to the report JSON (`figma-data/normalized/render-verification.report.json`) under a new `nodeCoverage` block.
- [ ] **Step 3:** Run `pnpm test:render-verification` → PASS (no annotations yet ⇒ all expectedNodes report as gaps, zero failures; drift A unchanged at 0).
- [ ] **Step 4: Commit** `feat(verifier): additive tree-walk pass over expectedNodes [INFRA-F41]`

## Task 4: Annotate the highest-gap component first (SelectBoxBase) + prove the loop closes

**Files:**
- Modify: `src/canonical/SelectBoxBase.vue` (add `:data-figma-node-id` to chip, chip-close, arrow, clear elements)

- [ ] **Step 1:** Map SelectBoxBase rendered elements to Figma node ids using `get_design_context` on the live multi-select variant (`1436:36574`): chip = `1436:36576`, chip-close = `1436:36578`, arrow = `1436:36582`. For `v-for` chip, bind the template to the representative node-id (document: "first instance verified").
- [ ] **Step 2:** Add `:data-figma-node-id` bindings. Re-run `pnpm test:render-verification` → the chip-close node flips from `coverage-gap` to a real comparison; assert it PASSES (we fixed it to grey-6 already).
- [ ] **Step 3:** Run `pnpm audit:render-coverage-gaps` → SelectBoxBase gap count drops. Confirm the burn-down works.
- [ ] **Step 4: Commit** `feat(select): data-figma-node-id annotations close coverage gaps [INFRA-F41]`

## Task 5 … N: Annotate remaining components by gap-count order

Repeat Task 4's pattern for each component in `audit:render-coverage-gaps` order (InputBoxBase, FormItem, CheckBox, Radio, Notification, PopupBox, …). Each is one commit. **This is the bulk of the effort and is mechanical repetition** — one component per task, drive its gap count to 0, never break FP=0 (unannotated stays a gap). Stop when coverage-gaps reaches an acceptable floor (some decorative/state-variant selectors may stay intentionally uncovered with a documented allowlist).

## Task Final: Wire node-coverage into the gate

**Files:**
- Modify: `scripts/audit-render-drift-gate.mjs`

- [ ] Add: real node comparisons that FAIL (actual≠expected) count toward `A_TRUE_DRIFT_CANDIDATE` (so annotated sub-elements now gate). Coverage *gaps* do NOT gate (they're tracked by `audit:render-coverage-gaps`, not the drift gate). Commit.

---

## Self-review notes
- **Spec coverage:** Tasks 1–3 build the mechanism; 4–N close gaps; Final gates them. The 62-gap baseline is the burn-down metric.
- **v-for / conditional:** template-level annotation + first-instance verify (Task 4 Step 1); conditionally-rendered elements (e.g. close× when `closable`) require the manifest variant to render them.
- **FP safety:** unannotated = gap (reported), never fail — preserved in Task 3 Step 1 and Task Final.
- **Exported-SVG icons:** Task 1 records resolved hex; resolves the popup-close exact-value ambiguity that blocked manual confirmation.
- **Open design check before execution:** confirm `data-figma-node-id` survives the canonical→base wrapping (e.g. SelectBoxFilled wraps SelectBoxBase — annotations live in the base, which is what renders).

## Execution note (context)
This plan is large and mechanical from Task 5 on. Recommended to execute in a **fresh focused session** (subagent-driven: one component-annotation task per subagent, review between). Tasks 1–4 prove the mechanism end-to-end before the repetitive annotation phase.
