From the field #07 - Figma to code that doesn’t hallucinate
Part 2: two tools, one core
Part 1 ended on one idea. Build a deterministic extraction core - exact values out of Figma, RULE ZERO enforced, nothing guessed anywhere - and that core does two jobs, not one. You generate with it and you verify with it.
So I built both. Two public tools, two halves of the same core, and you can clone either one and run it today.
This is what they are.
figma-kit, the generate half
figma-kit is a library plus a CLI. Node 20 or newer. Zero runtime dependencies, just node builtins and the global fetch. It is public under AGPL.
It is deterministic in the boring, load-bearing sense. The same Figma node plus the same tokens gives byte-identical output on every single run. No model in the data path. No temperature. No guessing. If you run it twice and get two different files, that is a bug, not a feature.
The whole thing is a chain of pure transforms. Figma, then ParsedElement[], then MappedElement[], then a CodegenSpec, then GeneratedFile[]. Each arrow is a function with no surprises. You can test any link in isolation, because nothing in the chain reaches out to a network or a model to decide what a value should be.
There are two ways into the chain.
The first is the MCP-primary path. parseDesignContext consumes the output of get_design_context, which is real React plus Tailwind JSX, not a picture. Your MCP client handles the Figma auth, and the kit parses the deterministic code it gets back. The model the client uses to fetch never touches a screenshot.
The second is extractViaRest. It fetches the node tree directly with a FIGMA_TOKEN and is fully self-contained. No MCP client needed. Different transport, same discipline. Read the structured values, never interpret a render.
Why keep both. Because reality is messy. Sometimes you are inside an MCP-enabled editor and get_design_context is right there. Sometimes you are in CI with nothing but a token and a node id. The two paths converge on the same ParsedElement[], so everything downstream, the mapping, the spec, the generators, does not know or care which door the values came through. One core, two front doors.
Then comes the part that matters most in practice, which is what happens to the raw values.
A raw gap: 16px is not what you want frozen into a component. You want gap: var(--space-4), mapped back to the design token, so the output speaks your design system instead of a pile of magic numbers. loadTokensFromCss reads any design-token stylesheet, resolves var() chains and rem to px, and builds reverse maps from value to token name.
The key word is any. Token mapping works by configurable name patterns. Any prefix, zero hardcoded prefixes. Point it at your stylesheet and it learns your scale. There is also a standard Tailwind scale option if you have no token file at all.
And here is the line where most tools quietly lie. A matched value renders as var(--token). An unmatched value is reported as a warning. It is never guessed into a token that happens to be close. The kit does not invent a name to make the output look tidier. It tells you it did not match, and it leaves the decision to you.
That warning is not a failure. It is information. An unmatched value is the kit saying: this number is not in your scale, so either it is a real one-off or your scale has a gap, and only you can tell which. A tool that silently rounded it to the nearest token would rob you of that signal and feel smoother doing it. Smoother and wrong. I would rather have the honest warning every time.
One spec, three skeletons. The B-full generator renders the same CodegenSpec to plain CSS, to a Vue SFC, or to a React component with its CSS. Same values, three shapes. You pick the framework, the values do not change.
Now the part I care about most.
RULE ZERO is not a guideline here. It is an explicit, enforced, tested invariant. An absent property is null all the way through the pipeline, from parse to spec. Every single declaration is routed through one helper called declare, and declare emits nothing for an absent value. There is no path around it.
That means the generators are physically unable to output a property the design did not specify. Not unlikely to. Unable to. There is no border-radius: 0 invented to fill a gap, no estimated margin that nobody asked for. To put it in the words from Part 1:
If a property is absent in the extracted design, its value is zero or none. Never estimate it from a screenshot. Absent means zero.
I did not hope this held. I proved it. It lives in tests/rule-zero.test.ts.
The numbers, since this is From the Field and the numbers are the point.
18 source TypeScript files.
About 2090 lines of code.
5 test files, 25 tests, all green.
tsc --noEmit clean. The dist build clean.
The CLI is one command. You run generate with -c or --design-context for a file or stdin, or --url for the REST path. You add --tokens to point at your stylesheet or --tailwind for the standard scale. You set -n for the name, -f to pick css, vue, react, or all, and -o for the output directory or --stdout to print.
One honest note on provenance, because it matters. The skeleton generator is a net-new build. The private pipeline this distills from only ever had a spec emitter, never a generator. I did not lift a generator out of something private and slap a license on it. I built a deterministic generator from scratch for the public release, and the determinism and RULE ZERO came along because that is how the core works.
It is public. Clone it and run it today.
qa-pack, the verify half
If figma-kit writes code from the design, qa-pack checks code against the design. Same core, opposite direction. It is public under MIT.
qa-pack is not a node app. It is a Claude Code plugin: agents, commands, and a qa-methodology skill. You install it into a project and it tests a built implementation against what the design says it should be.
The first rule is the one everything else hangs on. The reference is the source of truth, never the implementation. What a feature should look like comes from a fresh extraction of the design, never from a screenshot guess and never from memory of how the code already behaves. A QA run that trusts the implementation’s own assumptions will only ever confirm the implementation’s own bugs. So qa-pack refuses to.
It carries the same extraction discipline as figma-kit, and the same RULE ZERO, word for word. If a CSS property is absent in the extracted design code, its value is zero or none. No rounded class means border-radius: 0. No shadow class means no shadow. Never estimate from screenshots. Absent equals zero.
And it carries the one small guard from Part 1, the one that bites everyone who builds this. When qa-pack reads the implementation back with getComputedStyle, it reads from the deepest text element, not from the container. Read the container and you collect inherited and default values that were never really set, and every one of them becomes a false positive. Read the leaf and you get what the element actually renders. That single choice is the difference between a report full of noise and a report you can act on.
Coverage is 13 categories, A through M. Texts, typography, colors, spacing, borders and radius, layout, icons, completeness, interactive states, accessibility, cross-device, UX scale, cross-component consistency. Every report covers all 13. The ones that are out of scope are marked NOT TESTED with a reason, so the gaps are loud, not silent.
There are two tracks. Track A is design-based: a fresh Figma extraction and a full, pixel-perfect audit across all of A to M, measured against the design values. Track B is functional: acceptance criteria plus the app’s own conventions, for the work that has no design reference. Same 13 categories either way. Track B just checks against the design system and the sibling components instead of against Figma.
The loop is four commands. check runs the audit and writes the report. fix reads that report and traces each bug to its cause in the code. retest re-runs against the same report to confirm what closed. full runs the whole loop end to end. There is an init to set the project up the first time. The report is the state. Each run appends to it, so the history of what broke and what got fixed lives in one file.
That last detail is doing more work than it looks. Because the report is append-only and the report is the state, fix and retest are not guessing at what check meant. They parse the same structured run history. A bug found in one run, a cause traced in the next, a confirmation in the one after, all in a single file you can read top to bottom. The tool does not hold opinions in its head between runs. It writes them down, then reads them back. Same instinct as the rest of the core. Nothing left to guess.
It is public. Clone it and run it today.
the point
Step back and the two tools are not two ideas. They are one.
The same deterministic extraction feeds both. The same RULE ZERO feeds both. figma-kit reads the design and writes a skeleton. qa-pack reads the design and checks a build. One generates, one verifies, and the values they work from come out of the design the exact same way.
That is why I trust them for the same reason. Not because the code is clever. Because nothing in the pipeline ever guessed. A generated property is there because the design specified it. A flagged QA bug is a real divergence from a measured value, not a vibe about a screenshot. Honest output on both ends, for one reason, traced to one core.
You build with one. You check with the other. Neither one is allowed to make a number up.
where this goes
There is a third thing, and it is the obvious one. Generate with figma-kit, verify with qa-pack, and when verify finds a divergence, feed it back into generate and close the loop automatically. Generate, verify, fix, repeat, until the build matches the design with nothing guessed at any step.
That loop is real work, and I want to be straight about its status. It is not shipped. The two public halves are. The full orchestration is the next thing, and it is also where this stops being a developer convenience and starts being a business case.
Part 3 is that case. Why a deterministic design-to-code core is worth money to a team, not just satisfying to an engineer, and where the loop goes from here.
From the Field is what I actually build, what breaks, and what I learn. Real projects, real numbers, real bugs. No tutorials.