Skip to content

智能体接口(agent surface)

Tabula 把“一个 AI 编码智能体无需猜测即可正确使用这个项目”当作一项构建需求,而不是事后补上的想法。 下面的每一份产物都由 tabula build 生成——没有一份是手工维护的,因此它们都不可能像一份手写的风格指南那样与注册表产生漂移。

llms.txtllms-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 是长版本,被组织在稳定的 ## 标题之下,方便一次检索只取出一个章节,而不必读整份文件: ## The ten laws, with the reason each exists## The merge algorithm(手工逐步演算)、## Worked examples## Vocabulary — <family>(每个 class 族群一个章节,列出每个 class 真实的声明与描述)、## 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? }——最后一个字段的含义、以及为什么智能体必须去读它, 见 过期性检查与仅限产物的检查方式

工具回答什么
resolve_classes“这个元素实际长什么样?”——一个 class 字符串的完整局部样式模型:基础声明、条件带、环境属性、原子/未知类、已声明的组依赖。
preview_merge“这次 cn() 调用会产生什么?”——合并后的字符串,外加每一个被丢弃的 class、是什么盖过了它、以及在哪个 CSS 属性上。
find_class_for按意图做反向查找——这里最有价值的工具。{ intent: "raised card background" }bg-surface-raised。匹配的是真实的 class 名称、族群、已声明的取值与设计令牌描述;绝不是一张同义词表或一个模糊评分。
get_tokens每一个设计令牌(或一个命名空间/子串切片),直接来自 tokens.resolved.json
get_vocabulary完整的 class 列表,分页返回(绝不会被静默截断——一个 hasMore/pages 字段会明确说明这一点)。
explain_ban为什么一个 class 被禁用或未注册,并附带替代方案——绝不是一句干巴巴的“未找到”,那正是会把智能体推向去发明一个任意值的原因。
find_group_marker一个具名的 group/<name> 标记声明在哪里,因此一个跨元素的依赖关系变成一次查找,而不是一次遍历整棵树。
check_classes预检:在写下一个 class 属性之前调用。报告未知的 class(附带“你是不是想写”的建议)、被禁用的机制,以及同一字符串内的槽位冲突。
resolve_element通过 { file, line, col } 指向一个 JSX 元素,得到它自身已解析的 class、它从同文件祖先那里继承来的文本上下文,以及它引用的每一个 group/peer 是否在该文件里有对应的标记。在一个组件边界处,它会返回 status: "unknown" 以及下一个该打开的文件——它从不猜测一个组件会渲染出什么。
validate_source用真实的 strict ESLint 配置对照你的注册表来 lint 一段源码字符串。能捕捉到 check_classes 在结构上无法捕捉的东西,因为它看到的是 AST:运行时 class 构造、className 顺序、group/peer 结构、继承边界、例外作用域。内联的 eslint-disable 注释会被忽略,因此一段代码片段无法靠注释话术换来一个干净的判定。
propose_exception通过真实的设计令牌校验器验证一项拟议的例外,并返回一份补丁——什么都不写。如果有一个设计令牌在所请求取值的约 5% 以内,也会推动你转而使用它。
propose_token对普通设计令牌做同样的事——这是你应当优先尝试的方案,因为一个设计令牌是永久性的词汇,而一个例外是带有到期日的临时文书。它通过拼接进你真实的设计令牌文档、并运行真实的校验器来验证,因此一份不完整的轴映射或一个无意义的取值会在这里就被拒绝,而不是等到构建时才被拒绝。返回一份补丁——什么都不写。
doctor健康检查:过期性、漂移、即将过期的例外、逃逸预算——即便处于过期状态也会作答,因为如果过期性报告器因为自身过期而拒绝回答,就永远说不出到底是什么发生了漂移。
explain一个 TAB-Exxx/TAB-Wxxx 代码的成因与修复方式,取自 @tabula-css/core 中冻结的目录。

每一个工具都毫无例外地遵循一条规则:绝不返回一个推断出来、而非读取出来的值。 一个未知的 class 会被附带一条建议来报告, 而绝不会被悄悄纠正;一个超出容差范围的取值会得到沉默的回应,而不是一个错误的猜测。这整个接口存在的意义, 就是要防止一个模型自信地吐出一个它从未真正查证过的、看似合理的取值——一个会猜测的 MCP 工具,只会以工具调用的权威性, 把这种失败重新制造出来。

在依赖它们之前,有两个限制值得了解。

resolve_element 是一次同文件分析,并且会如实说明这一点,而不是把这个限制掩盖过去。一个组件祖先, 或者一个 class 字符串在运行时才拼出来的祖先,都会让这次遍历以 status: "unknown" 外加一个具名的下一步结束。 这才是诚实的答案:<Card> 会渲染成什么,从使用它的这个文件里是无法得知的,一个靠猜测的工具恰好会在这些真正要紧的情形里出错。 它还完全省略了 axisValues——一个元素在运行时处于哪种轴组合之下,是关于正在运行的文档的一个事实,而不是关于源码的事实, 所以它会指引你转而使用带有显式 axes 参数的 resolve_classes,而不是在一个名字暗示别的含义的字段下, 报出默认组合的数字。

