Skip to content

에이전트 편집 벤치마크

Tabula의 전체 전제는 지역성을 보존하는 스타일링 프로파일이 전통적인 캐스케이드 기반 프로파일보다 AI 에이전트의 편집을 더 신뢰할 수 있게 만든다는 것이다. 이 전제는 이 프로젝트 이전에는 어디에도 직접적으로 뒷받침하는 벤치마크가 없었다(.orchestrator/RESEARCH.md §4, 미해결 질문 1) — benchmark/ 디렉터리는 이를 검증하기 위해 만들어진 도구다.

이 저장소에는 결과가 들어 있지 않다. 아래에 설명된 harness를 실행하는 것은 그 연구를 실행하는 것과 같지 않다; 여기 있는 어떤 것도 어떤 모델에 대한 수치를 산출하는 데 쓰인 적이 없다.

무엇을 측정하는가

SPEC J16의 우선순위 순서에 따른 네 가지 질문:

질문방법
Q1플랫 프로파일이 캐스케이드가 무거운 CSS보다 평범한 에이전트 편집을 더 신뢰할 수 있게 만드는가?hover 상태, 간격, radius, 새로운 variant, 형제 사이에서 스타일 옮기기 등 여덟 개의 편집 태스크 — 두 arm 모두에 동일하게 정의됨.
Q2className 통과(passthrough) 패턴이 새는가?호출 지점에서만 풀 수 있는 네 개의 태스크, 그중 하나는 소비자의 오버라이드가 의도적으로 져야 하며 에이전트가 그 이유를 진단해야 하는 경우를 포함한다.
Q3타이포그래피 basestrict(SPEC J5)오늘은 strict arm에 대한 두 개의 타이포그래피 태스크뿐이며, base 프로파일에 대응하는 것은 스키마에 명시되어 있고 todo로 기록되어 있다. 아직 존재하지 않는 base 프로파일 fixture에 의존한다.
Q4닫힌 어휘 대 등록된 exception목표 값에 기존 토큰이 없는 두 개의 태스크로, 플랫 arm은 하나를 만들어야 하고(tabula except add) 재빌드해야 하지만, 캐스케이드 arm은 그냥 리터럴을 쓰면 된다.

두 arm — arms/flat(Tabula strict 프로파일, 여섯 개의 컴포넌트, 커밋된 .tabula/)와 arms/cascade(전통적인 전역 CSS로 만든 같은 여섯 개의 컴포넌트) — 는 클래스 속성을 벗겨내면 바이트 단위로 동일한 DOM을 렌더링한다(arms/anchors.json, 셀프 테스트로 검사됨); 만약 이 검사가 실패한다면, 이 스위트에서 도출된 어떤 비교도 의미가 없다. 마크업의 차이가 관찰된 편집 성공률 차이를 설명해버릴 수 있기 때문이다.

무엇이 성공으로 간주되는가

결코 스크린샷이 아니고, 결코 사람의 판단이 아니다. 태스크의 성공 기준은 엘리먼트, CSS longhand 프로퍼티, 선택적인 pseudo-class 조건, 그리고 — 결정적으로 — 계산된/렌더링된 값이 아니라 지정된(specified) 값을 명시한다:

json
{ "ref": { "bench": "button.solid" }, "property": "background-color",
  "expectedToken": "color.danger", "condition": "hover" }

그 지정된 값은 각 패러다임 자신의 고유한 메커니즘으로 계산되며, 결코 공유된 재구현으로 계산되지 않는다: 플랫 arm은 실제 @tabula-css/cli 파이프라인으로 토큰에서 자신의 레지스트리를 재빌드하고 @tabula-css/mergeresolve()로 답한다; 캐스케이드 arm의 스타일시트는 PostCSS로 컴파일되며 목적에 맞게 만들어진 캐스케이드 시뮬레이터(문서화되어 있고, 부분집합이 검증됨 — benchmark/DECISIONS.md §3 참고)가 특정성, 소스 순서, !important, 상속에 따라 이기는 declaration을 계산한다.

플랫 arm의 경우, "성공"의 일부로 세 가지 게이트가 더 있다. 이들이 프로파일 자신의 계약의 일부이기 때문이다: strict ESLint 설정이 깨끗하게 유지되어야 하고, 커밋된 .tabula/가 새 빌드와 일치해야 하고, 렌더링된 어떤 엘리먼트도 어휘 밖의 클래스를 갖지 않아야 한다. 올바른 픽셀을 만들어내면서도 프로파일 게이트를 깨뜨리는 편집은 성공한 것이 아니다 — CI라면 그것을 거부할 것이다.

모든 태스크는 또한 mustNotChange 불변식(그래서 다른 무언가를 깨뜨릴 만큼 폭넓은 변경으로 "통과"될 수 없다)과 filesInScope 목록을 갖고 있다 — 그 밖을 편집하면 어느 arm에서든 태스크가 무조건 실패한다.

실행하기

bash
npm run build              # the suite runs against packages' built dist/, not source
npm run test:benchmark      # build + the harness's own self-test suite

node benchmark/dist/run.js list
node benchmark/dist/run.js show q1-01-primary-hover-destructive --arm flat
node benchmark/dist/run.js score q1-01-primary-hover-destructive --arm flat --patch my.json
node benchmark/dist/run.js report results/

