拿到 Poppy 草案后,卡住的通常不是“怎么实现”,而是顺序:先按草案文字把能力声明和约束条件摘出来,落成可校验的配置检查,再在兼容层里显式记录草案要求与自己实现选择之间的差异,最后用本地用例把支持、不支持、部分支持三条边界跑一遍。草案是描述,不是成品;没有写清的行为应标为待确认,而不是默认接受。
Poppy 草案的落地顺序建议固定为:读声明 → 转配置检查 → 记兼容差异 → 跑本地边界用例。能力声明只当作输入约束,不当作接口契约;兼容层里 strict 与 loose 两种模式都要留下接受和拒绝日志,用来反推边界;草案未定义的部分进入待确认清单,默认拒绝或忽略,等草案更新或上游确认后再放开。适用场景是拿到草案文本、配置和日志可控的自建实现,风险边界是不要把实现补丁写成协议要求。
在 Poppy 草案里摘出能力声明和约束条件
草案文本里通常混着三类句子:能力描述(支持什么)、约束条件(取值范围、必填项、组合限制)、以及尚未定论的表述。摘录时先把可验证的句子挑出来,凡是需要靠猜才能补全的句子,先不写进实现清单。
建议按下面这张表逐条填写,一行只对应一个可独立验证的点。表格填不满说明声明还没读透,此时不该进入编码。
| 能力 | 约束 | 缺失后果 | 验证方式 |
|---|---|---|---|
| <能力名,如字段/方法/模式> | 必填字段、取值域、互斥组合 | 实现只能靠默认值猜测,边界不可控 | 草案原文能否找到对应句子;配置校验能否报出缺失 |
| <可选能力> | 依赖哪个必填能力才生效 | 单独开启时行为未定义 | 开启/关闭两次跑本地用例,观察日志差异 |
| <扩展位> | 草案只说“保留”或未提及 | 被当成已定义字段直接放行 | 归入待确认清单,不作为实现依据 |
用通用伪代码搭一个声明读取与校验骨架
骨架的目标只有一个:把能力声明翻译成配置层的检查,不绑定任何具体接口名。字段名全部用占位符,替换成草案里的实际词即可。
# declaration.yaml(占位符示例)
capabilities:
- name: <feature_a>
required_fields: [<field_1>, <field_2>]
optional_fields: [<field_3>]
constraints:
<constraint_key>: <value_range_or_enum>
def load_declaration(path):
raw = read_text(path)
try:
decl = parse(raw)
except ParseError as e:
log.error("parse failed at %s", e.position)
raise
errors, warnings = [], []
known_keys = {"capabilities", "constraints"}
for key in raw.keys():
if key not in known_keys:
warnings.append("unknown key: %s" % key) # 记录,不静默丢弃
for cap in decl.get("capabilities", []):
for f in cap.get("required_fields", []):
if f not in cap:
errors.append("cap %s missing %s" % (cap["name"], f))
if errors:
log.error("declaration invalid: %s", "; ".join(errors))
raise ConfigError(errors)
for w in warnings:
log.warn("declaration warning: %s", w)
return decl, warnings
两个验证点要落到行为上:缺必填字段时进程必须报错退出,而不是用默认值继续跑;出现未知字段时必须写入日志,方便判断草案版本是否已经变化。这套骨架放在启动路径或构建期检查里都可以,关键是同一份声明在两种路径下给出同样的结论。
在兼容层记录草案要求与实现选择的差异
兼容层最常见的错误是把实现补丁写成协议要求:草案没说的宽松处理,被后来的人当成必须遵守的规则。建议用 strict 与 loose 两种模式把差异显式化。
# compat.yaml(占位符示例)
compat:
mode: strict # strict | loose
on_unknown_field: reject # reject | record
on_missing_optional: default
log_accept: true
log_reject: true
note: "<此处写本次实现偏离草案的原因>"
strict 模式下未知字段直接拒绝,loose 模式下只记录并继续;两种模式都要在日志里留下同一条记录的两个字段:判定结果(accept / reject)和原因(原因里带上触发字段名)。验证方式是用同一份输入分别跑两种模式,对比日志中的接受和拒绝记录;如果两份日志的差异无法解释,说明兼容层里混进了没有声明的行为。
用一组本地用例验证兼容边界
边界至少要覆盖三类:草案明确支持的、明确不支持的、以及只部分支持的。用例不需要连外部服务,本地跑通即可。
| 用例 | 输入构造 | 期望行为 | 命令输出应观察的字段 |
|---|---|---|---|
| 完全支持 | 字段齐全且取值在约束内 | 接受并继续 | 退出码、level、mode、matched_rule |
| 明确不支持 | 草案声明不支持的能力 | 拒绝并给出原因 | 退出码、reason、field |
| 部分支持 | 必填齐全、可选字段超出取值域 | 按模式决定,strict 拒绝、loose 记录 | mode、decision、warn_count |
| 未知字段 | 加入草案未定义的键 | 记录或拒绝,取决于 on_unknown_field | unknown_key、decision |
跑用例时用本地的入口脚本加占位参数,例如 <entry> `--config` <file> `--mode` strict,替换成你自己的程序名和参数即可。观察点不在“跑没跑通”,而在输出里能否定位到具体字段和判定规则。部分支持这一行最容易出问题:很多实现会把它当成支持直接放行。
把未定义行为标为待确认而不是默认放行
草案里“保留”“暂不定义”“后续补充”这类表述,落地时建议一律进待确认清单,默认拒绝或忽略,不做静默放行。清单模板可以这样写:
unconfirmed:
- id: UC-01
topic: "<草案未定义的行为>"
evidence: "<草案原文位置或“未提及”>"
current_action: reject # reject | ignore,不放行
impact: "<影响哪些用例>"
owner: "<跟进人>"
review_trigger: "草案更新 / 上游声明变更"
回归验证步骤建议固定为四步:第一,把待确认项对应的本地用例标记为“预期拒绝”;第二,改动实现后重跑全部用例,对比退出码和 decision 字段;第三,检查日志里有没有原本 reject 的记录变成 accept 且没有原因字段;第四,草案更新时逐条复核待确认清单,确认后再放开,并补一条能覆盖新行为的用例。这样做的好处是边界始终写在配置和日志里,而不是散在代码注释和口头约定中。