エージェントによる編集ベンチマーク
Tabula の前提全体は、局所性を保つスタイリングプロファイルの方が、従来のカスケードベースのものよりも、AI エージェントの編集をより信頼できるものにする、というものです。この前提を直接裏付けるベンチマークは、このプロジェクト以前にはどこにも存在しませんでした(.orchestrator/RESEARCH.md §4、未解決の問い1)――benchmark/ ディレクトリは、それを検証するために構築された道具です。
このリポジトリには結果が一切含まれていません。 以下に説明するハーネスを実行することは、研究そのものを実行することとは異なります。ここにあるものは、いかなるモデルについての数値を生成するためにも使われていません。
何を測定するか
4つの問い、SPEC J16 の優先順位順です:
| Question | How | |
|---|---|---|
| Q1 | フラットなプロファイルは、カスケードの多い CSS よりも、通常のエージェントの編集をより信頼できるものにするか? | 8つの編集タスク――ホバー状態、間隔、角丸、新しいバリアント、兄弟要素間でのスタイルの移動――を、両方のアームに対して同一に定義。 |
| Q2 | className パススルーのパターンは漏れるか? | 呼び出し箇所からしか解けない4つのタスク。そのうち1つは、消費者側のオーバーライドが意図的に負けるべきであり、エージェントはその理由を診断しなければならない。 |
| Q3 | タイポグラフィの base 対 strict(SPEC J5) | 現時点では strict アームに対する2つのタイポグラフィタスク。base プロファイルの対応版はスキーマ上で規定されており、まだ存在しない base プロファイルのフィクスチャを待って todo として記録されている。 |
| Q4 | 閉じた語彙 対 登録済みの例外 | 対象の値に既存のトークンがない2つのタスク。フラットなアームは新たにトークンを発行し(tabula except add)、再ビルドしなければならないが、カスケードのアームは単にリテラルを書けばよい。 |
両方のアーム――arms/flat(Tabula の strict プロファイル、6つのコンポーネント、コミットされた .tabula/)と arms/cascade(同じ6つのコンポーネントを、伝統的なグローバル CSS で実装したもの)――は、クラス属性を取り除くと、バイト単位で同一の DOM を描画します(arms/anchors.json、セルフテストによってチェックされます)。もしこのチェックが一度でも失敗すれば、このスイートから得られるいかなる比較も意味を持たなくなります。マークアップの違いが、観測される編集成功率の違いを説明してしまうことになるからです。
何をもって成功とするか
スクリーンショットは決して使わず、人間による判断も決して使いません。 タスクの成功基準は、要素、CSS のロングハンドプロパティ、任意の疑似クラス条件、そして――決定的に――計算後/描画後の値ではなく、指定された (specified) 値を名指しします:
{ "ref": { "bench": "button.solid" }, "property": "background-color",
"expectedToken": "color.danger", "condition": "hover" }指定された値は、それぞれのパラダイム自身の仕組みによって計算され、共有の再実装によって計算されることは決してありません:フラットなアームは、実際の @tabula-css/cli パイプラインを使ってトークンからレジストリを再ビルドし、@tabula-css/merge の resolve() を介して答えます。カスケードのアームのスタイルシートは PostCSS でコンパイルされ、目的に特化して構築されたカスケードシミュレータ(文書化されており、サブセットがチェック済み――benchmark/DECISIONS.md §3 を参照)が、詳細度・ソース順序・!important・継承によって勝利する宣言を計算します。
フラットなアームについては、さらに3つのゲートが「成功」の一部になります。それらはプロファイル自身の契約の一部だからです:strict の ESLint 設定はクリーンなままでなければならず、コミットされた .tabula/ は新規ビルドと一致しなければならず、描画されるどの要素も語彙の外にあるクラスを持ってはなりません。正しいピクセルを生み出しながらプロファイルのゲートを破る編集は、成功したことにはなりません――CI がそれを拒否するはずだからです。
すべてのタスクは mustNotChange という不変条件も持ちます(そのため、他の何かを壊してしまうほど広い変更によってタスクが「合格」になることはありません)。また filesInScope の一覧も持ちます――その外を編集すると、どちらのアームでもタスクは即座に失敗します。
実行する
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(これは正常で、有益な結果です)、そしてハーネス自体が実行できなかった場合(不正な形式のパッチ、タスクとは無関係のビルドエラー)は 2 で終了します――Tabula の他のツール群と同じ、3通りの終了コードの慣習です。
エージェントを接続する
エージェントの実行は、意図的にこのリポジトリのスコープ外です。 ここにあるものは、モデルを呼び出したり API キーを読んだりすることは一切なく、セルフテストは完全にオフラインで実行されます。プロトコルはパッチを入れて、スコアを出すというものであり、プロンプトをパッチに変換できるハーネスであれば何でもこれを駆動できます:
show <task> --arm <arm> --jsonは、パケットを出力します:指示、スコープ内のすべてのファイルの内容、そしてそのアーム自身のエージェント向けコンテキスト――フラットなアームであれば、生成されたllms.txt/vocabulary.txt/tokens.resolved.jsonであり、カスケードのアームであれば普通のスタイルシートです。そのパラダイムでは、スタイルシートこそがドキュメントだからです。どちらのアームにも、その流儀が自然には提供しないものは一切与えられません。- そのパケットをエージェントに渡し、パッチを回収します:
{"format":"files","files":{"<path>":"<full content>"}}の形式か、あるいは unified diff です。 score <task> --arm <arm> --patch <file> --out results/は、1つのTaskResultの JSON ファイルを書き出します。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 をヘッドレスモードで用いた具体的なループの例は benchmark/README.md § Plugging in an agent にあります。何かを語ろうとする比較であれば、固定しておくべきことが2つあります:両方のアームに同一のエージェント、予算、試行回数を与えること、そして一方のアームにだけ、もう一方の流儀が自然には供給しないコンテキストを追加しないこと――show コマンドのコンテキストの一覧は、まさにその原則に基づいて選ばれています。
安全性
スコアリングはパッチを当てたアームのコードを実行します――レンダラーを実行せずに描画後の DOM を観測する方法はなく、静的なクラス抽出はここでは機能しません(variants() の設定や cn() の呼び出しは、実際のクラス文字列を、リテラルなソーステキストではなく props の関数にしてしまいます)。パッチはエージェントが書いたコードです。信頼できないパッチはコンテナの中で実行してください。 各スコアリングは、benchmark/.work/(gitignore 対象)配下にある、そのアームの新しいコピーに対して実行されます――arms/ 内のコミット済みのフィクスチャに書き込まれることは決してありません――そして、パッチのパスはあらゆる書き込みの前に検証されます(絶対パス、.. セグメント、ワークスペースからの脱出はすべて拒否されます)。
既知の限界
- メディアクエリは対象外です。これは決定によるものです――ブレークポイントに関するタスクは一切ありません。
- 単一の軸の状態のみ。 すべての基準はデフォルトのテーマの下で評価されます。複数の軸にまたがる基準は、現在のタスクスキーマでは表現できません。
- アームによって、箇所ごとに異なるプロパティ名を使っています(
padding-inline-start対padding-left)――基準はそれぞれのアーム自身の流儀で書かれています。問われている問いは同じですが、綴りは同じではありません。 - Q2 の診断フィクスチャの欠陥(負けてしまう
classNameのオーバーライド)は、cn()の呼び出しの順序を変えるのではなく、バインディングの名前を変えることによって表現されています。これは、リンタからは見えないままにするためです――このタスクは純粋な診断能力を測るものであり、プロファイル自身のツールがそのバグを代わりに捕捉してくれたかどうかを測るものではありません。
強制された設計上の選択のすべて、それぞれが打ち破った代替案、そしてその理由についての完全な記録――カスケードのアームがその内側にとどまるよう構築されている、カスケードシミュレータの正確なサブセットを含む――については benchmark/DECISIONS.md を参照してください。