score는 패치가 통과하면 0을, 실패하면(정상적이고 유의미한 결과) 1을, harness 자체가 실행될 수 없었다면(형식이 잘못된 패치, 태스크와 무관한 빌드 에러) 2를 반환한다 — Tabula 나머지 도구들과 같은 삼중 종료 코드 관례다.

에이전트 연결하기

에이전트 실행은 의도적으로 이 저장소의 범위 밖이다. 여기 있는 어떤 것도 모델을 호출하거나 API 키를 읽지 않으며, 셀프 테스트는 완전히 오프라인으로 실행된다. 프로토콜은 패치를 넣으면 점수가 나온다는 것이므로, 프롬프트를 패치로 바꿀 수 있는 어떤 harness든 이를 구동할 수 있다:

  1. show <task> --arm <arm> --json은 패킷을 방출한다: 지시문, 범위 안의 모든 파일의 내용, 그리고 그 arm 고유의 에이전트용 컨텍스트 — 플랫 arm이라면 생성된 llms.txt / vocabulary.txt / tokens.resolved.json이고, 캐스케이드 arm이라면 순수한 스타일시트다. 그 패러다임에서는 스타일시트 자체가 문서이기 때문이다. 어느 arm도 상대 arm의 관용구가 자연스럽게 제공하지 않을 것을 받지 않는다.
  2. 그 패킷을 에이전트에게 건네주고 패치를 받는다: {"format":"files","files":{"<path>":"<full content>"}} 형식이거나 유니파이드 diff 중 하나.
  3. score <task> --arm <arm> --patch <file> --out results/는 하나의 TaskResult JSON 파일을 기록한다.
  4. report results/는 디렉터리 안의 모든 결과 파일을 고정폭 텍스트 표로 집계한다:
Tabula agent-editing benchmark — aggregate

scored 24   passed 18   failed 6

question                            flat     cascade
─────────────────────────────────  ───────  ───────
Q1 flat vs cascade edit success      7/8      5/8
Q2 className passthrough             3/4      2/4
Q3 typography strictness             2/2       — 
Q4 vocabulary closure                2/2      1/2
─────────────────────────────────  ───────  ───────
all                                 14/16    8/14

failures by kind
    3  cascade-subset-violation
    2  wrong-value
    1  patch-apply

(이 표의 숫자는 서식을 보여주기 위한 예시일 뿐이다 — 위의 "결과 없음" 노트 참고.)

Claude Code를 headless 모드로 사용하는 실제 작업 루프는 benchmark/README.md § Plugging in an agent에 있다. 의미 있는 비교를 위해 고정해야 할 두 가지: 두 arm에 같은 에이전트, 예산, 시도 횟수를 주어라, 그리고 한 arm의 관용구가 자연스럽게 제공하지 않을 컨텍스트를 다른 arm에 추가하지 말라 — show 명령의 컨텍스트 목록은 정확히 그 원칙으로 선택된 것이다.

안전

점수 매기기는 패치된 arm의 코드를 실행한다 — 렌더러를 실제로 돌리지 않고서는 렌더링된 DOM을 관찰할 방법이 없고, 정적 클래스 추출은 여기서 통하지 않는다(variants() 설정과 cn() 호출은 실제 클래스 문자열을 리터럴 소스 텍스트가 아니라 props의 함수로 만든다). 패치는 에이전트가 작성한 코드다; 신뢰할 수 없는 패치는 컨테이너 안에서 실행하라. 각 채점은 benchmark/.work/(gitignore 됨) 아래의 arm 사본에 대해 실행된다 — arms/에 커밋된 fixture는 결코 기록 대상이 되지 않는다 — 그리고 패치 경로는 어떤 기록이든 일어나기 전에 검증된다(절대 경로, .. 세그먼트, 워크스페이스 탈출은 모두 거부된다).

알려진 한계

  • 미디어 쿼리는 꺼져 있다, 결정에 의해서 — 어떤 태스크도 breakpoint에 관한 것일 수 없다.
  • 단일 axis 상태. 모든 기준은 기본 테마 아래에서 평가된다; 다중 axis 기준은 현재 태스크 스키마로는 표현할 수 없다.
  • 두 arm은 곳에 따라 서로 다른 프로퍼티 이름을 사용한다(padding-inline-startpadding-left) — 기준은 각 arm 고유의 관용구로 작성되어 있다; 물어보는 질문은 같지만, 철자는 다르다.
  • Q2 진단 fixture의 결함(지고 있는 className 오버라이드)은 cn() 호출을 재정렬하는 것이 아니라 바인딩 이름을 바꾸는 방식으로 표현되는데, 이는 특별히 린터에게 보이지 않게 남아 있도록 하기 위해서다 — 이 태스크는 순수한 진단 능력을 측정하는 것이지, 프로파일 자신의 도구가 그 버그를 대신 잡아주었을지를 측정하는 것이 아니다.

강제된 모든 설계 선택의 전체 기록과, 각각이 이긴 대안, 그 이유 — 캐스케이드 arm이 그 안에 머물도록 만들어진 정확한 캐스케이드 시뮬레이터의 부분집합까지 포함해서 — 는 benchmark/DECISIONS.md를 참고하라.

Released under the MIT License.