validate_source 需要安装 eslint@typescript-eslint/parser(它们是 @tabula-css/mcp 的可选 peer 依赖)。 如果没有安装,它会返回 LINTER_UNAVAILABLE 以及安装命令——而绝不会把一次仅基于注册表的局部检查包装成一次源码 lint。 它的答案还携带 ruleCount,这样你就能看出这个判定确实来自 N 条真实的规则,而不是来自一份空配置; ok: true 配上 ruleCount: 0 会是一次形式主义的橡皮图章,而这正是让这一点变得可见的原因。

过期性检查与仅限产物的检查方式

过期性会在每一次请求时重新计算(宿主会在 .tabula/ 的指纹发生变化时重新读取它),因此“编辑—重建”这个循环是安全的: 一个智能体编辑了一个设计令牌、运行了 tabula build、再次提问,会从同一个服务器得到新的答案,而无需重启。

这项检查有三条腿:

  1. 自洽性——manifest.inputsHashregistry.sourceHash 的比对:这些产物是否由同一次构建写出的?
  2. 完整性——每个产物的 sha256 与清单摘要的比对:是否有一个被手动编辑过,或者一次构建在写入过程中被中断了?
  3. 源码漂移——磁盘上的设计令牌与配置,重新哈希后与 manifest.inputsHash 的比对。这是唯一能捕捉到 *“一个设计令牌被编辑过、但从未重建过”*这种情形的一条腿——也是最常见的情形。

第三条腿需要解析出已安装的 tailwindcss@tabula-css/* 系列包的版本,因为它们本身也是输入哈希的一部分。 当其中任何一个无法被解析时,第三条腿就不会运行。 这会发生在一种隔离式或 pnpm 风格的 node_modules 布局下, 或者当项目源码从服务器的工作目录中无法读取时。

服务器不会把这一点掩盖过去。当第三条腿被跳过时,每一个响应都会携带:

json
{ "profileVersion": "…", "sourceHash": "…", "stale": false, "staleCheck": "artifacts-only" }

当一个智能体看到 staleCheck: "artifacts-only" 时应当怎么做:stale: false 当作一个比平时更弱的证据来对待。 它的含义是“这些产物内部是自洽的、且未被修改过”——它并不意味着“它们与当前的设计令牌文件相符”。一个自上次构建之后 被编辑过的设计令牌不会被检测出来,而你得到的答案会描述编辑之前的那份配置档案,却宣称自己是新鲜的。在信任这种状态下的 一个取值之前,先运行 tabula build(或者 tabula build --check,它会在存在漂移时以退出码 1 结束、且不写入任何文件), 再重新提问。这个字段的缺席才是更有力的情形:意味着第三条腿运行过了,而 stale: false 就意味着注册表与源码相符。

AGENTS.md.snippet

.tabula/AGENTS.md.snippet 是一段可以直接粘贴进你项目 AGENTS.mdCLAUDE.md 的现成代码块: 与 llms.txt 相同的规则,被表述为直接的指令,外加已注册 class 的实时计数(在 examples/reference-ui 中是 Only classes in .tabula/vocabulary.txt exist (477 of them))。粘贴它就是整个集成步骤——它会随每一次 tabula build 被重新生成,因此随着词汇表的增长或收缩,它永远不需要人工维护。

失败可见性保证

默认情况下,一个未注册的 class 从不会是一个运行时错误——构建产生的 source(none) + @source inline(...) 设置意味着 Tailwind 永远不会去扫描封闭词汇表之外的任何东西,因此一个未注册的 class 只会不产生任何 CSS。这是刻意为之的设计, 也是 cn() 会因环境而表现不同的原因:

  • 开发环境isDev() 为 true):cn() 会对一个未知 class 抛出异常,并附带按 Levenshtein 距离找到的最接近的 已注册名称(TAB-E300)——一个笔误会在调用现场立刻、响亮地让你本地的构建失败。
  • 生产环境cn() 从不抛出异常。一个未知的 class 会被保留为不透明的——它不占用任何槽位,从不会被丢弃, 也从不会遮蔽另一个 class——并通过 console.error 记录一次日志,这样它就能在不使一次真实渲染崩溃的前提下被看见。 它也会被记录给 <TabulaAudit>(一个仅在开发环境下存在的 DOM 一致性检查器),以便在开发期间被捕捉到。

同样的不对称性也适用于 dyn()(受核准的内联样式逃生舱口):一个未注册的 --d-* 键会在开发环境下抛出异常 (TAB-E304),在生产环境下则被静默丢弃。这份配置档案里没有任何东西会在开发环境下不可见地失败——每一道要紧的门禁 (未知 class、合并可靠性、未注册的动态属性)都会在一个开发者或智能体真正会看到的那个位置,大声地抛出异常。

Released under the MIT License.