From the field #07 - Figma do kodu, który nie halucynuje

Część 2: dwa narzędzia, jeden rdzeń

Część 1 skończyła się na jednej myśli. Zbuduj deterministyczny rdzeń ekstrakcji. Dokładne wartości wyciągnięte z Figmy, wyegzekwowany RULE ZERO, nic zgadywane nigdzie. I ten rdzeń robi dwie roboty, nie jedną. Generujesz nim i weryfikujesz nim.

Więc zbudowałem oba. Dwa publiczne narzędzia, dwie połówki tego samego rdzenia. Każde możesz sklonować i odpalić dzisiaj.

To jest to, czym są.

figma-kit, połowa generująca

figma-kit to biblioteka plus CLI. Node 20 albo nowszy. Zero zależności runtime, tylko wbudowane moduły node i globalny fetch. Publiczny na licencji AGPL.

Jest deterministyczny w tym nudnym, nośnym sensie. Ten sam node Figmy plus te same tokeny daje bajt w bajt identyczny output przy każdym pojedynczym uruchomieniu. Żadnego modelu w ścieżce danych. Żadnej temperatury. Żadnego zgadywania. Jeśli odpalisz dwa razy i dostaniesz dwa różne pliki, to jest bug, nie feature.

Całość to łańcuch czystych transformacji. Figma, potem ParsedElement[], potem MappedElement[], potem CodegenSpec, potem GeneratedFile[]. Każda strzałka to funkcja bez niespodzianek. Możesz przetestować dowolne ogniwo w izolacji, bo nic w tym łańcuchu nie sięga do sieci ani do modelu, żeby zdecydować, jaka powinna być wartość.

Do łańcucha prowadzą dwa wejścia.

Pierwsze to ścieżka MCP-primary. parseDesignContext konsumuje output z get_design_context, którym jest realny React plus Tailwind JSX, nie obrazek. Twój klient MCP ogarnia auth do Figmy, a kit parsuje deterministyczny kod, który dostaje z powrotem. Model, którego klient używa do pobrania, nigdy nie dotyka zrzutu ekranu.

Drugie to extractViaRest. Pobiera drzewo node’ów bezpośrednio z użyciem FIGMA_TOKEN i jest w pełni samowystarczalne. Żadnego klienta MCP. Inny transport, ta sama dyscyplina. Czytaj ustrukturyzowane wartości, nigdy nie interpretuj renderu.

Po co trzymać oba. Bo rzeczywistość jest niechlujna. Czasem siedzisz w edytorze z MCP i get_design_context masz pod ręką. Czasem jesteś w CI z niczym poza tokenem i id node’a. Obie ścieżki zbiegają się w tym samym ParsedElement[], więc wszystko poniżej, mapowanie, spec, generatory, nie wie i nie obchodzi go, którymi drzwiami weszły wartości. Jeden rdzeń, dwa wejścia.

Potem przychodzi część, która w praktyce liczy się najbardziej, czyli to, co dzieje się z surowymi wartościami.

Surowy gap: 16px to nie jest to, co chcesz zamrozić w komponencie. Chcesz gap: var(--space-4), zmapowane z powrotem na token designu, żeby output mówił językiem twojego design systemu, a nie stertą magicznych liczb. loadTokensFromCss czyta dowolny arkusz design-tokenów, rozwiązuje łańcuchy var() i rem na px, i buduje mapy odwrotne od wartości do nazwy tokena.

Słowo klucz to dowolny. Mapowanie tokenów działa przez konfigurowalne wzorce nazw. Dowolny prefiks, zero zahardkodowanych prefiksów. Wyceluj go w swój arkusz stylów, a nauczy się twojej skali. Jest też opcja standardowej skali Tailwind, jeśli nie masz w ogóle pliku z tokenami.

I tu jest linia, w której większość narzędzi po cichu kłamie. Dopasowana wartość renderuje się jako var(--token). Niedopasowana wartość jest raportowana jako warning. Nigdy nie jest zgadywana do tokena, który akurat jest blisko. Kit nie wymyśla nazwy, żeby output wyglądał schludniej. Mówi ci, że nie dopasował, i zostawia decyzję tobie.

Ten warning to nie porażka. To informacja. Niedopasowana wartość to kit mówiący: ta liczba nie jest w twojej skali, więc albo to realny jednorazowy wyjątek, albo twoja skala ma dziurę, a tylko ty wiesz które. Narzędzie, które po cichu zaokrągliłoby ją do najbliższego tokena, okradłoby cię z tego sygnału i robiłoby to gładziej. Gładziej i źle. Wolę uczciwy warning za każdym razem.

