동결(Eject) — 되돌릴 수 없는 동결
EXPERIMENTAL
tabula eject(돌아올 수 없는 동결과 --report 역매핑 분석 모두)는 v0.3.0에서 실험적으로 제공된다: 명령은 동작하고 테스트되어 있지만, 그 표면 — 플래그, 리포트 형식, 티어 표 — 은 향후 마이너 릴리스에서 바뀔 수 있다. GitHub 이슈를 통한 피드백을 환영한다.
tabula eject는 검증된 .tabula/를 프로젝트 소유의 디렉터리로 복사하고, 그 디렉터리로 흘러들어가는 토큰 파이프라인을 멈춘다. 리서치(그리고 .orchestrator/plan.md에 기록된 탐침들)는 .tabula/source.css가 클래스 변경 없이 순정 @tailwindcss/cli로 컴파일된다는 것을 확립했다 — 그래서 동결은 재작성이 아니라 얼림이다: 배포한 그대로의 CSS를 유지하고, cn()을 유지하며, 생성기를 포기한다.
이는 마이그레이션 없이 프로파일에서 벗어나고 싶은 날을 위해 존재한다: 유지보수 단계로 들어가는 프로젝트, Tabula를 채택하지 않을 팀으로의 인수인계, 또는 수년 뒤에도 Tailwind만으로 빌드되어야 하는 아카이브.
이것은 돌아올 수 없는 문이다. eject 이후에는 다시 빌드하는 일도,
tabula build --check드리프트 게이트도, 동결된 복사본 위의tabula scan폐쇄성 게이트도 없다.tokens/안의 토큰 변경은 더 이상 거기에 닿지 않는다. 되돌아가려면 동결된 디렉터리를 지우고 소스로부터 다시 빌드해야 한다 — 반대 방향으로는 아무것도 흐르지 않는다. eject가 기본값으로 드라이 런인 것은 정확히 이 문을 건너는 일이(--yes라는) 의도적인 두 번째 단계가 되도록, 결코 사고가 되지 않도록 하기 위해서다.
명령
tabula eject [--to <dir>] [--yes] [--force] [--write-imports]--to <dir>— 동결 대상. 기본값은 프로젝트 루트 아래의tabula-frozen/이다.--yes— 실행한다. 없으면 eject는 드라이 런이다: 전체 계획 — 복사할 모든 파일, 권한 변경, 발견된@import재작성, 만료 경고, 돌아올 수 없는 문 배너,cn()정책 안내 — 을 출력하고 아무것도 건드리지 않는다.--force— 이미 파일이 있는 대상 디렉터리를 허용한다(그렇지 않으면 비어 있지 않은 대상은 거부된다).--write-imports—.tabula/source.css를 가리키는 프로젝트 CSS의@import줄을 동결된 복사본을 가리키도록 다시 쓴다. 없으면 eject는 발견한 파일과 정확한 새 import 줄을 출력만 하므로, 직접 그 변경을 적용할 수 있다.
전제 조건 (순서대로 폐쇄적으로 실패한다)
- 로드 가능한 프로젝트 — 루트에
tabula.config.json+tokens/*.tokens.json(그렇지 않으면TAB-E901). 프로젝트 루트에서 eject를 실행하라. - 기존의 검증된
.tabula/— 매니페스트가 존재해야 하고, 입력이 여전히 그것으로 해시되어야 하며(오래되지 않음), 모든 아티팩트의sha256이 매니페스트와 일치해야 하고, 매니페스트가 커버하지 않는 것이 디렉터리 안에 숨어 있어서는 안 된다. 이는tabula doctor가 쓰는 것과 같은 매니페스트-해시 드리프트 게이트이며, 같은 코드를 재사용한다:TAB-E303(누락/오래됨)과TAB-E601(손편집 또는 커버되지 않음). eject는 오직 검증된 상태만 동결한다 — 드리프트되었거나 오래된.tabula/는 거부되는데, 그것을 동결하면 더 이상 토큰과 일치하지 않는 것을 동결하는 셈이 되기 때문이다. - 사용 가능한 대상 —
--force가 주어지지 않는 한 대상 디렉터리는 존재하지 않거나 비어 있어야 한다(TAB-E240). 쓸 수 없는 대상도 마찬가지로TAB-E240이다.
네 가지 위험 요소, 그리고 eject가 그것들을 다루는 방법
동결은 미묘하게 잘못되기 쉽다. 네 가지 함정이 미리 못박혔고, 각각은 우연에 맡겨지는 대신 명시적으로 다뤄진다.
1. cn()은 여전히 @tabula-css/merge를 필요로 한다 — tailwind-merge로 바꾸지 말 것
동결된 CSS는 순정 Tailwind이지만, 런타임은 그렇지 않다. cn()은 여러분의 registry.json 으로부터 클래스 충돌을 리졸브한다; tailwind-merge는 여러분의 어휘를 알지 못하므로, cn()을 교체하면 렌더링된 출력이 달라진다. concepts.md의 반례:
cn("pt-sm", "p-md") // Tabula → "p-md" (p-md is later and covers padding-top)
cn("pt-sm", "p-md") // tailwind-merge → "pt-sm p-md" (keeps pt-sm — different CSS)그래서 동결된 디렉터리는 registry.json을 유지하며, EJECTED.md는 @tabula-css/merge를 유지하라고 문서로 알려준다. 병합 런타임을 조용히 교체하라고 암시하는 것은 아무것도 없다.
2. 동결된 복사본은 여러분의 것이며(0644), 읽기 전용 아티팩트(0444)가 아니다
.tabula/ 아래의 아티팩트는 **읽기 전용(0444)**으로 기록되고 매니페스트로 게이팅되어, 에디터가 생성된 파일 위에 조용히 저장할 수 없다. 동결된 복사본은 그 반대다: 이제 여러분의 파일이다. eject는 프로젝트 소유의 디렉터리 안에 모든 복사본을 0644로 쓰므로, 더 이상 실행되지 않는 권한 비트나 드리프트 검사와 씨름하지 않고 편집할 수 있다.
3. 만료된 예외는 경고할 뿐, 막지 않는다
평소에는 만료된 예외가 강한 빌드 오류(TAB-E141)다 — 닫힌 어휘가 조용히 열려버리는 것을 막는 메커니즘이다. 하지만 eject 이후에는 다시 빌드하는 일이 없으므로, 그 오류는 다시는 발동할 수 없다. 그것을 근거로 동결을 막는 것은 이미 eject가 취소해버린 미래의 재빌드를 근거로 막는 셈이 된다. 대신 eject는 만료됐거나 90일 이내에 만료될 모든 예외에 대해 TAB-W402로 예외마다 경고하며, EJECTED.md는 모든 예외를 그 상태와 함께 나열하므로, 그 빚을 동결하는 것은 물려받는 뜻밖의 일이 아니라 눈으로 보고 내리는 선택이 된다.
4. 돌아올 수 없는 문은 놓칠 수 없다
모든 실행 — 드라이 런과 실제 실행 모두 — 은 토큰 변경이 흐름을 멈추고 다시 빌드하는 일이 없다는 것을 명시하는 배너를 출력한다. 드라이 런 기본값은 무언가 쓰이기 전에 항상 전체 계획을 보게 됨을 뜻한다.
계속 동작하는 것과 멈추는 것
계속 동작하는 것:
- 런타임의
cn()—@tabula-css/merge와 복사된registry.json으로(위험 요소 1). types.d.ts— 클래스 이름 타입이 CSS와 함께 동결된다; 에디터 자동완성과 클래스 문자열의 타입 검사는 바뀌지 않는다.llms.txt/llms-full.txt/AGENTS.md.snippet— 에이전트 서피스도 함께 복사되므로, 동결된 디렉터리를 읽는 어시스턴트는 여전히 어휘와 규칙을 얻는다.- 순정 Tailwind 위에서의 컴파일 —
source.css는@tailwindcss/cli로 직접 빌드된다.
멈추는 것:
- 다시 빌드하기 —
tabula build는 더 이상 동결된 디렉터리를 대상으로 하지 않는다; 토큰 편집이 거기에 닿지 않는다. - 토큰 변경의 흐름 — 파이프라인이 끊어진다; 동결은 한 시점의 스냅샷이다.
- scan과 드리프트 게이트 —
tabula scan폐쇄성과build --check드리프트는 더 이상 동결된 복사본을 지배하지 않는다. 이제 평범한 프로젝트 CSS다.
동결한 뒤에
동결된 디렉터리에 기록되는 EJECTED.md는 출처(프로파일 id, 버전, 입력 해시), 돌아올 수 없는 문 진술, cn() 정책 안내, 전체 예외 목록, 그리고 검증 명령을 기록한다. 동결이 순정 Tailwind로 컴파일되는지 확인하려면:
npx @tailwindcss/cli -i tabula-frozen/source.css -o out.css--write-imports를 사용했다면, 앱 스타일시트의 @import는 이미 tabula-frozen/source.css를 가리킨다; 그렇지 않다면 eject가 붙여넣을 정확한 줄을 출력했다.
역매핑 리포트 (--report)
Eject는 Tabula의 CSS를 동결하고 렌더링을 정확히 유지한다. 다른 질문은 이것이다: 내 클래스 사용 중 얼마나 많은 부분이 대신 순정(vanilla) Tailwind v4로 옮겨갈 수 있을까? tabula eject --report는 이 질문에 정직하게 답한다. 이는 분석 모드다 — 동결도 없고, .tabula/에 대한 쓰기도 없으며, 항상 종료 코드 0을 낸다 — 스캔된 소스에서 실제로 사용된 클래스를 동결된 역매핑 표에 대조하여 네 가지 티어로 분류한다:
| 티어 | 의미 | --write? |
|---|---|---|
| A | 순정에도 동일한 철자가 존재하고 방출되는 선언이 동등하다 — 툴체인을 바꿔도 렌더링이 바뀌지 않는다. | 필요 없음 |
| B | 증명 가능하게 1:1이며 렌더링을 보존하는 이름 변경(예: opacity-disabled → opacity-50). | 예 |
| C | 매핑은 존재하지만 렌더링이나 의미가 달라진다 — 정확한 주의 사항과 함께 리포트로만 남는다. | 결코 없음 |
| D | 순정에 대응물이 없다(type-* 타이포그래피 번들) — 동결된 CSS를 유지하거나 손으로 재설계해야 한다. | 결코 없음 |
변형 체인은 가장 약한 부분을 기준으로 분류된다: Tabula가 :where()로 낮추는 모든 자기-상태 (self-state) 변형(hover, focus, active, …)은 티어 C다 — 그 특정성(specificity)은 (0,1,0)인 반면 순정의 것은 (0,2,0)이고, hover는 추가로 순정의 @media (hover:hover) 게이트를 잃는다 — 그래서 기반이 아무리 이식 가능해도 실제 체인은 모두 C에 놓인다.
tabula eject --report [--theme-port] [--format=json] [--write]--report— 프로젝트 옆에tabula-eject-report.md를 쓴다(그리고 stdout에도 그대로 출력한다): 헤더 판정, 티어별 개수, 파일별 작업 목록(file:line, 클래스, 티어, 대상-또는-주의사항), 티어-D 목록, 그리고 상위 주의 사항들.--format=json은 같은 데이터를 JSON 문서로 방출한다.--theme-port— with-theme-port 시나리오를 리포트하고 네임스페이스 포트 안내를 추가한다. 아래 두 시나리오를 참고하라.--write—tabula migrate와 같은 코드모드 엔진을 통해 티어-B 이름 변경만 적용한다 (실제 유니파이드 diff, 클래스-싱크만). 그 외 나머지는 구조적으로 리포트 전용이다. 적용할 것이 없는 채로--write를 주면0 rewrites를 출력하고 종료 코드 0을 낸다.
두 가지 시나리오
Tabula의 theme.css는 --tb-* 커스텀 프로퍼티만 정의하고, source.css는 순정의 기본 테마 위에서 동작한다. 그래서 bg-accent나 p-lg 같은 이름 있는 토큰 유틸리티는 유효한 순정 유틸리티 형태이지만, 순정은 --color-accent / --spacing-lg를 읽는데 Tabula는 이를 채운 적이 없다:
- as-is — eject하고, 철자는 유지하되, 테마 변수는 전혀 포트하지 않는다. 토큰 유틸리티는 아무것도 방출하지 않거나(색상/간격/크기) 순정의 기본값으로 리졸브된다(
rounded-md는 Tabula의0.5rem이 아니라0.375rem이 된다). 이 계열들은 as-is에서 티어 C다. - with-theme-port (
--theme-port) — Tabula의 토큰 값을 순정 네임스페이스로 복사한다 (--tb-color-*→--color-*,--tb-spacing-*→--spacing-*,--tb-radius-*→--radius-*). 같은 계열들이 티어 A가 된다.
포트가 모든 계열을 구제하는 것은 아니다. 물리적 대 논리적 간격 계열(px, mx, inset-x, scroll-px, border-x — Tabula에서는 논리적 인라인, 순정에서는 물리적 좌/우이므로 LTR에서는 동일하지만 RTL에서는 뒤집힌다)과 ring-* / shadow-* 색상 유틸리티(Tabula에서는 비활성인 --tb-ring-color가 순정에서는 활성 --tw-ring-color가 된다 — 링을 렌더링하지 않던 요소가 갑자기 링을 렌더링할 수 있다)는 두 시나리오 모두에서 티어 C로 남는다.
정직한 수치 (reference-ui)
examples/reference-ui 프로젝트에서 측정한 값이다(기본 클래스 478개 + 변형 체인 1131개 = 토큰 1609개, 레지스트리에 대해 jq로 검증됨 — .orchestrator/R2.4-reverse-map.md):
| 시나리오 | A | B | C | D |
|---|---|---|---|---|
| as-is | 83 | 11 | 1511 | 4 |
| with-theme-port | 405 | 11 | 1189 | 4 |
오직 토큰 11개(0.7%) — 스칼라 토큰 이름 변경인 opacity-disabled, duration-fast, ease-standard, border-{t,b,y,s,e}-thin, align-start, align-end, not-prose — 만이 --write에 안전하며, 이 집합은 두 시나리오에서 동일하다(테마 포트를 해도 늘어나지 않는다). 모든 변형 체인은 두 시나리오 모두에서 C다. 이 명령이 구현하는 결론은 의도적이다: 완전한 순정 마이그레이션이 산출물이 아니라 — 정직한 리포트가 산출물이다 — 그리고 렌더링을 정확히 유지하는 방법은 여전히 (CSS를 동결하는) tabula eject다.
동결 대 복원
동결(Ejecting)은 출력을 얼린다; .tabula/에서 프로파일 복원하기는 소스를 재구성한다. 이 둘은 반대 방향이다: 생성기 사용을 멈추고 CSS를 유지하고 싶다면 동결하라; 계속 생성할 수 있도록 토큰을 되찾고 싶다면 복원하라.