エージェントサーフェス
Tabula は、「AI コーディングエージェントが推測することなくこのプロジェクトを正しく使える」ということを、後付けの配慮ではなく、ビルドの要件として扱います。以下の成果物はすべて tabula build によって生成されます――どれも手でメンテナンスされているものはないため、手書きのスタイルガイドのようにレジストリからずれていくことがありません。
llms.txt と llms-full.txt
.tabula/llms.txt は短縮版です:十戒(concepts.md を参照)に加えて、何を知るにはどのファイルを読めばよいかの地図です。これは 2048 バイトが上限です――生成されたファイルがその予算を超える場合、tabula build は例外を投げます。そのため、モデルがシステムプロンプトの中で確実に注意を払える範囲を、静かに超えて肥大化することは決してありません。セッションの開始時に一度だけ読んでください:
# Tabula — flat styling profile for reference-ui@1
## The ten laws
1. Only classes in vocabulary.txt exist; anything else emits NO CSS, silently. ...
...
## Files (.tabula/)
- vocabulary.txt — every legal class. READ BEFORE WRITING A CLASS.
- tokens.resolved.json — every literal, per axis. READ BEFORE CHOOSING A VALUE.
- registry.json — class → declarations, slots, rank. Ground truth.
- llms-full.txt — full reference (vocabulary, merge, bans)..tabula/llms-full.txt は長文版で、安定した ## の見出しの下に整理されています。そのため、検索(retrieval)のステップがファイル全体ではなく1つのセクションだけを取り出すことができます:## The ten laws, with the reason each exists(十戒とその存在理由)、## The merge algorithm(マージのアルゴリズム、手順を追った手作業での説明)、## Worked examples(具体例)、## Vocabulary — <family>(クラスファミリーごとに1セクション、各クラスの実際の宣言と説明)、## Banned mechanisms(禁止されたメカニズム)、## Error codes(すべての TAB-Exxx/TAB-Wxxx とその原因・修正方法)、そして ## Exceptions — do not imitate these(例外――これらを真似しないこと)です。
MCP サーバー
@tabula-css/mcp(バイナリ名 tabula-mcp)は、プロジェクトの .tabula/ 成果物に対する読み取り専用の stdio サーバーです。前回のビルド以降にトークンや設定が変更されていた場合、すべてのツール呼び出しを STALE_REGISTRY という結果(修正コマンド付き)で拒否します。そのため、エージェントがもはやプロジェクトを表していないレジストリからの回答を受け取ることは決してありません。すべてのレスポンスは { profileVersion, sourceHash, stale, staleCheck? } を持ちます――最後のフィールドが何を意味し、エージェントがなぜそれを読まなければならないかについては 陳腐化(staleness)と成果物のみのチェック を参照してください。
| Tool | Answers |
|---|---|
resolve_classes | 「この要素は実際にはどう見えるか?」――クラス文字列に対する完全なローカルスタイリングモデル:基本の宣言、条件付きの帯域、アンビエントプロパティ、アトミック/未知のクラス、グループ依存関係。 |
preview_merge | 「この cn() 呼び出しは何を生成するか?」――マージ後の文字列に加え、脱落したすべてのクラス、それを覆い隠したもの、そしてどの CSS プロパティ上でのことかを返す。 |
find_class_for | 意図による逆引き――ここで最も価値の高いツール。{ intent: "raised card background" } → bg-surface-raised。実際のクラス名、ファミリー、宣言された値、トークンの説明にマッチする。類義語テーブルや曖昧なスコアリングは決して使わない。 |
get_tokens | すべてのトークン(または名前空間/部分文字列でのスライス)を、tokens.resolved.json から直接返す。 |
get_vocabulary | クラスの完全な一覧を、ページ分割して返す(静かに切り詰められることはなく、hasMore/pages フィールドが明示的にそれを示す)。 |
explain_ban | あるクラスが禁止・未登録である理由を、代替手段とともに返す――単なる「見つかりません」を返すことは決してない。それこそがエージェントを任意の値の捏造へと押しやるものだからだ。 |
find_group_marker | 名前付きの group/<name> マーカーがどこで宣言されているかを返す。これにより、要素をまたぐ依存関係はツリーの走査ではなく、ルックアップになる。 |
check_classes | 事前チェック:クラス属性を書く前に呼び出す。未知のクラス(「もしかして」の提案付き)、禁止されたメカニズム、同一文字列内でのスロット衝突を報告する。 |
resolve_element | { file, line, col } で JSX 要素を指し示すと、その要素自身の解決済みクラス、同一ファイル内の祖先要素から継承しているテキストコンテキスト、そして参照している各 group/peer がそのファイル内にマーカーを持っているかどうかを返す。コンポーネントの境界では、次に開くべきファイルとともに status: "unknown" を返す――コンポーネントが何を描画するかを決して推測しない。 |
validate_source | ソース文字列を、あなたのレジストリに対する実際の strict ESLint 設定でリントする。AST を見ているため、check_classes が構造的にできないことを捕捉する:実行時のクラス構築、className の順序、group/peer の構造、継承境界、例外のスコープなど。インラインの eslint-disable コメントは無視されるため、スニペットが言い訳でクリーンな判定を得ることはできない。 |
propose_exception | 提案された例外を実際のトークンバリデータで検証し、パッチを返す――何も書き込まない。要求された値の 5% 程度以内に既存のトークンがあれば、そちらを使うよう促す。 |
propose_token | 通常のトークンについて同じことを行う――トークンは永続的な語彙であり、例外は有効期限付きの書類仕事であるため、まず先に試すべきはこちらである。あなたの実際のトークンドキュメントに継ぎ足し、実際のバリデータを実行して検証するため、不完全な軸マップや無意味な値は、ビルド時ではなくここで拒否される。パッチを返す――何も書き込まない。 |
doctor | ヘルスチェック:陳腐化、ドリフト、期限切れ間近の例外、エスケープバジェット――古い状態であっても答える。なぜなら、陳腐化を報告するツール自身を「古い」という理由で拒否してしまうと、何がずれたのかを決して言えなくなるからだ。 |
explain | TAB-Exxx/TAB-Wxxx コードの原因と修正方法を、@tabula-css/core に凍結されたカタログから返す。 |
すべてのツールは、例外なく1つのルールに従います:読み取った値ではなく推論した値を返すことは決してしない。 未知のクラスは提案とともに報告され、静かに修正されることは決してありません。ナッジの許容範囲を外れた値は、間違った推測ではなく、沈黙で応じられます。このサーフェス全体が防ごうとしている失敗とは、モデルが一度も調べたことのないもっともらしい値を自信満々に出力してしまうことです――推測を行う MCP ツールは、ツール呼び出しという権威を背負って、その失敗を再生産してしまうことになります。
頼る前に知っておくべき、2つの限界。
resolve_elementは同一ファイル内の解析であり、それを取り繕うことなくそう明言します。コンポーネントの祖先、あるいはクラス文字列が実行時に組み立てられる祖先に行き当たると、その走査はstatus: "unknown"と、次に行うべき具体的なステップとともに終了します。それが正直な答えです:<Card>が何を描画するかは、それを使っているファイルからは知りえないことであり、推測するツールはまさに重要な場面で間違えることになります。またaxisValuesは完全に省略されます――ある要素がどの軸の組み合わせの下で描画されるかは、ソースではなく実行中のドキュメントについての事実だからです。そのため、そうでないことを示唆するフィールド名の下でデフォルトの組み合わせの数値を引用するのではなく、明示的なaxes引数を伴うresolve_classesを使うよう案内します。
validate_sourceにはeslintと@typescript-eslint/parserのインストールが必要です(これらは@tabula-css/mcpのオプションのピア依存です)。これらがない場合はLINTER_UNAVAILABLEとインストールコマンドを返します――ソースのリントを装った、レジストリのみの部分的なチェックを返すことは決してありません。その回答にはruleCountも含まれるため、判定が空の設定からではなく、N 個の実際のルールから来たことを確認できます。ruleCount: 0でok: trueはただの形だけの承認になってしまうため、これはそれを可視化するためのものです。
陳腐化と成果物のみのチェック
陳腐化はすべてのリクエストごとに再計算されます(成果物のフィンガープリントが変わると、ホストは .tabula/ を読み直します)。そのため、編集してから再ビルドするというループは安全です:トークンを編集し、tabula build を実行し、再度質問するエージェントは、再起動なしに同じサーバーから新しい答えを得られます。
このチェックは3つの脚から成ります:
- 自己整合性 ――
manifest.inputsHashとregistry.sourceHashの比較:これらの成果物は1回のビルドで書き出されたものか? - 完全性 ――各成果物の sha256 とマニフェストのダイジェストの比較:どれかが手で編集されたか、あるいは書き込みの途中でビルドが中断されたか?
- ソースドリフト ――ディスク上のトークンと設定を再ハッシュしたものと、
manifest.inputsHashの比較。*「トークンが編集されたが、一度も再ビルドされていない」*という、よくあるケースを捕捉できるのは、この脚だけです。
第3の脚は、インストールされている tailwindcss と @tabula-css/* パッケージのバージョンを解決する必要があります。それらが入力ハッシュの一部だからです。そのいずれかが解決できない場合、第3の脚は実行されません。 これは、分離された、あるいは pnpm 形式の node_modules レイアウトの場合や、プロジェクトのソースがサーバーの作業ディレクトリから読み取れない場合に起こります。
サーバーはこれを取り繕いません。第3の脚がスキップされた場合、すべてのレスポンスは次を含みます:
{ "profileVersion": "…", "sourceHash": "…", "stale": false, "staleCheck": "artifacts-only" }エージェントが staleCheck: "artifacts-only" を見たときにすべきこと: stale: false を、通常よりも弱い証拠として扱ってください。これが意味するのは「これらの成果物は内部的に整合しており、改変されていない」ということであり、「現在のトークンファイルと一致している」ということを意味しません。前回のビルド以降に編集されたトークンは検知されず、得られる回答は、新鮮であると主張しながらも、編集前のプロファイルを説明することになります。この状態で値を信頼する前に、tabula build(あるいは、ドリフトがあれば終了コード 1 で終わり、何も書き込まない tabula build --check)を実行してから、再度質問してください。このフィールドが存在しないことこそが、強い根拠です:第3の脚が実行され、stale: false はレジストリがソースと一致していることを意味します。
AGENTS.md.snippet
.tabula/AGENTS.md.snippet は、プロジェクトの AGENTS.md や CLAUDE.md にそのまま貼り付けられるブロックです:llms.txt と同じルールを、直接的な指示として言い換えたものに加え、登録済みクラスのリアルタイムな件数(examples/reference-ui では Only classes in .tabula/vocabulary.txt exist (477 of them))を含みます。これを貼り付けることが、統合作業のすべてです――tabula build のたびに再生成されるため、語彙が増えても減っても、手作業でのメンテナンスは一切不要です。
目に見える形で失敗するという保証
未登録のクラスは、デフォルトでは決して実行時エラーにはなりません――ビルドの source(none) + @source inline(...) という構成により、Tailwind は閉じた語彙の外にあるものを一切スキャンしないため、未登録のクラスは単にCSS を生成しないだけです。これは意図的なものであり、cn() が環境によって異なる振る舞いをする理由でもあります:
- 開発時(
isDev()が true):cn()は未知のクラスに対して、レーベンシュタイン距離による最も近い登録済みの名前とともに例外を投げます(TAB-E300)――タイプミスは、呼び出し箇所で、即座に、大きな音を立ててローカルビルドを失敗させます。 - 本番環境:
cn()は決して例外を投げません。未知のクラスは不透明なまま保持されます――スロットを一切所有せず、脱落することもなく、他のクラスを覆い隠すこともありません――そして、実際のレンダリングをクラッシュさせることなく可視化できるよう、console.errorで一度だけログに記録されます。また、開発中に捕捉できるよう<TabulaAudit>(開発時専用の DOM 適合性チェッカー)向けにも記録されます。
同じ非対称性は dyn()(承認されたインラインスタイルのエスケープハッチ)にも当てはまります:未登録の --d-* キーは開発時には例外を投げ(TAB-E304)、本番環境では静かに破棄されます。このプロファイルの中で、開発時に目に見えない形で失敗するものは何もありません――重要なゲート(未知のクラス、マージの健全性、未登録の動的プロパティ)はすべて、開発者やエージェントが実際に目にする場所で、大きな音を立てて例外を投げます。