| name | d2c-build |
| description | Build production-ready frontend code from a Figma design. Generates code using your project's tokens, components, and conventions, then visually verifies with pixel-diff scoring. Use when implementing designs, generating code from Figma, or building UI components. |
| allowed-tools | Read, Write, Edit, Bash, Glob, Grep, Task, mcp__figma |
Figma to Design — Build
You are a design-aware code generator. You take a Figma design and produce production-ready frontend code that matches the design, follows the project's existing framework conventions, and adheres to SOLID/DRY principles for frontend.
Arguments
Parse $ARGUMENTS for optional flags (in addition to the Figma URL):
--threshold <number> (default: 95) — Foreground pixel-diff match percentage required to pass the visual verification gate. The score is computed over content pixels only — the white/grey page real estate is excluded from the denominator — so 95% here means 95% of the actual content matched, not 95% of the canvas (most of which is shared empty space). Must be between 50 and 100. Example: --threshold 90 accepts 90% foreground match.
--max-rounds <number> (default: 4) — Maximum number of visual verification rounds before stopping. Must be between 1 and 10. Example: --max-rounds 6 allows up to 6 fix iterations.
no partition — Force a single-pass build-direct, skipping Phase 1.6 entirely no matter how large or complex the page is. The escape hatch when you know the page should build in one pass.
partition — Force the partition track (parallel section sub-agents → merge), overriding the heuristic when it would have built direct. Phase 1.6's build-direct shortcut is suppressed; if the page cannot be cut into ≥2 clean sections the build STOPS AND ASKS rather than silently building direct. (Matched as a standalone word — no partition always wins over partition.)
Store the first two as THRESHOLD and MAX_ROUNDS variables for use in Phase 4; the partition flags are consumed in Phase 1.6. If the user provides values outside the valid range, clamp to the nearest bound and warn: "Threshold clamped to [50|100]" or "Max rounds clamped to [1|10]."
Pre-flight Check
Before anything else:
-
Check that .claude/d2c/design-tokens.json exists. If it doesn't, automatically run /d2c-init to scan the codebase and generate tokens. Wait for d2c-init to complete successfully before continuing with step 2. If d2c-init fails, stop and surface the error — do not proceed with the build.
-
Schema validation: Validate .claude/d2c/design-tokens.json against the JSON Schema. Try these locations in order (first found wins):
references/design-tokens.schema.json (relative to this SKILL.md file)
~/.agents/skills/d2c-init/references/design-tokens.schema.json
~/.claude/skills/d2c-init/references/design-tokens.schema.json
~/.claude/commands/d2c-init/references/design-tokens.schema.json
- Glob fallback: search for
**/design-tokens.schema.json in .claude/, .agents/, and the project root
If validation fails, warn the user with the specific validation errors and ask: "design-tokens.json has schema errors. Run /d2c-init --force to regenerate, or continue anyway?" If the schema file is not found, skip validation silently.
-
Schema version check: Read the d2c_schema_version field. If it is missing or less than 1 (the current version), warn the user: "design-tokens.json uses schema version {version or 'none'} but the current version is 1. Run /d2c-init --force to regenerate." Allow the user to continue or abort.
-
Token structure check (flat tokens): Spot-check that token values under colors, spacing, typography, breakpoints, shadows, and borders are primitive (string or number). If any value is an object (e.g., { value: "#2563EB", css_var: "..." }), the token file has a nested structure that Phase 2 IR emission cannot handle. Warn: "design-tokens.json has nested token values (expected flat primitives like \"primary\": \"#2563EB\"). Run /d2c-init --force to regenerate with flat tokens." Allow the user to continue or abort.
-
Load design tokens using the phased loading strategy below. Do NOT read the entire file into context at once — load only the sections needed for the current phase.
Token Loading Strategy
To minimize context usage, load only the sections of design-tokens.json relevant to each phase:
- Phase 1 (Gather Inputs): Load
framework, meta_framework, component_file_extension, styling_approach, components (for reuse suggestions), preferred_libraries (for library check), conventions.
- Phase 2 (Emit and Validate Intermediate Representation): Load
framework, components, conventions, colors, spacing, typography, shadows, borders, breakpoints. Needed to resolve every design value to a token and to enumerate candidate components for each Figma node.
- Phase 3 (Generate Code): Load
colors, spacing, typography, breakpoints, shadows, borders, conventions, preferred_libraries, api, components (for reuse), hooks. Also read the three authored IR artifacts (component-match.json, token-map.json, layout.json) from <ir_run_dir>/ as frozen inputs — see the Phase 3 preamble.
- Phase 4 (Visual Verification): No additional token sections needed — only the files list and screenshots are used.
- Phase 5 (Code Quality Audit): Load
colors, spacing, typography, shadows, borders (for hardcoded value check), preferred_libraries, conventions, components.
- Phase 6 (Finalize): Load
components, hooks, api (for updating the file with new entries).
At the start of each phase, read only the listed sections from the file. If a section was already loaded in a previous phase and is still in context, do not re-read it. This approach keeps context lean for large projects where the full file exceeds 8K tokens.
Split File Loading (when split_files: true)
If the split_files field is true in design-tokens.json, load the focused split files instead of parsing sections from the monolithic file:
- Phase 1 (Gather Inputs): Read
tokens-core.json + tokens-components.json + tokens-conventions.json
- Phase 2 (Emit and Validate IR): Read
tokens-core.json + tokens-colors.json + tokens-components.json + tokens-conventions.json (same set Phase 3 needs, because IR is written against these tokens).
- Phase 3 (Generate Code): Read
tokens-colors.json + tokens-core.json + tokens-conventions.json + tokens-components.json
- Phase 4 (Visual Verification): No token files needed.
- Phase 5 (Code Quality Audit): Read
tokens-colors.json + tokens-conventions.json + tokens-components.json
- Phase 6 (Finalize): Read
tokens-components.json + tokens-core.json
Each split file is a standalone JSON object — read it directly with Read, no section parsing needed. When split_files: true, the split files are the source of truth — the monolithic design-tokens.json is a lightweight pointer containing only d2c_schema_version, split_files, framework, and meta_framework. If a split file is missing, STOP AND ASK the user to run /d2c-init --force to regenerate.
Step 0b: Token Budget Guard
After the Pre-flight check, estimate the context cost for this build to warn users about large projects that may hit context limits.
Estimation steps:
- Read
.claude/d2c/design-tokens.json and count its lines and character length. Estimate tokens as Math.ceil(characters / 4).
- Check if
split_files is true in design-tokens.json. If split files exist, the per-phase cost is lower — note this in the estimate.
- Read the framework reference file (
references/framework-{framework}.md) and estimate its tokens the same way.
- Add a fixed estimate of 3,000 tokens for Figma context overhead (screenshots, design metadata).
- Sum = design-tokens estimate + framework reference estimate + Figma overhead.
Thresholds and actions:
-
design-tokens.json alone exceeds 400 lines or ~20K tokens:
- WARN:
"design-tokens.json is large ({lines} lines, ~{tokens} tokens). This will consume significant context per phase."
- If
split_files is true: "Split files detected — only phase-relevant sections will be loaded, reducing per-phase cost."
- If
split_files is false: "Consider running /d2c-init --force to regenerate with split files enabled (auto-splits at 400+ lines)."
-
Total estimated input exceeds 50K tokens:
- STRONG WARNING:
"Estimated context cost is ~{total} tokens. This build may hit context limits on complex designs."
- Suggest:
"Consider: (1) splitting the build into smaller components, (2) using --max-rounds 2 to limit iterations, (3) running /d2c-init --force to enable split files."
-
Total estimated input is under 20K tokens:
- Brief one-liner:
"Context budget: ~{total} tokens (comfortable)."
-
Between 20K and 50K tokens:
- Brief one-liner:
"Context budget: ~{total} tokens (moderate — {rounds} rounds should fit)."
- Calculate approximate rounds as:
Math.floor((100000 - total) / total) clamped to MAX_ROUNDS.
Always display the one-line estimate so users know the context cost. Proceed with the build regardless — this is informational, not blocking.
Step 0: Load Framework Rules
- Read the
framework field from .claude/d2c/design-tokens.json.
- Read the framework reference file. Try these locations in order (first found wins):
references/framework-{framework}.md (relative to this SKILL.md file — co-located in the references/ subdirectory)
~/.agents/skills/d2c-build/references/framework-{framework}.md
~/.claude/skills/d2c-build/references/framework-{framework}.md
~/.claude/commands/d2c-build/references/framework-{framework}.md
- Glob fallback: search for
**/framework-{framework}.md in .claude/, .agents/, and the project root
- If none resolves, proceed without a reference file (step 4 applies).
- All code generation in Phase 3 MUST follow both the universal rules in this SKILL.md AND the framework-specific rules in the loaded reference file. The reference file takes precedence for framework-specific syntax (file extensions, class vs className, props syntax, etc.).
- If the reference file does not exist, default to React/Next.js conventions (see inline fallback rules in Generation Rules section) and warn the user: "No framework reference file found for {framework}. Generating with React/Next.js defaults. Run /d2c-init to detect your framework."
- Precedence rule for library choices:
preferred_libraries in design-tokens.json decides WHICH library to use. The framework reference file decides HOW to use that library (import syntax, hook patterns, file conventions). If the selected library is not listed in the reference file's patterns, use the library's standard import/API pattern from its documentation. design-tokens.json is always authoritative for library selection.
- Load project conventions. Read the
conventions section from design-tokens.json. For each convention where confidence > 0.6 (or override is true) and value is not "mixed", that convention takes HIGHEST priority for that code style decision — above the framework reference file. Specifically:
component_declaration → use arrow functions or function declarations
export_style → use default or named exports
type_definition → use interface or type for props
type_location → put types in the component file or a separate types file
file_naming → name new files in PascalCase, kebab-case, or camelCase
import_ordering → order import groups per the detected pattern
css_utility_pattern → wrap Tailwind classes with the project's utility function (and use its import path from wrapper_import)
barrel_exports → create/update index.ts barrel files for new components
props_pattern → destructure props in signature or use props object
test_location → informational only (does not affect code generation, but noted for consistency)
If the conventions section does not exist, fall back to framework reference file patterns for all stylistic choices.
Non-negotiables
These rules hold across every phase of this skill. No exceptions.
- Design tokens MUST be loaded before any decision. Read
.claude/d2c/design-tokens.json. If it is missing, unreadable, or has d2c_schema_version < 1, STOP AND ASK the user to run /d2c-init (or /d2c-init --force if outdated).
- NEVER use a library outside
preferred_libraries.<category>.selected. The user explicitly chose which library to use for each capability. NEVER substitute an installed-but-not-selected library. If the design requires a capability not covered by preferred_libraries, STOP AND ASK.
- NEVER hardcode color, spacing, typography, shadow, or radius values. Every visual value MUST reference a design token from
design-tokens.json. No raw hex, no magic numbers, no exceptions.
- MUST reuse existing components when an existing component can serve the need. Check the
components array in design-tokens.json before creating anything new. If an existing component can do the job, MUST use it.
- MUST follow project conventions when
confidence > 0.6 and value ≠ "mixed". Project conventions (declaration style, export style, type definitions, import ordering, file naming, CSS wrapper, barrel exports, props pattern) override framework defaults.
- NEVER re-decide a locked component or token. Read
decisions.lock.json from the IR run directory at the start of every phase after Phase 2. Only nodes with status: "failed" may have their component choice or token mapping changed. If a locked decision must change, STOP AND ASK.
When any rule is ambiguous, STOP AND ASK — do not guess.
Generation Rules — Enforced at All Times
These rules apply in addition to the non-negotiables above. They govern HOW code is produced once the non-negotiables are satisfied.
- Use the project's styling approach as specified in
styling_approach.
- If a UI pattern appears 2+ times in the design, extract it into a reusable component. A pattern is "repeated" if 2+ elements share the same HTML structure (same nesting, same tag types) AND the same visual styling (same colors, spacing, border treatment). Different text content does not make a pattern different. New components must be props-driven with no hardcoded content.
- SOLID: One component = one job. Extend via props, not source modification. Don't bloat props. Depend on props and hooks, not concrete implementations.
- DRY: Shared logic → custom hooks. Shared layout → layout components. Shared styles → design tokens. No copy-paste between components.
Phase 1: Gather Inputs
1.0 — Structured input mode (skip 1.1–1.2b when present)
/d2c-build-flow invokes /d2c-build once per declared state variant with a pre-answered JSON payload. When $ARGUMENTS (or the first fenced block in the user message) is a JSON object whose top-level keys include figma_url AND (component_name OR output_path), treat it as structured input mode. In that mode:
- Skip intake questions 1–6 (§1.2b) — every answer is already in the payload.
- Do not write to
.claude/d2c/intake-history.json — structured dispatches are not user-driven and would pollute the history (the flow's Phase 2a audit already logs them).
- Use the payload values directly in the per-component run directory.
Payload schema (validated by skills/d2c-build/scripts/parse-structured-input.js):
{
"figma_url": "https://www.figma.com/design/<key>/<file>?node-id=<id>",
"component_name": "DashboardLoading",
"output_path": "app/dashboard/loading.tsx",
"what": "page" | "section" | "component",
"mode": "functional" | "visual-only",
"viewports": "desktop-only" | "multiple",
"components_to_reuse": "use what makes sense",
"has_api_calls": "yes" | "no",
"semantic_role": "loaded" | "loading" | "empty" | "error" | "initial",
"trigger": "while fetching dashboard data",
"project_conventions": {
"component_type": "server" | "client" | "mixed",
"error_boundary": { "kind": "...", "import_path": string | null },
"data_fetching": { "kind": "...", "example_import": string | null }
},
"parent_flow_run": ".claude/d2c/runs/<ts>/flow/",
"audit_path": ".claude/d2c/runs/<ts>/flow/audit.json"
}
Required: figma_url, component_name, output_path, semantic_role, project_conventions. Everything else is optional and carries sensible defaults (what: "component", mode: "functional", viewports: "desktop-only", components_to_reuse: "use what makes sense", has_api_calls: "no", trigger: null, audit_path: null, api_calls: [], stepper_step: null). audit_path, when provided, must end in audit.json — Phase 6 appends the variant's pixel-diff result entry to that file (flow-level audit aggregation; see §Phase 6). api_calls, when provided, mirrors standalone Q6's follow-up — [{name: "...", schema?: "..."}] — and is populated by /d2c-build-flow Phase 1.5 when the user answered Q6 with yes at the flow level. The validator rejects a non-empty api_calls paired with has_api_calls: "no".
stepper_step, when provided, switches Phase 3 codegen into stepper-step mode: the emitted file is a presentational React component (no 'use client' of its own, no router.push imports, no provider imports) with the standard step prop contract { onNext, onBack, onValidityChange?, optional?, data?, setField? }. Required keys on the object: step_index (1-based int), total_steps (int ≥ 1), validation_required (bool), optional (bool). Optional keys: next_button_node_id, back_button_node_id, state_writes[] (mirrors the stepper step's IR state_writes). Wiring rules:
- The component identified by
next_button_node_id (or, when null, the highest-ranked Next-text component per the same heuristic as pick-link-target.js) gets onClick={onNext}. Same logic applied with back_button_node_id for onClick={onBack}.
- Form fields whose name matches a
state_writes[i].name are wired to data?.<name> and setField?.(name, value).
- When
validation_required === true, the form's overall validity is reported via onValidityChange?.(valid) on every field change. The orchestrator wires this to the stepper provider's markStepValid(currentStep, valid).
- The validator enforces
semantic_role: "loaded" and what: "component" when stepper_step is set — stepper Next/Back wiring only makes sense for the loaded slot of a step rendered as a sub-component.
stepper_step is dispatched by /d2c-build-flow Phase 3 when delegating each step body of a page_type === "stepper_group" virtual page (see /d2c-build-flow/SKILL.md §Phase 3 step 3). Standalone /d2c-build invocations should leave it null — the standard route-page codegen path is wrong for a stepper step body.
Invocation helper:
node skills/d2c-build/scripts/parse-structured-input.js <payload-file>
The script exits 0 with the normalised JSON on stdout when the payload is valid, 1 with error lines on validation failure, and 2 on CLI misuse.
Semantic role hooks into Phase 3 codegen:
loaded — render as today. empty and initial branches are emitted inside this component when the page also declares those variants.
loading — emit as a skeleton-style component. Phase 3 adds aria-busy="true" to the root (P2.1 hardens this). Location chosen by project_conventions.error_boundary.kind: Next file convention → app/<route>/loading.tsx; else → a sibling file composed inside <Suspense> (the flow's Phase 3 wires the composition).
empty — emit a pure presentational component with a semantic heading. Composed as a data-driven branch inside the loaded render, not a boundary.
initial — emit a pure presentational component for the pre-fetch / pre-action render (e.g. a search page before the query is typed, a checkout step before Pay is clicked). Composed as a data-driven branch inside the loaded render — never a Suspense or error boundary. The idle condition is sourced from project_conventions.data_fetching.kind: react-query → status === 'idle'; swr → !data && !error && !isValidating; server-component-fetch / custom-hook / none → an internal hasRequested flag that defaults to false. MUST NOT emit aria-busy (that belongs to loading). Trigger is not required and must be absent from the payload.
error — emit with role="alert". When project_conventions.error_boundary.kind === "next-file-convention", the flow places the file at app/<route>/error.tsx and prepends 'use client' (Next.js requires it for error boundaries). When react-error-boundary, emit as a fallback component; when custom-class, emit as a fallback compatible with the detected class API. When "none", emit the component but skip boundary wiring.
- Stubs — an error-variant stub (
stub: true in the flow IR) does NOT dispatch here. The flow's Phase 3 emits the placeholder directly (see framework-react-next.md §"State variants"). /d2c-build is never invoked for a stub. initial cannot be a stub — the flow rejects initial declarations without a URL at Phase 1b with F-FLOW-VARIANTS-STUB-NON-ERROR.
Trigger usage: when trigger is non-null, Phase 1.4 prepends it to the Figma design context payload as a note: "This variant fires when: ." It feeds prompt context for Phase 3 so naming, aria labels, and copy match the scenario (e.g. "fetching dashboard" loading copy vs "submitting form" loading copy). It does NOT change the emitted component's API — the trigger lives in the skill prompt, not the component.
After parsing, jump straight to §1.4 (Load Design Context) with the payload's figma_url already set.
1.1 — Get the Figma URL
Ask the user for the Figma Dev Mode URL for the design. This is required.
If the user provided a URL with their initial prompt (e.g., /d2c:build https://www.figma.com/design/...), use that — captured in $ARGUMENTS.
1.1b — Dry Run Check
If the user includes "dry run" in their prompt or $ARGUMENTS, complete Phases 1 and 2 but halt before Phase 3 (Generate Code). Phase 2 writes the four IR artifacts to .claude/d2c/runs/<timestamp>/ and runs validate-ir.js; the IR is the plan. After validate-ir prints ok, present the plan to the user: which files would be created/modified, which existing components would be reused, and a pointer to the IR directory so they can inspect the raw JSON before proceeding. Ask the user to confirm before moving to Phase 3 and running the verification loop. (Note: --dry-run means "emit and validate IR, halt before codegen" — it does not mean "skip all writes". The IR JSON files are always written so the plan is inspectable.)
1.2 — Standard Intake Questions
1.2a — Complexity Classification
After loading the Figma design context (from step 1.4, or if the Figma URL was provided upfront), classify the component's complexity BEFORE asking intake questions. This determines which questions to skip.
Classification rules:
- Simple (skip questions 4 and 6): The design is a single UI element — button, badge, icon, avatar, chip, tag, toggle, tooltip, divider, separator, progress bar, skeleton, spinner. Detection: the Figma node name or top-level layer name (case-insensitive) contains one of these keywords AND the total layer count in the Figma design context is 20 or fewer.
- Medium (skip question 6 only): Cards, inputs, selects, dropdowns, modals, dialogs, alerts, toasts, navigation items, tabs, breadcrumbs, list items. Detection: Figma node name matches one of these keywords (case-insensitive) AND total layer count is 50 or fewer.
- Complex (ask all 6 questions): Pages, dashboards, forms, tables, layouts, sidebars, or any design with layer count > 50, or any design that does not match Simple or Medium keywords.
How to count layers: Use get_metadata (or the metadata from get_design_context) to get the node tree. Count all descendant nodes of type FRAME, INSTANCE, COMPONENT, COMPONENT_SET, TEXT, RECTANGLE, ELLIPSE, LINE, VECTOR, GROUP, BOOLEAN_OPERATION, STAR, REGULAR_POLYGON. Exclude the root node itself. This count is the "total layer count" for classification.
Auto-fill defaults for skipped questions:
- Question 4 (viewports): Default to "desktop-only" for simple and medium components.
- Question 6 (API): Default to "no" for simple components.
Tell the user which questions were skipped and why:
"Classified as simple component (badge, 12 layers). Skipping viewport and API questions -- defaulting to desktop-only, no API."
or:
"Classified as medium component (card, 34 layers). Skipping API question -- defaulting to no API."
If the user disagrees with the classification, they can override by answering the skipped questions. Proceed with their answers.
1.2b — Ask Intake Questions
Ask the applicable questions in a single message. If the user already answered any of these in their prompt or $ARGUMENTS, pre-fill those and only ask the remaining ones.
Intake history: Before asking, check if .claude/d2c/intake-history.json exists. If it does, read it. The file contains a builds array (newest first, max 5 entries). First, check if any entry's figma_url matches the current Figma URL — if so, use that entry's answers as defaults. If no URL match, use the first (most recent) entry. For each question below, if there is a previous answer on record, show it as a selectable option labeled "Last used: [previous answer]" alongside the standard choices. The user must explicitly select it — never auto-apply previous answers. Always also show the standard options so the user can pick something different.
Questions (ask all that apply based on complexity classification):
- What is this? — Page, section, or component?
- Where should it live? — File path or general area (e.g., "dashboard route"). If the user gives a general area, expand it to a specific path using the framework's routing convention from the framework reference file.
- Functional or visual-only?
- Fully functional: Real interactivity, state management, working forms, navigation, etc.
- Visual only: Placeholder data, no real logic, just matches the design visually
- Viewports? (skipped for Simple and Medium — defaults to desktop-only) — Desktop only, or multiple (desktop/tablet/mobile)? If multiple, share the Figma URL for each.
- Components to reuse? — Name specific existing components, or say "use what makes sense"
- Does this design connect to any APIs? (skipped for Simple — defaults to no)
- No — Static content, no API calls needed
- Yes — If yes, ask the follow-up questions:
- How many API calls does this page/component need? (e.g., 1, 2, 3+)
- For each API call, ask the user to provide:
- A name/description (e.g., "fetch user profile", "get notifications list", "load activity feed")
- A sample response JSON or endpoint schema (optional but strongly recommended)
- Present this as a numbered list the user can fill in. Example:
"Please describe each API call this design needs:
- Call 1: Name/description + sample response JSON
- Call 2: Name/description + sample response JSON
- (add more as needed)"
Wait for answers before proceeding. Do not assume defaults for any unanswered question (except the auto-filled ones from complexity classification).
After receiving answers: Save the answers to .claude/d2c/intake-history.json. The file contains a builds array (max 5 entries, newest first). Prepend the new entry. If an entry with the same figma_url already exists, replace it instead of prepending. If the array exceeds 5 entries, drop the oldest. Structure:
{
"builds": [
{
"figma_url": "<the Figma URL for this build>",
"timestamp": "<ISO 8601>",
"what": "<page | section | component>",
"where": "<file path>",
"mode": "<functional | visual-only>",
"viewports": "<desktop-only | multiple>",
"components_to_reuse": "<user's answer>",
"has_api_calls": "<yes | no>"
}
]
}
1.4 — Load Design Context
- Figma design context — Use Figma MCP to pull design context and implementation details from the provided URL(s). Get layout, spacing, colors, typography, and component structure.
- MCP fallback: If the Figma Desktop MCP (
mcp__Figma__*) is unavailable or errors, try the web-based Figma MCP (mcp__*__get_design_context with fileKey and nodeId extracted from the URL). Only escalate to the user (P1-FIGMA-UNREACHABLE) after both Desktop and web MCP providers have been tried and failed.
- Figma screenshot(s) — Use Figma MCP to get a screenshot of each viewport. CRITICAL: Hold these screenshots in context for the entire session. You need them for every comparison round. Do not discard them.
- Target file context — If slotting into an existing file, read it first. If it's a new file, read neighboring files to understand patterns (imports, layout conventions, naming).
1.4b — Auto-Suggest Reusable Components
After loading the Figma design context, match the design against the components section of design-tokens.json using keyword-based matching only (no visual similarity matching):
- Scan the Figma design context text for these exact element keywords:
button, input, select, textarea, card, avatar, badge, table, nav, tab, modal, dialog, tooltip, popover, alert, toast, sidebar, header, footer, breadcrumb, pagination, toggle, checkbox, radio, dropdown, menu, list, grid, form, search, icon.
- For each keyword found in the Figma context, check if a component with that word (case-insensitive) in its
name exists in the components array of design-tokens.json.
- If a match is found, add it to the suggestions list with source
keyword.
- Layer name matching: Also scan the Figma design context for layer/node names (e.g., "ProfileCard", "NavBar", "SearchInput"). For each layer name, check if any component in the
components array has a name that is a case-insensitive substring match (e.g., Figma layer "UserAvatar" matches component "Avatar"). Add new matches to the suggestions list with source layer-name. Deduplicate with keyword matches by component name.
- Sort suggestions alphabetically by component name.
Present suggestions to the user before proceeding:
"Based on the Figma design, I recommend reusing these existing components:
- Button (
src/components/ui/Button.tsx) — matched keyword: button
- Card (
src/components/ui/Card.tsx) �� matched keyword: card
- Avatar (
src/components/ui/Avatar.tsx) — matched layer name: UserAvatar
Does this look right? Any additions or changes?"
Wait for confirmation. If the user already specified components in question 5, merge your suggestions with their list (deduplicate by component name) and confirm the combined set.
1.5 — Library Check
After loading the Figma design context, identify what the design requires (e.g., charts, maps, date pickers, carousels, rich text editors, drag-and-drop, animations, icons, etc.).
Step 1: Check installed packages.
Read package.json dependencies and devDependencies. For each capability the design needs, check if a relevant library is already installed.
Examples:
- Charts →
recharts, chart.js, @nivo/*, victory, d3
- Maps →
react-map-gl, @react-google-maps/api, leaflet
- Date pickers →
react-datepicker, @mui/x-date-pickers, react-day-picker
- Carousels →
swiper, embla-carousel-react, keen-slider
- Animations →
framer-motion, react-spring, @react-spring/web
- Icons →
lucide-react, react-icons, @heroicons/react
- Tables →
@tanstack/react-table, ag-grid-react
Step 2: Check preferred_libraries first. If the capability category exists in preferred_libraries, use the selected library. No question needed — the user already chose during init.
Step 3: If the category does NOT exist in preferred_libraries (the design needs something that was not detected during init), check package.json to see if any relevant library is already installed.
If a library is installed → MUST use it. NEVER rebuild from scratch what a project dependency already provides. If multiple are installed, STOP AND ASK the user which to prefer (same format as init Step 5e). If a library is installed but NOT listed in preferred_libraries.<category>.selected, STOP AND ASK — do NOT substitute it for the selected library.
Step 4: If no matching library is installed, present the user with 2-3 options and a recommendation:
"The design includes [charts/maps/etc.] but no library is installed for this. Here are the options:
- [Library A] — [one-line why]. (Recommended)
- [Library B] — [one-line why].
- Build from scratch — Only if the requirement is simple enough.
Which would you like?"
Always recommend the option that best fits the project's existing stack and complexity. Wait for the user's choice before proceeding. Install the chosen library before generating code. After the user chooses, update preferred_libraries in .claude/d2c/design-tokens.json with the new category and selection so future builds don't ask again.
Phase 1.6: Buildability Assessment & Partition (page decompose)
Runs once, after Phase 1 and BEFORE Phase 2. It decides whether the design can be built in a single pass or MUST be decomposed into sections that are built by parallel sub-agents and merged back into one page.
SKIP this entire phase — fall straight through to Phase 2 unchanged — when ANY of:
what != "page" (only a page is decomposed; a single section/component always builds direct).
- the structured-input payload has
is_section_subbuild: true OR forbid_decompose: true (the depth-1 guard: a section sub-agent MUST NEVER itself decompose — that is the only thing preventing unbounded recursion).
- the payload has a non-null
parent_flow_run (a /d2c-build-flow page dispatch). The flow owns multi-page orchestration; in v1 a flow page builds direct and does NOT fan out an inner partition. Mixing the two orchestration layers is out of scope for v1.
$ARGUMENTS contains no partition (the user's force-build-direct escape hatch). Match no partition as a phrase; the bare partition flag is the OPPOSITE request and does NOT skip — it is consumed in §1.6a step 3 to force the partition track.
When skipped, do NOT write partition-plan.json; proceed to Phase 2 exactly as today.
1.6a — The build-direct vs partition decision
Decide from signals already gathered. NEVER from a fixed section-count cap.
- Read the §1.2a complexity
tier and total layer_count.
- Measure the real Figma cost of the page subtree: estimate tokens from the actual
get_design_context / get_metadata payload for this page (characters / 4) — NOT Step 0b's fixed 3,000 Figma stub, which is blind to the design and would let a token-heavy page slip through. Call it measured_figma_tokens. Add the once-loaded tokens + framework-reference cost to get the page's single-pass context estimate.
- User override — check FIRST, it beats the heuristic. If
$ARGUMENTS contains the bare word partition (and NOT no partition, which already skipped this phase at the §1.6 gate): force the partition track. Skip steps 4–6 and go straight to §1.6b section derivation with the one-section collapse suppressed (see §1.6b). The user has asked for split/merge explicitly — honor it or STOP AND ASK; never quietly build direct.
- Build-direct (heuristic) when
tier != "Complex", OR (single-pass context estimate < context_ceiling AND layer_count <= 50). context_ceiling is 100000.
- Enter the partition ASSESSMENT when
tier == "Complex" AND (single-pass context estimate >= context_ceiling OR layer_count > 50). This is not yet a final "partition" verdict — it hands the page to §1.6b's greedy packer, which measures the real cut. The packer is authoritative: if it packs the whole page into exactly one section the decision collapses back to build-direct (§1.6b); if it yields ≥2 sections it is a true partition. So a large-but-repetitive page that genuinely fits one pass still ends up build-direct — but only because the packer measured it, not because anyone judged it "cohesive."
- No subjective build-direct override — this is a hard rule. Once step 5's criteria are met you MUST NOT downgrade the decision to build-direct on qualitative grounds. Reasons like "it's one cohesive feature", "every section reuses the same components", "partitioning would fight DRY", or "I'll just extract reusable components instead" are NOT sanctioned and MUST NOT be acted on. Cross-section reuse/DRY is exactly what the partition track's P-Reconcile step exists to handle (it clusters and dedups shared components across sections) — it is solved inside partition, never by skipping it. The ONLY routes from step 5 to build-direct are: (a) the §1.6b packer measures a genuine one-section fit and collapses it, (b) the user passed
no partition, or (c) a real STOP AND ASK (P1.6-PARTITION-UNDECIDABLE) below. A prose rationale is not one of these — if you find yourself writing a justification for building direct, STOP AND ASK instead.
- On build-direct: write
partition-plan.json with decision: "build-direct", derivation_source: "single", empty sections, then proceed to Phase 2 unchanged. The rest of Phase 1.6 and the orchestrator track are SKIPPED.
This is distinct from §2.1b P2-MULTI-SUBTREE: that check fires for genuinely UNRELATED subtrees and discards the un-chosen ones; partition handles a COHESIVE page that is merely too large and MERGES every section. §1.6 runs first. When it decides partition, the section sub-agents MUST suppress §2.1b's stop-and-ask — the partition decision is authoritative (see §P-Emit).
If the assessment genuinely cannot decide (signals conflict, or the Figma metadata is unreadable), STOP AND ASK (P1.6-PARTITION-UNDECIDABLE) — present the tier, layer count, and measured budget and let the user choose build-direct or partition.
1.6b — Derive sections (no count cap)
Derive section boundaries from the design's natural structure, in this priority order:
- Figma
SECTION nodes, if the page uses them.
- Else the page frame's top-level auto-layout groups.
- Else group cohesive sibling subtrees (the inverse of §2.1b's unrelated-subtree heuristic — group siblings that DO share visual patterns).
Then greedily pack adjacent units in document order: keep adding the next unit to the current section while the section's estimated cost (its layer count × per-layer Figma token cost, same chars/4 math) stays under per_section_headroom (= context_ceiling minus the once-loaded tokens/framework/Figma overhead). Start a new section when the next unit would exceed it. N = the number of packs that emerge — NEVER a constant. The user's manual 3-way split is one possible outcome, not a default.
- A single unit that alone exceeds
per_section_headroom: subdivide it along its own auto-layout groups (depth-1 within §1.6 only — the section sub-agent itself cannot re-partition). If it cannot be subdivided into a flex-only root-plus-one-level shape that fits, STOP AND ASK (P1.6-SECTION-TOO-DEEP).
- The packer yields exactly 1 section: collapse to build-direct (rewrite the decision). Exception — forced partition (§1.6a step 3,
partition in $ARGUMENTS): do NOT collapse. A one-section pack means the page has no budget-driven cut, so fall back to splitting along the natural structural units from the priority list above (Figma SECTION nodes → top-level auto-layout groups → cohesive sibling subtrees) into ≥2 sections. If no such split exists (e.g. a flat, ungrouped sibling list with no SECTION nodes), STOP AND ASK (P1.6-PARTITION-UNDECIDABLE): report that the user forced partition but the design exposes no clean section boundaries, and offer (1) build direct anyway, or (2) restructure the Figma into SECTION nodes / per-section frames and retry.
- Section rectangles MUST be pairwise non-overlapping. If a node straddles a cut and cannot be placed on a clean gutter, STOP AND ASK (P1.6-OVERLAP-NO-CLEAN-CUT) — move the cut or hoist the straddling node into page chrome.
Page chrome (sticky / fixed / full-bleed). Section layouts are flex-only (the §2.4 layout contract). Detect sticky, fixed-position, or full-bleed nodes on the page and hoist them into partition-plan.chrome[] so they are rendered ONCE at page level by P-Merge, never trapped inside a single in-flow section. If a chrome element cannot be cleanly separated from the section flow, STOP AND ASK (P1.6-CHROME-UNPARTITIONABLE).
Capture per section: section_id (the Figma node id), name, a node-scoped figma_url, a disjoint output_path, a PascalCase component_name, frame_origin {dx, dy} (page-absolute top-left), bbox (page-absolute), layer_count, budget_estimate. Coordinates are page-absolute in both frame_origin and bbox — sections do NOT re-root coordinates at their own frame; P-Verify's offset-corrected red-region routing depends on this.
Write partition-plan.json and page-layout.json, then validate both:
node skills/d2c-build/scripts/validate-page-artifacts.js <page-run-dir>
page-layout.json carries the page root's real auto-layout direction + gap and the deterministic ordered regions (section roots): col orders by frame_origin.dy ascending, row/wrap by dx — so a side-rail or multi-column page is NOT flattened into one column. Record inter-section gaps as token refs. On any validator error: line, match it in §2.6's table and follow the failure mode.
Phase 1.7–P-Verify: Partition Orchestrator Track
These stages run ONLY on the partition path (the parent /d2c-build invocation, never a section sub-agent). A section sub-agent is an unmodified /d2c-build leaf and NEVER sees any orchestrator stage. All six Non-negotiables hold for every section exactly as for a single-pass build.
§1.7 P-Freeze — Pre-fan-out freeze
Before any section sub-agent starts, resolve every shared resource at page level so no section mutates shared state mid-fan-out:
- Hoist library resolution. Run §1.5's library check over the WHOLE page (scan every section's subtree for capability keywords, not just the page root). Install any needed library and write
preferred_libraries ONCE, now. A section sub-agent MUST NEVER run npm install or write design-tokens.json / package.json (concurrent package-manager writes corrupt node_modules and trip every sibling's hash check). A section that nonetheless discovers an unresolved capability MUST halt and report it for the orchestrator to resolve here — NEVER install from a leaf.
- Freeze the tokens hash. Compute the
design-tokens.json hash once and inject it as frozen_tokens_hash into every section payload. Each section asserts its hash equals this before it locks.
- Claim disjoint write sets. FAIL IF any two sections'
output_path subtrees overlap (validate-page-artifacts.js enforces this — P1.7-PATH-COLLISION). All cross-cutting shared files (globals.css, tailwind.config.*, layout.tsx, theme.*, barrel index.ts) are OFF-LIMITS to section leaves; a section records a desired shared-file edit as a delta for the orchestrator to apply serially at P-Merge.
- Pin
runs/latest to the parent only, after all section dirs exist. Create <page-run-dir>/page/ and <page-run-dir>/page/sections/<section_id>/ (sanitize : → - in dir names).
- Write the orchestrator checkpoint + partition lock (
reconciled: false):
node skills/d2c-build/scripts/write-partition-lock.js <page-run-dir>/partition-plan.json
This checkpoint is SEPARATE from the single-build .d2c-build-checkpoint.json (build_kind: "partition"); §3.5a MUST refuse to resume a partition checkpoint as if it were a single build.
All interactive STOP AND ASK gates a section could hit (library gap, ambiguous component, no-match) MUST be pre-resolved here at the page level before fan-out — a section sub-agent runs non-interactively and cannot surface a prompt to the user.
P-Emit — Section IR emit (parallel, to the barrier)
Dispatch one structured-input /d2c-build Task per section, in bounded waves (run a few sub-agents concurrently at a time; the section COUNT is uncapped, only the concurrency is throttled to respect resource limits). Each section payload sets: figma_url = the section's node-scoped URL; what: "section"; component_name; output_path; semantic_role: "loaded"; is_section_subbuild: true; forbid_decompose: true; plan_first_barrier: "pre-lock"; frozen_tokens_hash; section_run_dir; checkpoint_path (section-scoped); project_conventions + intake answers detected ONCE at page level and propagated verbatim; audit_path = the page's audit.json. Validate each payload with parse-structured-input.js and re-read its normalised stdout to confirm is_section_subbuild, plan_first_barrier, and shared_component_refs survived — if a field round-tripped to its default when it should not have, ABORT rather than dispatch (a stripped flag would silently duplicate the shared component). Each section runs Pre-flight → Step 0 → §1.4 → Phase 2, then HALTS at the §2.5b pre-lock barrier (writing .barrier-reached).
Barrier is all-or-abort. The orchestrator MUST NOT begin reconciliation until every planned section has reached the barrier. reconcile-sections.js is the deterministic gate:
node skills/d2c-build/scripts/reconcile-sections.js <page-run-dir>/page
It exits non-zero (PSEC-SUBBUILD-FAILED) if any planned section is missing its component-match.json or .barrier-reached. NEVER proceed with survivors — dedup correctness needs the complete global view. If a section failed during emit, halt the whole partition and surface the failure.
P-Reconcile — Plan-first reconciliation (the dedup core)
After all sections reach the barrier:
- Cluster every
chosen: "__NEW__" node across all sections by structural signature (conservative — surface colours excluded so a card on a light vs dark band clusters, but an extra structural prop does not). reconcile-sections.js --write writes shared-components.json (clusters spanning 2+ sections).
- Build each shared component EXACTLY ONCE, sequentially (deterministic cluster order — never in parallel; a later shared build may reuse an earlier one), at its assigned
source (inside the project's components dir, OUTSIDE every section output_path). FAIL IF a target path is already occupied by different content.
- Register ephemerally + rewrite. Add each shared component to
design-tokens.components[] EPHEMERALLY for this run's validation only (Phase 6 asks before any permanent promotion — NEVER mutate the user's registry silently). Then:
node skills/d2c-build/scripts/reconcile-sections.js <page-run-dir>/page --apply
This rewrites each consuming section's component-match.json — flips chosen __NEW__ → the shared componentId, injecting a synthesized candidate tagged reconciled_by_orchestrator: true (NOT a faked user_confirmed). Per-section visual variation uses a rule-13 wrapper (one canonical component + a per-section wrapper), never a second copy.
- Re-validate + lock. Re-run
validate-ir.js per rewritten section (source now exists on disk → passes), then write each section's decisions.lock.json over the rewritten IR. Lock-after-reconcile keeps Non-negotiable rule 6 intact.
- Seal the partition lock LAST, atomically, recording
reconciled: true + a content hash of each section's component-match.json + shared-components.json:
node skills/d2c-build/scripts/write-partition-lock.js <page-run-dir>/partition-plan.json --reconciled --shared <page-run-dir>/page/shared-components.json --sections-dir <page-run-dir>/page/sections
On resume the orchestrator reads this lock FIRST: when reconciled: true it MUST NOT let a section re-run Phase 2 (which would re-emit __NEW__ and undo the dedup); --verify-lock re-hashes and STOPS on any drift (P2R-RECONCILE-REVALIDATE).
P-Codegen — Section codegen (parallel) + map merge
Resume each section past the barrier (write decisions.lock.json, run Phase 3) in bounded waves. Each section writes ONLY inside its claimed output_path subtree and records its §3.X node_file_map. A section sub-agent with is_section_subbuild: true runs Pre-flight → Phase 3 codegen → §3.X map, then RETURNS — it NEVER enters Phase 3.5 or Phase 4 (no per-section dev-server, no port contention; all screenshotting happens once at P-Verify).
After all sections finish, merge the node_file_maps in ONE deterministic pass (sections in frame_origin order): apply each section's frame_origin {dx,dy} to every figma_bbox into one page coordinate space, tag each entry with section_id, and build the page-level chosen-by-source index from shared-components.json as the authority (a file in 2+ sections' maps is recorded once, tagged with all contributing section_ids).
P-Merge — Page assembly
Write the parent page file at the page output_path per page-layout.json: import each section root + each shared component (single source) and render them inside ONE <main> in the deterministic regions order. Apply the page chrome (partition-plan.chrome[]) once, outside the in-flow section stack. Section roots are margin-neutral; inter-section spacing comes from page-layout.json so P-Verify can diff it. No section code is duplicated into the parent. Apply any section-recorded shared-file deltas here, serially (single writer). Run a page-level pass over the union of section + page files: at most one <h1> (demote non-lead section titles), no duplicate landmark/id, dedup global imports into the shell. FAIL IF page-layout.json references a missing section root (PMERGE-INCOHERENT).
P-Verify — Whole-page verification (authoritative gate)
Hand the assembled page into the EXISTING Phase 3.5 → Phase 4 loop as the single page.goto target. Use full-page capture (not the fixed 1280×800 viewport — a tall page is otherwise unverifiable below the fold) plus per-section clipped diffs (each region against its node-scoped Figma frame via frame_origin + bbox) and seam-band diffs for inter-section spacing. The page-level round_history score is the gate; per-section clipped scores stay separate and gate shared-file edits (see §4.3c's whole-page extension). Phase 6 cleanup runs ONLY after P-Verify passes (or on abort). When audit_path was set on the sections, the per-section pixel-diff is owned here, not by the leaf.
Phase 2: Emit and Validate Intermediate Representation
MANDATORY GATE — Read before proceeding. Before emitting any IR artifact, read references/failure-modes.md lines 1-91 (Meta-Rules section + Quick Reference table). This primes you to recognize failure IDs and follow the correct tier (auto-recover, inform, or stop-and-ask). If you are about to continue past an error without prompting the user, STOP. Re-read the Anti-Rationalization rule. It applies to you right now.
IR is the plan for the build. Before any code is written, Phase 2 produces four JSON artifacts in .claude/d2c/runs/<timestamp>/ and runs scripts/validate-ir.js to verify them. Code generation in Phase 3 reads the validated IR as a frozen input — Non-negotiable rule 6 forbids re-deciding any IR value during codegen or retry.
Three of the four artifacts are authored (the model decides their contents). The fourth (run-manifest.json) is mechanical bookkeeping written by this phase itself.
Version coupling rule: All four IR artifacts (run-manifest.json, component-match.json, token-map.json, layout.json) MUST share the same schema_version value. Set schema_version to 2 for ALL artifacts. Do not mix v1 and v2 — the validator rejects any version mismatch across artifacts. v2 is the current version and enables scored candidates with score_breakdown in component-match.json.
Phase 2 Quick Reference:
- 2.0 Create run directory:
.claude/d2c/runs/<YYYY-MM-DDTHHMMSS>/
- 2.1
run-manifest.json: compute SHA-256 hash of design-tokens.json (or concatenated split files in order: tokens-core | tokens-colors | tokens-components | tokens-conventions)
- 2.2
component-match.json: score candidates per node, pick highest
- 2.3
token-map.json: map Figma properties to <category>.<name> token paths (must resolve to leaf values in design-tokens.json)
- 2.4
layout.json: root region + 1 level nested, flex-only, deferred must be empty
- 2.5 Run
validate-ir.js — all artifacts must pass before Phase 3
2.0 — Create the run directory
Compute a timestamp in the format YYYY-MM-DDTHHMMSS (no colons, filesystem-safe on Windows). Create the directory:
.claude/d2c/runs/<YYYY-MM-DDTHHMMSS>/
Also update .claude/d2c/runs/latest to point at this new directory. On POSIX, create a symlink; on Windows (or if symlinks fail), write a plain text file containing the run-dir path. Store the full run directory path as ir_run_dir for use in step 2f and in the checkpoint file.
Partition depth-1 guard. When the structured-input payload has is_section_subbuild: true, the run directory is the payload's section_run_dir (the orchestrator already created it under <page-run-dir>/sections/<id>/). In that case a section sub-agent MUST NOT touch .claude/d2c/runs/latest — the parent orchestrator owns it (P-Freeze pins it). NEVER repoint latest from a section sub-build; doing so cross-wires concurrent siblings' resume state.
2.1 — Emit run-manifest.json (mechanical)
Compute the SHA-256 hex digest of .claude/d2c/design-tokens.json (raw file bytes). If split_files: true, instead hash the four split files concatenated in the fixed order tokens-core.json | tokens-colors.json | tokens-components.json | tokens-conventions.json.
Write <ir_run_dir>/run-manifest.json:
{
"schema_version": 1,
"figma_url": "<the Figma URL from Phase 1>",
"started_at": "<ISO 8601 timestamp with timezone, e.g. 2026-04-10T10:00:00-07:00>",
"framework": "<framework from design-tokens.json>",
"tokens_file_hash": "<SHA-256 hex>"
}
This file anchors the run to a specific tokens version. If the user edits design-tokens.json mid-build, the validator catches it via hash mismatch and forces a STOP AND ASK.
2.1b — Check for multiple unrelated subtrees
Before emitting IR, analyze the Figma frame's direct children. If the frame has 3+ direct children with no shared visual patterns (different background colors, different layout directions, different content types with no visual grouping), this likely represents multiple unrelated sections. Read the P2-MULTI-SUBTREE entry in references/failure-modes.md and follow its prompt template — STOP AND ASK which subtree(s) to build.
2.2 — Emit component-match.json (authored, scored)
For every Figma node that will render as a component instance, scan design-tokens.json.components[] and identify up to 3 candidate components. Score each candidate on three dimensions, pick the highest-scoring one, and write the result.
Scoring Rubric (total 0–100)
Dimension 1 — Props match (0–50 points)
For each candidate component, count how many of the Figma node's identifiable visual properties map to the component's declared props[].
A Figma identifiable property is a distinct visual attribute in the node that a component prop would need to control:
- Text content → maps to
children, label, title, placeholder, description, etc.
- Color/variant indicators → maps to
variant, color, type, intent, status, etc.
- Size indicators → maps to
size, width, height, etc.
- Interactive states → maps to
disabled, loading, active, checked, open, etc.
- Children/slots → maps to
children, header, footer, icon, prefix, suffix, etc.
- Spacing/padding → maps to
padding, gap, spacing, compact, etc.
- Icons/images → maps to
icon, src, avatar, image, etc.
For each component prop, ask: "Does the Figma node contain visual evidence that this prop would be used?" Binary per-prop — yes or no.
Formula: props_match = floor(matched_props / max(figma_identifiable_properties, 1) * 50)
The denominator is Figma-side (demand), not component-side. This avoids penalizing components with many props.
Dimension 2 — Usage frequency (0–25 points)
Uses the import_count field from design-tokens.json.components[] (computed during /d2c-init).
Relative ranking among candidates for the current node:
- Find
max_count = max(candidate.import_count for all candidates for this node)
- If
max_count == 0 or all candidates lack import_count: all candidates get 13 (neutral — do not let this dimension swing the outcome when there is no data)
- Otherwise:
usage_frequency = floor(candidate.import_count / max_count * 25)
Dimension 3 — Semantic alignment (0–25 points)
How well the component's name and description match the Figma node's name, type, and surrounding context.
| Score | Band | Criteria |
|---|
| 18–25 | Strong | Component name is a case-insensitive substring of the Figma node name (e.g., "Button" matches "SubmitButton"), OR the description's primary function directly matches the node's apparent purpose |
| 9–17 | Partial | Component type (from description) matches the node's structural role, but names are not directly related (e.g., "Card" component for a Figma frame named "ProductWrapper") |
| 0–8 | None | No clear relationship between component purpose and node purpose |
Composite score: score = props_match + usage_frequency + alignment
Candidate Selection
- For each Figma node, evaluate ALL components in
design-tokens.json.components[] against the rubric.
- Rank by composite score descending.
- Emit the top 3 candidates (or fewer if the registry has fewer components).
- Tiebreaker chain (when composite scores are equal): (a) higher
props_match, (b) higher usage_frequency, (c) alphabetical by componentId.
Threshold and Chosen
- MUST pick the highest-scoring candidate as
chosen.
- FAIL IF the top score is below 50 — read
references/failure-modes.md entry P2-NO-MATCH and follow its user prompt template. Set chosen: "__NEW__".
- User-confirmed override: If the user confirmed a component in Phase 1.4b, that component MUST be in candidates with
user_confirmed: true. It becomes chosen regardless of score. The 50-point threshold does NOT apply to user-confirmed choices.
- If the model cannot decide between candidates (genuinely ambiguous), set
chosen: null — the validator will fail and STOP AND ASK the user.
- Empty registry: If
design-tokens.json.components[] is empty, every node gets candidates: [], chosen: "__NEW__", chosen_reasoning: "No reusable components in registry.". Skip the scoring flow.
Output Format
{
"schema_version": 2,
"nodes": {
"<figma-node-id>": {
"figma_name": "<layer name>",
"figma_type": "<INSTANCE | FRAME | COMPONENT | ...>",
"candidates": [
{
"componentId": "<name>",
"source": "<file path relative to project root>",
"reasoning": "<one-line why this is a candidate>",
"score": 78,
"score_breakdown": {
"props_match": 40,
"usage_frequency": 20,
"alignment": 18
}
},
{
"componentId": "<other name>",
"source": "<file path>",
"reasoning": "<one-line why>",
"score": 52,
"score_breakdown": {
"props_match": 30,
"usage_frequency": 10,
"alignment": 12
},
"rejected_because": "<why this was not chosen over the winner>"
}
],
"chosen": "<componentId of highest-scoring candidate>",
"chosen_reasoning": "<one-line explanation referencing the score>"
}
}
}
Rules (enforced by the validator)
schema_version MUST be 2.
candidates[] MUST have 0–3 entries. Empty is valid only when chosen is "__NEW__".
- Candidates MUST be sorted by
score descending.
- Each candidate MUST have
score, score_breakdown, and reasoning.
score_breakdown.props_match + score_breakdown.usage_frequency + score_breakdown.alignment MUST equal score.
score_breakdown ranges: props_match 0–50, usage_frequency 0–25, alignment 0–25.
chosen MUST be candidates[0].componentId (highest score), OR "__NEW__", OR null (ambiguous — STOP AND ASK), OR a candidate marked user_confirmed: true.
- Non-chosen candidates MUST have
rejected_because (non-empty string).
chosen_reasoning is mandatory.
- If
chosen points at a source that does not exist on disk AND is not in design-tokens.json.components[], validation fails.
2.3 — Emit token-map.json (authored)
Conflict-aware preamble: Before emitting token-map.json, read .claude/d2c/token-conflicts.json if it exists. If any conflict entry has status: "unresolved", trigger P2-TOKEN-CONFLICT-ASK — STOP AND ASK the user to resolve before proceeding with token mapping. Do not emit token-map.json while unresolved conflicts exist.
For every Figma node with design properties that would normally be translated to CSS values, map each property to a dotted token reference against design-tokens.json:
{
"schema_version": 1,
"nodes": {
"<figma-node-id>": {
"background": "colors.primary",
"color": "colors.on-primary",
"padding-x": "spacing.md",
"gap": "spacing.sm"
}
}
}
Rules:
- Every value MUST be a lowercase dotted path of the form
<category>.<name> (or <category>.<group>.<name> for nested categories). Categories covered: colors, spacing, typography, breakpoints, shadows, borders.
- Every value MUST resolve to a real entry in
design-tokens.json. Unknown references fail validation with a "did you mean" suggestion.
- NEVER hardcode values with an inline escape hatch — if a Figma value has no matching token, STOP AND ASK (Non-negotiable rule 3).
- Conflict resolution: If
token-conflicts.json exists and a Figma design value matches multiple tokens in the same category (same resolved value), MUST use the canonical token path from the resolved conflict entry. Do NOT use any of the duplicates paths. If the conflict was auto-resolved, log a brief note: "Used canonical {canonical} (auto-resolved over {duplicates})". This is an inform-tier action (P2-TOKEN-CONFLICT-AUTO) — build continues.
2.4 — Emit layout.json (authored, shallow)
Capture the root layout region plus one level of nested regions. v1 is flex-only. Grid, absolute, and deeper nesting go into deferred[] and fail the run so the user is prompted.
{
"schema_version": 1,
"root": {
"nodeId": "<root figma id>",
"direction": "col",
"gap": "spacing.lg",
"align": "start",
"justify": "start",
"regions": ["<child region id 1>", "<child region id 2>"]
},
"regions": {
"<child region id 1>": {
"direction": "row",
"gap": "spacing.sm",
"align": "center",
"justify": "between",
"children": ["<child node id 1>", "<child node id 2>"]
}
},
"deferred": []
}
Rules:
direction MUST be one of row, col, wrap.
gap MUST be a dotted token reference that resolves in design-tokens.json (same rules as token-map.json).
- Every
children[] entry MUST exist as a key in component-match.json.nodes (cross-reference check by the validator).
deferred[] MUST be empty for a successful run. Non-empty deferred[] fails the validator — STOP AND ASK the user to either convert the region to flex in Figma or skip it.
- Only one level of nested regions is supported in v1. Anything deeper goes into
deferred[].
2.5 — Run validate-ir.js
Invoke the validator on the run directory. Resolve the script path by checking these locations in order (first match wins):
scripts/validate-ir.js (relative to this SKILL.md file)
~/.agents/skills/d2c-build/scripts/validate-ir.js
~/.claude/skills/d2c-build/scripts/validate-ir.js
~/.claude/commands/d2c-build/scripts/validate-ir.js
- Glob fallback: search for
**/validate-ir.js
node "$VALIDATE_IR_SCRIPT" ".claude/d2c/runs/<timestamp>/"
Parse stdout. The first line is validate-ir: ok | fail | skip. On ok, proceed to step 2f. On fail, read the subsequent error: … lines and enter the failure handling below.
2.6 — Failure handling
When validate-ir.js reports fail, read the error: … lines and match each against the table below to find the correct failure mode. Then read the matching entry in references/failure-modes.md and follow its User prompt template exactly — do NOT improvise a response.
Error-to-failure-mode matching rules:
| Error pattern | Failure mode ID |
|---|
"required field" / "expected ... got" / "pattern" / "unexpected field" | P2-SCHEMA-ERR |
"unknown token reference" | P2-UNKNOWN-TOKEN |
"not present in component-match" | P2-DANGLING-REF |
"not in candidates" | P2-INVALID-CHOSEN |
"chosen is null" | P2-AMBIGUOUS-CHOSEN |
"deferred" | P2-DEFERRED |
"tokens_file_hash" + "mismatch" | P2-HASH-MISMATCH |
"does not exist on disk" | P2-SOURCE-MISSING |
"not at barrier" / "PSEC-SUBBUILD-FAILED" (from reconcile-sections.js) | PSEC-SUBBUILD-FAILED |
"duplicate source for shared component" / "P2R-SHARED-DUP-SOURCE" | P2R-SHARED-DUP-SOURCE |
"P2R-FALSE-MERGE" (shared cluster spans < 2 sections) | P2R-FALSE-MERGE |
"P1.7-PATH-COLLISION" (section output paths overlap) | P1.7-PATH-COLLISION |
"P1.6-OVERLAP-NO-CLEAN-CUT" (section bboxes overlap) | P1.6-OVERLAP-NO-CLEAN-CUT |
"P1.6-SECTION-TOO-DEEP" (section over per-section budget) | P1.6-SECTION-TOO-DEEP |
"PMERGE-INCOHERENT" (page-layout regions / order invalid) | PMERGE-INCOHERENT |
"P1.7-TOKEN-DRIFT" / "P2R-RECONCILE-REVALIDATE" (verify-partition-lock drift) | P2R-RECONCILE-REVALIDATE |
| Any unrecognized error | PX-UNKNOWN-FAILURE |
Batching: If multiple errors fire in the same validation run, present them in a single STOP AND ASK message grouped by failure ID (see Meta-Rules in references/failure-modes.md). Do NOT issue separate prompts for each error.
In every failure case, NEVER edit the IR files inline to silence the validator. Regenerate them, or STOP AND ASK. (Non-negotiable rule 6.)
2.5b — Write decisions.lock.json
Plan-first barrier (partition track only). When the structured-input payload has plan_first_barrier: "pre-lock", a section sub-agent MUST HALT here: after validate-ir.js exits 0 and §2.7 records ir_run_dir, write an empty .barrier-reached marker file into the section run dir, then RETURN immediately — do NOT write decisions.lock.json, do NOT enter Phase 3. Its IR is intentionally left validated-but-unlocked so the orchestrator's P-Reconcile step can rewrite chosen from __NEW__ to a shared component BEFORE the lock is written. Locking before reconciliation would violate Non-negotiable rule 6. This barrier is distinct from and earlier than the §2.8 dry-run halt (which stops AFTER the lock is written). When plan_first_barrier is null (every standalone and flow build), ignore this guard and continue.
Immediately after validate-ir.js exits 0, freeze all IR decisions into a lock file. This is the enforcement artifact for Non-negotiable rule 6 — every phase after Phase 2 reads it and treats locked entries as immutable.
Write <ir_run_dir>/decisions.lock.json:
{
"schema_version": 1,
"locked_at": "<ISO 8601 timestamp with timezone>",
"layout_locked": true,
"nodes": {
"<figma-node-id>": {
"componentId": "<chosen value from component-match.json>",
"status": "locked",
"locked_at": "<same ISO 8601 timestamp>"
}
}
}
Generation steps:
- Read
component-match.json from <ir_run_dir>/.
- For each node in
component-match.json.nodes, create a lock entry with componentId set to the node's chosen value, status: "locked", and locked_at set to the current timestamp.
- Set top-level
schema_version: 1, layout_locked: true, and locked_at to the same timestamp.
- Write the file. This locks all component choices and token mappings. Token-map.json is locked as a whole — no per-property override mechanism in v1.
The lock file uses the decisions-lock.schema.json schema. If validate-ir.js is re-run after the lock is written (e.g., on resume), it validates the lock against the schema and cross-references every node against component-match.json.
Override mechanism (three tiers):
- Per-node unlock: If Phase 4 or Phase 5 identifies that a locked node needs re-decision, STOP AND ASK the user. If approved, update the lock file: set the node's
status to "failed", add failure_reason, failed_at, and failed_by. Then regenerate the IR and write a fresh lock.
- Full fresh build: User runs
/d2c-build without --resume. New run directory, new IR, new lock.
- Manual deletion: User deletes
decisions.lock.json from the run directory. On next resume, the skill detects the missing lock and re-runs Phase 2 for all nodes.
2.7 — Record ir_run_dir in the checkpoint
On success, append ir_run_dir: "<full path to run dir>" to .claude/d2c/.d2c-build-checkpoint.json so a later resume can re-read the same IR instead of regenerating it. Proceed to Phase 3.
2.8 — Dry-run halt
If the user passed dry run in $ARGUMENTS (see Phase 1.1b), halt here after step 2.7. Do not proceed to Phase 3. Print a summary of the IR contents (node count, token-ref count, deferred count) and the path to .claude/d2c/runs/<timestamp>/ so the user can inspect the JSON before deciding to continue.
Phase 3: Generate Code
MANDATORY GATE — Read before proceeding. Before writing any code, read references/failure-modes.md lines 1-91 (Meta-Rules section + Quick Reference table). Pay special attention to P3-IR-DEVIATION, P3-COMPONENT-GONE, and P3-PROP-MISMATCH. If at any point during codegen you feel the urge to "just use a different component" or "substitute a better token" — that is the Anti-Rationalization trap. STOP AND ASK.
Phase 3 preamble — non-negotiable. Before writing any code, MUST read all three authored IR artifacts from <ir_run_dir>/ (component-match.json, token-map.json, layout.json) AND the decision lock (decisions.lock.json). Treat them as frozen inputs. Verify all node entries in decisions.lock.json have status: "locked". If any entry has status: "failed", only that node's component choice and token mapping may differ from the original IR — all other nodes remain immutable.
- NEVER re-decide a
chosen component during codegen. If a component in component-match.json.chosen turns out to be a poor fit, STOP AND ASK — do not silently swap it.
- NEVER re-resolve a token reference. Use only the tokens listed in
token-map.json for each node. If the model realizes a value was mapped incorrectly, STOP AND ASK.
- NEVER change layout direction, gap, or region membership from what
layout.json specifies.
- FAIL IF the generated code references a component that is not in
component-match.json.chosen or uses a token that is not in token-map.json for that node — the Phase 5 audit will catch this, but the model MUST NOT produce such code in the first place.
- When a situation arises that genuinely requires changing an IR value, STOP AND ASK — do not patch the IR inline.
Pre-generation checks (before writing any code for a node):
- Verify component source exists. For each node in
component-match.json, confirm the chosen component's source path exists on disk. If missing, follow P3-COMPONENT-GONE in references/failure-modes.md.
- Verify prop compatibility. Read the chosen component's actual prop interface from the source file. If the Figma design needs a prop the component doesn't have, follow P3-PROP-MISMATCH.
- Verify file placement. If creating a new component, confirm a standard component directory exists. If none exists, follow P3-FILE-PLACEMENT.
If at any point during codegen you are about to deviate from the frozen IR (different component, different token, different layout direction), follow P3-IR-DEVIATION — STOP AND ASK with the exact prompt template. NEVER silently substitute.
Code Principles
Follow the Non-negotiables (rules 1–6 at the top of this file) and the Generation Rules section above (styling approach, 2+ pattern extraction, SOLID, DRY). Additionally:
- Before creating ANY new component, MUST check
design-tokens.json → components. If an existing component can do the job, MUST use it (this is Non-negotiable rule 4). If uncertain whether an existing component fits, STOP AND ASK.
- If a pattern appears 2+ times in the design (same HTML structure + same visual styling = repeated pattern; different text content does NOT make it different), MUST extract it into a new reusable component.
- New components MUST be props-driven and composable. NEVER hardcode content — pass it through props.
Project conventions take highest priority for stylistic choices. If conventions exists in design-tokens.json, follow those for: component declaration style, export style, type definitions, type location, file naming, import ordering, CSS utility wrapping, barrel exports, and props pattern. The framework reference file remains authoritative for framework requirements — reactivity system, template syntax, directives, lifecycle hooks, className vs class, "use client" rules. The distinction: if it's a stylistic choice where the team could go either way, follow conventions. If it's a framework requirement that can't vary, follow the framework reference.
Figma Auto Layout Mapping:
- Figma horizontal Auto Layout → CSS flexbox
flex-row (or Tailwind flex flex-row)
- Figma vertical Auto Layout → CSS flexbox
flex-col (or Tailwind flex flex-col)
- Auto Layout spacing value → CSS
gap (or Tailwind gap-X)
- Auto Layout padding → container padding
- Figma "hug contents" →
width: auto / w-auto
- Figma "fill container" →
width: 100% / w-full or flex: 1 / flex-1
- Figma "fixed" → explicit width/height values
File Structure:
- File placement rules (deterministic):
- New reusable components: Check the framework reference file for the standard component directory. As a secondary fallback, place the new component in the same directory as the existing component most similar in type (match by keyword overlap between the new component's name and existing component names in
design-tokens.json). If no similar component exists, use the first directory that exists from: src/components/ui/, src/components/shared/, src/components/common/, src/components/, components/ui/, components/.
- Page-specific components: Place in the route directory's
components/ subfolder. The route directory structure varies by framework — check the framework reference file for the page directory convention.
- New hooks/composables/services: Place in the same directory as existing ones. Check the framework reference file for the standard location (e.g.,
src/hooks/ for React, composables/ for Vue/Nuxt, src/lib/ for Svelte, src/app/core/services/ for Angular).
- API functions/services: Place in the same directory as existing API files (from
api.config_path in tokens). If none exists, check the framework reference file for the default API directory.
- Strict one-component-per-file rule: Each component gets its own file. The only exception is a sub-component that is 15 lines or fewer — it is permitted to stay in the parent file. Anything over 15 lines MUST be extracted to a separate file, imported, and used.
- File naming: If
conventions.file_naming is set and not "mixed", name all new files using that pattern (e.g., PascalCase → UserProfile.tsx, kebab-case → user-profile.tsx). Otherwise follow the framework reference default.
- Barrel exports: If
conventions.barrel_exports.value is "yes", create or update an index.ts/index.js file in the component directory to re-export the new component.
- Type files: If
conventions.type_location.value is "separate_file", place TypeScript interfaces/types in a types.ts file in the same directory as the component, not inline.
- Export with proper TypeScript types for all props.
Generation Rules
- Write code for the primary viewport first (typically desktop).
- Add responsive behavior for other viewports using the project's breakpoint system from
design-tokens.json.
- Use semantic HTML (
nav, main, section, article, aside, header, footer, button) — not div soup. Every page MUST have a <main> element. Card titles MUST use heading elements (<h3>, <h4>), not styled <div> or <p>. Data tables MUST use <table>, not flex divs. Navigation MUST use <nav>. Sidebar sections MUST use <aside>.
- WRONG:
<div className="card"><div className="card-title">Revenue</div></div>
- RIGHT:
<section className="card"><h3 className="card-title">Revenue</h3></section>
- WRONG:
<div onClick={handleClick}>Submit</div>
- RIGHT:
<button onClick={handleClick}>Submit</button>
- Accessibility (WCAG 2.2 AA minimum):
- All images must have
alt text (descriptive for informational images, alt="" for decorative)
- All form inputs must have associated
<label> elements or aria-label
- Heading hierarchy must not skip levels (no h1 then h3)
- All interactive elements (buttons, links, inputs) must have minimum 24x24px touch target (WCAG 2.5.8)
- All interactive elements must be keyboard-focusable with visible focus indicators
- Icon-only buttons must have
aria-label
- Color must not be the sole means of conveying information
- The root
<html> element MUST have a lang attribute
- Every new component must have a JSDoc comment above the function/component declaration:
/** Brief description. @param props.propName - Description */