数据截至 (上游 commit 9ed9049a14d9)
UCP — Schema 变体机制与文档工具链
本章给“要改 schema / 写规范文档”的人。它讲 UCP 一个很聪明的工程决定:不为每种操作各写一份 schema,而是一份 schema 配注解,按需派生变体——以及围绕这套注解建的文档构建宏与示例校验门禁。
1. 它要解决的小问题
同一个 checkout 对象,在不同场景下“哪些字段必填”是不同的:
- 创建请求要
line_items,但不该带id(还没创建)、不该带status(由商家定)。 - 完成请求要
payment,但不该带line_items(已锁定)。 - 响应要带
id/status/totals,但这些字段在请求里都该省略。
若为每种组合写一份 schema,会爆炸且难维护。UCP 的答案是注解。
2. 核心机制:ucp_request 注解
在一份基础 schema 里,每个字段用 ucp_request 标注它在各操作下的角色。看 checkout 真实 schema(source/schemas/shopping/checkout.json:28-141):
{
"properties": {
"id": { "type": "string", "ucp_request": "omit" },
"status": { "type": "string", "ucp_request": "omit" },
"line_items": { "ucp_request": { "create": "required", "update": "required", "complete": "omit" } },
"payment": { "ucp_request": { "create": "optional", "update": "optional", "complete": "required" } }
}
}
读法:
"ucp_request": "omit"—— 该字段是纯响应字段,所有请求里都省略(如id、status、totals)。"ucp_request": { "create": "required", "complete": "omit" }—— 按操作分别规定角色。- 字段上甚至有
"ucp_request": "omit"直接挂在ucp这个$ref旁(source/schemas/shopping/checkout.json:19-22),说明请求里整个ucp块都省。
这套注解也用在扩展里:discount 的 applied 字段标 ucp_request: "omit"(输出专用),而 discounts 字段在 create/update 是 optional、complete 时 omit。source/schemas/shopping/discount.json:93-141。
注意区分两层注解:
ucp_request是请求方向的字段角色;而 ucp.json 里那套platform_schema/business_schema/response_schema(02 章)是注册表上下文的变体。两者正交。
3. 谁来解析注解:ucp-schema
注解本身不是 JSON Schema 标准——需要一个工具把它**解析(resolve)**成某个具体操作/方向的标准 schema。这个工具是外部 Rust crate ucp-schema(不在本克隆内),本仓库通过子进程调用它。
本仓库两处真实调用,是理解其契约的最佳证据:
- 文档构建期:
main.py的_load_schema_variant把页面上下文(io_type=request/response,operation_id)映射成direction+operation,再调ucp-schema resolve。映射逻辑见main.py:281-301(如 op_id 含 "create" → operation="create",含 "complete" → "complete")。 - 示例校验期:
scripts/validate_examples.py的resolve_schema调ucp-schema resolve … --{direction} --op {op} --bundle(scripts/validate_examples.py:637-672),validate_payload调ucp-schema validate(scripts/validate_examples.py:680-726)。
所以 ucp-schema 的职责(inferred,依据这两处调用方式而非其源码):吃带 ucp_request 注解的基础 schema + --op/--direction → 吐出剥离注解、 字段必填性已按该操作定好的标准 JSON Schema。
4. 文档构建宏:把 schema 变成表格
UCP 的规范文档不手写字段表——而是用 MkDocs 宏从 schema 现拉。main.py 的 define_env 注册了几个宏(main.py:213、1102、1193、1399):
| 宏 | 干什么 | 定义处 |
|---|---|---|
schema_fields(entity, spec) | 渲染某实体的字段表(自动按页面 io_type 取变体) | main.py:1102-1103 |
extension_schema_fields(ref, spec) | 渲染带 #/$defs/... 片段的实体表 | main.py:1193-1194 |
method_fields(op_id, file, spec) | 从 OpenAPI/OpenRPC 渲染某操作的请求/响应表 | main.py:1398-1399 |
在 checkout 规范页里就能看到这些宏被直接写进 Markdown,例如 {{ schema_fields('checkout_resp', 'shopping/checkout') }}(docs/specification/shopping/checkout/index.md:1033)、{{ method_fields('create_checkout', 'rest.openapi.json', 'shopping/checkout') }}(docs/specification/shopping/checkout/index.md:1061)。构建时它们被替换成表格。
这意味着:改 schema,文档表格自动更新——schema 是单一事实源。
5. 示例校 验门禁:schema 漂移直接断 CI
UCP 把规范里每个 ```json 代码块都拿去对 schema 校验。机制是给每个块前加一行 HTML 注释注解(不渲染):
<!-- ucp:example schema=PATH [op=OP] [direction=DIR] [extract=JSONPATH] [target=JSONPATH] [def=NAME] -->
<!-- ucp:example skip reason="..." -->
未加注解的 json 块直接判失败。来源:docs/documentation/schema-authoring.md:670-714、校验器实现 scripts/validate_examples.py 模块 docstring §15-108。
校验器是个三层管线(scripts/validate_examples.py:74-88、process_block §875-1067):
Layer 1 表面语法 作者写的“增强 JSON”
(行注释 //、{{ ucp_version }}、HTTP 信封、省略号 ... 标记)
│ reduce_to_canonical_json:四步纯文本归约
▼
Layer 2 规范 JSON json.loads 能解析(省略号留作哨兵)
│
▼
Layer 3 语义校验
① extract= 选子树 ② 记录被省略路径 ③ 深合并进 scaffold(已知合法夹具)
④ coverage 走查:每个必填字段要么出现、要么被省略号显式确认
⑤ ucp-schema validate;被省略路径上的报错被抑制
几个值得学的细节:
- 省略号有语义。
[ ... ]/{ ... }表示“非空容器、内容省略”,coverage 仍核验该字段被确认存在,但不对其值报错。docs/documentation/schema-authoring.md:788-827、scripts/validate_examples.py:374-420。 - scaffold(夹具)补全。 示例只展示关注字段,校验器把它深合并进一个已知合法的 scaffold(
scripts/scaffolds/*.json)再校验,所以作者不必写全。scripts/validate_examples.py:501-518、783-806。 - 未知注解属性直接拒。 打错
shema=会被_KNOWN_ATTRS拦下而非静默忽略——typo 守卫。scripts/validate_examples.py:135-171。 - skip 理由要可 grep。 不能校验的块用
skip reason="..."并给精确分类(如"A2A transport binding"),用来追踪“还没覆盖什么”。docs/documentation/schema-authoring.md:840-860。
触发面三层(docs/documentation/schema-authoring.md:974-988):pre-commit 改了的 doc 只校验改动文件;改了 source/schemas/ 或校验器本身则跑全量(因为一处 schema 改动可能波及多篇文档的示例);CI 无条件全量兜底。
6. 巧妙之处(可借鉴)
- 巧:一份 schema 派生多变体。 用
ucp_request注解避免了 N 份近似 schema 的维护噩梦,且让“某字段在创建时必填、完成时省略”这种规则就近声明在字段上,可读性极高。source/schemas/shopping/checkout.json。 - 巧:文档即 schema 的投影。 宏让字段表永不和 schema 脱节;校验器让示例永不和 schema 脱节——“规范漂移”从“可能悄悄发生”变成“直接断 CI”。
main.py、scripts/validate_examples.py。 - 巧:增强 JSON 但在线格式仍严格。 作者写带
//注释和省略号的“增强 JSON”图清楚,校验器先归约成严格 RFC 8259 JSON 再校验——在线格式保持诚实。scripts/validate_examples.py:46-72。 - 巧:把校验内务藏起来。 注解活在 HTML 注释里、scaffold 和校验 schema 在仓库内务目录,规范读者只看到协议散文和 JSON。
docs/documentation/schema-authoring.md:907-933。
7. 边界
- 校验器的
//注释剥离是按行近似的:字符串里含转义反斜杠后跟//会误判(已知边界,当前语料无触发)。scripts/validate_examples.py:90-98。 - 只认一个模板变量
{{ ucp_version }};其余{{ name }}会留进 json.loads 并报错(故意的)。scripts/validate_examples.py:302-308。 - 不支持内层省略号
[a, ..., b]、尾逗号、块注释、JSON5。scripts/validate_examples.py:56-60。
8. 代码地图
| 主题 | 文件 | 符号 |
|---|---|---|
ucp_request 注解(真值) | source/schemas/shopping/checkout.json | 各 properties.*.ucp_request |
| 注解约定(作者契约) | docs/documentation/schema-authoring.md | “Schema Variants”、“ucp_request” |
| 变体解析(调 ucp-schema) | main.py | _load_schema_variant、_resolve_schema |
| 文档宏 | main.py | define_env、schema_fields、extension_schema_fields、method_fields |
| 示例校验管线 | scripts/validate_examples.py | reduce_to_canonical_json、strip_ellipsis、check_coverage、process_block |
| 注解解析 / typo 守卫 | scripts/validate_examples.py | parse_annotation、_KNOWN_ATTRS |
| scaffold 夹具 | scripts/scaffolds/ | *_request_create.json 等 |
| CI 门禁 | docs/documentation/schema-authoring.md | “What runs automatically” §786-814 |