# T1c Extract Recurse Children Plan

> Round 1 plan only. No code, no Figma API calls, no data regeneration.
> Goal: let extract capture child-node visual data with role and axis attribution.

## 0. Source Mechanism Design

### 0.1 `figma-extract-rules.md`

The upgrade must start with `src/design-system/translation/figma-extract-rules.md`, not script branches.
Round 1 creates `figma-extract-rules.draft.md`; Round 2 should consume it only after plan owner review.

Component-level fields:

| Field | Type | Notes |
|---|---|---|
| `figmaComponent` | string | Published Figma component set name. |
| `figmaComponentSetIds` | string[] | Covered component set IDs. |
| `extractionMode` | `top-level-only` \| `recursive-with-rules` | Missing section defaults to `top-level-only`. |
| `maxTraversalDepth` | number | Component traversal cap. |
| `childRoles` | array | Role rules. |

Child-role fields:

| Field | Type | Notes |
|---|---|---|
| `roleName` | enum | Closed enum: `track`, `fill-bar`, `handle`, `icon`, `label`, `thumb`, `spinner`, `arrow`, `divider`, `background`, `container`, `unknown-visual`. |
| `nodeNameMatch` | regex or literal | Match local name and full node path. |
| `nodeTypeMatch` | string[] | Optional Figma node type filter. |
| `depthLimit` | number | Per-role cap. |
| `captureKinds` | string[] | `fills`, `strokes`, `text`, `dimensions`, `effects`, `vectorNodeIds`. |
| `axisAttribution` | enum | `inherit-from-variant`, `pattern-match`, or `manual`. |
| `manualAxisMap` | object | Required only for `manual`. |
| `codeRoleHint` | string | Downstream selector/concept hint, not validation truth. |
| `evidenceSource` | string[] | Rule and verification source. |
| `reviewStatus` | enum | `draft`, `needs-full-raw-cache-confirmation`, `approved`. |

Design decisions:

- `roleName` starts as a closed enum so audit/generator/Code Connect can depend on stable role names.
- New roles are added by editing one source file, satisfying future one-place extension.
- `nodeNameMatch` supports regex and literal strings. Regex is necessary because Figma names often vary by case and punctuation.
- Current `figma-data/raw/components/*.json` files are reduced extract outputs and do not include `children`; draft regexes need full raw-cache confirmation.
- `inherit-from-variant` means child evidence receives the parent variant tuple.
- `pattern-match` means child name contains axis info such as `track--disabled`.
- `manual` requires explicit axis mapping in this source file.
- Nested nodes stop at `min(maxTraversalDepth, depthLimit)`.
- `INSTANCE` internals are traversed only when a role explicitly allows `INSTANCE`.

### 0.2 Extract Output Schema

Keep existing top-level fields and add `extractedChildren`:

```json
{
  "name": "theme=dark, size=M",
  "fills": [],
  "strokes": [],
  "text": null,
  "extractedChildren": [
    {
      "roleName": "track",
      "nodePath": "Slider/theme=dark, size=M/Track",
      "nodeId": "4934:7210",
      "nodeName": "Track",
      "nodeType": "RECTANGLE",
      "depth": 2,
      "axisTuple": { "theme": "dark", "size": "M" },
      "axisAttribution": "inherit-from-variant",
      "fills": [{ "type": "SOLID", "hex": "#262626", "v": "VariableID:..." }],
      "strokes": [],
      "text": null,
      "dimensions": { "h": 8, "w": 196, "r": 9999 },
      "evidenceLevel": "rule-matched",
      "evidenceSource": ["figma-extract-rules.md/Slider/track"]
    }
  ]
}
```

Evidence levels:

- `rule-matched`: matched reviewed rule, can feed audit.
- `unmatched-but-captured`: visible child paint found without rule, discovery only.
- `placeholder-only`: opacity-0/invisible placeholder, ignored by audit.

### 0.3 Downstream Consumption

Normalize should enrich both top-level paint and `variant.extractedChildren[]`.
Audit should collect refs from:

1. `variant.fills`, `variant.strokes`, `variant.text.fills`
2. `variant.extractedChildren` with `evidenceLevel === "rule-matched"`

Then:

- ignore `placeholder-only`
- list `unmatched-but-captured` in discovery output only
- emit `figma-data-missing` only if top-level refs and rule-matched child refs are empty

The current missing trigger is `audit-component-token-fidelity.mjs:1537-1546`.
Child role to code selector/property mapping belongs in `axis-implementation-map.md`, not in audit code.
Recommended axis map extension:

```markdown
- **figmaChildRole**: `track`
- **codeRoleSelector**: `.progress__track`
- **codeRoleProperty**: `background-color`
```

### 0.4 Round 1 Outputs

- `docs/internal/_plans/t1c-extract-recurse-plan.md`
- `src/design-system/translation/figma-extract-rules.draft.md`

Draft instances cover Slider, Rating, Progress, Notification, and Switch.
Because current reduced raw files cannot provide real child names, each instance is marked `needs-full-raw-cache-confirmation`.
Future Tooltip arrow support should only add a new `Tooltip / arrow` rule; extract, normalize, audit, and Code Connect should consume it mechanically.