Jeden spec, trzy szkielety. Generator B-full renderuje ten sam CodegenSpec do czystego CSS, do Vue SFC, albo do komponentu React z jego CSS. Te same wartości, trzy kształty. Wybierasz framework, wartości się nie zmieniają.

Teraz część, na której zależy mi najbardziej.

RULE ZERO nie jest tu wytyczną. Jest jawnym, wyegzekwowanym, przetestowanym inwariantem. Nieobecna właściwość to null przez cały pipeline, od parsowania do speca. Każda pojedyncza deklaracja przechodzi przez jeden helper o nazwie declare, a declare nie emituje nic dla nieobecnej wartości. Nie ma drogi naokoło.

To znaczy, że generatory są fizycznie niezdolne do wypisania właściwości, której design nie określił. Nie “raczej tego nie zrobią”. Niezdolne. Nie ma żadnego wymyślonego border-radius: 0 na zapełnienie dziury, żadnego oszacowanego margin, o który nikt nie prosił. Mówiąc słowami z Części 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.

Po polsku: Jeśli właściwość jest nieobecna w wyciągniętym designie, jej wartość to zero albo none. Nigdy nie szacuj jej ze zrzutu ekranu. Nieobecne znaczy zero.

Nie miałem nadziei, że to się trzyma. Udowodniłem to. Mieszka w tests/rule-zero.test.ts.

Liczby, bo to jest From the Field i liczby są tu całym sensem.

18 plików źródłowych TypeScript.

Około 2090 linii kodu.

5 plików testowych, 25 testów, wszystkie na zielono.

tsc --noEmit czysto. Build dist czysto.

CLI to jedna komenda. Odpalasz generate z -c albo --design-context dla pliku lub stdin, albo --url dla ścieżki REST. Dodajesz --tokens, żeby wycelować w swój arkusz stylów, albo --tailwind dla standardowej skali. Ustawiasz -n dla nazwy, -f, żeby wybrać css, vue, react albo all, i -o dla katalogu wyjściowego albo --stdout, żeby wypisać.

Jedna uczciwa uwaga o pochodzeniu, bo to ma znaczenie. Generator szkieletów to build napisany od zera. Prywatny pipeline, z którego to destyluję, miał zawsze tylko emiter speca, nigdy generatora. Nie wyjąłem generatora z czegoś prywatnego i nie nakleiłem na to licencji. Zbudowałem deterministyczny generator od zera na publiczne wydanie, a determinizm i RULE ZERO przyszły z nim, bo tak po prostu działa rdzeń.

Jest publiczny. Sklonuj go i odpal dzisiaj.

qa-pack, połowa weryfikująca

Jeśli figma-kit pisze kod z designu, qa-pack sprawdza kod względem designu. Ten sam rdzeń, przeciwny kierunek. Publiczny na licencji MIT.

qa-pack nie jest aplikacją node. To plugin do Claude Code: agenci, komendy i skill qa-methodology. Instalujesz go do projektu, a on testuje zbudowaną implementację względem tego, czym design mówi, że powinna być.

Pierwsza reguła to ta, na której wisi wszystko inne. Referencją jest źródło prawdy, nigdy implementacja. To, jak feature powinien wyglądać, pochodzi ze świeżej ekstrakcji designu, nigdy ze zgadywania ze zrzutu ekranu i nigdy z pamięci, jak kod już się zachowuje. Przebieg QA, który ufa własnym założeniom implementacji, potwierdzi tylko własne bugi implementacji. Więc qa-pack odmawia.

Niesie tę samą dyscyplinę ekstrakcji co figma-kit i ten sam RULE ZERO, słowo w słowo. Jeśli właściwość CSS jest nieobecna w wyciągniętym kodzie designu, jej wartość to zero albo none. Brak klasy rounded znaczy border-radius: 0. Brak klasy cienia znaczy brak cienia. Nigdy nie szacuj ze zrzutów ekranu. Nieobecne równa się zero.

I niesie ten jeden drobny guard z Części 1, ten, który gryzie każdego, kto to buduje. Kiedy qa-pack odczytuje implementację z powrotem przez getComputedStyle, czyta z najgłębszego elementu tekstowego, nie z kontenera. Czytasz kontener i zbierasz wartości dziedziczone i domyślne, które nigdy nie były naprawdę ustawione, a każda z nich staje się false positive. Czytasz liść i dostajesz to, co element faktycznie renderuje. Ten jeden wybór to różnica między raportem pełnym szumu a raportem, na którym możesz działać.

