Poppy 草案先读能力声明、落地实现再谈兼容边界

文章导读
拿到 Poppy 草案后,卡住的通常不是“怎么实现”,而是顺序:先按草案文字把能力声明和约束条件摘出来,落成可校验的配置检查,再在兼容层里显式记录草案要求与自己实现选择之间的差异,最后用本地用例把支持、不支持、部分支持三条边界跑一遍。草案是描述,不是成品;没有写清的行为应标为待确认,而不是默认接受。
📋 目录
  1. 一 在 Poppy 草案里摘出能力声明和约束条件
  2. 二 用通用伪代码搭一个声明读取与校验骨架
  3. 三 在兼容层记录草案要求与实现选择的差异
  4. 四 用一组本地用例验证兼容边界
  5. 五 把未定义行为标为待确认而不是默认放行
A A

拿到 Poppy 草案后,卡住的通常不是“怎么实现”,而是顺序:先按草案文字把能力声明和约束条件摘出来,落成可校验的配置检查,再在兼容层里显式记录草案要求与自己实现选择之间的差异,最后用本地用例把支持、不支持、部分支持三条边界跑一遍。草案是描述,不是成品;没有写清的行为应标为待确认,而不是默认接受。

Poppy 草案的落地顺序建议固定为:读声明 → 转配置检查 → 记兼容差异 → 跑本地边界用例。能力声明只当作输入约束,不当作接口契约;兼容层里 strict 与 loose 两种模式都要留下接受和拒绝日志,用来反推边界;草案未定义的部分进入待确认清单,默认拒绝或忽略,等草案更新或上游确认后再放开。适用场景是拿到草案文本、配置和日志可控的自建实现,风险边界是不要把实现补丁写成协议要求。

在 Poppy 草案里摘出能力声明和约束条件

草案文本里通常混着三类句子:能力描述(支持什么)、约束条件(取值范围、必填项、组合限制)、以及尚未定论的表述。摘录时先把可验证的句子挑出来,凡是需要靠猜才能补全的句子,先不写进实现清单。

建议按下面这张表逐条填写,一行只对应一个可独立验证的点。表格填不满说明声明还没读透,此时不该进入编码。

能力约束缺失后果验证方式
<能力名,如字段/方法/模式>必填字段、取值域、互斥组合实现只能靠默认值猜测,边界不可控草案原文能否找到对应句子;配置校验能否报出缺失
<可选能力>依赖哪个必填能力才生效单独开启时行为未定义开启/关闭两次跑本地用例,观察日志差异
<扩展位>草案只说“保留”或未提及被当成已定义字段直接放行归入待确认清单,不作为实现依据

用通用伪代码搭一个声明读取与校验骨架

骨架的目标只有一个:把能力声明翻译成配置层的检查,不绑定任何具体接口名。字段名全部用占位符,替换成草案里的实际词即可。

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 两种模式把差异显式化。

Poppy 草案先读能力声明、落地实现再谈兼容边界
# 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_fieldunknown_key、decision

跑用例时用本地的入口脚本加占位参数,例如 <entry> `--config` <file> `--mode` strict,替换成你自己的程序名和参数即可。观察点不在“跑没跑通”,而在输出里能否定位到具体字段和判定规则。部分支持这一行最容易出问题:很多实现会把它当成支持直接放行。

Poppy 草案先读能力声明、落地实现再谈兼容边界

把未定义行为标为待确认而不是默认放行

草案里“保留”“暂不定义”“后续补充”这类表述,落地时建议一律进待确认清单,默认拒绝或忽略,不做静默放行。清单模板可以这样写:

unconfirmed:
  - id: UC-01
    topic: "<草案未定义的行为>"
    evidence: "<草案原文位置或“未提及”>"
    current_action: reject      # reject | ignore,不放行
    impact: "<影响哪些用例>"
    owner: "<跟进人>"
    review_trigger: "草案更新 / 上游声明变更"

回归验证步骤建议固定为四步:第一,把待确认项对应的本地用例标记为“预期拒绝”;第二,改动实现后重跑全部用例,对比退出码和 decision 字段;第三,检查日志里有没有原本 reject 的记录变成 accept 且没有原因字段;第四,草案更新时逐条复核待确认清单,确认后再放开,并补一条能覆盖新行为的用例。这样做的好处是边界始终写在配置和日志里,而不是散在代码注释和口头约定中。