#!/usr/bin/env python3 # -*- coding: utf-8 -*- """play-loop 契约校验器(PlaySpec C5 / VerdictFeedback C6)。 零依赖:只用 Python 标准库(json/re/sys/pathlib)。之所以不引 jsonschema/ajv——本仓 没装 jsonschema、也没 vendor ajv,而 W-ARCH① 红线③要求零新依赖;且两份契约的消费方 (cheap-worker gate_judge.py、tier2 run.py)本就是 Python,落一份 stdlib 校验器与消费侧同栈。 本文件自带一个 JSON-Schema Draft-2020-12 子集校验器,只实现两份 schema 真正用到的关键字: type / required / properties / patternProperties / additionalProperties / enum / const / pattern / minimum / maximum / minItems / maxItems / minLength / maxLength / items / anyOf / oneOf / allOf / if-then-else / $ref(仅 #/$defs/ 本地引用)。 关键字集是刻意收窄的:schema 由本任务作者,只用上述关键字,正负样本套件即本校验器的回归证明—— 若某关键字实现错了,要么正样本会被误拦、要么负样本会漏过,跑套件即暴露。 用法: 1) 单点校验:python3 validate.py [ ...] 逐样本打印 PASS/FAIL 与错误路径;全部合规 exit 0,否则 exit 1。 2) 套件校验:python3 validate.py --suite [samples 根目录,默认脚本旁 samples/] 走 samples//valid(应全 PASS)与 samples//invalid(应全 FAIL), 目录名对应同名 schema(play-spec → play-spec.schema.json)。 打印每契约『正样本 N 过 / 负样本 M 拦』与总计;期望全部满足 exit 0,否则 exit 1。 """ from __future__ import annotations import json import re import sys from pathlib import Path _HERE = Path(__file__).resolve().parent # ── JSON 类型判定(区分 integer/number/boolean:Python 里 bool 是 int 子类,须显式排除)── def _json_type_ok(value, t: str) -> bool: if t == "object": return isinstance(value, dict) if t == "array": return isinstance(value, list) if t == "string": return isinstance(value, str) if t == "boolean": return isinstance(value, bool) if t == "integer": return isinstance(value, int) and not isinstance(value, bool) if t == "number": return isinstance(value, (int, float)) and not isinstance(value, bool) if t == "null": return value is None return False def _resolve_ref(ref: str, root: dict): """解析本地 $ref(仅支持 #/$defs/ 形态;其余抛错,防静默漏校验)。""" if not ref.startswith("#/"): raise ValueError(f"仅支持本地 #/$defs 引用,收到:{ref}") node = root for part in ref[2:].split("/"): # JSON Pointer 转义还原(~1→/,~0→~),本仓 key 无特殊字符,兜个底而已。 part = part.replace("~1", "/").replace("~0", "~") if not isinstance(node, dict) or part not in node: raise ValueError(f"$ref 指向不存在的节点:{ref}") node = node[part] return node def validate(schema, instance, root: dict, path: str = "#") -> list: """按 schema 校验 instance,返回错误串列表(空=合规)。schema 为 dict 或 bool。""" # 布尔 schema:true=恒过,false=恒否(本仓未用,兜底)。 if isinstance(schema, bool): return [] if schema else [f"{path}: 布尔 schema false,恒不合规"] if not isinstance(schema, dict): return [f"{path}: schema 非法(既非对象也非布尔)"] errors: list = [] # $ref:解析后就地校验(本仓 $ref 节点自带完整约束,直接接管本层)。 if "$ref" in schema: target = _resolve_ref(schema["$ref"], root) return validate(target, instance, root, path) # type if "type" in schema: types = schema["type"] types = types if isinstance(types, list) else [types] if not any(_json_type_ok(instance, t) for t in types): errors.append(f"{path}: 类型应为 {types},实际 {type(instance).__name__}") # const / enum if "const" in schema and instance != schema["const"]: errors.append(f"{path}: 应恒等于 {schema['const']!r},实际 {instance!r}") if "enum" in schema and instance not in schema["enum"]: errors.append(f"{path}: 应属枚举 {schema['enum']},实际 {instance!r}") # 字符串约束 if isinstance(instance, str): if "minLength" in schema and len(instance) < schema["minLength"]: errors.append(f"{path}: 字符串长度应 ≥ {schema['minLength']},实际 {len(instance)}") if "maxLength" in schema and len(instance) > schema["maxLength"]: errors.append(f"{path}: 字符串长度应 ≤ {schema['maxLength']},实际 {len(instance)}") if "pattern" in schema and re.search(schema["pattern"], instance) is None: errors.append(f"{path}: 应匹配正则 /{schema['pattern']}/,实际 {instance!r}") # 数值约束(排除 bool) if isinstance(instance, (int, float)) and not isinstance(instance, bool): if "minimum" in schema and instance < schema["minimum"]: errors.append(f"{path}: 应 ≥ {schema['minimum']},实际 {instance}") if "maximum" in schema and instance > schema["maximum"]: errors.append(f"{path}: 应 ≤ {schema['maximum']},实际 {instance}") # 数组约束 if isinstance(instance, list): if "minItems" in schema and len(instance) < schema["minItems"]: errors.append(f"{path}: 数组元素数应 ≥ {schema['minItems']},实际 {len(instance)}") if "maxItems" in schema and len(instance) > schema["maxItems"]: errors.append(f"{path}: 数组元素数应 ≤ {schema['maxItems']},实际 {len(instance)}") if "items" in schema: for i, item in enumerate(instance): errors += validate(schema["items"], item, root, f"{path}/{i}") # 对象约束 if isinstance(instance, dict): for key in schema.get("required", []): if key not in instance: errors.append(f"{path}: 缺必填字段 '{key}'") props = schema.get("properties", {}) pattern_props = schema.get("patternProperties", {}) add_props = schema.get("additionalProperties", True) for key, val in instance.items(): handled = False if key in props: errors += validate(props[key], val, root, f"{path}/{key}") handled = True for pat, sub in pattern_props.items(): if re.search(pat, key) is not None: errors += validate(sub, val, root, f"{path}/{key}") handled = True if not handled: if add_props is False: errors.append(f"{path}: 不允许的额外字段 '{key}'(additionalProperties=false)") elif isinstance(add_props, dict): errors += validate(add_props, val, root, f"{path}/{key}") # 组合子:allOf / anyOf / oneOf for i, sub in enumerate(schema.get("allOf", [])): errors += validate(sub, instance, root, f"{path}/allOf[{i}]") if "anyOf" in schema: branch_errs = [validate(sub, instance, root, f"{path}/anyOf[{i}]") for i, sub in enumerate(schema["anyOf"])] if not any(len(e) == 0 for e in branch_errs): errors.append(f"{path}: anyOf 无一分支匹配(各分支首错:" + ";".join(e[0] for e in branch_errs if e) + ")") if "oneOf" in schema: matched = sum(1 for sub in schema["oneOf"] if len(validate(sub, instance, root, path)) == 0) if matched != 1: errors.append(f"{path}: oneOf 应恰好 1 分支匹配,实际 {matched}") # 条件子:if-then-else(if 本身不产错,命中则套 then、否则套 else) if "if" in schema: if len(validate(schema["if"], instance, root, path)) == 0: if "then" in schema: errors += validate(schema["then"], instance, root, f"{path}/then") else: if "else" in schema: errors += validate(schema["else"], instance, root, f"{path}/else") return errors def _load(p: Path): return json.loads(p.read_text(encoding="utf-8")) def _validate_file(schema: dict, sample_path: Path) -> list: """校验单个样本文件,返回错误列表(JSON 解析失败也算一条错误)。""" try: instance = _load(sample_path) except Exception as e: # noqa: BLE001 —— 解析失败即样本不合格,如实报,不抛断链 return [f"#: JSON 解析失败:{e}"] return validate(schema, instance, schema) def _run_suite(samples_root: Path) -> int: """套件模式:正样本应全过、负样本应全拦。返回进程退出码(0=期望全满足)。""" contracts = [ ("play-spec", _HERE / "play-spec.schema.json"), ("verdict-feedback", _HERE / "verdict-feedback.schema.json"), ] total_pass = total_block = 0 all_ok = True for name, schema_path in contracts: schema = _load(schema_path) base = samples_root / name valid_dir, invalid_dir = base / "valid", base / "invalid" valids = sorted(valid_dir.glob("*.json")) if valid_dir.is_dir() else [] invalids = sorted(invalid_dir.glob("*.json")) if invalid_dir.is_dir() else [] print(f"\n══ {name}({schema_path.name})══") n_pass = 0 for f in valids: errs = _validate_file(schema, f) if errs: all_ok = False print(f" ✗ 正样本本应过却被拦:{f.name}") for e in errs[:4]: print(f" {e}") else: n_pass += 1 print(f" ✓ 正样本过:{f.name}") n_block = 0 for f in invalids: errs = _validate_file(schema, f) if errs: n_block += 1 # 打印首条错误,证明是按预期规则拦下的(非偶然)。 print(f" ✓ 负样本拦:{f.name} —— {errs[0]}") else: all_ok = False print(f" ✗ 负样本本应拦却放过:{f.name}") print(f" 小计:正样本 {n_pass}/{len(valids)} 过,负样本 {n_block}/{len(invalids)} 拦") total_pass += n_pass total_block += n_block print(f"\n══ 总计:正样本 {total_pass} 过 / 负样本 {total_block} 拦 —— " f"{'全部符合预期' if all_ok else '存在未达预期项(见上 ✗)'}") return 0 if all_ok else 1 def main(argv: list) -> int: if len(argv) >= 2 and argv[1] == "--suite": root = Path(argv[2]).resolve() if len(argv) >= 3 else (_HERE / "samples") return _run_suite(root) if len(argv) < 3: print(__doc__) return 2 schema = _load(Path(argv[1]).resolve()) exit_code = 0 for sample_arg in argv[2:]: sample_path = Path(sample_arg).resolve() errs = _validate_file(schema, sample_path) if errs: exit_code = 1 print(f"FAIL {sample_path.name}") for e in errs: print(f" {e}") else: print(f"PASS {sample_path.name}") return exit_code if __name__ == "__main__": sys.exit(main(sys.argv))