## 1. Problem Diagnosis

Blind spot 1: top-level-only paint extraction.
`extractNodeFrame(node)` at `figma-sync/extract.mjs:134-162` reads only the node passed to it.
`buildComponentEntry()` maps component-set children through that function at lines `180-187`, so child track/star/fill/icon paint is lost.

Blind spot 2: recursive vector ID collection has no visual data.
`collectVectorNodeIds()` at `figma-sync/extract.mjs:104-111` records IDs only.
`rating__4935_7294.json` has five vector IDs but no star fills.

Blind spot 3: no child axis attribution.
Flattening child paints without `axisTuple` would not tell audit whether a fill belongs to `status=warning`, `theme=light`, or child-local state.

Blind spot 4: no full raw response cache.
`extractComponents()` calls `getFile()` at line `244` and immediately reduces data.
The committed raw component files no longer contain `children`, so offline rule iteration cannot verify node names.

Blind spot 5: data growth control.
Current `figma-data/raw/components` is about 25 MB; unbounded recursion could grow committed data sharply.
Rules must be the size control.

## 2. Affected Components And Source Data Check

Audit JSON shows 171 `figma-data-missing` findings across 9 components:

| Component | Missing | Kinds |
|---|---:|---|
| Checkbox | 4 | icon-data |
| FormItem | 64 | icon-data |
| Input | 6 | fill-data |
| Notification | 14 | icon-data |
| Progress | 32 | track-data, fill-data |
| Rating | 20 | paint-data, icon-data |
| Slider | 8 | fill-data, track-data |
| StepItem | 9 | icon-data |
| Switch | 14 | paint-data |

The prompt expected 8 components; current JSON has 9. The long-term plan groups Checkbox + Input as 7 findings, which explains the mismatch.

Top-level placeholder scan over current reduced raw component files confirms:

| Component/file | Placeholder variant | vectorNodeIds |
|---|---|---:|
| Slider / `slider__4934_7206.json` | `theme=dark, size=M` | 0 |
| Rating / `rating__4935_7294.json` | `theme=dark, value=1` | 5 |
| Progress / `progress__4915_7287.json` | `theme=dark, status=default, size=M` | 0 |

Working table:

| Component | Missing | Top-level status | Child evidence currently available | Expected childRoles |
|---|---:|---|---|---|
| Slider | 8 | opacity-0 placeholder | no children; code has track/fill/thumb | track, fill-bar, handle, label |
| Rating | 20 | opacity-0 placeholder | 5 vector IDs; code has star icons | icon |
| Progress | 32 | opacity-0 placeholder | no children; code has track/fill/label | track, fill-bar, label |
| Notification | 14 | container paint present | 5 vector IDs; code has status/close icons | icon |
| Switch | 14 | track fill present | no children; code has thumb/spinner | thumb, spinner |
| FormItem | 64 | insufficient icon child data | not in draft instances | icon, label |
| StepItem | 9 | insufficient icon child data | not in draft instances | icon, label |
| Checkbox | 4 | insufficient icon child data | not in draft instances | icon |
| Input | 6 | insufficient child fill data | not in draft instances | background, icon |

Real child node names cannot be read from current reduced raw files. Round 2 must cache the full Figma API response and confirm at least one child node per draft rule before approval.

## 3. `extract.mjs` Algorithm Upgrade

Load rules once:

```js
const rules = loadFigmaExtractRules('src/design-system/translation/figma-extract-rules.md')
```

Contract:

```js
extractWithChildren(node, rulesForComponent, variantTuple)
```

DFS outline:

```text
frame = extractNodeFrame(variantNode)
frame.extractedChildren = []
if rules.extractionMode !== recursive-with-rules: return frame

walk(child, depth, path):
  stop if depth > maxTraversalDepth or child.visible === false
  find role rules where type, depth, and name/path match
  if matched:
    childFrame = extractNodeFrame(child)
    keep only captureKinds
    attach roleName, nodePath, depth, axisTuple, evidenceSource
    mark rule-matched or placeholder-only
  else if has visible paint:
    capture unmatched-but-captured discovery record
  recurse into children while within cap
```

Role matching order:

1. depth limit
2. node type filter
3. literal match
4. regex match against local name and full path

`unmatched-but-captured` should be reported for rule authoring, not counted as audit truth.

## 4. Raw Figma Response Cache

Path:

```text
figma-data/raw-cache/figma-file-response.json
```

Git behavior:

- Add `figma-data/raw-cache/` to `.gitignore`.
- Do not commit the cache.

Strategy:

- First `--with-extract` writes cache after `getFile()`.
- Later extract runs read cache by default.
- `--refresh-cache` forces API.
- If Figma file metadata includes version/lastModified, store it with cache and invalidate on mismatch.

This lets rule/schema iterations run offline.
It also fixes the Round 1 limitation where current raw components no longer contain child names.

## 5. Normalize Layer Contract

Current normalize:

- reads raw component files at `normalize-component-tokens.mjs:218-224`
- enriches `fills`, `strokes`, and `text.fills` in `enrichVariant()` at lines `175-212`

