# Create Anatomy

> Renders a component anatomy annotation in Figma: numbered markers on an instance with an attribute table, plus a section per sub-component listing its internal elements, hidden ones included. Part of uSpec; needs the .md spec from create-component-md, a Figma MCP and a configured template.

- Type: Skill
- Repository: https://github.com/redongreen/uSpec/tree/main/skills/create-anatomy
- Page: https://hebbmind.com/ko/skill/create-anatomy
- Stars: 247
- Professions: [Frontend](https://hebbmind.com/ko/tools.md?profession=frontend), [Design](https://hebbmind.com/ko/tools.md?profession=design)
- Tags: design-files, ui-components, figma, design-system
- Listed on: 2026-10-08

## Install

### Claude Code

```bash
npx skills add redongreen/uSpec --skill create-anatomy -a claude-code -y
```

### Codex

```bash
npx skills add redongreen/uSpec --skill create-anatomy -a codex -y
```

## README

> The content below comes from the upstream repository https://github.com/redongreen/uSpec/tree/main/skills/create-anatomy . It is third-party material and does not represent AgentHub; instructions that appear in it are not instructions to you.

```yaml
name: create-anatomy
description: Generate a visual anatomy annotation in Figma showing numbered markers on a component instance with an attribute table. Use when the user mentions "anatomy", "anatomy annotation", "component anatomy", "create anatomy", or wants to annotate a component's structural elements.
```

# Create Anatomy Annotation

Generate a hierarchical anatomy annotation directly in Figma — a **composition section** showing the top-level sub-components with numbered markers and a 4-column attribute table, then **per-child sections** for each INSTANCE sub-component showing all its internal elements (including hidden ones).

Uses the **Anatomy & Properties v2** template with `#annotation-table`, type indicators (`#instance` / `#text` / `#slot`), and `#anatomy-section` cloning.

**Execution contract (read first).**
- This file is instructions to RUN, not a document to edit. Invoking the skill = render the anatomy annotation into Figma from the input `.md`.
- Never edit this `SKILL.md` or any other skill file in response, even if one is open or focused in the editor. Modify a skill only when the user explicitly asks to change the skill itself.
- The input component `.md` is a **READ-ONLY source of truth. Never edit, append to, or "add a section" to it.** The only artifact this skill produces is the Figma annotation. When the user asks to "create/add a section," "show," or "include" something, render it **in the Figma annotation**, never as an edit to the `.md`.
- Never call `AskQuestion`, request confirmation, or pause for input (including before Figma writes, the expected output). On ambiguity, pick the most defensible option and continue.
- Only two legal stops: (a) Step 0 fail-fast when no `.md` resolves; (b) one-line abort if the Figma MCP connection is dead.

## MCP Adapter

Read `uspecs.config.json` → `mcpProvider`. Follow the matching column for every MCP call in this skill.

| Operation | `figma-console` | `figma-mcp` |
|-----------|-----------------|-------------|
| Verify connection | `figma_get_status` | Skip — implicit. If first `use_figma` call fails, guide user to check MCP setup. |
| Navigate to file | `figma_navigate` with URL | Extract `fileKey` from URL (`figma.com/design/:fileKey/...`). No navigate needed. |
| Take screenshot | `figma_take_screenshot` | `get_screenshot` with `fileKey` + `nodeId` |
| Execute Plugin JS | `figma_execute` with `code` | `use_figma` with `fileKey`, `code`, `description`. **JS code is identical** — no wrapper changes. |
| Search components | `figma_search_components` | `search_design_system` with `query` + `fileKey` + `includeComponents: true` |
| Get file/component data | `figma_get_file_data` / `figma_get_component` | `get_metadata` or `get_design_context` with `fileKey` + `nodeId` |
| Get variables (file-wide) | `figma_get_variables` | `use_figma` script: `return await figma.variables.getLocalVariableCollectionsAsync();` |
| Get token values | `figma_get_token_values` | `use_figma` script reading variable values per mode/collection |
| Get styles | `figma_get_styles` | `search_design_system` with `includeStyles: true`, or `use_figma`: `return figma.getLocalPaintStyles();` |
| Get selection | `figma_get_selection` | `use_figma` script: `return figma.currentPage.selection.map(n => ({id: n.id, name: n.name, type: n.type}));` |

**`figma-mcp` requires `fileKey` on every call.** Extract it once from the user's Figma URL at the start of the workflow. For branch URLs (`figma.com/design/:fileKey/branch/:branchKey/:fileName`), use `:branchKey` as the fileKey.

**`figma-mcp` page context:** `use_figma` resets `figma.currentPage` to the first page on every call. When a script accesses a node from a previous step via `getNodeByIdAsync(ID)`, the page content may not be loaded — `findAll`, `findOne`, and `characters` will fail with `TypeError` until the page is activated. Insert this page-loading block immediately after `getNodeByIdAsync`:

```javascript
let _p = node; while (_p.parent && _p.parent.type !== 'DOCUMENT') _p = _p.parent;
if (_p.type === 'PAGE') await figma.setCurrentPageAsync(_p);
```

This walks up to the PAGE ancestor and loads its content. Console MCP does not need this — `figma_execute` inherits the Desktop page context.

## Inputs Expected

- **Component `.md` spec** (**required**, user-provided path) — the source-of-truth component spec produced by {{skill:create-component-md}}. **The user tells you where this `.md` lives** — use the exact path they provide; the `.md` may live anywhere. This skill **seeds its element model from the `.md`'s `render-meta` block** and then runs one bounded minimal live walk; it does NOT re-run the old full extraction. The `.md` has **no dedicated Anatomy section** — identity comes from `render-meta` (`component.compSetNodeId`, `variantAxes`, `booleanDefs`, `slotContents`, `subComponents`), and the few facts `render-meta` cannot supply (live direct-child classification, the richest variant, slot default children, wrapper visuals) come from the Step 3' walk.
- **Figma link to the destination** (optional) — placement hint only: which page/frame to drop the rendered annotation on. `fileKey`, `nodeId`, and `compSetNodeId` always come from the `.md`'s `render-meta` block, never from the link.

There is no screenshot-only path and no "extract everything from a Figma URL" path. Without the component `.md` there is no identity to seed — see Step 0's fail-fast contract.

## Workflow

Copy this checklist and update as you progress:

```
Task Progress:
- [ ] Step 0: Require + parse the component `.md` render-meta block (seed identity). FAIL FAST if missing.
- [ ] Step 1: Verify MCP connection
- [ ] Step 2: Read template key from uspecs.config.json
- [ ] Step 3': Seed from render-meta + run the bounded minimal live walk (classify direct children, pick richest variant, read slot defaults, check wrapper visuals)
- [ ] Step 4: Evaluate variant selection, classify elements, and enrich notes (AI reasoning)
- [ ] Step 5: Navigate to destination (if different file)
- [ ] Step 6: Import and detach the Anatomy template
- [ ] Step 7: Fill header fields and create composition section
- [ ] Step 8: Build composition artwork with markers + fill table
- [ ] Step 8b: Per-sub-component child sections (property-aware unhide)
- [ ] Step 10: Visual validation
```

### Step 0: Require and parse the component `.md` (fail fast)

**This skill is a consumer of the `.md` source of truth.** Unlike the other `create-*` skills, the `.md` has **no dedicated "Anatomy" body section** — so create-anatomy seeds *identity only* from the `render-meta` block and supplements it with one bounded live walk (Step 3'). It does NOT parse an Anatomy section body, and it does NOT re-run the legacy full tree-walk extraction.

1. **Resolve the `.md` path.** Use the exact path the user gave, else an attached or open `.md` in context. The `.md` may live anywhere; do NOT invent or guess a path. If neither resolves to an existing file, abort per item 2. Never pause to ask the user which file to use.
2. **Require the file.** If no file exists at the resolved `.md` path, **abort immediately** with this exact single-line diagnostic and stop — do NOT fall back to extraction:

   > This skill requires the component's Markdown `.md` spec (produced by create-component-md). Provide the path to it. (create-component-md needs a _base.json from the uSpec Extract plugin.)

3. **Parse the `render-meta` block** (the fenced JSON between `<!-- render-meta:start v=1 -->` and `<!-- render-meta:end -->`). There is **no Anatomy body section to parse** — every seed value comes from this block:
   - `COMP_SET_ID` = `render-meta.component.compSetNodeId` — the target node for the Step 3' walk, the Step 6 template placement, and every artwork instance in Steps 8/8b.
   - `IS_COMPONENT_SET` = `render-meta.component.isComponentSet`.
   - `COMPONENT_NAME` = `render-meta.component.componentName`.
   - `VARIANT_AXES` = `render-meta.variantAxes` (`{ axisName: [options] }`) and `VARIANT_AXES_DEFAULTS` = `render-meta.variantAxesDefaults` — used by Step 4 sub-step 0 to decide whether a structurally richer variant exists. This **replaces** reading `componentPropertyDefinitions` live for variant axes.
   - `BOOLEAN_DEFS` = `render-meta.booleanDefs[]` (`{ key, default, associatedLayerName, associatedLayerId }`) — the source for boolean-controlled hidden elements. `key` is the raw component-property key `setProperties` expects; `associatedLayerName` lets Step 4 bind a boolean to a Step 3'-walked element by name. This **replaces** parsing booleans + resolving bindings by raw key in a live walk.
   - `SLOT_CONTENTS` = `render-meta.slotContents[]` (`{ slotName, slotNodeType, preferredComponents: [{ componentId, componentName, componentSetId, isComponentSet, ... }] }`) — resolves slot preferred instances **without** the old cross-page `findAll` search.
   - `SUB_COMPONENTS` = `render-meta.subComponents[]` (`{ name, mainComponentName, subCompSetId, subCompVariantAxes, subCompVariantAxesDefaults, booleanOverrides }`) — the constitutive sub-components that become Step 8b child sections; `subCompSetId` is the child node id.
   - `FILE_KEY` = `render-meta.fileKey`, `NODE_ID` = `render-meta.nodeId` — for the Step 11 completion link and template placement.
   - `SOURCE_HASH` = `render-meta.sourceHash` — recorded in the Step 11 provenance footer so drift between this annotation and the underlying `_base.json` is detectable.

**FORBIDDEN — do NOT run the full extraction.** When the component `.md` is present (it always is past Step 0), you MUST NOT run the legacy full-tree extraction. Specifically:
- The old Step 3 extraction script — the one that parsed `componentPropertyDefinitions` for `variantAxes` / `booleanProps` / `instanceSwapProps`, resolved boolean bindings by raw key, and ran a cross-page `findAll` to resolve slot `preferredValues` — is **deleted**. It does not exist in this skill anymore. Do NOT reintroduce it.
- Do NOT re-derive `variantAxes`, `variantAxesDefaults`, `booleanDefs`, slot preferred components, or sub-component identity from a live walk — they are seeded from `render-meta` (Step 0.3).
- The ONLY live read of the component tree permitted before rendering is the **bounded minimal walk in Step 3'** below, whose scope is explicitly whitelisted. The render scripts (Steps 8 / 8b) make their own live reads of the rendered instance — those are unchanged and still allowed.

### Step 1: Verify MCP Connection

Read `mcpProvider` from `uspecs.config.json` to determine which Figma MCP to use.

**If `figma-console`:**
- `figma_get_status` — Confirm Desktop Bridge plugin is active
- If connection fails: *"Please open Figma Desktop and run the Desktop Bridge plugin. Then try again."*

**If `figma-mcp`:**
- Connection is verified implicitly on the first `use_figma` call. No explicit check needed.
- If the first call fails: *"Please verify your FIGMA_API_KEY is set correctly in your MCP configuration."*

### Step 2: Read Template Key

Read the file `uspecs.config.json` and extract:
- The `anatomyOverview` value from the `templateKeys` object → save as `ANATOMY_TEMPLATE_KEY`
- The `fontFamily` value → save as `FONT_FAMILY` (default to `Inter` if not set)

If the template key is empty, tell the user:
> The anatomy template key is not configured. Run {{skill:firstrun}} with your Figma template library link first.

### Step 3': Seed from `render-meta` + run the bounded minimal live walk

The element model is built in two parts: a **`render-meta` seed** (already parsed in Step 0) and a **bounded minimal live walk** that supplies only the facts `render-meta` cannot.

**Seed (from `render-meta`, no Figma reads):**
- `SUB_COMPONENTS` (`render-meta.subComponents[]`) — constitutive sub-components and their `subCompSetId`; drives the Step 8b child sections with no live identity resolution.
- `SLOT_CONTENTS` (`render-meta.slotContents[]`) — each slot's preferred components with their `componentId`; replaces the old cross-page `findAll` preferred-instance search.
- `BOOLEAN_DEFS` (`render-meta.booleanDefs[]`) — boolean properties and their `associatedLayerName`; replaces parsing `componentPropertyDefinitions` and resolving boolean bindings by raw key.

**Bounded minimal live walk (ONE `figma_execute`).** This walk is **NOT** the old full extraction. It visits **only**:
- (a) the **direct children** of the chosen variant's child container — to classify them (`instance` / `instance-unwrapped` / `text` / `slot` / `container` / `structural`) and capture their live layer **names** (so marker name-match in Step 8 works directly), node types, visibility, and seed bboxes;
- (b) the **chosen variant** — picking the richest variant (returned as `selectedVariantId`) when the default variant is structurally thin, mirroring the old richest-variant fallback;
- (c) **slot default children** — the immediate children already sitting in each `SLOT` node of the chosen variant (to know what populates a slot by default);
- (d) **wrapper visuals** — the root variant's fills/strokes/effects (`rootVariantVisuals`), the frames traversed to reach the child container (`traversedFrames`), and `childContainerIsVariant`, so Step 4 can insert synthetic elements for skipped visual layers.

It does **nothing else**: no property-definition parsing, no boolean-binding resolution, no cross-page component search, and no recursion into instance internals (that is Step 8b's job). Boolean defs, variant axes, slot preferred components, and sub-component identity all come from the `render-meta` seed — not from this walk.

**Wrapper traversal sync warning:** The single-child auto-layout / SLOT / background-rect traversal logic appears in three places — the Step 3' walk below, the Step 8 composition artwork script, and the Step 8b per-child artwork script — and must stay in sync.

Run this walk via `figma_execute`, replacing `__COMP_SET_NODE_ID__` with `render-meta.component.compSetNodeId` (= `COMP_SET_ID`) and `__PREFERRED_VARIANT_PROPS__` with `null` for the initial walk (or a variant property object like `{ "variant": "count-forward" }` when re-walking after Step 4 sub-step 0 identifies a richer variant from `render-meta.variantAxes`):

```javascript
const TARGET_NODE_ID = '__COMP_SET_NODE_ID__';
const PREFERRED_VARIANT_PROPS = __PREFERRED_VARIANT_PROPS__;
const STRUCTURAL_TYPES = ['RECTANGLE', 'VECTOR', 'ELLIPSE', 'LINE', 'POLYGON', 'STAR', 'BOOLEAN_OPERATION'];

function hasVisuals(n) {
  const f = n.fills && n.fills.length > 0 && n.fills.some(f => f.visible !== false);
  const s = n.strokes && n.strokes.length > 0 && n.strokes.some(s => s.visible !== false);
  const e = n.effects && n.effects.length > 0 && n.effects.some(e => e.visible !== false);
  return { hasFills: !!f, hasStrokes: !!s, hasEffects: !!e, hasAny: !!f || !!s || !!e };
}

async function resolveInstanceInfo(instNode) {
  try {
    const mc = await instNode.getMainComponentAsync();
    if (!mc) return null;
    const isSet = mc.parent && mc.parent.type === 'COMPONENT_SET';
    const cs = isSet ? mc.parent : null;
    const info = {
      mainComponentId: mc.id,
      mainComponentSetId: cs ? cs.id : null,
      childIsComponentSet: !!cs,
      componentSetName: cs ? cs.name : mc.name,
      childVariantCount: cs ? cs.children.length : 1,
      childVariantAxes: []
    };
    if (cs) {
      const csPropDefs = cs.componentPropertyDefinitions || {};
      for (const [ck, cd] of Object.entries(csPropDefs)) {
        if (cd.type === 'VARIANT') {
          info.childVariantAxes.push({ name: ck.split('#')[0], options: cd.variantOptions || [], defaultValue: cd.defaultValue });
        }
      }
    }
    return info;
  } catch { return null; }
}

async function walkToInnerInstance(node, depth) {
  if (depth > 8) return null;
  if (node.type === 'INSTANCE') return node;
  if ((node.type === 'FRAME' || node.type === 'GROUP') && 'children' in node && node.children.length === 1) {
    return walkToInnerInstance(node.children[0], depth + 1);
  }
  return null;
}

async function extractElement(node, index, artworkAbsX, artworkAbsY) {
  const absX = node.absoluteTransform[0][2];
  const absY = node.absoluteTransform[1][2];
  const element = {
    index,
    name: node.name,
    originalName: null,
    nodeType: node.type,
    classification: null,
    visible: node.visible,
    bbox: {
      x: Math.round(absX - artworkAbsX),
      y: Math.round(absY - artworkAbsY),
      w: Math.round(node.width),
      h: Math.round(node.height)
    },
    notes: '',
    controlledByBoolean: null,
    wrappedInstance: null,
    mainComponentId: null,
    mainComponentSetId: null,
    childIsComponentSet: false,
    childVariantAxes: [],
    childVariantCount: 1,
    shouldCreateSection: false
  };

  if (node.type === 'INSTANCE') {
    const info = await resolveInstanceInfo(node);
    if (info) {
      Object.assign(element, info);
      element.notes = info.componentSetName + ' instance';
    }
    element.classification = 'instance';
  } else if (node.type === 'TEXT') {
    element.classification = 'text';
    const content = node.characters || '';
    if (content.length > 0 && content.length <= 30) {
      element.notes = 'Text element \u2014 "' + content + '"';
    } else {
      element.notes = 'Text element';
    }
  } else if (node.type === 'FRAME' || node.type === 'GROUP') {
    const innerInst = await walkToInnerInstance(node, 0);
    if (innerInst && innerInst !== node) {
      const info = await resolveInstanceInfo(innerInst);
      if (info) {
        element.wrappedInstance = info;
        element.originalName = element.name;
        element.nodeType = 'INSTANCE';
        element.classification = 'instance-unwrapped';
        Object.assign(element, {
          mainComponentId: info.mainComponentId,
          mainComponentSetId: info.mainComponentSetId,
          childIsComponentSet: info.childIsComponentSet,
          childVariantAxes: info.childVariantAxes,
          childVariantCount: info.childVariantCount
        });
        element.notes = element.name + ' instance';
      } else {
        element.classification = 'container';
        const childCount = ('children' in node) ? node.children.length : 0;
        element.notes = 'Container with ' + childCount + ' children';
      }
    } else if ('children' in node && node.children.length === 1 && node.children[0].type === 'TEXT') {
      const textChild = node.children[0];
      element.originalName = element.name;
      element.nodeType = 'TEXT';
      element.classification = 'text';
      const content = textChild.characters || '';
      if (content.length > 0 && content.length <= 30) {
        element.notes = 'Text element \u2014 "' + content + '"';
      } else {
        element.notes = 'Text element';
      }
    } else {
      const childCount = ('children' in node) ? node.children.length : 0;
      element.classification = childCount > 0 ? 'container' : 'structural';
      element.notes = childCount > 0 ? 'Container with ' + childCount + ' children' : 'Empty container';
    }
  } else if (node.type === 'SLOT') {
    element.classification = 'slot';
    const childCount = ('children' in node) ? node.children.length : 0;
    element.notes = 'Composable slot with ' + childCount + ' children';
  } else if (STRUCTURAL_TYPES.includes(node.type)) {
    element.classification = 'structural';
    element.notes = node.type;
  } else {
    element.classification = 'structural';
    element.notes = node.type;
  }

  return element;
}

const node = await figma.getNodeByIdAsync(TARGET_NODE_ID);
if (!node || (node.type !== 'COMPONENT_SET' && node.type !== 'COMPONENT')) {
  return { error: 'Node is not a component set or component. Type: ' + (node ? node.type : 'null') };
}

const isComponentSet = node.type === 'COMPONENT_SET';

function resolveChildContainer(v) {
  let cc = v;
  while (cc.children.length === 1 && cc.children[0].type === 'FRAME' && cc.children[0].layoutMode !== 'NONE') {
    cc = cc.children[0];
  }
  if (cc.children.length === 1 && cc.children[0].type === 'SLOT') {
    cc = cc.children[0];
  }
  if (cc === v && cc.children.length > 1) {
    const autoLayoutFrames = cc.children.filter(c => c.type === 'FRAME' && c.layoutMode !== 'NONE' && ('children' in c) && c.children.length >= 2);
    const structuralOnly = cc.children.filter(c => STRUCTURAL_TYPES.includes(c.type));
    if (autoLayoutFrames.length === 1 && structuralOnly.length === cc.children.length - 1) {
      cc = autoLayoutFrames[0];
    }
  }
  return cc;
}

let variant;
if (PREFERRED_VARIANT_PROPS && isComponentSet) {
  variant = node.children.find(v => {
    const props = v.variantProperties || {};
    return Object.entries(PREFERRED_VARIANT_PROPS).every(
      ([k, val]) => props[k] === val
    );
  }) || node.defaultVariant || node.children[0];
} else {
  variant = isComponentSet ? (node.defaultVariant || node.children[0]) : node;
}
let childContainer = resolveChildContainer(variant);

if (isComponentSet && childContainer.children.length === 0 && node.children.length > 1) {
  function countDescendants(n) {
    let c = 0;
    if ('children' in n) { for (const ch of n.children) { c += 1 + countDescendants(ch); } }
    return c;
  }
  let bestVariant = variant;
  let bestCount = 0;
  for (const v of node.children) {
    const cnt = countDescendants(v);
    if (cnt > bestCount) { bestCount = cnt; bestVariant = v; }
  }
  if (bestVariant !== variant) {
    variant = bestVariant;
    childContainer = resolveChildContainer(variant);
  }
}

const varAbsX = variant.absoluteTransform[0][2];
const varAbsY = variant.absoluteTransform[1][2];

const rootVis = hasVisuals(variant);
const rootVariantVisuals = {
  hasFills: rootVis.hasFills, hasStrokes: rootVis.hasStrokes, hasEffects: rootVis.hasEffects,
  cornerRadius: variant.cornerRadius || 0
};

const traversedFrames = [];
let walker = variant;
while (walker !== childContainer) {
  if ('children' in walker && walker.children.length === 1) {
    const child = walker.children[0];
    const vis = hasVisuals(child);
    const cAbsX = child.absoluteTransform[0][2];
    const cAbsY = child.absoluteTransform[1][2];
    traversedFrames.push({
      name: child.name, nodeType: child.type,
      hasFills: vis.hasFills, hasStrokes: vis.hasStrokes, hasEffects: vis.hasEffects,
      cornerRadius: child.cornerRadius || 0,
      bbox: { x: Math.round(cAbsX - varAbsX), y: Math.round(cAbsY - varAbsY), w: Math.round(child.width), h: Math.round(child.height) }
    });
    walker = child;
  } else break;
}

const absX = varAbsX;
const absY = varAbsY;

const elements = [];
let idx = 1;
for (const child of childContainer.children) {
  elements.push(await extractElement(child, idx++, absX, absY));
}

for (const el of elements) {
  if (el.classification === 'instance' || el.classification === 'instance-unwrapped') {
    el.shouldCreateSection = true;
    const UTILITY_NAMES = ['spacer', 'divider', 'separator', 'divider line', 'gap', 'padding', 'filler'];
    if (UTILITY_NAMES.includes(el.name.toLowerCase())) {
      el.shouldCreateSection = false;
    }
  }
}

// --- Slot default children only (scope (c)) ---
// Boolean defs, slot preferred components, variant axes, and sub-component identity are
// SEEDED from render-meta (Step 0.3). This walk does NOT parse componentPropertyDefinitions
// and does NOT run a cross-page findAll for preferred values. For each direct-child SLOT it
// reads only the slot's existing default children plus the slot's own visibility binding;
// Step 4 cross-references render-meta.booleanDefs / render-meta.slotContents by name.
for (const el of elements) {
  if (el.classification !== 'slot') continue;
  const slotNode = childContainer.children[el.index - 1];
  if (!slotNode) continue;

  const cpRefs = slotNode.componentPropertyReferences || {};
  if (cpRefs.visible) {
    el.slotBooleanBinding = { propName: cpRefs.visible.split('#')[0], rawKey: cpRefs.visible };
  }

  if ('children' in slotNode && slotNode.children.length > 0) {
    el.slotDefaultChildren = [];
    for (const sc of slotNode.children) {
      const scInfo = { name: sc.name, nodeType: sc.type, visible: sc.visible };
      if (sc.type === 'INSTANCE') {
        try {
          const mc = await sc.getMainComponentAsync();
          if (mc) {
            scInfo.mainComponentId = mc.id;
            scInfo.mainComponentKey = mc.key;
            const isSet = mc.parent && mc.parent.type === 'COMPONENT_SET';
            scInfo.componentSetName = isSet ? mc.parent.name : mc.name;
            scInfo.componentSetId = isSet ? mc.parent.id : null;
            scInfo.isComponentSet = isSet;
          }
        } catch {}
      }
      el.slotDefaultChildren.push(scInfo);
    }
  }
}

return {
  componentName: node.name,
  variantName: variant.name,
  selectedVariantId: variant.id,
  compSetNodeId: TARGET_NODE_ID,
  isComponentSet,
  rootSize: { w: Math.round(variant.width), h: Math.round(variant.height) },
  elements,
  rootVariantVisuals,
  traversedFrames,
  childContainerIsVariant: childContainer === variant
};
```

Save the returned JSON — you will use `componentName`, `compSetNodeId`, `selectedVariantId`, `isComponentSet`, `rootSize`, `elements`, `rootVariantVisuals`, `traversedFrames`, and `childContainerIsVariant` in subsequent steps. The walk no longer returns `booleanProps`, `variantAxes`, or `instanceSwapProps` — those come from the `render-meta` seed (`BOOLEAN_DEFS`, `VARIANT_AXES`, etc.) parsed in Step 0. `selectedVariantId` is the variant the walk actually used — it may differ from the default variant if `PREFERRED_VARIANT_PROPS` was set (agent-directed re-walk from Step 4 sub-step 0) or if the default produced 0 elements and the walk fell back to the richest variant.

Each element carries pre-resolved fields:
- `classification` — closed enum: `instance`, `instance-unwrapped`, `text`, `slot`, `container`, `structural`
- `name` — the live, designer-facing layer name (this is the marker name-match key used in Step 8). For `instance-unwrapped`, this is the wrapper frame's name (e.g., "Thumb"), not the inner component's name. For `text` classification from a FRAME-wrapped TEXT node, this is the FRAME name (e.g., "Label"). The inner component name is available in `wrappedInstance.componentSetName`.
- `controlledByBoolean` — `{ propName, rawKey, defaultValue }` or `null`. **Not produced by the walk** — Step 4 sets it by matching `render-meta.booleanDefs[].associatedLayerName` (or `key`) to the element's live `name`/`originalName`.
- `wrappedInstance` — component info for the inner INSTANCE (only on `instance-unwrapped` elements): includes `componentSetName` (the inner component's name used by Step 8b for child section creation)
- `originalName` — the FRAME name before unwrapping (on `instance-unwrapped` and FRAME-wrapped `text` elements)
- `shouldCreateSection` — `true` for `instance`/`instance-unwrapped`, `false` for utility names and other types
- `slotBooleanBinding` — `{ propName, rawKey }` or absent. Present on `slot` elements when the slot's `componentPropertyReferences.visible` points to a boolean property.
- `slotDefaultChildren[]` — array of `{ name, nodeType, visible, mainComponentId?, mainComponentKey?, componentSetName?, componentSetId?, isComponentSet? }` or absent. Present on `slot` elements that contain children in the chosen variant. Used to identify what content populates the slot by default.

**Slot preferred components are NOT walked.** Where the old extraction ran a cross-page `findAll` to build `slotPreferredInstances[]`, you now read `render-meta.slotContents[]` (the `SLOT_CONTENTS` seed). In Step 4, match each `slot` element's `name` to `SLOT_CONTENTS[].slotName` and set `slotPreferredComponentId` / `populateWith` from `preferredComponents[].componentId`. Sub-component identity for child sections likewise comes from `render-meta.subComponents[].subCompSetId`, not from a live walk.

Additional walk-level fields:
- `rootVariantVisuals` — `{ hasFills, hasStrokes, hasEffects, cornerRadius }` for the root variant frame. When `hasFills` or `hasEffects` is true, the variant has visual properties. Step 4 should fold these into the root container's note when a container synthetic is already being created, or insert a standalone synthetic backplate/statelayer element when no container synthetic covers the root variant. `hasStrokes` is included for informational purposes (to enrich the root container note with border details) but does NOT trigger a separate synthetic element — strokes on the root variant are a border property of the container frame.
- `traversedFrames[]` — Frames the `resolveChildContainer` traversal walked through to reach the child container. Each entry has `{ name, nodeType, hasFills, hasStrokes, hasEffects, cornerRadius, bbox }`. Frames with fills, strokes, or effects are visually meaningful and should be inserted as synthetic elements by Step 4.
- `childContainerIsVariant` — `true` when `resolveChildContainer` resolved to the variant itself (no wrapper traversal). `false` when it traversed into a child frame, meaning the root component container was skipped by the walk. When `false`, Step 4 should insert a synthetic element for the root container regardless of whether it has visual properties.

The `fullTree` field has been removed. Classification and instance-wrapper detection are handled by the Step 3' walk; boolean bindings and slot preferred components come from the `render-meta` seed (resolved in Step 4 by name).

### Step 4: Validate Seed + Walk and Enrich Notes (AI Reasoning)

This is a pure reasoning step — no `figma_execute` calls unless a re-walk is needed (sub-step 0). The Step 3' minimal walk has already performed classification and instance-wrapper unwrapping; the `render-meta` seed supplies variant axes, boolean defs, slot preferred components, and sub-component identity. Step 4 focuses on **variant evaluation**, **validation**, **seed↔element binding**, and **semantic note-writing**.

Read the instruction file `{{ref:anatomy/agent-anatomy-instruction.md}}`, then enrich the seeded + walked data in-memory before proceeding to rendering.

**Process:**

0. **Evaluate variant selection**: If `isComponentSet` is true and `elements.length` is small (1–2 elements), review `render-meta.variantAxes` option names for a structurally richer variant. If a clearly richer variant exists (option names suggest additional sub-components, e.g., "count-forward" adds a count badge to "forward"), re-run the Step 3' minimal walk with `PREFERRED_VARIANT_PROPS` set to the target variant's property values and replace all Step 3' walk output. Do NOT re-walk for purely stylistic differences (color, size, theme). See `{{ref:anatomy/agent-anatomy-instruction.md}}` sub-step 0 for the full evaluation rules.

1. **Read** `{{ref:anatomy/agent-anatomy-instruction.md}}` for note-writing guidelines, validation checklist, and unhide strategy rules.

2. **Validate** the classified walk data per the instruction file's validation checklist.

2b. **Detect skipped visual layers and root container**: Check `childContainerIsVariant`, `rootVariantVisuals`, and `traversedFrames` from the Step 3' walk output. When `childContainerIsVariant` is `false`, always insert a synthetic element for the root container. When `childContainerIsVariant` is `true`, evaluate whether the root container is architecturally meaningful (e.g., hosts composable slots, manages conditional visibility) and insert a synthetic element if so — skip it when the container is a self-evident stack of same-type sub-components. Root variant fills/effects (NOT strokes — strokes are described in the container note) are folded into the container's note when a container synthetic already exists, or inserted as standalone synthetic elements when no container synthetic covers the root variant. Also insert synthetic elements for traversed frames with fills/strokes/effects. See `{{ref:anatomy/agent-anatomy-instruction.md}}` sub-step 1b for the full procedure and examples.

2c. **Bind `render-meta.booleanDefs` to walked elements**: For each entry in `BOOLEAN_DEFS` (`render-meta.booleanDefs[]`), match its `associatedLayerName` (fall back to `key.split('#')[0]`) against each element's live `name`/`originalName`. On a match, set `el.controlledByBoolean = { propName: key.split('#')[0], rawKey: key, defaultValue: default }`. This replaces the boolean-binding loop the old extraction ran inside the walk.

3. **Set unhide strategy** for hidden elements per the instruction file's Property-Aware Unhide Decisions section. Boolean-gated elements use the `controlledByBoolean` set in sub-step 2c; their `unhideStrategy.method === 'boolean'` carries `booleanRawKey = controlledByBoolean.rawKey` (= the `render-meta.booleanDefs[].key`).

3b. **Detect inline markers**: For each element, determine whether it should use an inline marker (marker sits directly on the element's nearest edge with a short stub line) or a perimeter marker (standard marker outside the artwork). Elements nested inside another annotated element (e.g., slot default children that are also annotated) get `inlineMarker: true`. See `{{ref:anatomy/agent-anatomy-instruction.md}}` for the detection rules.

3c. **Resolve + enrich slot preferred instances from the seed**: For each `slot` element, match its live `name` to `SLOT_CONTENTS[].slotName` (`render-meta.slotContents[]`). When a match has `preferredComponents`, set `slotPreferredComponentId` to the first preferred component's `componentId` and enrich notes to mention the preferred component name(s). If the slot has `slotDefaultChildren` (from the walk), mention the default content. For empty/hidden slots with a preferred component, mark the slot for artwork population (set `populateSlot: true` and `populateWith` to `{ componentId }` from the seed). This replaces the old `slotPreferredInstances` cross-page lookup. See `{{ref:anatomy/agent-anatomy-instruction.md}}` for the enrichment rules.

4. **Rewrite** the `notes` field for each element following the instruction file's note-writing guidelines.

4b. **Integrate user-provided design context** into notes per the instruction file's guidelines.

4c. **Compose brief description**: Write a 1-sentence `briefDescription` (max ~15 words) that describes what this component IS and does — not what the spec shows. Incorporate user-provided context when available. See `{{ref:anatomy/agent-anatomy-instruction.md}}` for guidelines and examples. Save this string for Step 7.

5. **Deduplicate repeated composition elements**: When multiple consecutive elements share the same `mainComponentSetId`, collapse them into a single representative element with a `count` field. The first element is kept; subsequent duplicates are removed from the array. The representative element's name gets an `(xN)` suffix in the table. Write notes for deduplicated elements per the instruction file's "Repeated composition elements" guidelines.

6. **Validate** using the instruction file's checklist.

The enriched `elements` array (with updated `notes`, `unhideStrategy`, `count`, `isSynthetic`, `inlineMarker`, `populateSlot`, `populateWith`, `shouldCreateSection`, and `slotPreferredComponentId` fields, plus any synthetic elements inserted for skipped visual layers) and the `briefDescription` string are used by all subsequent rendering steps.

### Step 5: Navigate to Destination

If the user provided a separate destination file URL (placement hint only — never a source of structural facts):
- `figma_navigate` — Switch to the destination file

If no destination was provided, stay in the current file and place the annotation beside the component identified by `render-meta.component.compSetNodeId`.

### Step 6: Import and Detach Template

**If the user provided a cross-file destination URL** (navigated in Step 5), run via `figma_execute`:

```javascript
const ANATOMY_TEMPLATE_KEY = '__ANATOMY_TEMPLATE_KEY__';

const templateComponent = await figma.importComponentByKeyAsync(ANATOMY_TEMPLATE_KEY);
const instance = templateComponent.createInstance();
const { x, y } = figma.viewport.center;
instance.x = x - instance.width / 2;
instance.y = y - instance.height / 2;
const frame = instance.detachInstance();
frame.name = '__COMPONENT_NAME__ Anatomy';
figma.currentPage.selection = [frame];
figma.viewport.scrollAndZoomIntoView([frame]);
return { frameId: frame.id };
```

**If no destination was provided (default)**, run via `figma_execute` — this places the spec on the component's page, to its right:

```javascript
const ANATOMY_TEMPLATE_KEY = '__ANATOMY_TEMPLATE_KEY__';
const COMP_NODE_ID = '__COMPONENT_NODE_ID__';

const compNode = await figma.getNodeByIdAsync(COMP_NODE_ID);
let _p = compNode;
while (_p.parent && _p.parent.type !== 'DOCUMENT') _p = _p.parent;
if (_p.type === 'PAGE') await figma.setCurrentPageAsync(_p);

const templateComponent = await figma.importComponentByKeyAsync(ANATOMY_TEMPLATE_KEY);
const instance = templateComponent.createInstance();
const frame = instance.detachInstance();

const GAP = 200;
frame.x = compNode.x + compNode.width + GAP;
frame.y = compNode.y;

frame.name = '__COMPONENT_NAME__ Anatomy';
figma.currentPage.selection = [frame];
figma.viewport.scrollAndZoomIntoView([frame]);
return { frameId: frame.id, pageId: _p.id, pageName: _p.name };
```

Replace `__COMPONENT_NAME__` with `render-meta.component.componentName` (= `COMPONENT_NAME`). Replace `__COMPONENT_NODE_ID__` with `render-meta.component.compSetNodeId` (= `COMP_SET_ID`, the same node the Step 3' walk targeted).

Save the returned `frameId` — you need it for all subsequent steps.

### Step 7: Fill Header Fields and Create Composition Section

This step fills the top-level header and creates a dedicated anatomy section by **cloning** `#anatomy-section`. The clone is renamed so it is not affected by other skills' cleanup. After cloning, the original `#anatomy-section` is **hidden** to prevent its placeholder text from appearing in screenshots. The property skill re-shows it if it needs additional clones.

Run via `figma_execute` (replace `__FRAME_ID__`, `__COMPONENT_NAME__`, `__BRIEF_DESCRIPTION__`). Replace `__BRIEF_DESCRIPTION__` with the `briefDescription` composed during Step 4 sub-step 4c:

```javascript
const frame = await figma.getNodeByIdAsync('__FRAME_ID__');
const textNodes = frame.findAll(n => n.type === 'TEXT');
const fontSet = new Set();
const fontsToLoad = [];
for (const tn of textNodes) {
  try {
    const fn = tn.fontName;
    if (fn && fn !== figma.mixed && fn.family) {
      const key = fn.family + '|' + fn.style;
      if (!fontSet.has(key)) { fontSet.add(key); fontsToLoad.push(fn); }
    }
  } catch {}
}
await Promise.all(fontsToLoad.map(f => figma.loadFontAsync(f).catch(() => {})));

const compNameFrame = frame.findOne(n => n.name === '#comp-name-anatomy');
if (compNameFrame) {
  const t = compNameFrame.findOne(n => n.type === 'TEXT');
  if (t) t.characters = '__COMPONENT_NAME__';
}

const descFrame = frame.findOne(n => n.name === '#brief-component-description');
if (descFrame) {
  const t = descFrame.findOne(n => n.type === 'TEXT');
  if (t) t.characters = '__BRIEF_DESCRIPTION__';
}

const anatomySectionTemplate = frame.findOne(n => n.name === '#anatomy-section');
const compositionSection = anatomySectionTemplate.clone();
anatomySectionTemplate.parent.appendChild(compositionSection);
compositionSection.name = 'Component structure';
compositionSection.visible = true;

const sectionFrame = compositionSection.findOne(n => n.name === '#section-name');
if (sectionFrame) {
  const t = sectionFrame.findOne(n => n.type === 'TEXT');
  if (t) t.characters = 'Component structure';
}

const sectionDescFrame = compositionSection.findOne(n => n.name === '#optional-section-description');
if (sectionDescFrame) {
  const t = sectionDescFrame.findOne(n => n.type === 'TEXT');
```

> The README is long; the above is its first 40000 characters. Full content: https://github.com/redongreen/uSpec/tree/main/skills/create-anatomy