Pokrycie to 13 kategorii, od A do M. Teksty, typografia, kolory, spacing, ramki i radius, layout, ikony, kompletność, stany interaktywne, dostępność, cross-device, skala UX, spójność między komponentami. Każdy raport pokrywa wszystkie 13. Te, które są poza zakresem, są oznaczone jako NOT TESTED z powodem, więc dziury są głośne, nie ciche.

Są dwa tory. Tor A jest oparty na designie: świeża ekstrakcja z Figmy i pełny, pixel-perfect audyt przez całe A do M, mierzony względem wartości designu. Tor B jest funkcjonalny: kryteria akceptacji plus własne konwencje aplikacji, dla pracy, która nie ma referencji w designie. Te same 13 kategorii w obu przypadkach. Tor B po prostu sprawdza względem design systemu i sąsiednich komponentów zamiast względem Figmy.

Pętla to cztery komendy. check uruchamia audyt i zapisuje raport. fix czyta ten raport i prowadzi każdy bug do jego przyczyny w kodzie. retest uruchamia ponownie względem tego samego raportu, żeby potwierdzić, co się zamknęło. full uruchamia całą pętlę od początku do końca. Jest init do ustawienia projektu za pierwszym razem. Raport jest stanem. Każdy przebieg dopisuje się do niego, więc historia tego, co się popsuło i co zostało naprawione, mieszka w jednym pliku.

Ten ostatni detal robi więcej, niż na to wygląda. Bo raport jest append-only, a raport jest stanem, fix i retest nie zgadują, co miał na myśli check. Parsują tę samą ustrukturyzowaną historię przebiegów. Bug znaleziony w jednym przebiegu, przyczyna wytropiona w następnym, potwierdzenie w kolejnym, wszystko w jednym pliku, który możesz przeczytać od góry do dołu. Narzędzie nie trzyma opinii w głowie między przebiegami. Zapisuje je, a potem odczytuje z powrotem. Ten sam instynkt co reszta rdzenia. Nic nie zostaje do zgadnięcia.

Jest publiczny. Sklonuj go i odpal dzisiaj.

o co chodzi

Cofnij się o krok, a dwa narzędzia nie są dwoma pomysłami. Są jednym.

Ta sama deterministyczna ekstrakcja zasila oba. Ten sam RULE ZERO zasila oba. figma-kit czyta design i pisze szkielet. qa-pack czyta design i sprawdza build. Jedno generuje, drugie weryfikuje, a wartości, na których pracują, wychodzą z designu dokładnie tak samo.

Dlatego ufam im z tego samego powodu. Nie dlatego, że kod jest sprytny. Dlatego, że nic w pipeline nigdy nie zgadywało. Wygenerowana właściwość jest tam, bo design ją określił. Oflagowany bug QA to realna rozbieżność od zmierzonej wartości, nie przeczucie na temat zrzutu ekranu. Uczciwy output na obu końcach, z jednego powodu, sprowadzony do jednego rdzenia.

Budujesz jednym. Sprawdzasz drugim. Żadnemu z nich nie wolno wymyślić liczby.

dokąd to idzie

Jest trzecia rzecz, i jest tą oczywistą. Generuj figma-kitem, weryfikuj qa-packiem, a kiedy weryfikacja znajdzie rozbieżność, podaj ją z powrotem do generowania i zamknij pętlę automatycznie. Generuj, weryfikuj, napraw, powtarzaj, aż build pasuje do designu, z niczym zgadywanym na żadnym kroku.

Ta pętla to realna robota i chcę być szczery co do jej statusu. Nie jest dowieziona. Dwie publiczne połówki są. Pełna orkiestracja to następna rzecz, i to też jest miejsce, w którym to przestaje być wygodą dla developera, a zaczyna być case’em biznesowym.

Część 3 to ten case. Dlaczego deterministyczny rdzeń design-to-code jest wart pieniędzy dla zespołu, nie tylko satysfakcjonujący dla inżyniera, i dokąd ta pętla idzie dalej.

From the Field to jest to, co realnie buduję, co się psuje i czego się uczę. Realne projekty, realne liczby, realne bugi. Bez tutoriali.