Upgrade:

```json
{
  "extractedChildren": [
    {
      "roleName": "track",
      "nodePath": ".../Track",
      "axisTuple": { "theme": "dark" },
      "fills": [{ "hex": "#262626", "token": { "cssVar": "--bg-layer3" } }],
      "dimensions": { "h": 8, "w": 196 },
      "evidenceLevel": "rule-matched"
    }
  ]
}
```

Implementation:

- map `variant.extractedChildren ?? []`
- reuse existing `enrichPaint()`
- preserve role/path/axis/evidence fields
- do not flatten children into top-level `fills`

## 6. Audit Upgrade Points

Update `collectFigmaRefs(variant)` to collect:

- top-level paint refs as today
- child refs from `extractedChildren` with `rule-matched`
- fields: `kind`, `hex`, `cssVar`, `path`, `roleName`, `nodePath`, `axisTuple`, `evidenceSource`

Update missing logic at `audit-component-token-fidelity.mjs:1540-1559`:

- child refs satisfy expected paint kinds
- empty top-level refs no longer means missing if child refs exist
- `unmatched-but-captured` goes to a discovery section

New verdict recommendation:

- `⚠️ child-role-unmapped` when Figma child role exists but `axis-implementation-map.md` lacks code role mapping.
- Evidence level should be `heuristic` until both Figma role and code role are explicitly mapped.

Extend `axis-implementation-map.md` with optional `figmaChildRole`, `codeRoleSelector`, and `codeRoleProperty`.

## 7. Figma Sync Flow And Safety

Round 2 sequence:

1. Finalize `figma-extract-rules.md`.
2. Add raw cache and child extraction to `extract.mjs`.
3. Update normalize and audit.
4. Run `pnpm sync:figma-library --with-extract` in safe mode.
5. Review cleanup dry-run; do not delete without explicit user authorization.
6. Review `figma-data/` diff.
7. Rerun audit.

Data rules:

- Ignore `figma-data/raw-cache/`.
- Keep children inline under parent variants, not as separate component files.
- Exclude raw-cache from cleanup scans.
- Target committed raw component growth under 30%.

## 8. Boundary Cases

| Case | Strategy |
|---|---|
| Nested `INSTANCE` | Traverse only when role allows `INSTANCE`; otherwise capture metadata and stop. |
| `BOOLEAN_OPERATION` | Capture final fills/strokes/dimensions; do not assume individual vector meaning. |
| `visible: false` | Skip audit evidence; optional discovery count. |
| Emoji/special chars | Match original and normalized names; store original path. |
| Variant structures differ | Emit role-missing-for-variant, do not crash. |
| IMAGE fills | Preserve `imageRef`; classify unsupported unless mapped. |
| GRADIENT fills | Preserve stops; future audit support. |
| Missing axis tuple values | Inherit known tuple and record attribution note. |
| Same role appears N times | Use `occurrenceIndex`; Rating stars are expected. |
| Regex overmatch | Require node type and depth filters. |

## 9. Round 2 Sanity Targets

Baseline:

- Total findings: 36081
- `figma-data-missing`: 171
- Direct evidence: 7411
- Heuristic evidence: 28557
- Semantic inference: 113

Five draft components:

| Component | Current missing | Expected after rules |
|---|---:|---:|
| Slider | 8 | 0-2 |
| Rating | 20 | 0-4 |
| Progress | 32 | 0-8 |
| Notification | 14 | 0-4 |
| Switch | 14 | 0-4 |

Expected total missing after only these five instances: 87-105.
If FormItem, StepItem, Checkbox, and Input also get rules, target can drop below 30.
Total findings may rise because child refs become auditable, so use 36050-36250 as the initial review band.
Direct evidence should increase; heuristic missing should decrease.
Committed raw component data should grow less than 30%.

## 10. Engineering Breakdown And Risks

| sub-task | Estimate | Risk | Fallback |
|---|---:|---|---|
| Finalize rules schema + 5 instances | 0.5h | Low | Keep draft unconsumed. |
| Add raw response cache | 0.75h | Low | Use API path when cache missing. |
| Add `.gitignore` entry | 0.15h | Low | Use temp cache during development. |
| Parse rules markdown | 0.75h | Medium | Require strict fenced JSON blocks. |
| Add `extractChildrenByRules()` DFS | 1.5h | Medium | Start with direct children only. |
| Add placeholder classification | 0.5h | Low | Keep old top-level behavior when uncertain. |
| Normalize child tokens | 1h | Medium | Preserve raw child fields first. |
| Extend audit child refs | 1.5h | Medium | Feature flag old/new comparison. |
| Extend axis map child roles | 1h | Medium | Emit `child-role-unmapped`. |
| Safe sync + diff review | 0.75h | Low | Use cache if API unavailable. |
| Audit sanity comparison | 0.5h | Low | Stop if missing increases. |

Total estimate: about 8 hours.
High-risk tasks: 0.
Medium-risk tasks: 5.

Main risk: real Figma child names are not present in current committed raw component files.
Raw-cache confirmation must happen before the draft regexes become approved rules.
