lili 686aaaa421
Some checks failed
contract-gates / contract-gates (push) Has been cancelled
docs-gate / docs-gate (push) Has been cancelled
feat(reference-assets): 签认《山海行纪》并闭合可信消费门
冻结地图1二十分钟纵切版的平衡、证据与金标登记。

新增 ReferenceAsset/2 清单、策略、release、受信快照及 CLI/Service/acceptance provenance /4 消费链;保持 survivor live、R1 签名与部署关闭。
2026-07-28 09:11:54 -07:00

6399 lines
357 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""play-loop 契约校验器PlaySpec / VerdictFeedback / playtest/2 / playtest/3
零依赖:只用 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 / not / if-then-else / $ref(仅 #/$defs/<name> 本地引用)。
关键字集是刻意收窄的schema 由本任务作者,只用上述关键字,正负样本套件即本校验器的回归证明——
若某关键字实现错了,要么正样本会被误拦、要么负样本会漏过,跑套件即暴露。
用法:
1) 单点校验python3 validate.py <schema.json> <sample.json> [<sample2.json> ...]
逐样本打印 PASS/FAIL 与错误路径;全部合规 exit 0否则 exit 1。
2) 套件校验python3 validate.py --suite [samples 根目录,默认脚本旁 samples/]
走 samples/<contract>/valid应全 PASS与 samples/<contract>/invalid应全 FAIL
<contract> 目录名对应同名 schemaplay-spec → play-spec.schema.json
打印每契约『正样本 N 过 / 负样本 M 拦』与总计;期望全部满足 exit 0否则 exit 1。
"""
from __future__ import annotations
import hashlib
import json
import math
import re
import stat
import struct
import sys
import unicodedata
import zlib
from copy import deepcopy
from decimal import Decimal, InvalidOperation, ROUND_HALF_UP
from pathlib import Path
_HERE = Path(__file__).resolve().parent
_MISSING = object()
_RMB_QUANTUM = Decimal("0.00001")
_TRACE_PATH = None
_SELECTION_OVERLAY_CONFIG_HASH = "36ea1973df76c5c814ebaa256ea804b08b2ce8aad477271d738817a6e1801678"
_PRODUCER_SEEDED_GENERATOR_CONFIG_HASH = "73c582f1926e195dba1cb1394bcde9ad9ebd17b4994f90bc724f39dc8bd2c905"
_PRODUCER_LATTICE_ALGORITHM_VERSION = "match3-producer-canonical-lattice/2"
_PRODUCER_OPENCV_CONFIG = {
"schemaVersion": "generatorConfig/1",
"region": {
"minArea": 64, "minSide": 24, "maxAreaRatio": 0.35,
"nmsIou": 0.65, "limit": 24, "colorQuantizationStep": 32,
"morphKernel": 5,
},
"lattice": {
"minRows": 3, "maxRows": 12, "minColumns": 3, "maxColumns": 12,
"maxSpacingCv": 0.12, "maxSizeCv": 0.15, "maxAxisResidualPx": 3,
"minObservedCoverage": 0.70, "limit": 4,
},
"spatialGrid": {
"id": "g01", "columns": 8, "rows": 17,
"subAnchors": [
"center", "north", "south", "east", "west",
"northwest", "northeast", "southwest", "southeast",
],
"limit": 1,
},
"topLevelLimit": 29,
}
def _display_path(value) -> str | None:
"""审计日志只写仓内相对路径;仓外路径退化为文件名,避免机器绝对路径泄漏。"""
if value is None:
return None
if not isinstance(value, Path):
return str(value)
resolved = value.resolve()
repo_root = _HERE.parent.parent
try:
return resolved.relative_to(repo_root).as_posix()
except ValueError:
return resolved.name
def _audit_path_ref(value: Path):
"""按生产 auditPathRef 规则从可信实际路径生成审计引用。"""
resolved = value.resolve()
repo_root = _HERE.parent.parent
try:
return resolved.relative_to(repo_root).as_posix()
except ValueError:
return {
"basename": resolved.name,
"pathHash": hashlib.sha256(str(resolved).encode("utf-8")).hexdigest(),
}
def _trace(stage: str, check: str, *, ref=None, path=None, declared_hash=None,
computed_hash=None, result: str, error: str | None = None) -> None:
"""追加结构化审计日志;只记录身份与断言结果,不记录 brief、payload 或模型内容。"""
if _TRACE_PATH is None:
return
safe_error = None
if error is not None:
# schema/语义错误正文可能包含非法输入的实际值。持久日志只保留 JSON 指针和
# 稳定 check 名称;完整错误仍由 CLI 打印,不把 brief/payload/model 内容落盘。
match = re.match(r"^(#[^:]*):", str(error))
safe_error = f"{match.group(1)}: {check} 失败" if match else f"{check} 失败"
record = {
"stage": stage,
"check": check,
"ref": None if ref is None else str(ref),
"path": _display_path(path),
"declaredHash": declared_hash,
"computedHash": computed_hash,
"result": result,
"error": safe_error,
}
with _TRACE_PATH.open("a", encoding="utf-8") as handle:
handle.write(json.dumps(record, ensure_ascii=False, sort_keys=True, separators=(",", ":")) + "\n")
def _configure_trace(argv: list) -> list:
"""解析兼容式 `--trace-jsonl <path>`;移除该选项后继续走原 CLI 分支。"""
global _TRACE_PATH
if "--trace-jsonl" not in argv:
return argv
index = argv.index("--trace-jsonl")
if index + 1 >= len(argv):
raise ValueError("--trace-jsonl 缺输出路径")
_TRACE_PATH = Path(argv[index + 1]).resolve()
_TRACE_PATH.parent.mkdir(parents=True, exist_ok=True)
_TRACE_PATH.write_text("", encoding="utf-8")
return argv[:index] + argv[index + 2:]
# wait 只允许证明受控时间推进后才会出现的明确业务结果。直接输入、选择、移动、
# 攻击和交付步骤不得借 wait 冒充玩家操作。
_WAIT_ELIGIBLE_EVENT_TYPES = {
"trpg.floor-entered",
"puzzle.level-entered",
"sim-business.order-presented",
"sim-business.settlement-recorded",
}
_PROOF_REGISTRY_SNAPSHOTS = {
"2026-07-14.v2": "proof-obligations.2026-07-14.v2.json",
"2026-07-15.v3": "proof-obligations.v2.json",
}
_INTERACTION_REGISTRY_SNAPSHOTS = {
"2026-07-15.v1": "interaction-profiles.v1.json",
"2026-07-15.v2": "interaction-profiles.v2.json",
}
def _load_proof_registry(version: str) -> dict:
"""按证据声明版本读取冻结快照;未知版本不得回退到最新注册表。"""
filename = _PROOF_REGISTRY_SNAPSHOTS.get(version)
if filename is None:
raise ValueError(f"未知 proof registry version: {version}")
return _load(_HERE / filename)
def _load_interaction_registry(version: str) -> dict:
"""按 interactionRegistryVersion 分派冻结/当前注册表,未知版本禁止回退。"""
filename = _INTERACTION_REGISTRY_SNAPSHOTS.get(version)
if filename is None:
raise ValueError(f"未知 interaction registry version: {version}")
return _load(_HERE / filename)
# ── JSON 类型判定(区分 integer/number/booleanPython 里 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)
and (not isinstance(value, float) or math.isfinite(value)))
if t == "null":
return value is None
return False
def _resolve_ref(ref: str, root: dict):
"""解析本目录 JSON Schema 引用,返回(目标节点,目标根 schema
playtest/2 会直接引用 game-event/1 和公共 defs显式支持本地 fragment 与同目录
`file.json#/<pointer>`,其它目录和网络引用一律拒绝,避免校验器静默读到非 SoT。
"""
if ref.startswith("#"):
target_root = root
fragment = ref[1:]
else:
file_name, separator, fragment = ref.partition("#")
if "/" in file_name or "\\" in file_name or not file_name.endswith(".json"):
raise ValueError(f"仅支持本目录 JSON Schema 引用,收到:{ref}")
schema_path = (_HERE / file_name).resolve()
if schema_path.parent != _HERE or not schema_path.is_file():
raise ValueError(f"外部 $ref 文件不存在或越界:{ref}")
target_root = _load(schema_path)
fragment = fragment if separator else ""
node = target_root
if not fragment:
return node, target_root
if not fragment.startswith("/"):
raise ValueError(f"$ref fragment 必须是 JSON Pointer{ref}")
for part in fragment[1:].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, target_root
def validate(schema, instance, root: dict, path: str = "#") -> list:
"""按 schema 校验 instance返回错误串列表空=合规。schema 为 dict 或 bool。"""
# 布尔 schematrue=恒过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, target_root = _resolve_ref(schema["$ref"], root)
return validate(target, instance, target_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 isinstance(instance, float) and not math.isfinite(instance):
errors.append(f"{path}: JSON number 必须是有限数")
return errors
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 "not" in schema and len(validate(schema["not"], instance, root, path)) == 0:
errors.append(f"{path}: 命中了 not 禁止的结构")
# 条件子if-then-elseif 本身不产错,命中则套 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 _semantic_validate_registry_step(step: dict, path: str, genre: str) -> list:
"""校验一个静态或动态证据步骤的事件、动作白名单和谓词形状。"""
errors = []
event_type = ((step.get("event") or {}).get("type"))
if event_type and not str(event_type).startswith(f"{genre}."):
errors.append(f"{path}/event/type: 必须属于 profile 品类 {genre}")
allowed_actions = step.get("allowedActionTypes") or []
if len(allowed_actions) != len(set(allowed_actions)):
errors.append(f"{path}/allowedActionTypes: 动作类型不得重复")
if "wait" in allowed_actions and event_type not in _WAIT_ELIGIBLE_EVENT_TYPES:
errors.append(f"{path}/allowedActionTypes: wait 只能用于明确的定时业务结果")
for pidx, predicate in enumerate(((step.get("event") or {}).get("predicates") or [])):
ppath = f"{path}/event/predicates/{pidx}"
op = predicate.get("op")
value = predicate.get("value")
if op == "oneOf" and (not isinstance(value, list) or not value):
errors.append(f"{ppath}/value: oneOf 必须提供非空数组")
if op in ("gt", "gte", "lt", "lte") and (
not isinstance(value, (int, float)) or isinstance(value, bool)):
errors.append(f"{ppath}/value: 数值比较必须提供有限数值")
return errors
def _semantic_validate_registry(instance: dict, *, validate_interaction: bool = True) -> list:
"""补 proof-obligations/2 的 profile、义务、步骤和 brief 引用不变量。"""
errors = []
profiles = instance.get("profiles") or {}
expected = {
"narrative.branching-story", "trpg.dice-tower", "heritage.ordered-craft",
"puzzle.match-board", "sim-business.recipe-fulfillment",
}
if set(profiles) != expected:
errors.append(f"#/profiles: 必须且只能登记五个可信 profile {sorted(expected)}")
global_ids = set()
global_interaction_rule_ids = set()
interaction_registry = {"profiles": {}}
if validate_interaction:
try:
interaction_registry = _load(_HERE / "interaction-profiles.v1.json")
except Exception as exc: # noqa: BLE001 —— canonical 交互注册表损坏必须响亮失败
errors.append(f"#/interactionOptionalRules: 无法加载 canonical interaction registry{exc}")
seen_template_routes = set()
for profile_id, profile in profiles.items():
ppath = f"#/profiles/{profile_id}"
if profile.get("id") != profile_id:
errors.append(f"{ppath}/id: 必须与 profiles 的键一致")
template_route = profile.get("templateRoute")
if template_route in seen_template_routes:
errors.append(f"{ppath}/templateRoute: 五个可信 profile 的模板路由必须唯一")
seen_template_routes.add(template_route)
genre = str(profile.get("genre") or "")
obligations = profile.get("obligations") or []
local_ids = set()
for idx, obligation in enumerate(obligations):
oid = obligation.get("id")
path = f"{ppath}/obligations/{idx}"
if oid in global_ids:
errors.append(f"{path}/id: 义务 id 重复:{oid}")
global_ids.add(oid)
local_ids.add(oid)
if not str(oid).startswith(f"{genre}."):
errors.append(f"{path}/id: 义务 id 必须以 '{genre}.' 开头")
evidence = obligation.get("evidence") or {}
sequence = evidence.get("sequence") or []
step_ids = set()
for sidx, step in enumerate(sequence):
spath = f"{path}/evidence/sequence/{sidx}"
step_id = step.get("stepId")
if step_id in step_ids:
errors.append(f"{spath}/stepId: 同一义务步骤 id 重复:{step_id}")
step_ids.add(step_id)
errors += _semantic_validate_registry_step(step, spath, genre)
seen_same_value_paths = set()
for vidx, rule in enumerate(evidence.get("sameValue") or []):
vpath = f"{path}/evidence/sameValue/{vidx}"
pointer = rule.get("path")
if not pointer:
errors.append(f"{vpath}/path: sameValue 不允许根路径或空路径")
if pointer in seen_same_value_paths:
errors.append(f"{vpath}/path: sameValue 路径重复")
seen_same_value_paths.add(pointer)
refs = rule.get("stepIds") or []
if len(refs) != len(set(refs)):
errors.append(f"{vpath}/stepIds: stepId 不得重复")
for step_id in refs:
if step_id not in step_ids:
errors.append(f"{vpath}/stepIds: 引用了本义务不存在的步骤 {step_id}")
# wait 只证明受控时间窗内观察到了事件。多步骤关系义务若允许 wait
# 必须再用同值字段把该事件和前序业务事实关联,不能把时间邻近冒充因果。
same_value_rules = evidence.get("sameValue") or []
for sidx, step in enumerate(sequence):
if "wait" not in (step.get("allowedActionTypes") or []):
continue
spath = f"{path}/evidence/sequence/{sidx}"
step_id = step.get("stepId")
if len(sequence) == 1:
predicates = ((step.get("event") or {}).get("predicates") or [])
if not predicates:
errors.append(f"{spath}: 单步骤 wait 必须以 payload 谓词限定业务结果")
continue
correlated = any(
step_id in (rule.get("stepIds") or [])
and any(ref != step_id for ref in (rule.get("stepIds") or []))
for rule in same_value_rules
)
if not correlated:
errors.append(f"{spath}: 多步骤 wait 必须通过 sameValue 与其他步骤建立业务关联")
ordered = evidence.get("orderedSeries")
if isinstance(ordered, dict):
opath = f"{path}/evidence/orderedSeries"
if profile_id != "heritage.ordered-craft":
errors.append(f"{opath}: orderedSeries 首版只允许 heritage.ordered-craft")
errors += _semantic_validate_registry_step(ordered, opath, genre)
summary = ordered.get("summary") or {}
terminal = ordered.get("terminal") or {}
errors += _semantic_validate_registry_step(summary, f"{opath}/summary", genre)
errors += _semantic_validate_registry_step(terminal, f"{opath}/terminal", genre)
if ordered.get("allowedActionTypes") != ["tap", "key", "drag"]:
errors.append(f"{opath}/allowedActionTypes: 工序步骤必须固定为 tap/key/drag")
dynamic_ids = {ordered.get("seriesId"), summary.get("stepId"), terminal.get("stepId")}
if len(dynamic_ids) != 3:
errors.append(f"{opath}: seriesId、summary.stepId、terminal.stepId 必须两两不同")
for key in ("groupByPath", "indexPath", "itemIdPath", "totalPath", "judgementPath"):
if not ordered.get(key):
errors.append(f"{opath}/{key}: 动态链路径不得为空")
for holder_name, holder, keys in (
("summary", summary, ("groupByPath", "totalPath", "orderedItemIdsPath", "completedPath")),
("terminal", terminal, ("groupByPath", "scorePath", "worksPath")),
):
for key in keys:
if not holder.get(key):
errors.append(f"{opath}/{holder_name}/{key}: 动态链路径不得为空")
# 冻结的 2026-07-14.v2 注册表没有 interaction 规则,其只读验证不能依赖当前注册表。
if not validate_interaction:
continue
interaction_profile_ids = set()
promoted_by_interaction = set()
for ridx, rule in enumerate(profile.get("interactionOptionalRules") or []):
rpath = f"{ppath}/interactionOptionalRules/{ridx}"
rule_id = rule.get("id")
if rule_id in global_interaction_rule_ids:
errors.append(f"{rpath}/id: interaction 规则 id 必须在整表全局唯一")
global_interaction_rule_ids.add(rule_id)
interaction_profile_id = rule.get("interactionProfileId")
if interaction_profile_id in interaction_profile_ids:
errors.append(f"{rpath}/interactionProfileId: 同一 proof profile 不得重复匹配")
interaction_profile_ids.add(interaction_profile_id)
interaction_profile = (
(interaction_registry.get("profiles") or {}).get(interaction_profile_id) or {}
)
if not interaction_profile:
errors.append(f"{rpath}/interactionProfileId: 未命中 canonical interaction profile")
else:
if interaction_profile.get("proofProfileId") != profile_id:
errors.append(f"{rpath}/interactionProfileId: interaction proofProfileId 不一致")
if interaction_profile.get("genre") != profile.get("genre"):
errors.append(f"{rpath}/interactionProfileId: interaction genre 不一致")
if interaction_profile.get("templateRoute") != profile.get("templateRoute"):
errors.append(f"{rpath}/interactionProfileId: interaction templateRoute 不一致")
refs = rule.get("requireObligationIds") or []
if len(refs) != len(set(refs)):
errors.append(f"{rpath}/requireObligationIds: 义务 id 不得重复")
for target in refs:
if target in promoted_by_interaction:
errors.append(f"{rpath}/requireObligationIds: 同一义务不得由多条 interaction 规则提升")
promoted_by_interaction.add(target)
target_obj = next((o for o in obligations if o.get("id") == target), None)
if target_obj is None:
errors.append(f"{rpath}/requireObligationIds: 引用了本 profile 不存在的义务 {target}")
elif target_obj.get("defaultRequired") is not False:
errors.append(f"{rpath}/requireObligationIds: {target} 必须保持 defaultRequired=false")
rule_ids = set()
for ridx, rule in enumerate(profile.get("briefOptionalRules") or []):
rpath = f"{ppath}/briefOptionalRules/{ridx}"
rule_id = rule.get("id")
if rule_id in rule_ids:
errors.append(f"{rpath}/id: brief 可选规则 id 重复:{rule_id}")
rule_ids.add(rule_id)
for target in rule.get("requireObligationIds") or []:
if target not in local_ids:
errors.append(f"{rpath}/requireObligationIds: 引用了本 profile 不存在的义务 {target}")
continue
target_obj = next((o for o in obligations if o.get("id") == target), None)
if target_obj and target_obj.get("defaultRequired") is not False:
errors.append(f"{rpath}/requireObligationIds: {target} 已默认必需,不应再提升")
return errors
def _registry_event_types(registry: dict, profile_id: str | None = None) -> set:
"""返回 canonical 注册表声明的事件类型;传 profile 时只看该可信 profile。"""
profiles = registry.get("profiles") or {}
selected = [profiles.get(profile_id)] if profile_id else profiles.values()
event_types = set()
for profile in selected:
if not isinstance(profile, dict):
continue
for obligation in profile.get("obligations") or []:
evidence = obligation.get("evidence") or {}
for step in evidence.get("sequence") or []:
event_type = ((step.get("event") or {}).get("type"))
if event_type:
event_types.add(event_type)
ordered = evidence.get("orderedSeries")
if isinstance(ordered, dict):
for step in (ordered, ordered.get("summary") or {}, ordered.get("terminal") or {}):
event_type = ((step.get("event") or {}).get("type"))
if event_type:
event_types.add(event_type)
return event_types
def _semantic_validate_legacy_mapping(instance: dict) -> list:
"""历史适配表只能做精确、无歧义、可回溯的 allowlist 映射。"""
errors = []
registry = _load_proof_registry(instance.get("proofRegistryVersion"))
if instance.get("proofRegistryVersion") != registry.get("registryVersion"):
errors.append("#/proofRegistryVersion: 必须与 canonical proof-obligations.v2.json 一致")
profiles = registry.get("profiles") or {}
seen_ids = set()
seen_keys = set()
for idx, mapping in enumerate(instance.get("mappings") or []):
path = f"#/mappings/{idx}"
mapping_id = mapping.get("id")
if mapping_id in seen_ids:
errors.append(f"{path}/id: 映射 id 重复:{mapping_id}")
seen_ids.add(mapping_id)
profile_id = mapping.get("proofProfileId")
if profile_id not in profiles:
errors.append(f"{path}/proofProfileId: 未知 profile {profile_id}")
payload_paths = mapping.get("requiredPayloadPaths") or []
if len(payload_paths) != len(set(payload_paths)):
errors.append(f"{path}/requiredPayloadPaths: 路径不得重复")
composite = (profile_id, mapping.get("legacyTag"), tuple(sorted(payload_paths)))
if composite in seen_keys:
errors.append(f"{path}: profile+tag+payloadPaths 映射键重复")
seen_keys.add(composite)
if mapping.get("type") not in _registry_event_types(registry, profile_id):
errors.append(f"{path}/type: 目标事件不属于该 profile 注册表")
return errors
def _load_payload_canonical(text: str):
"""解析 Node 封存原文,并拒绝 JSON 非有限数与重复键。"""
def reject_constant(value):
raise ValueError(f"JSON 非有限数不允许:{value}")
def reject_duplicate_keys(pairs):
result = {}
for key, value in pairs:
if key in result:
raise ValueError(f"JSON 对象键重复:{key}")
result[key] = value
return result
return json.loads(
text, parse_constant=reject_constant, object_pairs_hook=reject_duplicate_keys,
)
def _json_semantically_equal(left, right) -> bool:
"""按 JSON 类型递归比较;数字允许 int/float 等值,但 bool 不能冒充 0/1。"""
if isinstance(left, bool) or isinstance(right, bool):
return isinstance(left, bool) and isinstance(right, bool) and left == right
if isinstance(left, (int, float)) or isinstance(right, (int, float)):
return (isinstance(left, (int, float)) and not isinstance(left, bool)
and isinstance(right, (int, float)) and not isinstance(right, bool)
and (not isinstance(left, float) or math.isfinite(left))
and (not isinstance(right, float) or math.isfinite(right))
and left == right)
if left is None or right is None:
return left is None and right is None
if isinstance(left, str) or isinstance(right, str):
return isinstance(left, str) and isinstance(right, str) and left == right
if isinstance(left, list) or isinstance(right, list):
return (isinstance(left, list) and isinstance(right, list) and len(left) == len(right)
and all(_json_semantically_equal(a, b) for a, b in zip(left, right)))
if isinstance(left, dict) or isinstance(right, dict):
return (isinstance(left, dict) and isinstance(right, dict)
and set(left) == set(right)
and all(_json_semantically_equal(left[key], right[key]) for key in left))
return False
def _semantic_validate_game_event(instance: dict) -> list:
"""校验 game-event/1 的派生字段、封存原文与 payloadHash。"""
errors = []
event_type = instance.get("type")
if instance.get("namespace") != f"game/{event_type}":
errors.append("#/namespace: 必须严格等于 game/<type>")
canonical = instance.get("payloadCanonical")
if isinstance(canonical, str):
expected_hash = hashlib.sha256(canonical.encode("utf-8")).hexdigest()
if instance.get("payloadHash") != expected_hash:
errors.append("#/payloadHash: 与 payloadCanonical 原始 UTF-8 字节的 SHA-256 不一致")
try:
parsed = _load_payload_canonical(canonical)
except (TypeError, ValueError, json.JSONDecodeError) as exc:
errors.append(f"#/payloadCanonical: 不是严格 JSON{exc}")
else:
if not isinstance(parsed, dict):
errors.append("#/payloadCanonical: 根值必须是 JSON 对象")
elif not _json_semantically_equal(parsed, instance.get("payload")):
errors.append("#/payloadCanonical: 解析结果与 payload 语义不一致")
payload = instance.get("payload") or {}
if event_type == "puzzle.swap-committed":
pair = payload.get("pair") or []
if len(pair) == 2:
pair_key = [(cell.get("row"), cell.get("column")) for cell in pair]
if pair_key != sorted(pair_key) or pair_key[0] == pair_key[1]:
errors.append("#/payload/pair: 必须按 row/column 保存两个不同端点")
if abs(pair_key[0][0] - pair_key[1][0]) + abs(pair_key[0][1] - pair_key[1][1]) != 1:
errors.append("#/payload/pair: 交换端点必须正交相邻")
before = payload.get("boardRevisionBefore")
after = payload.get("boardRevisionAfter")
if isinstance(before, int) and isinstance(after, int) and after <= before:
errors.append("#/payload/boardRevisionAfter: 必须严格大于 boardRevisionBefore")
elif event_type == "puzzle.match-formed":
runs = payload.get("runs") or []
run_keys = []
union = set()
for index, run in enumerate(runs):
path = f"#/payload/runs/{index}/cells"
cells = run.get("cells") or []
tuples = [(cell.get("row"), cell.get("column")) for cell in cells]
if run.get("axis") == "horizontal":
if len({row for row, _ in tuples}) != 1 or tuples != sorted(tuples, key=lambda item: item[1]):
errors.append(f"{path}: horizontal run 必须同 row 且按 column 排序")
elif any(right[1] != left[1] + 1 for left, right in zip(tuples, tuples[1:])):
errors.append(f"{path}: horizontal run 必须连续")
else:
if len({column for _, column in tuples}) != 1 or tuples != sorted(tuples):
errors.append(f"{path}: vertical run 必须同 column 且按 row 排序")
elif any(right[0] != left[0] + 1 for left, right in zip(tuples, tuples[1:])):
errors.append(f"{path}: vertical run 必须连续")
union.update(tuples)
run_keys.append((0 if run.get("axis") == "horizontal" else 1, tuple(tuples)))
if run_keys != sorted(run_keys):
errors.append("#/payload/runs: 必须按 axis 与 cells 规范排序")
matched = [(cell.get("row"), cell.get("column")) for cell in payload.get("matchedCells") or []]
if matched != sorted(set(matched)):
errors.append("#/payload/matchedCells: 必须去重并按 row/column 排序")
if set(matched) != union:
errors.append("#/payload/matchedCells: 必须等于 runs 的逐格去重并集")
if payload.get("matchedCount") != len(matched):
errors.append("#/payload/matchedCount: 必须等于 matchedCells 长度")
return errors
def _semantic_validate_acceptance_profile(instance: dict) -> list:
"""验收请求和来源清单必须精确绑定 canonical profile禁止调用方自报一套自洽别名。"""
errors = []
try:
schema_version = instance.get("schemaVersion")
# v3 仅在 v2 之上开口参照资产可选字段W-GOLD-LIVEproof registry 冻结版本不变。
expected_registry_version = {
"acceptance-request/1": "2026-07-14.v2",
"acceptance-provenance/1": "2026-07-14.v2",
"acceptance-request/2": "2026-07-15.v3",
"acceptance-provenance/2": "2026-07-15.v3",
"acceptance-request/3": "2026-07-15.v3",
"acceptance-provenance/3": "2026-07-15.v3",
"acceptance-provenance/4": "2026-07-15.v3",
}.get(schema_version)
if expected_registry_version and instance.get("proofRegistryVersion") != expected_registry_version:
errors.append(
f"#/proofRegistryVersion: {schema_version} 必须使用冻结版本 {expected_registry_version}",
)
registry = _load_proof_registry(instance.get("proofRegistryVersion"))
profile_id = instance.get("proofProfileId")
profile = (registry.get("profiles") or {}).get(profile_id)
if not isinstance(profile, dict):
errors.append(f"#/proofProfileId: 未知 canonical profile {profile_id}")
return errors
if instance.get("genre") != profile.get("genre"):
errors.append("#/genre: 必须与 proofProfileId 的 canonical genre 一致")
if instance.get("templateRoute") != profile.get("templateRoute"):
errors.append("#/templateRoute: 必须与 proofProfileId 的 canonical templateRoute 一致")
if instance.get("schemaVersion") in (
"acceptance-request/2", "acceptance-provenance/2",
"acceptance-request/3", "acceptance-provenance/3",
"acceptance-provenance/4",
):
binding = instance.get("interactionBinding")
if isinstance(binding, dict):
for message in _semantic_validate_interaction_binding(binding):
errors.append(f"#/interactionBinding{message[1:]}")
if binding.get("taskBindingHash") != instance.get("taskBindingHash"):
errors.append("#/interactionBinding/taskBindingHash: 必须与请求顶层任务绑定一致")
interaction_registry = _load_interaction_registry(
binding.get("interactionRegistryVersion"),
)
interaction_profile = (
(interaction_registry.get("profiles") or {}).get(
binding.get("interactionProfileId"),
) or {}
)
if interaction_profile.get("proofProfileId") != instance.get("proofProfileId"):
errors.append("#/interactionBinding/interactionProfileId: proof profile 不一致")
if interaction_profile.get("genre") != instance.get("genre"):
errors.append("#/interactionBinding/interactionProfileId: genre 不一致")
if interaction_profile.get("templateRoute") != instance.get("templateRoute"):
errors.append("#/interactionBinding/interactionProfileId: templateRoute 不一致")
except Exception as exc: # noqa: BLE001 —— canonical 注册表坏掉必须响亮失败
errors.append(f"#/proofRegistryVersion: 无法加载 canonical 注册表:{exc}")
return errors
def _json_pointer_get(document, pointer: str):
"""读取受限 JSON Pointer缺路径返回哨兵绝不把缺失与 null 混为一谈。"""
if pointer == "":
return document
if not isinstance(pointer, str) or not pointer.startswith("/"):
return _MISSING
node = document
for raw_part in pointer[1:].split("/"):
part = raw_part.replace("~1", "/").replace("~0", "~")
if isinstance(node, dict):
if part not in node:
return _MISSING
node = node[part]
elif isinstance(node, list) and part.isdigit():
index = int(part)
if index >= len(node):
return _MISSING
node = node[index]
else:
return _MISSING
return node
def _predicate_matches(payload: dict, predicate: dict) -> bool:
"""执行注册表白名单谓词;任何缺路径或类型不符都按不匹配 fail-closed。"""
op = predicate.get("op")
if op in ("changed", "increased"):
before = _json_pointer_get(payload, predicate.get("beforePath"))
after = _json_pointer_get(payload, predicate.get("afterPath"))
if before is _MISSING or after is _MISSING:
return False
if op == "changed":
return before != after
numeric = lambda value: isinstance(value, (int, float)) and not isinstance(value, bool)
return numeric(before) and numeric(after) and after > before
actual = _json_pointer_get(payload, predicate.get("path"))
if actual is _MISSING:
return False
if op == "exists":
return actual is not None
expected = predicate.get("value")
if op == "equals":
return _json_semantically_equal(actual, expected)
if op == "oneOf":
return (isinstance(expected, list)
and any(_json_semantically_equal(actual, item) for item in expected))
numeric = lambda value: isinstance(value, (int, float)) and not isinstance(value, bool)
if not numeric(actual) or not numeric(expected):
return False
if op == "gt":
return actual > expected
if op == "gte":
return actual >= expected
if op == "lt":
return actual < expected
if op == "lte":
return actual <= expected
return False
def _validate_canonical_step_ref(
canonical_step: dict, sequence_ref: dict, event_by_seq: dict, action_by_id: dict, path: str) -> list:
"""把一个已解析 sequenceRef 重新对照 canonical step拒绝错 type、坏 payload 与错动作。"""
errors = []
event = event_by_seq.get(sequence_ref.get("eventRef"))
action = action_by_id.get(sequence_ref.get("actionRef"))
if not isinstance(event, dict) or not isinstance(action, dict):
return errors
expected_event = canonical_step.get("event") or {}
if event.get("type") != expected_event.get("type"):
errors.append(f"{path}/eventRef: 事件 type 与 canonical step 不一致")
payload = event.get("payload") or {}
for pidx, predicate in enumerate(expected_event.get("predicates") or []):
if not _predicate_matches(payload, predicate):
errors.append(f"{path}/eventRef: payload 未满足 canonical 谓词 {pidx}")
action_type = ((action.get("normalized") or {}).get("type"))
if action_type not in (canonical_step.get("allowedActionTypes") or []):
errors.append(f"{path}/actionRef: 动作类型 {action_type!r} 不在该步骤白名单")
return errors
def _json_scalar_token(value):
"""把 JSON scalar 变成类型敏感的稳定文本对象、数组、null 与非有限数均不合法。"""
if value is None or isinstance(value, (dict, list)):
return None
if isinstance(value, float) and not math.isfinite(value):
return None
try:
return json.dumps(value, ensure_ascii=False, separators=(",", ":"), allow_nan=False)
except (TypeError, ValueError):
return None
def _quantize_nonnegative_rmb(value, path: str) -> tuple[float | None, list]:
"""按跨语言统一的十进制 ROUND_HALF_UP 五位口径量化非负金额。"""
if (not isinstance(value, (int, float)) or isinstance(value, bool)
or isinstance(value, float) and not math.isfinite(value) or value < 0):
return None, [f"{path}: 必须是有限非负金额"]
try:
quantized = Decimal(str(float(value))).quantize(_RMB_QUANTUM, rounding=ROUND_HALF_UP)
except (InvalidOperation, ValueError):
return None, [f"{path}: 无法按五位金额量化"]
return float(quantized), []
def _validate_same_value(
rules: list, sequence_refs: list, event_by_seq: dict, path: str) -> list:
"""机械核对跨步骤同路径 scalar 逐字相等,防止跨升级周期或跨订单拼证据。"""
errors = []
ref_by_step = {row.get("stepId"): row for row in sequence_refs}
for ridx, rule in enumerate(rules):
rpath = f"{path}/{ridx}"
tokens = []
for step_id in rule.get("stepIds") or []:
ref = ref_by_step.get(step_id)
event = event_by_seq.get((ref or {}).get("eventRef")) or {}
value = _json_pointer_get(event.get("payload") or {}, rule.get("path"))
token = None if value is _MISSING else _json_scalar_token(value)
if token is None:
errors.append(f"{rpath}: 步骤 {step_id} 缺少可比较的 JSON scalar")
tokens.append(token)
if tokens and (None in tokens or len(set(tokens)) != 1):
errors.append(f"{rpath}: 所列步骤的值必须逐字相等")
return errors
def _validate_ordered_series(
ordered: dict, sequence_refs: list, event_by_seq: dict, action_by_id: dict, path: str) -> list:
"""重建 heritage 动态工序链,核对完整 N 步、动作独立性、汇总和成品终态。"""
errors = []
if len(sequence_refs) < 2:
return [f"{path}: orderedSeries 至少需要工序、summary 与 terminal 引用"]
series_refs = sequence_refs[:-2]
summary_ref, terminal_ref = sequence_refs[-2:]
count = len(series_refs)
minimum = ordered.get("minItems")
maximum = ordered.get("maxItems")
if not isinstance(count, int) or count < minimum or count > maximum:
errors.append(f"{path}: 工序数必须在 {minimum}..{maximum},实际 {count}")
expected_ids = [f"{ordered.get('seriesId')}:{index}" for index in range(1, count + 1)]
expected_ids += [
(ordered.get("summary") or {}).get("stepId"),
(ordered.get("terminal") or {}).get("stepId"),
]
actual_ids = [row.get("stepId") for row in sequence_refs]
if actual_ids != expected_ids:
errors.append(f"{path}: 动态 stepId 必须严格展开为 seriesId:1..N/summary/terminal")
group_tokens = []
item_ids = []
series_action_ids = []
series_frame_refs = []
for index, ref in enumerate(series_refs, start=1):
rpath = f"{path}/{index - 1}"
errors += _validate_canonical_step_ref(ordered, ref, event_by_seq, action_by_id, rpath)
event = event_by_seq.get(ref.get("eventRef")) or {}
payload = event.get("payload") or {}
group_value = _json_pointer_get(payload, ordered.get("groupByPath"))
group_tokens.append(None if group_value is _MISSING else _json_scalar_token(group_value))
actual_index = _json_pointer_get(payload, ordered.get("indexPath"))
total = _json_pointer_get(payload, ordered.get("totalPath"))
item_id = _json_pointer_get(payload, ordered.get("itemIdPath"))
judgement = _json_pointer_get(payload, ordered.get("judgementPath"))
if actual_index != index or isinstance(actual_index, bool):
errors.append(f"{rpath}/eventRef: stepIndex 必须严格等于 {index}")
if total != count or isinstance(total, bool):
errors.append(f"{rpath}/eventRef: totalSteps 必须等于完整工序数 {count}")
if not isinstance(item_id, str) or not item_id.strip():
errors.append(f"{rpath}/eventRef: stepId 必须是非空字符串")
item_ids.append(item_id)
if judgement is _MISSING or judgement is None or (
isinstance(judgement, str) and not judgement.strip()):
errors.append(f"{rpath}/eventRef: judgement 必须非空")
series_action_ids.append(ref.get("actionRef"))
series_frame_refs.append(ref.get("postFrameRef"))
if group_tokens and (None in group_tokens or len(set(group_tokens)) != 1):
errors.append(f"{path}: 所有工序必须属于同一非空 workId")
if len(item_ids) != len(set(item_ids)):
errors.append(f"{path}: 工序 stepId 必须唯一")
if len(series_action_ids) != len(set(series_action_ids)):
errors.append(f"{path}: 每道工序必须使用两两不同的 actionId")
if len(series_frame_refs) != len(set(series_frame_refs)):
errors.append(f"{path}: 每道工序必须使用两两不同的动作后帧")
summary = ordered.get("summary") or {}
terminal = ordered.get("terminal") or {}
errors += _validate_canonical_step_ref(
summary, summary_ref, event_by_seq, action_by_id, f"{path}/{len(series_refs)}")
errors += _validate_canonical_step_ref(
terminal, terminal_ref, event_by_seq, action_by_id, f"{path}/{len(series_refs) + 1}")
last_action = series_action_ids[-1] if series_action_ids else None
last_frame = series_frame_refs[-1] if series_frame_refs else None
for label, ref in (("summary", summary_ref), ("terminal", terminal_ref)):
if ref.get("actionRef") in set(series_action_ids[:-1]):
errors.append(f"{path}/{label}: 只允许与最后一道工序共享 actionId")
if ref.get("postFrameRef") in set(series_frame_refs[:-1]):
errors.append(f"{path}/{label}: 只允许与最后一道工序共享 post frame")
if ref.get("actionRef") == last_action and ref.get("postFrameRef") != last_frame:
errors.append(f"{path}/{label}: 共享最后动作时必须同时共享其动作后帧")
summary_payload = (event_by_seq.get(summary_ref.get("eventRef")) or {}).get("payload") or {}
terminal_payload = (event_by_seq.get(terminal_ref.get("eventRef")) or {}).get("payload") or {}
group_token = group_tokens[0] if group_tokens else None
for label, holder, payload in (
("summary", summary, summary_payload), ("terminal", terminal, terminal_payload)):
value = _json_pointer_get(payload, holder.get("groupByPath"))
if _json_scalar_token(value) != group_token:
errors.append(f"{path}/{label}: workId 必须与工序链一致")
if _json_pointer_get(summary_payload, summary.get("totalPath")) != count:
errors.append(f"{path}/summary: totalSteps 必须等于工序数")
if _json_pointer_get(summary_payload, summary.get("orderedItemIdsPath")) != item_ids:
errors.append(f"{path}/summary: orderedStepIds 必须与工序 stepId 逐项一致")
if _json_pointer_get(summary_payload, summary.get("completedPath")) is not True:
errors.append(f"{path}/summary: allStepsCompleted 必须为 true")
score = _json_pointer_get(terminal_payload, terminal.get("scorePath"))
works = _json_pointer_get(terminal_payload, terminal.get("worksPath"))
if not isinstance(score, (int, float)) or isinstance(score, bool) or not math.isfinite(score):
errors.append(f"{path}/terminal: score 必须是有限数")
if not isinstance(works, (int, float)) or isinstance(works, bool) or not math.isfinite(works) or works <= 0:
errors.append(f"{path}/terminal: works 必须大于零")
return errors
def _all_checks_true(guard: dict | None) -> bool:
"""判断 guard 的机械检查是否全部为真;缺字段一律视为不通过。"""
checks = (guard or {}).get("checks") or {}
return bool(checks) and all(value is True for value in checks.values())
def _semantic_validate_environment(instance: dict, *, wait_max_ms: int = 3000) -> list:
"""环境必须逐字对应 runner 固定口径与本次顶层 artifact。"""
errors = []
environment = instance.get("environment") or {}
if environment.get("buildRef") != instance.get("artifactHash"):
errors.append("#/environment/buildRef: 必须严格等于顶层 artifactHash")
viewport = environment.get("viewport") or {}
if viewport != {"width": 390, "height": 844, "dpr": 1}:
errors.append("#/environment/viewport: 必须严格等于 390x844@1")
virtual_time = environment.get("virtualTime") or {}
expected_virtual_time = {
"mode": "cdp", "actionQuantumMs": 600, "waitMinMs": 100, "waitMaxMs": wait_max_ms,
}
for key, expected in expected_virtual_time.items():
if virtual_time.get(key) != expected:
errors.append(f"#/environment/virtualTime/{key}: 必须严格等于 {expected!r}")
return errors
def _semantic_validate_playtest(instance: dict, *, wait_max_ms: int = 3000) -> list:
"""校验共享事实链;调用方显式提供其协议代际的等待上限。"""
errors = []
errors += _semantic_validate_environment(instance, wait_max_ms=wait_max_ms)
phase = instance.get("phase")
decided = phase == "DECIDED"
mode = instance.get("acceptanceMode")
evidence_mode = instance.get("evidenceMode")
if mode == "historical_replay" and evidence_mode != "legacy-adapted":
errors.append("#/evidenceMode: historical_replay 必须使用 legacy-adapted")
if mode in ("v3", "v3_shadow") and evidence_mode != "native":
errors.append("#/evidenceMode: 新生产 v3/v3_shadow 必须使用 native")
terminal_fields = ("outcome", "finalPostguard", "decision", "compatibility")
if decided:
for field in terminal_fields:
if field not in instance:
errors.append(f"#: DECIDED 必须带 {field}")
else:
for field in terminal_fields:
if field in instance:
errors.append(f"#/{field}: 非 DECIDED 阶段不得提前写终裁字段")
floor = instance.get("floor") or {}
gate_results = (floor.get("gates") or {}).values()
if gate_results and floor.get("pass") != all(g.get("pass") is True for g in gate_results):
errors.append("#/floor/pass: 必须等于 A/B/C/D 四门 AND")
actor = instance.get("actor")
judge = instance.get("judge")
if actor:
if actor.get("requestedGameSeed") != actor.get("actualGameSeed"):
errors.append("#/actor: requestedGameSeed 与 actualGameSeed 不一致")
if actor and judge:
for field in ("apiClientId", "sessionId", "systemPromptVersion",
"contextProjectionVersion", "evidenceDir"):
if actor.get(field) == judge.get(field):
errors.append(f"#/judge/{field}: Actor 与 Judge 必须物理隔离,不能共用 {field}")
actions = instance.get("actions") or []
action_by_id = {}
post_frame_paths = set()
for idx, action in enumerate(actions):
aid = action.get("actionId")
if aid in action_by_id:
errors.append(f"#/actions/{idx}/actionId: actionId 重复:{aid}")
action_by_id[aid] = action
post_ref = action.get("postFrameRef") or {}
if post_ref.get("path"):
post_frame_paths.add(post_ref["path"])
valid = action.get("valid") is True
normalized = action.get("normalized")
dispatch = action.get("dispatchReceipt")
budget = action.get("virtualTimeBudgetMs")
time_advance = action.get("timeAdvance")
if valid:
if normalized is None or dispatch is None:
errors.append(f"#/actions/{idx}: 合法动作必须有 normalized 与 dispatchReceipt")
if normalized and normalized.get("type") in ("tap", "key", "drag") and budget != 600:
errors.append(f"#/actions/{idx}/virtualTimeBudgetMs: tap/key/drag 必须精确推进 600ms")
if normalized and normalized.get("type") == "wait" and budget != normalized.get("ms"):
errors.append(f"#/actions/{idx}/virtualTimeBudgetMs: wait 必须按请求值推进")
if not isinstance(time_advance, dict):
errors.append(f"#/actions/{idx}/timeAdvance: 合法动作必须封存推进事实")
else:
if time_advance.get("ok") is not True:
errors.append(f"#/actions/{idx}/timeAdvance/ok: 合法动作必须闭合")
if time_advance.get("requestedBudgetMs") != budget:
errors.append(f"#/actions/{idx}/timeAdvance/requestedBudgetMs: 必须等于动作预算")
if time_advance.get("plannedFrames") != time_advance.get("executedFrames"):
errors.append(f"#/actions/{idx}/timeAdvance: plannedFrames 必须等于 executedFrames")
else:
pre = action.get("preFrameRef") or {}
post = action.get("postFrameRef") or {}
if dispatch is not None or budget != 0:
errors.append(f"#/actions/{idx}: 非法动作不得派发或推进虚拟时间")
if time_advance is not None:
errors.append(f"#/actions/{idx}/timeAdvance: 非法动作必须为 null")
if pre.get("hash") != post.get("hash") or action.get("preEventSeq") != action.get("postEventSeq"):
errors.append(f"#/actions/{idx}: 非法动作重试期间帧和事件游标必须冻结")
events = instance.get("events") or []
event_by_seq = {}
last_seq = 0
for idx, event in enumerate(events):
seq = event.get("seq")
if seq in event_by_seq:
errors.append(f"#/events/{idx}/seq: 事件 seq 重复:{seq}")
if isinstance(seq, int) and seq <= last_seq:
errors.append(f"#/events/{idx}/seq: 事件必须按 seq 严格递增")
if isinstance(seq, int):
last_seq = seq
event_by_seq[seq] = event
for message in _semantic_validate_game_event(event):
errors.append(f"#/events/{idx}{message[1:]}")
if event.get("evidenceMode") != evidence_mode:
errors.append(f"#/events/{idx}/evidenceMode: 必须与顶层 evidenceMode 一致")
aid = event.get("actionId")
action = action_by_id.get(aid)
if action is None:
errors.append(f"#/events/{idx}/actionId: 引用了不存在的动作 {aid}")
elif not (action.get("preEventSeq", -1) < seq <= action.get("postEventSeq", -1)):
errors.append(f"#/events/{idx}: 事件 seq 不在动作 {aid} 的 (preSeq, postSeq] 区间")
obligations = instance.get("proofObligations") or []
obligation_by_id = {}
for idx, obligation in enumerate(obligations):
oid = obligation.get("id")
if oid in obligation_by_id:
errors.append(f"#/proofObligations/{idx}/id: 义务 id 重复:{oid}")
obligation_by_id[oid] = obligation
sequence_refs = obligation.get("sequenceRefs") or []
action_refs = obligation.get("actionRefs") or []
frame_refs = obligation.get("postFrameRefs") or []
event_refs = obligation.get("eventRefs") or []
projected_actions = []
projected_frames = []
projected_events = []
last_event_ref = 0
seen_steps = set()
for sidx, sequence_ref in enumerate(sequence_refs):
spath = f"#/proofObligations/{idx}/sequenceRefs/{sidx}"
step_id = sequence_ref.get("stepId")
event_ref = sequence_ref.get("eventRef")
action_ref = sequence_ref.get("actionRef")
frame_ref = sequence_ref.get("postFrameRef")
if step_id in seen_steps:
errors.append(f"{spath}/stepId: 同一义务步骤不得重复")
seen_steps.add(step_id)
if isinstance(event_ref, int) and event_ref <= last_event_ref:
errors.append(f"{spath}/eventRef: sequenceRefs 必须严格递增")
if isinstance(event_ref, int):
last_event_ref = event_ref
event = event_by_seq.get(event_ref)
action = action_by_id.get(action_ref)
if event is None:
errors.append(f"{spath}/eventRef: 引用了不存在的事件 {event_ref}")
elif event.get("actionId") != action_ref:
errors.append(f"{spath}: 事件与 actionRef 不在同一动作区间")
if action is None:
errors.append(f"{spath}/actionRef: 引用了不存在的动作 {action_ref}")
elif (action.get("postFrameRef") or {}).get("path") != frame_ref:
errors.append(f"{spath}/postFrameRef: 必须是 actionRef 的动作后帧")
if action_ref not in projected_actions:
projected_actions.append(action_ref)
if frame_ref not in projected_frames:
projected_frames.append(frame_ref)
if event_ref not in projected_events:
projected_events.append(event_ref)
if action_refs != projected_actions:
errors.append(f"#/proofObligations/{idx}/actionRefs: 必须由 sequenceRefs 确定性投影")
if frame_refs != projected_frames:
errors.append(f"#/proofObligations/{idx}/postFrameRefs: 必须由 sequenceRefs 确定性投影")
if event_refs != projected_events:
errors.append(f"#/proofObligations/{idx}/eventRefs: 必须由 sequenceRefs 确定性投影")
for aid in action_refs:
if aid not in action_by_id:
errors.append(f"#/proofObligations/{idx}/actionRefs: 引用了不存在的动作 {aid}")
for frame in frame_refs:
if frame not in post_frame_paths:
errors.append(f"#/proofObligations/{idx}/postFrameRefs: 引用了不存在的动作后帧 {frame}")
for seq in event_refs:
event = event_by_seq.get(seq)
if event is None:
errors.append(f"#/proofObligations/{idx}/eventRefs: 引用了不存在的事件 {seq}")
continue
if event.get("actionId") not in action_refs:
errors.append(f"#/proofObligations/{idx}: 事件 {seq} 与 actionRefs 不在同一动作区间")
action = action_by_id.get(event.get("actionId")) or {}
if (action.get("postFrameRef") or {}).get("path") not in frame_refs:
errors.append(f"#/proofObligations/{idx}: 事件 {seq} 对应动作的 post frame 未被引用")
if obligation.get("status") == "satisfied" and not sequence_refs:
errors.append(f"#/proofObligations/{idx}: satisfied 义务必须有权威 sequenceRefs")
# resolved obligation 必须能从固定版本注册表重建,防 runner 或模型临时删减必需义务。
try:
registry = _load_proof_registry(instance.get("proofRegistryVersion"))
profile_id = instance.get("proofProfileId")
profile = ((registry.get("profiles") or {}).get(profile_id) or {})
if not profile:
errors.append(f"#/proofProfileId: canonical 注册表不存在 profile {profile_id}")
elif profile.get("genre") != instance.get("genre"):
errors.append("#/proofProfileId: profile 品类与顶层 genre 不一致")
elif profile.get("templateRoute") != instance.get("templateRoute"):
errors.append("#/templateRoute: 必须与 proofProfileId 的 canonical templateRoute 一致")
registered = {o.get("id"): o for o in profile.get("obligations") or []}
allowed_event_types = _registry_event_types(registry, profile_id)
for eidx, event in enumerate(events):
if event.get("type") not in allowed_event_types:
errors.append(f"#/events/{eidx}/type: 事件不在当前 proof profile 白名单")
expected_required = {oid for oid, obj in registered.items() if obj.get("defaultRequired") is True}
rule_by_id = {r.get("id"): r for r in profile.get("briefOptionalRules") or []}
seen_rule_matches = set()
for rid in instance.get("briefRuleMatches") or []:
if rid in seen_rule_matches:
errors.append(f"#/briefRuleMatches: brief 规则重复:{rid}")
seen_rule_matches.add(rid)
rule = rule_by_id.get(rid)
if rule is None:
errors.append(f"#/briefRuleMatches: 引用了本品类不存在的 brief 规则 {rid}")
else:
expected_required.update(rule.get("requireObligationIds") or [])
for oid in obligation_by_id:
if oid not in registered:
errors.append(f"#/proofObligations: 出现注册表不存在的义务 {oid}")
for oid, resolved in obligation_by_id.items():
canonical = registered.get(oid) or {}
evidence = canonical.get("evidence") or {}
resolved_refs = resolved.get("sequenceRefs") or []
canonical_sequence = evidence.get("sequence") or []
ordered = evidence.get("orderedSeries")
if isinstance(ordered, dict):
if resolved.get("status") in ("satisfied", "failed"):
errors += _validate_ordered_series(
ordered, resolved_refs, event_by_seq, action_by_id,
f"#/proofObligations/{oid}/sequenceRefs",
)
else:
expected_steps = [row.get("stepId") for row in canonical_sequence]
actual_steps = [row.get("stepId") for row in resolved_refs]
if resolved.get("status") in ("satisfied", "failed") and actual_steps != expected_steps:
errors.append(f"#/proofObligations/{oid}/sequenceRefs: 必须完整保持 canonical step 顺序")
for sidx, (sequence_ref, canonical_step) in enumerate(zip(
resolved_refs, canonical_sequence)):
errors += _validate_canonical_step_ref(
canonical_step, sequence_ref, event_by_seq, action_by_id,
f"#/proofObligations/{oid}/sequenceRefs/{sidx}",
)
if resolved_refs:
errors += _validate_same_value(
evidence.get("sameValue") or [], resolved_refs, event_by_seq,
f"#/proofObligations/{oid}/sameValue",
)
# 四门失败时不会进入 Actor 取证允许义务数组为空一旦四门通过resolved 义务必须完整。
if floor.get("pass") is True:
for oid in expected_required:
resolved = obligation_by_id.get(oid)
if resolved is None:
errors.append(f"#/proofObligations: 缺少注册表要求的义务 {oid}")
elif resolved.get("required") is not True:
errors.append(f"#/proofObligations: 注册表要求的义务 {oid} 必须标 required=true")
except Exception as exc: # noqa: BLE001 —— canonical 注册表坏掉属于契约校验器自身响亮失败
errors.append(f"#/proofRegistryVersion: 无法加载 canonical 注册表:{exc}")
first_play = instance.get("firstPlay") or {}
for oid in first_play.get("proofObligationRefs") or []:
if oid not in obligation_by_id:
errors.append(f"#/firstPlay/proofObligationRefs: 引用了不存在的义务 {oid}")
first_feedback_at = first_play.get("firstFeedbackAtVirtualMs")
if first_feedback_at is not None:
direct_input_events = [
event for event in events
if event.get("virtualTimeMs") == first_feedback_at
and ((action_by_id.get(event.get("actionId")) or {}).get("normalized") or {}).get("type")
in ("tap", "key", "drag")
]
if not direct_input_events:
errors.append("#/firstPlay/firstFeedbackAtVirtualMs: 必须对应 tap/key/drag 的因果事件wait 不得计入")
rolls = instance.get("rolls") or []
roll_by_id = {}
seen_actor_sessions = set()
for idx, roll in enumerate(rolls):
rid = roll.get("rollId")
if rid in roll_by_id:
errors.append(f"#/rolls/{idx}/rollId: rollId 重复:{rid}")
roll_by_id[rid] = roll
if roll.get("requestedGameSeed") != roll.get("actualGameSeed"):
errors.append(f"#/rolls/{idx}: requestedGameSeed 与 actualGameSeed 不一致")
actor_session = roll.get("actorSessionId")
if actor_session in seen_actor_sessions:
errors.append(f"#/rolls/{idx}/actorSessionId: 两掷不得共用 Actor session")
seen_actor_sessions.add(actor_session)
guard = roll.get("rollGuard") or {}
roll_accept = roll.get("rollOutcome") == "accept"
if guard.get("pass") != roll_accept:
errors.append(f"#/rolls/{idx}: rollGuard.pass 必须与 rollOutcome=accept 等价")
if roll_accept and not _all_checks_true(guard):
errors.append(f"#/rolls/{idx}: accept 的 rollGuard 检查必须全真")
merge = instance.get("merge")
if merge:
for rid in merge.get("rollRefs") or []:
if rid not in roll_by_id:
errors.append(f"#/merge/rollRefs: 引用了不存在的 roll {rid}")
if isinstance(judge, dict):
seen_judge_ids = set()
for idx, row in enumerate(judge.get("obligationResults") or []):
oid = row.get("id")
if oid in seen_judge_ids:
errors.append(f"#/judge/obligationResults/{idx}/id: Judge 义务结果重复")
seen_judge_ids.add(oid)
canonical = obligation_by_id.get(oid)
if canonical is None:
errors.append(f"#/judge/obligationResults/{idx}/id: Judge 引用了未知义务 {oid}")
elif row.get("sequenceRefs") != canonical.get("sequenceRefs"):
errors.append(f"#/judge/obligationResults/{idx}/sequenceRefs: 必须原样引用候选序列")
if not decided:
return errors
outcome = instance.get("outcome")
decision = instance.get("decision") or {}
final_guard = instance.get("finalPostguard") or {}
compatibility = instance.get("compatibility") or {}
repair = instance.get("repair") or {}
failure = decision.get("failure") or {}
playtest = compatibility.get("playtest") or {}
judge_projection = compatibility.get("judge") or {}
if decision.get("outcome") != outcome:
errors.append("#/decision/outcome: 必须与顶层 outcome 一致")
accepted = outcome == "accept"
if decision.get("accepted") != accepted:
errors.append("#/decision/accepted: 必须严格等于 outcome == accept")
expected_frozen = mode in ("v3_shadow", "historical_replay") or not accepted
if decision.get("publishFrozen") != expected_frozen:
errors.append("#/decision/publishFrozen: 必须等于 v3_shadow 或 outcome!=accept")
expected_shadow_accepted = accepted if mode == "v3_shadow" else None
if decision.get("shadowAccepted") != expected_shadow_accepted:
errors.append("#/decision/shadowAccepted: shadow 时镜像 decision.acceptedactive v3 时必须为 null")
if final_guard.get("pass") != accepted:
errors.append("#/finalPostguard/pass: 必须严格等于最终 accept")
if merge and final_guard.get("mergeCandidate") != merge.get("mergeCandidate"):
errors.append("#/finalPostguard/mergeCandidate: 必须与 merge.mergeCandidate 一致")
# merge 只产候选finalPostguard 可以把候选降级为非 accept如成本硬帽/最终复核失败),但绝不能反向升级为 accept。
if accepted and merge and merge.get("mergeCandidate") != "accept":
errors.append("#/outcome: finalPostguard 不得把非 accept mergeCandidate 升级为 accept")
top_cost = instance.get("costRmb")
roll_cost = sum(float(roll.get("costRmb") or 0.0) for roll in rolls)
if abs(float(top_cost or 0.0) - roll_cost) > 1e-9:
errors.append("#/costRmb: 必须等于 rolls[].costRmb 之和")
if decision.get("costRmb") != top_cost:
errors.append("#/decision/costRmb: 必须与顶层 costRmb 一致")
if float(decision.get("parentChainCostRmb") or 0.0) + 1e-9 < float(top_cost or 0.0):
errors.append("#/decision/parentChainCostRmb: 不得小于当前 run costRmb")
if decision.get("rescuedByRoll") != ((merge or {}).get("rescuedByRoll")):
errors.append("#/decision/rescuedByRoll: 必须与 merge.rescuedByRoll 一致")
if decision.get("repairCountAcrossParentChain") != repair.get("countAcrossParentChain"):
errors.append("#/decision/repairCountAcrossParentChain: 必须与 repair.countAcrossParentChain 一致")
if decision.get("repairEligible") != repair.get("eligible"):
errors.append("#/decision/repairEligible: 必须与 repair.eligible 一致")
expected_attempted = repair.get("countAcrossParentChain", 0) > 0
if repair.get("attempted") != expected_attempted:
errors.append("#/repair/attempted: 必须等价于 parent 链已发生过一次 repair")
if decision.get("repairEligible"):
if outcome != "reject" or decision.get("repairCountAcrossParentChain") != 0:
errors.append("#/decision/repairEligible: 仅首次 verified reject 可为 true")
if not decision.get("repairFeedback") or repair.get("reason") != decision.get("repairFeedback"):
errors.append("#/repair/reason: eligible 时必须逐字镜像 decision.repairFeedback")
elif decision.get("repairFeedback") is not None:
errors.append("#/decision/repairFeedback: repairEligible=false 时必须为 null")
expected_layer = "none" if accepted else (
"tester_degraded" if outcome in ("inconclusive", "tester_error")
else ("mechanical" if floor.get("pass") is not True else "gameplay")
)
if failure.get("layer") != expected_layer:
errors.append("#/decision/failure/layer: 与 outcome/floor 的权威投影不一致")
if accepted and (failure.get("subtype") is not None or failure.get("reason") is not None):
errors.append("#/decision/failure: accept 的 subtype/reason 必须为 null")
if not accepted and not failure.get("reason"):
errors.append("#/decision/failure/reason: 非 accept 必须保留权威事实原因")
compatibility_accepted = accepted and mode == "v3"
for field in ("accepted", "ok"):
if compatibility.get(field) != compatibility_accepted:
errors.append(f"#/compatibility/{field}: 仅 active v3 accept 可为 trueshadow 必须冻结")
if compatibility.get("acceptanceVersion") != mode:
errors.append("#/compatibility/acceptanceVersion: 必须与顶层 acceptanceMode 一致")
if compatibility.get("publishFrozen") != expected_frozen:
errors.append("#/compatibility/publishFrozen: 必须与 decision.publishFrozen 一致")
failure_projection = compatibility.get("failureLayer") or {}
if failure_projection.get("layer") != failure.get("layer"):
errors.append("#/compatibility/failureLayer/layer: 必须与 decision.failure.layer 一致")
if failure_projection.get("reason") != failure.get("reason"):
errors.append("#/compatibility/failureLayer/reason: 必须与 decision.failure.reason 一致")
if compatibility.get("failureReason") != failure.get("reason"):
errors.append("#/compatibility/failureReason: 必须与 decision.failure.reason 一致")
failed_gates = [name for name, result in (floor.get("gates") or {}).items()
if result.get("pass") is not True]
expected_failed_gates = failed_gates if failure.get("layer") == "mechanical" else []
if failure_projection.get("failedGates") != expected_failed_gates:
errors.append("#/compatibility/failureLayer/failedGates: 必须精确投影机械失败门")
if playtest.get("outcome") != outcome or playtest.get("accepted") != accepted:
errors.append("#/compatibility/playtest: outcome/accepted 必须由 decision 单向投影")
if playtest.get("rollCount") != len(rolls):
errors.append("#/compatibility/playtest/rollCount: 必须等于 rolls 数量")
if playtest.get("rescuedByRoll") != ((merge or {}).get("rescuedByRoll")):
errors.append("#/compatibility/playtest/rescuedByRoll: 必须与 merge 一致")
if playtest.get("costRmb") != top_cost:
errors.append("#/compatibility/playtest/costRmb: 必须与顶层 costRmb 一致")
compatibility_first_play = playtest.get("firstPlay") or {}
expected_first_play = {
"playableAtMs": first_play.get("interactiveAtVirtualMs"),
"firstFeedbackMs": first_play.get("firstFeedbackAtVirtualMs"),
"loopClosed": first_play.get("loopClosed"),
"proofRefs": first_play.get("proofObligationRefs") or [],
}
if accepted and compatibility_first_play != expected_first_play:
errors.append("#/compatibility/playtest/firstPlay: 必须由顶层 firstPlay 逐字段投影")
for oid in compatibility_first_play.get("proofRefs") or []:
if oid not in obligation_by_id:
errors.append(f"#/compatibility/playtest/firstPlay/proofRefs: 引用了不存在的义务 {oid}")
evidence_refs = playtest.get("evidenceRefs") or {}
for aid in evidence_refs.get("actions") or []:
if aid not in action_by_id:
errors.append(f"#/compatibility/playtest/evidenceRefs/actions: 引用了不存在的动作 {aid}")
for frame in evidence_refs.get("frames") or []:
if frame not in post_frame_paths:
errors.append(f"#/compatibility/playtest/evidenceRefs/frames: 引用了不存在的动作后帧 {frame}")
for seq_text in evidence_refs.get("events") or []:
if int(seq_text) not in event_by_seq:
errors.append(f"#/compatibility/playtest/evidenceRefs/events: 引用了不存在的事件 {seq_text}")
expected_judge_projection = {
"ran": bool(rolls),
"verdict": outcome,
"accepted": accepted,
"degraded": outcome == "tester_error",
"reason": failure.get("reason"),
"acceptanceVersion": mode,
"schemaVersion": "playtest/2",
}
if judge_projection != expected_judge_projection:
errors.append("#/compatibility/judge: 必须是独立 Judge/终裁的固定轻量投影")
trace_projection = compatibility.get("trace") or {}
if trace_projection.get("playtest") != playtest:
errors.append("#/compatibility/trace/playtest: 必须逐字镜像 compatibility.playtest")
if trace_projection.get("gameplayJudge") != judge_projection:
errors.append("#/compatibility/trace/gameplayJudge: 必须逐字镜像 compatibility.judge")
if accepted:
if floor.get("pass") is not True:
errors.append("#/floor/pass: accept 必须四门全绿")
required = [o for o in obligations if o.get("required") is True]
if not required or any(o.get("status") != "satisfied" for o in required):
errors.append("#/proofObligations: accept 的全部 required 义务必须 satisfied")
if any(o.get("contradictions") or o.get("blockingProblems") for o in obligations):
errors.append("#/proofObligations: accept 不能与矛盾或阻塞问题共存")
if judge is None or judge.get("decision") != "accept" or judge.get("degraded") is True:
errors.append("#/judge: accept 必须有健康且建议 accept 的独立 Judge")
if (judge or {}).get("contradictions") or (judge or {}).get("blockingProblems"):
errors.append("#/judge: accept 不能与 Judge 矛盾或阻塞问题共存")
if final_guard.get("contradictions") or final_guard.get("blockingProblems"):
errors.append("#/finalPostguard: accept 不能与矛盾或阻塞问题共存")
if merge and merge.get("conflicts"):
errors.append("#/merge/conflicts: accept 不能与两掷冲突共存")
if not _all_checks_true(final_guard):
errors.append("#/finalPostguard/checks: accept 的最终机械检查必须全真")
if first_play.get("loopClosed") is not True or not first_play.get("proofObligationRefs"):
errors.append("#/firstPlay: accept 必须以 proof obligation 引用证明 loopClosed")
if playtest.get("proofComplete") is not True:
errors.append("#/compatibility/playtest/proofComplete: accept 必须为 true")
if any(not evidence_refs.get(kind) for kind in ("actions", "frames", "events")):
errors.append("#/compatibility/playtest/evidenceRefs: accept 必须保留动作、画面、事件三类引用")
return errors
def _semantic_validate_single_roll(instance: dict) -> list:
"""校验 Node runner 输出的单掷事实,并复用 playtest/2 的证据链不变量。"""
errors = _semantic_validate_environment(instance)
evidence_mode = instance.get("evidenceMode")
evidence_view = {
"phase": "COLLECTING",
"acceptanceMode": "historical_replay" if evidence_mode == "legacy-adapted" else "v3_shadow",
"evidenceMode": evidence_mode,
"genre": instance.get("genre"),
"templateRoute": instance.get("templateRoute"),
"proofProfileId": instance.get("proofProfileId"),
"proofRegistryVersion": instance.get("proofRegistryVersion"),
"acceptanceRequestHash": instance.get("acceptanceRequestHash"),
"artifactHash": instance.get("artifactHash"),
"environment": instance.get("environment"),
"floor": {"pass": True, "gates": {}},
"actor": instance.get("actor"),
"judge": instance.get("judge"),
"actions": instance.get("actions") or [],
"events": instance.get("events") or [],
"briefRuleMatches": instance.get("briefRuleMatches") or [],
"proofObligations": instance.get("proofObligations") or [],
"firstPlay": instance.get("firstPlay") or {},
"rolls": [],
}
errors += _semantic_validate_playtest(evidence_view)
if instance.get("requestedSeed") != instance.get("actualSeed"):
errors.append("#/actualSeed: 必须等于 requestedSeed")
actor = instance.get("actor") or {}
seed_pairs = (
("requestedGameSeed", "requestedSeed"),
("actualGameSeed", "actualSeed"),
("policySeed", "policySeed"),
)
for actor_field, top_field in seed_pairs:
if actor.get(actor_field) != instance.get(top_field):
errors.append(f"#/actor/{actor_field}: 必须与顶层 {top_field} 一致")
action_by_id = {row.get("actionId"): row for row in instance.get("actions") or []}
frame_by_ref = {}
for idx, frame in enumerate(instance.get("frames") or []):
ref = frame.get("ref")
if ref in frame_by_ref:
errors.append(f"#/frames/{idx}/ref: frame ref 重复:{ref}")
frame_by_ref[ref] = frame
if frame.get("artifactHash") != instance.get("artifactHash"):
errors.append(f"#/frames/{idx}/artifactHash: 必须与单掷 artifactHash 一致")
action_id = frame.get("actionId")
if action_id is None:
continue
action = action_by_id.get(action_id)
if action is None:
errors.append(f"#/frames/{idx}/actionId: 引用了不存在的动作 {action_id}")
continue
expected_ref = action.get("postFrameRef") if frame.get("kind") == "post" else action.get("preFrameRef")
expected_ref = expected_ref or {}
if frame.get("ref") != expected_ref.get("path") or frame.get("hash") != expected_ref.get("hash"):
errors.append(f"#/frames/{idx}: frame 必须与动作的 {frame.get('kind')}FrameRef 一致")
for oidx, obligation in enumerate(instance.get("proofObligations") or []):
for sidx, sequence_ref in enumerate(obligation.get("sequenceRefs") or []):
frame_ref = sequence_ref.get("postFrameRef")
frame = frame_by_ref.get(frame_ref)
if frame is None or frame.get("kind") != "post":
errors.append(
f"#/proofObligations/{oidx}/sequenceRefs/{sidx}/postFrameRef: "
"必须引用本单掷封存的动作后帧"
)
judge = instance.get("judge") or {}
if judge.get("packageHash") != instance.get("judgePackageHash"):
errors.append("#/judge/packageHash: 必须等于 judgePackageHash")
judge_decision = judge.get("decision")
failure_class = judge.get("failureClass")
if judge_decision == "accept":
required = [row for row in instance.get("proofObligations") or [] if row.get("required") is True]
if not required or any(row.get("status") != "satisfied" for row in required):
errors.append("#/judge/decision: accept 时全部 required 义务必须 satisfied")
if (failure_class != "none" or judge.get("problems") or judge.get("contradictions")
or judge.get("blockingProblems")):
errors.append("#/judge/decision: accept 必须 failureClass=none 且无问题、矛盾或阻塞项")
elif judge_decision == "reject":
if failure_class not in ("broken", "hollow", "off_brief") or not judge.get("problems"):
errors.append("#/judge/decision: reject 必须有产物 failureClass 和非空 problems")
elif failure_class != "none":
errors.append("#/judge/failureClass: inconclusive/tester_error 必须为 none")
if instance.get("costRmb") != (instance.get("cost") or {}).get("costRmb"):
errors.append("#/costRmb: 必须与 cost.costRmb 一致")
cost = instance.get("cost") or {}
entry_cost = sum(float(row.get("rmb") or 0.0) for row in cost.get("entries") or [])
expected_cost, quantize_errors = _quantize_nonnegative_rmb(entry_cost, "#/cost/entries")
errors += quantize_errors
if not quantize_errors and float(cost.get("costRmb") or 0.0) != expected_cost:
errors.append("#/cost/entries: cost.costRmb 必须严格等于 ROUND_HALF_UP(sum(entries[].rmb), 5)")
return errors
def _semantic_validate_actor_selection(instance: dict, path: str = "#") -> list:
"""校验 ActorSelection/1 里由目标种类决定、schema 难以表达的选择约束。"""
errors = []
selection_type = instance.get("type")
refs = []
if selection_type == "tap":
refs = [(instance.get("target") or {}, f"{path}/target")]
elif selection_type == "drag":
refs = [
(instance.get("from") or {}, f"{path}/from"),
(instance.get("to") or {}, f"{path}/to"),
]
for target, target_path in refs:
target_id = str(target.get("id") or "")
has_cell = "cell" in target
has_anchor = "subAnchor" in target
if target_id.startswith("r") and (has_cell or has_anchor):
errors.append(f"{target_path}: region 只能选择目标 ID不得携带 cell/subAnchor")
if target_id.startswith(("l", "g")) and not has_cell:
errors.append(f"{target_path}: lattice/spatial_grid 必须携带从 1 起的 cell")
if selection_type == "drag" and instance.get("from") == instance.get("to"):
errors.append(f"{path}: drag 起终 targetRef 不得完全相同")
return errors
def _point_in_bounds(point: dict, bounds: dict) -> bool:
"""按像素半开区间判断点是否落在 bounds 内,右/下边界不属于区域。"""
return (
bounds.get("x", 0) <= point.get("x", -1) < bounds.get("x", 0) + bounds.get("width", 0)
and bounds.get("y", 0) <= point.get("y", -1) < bounds.get("y", 0) + bounds.get("height", 0)
)
def _semantic_validate_visual_target_set(instance: dict) -> list:
"""校验 OpenCV 与 producer-seeded 两种 TargetSet 的输入绑定、预算和几何。"""
errors = []
generator = instance.get("generator") or {}
generator_id = generator.get("id")
expected_version = {
"opencv-visual-targets": "1.0.1",
"match3-producer-seeded-targets": "1.0.0",
}.get(generator_id)
if expected_version is None:
errors.append("#/generator/id: 未知 generator identity")
elif generator.get("version") != expected_version:
errors.append("#/generator/version: generator identity 与版本不匹配")
producer_capability, producer_geometry, producer_geometry_hash = _canonical_producer_geometry()
if generator_id == "match3-producer-seeded-targets":
expected_producer_hash = _producer_generator_config_hash(producer_capability)
if expected_producer_hash != _PRODUCER_SEEDED_GENERATOR_CONFIG_HASH:
errors.append("#/generator/configHash: validator 内置 producer config 与锁版 hash 漂移")
if generator.get("configHash") != expected_producer_hash:
errors.append("#/generator/configHash: producer-seeded 算法或 authority hash 不匹配")
producer_source_count = 0
targets = instance.get("targets") or []
target_ids = [row.get("id") for row in targets]
if len(target_ids) != len(set(target_ids)):
errors.append("#/targets: target id 必须全局唯一")
kinds = [row.get("kind") for row in targets]
kind_order = {"lattice": 0, "region": 1, "spatial_grid": 2}
if kinds != sorted(kinds, key=lambda kind: kind_order.get(kind, 99)):
errors.append("#/targets: 必须按 lattice、region、spatial_grid 的确定性种类顺序输出")
if sum(1 for kind in kinds if kind == "region") > 24:
errors.append("#/targets: region 候选不得超过 24 个")
if sum(1 for kind in kinds if kind == "lattice") > 4:
errors.append("#/targets: lattice 候选不得超过 4 个")
if [row.get("id") for row in targets if row.get("kind") == "spatial_grid"] != ["g01"]:
errors.append("#/targets: 必须且只能包含永久降级面 g01")
expected_ids = {
"lattice": [f"l{index:02d}" for index in range(1, kinds.count("lattice") + 1)],
"region": [f"r{index:02d}" for index in range(1, kinds.count("region") + 1)],
"spatial_grid": ["g01"],
}
for kind, ids in expected_ids.items():
actual_ids = [row.get("id") for row in targets if row.get("kind") == kind]
if actual_ids != ids:
errors.append(f"#/targets: {kind} id 必须按稳定顺序连续编号 {ids}")
for idx, target in enumerate(targets):
path = f"#/targets/{idx}"
bounds = target.get("bounds") or {}
if bounds.get("x", 0) + bounds.get("width", 0) > 390:
errors.append(f"{path}/bounds: 右边界越出 390px 视口")
if bounds.get("y", 0) + bounds.get("height", 0) > 844:
errors.append(f"{path}/bounds: 下边界越出 844px 视口")
gestures = target.get("allowedGestures") or []
if len(gestures) != len(set(gestures)):
errors.append(f"{path}/allowedGestures: gesture 不得重复")
if target.get("kind") == "region" and not _point_in_bounds(target.get("safePoint") or {}, bounds):
errors.append(f"{path}/safePoint: 必须落在 region bounds 内")
safe_point = target.get("safePoint") or {}
if safe_point and (safe_point.get("x", 390) >= 390 or safe_point.get("y", 844) >= 844):
errors.append(f"{path}/safePoint: 必须落在有效像素索引范围 x<390、y<844")
if target.get("kind") == "lattice":
dimensions = target.get("dimensions") or {}
rows, columns = dimensions.get("rows"), dimensions.get("columns")
row_centers = target.get("rowCenters") or []
column_centers = target.get("columnCenters") or []
if len(row_centers) != rows:
errors.append(f"{path}/rowCenters: 数量必须等于 dimensions.rows")
if len(column_centers) != columns:
errors.append(f"{path}/columnCenters: 数量必须等于 dimensions.columns")
if any(right <= left for left, right in zip(row_centers, row_centers[1:])):
errors.append(f"{path}/rowCenters: 必须严格递增")
if any(right <= left for left, right in zip(column_centers, column_centers[1:])):
errors.append(f"{path}/columnCenters: 必须严格递增")
observed = target.get("observedCells")
valid_observed_shape = isinstance(observed, list) and all(
isinstance(cell, dict) and set(cell) == {"row", "column"}
for cell in observed
)
observed_keys = [
(cell["row"], cell["column"])
for cell in observed
] if valid_observed_shape else []
expected_order = sorted(observed, key=lambda cell: (cell["row"], cell["column"])) \
if valid_observed_shape else []
in_bounds = valid_observed_shape and all(
isinstance(row, int) and not isinstance(row, bool)
and isinstance(column, int) and not isinstance(column, bool)
and 1 <= row <= (rows or 0) and 1 <= column <= (columns or 0)
for row, column in observed_keys
)
if not valid_observed_shape or observed != expected_order \
or len(set(observed_keys)) != len(observed_keys) or not in_bounds:
errors.append(f"{path}/observedCells: 必须是 dimensions 内行优先唯一子集")
row_support = [sum(1 for row, _column in observed_keys if row == value)
for value in range(1, (rows or 0) + 1)]
column_support = [sum(1 for _row, column in observed_keys if column == value)
for value in range(1, (columns or 0) + 1)]
if row_support and (min(row_support) < 3 or min(column_support) < 3):
errors.append(f"{path}/observedCells: 每行每列至少需要 3 个直接支持点")
expected_coverage = round(
len(observed_keys) * 1000 / max(1, (rows or 0) * (columns or 0)),
)
if not valid_observed_shape \
or (target.get("metrics") or {}).get("coveragePermille") != expected_coverage:
errors.append(f"{path}/metrics/coveragePermille: 必须与 observedCells 覆盖率一致")
geometry_source = target.get("geometrySource")
if generator_id == "match3-producer-seeded-targets":
producer_source_count += 1
expected_source = {
"mode": "producer_capability",
"capabilityId": producer_capability.get("id"),
"capabilityVersion": producer_capability.get("version"),
"capabilityConfigHash": producer_capability.get("configHash"),
"geometryHash": producer_geometry_hash,
}
if geometry_source != expected_source:
errors.append(f"{path}/geometrySource: 必须逐字匹配 registry capability 与 canonical geometry")
if target.get("dimensions") != producer_geometry.get("dimensions") \
or target.get("rowCenters") != producer_geometry.get("rowCenters") \
or target.get("columnCenters") != producer_geometry.get("columnCenters"):
errors.append(f"{path}: producer lattice 几何必须逐字匹配 registry")
grid = ((producer_capability.get("config") or {}).get("grid") or {})
expected_bounds = {
"x": grid.get("originX"), "y": grid.get("originY"),
"width": grid.get("columns", 0) * grid.get("pitchPx", 0),
"height": grid.get("rows", 0) * grid.get("pitchPx", 0),
}
if target.get("bounds") != expected_bounds:
errors.append(f"{path}/bounds: producer lattice bounds 必须逐字匹配 registry")
if len(observed_keys) != (producer_geometry.get("dimensions", {}).get("rows", 0)
* producer_geometry.get("dimensions", {}).get("columns", 0)):
errors.append(f"{path}/observedCells: producer lattice 必须逐格通过当前 PNG 支撑扫描")
elif geometry_source is not None:
errors.append(f"{path}/geometrySource: OpenCV lattice 不得携带 producer provenance")
tolerance = 3
x1, y1 = bounds.get("x", 0), bounds.get("y", 0)
x2 = x1 + bounds.get("width", 0)
y2 = y1 + bounds.get("height", 0)
if not all(x1 - tolerance <= value <= x2 + tolerance for value in column_centers):
errors.append(f"{path}/columnCenters: 必须落在 lattice bounds 容差内")
if not all(y1 - tolerance <= value <= y2 + tolerance for value in row_centers):
errors.append(f"{path}/rowCenters: 必须落在 lattice bounds 容差内")
canonical = {
"schemaVersion": instance.get("schemaVersion"),
"sourceFrameHash": instance.get("sourceFrameHash"),
"viewport": instance.get("viewport"),
"generator": instance.get("generator"),
"targets": instance.get("targets"),
}
canonical_bytes = json.dumps(
canonical, ensure_ascii=False, sort_keys=True, separators=(",", ":"), allow_nan=False,
).encode("utf-8")
expected_hash = hashlib.sha256(b"VisualTargetSet/1\x00" + canonical_bytes).hexdigest()
if instance.get("targetSetHash") != expected_hash:
errors.append("#/targetSetHash: 与 VisualTargetSet canonical 内容不一致")
if generator_id == "match3-producer-seeded-targets" and producer_source_count != 1:
errors.append("#/targets: producer-seeded TargetSet 必须且只能包含一个 producer lattice")
if generator_id == "opencv-visual-targets" and producer_source_count:
errors.append("#/targets: OpenCV TargetSet 不得包含 producer lattice")
return errors
def _semantic_validate_target_resolution(instance: dict, path: str = "#") -> list:
"""校验 TargetResolution/1 的选择绑定、点角色和退化 drag。"""
errors = []
status = instance.get("status")
selection = instance.get("selection")
if not isinstance(selection, dict):
return errors
errors += _semantic_validate_actor_selection(selection, f"{path}/selection")
selection_type = selection.get("type")
if selection_type in ("key", "wait"):
errors.append(f"{path}/selection: key/wait 只能使用 status=not_applicable")
return errors
if status not in ("resolved", "failed"):
errors.append(f"{path}/status: tap/drag 只能是 resolved 或 failed")
return errors
if instance.get("targetSetHash") != selection.get("targetSetHash"):
errors.append(f"{path}/targetSetHash: 必须与 selection.targetSetHash 相同")
if status == "failed":
return errors
points = instance.get("resolvedPoints") or []
for idx, row in enumerate(points):
point = row.get("point") or {}
if point.get("x", 390) >= 390 or point.get("y", 844) >= 844:
errors.append(f"{path}/resolvedPoints/{idx}/point: 必须落在有效像素索引范围 x<390、y<844")
if selection_type == "tap":
expected = [("target", selection.get("target"))]
else:
expected = [("from", selection.get("from")), ("to", selection.get("to"))]
actual = [(row.get("role"), row.get("target")) for row in points]
if actual != expected:
errors.append(f"{path}/resolvedPoints: 点角色和 targetRef 必须逐项镜像 selection")
if selection_type == "drag" and len(points) == 2 and points[0].get("point") == points[1].get("point"):
errors.append(f"{path}/resolvedPoints: drag 解析后的起终坐标不得相同")
return errors
def _semantic_validate_cost_reservation(instance: dict) -> list:
"""校验 CostReservation/1 的 A/B 原子预留和 usage 上界。"""
errors = []
slots = instance.get("slots") or []
slot_names = [row.get("slot") for row in slots]
if sorted(slot_names) != ["judge-a", "judge-b"]:
errors.append("#/slots: 必须恰好各预留一次 judge-a 与 judge-b")
for field in ("reservedRmb", "retryReservedRmb"):
actual, actual_errors = _quantize_nonnegative_rmb(instance.get(field), f"#/{field}")
errors += actual_errors
total = sum(float(row.get(field) or 0) for row in slots)
expected, total_errors = _quantize_nonnegative_rmb(total, f"#/slots/*/{field}")
errors += total_errors
if not actual_errors and not total_errors and actual != expected:
errors.append(f"#/{field}: 必须等于两槽 {field} 的五位半入和")
for idx, slot in enumerate(slots):
if slot.get("inputTokenUpperBound") != slot.get("requestBytes"):
errors.append(
f"#/slots/{idx}/inputTokenUpperBound: 必须等于完整 HTTP request body 的 UTF-8 字节数"
)
slot_by_name = {row.get("slot"): row for row in slots}
seen_attempts = set()
for idx, settlement in enumerate(instance.get("settlements") or []):
path = f"#/settlements/{idx}"
slot_name = settlement.get("slot")
attempt_key = (slot_name, settlement.get("attempt"))
if attempt_key in seen_attempts:
errors.append(f"{path}: 同一槽同一次 attempt 不得重复结算")
seen_attempts.add(attempt_key)
upper = (slot_by_name.get(slot_name) or {}).get("inputTokenUpperBound")
if upper is not None and settlement.get("promptTokens", 0) >= upper:
errors.append(f"{path}/promptTokens: 必须严格小于预留时的保守 input token 上界")
if settlement.get("attempt") == 2:
first = next(
(row for row in instance.get("settlements") or []
if row.get("slot") == slot_name and row.get("attempt") == 1),
None,
)
if first is None or first.get("contentReceived") is not False:
errors.append(f"{path}/attempt: 仅第一次未收到任何模型内容时允许同槽重试")
if (slot_by_name.get(slot_name) or {}).get("retryReservedRmb", 0) <= 0:
errors.append(f"{path}/attempt: 第二次调用前必须完成该槽追加预留")
if settlement.get("contentReceived") is True and not settlement.get("providerRequestId"):
errors.append(f"{path}/providerRequestId: 收到模型内容时必须保留 provider request id")
if instance.get("status") == "reserved" and instance.get("settlements"):
errors.append("#/status: reserved 状态不得已有 settlements")
if instance.get("status") == "reserved" and instance.get("retryReservedRmb") != 0:
errors.append("#/retryReservedRmb: 初始 reserved 阶段不得提前占用重试额度")
return errors
def _semantic_validate_v3_action(action: dict, path: str) -> list:
"""校验 playtest/3 正式动作的 selection、resolution、normalized 与时钟闭环。"""
errors = []
selection = ((action.get("request") or {}).get("parsed") or {})
resolution = action.get("targetResolution") or {}
normalized = action.get("normalized") or {}
selection_type = selection.get("type")
protocol_errors = action.get("protocolErrors") or []
if action.get("retryCount") != len(protocol_errors):
errors.append(f"{path}/retryCount: 必须严格等于 protocolErrors 数量")
errors += _semantic_validate_actor_selection(selection, f"{path}/request/parsed")
if selection_type in ("tap", "drag"):
if resolution.get("status") != "resolved":
errors.append(f"{path}/targetResolution: 正式 tap/drag 动作必须 resolved")
else:
errors += _semantic_validate_target_resolution(resolution, f"{path}/targetResolution")
if resolution.get("selection") != selection:
errors.append(f"{path}/targetResolution/selection: 必须逐字镜像 request.parsed")
if normalized.get("type") != selection_type:
errors.append(f"{path}/normalized/type: 必须与 selection.type 相同")
points = resolution.get("resolvedPoints") or []
if selection_type == "tap" and len(points) == 1:
if normalized.get("x") != (points[0].get("point") or {}).get("x") or normalized.get("y") != (points[0].get("point") or {}).get("y"):
errors.append(f"{path}/normalized: tap 坐标必须来自 resolved target 点")
if selection_type == "drag" and len(points) == 2:
if normalized.get("from") != points[0].get("point") or normalized.get("to") != points[1].get("point"):
errors.append(f"{path}/normalized: drag 坐标必须逐项来自 resolved from/to 点")
elif selection_type in ("key", "wait"):
if resolution != {"status": "not_applicable"}:
errors.append(f"{path}/targetResolution: key/wait 必须精确等于 not_applicable")
if normalized.get("type") != selection_type:
errors.append(f"{path}/normalized/type: 必须与 selection.type 相同")
mirror_field = "key" if selection_type == "key" else "ms"
if normalized.get(mirror_field) != selection.get(mirror_field):
errors.append(f"{path}/normalized/{mirror_field}: 必须镜像 selection")
expected_budget = selection.get("ms") if selection_type == "wait" else 600
if action.get("virtualTimeBudgetMs") != expected_budget:
errors.append(f"{path}/virtualTimeBudgetMs: 必须等于动作预算 {expected_budget}")
advance = action.get("timeAdvance") or {}
if advance.get("requestedBudgetMs") != expected_budget:
errors.append(f"{path}/timeAdvance/requestedBudgetMs: 必须等于动作预算")
if advance.get("plannedFrames") != advance.get("executedFrames"):
errors.append(f"{path}/timeAdvance: plannedFrames 必须等于 executedFrames")
if action.get("postEventSeq", 0) < action.get("preEventSeq", 0):
errors.append(f"{path}/postEventSeq: 不得早于 preEventSeq")
return errors
def _semantic_validate_playtest_v3(instance: dict) -> list:
"""校验 playtest/3 的结构化动作、双 Judge 与 source roll 单向投影。"""
errors = _semantic_validate_environment(instance, wait_max_ms=600)
errors += _semantic_validate_acceptance_profile(instance)
if (instance.get("environment") or {}).get("buildRef") != instance.get("artifactHash"):
errors.append("#/environment/buildRef: 必须等于顶层 artifactHash")
for idx, action in enumerate(instance.get("actions") or []):
errors += _semantic_validate_v3_action(action, f"#/actions/{idx}")
rolls = instance.get("rolls") or []
roll_by_id = {}
for idx, roll in enumerate(rolls):
path = f"#/rolls/{idx}"
roll_id = roll.get("rollId")
if roll_id in roll_by_id:
errors.append(f"{path}/rollId: rollId 不得重复")
roll_by_id[roll_id] = roll
if roll_id != f"roll-{roll.get('roll')}":
errors.append(f"{path}/rollId: 必须与 roll 数字一致")
if roll.get("requestedGameSeed") != roll.get("actualGameSeed"):
errors.append(f"{path}/actualGameSeed: 必须等于 requestedGameSeed")
a_ref, b_ref = roll.get("judgeARef") or {}, roll.get("judgeBRef") or {}
for field in ("rawRef", "parsedRef", "normalizedRef"):
if a_ref.get(field) == b_ref.get(field):
errors.append(f"{path}: Judge A/B 的 {field} 必须隔离")
source_roll_id = instance.get("sourceRollId")
judge = instance.get("judge")
if judge is None:
if source_roll_id is not None:
errors.append("#/sourceRollId: judge=null 时必须为 null")
else:
if judge.get("sourceRollId") != source_roll_id:
errors.append("#/judge/sourceRollId: 必须镜像顶层 sourceRollId")
if judge.get("sameModel") != (judge.get("independenceChecks") or {}).get("sameModel"):
errors.append("#/judge/sameModel: 必须镜像 independenceChecks.sameModel")
source_roll = roll_by_id.get(source_roll_id)
if source_roll_id is not None and source_roll is None:
errors.append("#/sourceRollId: 必须引用本 run 已封存的 roll")
if source_roll is not None:
mirror_pairs = (
("consensusRef", source_roll.get("judgeConsensusRef")),
("consensusHash", source_roll.get("judgeConsensusHash")),
("packageHash", source_roll.get("judgePackageHash")),
("judgeAResultHash", (source_roll.get("judgeARef") or {}).get("resultHash")),
("judgeBResultHash", (source_roll.get("judgeBRef") or {}).get("resultHash")),
)
for judge_field, expected in mirror_pairs:
if judge.get(judge_field) != expected:
errors.append(f"#/judge/{judge_field}: 必须镜像 source roll 的封存事实")
if source_roll_id is None and judge.get("decision") in ("accept", "reject"):
errors.append("#/judge/decision: 无 source roll 时不能投影 accept/reject")
roll_cost = sum(float(row.get("costRmb") or 0) for row in rolls)
expected_cost, cost_errors = _quantize_nonnegative_rmb(roll_cost, "#/rolls/*/costRmb")
actual_cost, actual_errors = _quantize_nonnegative_rmb(instance.get("costRmb"), "#/costRmb")
errors += cost_errors + actual_errors
if not cost_errors and not actual_errors and expected_cost != actual_cost:
errors.append("#/costRmb: 必须等于 rolls 成本五位半入和")
if instance.get("phase") == "DECIDED":
decision = instance.get("decision") or {}
outcome = instance.get("outcome")
if decision.get("outcome") != outcome or decision.get("accepted") != (outcome == "accept"):
errors.append("#/decision: outcome/accepted 必须由顶层 outcome 单向投影")
compatibility = instance.get("compatibility") or {}
if compatibility.get("sourceRollId") != source_roll_id:
errors.append("#/compatibility/sourceRollId: 必须镜像顶层 sourceRollId")
if outcome == "accept" and (judge or {}).get("decision") != "accept":
errors.append("#/judge/decision: 最终 accept 必须来自 source roll 的 consensus accept")
return errors
def _semantic_validate_single_roll_v3(instance: dict) -> list:
"""校验 playtest/3 Node→Python 单掷事实,不借用冻结 v2 的 Judge/动作语义。"""
errors = _semantic_validate_environment(instance, wait_max_ms=600)
errors += _semantic_validate_acceptance_profile(instance)
# 事件因果、payloadCanonical、proof 引用、sameValue 与 orderedSeries 与协议代际无关。
# 复用冻结 v2 的事实层校验,但显式移除 Actor/Judge避免把旧单 Judge 身份语义套到双 Judge。
errors += _semantic_validate_playtest({
"phase": "COLLECTING",
"acceptanceMode": "historical_replay"
if instance.get("evidenceMode") == "legacy-adapted" else "v3_shadow",
"evidenceMode": instance.get("evidenceMode"),
"genre": instance.get("genre"),
"templateRoute": instance.get("templateRoute"),
"proofProfileId": instance.get("proofProfileId"),
"proofRegistryVersion": instance.get("proofRegistryVersion"),
"acceptanceRequestHash": instance.get("acceptanceRequestHash"),
"artifactHash": instance.get("artifactHash"),
"environment": instance.get("environment"),
# 单掷 schema 允许用于局部 fixturerequired 完整性由生产 registry/guard 另查,
# 这里关掉 floorPass 只跳过“必须出现全部义务”,不跳过已出现义务的关系校验。
"floor": {"pass": False, "gates": {}},
"actor": None,
"judge": None,
"actions": instance.get("actions") or [],
"events": instance.get("events") or [],
"briefRuleMatches": instance.get("briefRuleMatches") or [],
"proofObligations": instance.get("proofObligations") or [],
"firstPlay": instance.get("firstPlay") or {},
"rolls": [],
}, wait_max_ms=600)
if (instance.get("environment") or {}).get("buildRef") != instance.get("artifactHash"):
errors.append("#/environment/buildRef: 必须等于单掷 artifactHash")
if instance.get("rollId") != f"roll-{instance.get('roll')}":
errors.append("#/rollId: 必须与 roll 数字一致")
if instance.get("requestedSeed") != instance.get("actualSeed"):
errors.append("#/actualSeed: 必须等于 requestedSeed")
actor = instance.get("actor") or {}
seed_mirrors = (
("requestedGameSeed", "requestedSeed"),
("actualGameSeed", "actualSeed"),
("policySeed", "policySeed"),
)
for actor_field, top_field in seed_mirrors:
if actor.get(actor_field) != instance.get(top_field):
errors.append(f"#/actor/{actor_field}: 必须镜像顶层 {top_field}")
actions = instance.get("actions") or []
action_by_id = {}
for idx, action in enumerate(actions):
action_id = action.get("actionId")
if action_id in action_by_id:
errors.append(f"#/actions/{idx}/actionId: actionId 不得重复")
action_by_id[action_id] = action
errors += _semantic_validate_v3_action(action, f"#/actions/{idx}")
frame_refs = set()
for idx, frame in enumerate(instance.get("frames") or []):
path = f"#/frames/{idx}"
frame_ref = frame.get("ref")
if frame_ref in frame_refs:
errors.append(f"{path}/ref: frame ref 不得重复")
frame_refs.add(frame_ref)
if frame.get("artifactHash") != instance.get("artifactHash"):
errors.append(f"{path}/artifactHash: 必须等于单掷 artifactHash")
action = action_by_id.get(frame.get("actionId"))
if action is None:
errors.append(f"{path}/actionId: 引用了不存在的正式动作")
continue
expected_ref = action.get("postFrameRef") if frame.get("kind") == "post" else action.get("preFrameRef")
expected_ref = expected_ref or {}
if frame_ref != expected_ref.get("path") or frame.get("hash") != expected_ref.get("hash"):
errors.append(f"{path}: 必须逐字镜像动作的 {frame.get('kind')}FrameRef")
a_ref, b_ref = instance.get("judgeARef") or {}, instance.get("judgeBRef") or {}
for field in ("rawRef", "parsedRef", "normalizedRef"):
if a_ref.get(field) == b_ref.get(field):
errors.append(f"#/judgeBRef/{field}: Judge A/B 文件必须隔离")
judge = instance.get("judge") or {}
if judge.get("sourceRollId") != instance.get("rollId"):
errors.append("#/judge/sourceRollId: 必须等于当前单掷 rollId")
if judge.get("sameModel") != (judge.get("independenceChecks") or {}).get("sameModel"):
errors.append("#/judge/sameModel: 必须镜像 independenceChecks.sameModel")
judge_mirrors = (
("consensusRef", instance.get("judgeConsensusRef")),
("consensusHash", instance.get("judgeConsensusHash")),
("packageHash", instance.get("judgePackageHash")),
("judgeAResultHash", a_ref.get("resultHash")),
("judgeBResultHash", b_ref.get("resultHash")),
)
for judge_field, expected in judge_mirrors:
if judge.get(judge_field) != expected:
errors.append(f"#/judge/{judge_field}: 必须镜像单掷封存事实")
cost = instance.get("cost") or {}
if instance.get("costRmb") != cost.get("costRmb"):
errors.append("#/costRmb: 必须与 cost.costRmb 一致")
entry_cost = sum(float(row.get("rmb") or 0) for row in cost.get("entries") or [])
expected_cost, expected_errors = _quantize_nonnegative_rmb(entry_cost, "#/cost/entries")
actual_cost, actual_errors = _quantize_nonnegative_rmb(cost.get("costRmb"), "#/cost/costRmb")
errors += expected_errors + actual_errors
if not expected_errors and not actual_errors and expected_cost != actual_cost:
errors.append("#/cost/entries: costRmb 必须等于明细五位半入和")
return errors
def _canonical_domain_hash(domain_label: str, value) -> str:
"""按 Node stableStringify 的口径计算带域标签 SHA-256。"""
canonical = json.dumps(
value, ensure_ascii=False, sort_keys=True, separators=(",", ":"), allow_nan=False,
)
return hashlib.sha256(f"{domain_label}\n{canonical}".encode("utf-8")).hexdigest()
def _canonical_producer_geometry() -> tuple[dict, dict, str]:
"""从 interaction registry authority 复算 producer lattice 几何身份。"""
registry = _load(_HERE / "interaction-profiles.v2.json")
capability = (((registry.get("profiles") or {}).get("match3.orthogonal-swap-v1") or {})
.get("producerCapability") or {})
grid = ((capability.get("config") or {}).get("grid") or {})
rows, columns = grid.get("rows"), grid.get("columns")
pitch, origin_x, origin_y = grid.get("pitchPx"), grid.get("originX"), grid.get("originY")
if not all(isinstance(value, int) for value in (rows, columns, pitch, origin_x, origin_y)):
return capability, {}, ""
geometry = {
"dimensions": {"rows": rows, "columns": columns},
"rowCenters": [origin_y + row * pitch + pitch // 2 for row in range(rows)],
"columnCenters": [origin_x + column * pitch + pitch // 2 for column in range(columns)],
}
return capability, geometry, _canonical_domain_hash("match3-lattice-geometry/1", geometry)
def _producer_generator_config_hash(capability: dict) -> str:
"""独立复算 renderer 的 generatorConfig/1\0 producer-seeded hash。"""
value = {
"schemaVersion": "generatorConfig/1",
"opencv": _PRODUCER_OPENCV_CONFIG,
"producerSeed": {
"algorithmVersion": _PRODUCER_LATTICE_ALGORITHM_VERSION,
"capability": capability,
},
}
canonical = json.dumps(value, ensure_ascii=False, sort_keys=True, separators=(",", ":"), allow_nan=False)
return hashlib.sha256(b"generatorConfig/1\x00" + canonical.encode("utf-8")).hexdigest()
def _semantic_validate_interaction_registry(instance: dict) -> list:
"""交互注册表必须与 proof profile 对账,并封存规则配置及整表哈希。"""
errors = []
profiles = instance.get("profiles") or {}
if set(profiles) != {"match3.orthogonal-swap-v1"}:
errors.append("#/profiles: 第一版必须且只能登记 match3.orthogonal-swap-v1")
return errors
profile = profiles.get("match3.orthogonal-swap-v1") or {}
if profile.get("id") != "match3.orthogonal-swap-v1":
errors.append("#/profiles/match3.orthogonal-swap-v1/id: 必须与注册表键一致")
proof_registry = _load(_HERE / "proof-obligations.v2.json")
proof_profile = (proof_registry.get("profiles") or {}).get(profile.get("proofProfileId"))
path = "#/profiles/match3.orthogonal-swap-v1"
if not isinstance(proof_profile, dict):
errors.append(f"{path}/proofProfileId: 未命中 canonical proof profile")
else:
if profile.get("genre") != proof_profile.get("genre"):
errors.append(f"{path}/genre: 必须与 canonical proof profile 一致")
if profile.get("templateRoute") != proof_profile.get("templateRoute"):
errors.append(f"{path}/templateRoute: 必须与 canonical proof profile 一致")
solver = profile.get("solver") or {}
expected_config_hash = _canonical_domain_hash(
"match3-solver-config/1", solver.get("config") or {},
)
if solver.get("configHash") != expected_config_hash:
errors.append(f"{path}/solver/configHash: 与版本化 solver config 不一致")
registry_content = {key: deepcopy(value) for key, value in instance.items() if key != "registryHash"}
expected_registry_hash = _canonical_domain_hash(
"interaction-profile-registry/1", registry_content,
)
if instance.get("registryHash") != expected_registry_hash:
errors.append("#/registryHash: 与 canonical 注册表内容不一致")
return errors
def _semantic_validate_interaction_registry_v2(instance: dict) -> list:
"""View/4 enforce registry 必须绑定 proof profile并封存完整协议、感知和恢复策略。"""
errors = []
profile = (instance.get("profiles") or {}).get("match3.orthogonal-swap-v1") or {}
proof_profile = (
(_load(_HERE / "proof-obligations.v2.json").get("profiles") or {})
.get(profile.get("proofProfileId")) or {}
)
for field in ("genre", "templateRoute"):
if profile.get(field) != proof_profile.get(field):
errors.append(f"#/profiles/match3.orthogonal-swap-v1/{field}: 必须与 proof profile 一致")
components = [
profile.get("solver") or {},
profile.get("producerCapability") or {},
((profile.get("actorProtocol") or {}).get("parser") or {}),
((profile.get("actorProtocol") or {}).get("resolver") or {}),
profile.get("overlayNormalizer") or {},
profile.get("equivalenceAlgorithm") or {},
]
for component in components:
expected_hash = _canonical_domain_hash(
f"interaction-component-config/{component.get('id')}/1",
component.get("config") or {},
)
if component.get("configHash") != expected_hash:
errors.append(
"#/profiles/match3.orthogonal-swap-v1: "
f"{component.get('id')} configHash 与 strict config 不一致",
)
prompt = ((profile.get("actorProtocol") or {}).get("prompt") or {})
prompt_ref = prompt.get("artifactRef")
prompt_path = (_HERE / str(prompt_ref)).resolve()
if prompt_path.parent != (_HERE / "artifacts").resolve() or not prompt_path.is_file():
errors.append("#/profiles/match3.orthogonal-swap-v1/actorProtocol/prompt/artifactRef: 工件不存在或越界")
elif hashlib.sha256(prompt_path.read_bytes()).hexdigest() != prompt.get("hash"):
errors.append("#/profiles/match3.orthogonal-swap-v1/actorProtocol/prompt/hash: 与工件原始字节不一致")
content = {key: deepcopy(value) for key, value in instance.items() if key != "registryHash"}
expected = _canonical_domain_hash("interaction-profile-registry/2", content)
if instance.get("registryHash") != expected:
errors.append("#/registryHash: 与 View/4 enforce registry canonical 内容不一致")
return errors
def _semantic_validate_match3_producer_preflight(instance: dict) -> list:
"""生产者 preflight 必须把能力身份、隔离事实和退出分类封成一份不可重签洗白的事实。"""
errors = []
profile = (
(_load(_HERE / "interaction-profiles.v2.json").get("profiles") or {})
.get(instance.get("interactionProfileId")) or {}
)
canonical_capability = profile.get("producerCapability") or {}
expected = instance.get("expectedCapability") or {}
probed = instance.get("probedCapability")
def capability_hash_ok(capability: dict) -> bool:
expected_hash = _canonical_domain_hash(
f"interaction-component-config/{capability.get('id')}/1",
capability.get("config") or {},
)
return capability.get("configHash") == expected_hash
expected_matches_registry = expected == canonical_capability
probed_hash_ok = isinstance(probed, dict) and capability_hash_ok(probed)
probed_matches_registry = probed == canonical_capability
binding_content = {
"interactionProfileId": instance.get("interactionProfileId"),
"interactionRegistryVersion": instance.get("interactionRegistryVersion"),
"taskBindingHash": instance.get("taskBindingHash"),
}
expected_binding_hash = _canonical_domain_hash("interaction-binding/1", binding_content)
binding_matches = instance.get("interactionBindingHash") == expected_binding_hash
# 身份漂移本身是一种可封存的 tester_error 事实,只有伪装成 compatible/incompatible 才拒绝。
stages = [
"environment_setup", "browser_connected", "capability_probed",
"initial_frame_captured", "first_action_dispatched",
"post_frame_captured", "normalizer_completed",
]
stage = instance.get("executionStage")
stage_index = stages.index(stage) if stage in stages else -1
capability_was_probed = stage_index >= stages.index("capability_probed")
profile_error = (
not expected_matches_registry or not binding_matches
or (capability_was_probed and (not probed_hash_ok or not probed_matches_registry))
)
# producer grid 的 origin 是棋格左上角lattice 证据绑定的是逐格中心坐标。
grid = (canonical_capability.get("config") or {}).get("grid") or {}
rows = grid.get("rows")
columns = grid.get("columns")
pitch = grid.get("pitchPx")
origin_x = grid.get("originX")
origin_y = grid.get("originY")
def center_axis(origin, count):
if not all(isinstance(value, int) for value in (origin, count, pitch)):
return []
return [origin + index * pitch + pitch // 2 for index in range(count)]
canonical_geometry = {
"dimensions": {"rows": rows, "columns": columns},
"rowCenters": center_axis(origin_y, rows),
"columnCenters": center_axis(origin_x, columns),
}
canonical_geometry_hash = _canonical_domain_hash(
"match3-lattice-geometry/1", canonical_geometry,
)
event_interval = instance.get("eventInterval") or {}
pre_seq = event_interval.get("preEventSeq")
post_seq = event_interval.get("postEventSeq")
if isinstance(pre_seq, int) and isinstance(post_seq, int) and post_seq < pre_seq:
errors.append("#/eventInterval/postEventSeq: 不得早于 preEventSeq")
endpoint = instance.get("selectedEndpoint") or {}
lattice = instance.get("lattice") or {}
if endpoint and lattice and endpoint.get("latticeId") != lattice.get("latticeId"):
errors.append("#/selectedEndpoint/latticeId: 必须绑定 preflight lattice")
normalizer = instance.get("normalizerAudit") or {}
normalizer_not_run = normalizer.get("status") == "not_run"
if normalizer_not_run and any(normalizer.get(field) is not None for field in (
"ref", "hash", "changedPixelCount", "outsideSelectedCellCount",
"maximumReplayResidual", "supportMaskHash",
)):
errors.append("#/normalizerAudit: not_run 不得伪造 ref/hash 或像素结果")
if not normalizer_not_run and any(normalizer.get(field) is None for field in (
"ref", "hash", "changedPixelCount", "outsideSelectedCellCount",
"maximumReplayResidual",
)):
errors.append("#/normalizerAudit: 已运行 normalizer 必须封存 ref/hash 与完整数值")
# executionStage 是已完成的最后边界;未来阶段的证据不得提前出现。
stage_evidence = (
("capability_probed", ("probedCapability",)),
("initial_frame_captured", ("initialFrame",)),
("first_action_dispatched", ("selectedEndpoint", "lattice", "swapSet")),
("post_frame_captured", ("postFrame", "eventInterval")),
)
for required_stage, fields in stage_evidence:
if stage_index < stages.index(required_stage):
for field in fields:
if instance.get(field) is not None:
errors.append(f"#/{field}: executionStage={stage} 时不得提前出现")
if stage_index < stages.index("normalizer_completed") and not normalizer_not_run:
errors.append("#/normalizerAudit/status: normalizer_completed 前必须为 not_run")
environment = instance.get("environment") or {}
canvas_backing = environment.get("canvasBacking")
if stage_index < stages.index("browser_connected"):
if environment.get("browserVersion") is not None \
or environment.get("runtimeIdentityHash") is not None \
or canvas_backing is not None \
or environment.get("freshContext") is not False \
or environment.get("contextIsolation") != "not-created":
errors.append("#/environment: browser_connected 前不得伪造浏览器或 runtime identity")
elif environment.get("browserVersion") is None \
or environment.get("freshContext") is not True \
or environment.get("contextIsolation") != "one-time-dedicated":
errors.append("#/environment: browser_connected 后必须封存一次性浏览器 context 身份")
if isinstance(canvas_backing, dict):
scale_x = canvas_backing.get("scaleX")
scale_y = canvas_backing.get("scaleY")
if not isinstance(scale_x, int) or not isinstance(scale_y, int) \
or scale_x != scale_y \
or canvas_backing.get("backingWidth") != canvas_backing.get("cssWidth", 0) * scale_x \
or canvas_backing.get("backingHeight") != canvas_backing.get("cssHeight", 0) * scale_y:
errors.append("#/environment/canvasBacking: backing 尺寸必须等于 CSS 尺寸乘统一整数 scale")
if environment.get("freshContext") is True and environment.get("contextDestroyed") is not True:
errors.append("#/environment/contextDestroyed: 已创建的一次性 context 必须销毁后才能签发事实")
expected_support_mask_hash = (
((canonical_capability.get("config") or {}).get("selectionOverlay") or {})
.get("supportMaskHash")
)
compatible_evidence = (
probed_matches_registry
and lattice.get("dimensions") == canonical_geometry["dimensions"]
and lattice.get("geometryHash") == canonical_geometry_hash
and normalizer.get("status") == "normalized"
and normalizer.get("changedPixelCount") == 472
and normalizer.get("outsideSelectedCellCount") == 0
and isinstance(normalizer.get("maximumReplayResidual"), int)
and normalizer.get("maximumReplayResidual") <= 1
and normalizer.get("supportMaskHash") == expected_support_mask_hash
and event_interval.get("businessEventCount") == 0
and pre_seq == post_seq
)
result = instance.get("result")
reason = instance.get("reasonCode")
completed_fields = (
"initialFrame", "postFrame", "eventInterval", "selectedEndpoint", "lattice", "swapSet",
)
if result == "compatible":
if reason != "none":
errors.append("#/reasonCode: compatible 必须为 none")
if stage != "normalizer_completed" or any(instance.get(field) is None for field in completed_fields):
errors.append("#/executionStage: compatible 必须完成全部 preflight 证据阶段")
if environment.get("freshContext") is not True or environment.get("contextDestroyed") is not True \
or environment.get("contextIsolation") != "one-time-dedicated" \
or environment.get("browserVersion") is None \
or environment.get("runtimeIdentityHash") is None:
errors.append("#/environment: compatible 必须来自已销毁的一次性隔离 context")
if profile_error or not compatible_evidence:
errors.append("#/result: compatible 必须具备 canonical probe/geometry、472 support、零格外变化、残差≤1且首击无事件")
elif result == "incompatible":
if reason != "profile_contract_incompatible":
errors.append("#/reasonCode: incompatible 必须固定 profile_contract_incompatible")
if stage != "normalizer_completed" or any(instance.get(field) is None for field in completed_fields):
errors.append("#/executionStage: incompatible 必须完成全部 preflight 证据阶段")
if environment.get("freshContext") is not True or environment.get("contextDestroyed") is not True \
or environment.get("contextIsolation") != "one-time-dedicated" \
or environment.get("browserVersion") is None \
or environment.get("runtimeIdentityHash") is None:
errors.append("#/environment: incompatible 必须来自已销毁的一次性隔离 context")
if profile_error:
errors.append("#/result: capability 身份或 hash 漂移属于 tester_error不得降为 incompatible")
if compatible_evidence:
errors.append("#/result: 已满足 producer contract 的证据不得标为 incompatible")
if normalizer.get("status") == "not_run":
errors.append("#/normalizerAudit/status: 未运行 normalizer 属于 tester_error不得标为 incompatible")
elif result == "tester_error":
if normalizer.get("status") != "not_run":
errors.append("#/normalizerAudit/status: tester_error 必须在 normalizer 前 fail-closed")
if reason == "profile_contract_error":
if not profile_error:
errors.append("#/reasonCode: profile_contract_error 必须有 capability/registry/probe 身份错误")
if not capability_was_probed and expected_matches_registry and binding_matches:
errors.append("#/executionStage: probe 身份错误必须至少完成 capability_probed")
elif reason == "environment_error":
if profile_error:
errors.append("#/reasonCode: capability 身份错误必须归 profile_contract_error")
if normalizer.get("status") != "not_run":
errors.append("#/normalizerAudit/status: environment_error 必须为 not_run")
if stage == "normalizer_completed":
errors.append("#/executionStage: environment_error 不得声称 normalizer_completed")
elif reason == "board_perception_unresolved":
if profile_error:
errors.append("#/reasonCode: capability 身份错误必须归 profile_contract_error")
if stage != "initial_frame_captured" or instance.get("initialFrame") is None:
errors.append("#/executionStage: board_perception_unresolved 必须停在已封存首帧的阶段")
if any(instance.get(field) is not None for field in (
"postFrame", "eventInterval", "selectedEndpoint", "lattice", "swapSet")):
errors.append("#/reasonCode: 初始感知未解析不得伪造首击或后续证据")
else:
errors.append("#/reasonCode: tester_error 原因码不受支持")
content = {key: deepcopy(value) for key, value in instance.items() if key != "factHash"}
expected_fact_hash = _canonical_domain_hash("match3-producer-preflight/1", content)
if instance.get("factHash") != expected_fact_hash:
errors.append("#/factHash: 与 canonical Match3ProducerPreflight 内容不一致")
return errors
def _semantic_validate_interaction_binding(instance: dict) -> list:
"""bindingHash 必须覆盖 profile、registryVersion 与当前任务绑定。"""
errors = []
try:
registry = _load_interaction_registry(instance.get("interactionRegistryVersion"))
except ValueError as exc:
return [f"#/interactionRegistryVersion: {exc}"]
profile_id = instance.get("interactionProfileId")
if instance.get("interactionRegistryVersion") != registry.get("registryVersion"):
errors.append("#/interactionRegistryVersion: 必须与 canonical interaction registry 一致")
if profile_id not in (registry.get("profiles") or {}):
errors.append(f"#/interactionProfileId: 未知 canonical interaction profile {profile_id}")
content = {
"interactionProfileId": profile_id,
"interactionRegistryVersion": instance.get("interactionRegistryVersion"),
"taskBindingHash": instance.get("taskBindingHash"),
}
expected = _canonical_domain_hash("interaction-binding/1", content)
if instance.get("interactionBindingHash") != expected:
errors.append(
"#/interactionBindingHash: 必须覆盖 interactionProfileId/interactionRegistryVersion/taskBindingHash",
)
return errors
def _semantic_validate_proof_resolution(instance: dict) -> list:
"""Resolution 必须按注册表顺序重建 required 集,并能复算语义 hash。"""
errors = []
registry = _load(_HERE / "proof-obligations.v2.json")
if instance.get("proofRegistryVersion") != registry.get("registryVersion"):
errors.append("#/proofRegistryVersion: 必须与 canonical proof registry 一致")
profile = (registry.get("profiles") or {}).get(instance.get("proofProfileId")) or {}
if not profile:
errors.append("#/proofProfileId: 未命中 canonical proof profile")
return errors
obligations = profile.get("obligations") or []
obligation_by_id = {row.get("id"): row for row in obligations}
brief_rules = {row.get("id"): row for row in profile.get("briefOptionalRules") or []}
interaction_rules = {
row.get("id"): row for row in profile.get("interactionOptionalRules") or []
}
brief_matches = instance.get("briefRuleMatches") or []
interaction_matches = instance.get("interactionRuleMatches") or []
for rule_id in brief_matches:
if rule_id not in brief_rules:
errors.append(f"#/briefRuleMatches: 未知 canonical brief rule {rule_id}")
for rule_id in interaction_matches:
if rule_id not in interaction_rules:
errors.append(f"#/interactionRuleMatches: 未知 canonical interaction rule {rule_id}")
if instance.get("interactionBindingStatus") == "absent" and interaction_matches:
errors.append("#/interactionRuleMatches: absent 时必须为空")
required = {row.get("id") for row in obligations if row.get("defaultRequired") is True}
for rule_id in brief_matches:
required.update((brief_rules.get(rule_id) or {}).get("requireObligationIds") or [])
for rule_id in interaction_matches:
required.update((interaction_rules.get(rule_id) or {}).get("requireObligationIds") or [])
expected = [row.get("id") for row in obligations if row.get("id") in required]
if instance.get("requiredObligationIds") != expected:
errors.append("#/requiredObligationIds: 必须按 profile obligations 原顺序保存确定性并集")
if any(oid not in obligation_by_id for oid in instance.get("requiredObligationIds") or []):
errors.append("#/requiredObligationIds: 含 canonical profile 不存在的义务")
content = {key: deepcopy(value) for key, value in instance.items() if key != "resolutionHash"}
expected_hash = _canonical_domain_hash("proof-obligation-resolution/1", content)
if instance.get("resolutionHash") != expected_hash:
errors.append("#/resolutionHash: 与 canonical Resolution 内容不一致")
return errors
def _semantic_validate_single_roll_proof_v1(instance: dict) -> list:
"""冻结旧包仍使用完整 v3 actionRecord只读复用历史事实语义。"""
fact_view = deepcopy(instance)
fact_view["rollId"] = f"roll-{instance.get('roll')}"
fact_view["seedOptionSource"] = "requested-seed"
return _semantic_validate_single_roll_v3(fact_view)
def _semantic_validate_single_roll_proof_v2(instance: dict) -> list:
"""新内部包只验证 ProofAction 窄投影,禁止把 Actor/TargetSet/interaction 审计带入。"""
errors = _semantic_validate_environment(instance, wait_max_ms=600)
errors += _semantic_validate_acceptance_profile(instance)
synthetic_actions = []
for action in instance.get("actions") or []:
synthetic_actions.append({
"actionId": action.get("actionId"),
"normalized": action.get("normalized"),
"valid": True,
"retryCount": 0,
"preFrameRef": action.get("preFrameRef"),
"preEventSeq": action.get("preEventSeq"),
"dispatchReceipt": {"accepted": True, "wallTimeMs": 0},
"virtualTimeBudgetMs": action.get("virtualTimeBudgetMs"),
"timeAdvance": action.get("timeAdvance"),
"postFrameRef": action.get("postFrameRef"),
"postEventSeq": action.get("postEventSeq"),
"protocolErrors": [],
})
errors += _semantic_validate_playtest({
"phase": "COLLECTING", "acceptanceMode": "v3_shadow", "evidenceMode": "native",
"genre": instance.get("genre"), "templateRoute": instance.get("templateRoute"),
"proofProfileId": instance.get("proofProfileId"),
"proofRegistryVersion": instance.get("proofRegistryVersion"),
"acceptanceRequestHash": instance.get("acceptanceRequestHash"),
"artifactHash": instance.get("artifactHash"), "environment": instance.get("environment"),
"floor": {"pass": False, "gates": {}}, "actor": None, "judge": None,
"actions": synthetic_actions, "events": instance.get("events") or [],
"briefRuleMatches": instance.get("briefRuleMatches") or [],
"proofObligations": instance.get("proofObligations") or [],
"firstPlay": instance.get("firstPlay") or {}, "rolls": [],
}, wait_max_ms=600)
actor = instance.get("actor") or {}
for actor_field, top_field in (
("requestedGameSeed", "requestedSeed"),
("actualGameSeed", "actualSeed"),
("policySeed", "policySeed"),
):
if actor.get(actor_field) != instance.get(top_field):
errors.append(f"#/actor/{actor_field}: 必须镜像顶层 {top_field}")
if instance.get("requestedSeed") != instance.get("actualSeed"):
errors.append("#/actualSeed: 必须等于 requestedSeed")
if (instance.get("judge") or {}).get("packageHash") != instance.get("judgePackageHash"):
errors.append("#/judge/packageHash: 必须等于 judgePackageHash")
if instance.get("costRmb") != (instance.get("cost") or {}).get("costRmb"):
errors.append("#/costRmb: 必须与 cost.costRmb 一致")
return errors
def _semantic_validate_collection_proof(instance: dict) -> list:
"""CollectionProof 保存完整 runner 事实;基础语义与 playtest/3 动作、事件口径一致。"""
errors = _semantic_validate_environment(instance, wait_max_ms=600)
errors += _semantic_validate_acceptance_profile(instance)
if instance.get("requestedSeed") != instance.get("actualSeed"):
errors.append("#/actualSeed: 必须等于 requestedSeed")
if (instance.get("environment") or {}).get("buildRef") != instance.get("artifactHash"):
errors.append("#/environment/buildRef: 必须等于 artifactHash")
for index, action in enumerate(instance.get("actions") or []):
errors += _semantic_validate_v3_action(action, f"#/actions/{index}")
for index, event in enumerate(instance.get("events") or []):
errors += [f"#/events/{index}{error[1:]}" for error in _semantic_validate_game_event(event)]
return errors
def _semantic_validate_judge_package(instance: dict) -> list:
"""JudgePackage 只允许事实投影,并机械复算文字引用范围与 brief hash。"""
errors = []
forbidden = {
"interactionBinding", "interactionRuleMatches", "requiredObligationIds",
"proofObligationResolutionRef", "proofObligationResolutionHash", "resolutionHash",
"interactionProfileId", "interactionRegistryVersion", "interactionBindingHash",
"solver", "BoardProjection", "SwapSet", "Match3BoardProjection", "Match3SwapSet",
"boardProjection", "match3BoardProjection", "swapSet", "candidateId",
"candidateSetHash", "beforeRuns", "newRuns", "classifier",
"interactionBindingFileHash", "interactionCandidates", "pairResolution",
"boardIdentityCheck", "actionGroup", "selectionAttempt", "groupedAction",
"targetSetHash", "targetSetRef", "guideManifestRef", "guideManifestHash",
}
def walk(value, path="#"):
if isinstance(value, dict):
for key, child in value.items():
if key in forbidden:
errors.append(f"{path}/{key}: JudgePackage 禁止泄漏 interaction/Resolution/solver 字段")
walk(child, f"{path}/{key}")
elif isinstance(value, list):
for index, child in enumerate(value):
walk(child, f"{path}/{index}")
# gameEvents[].payload 属于游戏业务命名空间,允许出现 candidateId/newRuns/solver 等同名字段;
# 只扫描 runner 自有投影,避免把业务事件误判为验收器内部身份泄漏。
for key in (
"registry", "proofObligations", "actions", "frames", "textReferenceScope",
"environment", "floor",
):
walk(instance.get(key), f"#/{key}")
for key in forbidden:
if key in instance:
errors.append(f"#/{key}: JudgePackage 禁止泄漏 interaction/Resolution/solver 字段")
if hashlib.sha256(str(instance.get("brief") or "").encode("utf-8")).hexdigest() != instance.get("briefHash"):
errors.append("#/briefHash: 必须等于 JudgePackage brief 原始 UTF-8 字节 hash")
registry = instance.get("registry") or {}
for field, top_field in (
("registryVersion", "proofRegistryVersion"), ("proofProfileId", "proofProfileId"),
("genre", "genre"), ("templateRoute", "templateRoute"),
):
if registry.get(field) != instance.get(top_field):
errors.append(f"#/registry/{field}: 必须镜像顶层 {top_field}")
expected_scope = {
"actionRefs": list(dict.fromkeys(
row.get("actionId") for row in instance.get("actions") or [] if row.get("actionId")
)),
"frameRefs": list(dict.fromkeys(
row.get("ref") for row in instance.get("frames") or [] if row.get("ref")
)),
"eventRefs": list(dict.fromkeys(
f"event:{row.get('seq')}" for row in instance.get("gameEvents") or []
if isinstance(row.get("seq"), int) and row.get("seq") >= 1
)),
}
if instance.get("textReferenceScope") != expected_scope:
errors.append("#/textReferenceScope: 必须由 actions/frames/gameEvents 按出现顺序确定性投影")
return errors
def _semantic_validate_match3_projection(instance: dict) -> list:
"""投影必须完整、行优先、匿名 classId 稳定,并能复算 projectionHash。"""
errors = []
dimensions = instance.get("dimensions") or {}
rows, columns = dimensions.get("rows"), dimensions.get("columns")
cells = instance.get("cells") or []
if isinstance(rows, int) and isinstance(columns, int) and len(cells) != rows * columns:
errors.append("#/cells: 必须完整覆盖 dimensions 声明的每一个棋格")
class_map = {}
next_class = 1
all_classified = True
for index, entry in enumerate(cells):
path = f"#/cells/{index}"
expected = {
"row": index // columns + 1 if isinstance(columns, int) and columns > 0 else None,
"column": index % columns + 1 if isinstance(columns, int) and columns > 0 else None,
}
if entry.get("cell") != expected:
errors.append(f"{path}/cell: cells 必须按行优先完整排列")
alternatives = entry.get("alternatives") or []
alternative_ids = [row.get("classId") for row in alternatives]
if len(alternative_ids) != len(set(alternative_ids)):
errors.append(f"{path}/alternatives: classId 不得重复")
sorted_alternatives = sorted(
alternatives, key=lambda row: (-row.get("confidencePermille", -1), row.get("classId", "")),
)
if alternatives != sorted_alternatives:
errors.append(f"{path}/alternatives: 必须按 confidencePermille 降序、classId 升序排列")
if entry.get("state") != "classified":
all_classified = False
continue
class_id = entry.get("classId")
if instance.get("interactionRegistryVersion") == "2026-07-15.v1":
if class_id not in class_map:
class_map[class_id] = f"vc{next_class:02d}"
next_class += 1
if class_map[class_id] != class_id:
errors.append(f"{path}/classId: v1 必须按行优先首次出现次序编号")
if instance.get("projectionStatus") == "resolved" and not all_classified:
errors.append("#/projectionStatus: resolved 要求全部棋格 classified")
registry = _load_interaction_registry(instance.get("interactionRegistryVersion"))
if instance.get("interactionRegistryVersion") != registry.get("registryVersion"):
errors.append("#/interactionRegistryVersion: 与 canonical interaction registry 不一致")
if instance.get("interactionProfileId") not in (registry.get("profiles") or {}):
errors.append("#/interactionProfileId: 未知 canonical interaction profile")
content = {key: deepcopy(value) for key, value in instance.items() if key != "projectionHash"}
expected_hash = _canonical_domain_hash("match3-board-projection/1", content)
if instance.get("projectionHash") != expected_hash:
errors.append("#/projectionHash: 与 canonical 投影内容不一致")
return errors
def _match3_cell_key(cell: dict) -> str:
return f"r{cell.get('row')}c{cell.get('column')}"
def _match3_run_key(run: dict) -> tuple:
return (
0 if run.get("axis") == "horizontal" else 1,
tuple((cell.get("row"), cell.get("column")) for cell in run.get("cells") or []),
run.get("classId"),
)
def _semantic_validate_match3_swap_set(instance: dict) -> list:
"""交换集校验 pair、run、稳定 ID、状态关系和两层 canonical hash。"""
errors = []
registry = _load_interaction_registry(instance.get("interactionRegistryVersion"))
profile = (registry.get("profiles") or {}).get(instance.get("interactionProfileId")) or {}
canonical_solver = profile.get("solver") or {}
solver = instance.get("solver") or {}
for field in ("id", "version", "configHash"):
if solver.get(field) != canonical_solver.get(field):
errors.append(f"#/solver/{field}: 必须与 canonical interaction profile 一致")
if instance.get("interactionRegistryVersion") != registry.get("registryVersion"):
errors.append("#/interactionRegistryVersion: 与 canonical interaction registry 不一致")
before_runs = instance.get("beforeRuns") or []
if before_runs != sorted(before_runs, key=_match3_run_key):
errors.append("#/beforeRuns: 必须按 axis/首格/classId 规范排序")
candidates = instance.get("candidates") or []
pair_keys = []
for index, candidate in enumerate(candidates):
path = f"#/candidates/{index}"
expected_id = f"ms{index + 1:02d}"
if candidate.get("candidateId") != expected_id:
errors.append(f"{path}/candidateId: 必须按规范 pair 顺序连续编号")
pair = candidate.get("pair") or []
if len(pair) == 2:
left, right = pair
if (left.get("row"), left.get("column")) >= (right.get("row"), right.get("column")):
errors.append(f"{path}/pair: 两端必须按 row/column 字典序保存")
distance = abs(left.get("row", 0) - right.get("row", 0)) + abs(
left.get("column", 0) - right.get("column", 0),
)
if distance != 1:
errors.append(f"{path}/pair: 两端必须正交相邻")
expected_pair_key = f"{_match3_cell_key(left)}-{_match3_cell_key(right)}"
if candidate.get("pairKey") != expected_pair_key:
errors.append(f"{path}/pairKey: 必须由规范 pair 逐字生成")
if len(set(candidate.get("beforeClassIds") or [])) != 2:
errors.append(f"{path}/beforeClassIds: 交换前两端必须属于不同匿名类别")
new_runs = candidate.get("newRuns") or []
if new_runs != sorted(new_runs, key=_match3_run_key):
errors.append(f"{path}/newRuns: 必须按 axis/首格/classId 规范排序")
for run_index, run in enumerate(new_runs):
run_path = f"{path}/newRuns/{run_index}"
cells = run.get("cells") or []
if not any(cell in pair for cell in cells):
errors.append(f"{run_path}/cells: 新 run 必须包含至少一个交换端点")
if run.get("axis") == "horizontal":
canonical = sorted(cells, key=lambda cell: (cell.get("column"), cell.get("row")))
if len({cell.get("row") for cell in cells}) != 1 or cells != canonical:
errors.append(f"{run_path}/cells: 横向 run 必须同 row 且按 column 排序")
elif any(
right.get("column") != left.get("column") + 1
for left, right in zip(cells, cells[1:])):
errors.append(f"{run_path}/cells: 横向 run 必须连续")
else:
canonical = sorted(cells, key=lambda cell: (cell.get("row"), cell.get("column")))
if len({cell.get("column") for cell in cells}) != 1 or cells != canonical:
errors.append(f"{run_path}/cells: 纵向 run 必须同 column 且按 row 排序")
elif any(
right.get("row") != left.get("row") + 1
for left, right in zip(cells, cells[1:])):
errors.append(f"{run_path}/cells: 纵向 run 必须连续")
pair_keys.append(candidate.get("pairKey"))
content = {key: deepcopy(value) for key, value in candidate.items() if key != "candidateHash"}
expected_hash = _canonical_domain_hash("match3-swap-candidate/1", content)
if candidate.get("candidateHash") != expected_hash:
errors.append(f"{path}/candidateHash: 与 canonical candidate 内容不一致")
if pair_keys != sorted(pair_keys, key=lambda key: tuple(
int(value) for value in re.findall(r"[0-9]+", key or ""))):
errors.append("#/candidates: 必须按规范 pair 数值顺序输出")
if len(pair_keys) != len(set(pair_keys)):
errors.append("#/candidates: 无序 pair 不得正反重复")
status = instance.get("status")
if status == "resolved" and not candidates:
errors.append("#/status: resolved 必须有非空 candidates")
if status in ("no_candidate", "indeterminate") and candidates:
errors.append(f"#/status: {status} 时 candidates 必须为空")
if status == "indeterminate" and before_runs:
errors.append("#/beforeRuns: indeterminate 时不得声称已可靠枚举交换前连续段")
content = {key: deepcopy(value) for key, value in instance.items() if key != "candidateSetHash"}
expected_hash = _canonical_domain_hash("match3-swap-set/1", content)
if instance.get("candidateSetHash") != expected_hash:
errors.append("#/candidateSetHash: 与 canonical SwapSet 内容不一致")
return errors
def _cell_tuple(cell: dict) -> tuple:
return cell.get("row"), cell.get("column")
def _semantic_validate_actor_view_v4(instance: dict) -> list:
"""ActorView/4 保持真实 View/3 source 绑定,并把普通路径与 M3 pair 路径互斥。"""
errors = []
catalog = instance.get("visualTargetCatalog") or {}
if catalog.get("sourceFrameRef") != instance.get("currentFrameRef"):
errors.append("#/visualTargetCatalog/sourceFrameRef: 必须镜像 currentFrameRef")
if catalog.get("sourceFrameHash") != instance.get("currentFrameHash"):
errors.append("#/visualTargetCatalog/sourceFrameHash: 必须镜像 currentFrameHash")
legal = instance.get("legalSelections") or {}
types = legal.get("types") or []
order = ["tap", "drag", "key", "wait", "swap_pair"]
if types != [item for item in order if item in types] or len(types) != len(set(types)):
errors.append("#/legalSelections/types: 必须按协议顺序去重")
safe_keys = legal.get("safeKeys") or []
if ("key" in types) != bool(safe_keys):
errors.append("#/legalSelections/safeKeys: 必须与 key 能力同时存在或同时为空")
candidates = instance.get("interactionCandidates")
if candidates is None:
if "swap_pair" in types:
errors.append("#/legalSelections/types: 普通 View/4 不得允许 swap_pair")
else:
if types != ["swap_pair"] or safe_keys:
errors.append("#/legalSelections: 标准 Match-3 resolved 只能允许 swap_pair")
rows = ((candidates.get("match3Swaps") or {}).get("candidates") or [])
ids = []
pair_keys = []
for index, row in enumerate(rows):
ids.append(row.get("candidateId"))
pair = [_cell_tuple(item.get("cell") or {}) for item in row.get("pair") or []]
if len(pair) == 2:
if pair != sorted(pair) or pair[0] == pair[1]:
errors.append(f"#/interactionCandidates/match3Swaps/candidates/{index}/pair: 必须规范排序")
if abs(pair[0][0] - pair[1][0]) + abs(pair[0][1] - pair[1][1]) != 1:
errors.append(f"#/interactionCandidates/match3Swaps/candidates/{index}/pair: 必须正交相邻")
pair_keys.append(tuple(pair))
if len(ids) != len(set(ids)):
errors.append("#/interactionCandidates/match3Swaps/candidates: candidateId 不得重复")
if pair_keys != sorted(pair_keys):
errors.append("#/interactionCandidates/match3Swaps/candidates: 必须按 pair 规范排序")
progress = instance.get("objectiveProgress")
if isinstance(progress, dict):
payload = {
"candidateOnly": progress.get("candidateOnly"),
"snapshotSeq": progress.get("snapshotSeq"),
"obligations": progress.get("obligations"),
}
canonical = json.dumps(payload, ensure_ascii=False, sort_keys=True, separators=(",", ":"))
if progress.get("hash") != hashlib.sha256(canonical.encode("utf-8")).hexdigest():
errors.append("#/objectiveProgress/hash: 与稳定投影不一致")
return errors
def _semantic_validate_actor_protocol_bundle(instance: dict) -> list:
"""联合锁定 Prompt、View、Selection 与 validator 代际,并校验当前可见候选。"""
errors = []
view = instance.get("actorView") or {}
selection = instance.get("selection") or {}
prompt = instance.get("prompt") or {}
validator = instance.get("validator") or {}
view_version = view.get("projectionVersion")
selection_version = selection.get("schemaVersion")
errors += _semantic_validate_actor_view_v4(view)
if view_version == "ActorView/3":
expected = {
"selection": "ActorSelection/1", "promptVersion": "2.4.0",
"validatorId": "actor-selection", "validatorVersion": "1.0.0",
}
else:
profile = (_load(_HERE / "interaction-profiles.v2.json").get("profiles") or {})[
"match3.orthogonal-swap-v1"
]
protocol = profile.get("actorProtocol") or {}
expected = {
"selection": protocol.get("selectionVersion"),
"promptVersion": (protocol.get("prompt") or {}).get("version"),
"validatorId": (protocol.get("parser") or {}).get("id"),
"validatorVersion": (protocol.get("parser") or {}).get("version"),
}
if prompt != protocol.get("prompt"):
errors.append("#/prompt: View/4 必须逐字使用 v2 registry prompt identity")
if selection_version != expected["selection"]:
errors.append("#/selection/schemaVersion: 与 ActorView 代际不匹配")
if prompt.get("version") != expected["promptVersion"]:
errors.append("#/prompt/version: 与 ActorView 代际不匹配")
if validator.get("id") != expected["validatorId"] or validator.get("version") != expected["validatorVersion"]:
errors.append("#/validator: 与 ActorView 代际不匹配")
if validator.get("selectionSchemaVersion") != selection_version:
errors.append("#/validator/selectionSchemaVersion: 必须镜像 selection")
errors += _semantic_validate_actor_selection_context({"actorView": view, "selection": selection})
return errors
def _semantic_validate_actor_selection_context(instance: dict) -> list:
"""联合校验 View/4 可见能力与 Selection/2不允许普通动作穿过 M3 专用门。"""
errors = []
view = instance.get("actorView") or {}
selection = instance.get("selection") or {}
candidates = view.get("interactionCandidates")
selection_type = selection.get("type")
legal_types = (view.get("legalSelections") or {}).get("types") or []
if selection_type not in legal_types:
errors.append("#/selection/type: 不在 ActorView legalSelections 中")
catalog_hash = (view.get("visualTargetCatalog") or {}).get("targetSetHash")
if selection_type in ("tap", "drag", "swap_pair") and selection.get("targetSetHash") != catalog_hash:
errors.append("#/selection/targetSetHash: 与 ActorView 当前目录不一致")
if candidates is None:
if selection_type == "swap_pair":
errors.append("#/selection/type: 普通 View/4 不得返回 swap_pair")
if selection_type == "key" and selection.get("key") not in (view.get("legalSelections") or {}).get("safeKeys", []):
errors.append("#/selection/key: 不在 ActorView safeKeys 中")
return errors
if selection_type != "swap_pair":
errors.append("#/selection/type: 标准 Match-3 View/4 只能返回 swap_pair")
return errors
swaps = candidates.get("match3Swaps") or {}
if selection.get("candidateSetHash") != swaps.get("candidateSetHash"):
errors.append("#/selection/candidateSetHash: 已过期或与 ActorView 不一致")
candidate = next(
(row for row in swaps.get("candidates") or [] if row.get("candidateId") == selection.get("candidateId")),
None,
)
if candidate is None:
errors.append("#/selection/candidateId: 未命中 ActorView 当前候选")
return errors
endpoint = selection.get("firstEndpoint") or {}
if endpoint.get("id") != candidate.get("latticeId"):
errors.append("#/selection/firstEndpoint/id: 必须等于候选 latticeId")
pair = [(item.get("cell") or {}) for item in candidate.get("pair") or []]
if endpoint.get("cell") not in pair:
errors.append("#/selection/firstEndpoint/cell: 必须命中所选 candidate 的一个端点")
if endpoint.get("subAnchor") != "center":
errors.append("#/selection/firstEndpoint/subAnchor: 必须为 center")
return errors
def _semantic_validate_selection_attempt(instance: dict) -> list:
content = {key: deepcopy(value) for key, value in instance.items() if key != "attemptHash"}
expected = _canonical_domain_hash("actor-selection-attempt/1", content)
return [] if instance.get("attemptHash") == expected else ["#/attemptHash: 与 canonical attempt 内容不一致"]
def _semantic_validate_pair_resolution(instance: dict) -> list:
"""PairResolution 必须保存规范 pair第二端只能是第一端之外的另一端。"""
errors = []
pair = instance.get("pair") or []
tuples = [_cell_tuple(cell) for cell in pair]
if len(tuples) == 2:
if tuples != sorted(tuples) or tuples[0] == tuples[1]:
errors.append("#/pair: 必须规范排序且端点不同")
if abs(tuples[0][0] - tuples[1][0]) + abs(tuples[0][1] - tuples[1][1]) != 1:
errors.append("#/pair: 必须正交相邻")
first = instance.get("firstEndpoint") or {}
second = instance.get("secondEndpoint") or {}
if first.get("id") != instance.get("latticeId") or second.get("id") != instance.get("latticeId"):
errors.append("#/latticeId: 两端必须属于同一候选 lattice")
if first.get("cell") not in pair:
errors.append("#/firstEndpoint/cell: 不属于 canonical pair")
expected_second = next((cell for cell in pair if cell != first.get("cell")), None)
if second.get("cell") != expected_second:
errors.append("#/secondEndpoint/cell: 必须由 candidate 确定为另一端")
content = {key: deepcopy(value) for key, value in instance.items() if key != "resolutionHash"}
if instance.get("resolutionHash") != _canonical_domain_hash("match3-pair-resolution/1", content):
errors.append("#/resolutionHash: 与 canonical pair resolution 不一致")
return errors
def _validate_lattice_geometry(lattice: dict, path: str) -> list:
errors = []
dimensions = lattice.get("dimensions") or {}
rows = lattice.get("rowCenters") or []
columns = lattice.get("columnCenters") or []
if len(rows) != dimensions.get("rows") or rows != sorted(set(rows)):
errors.append(f"{path}/rowCenters: 必须与 rows 等长且严格递增")
if len(columns) != dimensions.get("columns") or columns != sorted(set(columns)):
errors.append(f"{path}/columnCenters: 必须与 columns 等长且严格递增")
return errors
def _semantic_validate_board_identity(instance: dict) -> list:
"""单文件只验证封存形状;跨文件 bundle 负责从 TargetSet/Projection 重算等价分区。"""
errors = []
registry = _load(_HERE / "interaction-profiles.v2.json")
profile = (registry.get("profiles") or {}).get(instance.get("interactionProfileId")) or {}
if instance.get("interactionRegistryVersion") != registry.get("registryVersion"):
errors.append("#/interactionRegistryVersion: BoardIdentity 必须使用 v2 registry")
if instance.get("overlayNormalizer") != profile.get("overlayNormalizer"):
errors.append("#/overlayNormalizer: 必须逐字镜像 v2 registry")
if instance.get("equivalenceAlgorithm") != profile.get("equivalenceAlgorithm"):
errors.append("#/equivalenceAlgorithm: 必须逐字镜像 v2 registry")
for side in ("initial", "postFirst"):
errors += _validate_lattice_geometry((instance.get(side) or {}).get("lattice") or {}, f"#/{side}/lattice")
initial = instance.get("initial") or {}
post = instance.get("postFirst") or {}
normalization = instance.get("overlayNormalizationEvidence") or {}
if normalization.get("overlayReferenceFrameRef") != initial.get("sourceFrameRef") \
or normalization.get("overlayReferenceFrameHash") != initial.get("sourceFrameHash"):
errors.append("#/overlayNormalizationEvidence: overlay reference 必须绑定 initial frame")
if normalization.get("postFirstFrameRef") != post.get("sourceFrameRef") \
or normalization.get("postFirstFrameHash") != post.get("sourceFrameHash"):
errors.append("#/overlayNormalizationEvidence: post-first frame 必须绑定 postFirst")
if normalization.get("selectedCell") != (instance.get("firstEndpoint") or {}).get("cell"):
errors.append("#/overlayNormalizationEvidence/selectedCell: 必须镜像 firstEndpoint.cell")
same = (
(initial.get("lattice") or {}).get("dimensions") == (post.get("lattice") or {}).get("dimensions")
and (initial.get("lattice") or {}).get("rowCenters") == (post.get("lattice") or {}).get("rowCenters")
and (initial.get("lattice") or {}).get("columnCenters") == (post.get("lattice") or {}).get("columnCenters")
and initial.get("equivalenceMatrixHash") == post.get("equivalenceMatrixHash")
and initial.get("equivalenceMatrixHash") is not None
)
if instance.get("result") == "equivalent" and not same:
errors.append("#/result: equivalent 要求 geometry 与匿名等价矩阵一致")
if instance.get("result") == "equivalent" and (
normalization.get("status") not in ("normalized", "not-needed")
or normalization.get("outsideSelectedCellCount") != 0
or normalization.get("reasonCodes") != []
):
errors.append("#/result: equivalent 要求归一化证据已解决且无越界像素")
if instance.get("result") == "changed" and same:
errors.append("#/result: changed 必须存在 geometry 或等价分区变化")
content = {key: deepcopy(value) for key, value in instance.items() if key != "checkHash"}
if instance.get("checkHash") != _canonical_domain_hash("board-identity-check/1", content):
errors.append("#/checkHash: 与 canonical BoardIdentityCheck 不一致")
return errors
def _semantic_validate_action_group(instance: dict) -> list:
errors = []
pair = instance.get("pair") or []
tuples = [_cell_tuple(cell) for cell in pair]
if len(tuples) == 2 and (tuples != sorted(tuples) or tuples[0] == tuples[1]):
errors.append("#/pair: 必须规范排序且端点不同")
first = instance.get("firstEndpoint") or {}
second = instance.get("secondEndpoint") or {}
if first.get("id") != instance.get("latticeId") or second.get("id") != instance.get("latticeId"):
errors.append("#/latticeId: 两端必须属于同一 lattice")
if first.get("cell") not in pair or second.get("cell") not in pair or first.get("cell") == second.get("cell"):
errors.append("#/firstEndpoint: 两端必须逐一覆盖 canonical pair")
first_action = instance.get("firstActionId")
second_action = instance.get("secondActionId")
if first_action is not None and first_action == second_action:
errors.append("#/secondActionId: 两个原生 actionId 必须不同")
state = instance.get("state")
refs = (
instance.get("firstGroupedActionRef"), instance.get("firstGroupedActionHash"),
instance.get("secondGroupedActionRef"), instance.get("secondGroupedActionHash"),
instance.get("boardIdentityCheckRef"), instance.get("boardIdentityCheckHash"),
)
if state == "awaiting_first" and (first_action is not None or second_action is not None or any(refs)):
errors.append("#/state: awaiting_first 不得提前封存动作或 BoardIdentity")
if state == "awaiting_second" and (
first_action is None or second_action is not None or not all(refs[:2] + refs[4:]) or any(refs[2:4])
):
errors.append("#/state: awaiting_second 必须只有 first action 与 BoardIdentity")
if state == "completed" and (
first_action is None or second_action is None or not all(refs) or instance.get("abortReason") is not None
):
errors.append("#/state: completed 必须闭合双 tap、BoardIdentity 且无 abortReason")
if state == "aborted" and (instance.get("abortReason") is None or second_action is not None or any(refs[2:4])):
errors.append("#/state: aborted 必须有原因且不得派发 second action")
content = {key: deepcopy(value) for key, value in instance.items() if key != "actionGroupHash"}
if instance.get("actionGroupHash") != _canonical_domain_hash("match3-action-group/1", content):
errors.append("#/actionGroupHash: 与 canonical actionGroup 不一致")
return errors
def _semantic_validate_roll_audit(instance: dict) -> list:
errors = []
groups = instance.get("groups") or []
reset = instance.get("reset")
recovery = (((_load(_HERE / "interaction-profiles.v2.json").get("profiles") or {})
.get(instance.get("interactionProfileId")) or {}).get("recoveryPolicy") or {})
expected_budget = recovery.get("maxActionGroupsPerRoll", 0) * 2 + recovery.get("maxResetPerRoll", 0)
if instance.get("initialRemainingBudget") != expected_budget:
errors.append("#/initialRemainingBudget: 必须由 v2 recoveryPolicy 确定性计算")
if len({row.get("actionGroupId") for row in groups}) != len(groups):
errors.append("#/groups: actionGroupId 不得重复")
if reset is None and len(groups) > 1:
errors.append("#/reset: 两个 actionGroup 之间必须有一次授权 reset")
if reset is not None:
if len(groups) != 2 or (reset.get("betweenActionGroupIds") or []) != [
groups[0].get("actionGroupId"), groups[1].get("actionGroupId"),
]:
errors.append("#/reset/betweenActionGroupIds: reset 必须位于两条 sealed group 之间")
if groups[0].get("state") != "aborted":
errors.append("#/groups/0/state: 只有首组 aborted 才允许 reset")
if reset.get("resultingCandidateSetHash") == groups[0].get("candidateSetHash"):
errors.append("#/reset/resultingCandidateSetHash: reset 后必须生成新 candidateSet 身份")
if groups[1].get("candidateSetHash") != reset.get("resultingCandidateSetHash"):
errors.append("#/groups/1/candidateSetHash: 必须使用 reset 后的新集合")
executed_actions = sum(2 if row.get("state") == "completed" else 1 for row in groups) + (1 if reset else 0)
if executed_actions > expected_budget:
errors.append("#/groups: 实际 group/reset 原生动作数超过 recoveryPolicy 预算")
if instance.get("finalState") == "completed" and groups[-1].get("state") != "completed":
errors.append("#/finalState: completed 必须来自最后 actionGroup completed")
if len(groups) == 2 and groups[-1].get("state") == "aborted" \
and instance.get("finalState") != "inconclusive_board_identity_unresolved":
errors.append("#/finalState: 第二 actionGroup aborted 必须固定 inconclusive")
content = {key: deepcopy(value) for key, value in instance.items() if key != "auditHash"}
if instance.get("auditHash") != _canonical_domain_hash("match3-roll-interaction-audit/1", content):
errors.append("#/auditHash: 与 canonical roll interaction audit 不一致")
return errors
def _semantic_validate_match3_audio_audit(instance: dict) -> list:
"""复算完整 limiter 后窗口账;空账只表示尚未启音,不能进入正式 Audit/2。"""
errors = []
samples = instance.get("samples") or []
capacity = instance.get("capacity")
if isinstance(capacity, int) and len(samples) > capacity:
errors.append("#/samples: 样本数不得超过 capacity")
if not samples:
for field in (
"auditId", "themeHash", "configHash", "audioArtifactBinding", "runtimeBinding",
"firstWindowSeq", "lastWindowSeq",
):
if instance.get(field) is not None:
errors.append(f"#/{field}: 空音频账必须为 null")
if instance.get("droppedWindowCount") != 0:
errors.append("#/droppedWindowCount: 空音频账必须为 0")
if instance.get("pendingOnsetCueReceiptRefs"):
errors.append("#/pendingOnsetCueReceiptRefs: 空音频账不得伪造 pending cue")
return errors
runtime = instance.get("runtimeBinding") or {}
artifact = instance.get("audioArtifactBinding") or {}
expected_audit_id = _canonical_domain_hash("match3-audio-audit/1", {
"themeHash": instance.get("themeHash"),
"configHash": instance.get("configHash"),
"audioArtifactBinding": artifact,
"plugin": {
"name": runtime.get("pluginName"),
"version": runtime.get("pluginVersion"),
},
"auditTapId": runtime.get("auditTapId"),
})
if instance.get("auditId") != expected_audit_id:
errors.append("#/auditId: 未绑定 theme/config/已验证 BGM 原始字节/plugin/auditTap")
first_seq = samples[0].get("windowSeq")
last_seq = samples[-1].get("windowSeq")
if instance.get("firstWindowSeq") != first_seq:
errors.append("#/firstWindowSeq: 必须等于首个完整窗口序号")
if instance.get("lastWindowSeq") != last_seq:
errors.append("#/lastWindowSeq: 必须等于末个完整窗口序号")
if isinstance(first_seq, int) and first_seq != instance.get("droppedWindowCount", 0) + 1:
errors.append("#/droppedWindowCount: 必须与 ring 丢弃后的首窗口序号闭合")
previous = None
global_cues = [
cue
for sample in samples
for cue in (sample.get("cueReceiptRefs") or [])
]
global_cue_seqs = [cue.get("cueSeq") for cue in global_cues]
if global_cue_seqs != sorted(set(global_cue_seqs)):
errors.append("#/samples: cue receipt 必须在整份审计账中唯一升序封存")
global_transition_seqs = [
transition.get("transitionSeq")
for sample in samples
for transition in (sample.get("transitionRefs") or [])
]
if global_transition_seqs != sorted(set(global_transition_seqs)):
errors.append("#/samples: transition 必须在整份审计账中唯一升序封存")
onset_check_rows = []
for index, sample in enumerate(samples):
path = f"#/samples/{index}"
if sample.get("windowSeq") != first_seq + index:
errors.append(f"{path}/windowSeq: 必须从 firstWindowSeq 严格连续")
if sample.get("auditTapId") != runtime.get("auditTapId"):
errors.append(f"{path}/auditTapId: 必须镜像 runtimeBinding.auditTapId")
if sample.get("nodeIds") != runtime.get("nodeIds"):
errors.append(f"{path}/nodeIds: 必须逐字镜像 runtimeBinding.nodeIds")
start = sample.get("windowStartAudioTime")
end = sample.get("windowEndAudioTime")
fft_size = sample.get("fftSize")
sample_rate = sample.get("sampleRate")
if all(isinstance(value, (int, float)) for value in (start, end, fft_size, sample_rate)) \
and sample_rate > 0 \
and not math.isclose(end - start, fft_size / sample_rate, rel_tol=0, abs_tol=1e-6):
# runtime 在 AudioContext 积满一个 fft 窗前返回 null因此正式账中每个窗都必须完整。
errors.append(f"{path}: 音频窗边界必须等于 fftSize/sampleRate")
if isinstance(end, (int, float)) and not math.isclose(
sample.get("audioTime", -1), end, rel_tol=0, abs_tol=1e-9,
):
errors.append(f"{path}/audioTime: 必须等于 windowEndAudioTime")
noise_floor = sample.get("noiseFloorDbfs")
threshold = sample.get("onsetThresholdDbfs")
if isinstance(noise_floor, (int, float)) and isinstance(threshold, (int, float)) \
and not math.isclose(threshold, max(noise_floor + 12, -45), rel_tol=0, abs_tol=1e-9):
errors.append(f"{path}/onsetThresholdDbfs: 必须为 max(noiseFloor+12dB, -45dBFS)")
rms, peak = sample.get("rms"), sample.get("peak")
if isinstance(rms, (int, float)) and isinstance(peak, (int, float)) and peak < rms:
errors.append(f"{path}/peak: peak 不得小于 RMS")
if sample.get("muted") is True and (rms != 0 or peak != 0):
errors.append(f"{path}: 静音只能由 limiter 后 auditTap 的零 RMS/peak 证明")
transition_refs = sample.get("transitionRefs") or []
transition_seqs = [row.get("transitionSeq") for row in transition_refs]
if transition_seqs != sorted(set(transition_seqs)):
errors.append(f"{path}/transitionRefs: transitionSeq 必须唯一升序")
if any(seq > sample.get("lastTransitionSeq", -1) for seq in transition_seqs):
errors.append(f"{path}/lastTransitionSeq: 不得小于窗口引用的 transition")
cue_refs = sample.get("cueReceiptRefs") or []
cue_seqs = [row.get("cueSeq") for row in cue_refs]
if cue_seqs != sorted(set(cue_seqs)):
errors.append(f"{path}/cueReceiptRefs: cueSeq 必须唯一升序")
if any(seq > sample.get("lastCueSeq", -1) for seq in cue_seqs):
errors.append(f"{path}/lastCueSeq: 不得小于窗口引用的 cue receipt")
if any(row.get("transitionSeq") not in transition_seqs for row in cue_refs):
errors.append(f"{path}/cueReceiptRefs: 每个 cue 必须引用同窗 transition")
transition_by_seq = {row.get("transitionSeq"): row for row in transition_refs}
expected_cue_kind = {
"swap": "swap", "swap_forward": "swap", "rollback": "rollback",
"clear": "clear", "fall_refill": "fall",
}
for cue_index, cue in enumerate(cue_refs):
transition = transition_by_seq.get(cue.get("transitionSeq")) or {}
for field in (
"transactionId", "phase", "cascadeIndex", "transitionVirtualTimeMs",
):
if cue.get(field) != transition.get(field):
errors.append(f"{path}/cueReceiptRefs/{cue_index}/{field}: 必须镜像完整 transition")
cue_kind = expected_cue_kind.get(transition.get("phase"))
if transition.get("phase") == "settle" \
and transition.get("terminalKind") in ("win", "lose"):
cue_kind = f"terminal-{transition.get('terminalKind')}"
if cue.get("cueKind") != cue_kind:
errors.append(f"{path}/cueReceiptRefs/{cue_index}/cueKind: 与 transition phase/terminal 不一致")
expected_late = max(0, (cue.get("startCallAudioTime", 0)
- cue.get("scheduledAudioTime", 0)) * 1000)
if not math.isclose(cue.get("lateByMs", -1), expected_late, rel_tol=0, abs_tol=1e-6):
errors.append(f"{path}/cueReceiptRefs/{cue_index}/lateByMs: 不能由调度时间复算")
for check in sample.get("onsetChecks") or []:
onset_check_rows.append((sample.get("windowSeq"), check))
if previous is not None:
if sample.get("frame", -1) < previous.get("frame", -1):
errors.append(f"{path}/frame: 必须单调不减")
if sample.get("virtualNowMs", -1) < previous.get("virtualNowMs", -1):
errors.append(f"{path}/virtualNowMs: 必须单调不减")
if start < previous.get("windowStartAudioTime", -1):
errors.append(f"{path}/windowStartAudioTime: 必须单调不减")
if sample.get("lastTransitionSeq", -1) < previous.get("lastTransitionSeq", -1):
errors.append(f"{path}/lastTransitionSeq: 必须跨窗单调不减")
if sample.get("lastCueSeq", -1) < previous.get("lastCueSeq", -1):
errors.append(f"{path}/lastCueSeq: 必须跨窗单调不减")
previous = sample
def cue_identity(value: dict) -> tuple:
return (
value.get("cueSeq"), value.get("transitionSeq"), value.get("cueSemanticHash"),
)
cue_by_identity = {cue_identity(cue): cue for cue in global_cues}
window_by_seq = {sample.get("windowSeq"): sample for sample in samples}
closed_identities = []
for owner_seq, check in onset_check_rows:
identity = cue_identity(check.get("cueReceiptRef") or {})
closed_identities.append(identity)
cue = cue_by_identity.get(identity)
if cue is None:
errors.append("#/samples/onsetChecks/cueReceiptRef: 未命中完整 cue ledger")
continue
expected_deadline = cue.get("scheduledAudioTime", 0) + 0.05
if not math.isclose(
check.get("deadlineAudioTime", -1), expected_deadline, rel_tol=0, abs_tol=1e-9,
):
errors.append("#/samples/onsetChecks/deadlineAudioTime: 必须等于 scheduledAudioTime+50ms")
if check.get("status") == "detected":
first_seq = check.get("firstAboveWindowSeq")
confirmation_seq = check.get("confirmationWindowSeq")
first_window = window_by_seq.get(first_seq)
confirmation_window = window_by_seq.get(confirmation_seq)
if not isinstance(first_seq, int) or not isinstance(confirmation_seq, int) \
or confirmation_seq != first_seq + 1 or owner_seq != confirmation_seq:
errors.append("#/samples/onsetChecks: detected 必须在连续第二窗确认并封存")
if first_window is None or confirmation_window is None:
errors.append("#/samples/onsetChecks: detected 引用的连续两窗必须仍在审计账内")
continue
first_end = first_window.get("windowEndAudioTime")
if first_end < cue.get("scheduledAudioTime", 0) \
or first_end > check.get("deadlineAudioTime", -1):
errors.append("#/samples/onsetChecks: 首个过阈窗必须落在 cue 调度后的 50ms 内")
if not math.isclose(
check.get("analyserOnsetAudioTime", -1), first_end, rel_tol=0, abs_tol=1e-9,
):
errors.append("#/samples/onsetChecks/analyserOnsetAudioTime: 必须等于首个过阈窗 end")
for window in (first_window, confirmation_window):
window_rms = window.get("rms")
window_db = 20 * math.log10(max(window_rms, 1e-12)) \
if isinstance(window_rms, (int, float)) else -240
if window.get("onsetDeltaDb", -999) < 12 \
or window_db < window.get("onsetThresholdDbfs", -45):
errors.append("#/samples/onsetChecks: detected 的连续两窗未达到 cue onset 阈值")
break
else:
for field in (
"firstAboveWindowSeq", "confirmationWindowSeq", "analyserOnsetAudioTime",
):
if check.get(field) is not None:
errors.append(f"#/samples/onsetChecks/{field}: missed 时必须为 null")
owner = window_by_seq.get(owner_seq) or {}
if owner.get("windowEndAudioTime", -1) < check.get("deadlineAudioTime", 0):
errors.append("#/samples/onsetChecks: missed 只能在 deadline 到达后封存")
pending = instance.get("pendingOnsetCueReceiptRefs") or []
pending_identities = [cue_identity(row) for row in pending]
for row in pending:
identity = cue_identity(row)
cue = cue_by_identity.get(identity)
if cue is None:
errors.append("#/pendingOnsetCueReceiptRefs: 未命中完整 cue ledger")
continue
expected_deadline = cue.get("scheduledAudioTime", 0) + 0.05
if not math.isclose(row.get("deadlineAudioTime", -1), expected_deadline, rel_tol=0, abs_tol=1e-9):
errors.append("#/pendingOnsetCueReceiptRefs/deadlineAudioTime: 必须等于 scheduledAudioTime+50ms")
if samples[-1].get("windowEndAudioTime", 0) >= row.get("deadlineAudioTime", 0):
errors.append("#/pendingOnsetCueReceiptRefs: deadline 已到的 cue 不得继续 pending")
if len(set(closed_identities + pending_identities)) != len(closed_identities + pending_identities):
errors.append("#/samples: 同一 cue receipt 不得重复进入 onset check/pending")
if set(closed_identities + pending_identities) != set(cue_by_identity):
errors.append("#/samples: 每个完整 cue receipt 必须恰好进入 onset check 或 pending")
# 完成事务的视觉阶段是音频闭包的权威输入。每个应发声 transition 必须恰好生成一张
# 有实际声部的 receipt中间观察态允许 pending全部关闭后才检查真实 onset 质量。
transitions = [
transition
for sample in samples
for transition in (sample.get("transitionRefs") or [])
]
cues_by_transition = {}
for cue in global_cues:
cues_by_transition.setdefault(cue.get("transitionSeq"), []).append(cue)
checks_by_identity = {
cue_identity(check.get("cueReceiptRef") or {}): check
for _, check in onset_check_rows
}
transactions = {}
for transition in transitions:
key = (transition.get("transactionId"), transition.get("transactionHash"))
transactions.setdefault(key, []).append(transition)
audible_phases = {"swap", "swap_forward", "rollback", "clear", "fall_refill"}
for (transaction_id, _), transaction_transitions in transactions.items():
if not any(row.get("phase") == "settle" for row in transaction_transitions):
continue
expected = [
row for row in transaction_transitions
if row.get("phase") in audible_phases
or (row.get("phase") == "settle" and row.get("terminalKind") in ("win", "lose"))
]
transaction_cues = []
for transition in expected:
matched = cues_by_transition.get(transition.get("transitionSeq"), [])
if len(matched) != 1:
errors.append(
"#/samples: 完成事务的每个应发声 transition 必须恰好有一张 cue receipt"
f"transaction={transaction_id}, transitionSeq={transition.get('transitionSeq')}",
)
continue
cue = matched[0]
transaction_cues.append((transition, cue))
if not isinstance(cue.get("layerCount"), int) or cue.get("layerCount") <= 0:
errors.append("#/samples/cueReceiptRefs/layerCount: 应发声 cue 必须至少成功调度一层")
transaction_cue_identities = {cue_identity(cue) for _, cue in transaction_cues}
if transaction_cue_identities & set(pending_identities):
continue
detected = [
(transition, cue)
for transition, cue in transaction_cues
if (checks_by_identity.get(cue_identity(cue)) or {}).get("status") == "detected"
]
if not detected:
errors.append(f"#/samples/onsetChecks: 完成事务至少需要一个 detectedtransaction={transaction_id}")
for transition, cue in transaction_cues:
if transition.get("phase") == "settle" \
and transition.get("terminalKind") in ("win", "lose") \
and (checks_by_identity.get(cue_identity(cue)) or {}).get("status") != "detected":
errors.append("#/samples/onsetChecks: terminal cue 必须为 detected")
return errors
def _semantic_validate_roll_audit_v2(instance: dict) -> list:
"""Audit/2 单文件只复算自身 hash文件存在性和 sidecar 语义由目录门完成。"""
content = {key: deepcopy(value) for key, value in instance.items() if key != "auditHash"}
expected_hash = _canonical_domain_hash("match3-roll-interaction-audit/2", content)
return [] if instance.get("auditHash") == expected_hash else [
"#/auditHash: 与 canonical Match3RollInteractionAudit/2 内容不一致",
]
def _semantic_validate_single_roll_proof_v3(instance: dict) -> list:
"""Proof/3 复用冻结 Proof/2 的业务语义,只新增 Audit/2 单向引用。"""
return _semantic_validate_single_roll_proof_v2(instance)
def _semantic_validate_build_manifest(instance: dict) -> list:
errors = []
refs = [row.get("ref") for row in instance.get("files") or []]
hashes = [row.get("sha256") for row in instance.get("files") or []]
if len(refs) != len(set(refs)):
errors.append("#/files: ref 不得重复")
if len(hashes) != len(set(hashes)):
errors.append("#/files: 同一原始字节不得冒充多个 build 文件")
return errors
def _cell_key(cell: dict) -> tuple:
return cell.get("row"), cell.get("column")
def _semantic_validate_visual_transaction(instance: dict) -> list:
errors = []
rounds = instance.get("rounds") or []
if instance.get("valid") is True and not rounds:
errors.append("#/rounds: 有效交换必须至少一轮")
if instance.get("valid") is False and rounds:
errors.append("#/rounds: 无效交换不得伪造消除轮次")
if instance.get("visualComplete") is True and instance.get("degradedReason") is not None:
errors.append("#/degradedReason: 完整视觉事务必须为 null")
if instance.get("visualComplete") is False and instance.get("degradedReason") is None:
errors.append("#/degradedReason: 降级视觉事务必须给出原因")
for index, round_value in enumerate(rounds):
if round_value.get("cascadeIndex") != index:
errors.append(f"#/rounds/{index}/cascadeIndex: 必须从 0 连续递增")
cells = round_value.get("matchedCells") or []
if [_cell_key(cell) for cell in cells] != sorted(set(_cell_key(cell) for cell in cells)):
errors.append(f"#/rounds/{index}/matchedCells: 必须行优先唯一排序")
content = {key: deepcopy(value) for key, value in instance.items() if key != "transactionHash"}
expected = _canonical_domain_hash("match3-visual-transaction/1", content)
if instance.get("transactionHash") != expected:
errors.append("#/transactionHash: 与 canonical 视觉事务不一致")
return errors
def _semantic_validate_visual_audit(instance: dict) -> list:
cells = instance.get("matchedCells") or []
if [_cell_key(cell) for cell in cells] != sorted(set(_cell_key(cell) for cell in cells)):
return ["#/matchedCells: 必须行优先唯一排序"]
return []
def _semantic_validate_effect_audit(instance: dict) -> list:
errors = []
cascades = instance.get("cascadeIndexes") or []
if cascades != sorted(cascades):
errors.append("#/cascadeIndexes: 必须唯一升序")
refs = [row.get("ref") for row in instance.get("renderReceiptRefs") or []]
if len(refs) != len(set(refs)):
errors.append("#/renderReceiptRefs: receipt ref 不得重复")
return errors
def _semantic_validate_filmstrip(instance: dict) -> list:
errors = []
frames = instance.get("frames") or []
times = [row.get("relativeMs") for row in frames]
if times != sorted(times) or len(times) != len(set(times)):
errors.append("#/frames/relativeMs: 必须严格递增")
phases = [row.get("phase") for row in frames]
cursor = -1
for phase in ("swap", "clear", "fall_refill", "settle"):
try:
cursor = phases.index(phase, cursor + 1)
except ValueError:
errors.append("#/frames: 必须按顺序包含 swap→clear→fall_refill→settle")
break
if frames and (
frames[-1].get("phase") != "settle"
or frames[-1].get("ref") != instance.get("settlementFrameRef")
or frames[-1].get("hash") != instance.get("settlementFrameHash")
):
errors.append("#/settlementFrameRef: 必须等于末个 settle 帧")
return errors
def _reference_asset_path_errors(value, path: str) -> list:
"""校验新参照资产契约中的 NFC、相对路径和目录边界约束。"""
if not isinstance(value, str):
return []
errors = []
if unicodedata.normalize("NFC", value) != value:
errors.append(f"{path}: 路径必须是 NFC 规范化字符串")
parts = value.split("/")
if value.startswith("/") or "\\" in value or "" in parts or any(part in (".", "..") for part in parts):
errors.append(f"{path}: 必须是仓内相对 POSIX 路径,不得越界")
if any(ord(char) < 0x20 or ord(char) == 0x7F for char in value):
errors.append(f"{path}: 路径不得包含控制字符")
return errors
def _reference_asset_record_path_errors(record: dict, path: str = "#") -> list:
"""检查 Record/2 中所有可能指向制品或证据的引用。"""
errors = []
for field in ("assetRef", "artifactRef", "consumptionManifestRef"):
if record.get(field) is not None:
errors += _reference_asset_path_errors(record[field], f"{path}/{field}")
for field in ("designRef", "evidenceRefs"):
for index, ref in enumerate(record.get(field) or []):
errors += _reference_asset_path_errors(ref, f"{path}/{field}/{index}")
return errors
def _semantic_validate_reference_asset_record_v2(instance: dict) -> list:
"""补 Record/2 的路径规范化约束,结构与生命周期条件由 schema 层负责。"""
return _reference_asset_record_path_errors(instance)
def _semantic_validate_reference_asset_registry_v2(instance: dict) -> list:
"""补 Registry/2 的唯一性、迁移状态和每条记录的路径约束。"""
errors = _semantic_validate_reference_asset_registry(instance)
for index, record in enumerate(instance.get("records") or []):
if isinstance(record, dict):
errors += _reference_asset_record_path_errors(record, f"#/records/{index}")
return errors
def _semantic_validate_reference_asset_consumption_manifest(instance: dict) -> list:
"""强制 manifest entries 的 canonical 路径、唯一性与排序。"""
errors = []
entries = instance.get("entries") or []
paths = []
for index, entry in enumerate(entries):
if not isinstance(entry, dict):
continue # 结构层负责报告非对象 entry
path = entry.get("path")
if isinstance(path, str):
paths.append(path)
errors += _reference_asset_path_errors(path, f"#/entries/{index}/path")
if len(paths) != len(set(paths)):
errors.append("#/entries: path 不得重复")
if paths != sorted(paths):
errors.append("#/entries: 必须按 path 的 canonical 字典序升序排列")
return errors
def _semantic_validate_reference_asset_consumption_manifest_bytes(
sample_path: Path, instance: dict,
) -> list:
"""校验真实 manifest 文件的原始字节必须等于 canonical JSON。"""
descriptor = _load(sample_path)
if isinstance(descriptor, dict) and "_base" in descriptor:
# 声明式负样本不是生产 manifest 字节,结构/语义变异由实例校验负责。
return []
raw = sample_path.read_bytes()
canonical = _canonical_json_bytes(instance)
if raw != canonical:
return [
"#: manifest 原始字节必须逐字等于 canonical JSONUTF-8 无 BOM、无尾随换行、"
"键按 Unicode code point 排序且不转义非 ASCII",
]
return []
def _semantic_validate_reference_asset_release(instance: dict) -> list:
"""检查 release 中两个外部引用的 canonical 路径。"""
errors = []
for field in ("registryRef", "policyRef"):
if instance.get(field) is not None:
errors += _reference_asset_path_errors(instance[field], f"#/{field}")
return errors
_REFERENCE_ASSET_SCHEMA_FILES = {
"ReferenceAssetReleaseV1": "reference-asset-release.schema.json",
"ReferenceAssetRegistryV2": "reference-asset-registry-v2.schema.json",
"ReferenceAssetConsumptionPolicyV1": "reference-asset-consumption-policy.schema.json",
"ReferenceAssetVerificationReceiptV1": "reference-asset-verification-receipt.schema.json",
}
def _load_reference_asset_file(
ref: str | None,
path: str,
expected_title: str,
errors: list,
) -> tuple[dict | None, str | None, bool]:
"""静态读取仓内普通 JSON 文件,并闭合其 schema、语义和原始字节 hash。
这里只服务 U1 的仓内 fixture 校验。它故意不承担 U3 的受信 fd、祖先 symlink、
TOCTOU 或大小限额职责;运行时消费必须使用 reference_asset_gate.py 的实现。
"""
if not isinstance(ref, str):
return None, None, False
path_errors = _reference_asset_path_errors(ref, path)
errors.extend(path_errors)
if path_errors:
return None, None, False
target = _HERE.parent.parent / ref
try:
target_stat = target.lstat()
except FileNotFoundError:
errors.append(f"{path}: 引用文件不存在")
return None, None, False
except OSError:
errors.append(f"{path}: 引用文件不可读取")
return None, None, False
if stat.S_ISLNK(target_stat.st_mode):
errors.append(f"{path}: 末级引用不得是 symlink")
return None, None, False
if not stat.S_ISREG(target_stat.st_mode):
errors.append(f"{path}: 引用文件必须是普通文件")
return None, None, False
try:
raw = target.read_bytes()
raw_hash = hashlib.sha256(raw).hexdigest()
instance = _load_payload_canonical(raw.decode("utf-8"))
except (OSError, UnicodeError, ValueError) as exc:
errors.append(f"{path}: 引用文件不是合法 UTF-8 JSON{exc}")
return None, None, False
if not isinstance(instance, dict):
errors.append(f"{path}: 引用文件顶层必须是对象")
return None, raw_hash, False
schema_file = _REFERENCE_ASSET_SCHEMA_FILES.get(expected_title)
if schema_file is None:
errors.append(f"{path}: 未登记的参照资产 schema 类型 {expected_title}")
return instance, raw_hash, False
schema = _load(_HERE / schema_file)
schema_errors = validate(schema, instance, schema)
if schema_errors:
errors.extend(f"{path}: {message}" for message in schema_errors)
return instance, raw_hash, False
semantic_errors = _semantic_validate(schema, instance)
if semantic_errors:
errors.extend(f"{path}: {message}" for message in semantic_errors)
return instance, raw_hash, False
return instance, raw_hash, True
def _semantic_validate_reference_asset_verification_receipt(instance: dict) -> list:
"""冻结 release、policy、record 与制品双身份,任何 hash 漂移都 fail closed。"""
errors = []
release_ref = instance.get("releaseRef")
release, _, release_valid = _load_reference_asset_file(
release_ref,
"#/releaseRef",
"ReferenceAssetReleaseV1",
errors,
)
if isinstance(release, dict) and release_valid:
for receipt_field, release_field in (
("expectedRegistryHash", "registryHash"),
("policyHash", "policyHash"),
("verifierVersion", "verifierVersion"),
("trustedRootId", "trustedRootId"),
):
if instance.get(receipt_field) != release.get(release_field):
errors.append(
f"#/{receipt_field}: 必须与 releaseRef 对应的 release.{release_field} 一致",
)
policy_ref = release.get("policyRef")
policy, policy_hash, policy_valid = _load_reference_asset_file(
policy_ref,
"#/releaseRef/policyRef",
"ReferenceAssetConsumptionPolicyV1",
errors,
)
if policy_valid and isinstance(policy, dict):
if policy_hash != release.get("policyHash"):
errors.append("#/releaseRef/policyHash: 必须等于 policy 原始字节 SHA-256")
for field in ("recordId", "role", "consumerRef"):
if instance.get(field) != policy.get(field):
errors.append(f"#/{field}: 必须与 release policy 的 {field} 一致")
registry_ref = release.get("registryRef")
registry, registry_hash, registry_valid = _load_reference_asset_file(
registry_ref,
"#/releaseRef/registryRef",
"ReferenceAssetRegistryV2",
errors,
)
if registry_valid and isinstance(registry, dict):
if registry_hash != release.get("registryHash"):
errors.append("#/releaseRef/registryHash: 必须等于 registry 原始字节 SHA-256")
if instance.get("expectedRegistryHash") != release.get("registryHash"):
errors.append("#/expectedRegistryHash: 必须与 release.registryHash 一致")
if instance.get("observedRegistryHash") != registry_hash:
errors.append("#/observedRegistryHash: 必须等于 registry 原始字节 SHA-256")
if instance.get("registryVersion") != registry.get("registryVersion"):
errors.append("#/registryVersion: 必须与 release registry 的 registryVersion 一致")
record = next(
(
candidate for candidate in registry.get("records") or []
if isinstance(candidate, dict) and candidate.get("recordId") == instance.get("recordId")
),
None,
)
if record is None:
errors.append("#/recordId: release registry 中不存在对应 recordId")
elif record.get("lifecycleStatus") != "active":
errors.append("#/recordId: release registry record 必须处于 active 生命周期")
else:
for field in ("role", "consumerRef", "artifactRef", "consumptionManifestRef"):
if instance.get(field) != record.get(field):
errors.append(f"#/{field}: 必须与 release registry record 一致")
for receipt_field, record_field in (
("artifactHash", "artifactHash"),
("consumptionManifestHash", "consumptionManifestHash"),
):
if (instance.get("expected") or {}).get(receipt_field) != record.get(record_field):
errors.append(f"#/expected/{receipt_field}: 必须与 release registry record 一致")
trusted_root_id = instance.get("trustedRootId")
if isinstance(trusted_root_id, str) and (
Path(trusted_root_id).is_absolute()
or trusted_root_id.startswith(("/", "\\"))
or "/" in trusted_root_id
or "\\" in trusted_root_id
):
errors.append("#/trustedRootId: 只能是逻辑身份,不得是绝对路径或文件路径")
expected = instance.get("expected") or {}
observed = instance.get("observed") or {}
if isinstance(expected, dict) and isinstance(observed, dict):
for field in ("artifactHash", "consumptionManifestHash"):
if expected.get(field) != observed.get(field):
errors.append(f"#/observed/{field}: 必须等于 expected/{field}")
if instance.get("expectedRegistryHash") != instance.get("observedRegistryHash"):
errors.append("#/observedRegistryHash: 必须等于 expectedRegistryHash")
for field in ("artifactRef", "consumptionManifestRef"):
if instance.get(field) is not None:
errors += _reference_asset_path_errors(instance[field], f"#/{field}")
return errors
def _semantic_validate_acceptance_provenance_v4(instance: dict) -> list:
"""读取 v4 provenance 的回执文件,并闭合 ref/hash 与消费身份声明。"""
errors = []
seen = set()
receipt_record_ids = set()
record_ids = instance.get("referenceAssetRecordIds")
consumed_assets = instance.get("consumedReferenceAssets")
consumed_by_id = {}
if isinstance(consumed_assets, list):
for index, asset in enumerate(consumed_assets):
if not isinstance(asset, dict):
continue # 结构层负责报告非对象资产
record_id = asset.get("recordId")
if record_id in consumed_by_id:
errors.append(f"#/consumedReferenceAssets/{index}/recordId: recordId 不得重复")
else:
consumed_by_id[record_id] = asset
if not isinstance(record_ids, list) or not record_ids:
errors.append("#/referenceAssetRecordIds: 有 receipt 时必须声明非空 recordId 集合")
if not isinstance(instance.get("consumerRef"), str):
errors.append("#/consumerRef: 有 receipt 时必须声明 consumerRef")
if not isinstance(consumed_assets, list) or not consumed_assets:
errors.append("#/consumedReferenceAssets: 有 receipt 时必须声明非空消费资产集合")
for index, receipt in enumerate(instance.get("referenceAssetVerificationReceipts") or []):
if not isinstance(receipt, dict):
continue # 结构层负责报告非对象回执引用
ref = receipt.get("ref")
if isinstance(ref, str):
errors += _reference_asset_path_errors(ref, f"#/referenceAssetVerificationReceipts/{index}/ref")
if ref in seen:
errors.append(f"#/referenceAssetVerificationReceipts/{index}/ref: 回执 ref 不得重复")
seen.add(ref)
loaded_receipt, raw_hash, receipt_valid = _load_reference_asset_file(
ref,
f"#/referenceAssetVerificationReceipts/{index}/ref",
"ReferenceAssetVerificationReceiptV1",
errors,
)
if raw_hash is not None and receipt.get("hash") != raw_hash:
errors.append(
f"#/referenceAssetVerificationReceipts/{index}/hash: 必须等于 receipt 原始字节 SHA-256",
)
if not receipt_valid or not isinstance(loaded_receipt, dict):
continue
receipt_record_id = loaded_receipt.get("recordId")
if receipt_record_id in receipt_record_ids:
errors.append(f"#/referenceAssetVerificationReceipts/{index}: receipt recordId 不得重复")
receipt_record_ids.add(receipt_record_id)
if not isinstance(record_ids, list) or receipt_record_id not in record_ids:
errors.append(f"#/referenceAssetRecordIds: 缺少 receipt 的 recordId {receipt_record_id}")
if loaded_receipt.get("consumerRef") != instance.get("consumerRef"):
errors.append(f"#/consumerRef: 必须与 receipt {receipt_record_id} 一致")
consumed = consumed_by_id.get(receipt_record_id)
if consumed is None:
errors.append(f"#/consumedReferenceAssets: 缺少 receipt 的 recordId {receipt_record_id}")
else:
if consumed.get("role") != loaded_receipt.get("role"):
errors.append(f"#/consumedReferenceAssets/{receipt_record_id}/role: 必须与 receipt 一致")
expected_hash = (loaded_receipt.get("expected") or {}).get("artifactHash")
if consumed.get("artifactHash") != expected_hash:
errors.append(f"#/consumedReferenceAssets/{receipt_record_id}/artifactHash: 必须与 receipt expected 一致")
if isinstance(record_ids, list) and set(record_ids) != receipt_record_ids:
errors.append("#/referenceAssetRecordIds: 必须与 receipt recordId 集合一致")
if set(consumed_by_id) != receipt_record_ids:
errors.append("#/consumedReferenceAssets: 必须与 receipt recordId 集合一致")
return errors
def _semantic_validate_reference_asset_registry(instance: dict) -> list:
"""参照资产注册表跨字段语义recordId 全局唯一、注释不悬空、迁移清单不得含 active。
JSON Schema 子集表达不了跨数组元素的字段唯一性recordId 唯一(金标 SoT §7『稳定唯一』
在此强制。registryVersion 带 .migration-list 后缀表示本快照只是迁移清单、不是可供新 live
消费的机器注册表——含 active 记录即自相矛盾(『不伪装 active』红线的机器化
"""
errors = []
records = instance.get("records") or []
seen: dict = {}
for index, record in enumerate(records):
if not isinstance(record, dict):
continue # 结构错由 schema 层报,语义层只做跨字段不变量
record_id = record.get("recordId")
if record_id in seen:
errors.append(
f"#/records/{index}/recordId: {record_id} 与 #/records/{seen[record_id]} 重复,"
"违反金标 SoT §7 recordId 稳定唯一约束"
)
else:
seen[record_id] = index
for note_key in instance.get("migrationNotes") or {}:
if note_key != "_registry" and note_key not in seen:
errors.append(f"#/migrationNotes/{note_key}: 注释指向 records 中不存在的 recordId")
registry_version = instance.get("registryVersion") or ""
if registry_version.endswith(".migration-list"):
for index, record in enumerate(records):
if isinstance(record, dict) and record.get("lifecycleStatus") == "active":
errors.append(
f"#/records/{index}/lifecycleStatus: migration-list 版本注册表不得含 active 记录"
"(迁移完成前不是可供 live 消费的机器注册表)"
)
return errors
def _semantic_validate(schema: dict, instance) -> list:
"""按 schema 标题分派跨字段语义校验;旧 C5/C6 不改变既有行为。"""
if not isinstance(instance, dict):
return []
title = schema.get("title")
if title == "ProofObligationRegistry":
return _semantic_validate_registry(instance)
if title == "ProofObligationRegistryLegacyV2":
return _semantic_validate_registry(instance, validate_interaction=False)
if title == "LegacyGameLogMapping":
return _semantic_validate_legacy_mapping(instance)
if title == "GameEvent":
return _semantic_validate_game_event(instance)
if title == "PlaytestEvidence":
return _semantic_validate_playtest(instance)
if title == "SingleRollFact":
return _semantic_validate_single_roll(instance)
if title == "ActorSelectionV1":
return _semantic_validate_actor_selection(instance)
if title in ("ActorViewV3Frozen", "ActorViewV4"):
return _semantic_validate_actor_view_v4(instance)
if title == "ActorProtocolBundleV1":
return _semantic_validate_actor_protocol_bundle(instance)
if title == "ActorSelectionContextV1":
return _semantic_validate_actor_selection_context(instance)
if title == "ActorSelectionAttemptV1":
return _semantic_validate_selection_attempt(instance)
if title == "Match3PairResolutionV1":
return _semantic_validate_pair_resolution(instance)
if title == "VisualTargetSetV1":
return _semantic_validate_visual_target_set(instance)
if title == "TargetResolutionV1":
return _semantic_validate_target_resolution(instance)
if title == "CostReservationV1":
return _semantic_validate_cost_reservation(instance)
if title == "PlaytestEvidenceV3":
return _semantic_validate_playtest_v3(instance)
if title == "SingleRollFactV3":
return _semantic_validate_single_roll_v3(instance)
if title in (
"AcceptanceRequest", "AcceptanceProvenance",
"AcceptanceRequestV2", "AcceptanceProvenanceV2",
"AcceptanceRequestV3", "AcceptanceProvenanceV3",
):
return _semantic_validate_acceptance_profile(instance)
if title == "InteractionProfileRegistry":
return _semantic_validate_interaction_registry(instance)
if title == "InteractionProfileRegistryV2":
return _semantic_validate_interaction_registry_v2(instance)
if title == "ReferenceAssetRegistryV1":
return _semantic_validate_reference_asset_registry(instance)
if title == "ReferenceAssetRecordV2":
return _semantic_validate_reference_asset_record_v2(instance)
if title == "ReferenceAssetRegistryV2":
return _semantic_validate_reference_asset_registry_v2(instance)
if title == "ReferenceAssetConsumptionManifestV1":
return _semantic_validate_reference_asset_consumption_manifest(instance)
if title == "ReferenceAssetConsumptionPolicyV1":
return []
if title == "ReferenceAssetReleaseV1":
return _semantic_validate_reference_asset_release(instance)
if title == "ReferenceAssetVerificationReceiptV1":
return _semantic_validate_reference_asset_verification_receipt(instance)
if title == "AcceptanceProvenanceV4":
return (
_semantic_validate_acceptance_profile(instance)
+ _semantic_validate_acceptance_provenance_v4(instance)
)
if title == "Match3ProducerPreflightV1":
return _semantic_validate_match3_producer_preflight(instance)
if title == "InteractionBindingV1":
return _semantic_validate_interaction_binding(instance)
if title == "ProofObligationResolutionV1":
return _semantic_validate_proof_resolution(instance)
if title == "SingleRollProofV2":
return _semantic_validate_single_roll_proof_v2(instance)
if title == "CollectionProofV1":
return _semantic_validate_collection_proof(instance)
if title == "JudgePackageV1":
return _semantic_validate_judge_package(instance)
if title == "Match3BoardProjectionV1":
return _semantic_validate_match3_projection(instance)
if title == "Match3SwapSetV1":
return _semantic_validate_match3_swap_set(instance)
if title == "BoardIdentityCheckV1":
return _semantic_validate_board_identity(instance)
if title == "Match3ActionGroupV1":
return _semantic_validate_action_group(instance)
if title == "Match3RollInteractionAuditV1":
return _semantic_validate_roll_audit(instance)
if title == "Match3AudioAuditV1":
return _semantic_validate_match3_audio_audit(instance)
if title == "Match3RollInteractionAuditV2":
return _semantic_validate_roll_audit_v2(instance)
if title == "SingleRollProofV3":
return _semantic_validate_single_roll_proof_v3(instance)
if title == "BuildManifestV1":
return _semantic_validate_build_manifest(instance)
if title == "Match3VisualTransactionV1":
return _semantic_validate_visual_transaction(instance)
if title == "Match3VisualAuditV1":
return _semantic_validate_visual_audit(instance)
if title == "Match3EffectAuditV1":
return _semantic_validate_effect_audit(instance)
if title == "Match3FilmstripAnalyzerV1":
return _semantic_validate_filmstrip(instance)
return []
def _canonical_json_bytes(value) -> bytes:
"""按项目自有规则生成 canonical JSON 原始字节,不加 BOM 或尾随换行。"""
# Python 对字符串键按 Unicode code point 排序ensure_ascii=False 保留非 ASCII 原字节。
return json.dumps(
value,
ensure_ascii=False,
sort_keys=True,
separators=(",", ":"),
allow_nan=False,
).encode("utf-8")
def _load(p: Path):
return _load_payload_canonical(p.read_text(encoding="utf-8"))
def _pointer_parts(pointer: str) -> list:
"""把 JSON Pointer 拆成键路径;样本变异器只接受绝对 pointer。"""
if not pointer.startswith("/"):
raise ValueError(f"JSON Pointer 必须以 / 开头:{pointer}")
return [part.replace("~1", "/").replace("~0", "~")
for part in pointer[1:].split("/")]
def _walk_pointer(root, pointer: str):
"""走到 JSON Pointer 的父节点,返回(父节点,末段键)。"""
parts = _pointer_parts(pointer)
if not parts:
raise ValueError("不允许替换样本根节点")
node = root
for part in parts[:-1]:
node = node[int(part)] if isinstance(node, list) else node[part]
return node, parts[-1]
def _set_pointer(root, pointer: str, value) -> None:
"""按 JSON Pointer 设置测试样本字段,供负样本从正样本做最小单点变异。"""
parent, key = _walk_pointer(root, pointer)
if isinstance(parent, list):
parent[int(key)] = deepcopy(value)
else:
parent[key] = deepcopy(value)
def _delete_pointer(root, pointer: str) -> None:
"""按 JSON Pointer 删除测试样本字段。"""
parent, key = _walk_pointer(root, pointer)
if isinstance(parent, list):
del parent[int(key)]
else:
del parent[key]
def _load_sample_instance(sample_path: Path):
"""读取样本;带 _base 的负样本按声明式变异生成完整待校验实例。
负样本往往只改一个跨字段不变量。若复制整份几百行 playtest/2 正样本,后续协议加字段时
多份副本会一起漂移。`_base + _set/_delete` 让每个负样本只声明那一个真实错法,同时
校验器最终看到的仍是完整契约实例,不会因缺一堆无关必填字段而“偶然被拦”。
"""
descriptor = _load(sample_path)
if not isinstance(descriptor, dict) or "_base" not in descriptor:
return descriptor
base_path = (sample_path.parent / descriptor["_base"]).resolve()
# fixture 可以在同一契约族内逐层复用;每层仍只声明本次变异,最终得到完整实例。
instance = deepcopy(_load_sample_instance(base_path))
for pointer, value in (descriptor.get("_set") or {}).items():
_set_pointer(instance, pointer, value)
for pointer in descriptor.get("_delete") or []:
_delete_pointer(instance, pointer)
return instance
def _sealed_sample_bytes(sample_path: Path, instance: dict) -> bytes:
"""生产文件按原始字节取 hash声明式 fixture 按 Node 两空格封存口径物化。"""
descriptor = _load(sample_path)
if isinstance(descriptor, dict) and "_base" in descriptor:
return (json.dumps(instance, ensure_ascii=False, indent=2) + "\n").encode("utf-8")
return sample_path.read_bytes()
def _raw_sample_hash(path: Path, instance: dict) -> str:
return hashlib.sha256(_sealed_sample_bytes(path, instance)).hexdigest()
def _extract_top_level_json_value(source: str, wanted_key: str) -> str:
"""保留顶层字段的原始 JSON 片段,用于复核 helper 两次 Projection 字节身份。"""
decoder = json.JSONDecoder()
index = 0
length = len(source)
def skip_space(position: int) -> int:
while position < length and source[position].isspace():
position += 1
return position
index = skip_space(index)
if index >= length or source[index] != "{":
raise ValueError("顶层不是对象")
index += 1
while True:
index = skip_space(index)
if index < length and source[index] == "}":
break
key, index = decoder.raw_decode(source, index)
if not isinstance(key, str):
raise ValueError("对象键不是字符串")
index = skip_space(index)
if index >= length or source[index] != ":":
raise ValueError("对象键后缺冒号")
value_start = skip_space(index + 1)
_, value_end = decoder.raw_decode(source, value_start)
if key == wanted_key:
return source[value_start:value_end]
index = skip_space(value_end)
if index < length and source[index] == ",":
index += 1
continue
if index < length and source[index] == "}":
break
raise ValueError("对象字段分隔非法")
raise ValueError(f"缺少顶层字段 {wanted_key}")
def _decode_png_rgb(raw: bytes) -> tuple[int, int, bytes]:
"""解码证据门使用的 8-bit RGB/RGBA 非隔行 PNG拒绝未知格式而不猜测。"""
if not raw.startswith(b"\x89PNG\r\n\x1a\n"):
raise ValueError("PNG signature")
index = 8
width = height = color_type = None
compressed = bytearray()
while index + 12 <= len(raw):
size = struct.unpack(">I", raw[index:index + 4])[0]
kind = raw[index + 4:index + 8]
payload = raw[index + 8:index + 8 + size]
if index + 12 + size > len(raw):
raise ValueError("PNG chunk")
if zlib.crc32(kind + payload) & 0xFFFFFFFF != struct.unpack(
">I", raw[index + 8 + size:index + 12 + size],
)[0]:
raise ValueError("PNG crc")
if kind == b"IHDR":
width, height, depth, color_type, compression, filtering, interlace = struct.unpack(
">IIBBBBB", payload,
)
if depth != 8 or color_type not in (2, 6) or compression != 0 \
or filtering != 0 or interlace != 0:
raise ValueError("PNG format")
elif kind == b"IDAT":
compressed.extend(payload)
elif kind == b"IEND":
break
index += 12 + size
if not isinstance(width, int) or not isinstance(height, int) or width <= 0 or height <= 0:
raise ValueError("PNG IHDR")
channels = 3 if color_type == 2 else 4
scanlines = zlib.decompress(bytes(compressed))
stride = width * channels
if len(scanlines) != height * (stride + 1):
raise ValueError("PNG scanline size")
decoded = bytearray(height * stride)
source_index = 0
previous = bytearray(stride)
for row in range(height):
filter_type = scanlines[source_index]
source_index += 1
current = bytearray(scanlines[source_index:source_index + stride])
source_index += stride
for offset in range(stride):
left = current[offset - channels] if offset >= channels else 0
up = previous[offset]
up_left = previous[offset - channels] if offset >= channels else 0
if filter_type == 1:
current[offset] = (current[offset] + left) & 0xFF
elif filter_type == 2:
current[offset] = (current[offset] + up) & 0xFF
elif filter_type == 3:
current[offset] = (current[offset] + ((left + up) // 2)) & 0xFF
elif filter_type == 4:
predictor = left + up - up_left
distances = (abs(predictor - left), abs(predictor - up), abs(predictor - up_left))
paeth = (left, up, up_left)[distances.index(min(distances))]
current[offset] = (current[offset] + paeth) & 0xFF
elif filter_type != 0:
raise ValueError("PNG filter")
decoded[row * stride:(row + 1) * stride] = current
previous = current
if channels == 3:
return width, height, bytes(decoded)
rgb = bytearray(width * height * 3)
for pixel in range(width * height):
rgb[pixel * 3:pixel * 3 + 3] = decoded[pixel * 4:pixel * 4 + 3]
return width, height, bytes(rgb)
def _projection_equivalence_hash(projection: dict) -> str | None:
"""按 cell 两两是否同类生成与 classId 字面无关的等价分区 hash。"""
cells = projection.get("cells") or []
if not cells or any(row.get("state") != "classified" for row in cells):
return None
labels = [row.get("classId") for row in cells]
relations = [[left == right for right in labels] for left in labels]
payload = {"dimensions": projection.get("dimensions"), "relations": relations}
return _canonical_domain_hash("anonymous-cell-equivalence/1", payload)
def _proof_action_projection(action: dict) -> dict:
"""完整原生 actionRecord 到 ProofAction/1 的唯一允许投影。"""
return {
"schemaVersion": "ProofAction/1",
"actionId": action.get("actionId"),
"normalized": deepcopy(action.get("normalized")),
"preFrameRef": deepcopy(action.get("preFrameRef")),
"preEventSeq": action.get("preEventSeq"),
"virtualTimeBudgetMs": action.get("virtualTimeBudgetMs"),
"timeAdvance": deepcopy(action.get("timeAdvance")),
"postFrameRef": deepcopy(action.get("postFrameRef")),
"postEventSeq": action.get("postEventSeq"),
}
def _judge_action_projection(action: dict, events: list) -> dict:
"""完整原生 actionRecord 到 JudgeAction 的唯一允许投影。"""
pre_seq, post_seq = action.get("preEventSeq", 0), action.get("postEventSeq", 0)
return {
"actionId": action.get("actionId"),
"normalized": deepcopy(action.get("normalized")),
"valid": action.get("valid"),
"preFrameRef": deepcopy(action.get("preFrameRef")),
"preFrameHash": (action.get("preFrameRef") or {}).get("hash"),
"preEventSeq": pre_seq,
"virtualTimeBudgetMs": action.get("virtualTimeBudgetMs"),
"postFrameRef": deepcopy(action.get("postFrameRef")),
"postFrameHash": (action.get("postFrameRef") or {}).get("hash"),
"postEventSeq": post_seq,
"gameEvents": [deepcopy(event) for event in events if pre_seq < event.get("seq", 0) <= post_seq],
}
def _proof_obligation_from_candidate(candidate: dict) -> dict:
"""Judge 候选到 SingleRollProof obligation 的确定性状态投影。"""
state = candidate.get("candidateState")
return {
"id": candidate.get("id"),
"required": candidate.get("required"),
"status": "satisfied" if state == "complete" else "missing",
"sequenceRefs": deepcopy(candidate.get("sequenceRefs") or []),
"actionRefs": deepcopy(candidate.get("actionRefs") or []),
"postFrameRefs": deepcopy(candidate.get("postFrameRefs") or []),
"eventRefs": deepcopy(candidate.get("eventRefs") or []),
"contradictions": [],
"blockingProblems": deepcopy(candidate.get("candidateProblems") or []),
}
def _schema_errors(schema_name: str, instance: dict) -> list:
schema = _load(_HERE / schema_name)
errors = validate(schema, instance, schema)
if not errors:
errors += _semantic_validate(schema, instance)
return errors
def _validate_match3_action_bundle(manifest_path: Path) -> list:
"""读取 D-F 全部 runner 审计制品,验证 pair 双 tap 到两步 proof 的完整闭包。"""
errors = []
manifest = _load_sample_instance(manifest_path)
manifest_schema = _load(_HERE / "match3-action-bundle.schema.json")
errors += validate(manifest_schema, manifest, manifest_schema)
_trace(
"match3-action-bundle", "manifest-schema", path=manifest_path,
result="pass" if not errors else "fail", error=errors[0] if errors else None,
)
if errors:
return errors
def load_ref(field: str):
path = (manifest_path.parent / manifest[field]).resolve()
if not path.is_file():
errors.append(f"#/{field}: 引用文件不存在")
_trace("load-ref", "file-exists", ref=manifest.get(field), path=path,
result="fail", error="引用文件不存在")
return None, None
_trace("load-ref", "file-exists", ref=manifest.get(field), path=path, result="pass")
return path, _load_sample_instance(path)
specs = {
"requestRef": "acceptance-request-v2.schema.json",
"provenanceRef": "acceptance-provenance-v2.schema.json",
"bindingRef": "interaction-binding.schema.json",
"registryRef": "interaction-profile-registry-v2.schema.json",
"actorProtocolBundleRef": "actor-protocol-bundle.schema.json",
"actorViewRef": "actor-view-v4.schema.json",
"selectionRef": "actor-selection-v2.schema.json",
"selectionAttemptRef": "actor-selection-attempt.schema.json",
"pairResolutionRef": "match3-pair-resolution.schema.json",
"swapSetRef": "match3-swap-set.schema.json",
"initialTargetSetRef": "visual-target-set.schema.json",
"postFirstTargetSetRef": "visual-target-set.schema.json",
"initialProjectionRef": "match3-board-projection.schema.json",
"postFirstProjectionRef": "match3-board-projection.schema.json",
"firstGroupedActionRef": "grouped-action-ref.schema.json",
"secondGroupedActionRef": "grouped-action-ref.schema.json",
"boardIdentityCheckRef": "board-identity-check.schema.json",
"actionGroupRef": "match3-action-group.schema.json",
"resolutionRef": "proof-obligation-resolution.schema.json",
"collectionProofRef": "collection-proof.schema.json",
"singleRollProofRef": "single-roll-proof-v2.schema.json",
"judgePackageRef": "judge-package.schema.json",
"rollAuditRef": "match3-roll-interaction-audit.schema.json",
}
loaded = {}
paths = {}
for field, schema_name in specs.items():
path, value = load_ref(field)
if path is None:
continue
paths[field], loaded[field] = path, value
ref_errors = [f"#/{field}{error[1:]}" for error in _schema_errors(schema_name, value)]
errors += ref_errors
_trace("load-ref", "schema-and-semantic", ref=manifest.get(field), path=path,
result="pass" if not ref_errors else "fail", error=ref_errors[0] if ref_errors else None)
for field in ("firstActionRef", "secondActionRef"):
path, value = load_ref(field)
if path is None:
continue
paths[field], loaded[field] = path, value
root = _load(_HERE / "playtest-evidence-v3.schema.json")
action_schema = root["$defs"]["actionRecord"]
action_errors = validate(action_schema, value, root)
if not action_errors:
action_errors += _semantic_validate_v3_action(value, "#")
errors += [f"#/{field}{error[1:]}" for error in action_errors]
perception_audit_path, perception_audit = load_ref("postFirstPerceptionAuditRef")
if perception_audit_path is not None:
paths["postFirstPerceptionAuditRef"] = perception_audit_path
loaded["postFirstPerceptionAuditRef"] = perception_audit
if (manifest_path.parent / manifest["postFirstPerceptionAuditRef"]).is_symlink():
errors.append("#/postFirstPerceptionAuditRef: 感知审计不得是符号链接")
if not isinstance(perception_audit, dict):
errors.append("#/postFirstPerceptionAuditRef: 感知审计顶层必须是 JSON 对象")
events_path, events = load_ref("eventsRef")
if events_path is not None:
paths["eventsRef"], loaded["eventsRef"] = events_path, events
if not isinstance(events, list) or len(events) < 2:
errors.append("#/eventsRef: 必须封存覆盖双 tap 区间的完整 game event slice")
else:
for index, event in enumerate(events):
errors += [
f"#/eventsRef/{index}{error[1:]}"
for error in _schema_errors("game-event.schema.json", event)
]
brief_path = (manifest_path.parent / manifest["briefRef"]).resolve()
if brief_path.is_symlink() or not brief_path.is_file():
errors.append("#/briefRef: brief 必须是存在的普通非符号链接文件")
_trace("load-ref", "brief-regular-file", ref=manifest.get("briefRef"), path=brief_path,
result="fail", error="brief 不是普通文件")
brief_bytes = b""
else:
brief_bytes = brief_path.read_bytes()
_trace("load-ref", "brief-regular-file", ref=manifest.get("briefRef"), path=brief_path, result="pass")
frame_bytes = {}
for ref_field, hash_field in (
("initialFrameFileRef", "initialFrameHash"),
("postFirstFrameFileRef", "postFirstFrameHash"),
("secondPostFrameFileRef", "secondPostFrameHash"),
):
frame_path = (manifest_path.parent / manifest[ref_field]).resolve()
try:
mode = frame_path.lstat().st_mode
except FileNotFoundError:
mode = None
if mode is None or not stat.S_ISREG(mode) or frame_path.is_symlink():
errors.append(f"#/{ref_field}: frame 必须是存在的普通非符号链接文件")
_trace("frame", "regular-file", ref=manifest.get(ref_field), path=frame_path,
declared_hash=manifest.get(hash_field), result="fail", error="frame 文件不存在或不是普通文件")
continue
raw = frame_path.read_bytes()
computed = hashlib.sha256(raw).hexdigest()
frame_bytes[ref_field] = raw
if manifest.get(hash_field) != computed:
errors.append(f"#/{hash_field}: 与 frame 实际字节 SHA-256 不一致")
_trace("frame", "bytes-sha256", ref=manifest.get(ref_field), path=frame_path,
declared_hash=manifest.get(hash_field), computed_hash=computed,
result="fail", error="frame 字节 hash 漂移")
else:
_trace("frame", "bytes-sha256", ref=manifest.get(ref_field), path=frame_path,
declared_hash=manifest.get(hash_field), computed_hash=computed, result="pass")
if errors:
# 结构错误后仍允许下面的跨字段门产出目标根因,但缺文件时必须停止解引用。
if any(value is None for value in loaded.values()) or len(loaded) < len(specs) + 4:
return errors
request = loaded["requestRef"]
provenance = loaded["provenanceRef"]
binding = loaded["bindingRef"]
registry = loaded["registryRef"]
protocol = loaded["actorProtocolBundleRef"]
view = loaded["actorViewRef"]
selection = loaded["selectionRef"]
attempt = loaded["selectionAttemptRef"]
pair_resolution = loaded["pairResolutionRef"]
swap_set = loaded["swapSetRef"]
initial_targets = loaded["initialTargetSetRef"]
post_targets = loaded["postFirstTargetSetRef"]
initial_projection = loaded["initialProjectionRef"]
post_projection = loaded["postFirstProjectionRef"]
perception_audit = loaded["postFirstPerceptionAuditRef"]
first_grouped = loaded["firstGroupedActionRef"]
second_grouped = loaded["secondGroupedActionRef"]
first_action = loaded["firstActionRef"]
second_action = loaded["secondActionRef"]
board = loaded["boardIdentityCheckRef"]
group = loaded["actionGroupRef"]
events = loaded["eventsRef"]
resolution = loaded["resolutionRef"]
collection = loaded["collectionProofRef"]
proof = loaded["singleRollProofRef"]
judge_package = loaded["judgePackageRef"]
roll_audit = loaded["rollAuditRef"]
def bundle_check(error_path: str, actual, expected, stage: str, check: str) -> None:
"""记录 bundle 跨文件等值断言;日志只存 hash/路径类标量,不存业务内容。"""
trace_hash = lambda value: value if isinstance(value, str) and re.fullmatch(r"[0-9a-f]{64}", value) else None
if actual != expected:
message = f"{error_path}: 跨文件因果身份不一致"
errors.append(message)
_trace(stage, check, declared_hash=trace_hash(actual),
computed_hash=trace_hash(expected),
result="fail", error=message)
else:
_trace(stage, check, declared_hash=trace_hash(actual),
computed_hash=trace_hash(expected), result="pass")
if registry != _load(_HERE / "interaction-profiles.v2.json"):
errors.append("#/registryRef: D-F 只能使用 canonical v2 registry")
roll_errors = _validate_match3_roll_audit(paths["rollAuditRef"])
errors += [f"#/rollAuditRef{error[1:]}" for error in roll_errors]
for field in ("interactionProfileId", "interactionRegistryVersion", "interactionBindingHash"):
if roll_audit.get(field) != binding.get(field):
errors.append(f"#/rollAuditRef/{field}: 必须镜像 bundle binding")
request_hash = _canonical_domain_hash("acceptance-request/2", request)
for field in (
"gameId", "briefHash", "genre", "templateRoute", "proofProfileId",
"proofRegistryVersion", "taskBindingHash", "interactionBinding",
"sourceArtifactHash", "parentAcceptanceRequestHash", "repairOrdinal",
):
if provenance.get(field) != request.get(field):
errors.append(f"#/provenanceRef/{field}: 必须镜像 request")
if provenance.get("acceptanceRequestHash") != request_hash:
errors.append("#/provenanceRef/acceptanceRequestHash: 与 canonical request 不一致")
if request.get("interactionBinding") != binding or provenance.get("interactionBinding") != binding:
errors.append("#/bindingRef: request/provenance/file 三方 binding 必须逐字一致")
if binding.get("interactionRegistryVersion") != "2026-07-15.v2":
errors.append("#/bindingRef/interactionRegistryVersion: D-F enforce 必须使用 v2")
for field, expected in (
("gameId", request.get("gameId")),
("artifactHash", provenance.get("artifactHash")),
("taskBindingHash", request.get("taskBindingHash")),
("acceptanceRequestHash", request_hash),
("seed", proof.get("requestedSeed")),
):
bundle_check(
f"#/rollAuditRef/{field}", roll_audit.get(field), expected,
"roll-preflight", f"formal-{field}",
)
if protocol.get("actorView") != view or protocol.get("selection") != selection:
errors.append("#/actorProtocolBundleRef: 必须逐字封存当前 View 与 Selection")
view_swaps = ((view.get("interactionCandidates") or {}).get("match3Swaps") or {})
expected_candidates = [
{
"candidateId": row.get("candidateId"),
"latticeId": swap_set.get("latticeId"),
"pair": [{"cell": deepcopy(cell)} for cell in row.get("pair") or []],
}
for row in swap_set.get("candidates") or []
]
if view_swaps != {
"candidateSetHash": swap_set.get("candidateSetHash"), "candidates": expected_candidates,
}:
errors.append("#/actorViewRef/interactionCandidates: 必须是 SwapSet 的精确脱敏投影")
errors += [f"#/actorProtocolBundleRef{error[1:]}" for error in _semantic_validate_actor_selection_context({
"actorView": view, "selection": selection,
})]
raw_hashes = {field: _raw_sample_hash(paths[field], loaded[field]) for field in paths}
normalization = board.get("overlayNormalizationEvidence") or {}
audit_raw = paths["postFirstPerceptionAuditRef"].read_bytes()
audit_input = perception_audit.get("input") or {}
audit_invocations = ((perception_audit.get("helper") or {}).get("invocations") or [])
if manifest.get("postFirstPerceptionAuditHash") != raw_hashes.get("postFirstPerceptionAuditRef"):
errors.append("#/postFirstPerceptionAuditHash: 与感知审计原始字节不一致")
if normalization.get("auditRef") != manifest.get("postFirstPerceptionAuditRef") \
or normalization.get("auditHash") != raw_hashes.get("postFirstPerceptionAuditRef"):
errors.append("#/boardIdentityCheckRef/overlayNormalizationEvidence: 未绑定感知审计原始文件")
if perception_audit.get("schemaVersion") != "Match3PerceptionShadowAudit/1" \
or perception_audit.get("mode") != "shadow" \
or perception_audit.get("status") not in ("shadow_resolved", "shadow_indeterminate") \
or perception_audit.get("error") is not None \
or perception_audit.get("auditBytes") != len(audit_raw):
errors.append("#/postFirstPerceptionAuditRef: 审计身份、状态或 auditBytes 非法")
if perception_audit.get("interactionBinding") != binding:
errors.append("#/postFirstPerceptionAuditRef/interactionBinding: 必须镜像可信 binding")
expected_audit_input = {
"sourcePath": _audit_path_ref(
(manifest_path.parent / manifest["postFirstFrameFileRef"]).resolve(),
),
"sourceFrameRef": post_targets.get("sourceFrameRef"),
"expectedSourceHash": post_targets.get("sourceFrameHash"),
"targetSetRef": manifest.get("postFirstTargetSetRef"),
"targetSetPath": _audit_path_ref(paths["postFirstTargetSetRef"]),
"expectedTargetSetHash": post_targets.get("targetSetHash"),
"latticeId": post_projection.get("latticeId"),
"projectionRef": manifest.get("postFirstProjectionRef"),
"overlayReferenceFrameRef": initial_targets.get("sourceFrameRef"),
"overlayReferencePath": _audit_path_ref(
(manifest_path.parent / manifest["initialFrameFileRef"]).resolve(),
),
"expectedOverlayReferenceHash": initial_targets.get("sourceFrameHash"),
"selectedCell": (pair_resolution.get("firstEndpoint") or {}).get("cell"),
}
for field, expected in expected_audit_input.items():
if audit_input.get(field) != expected:
errors.append(f"#/postFirstPerceptionAuditRef/input/{field}: 未绑定双 tap 当前闭包")
if perception_audit.get("projection") != post_projection:
errors.append("#/postFirstPerceptionAuditRef/projection: 必须逐字镜像 post-first Projection")
first_invocation = audit_invocations[0] if len(audit_invocations) == 2 else {}
second_invocation = audit_invocations[1] if len(audit_invocations) == 2 else {}
helper = perception_audit.get("helper") or {}
expected_flags = [
"--source", "--target-set", "--expected-source-hash", "--expected-target-set-hash",
"--lattice-id", "--interaction-binding-hash", "--interaction-registry-version",
"--source-frame-ref", "--target-set-ref", "--overlay-reference",
"--expected-overlay-reference-hash", "--selected-row", "--selected-column",
]
helper_args = helper.get("args") or []
helper_values = {}
if helper.get("path") != "game-runtime/games/_wg1-gen/_shared/match3_board_perception.py" \
or len(helper_args) != len(expected_flags) * 2:
errors.append("#/postFirstPerceptionAuditRef/helper: helper 身份或参数形状非法")
else:
for index, flag in enumerate(expected_flags):
if helper_args[index * 2] != flag:
errors.append("#/postFirstPerceptionAuditRef/helper/args: 参数顺序非法")
break
helper_values[flag] = helper_args[index * 2 + 1]
expected_helper_values = {
"--source": audit_input.get("sourcePath"),
"--target-set": audit_input.get("targetSetPath"),
"--expected-source-hash": audit_input.get("expectedSourceHash"),
"--expected-target-set-hash": audit_input.get("expectedTargetSetHash"),
"--lattice-id": audit_input.get("latticeId"),
"--interaction-binding-hash": binding.get("interactionBindingHash"),
"--interaction-registry-version": binding.get("interactionRegistryVersion"),
"--source-frame-ref": audit_input.get("sourceFrameRef"),
"--target-set-ref": audit_input.get("targetSetRef"),
"--overlay-reference": audit_input.get("overlayReferencePath"),
"--expected-overlay-reference-hash": audit_input.get("expectedOverlayReferenceHash"),
"--selected-row": str(((pair_resolution.get("firstEndpoint") or {}).get("cell") or {}).get("row")),
"--selected-column": str(((pair_resolution.get("firstEndpoint") or {}).get("cell") or {}).get("column")),
}
if any(helper_values.get(flag) != value for flag, value in expected_helper_values.items()):
errors.append("#/postFirstPerceptionAuditRef/helper/args: 参数未绑定当前证据闭包")
invocation_keys = {
"ordinal", "exitCode", "signal", "spawnError", "stdoutBytes", "stdoutHash", "stdout",
"stderrBytes", "stderrHash", "stderr", "projectionRawBytes", "projectionRawHash",
"diagnostics", "peakRssBytes",
}
projection_raw_hashes = []
for ordinal, invocation in enumerate((first_invocation, second_invocation), start=1):
stdout = invocation.get("stdout")
stderr = invocation.get("stderr")
try:
output = json.loads(stdout)
projection_raw = _extract_top_level_json_value(stdout, "projection")
except (TypeError, ValueError, json.JSONDecodeError):
output = {}
projection_raw = ""
raw_stdout = stdout.encode("utf-8") if isinstance(stdout, str) else b""
raw_stderr = stderr.encode("utf-8") if isinstance(stderr, str) else b""
raw_projection = projection_raw.encode("utf-8")
projection_hash = hashlib.sha256(raw_projection).hexdigest()
projection_raw_hashes.append(projection_hash)
if set(invocation) != invocation_keys \
or invocation.get("ordinal") != ordinal \
or invocation.get("exitCode") != 0 \
or invocation.get("signal") is not None \
or invocation.get("spawnError") is not None \
or invocation.get("stdoutBytes") != len(raw_stdout) \
or invocation.get("stdoutHash") != hashlib.sha256(raw_stdout).hexdigest() \
or invocation.get("stderrBytes") != len(raw_stderr) \
or invocation.get("stderrHash") != hashlib.sha256(raw_stderr).hexdigest() \
or stderr != "" \
or invocation.get("projectionRawBytes") != len(raw_projection) \
or invocation.get("projectionRawHash") != projection_hash \
or set(output) != {"ok", "projection", "diagnostics"} \
or output.get("ok") is not True \
or output.get("projection") != perception_audit.get("projection") \
or output.get("diagnostics") != invocation.get("diagnostics"):
errors.append(
f"#/postFirstPerceptionAuditRef/helper/invocations/{ordinal - 1}: "
"执行状态、原始输出或解析结果不一致",
)
if len(projection_raw_hashes) != 2 or projection_raw_hashes[0] != projection_raw_hashes[1]:
errors.append("#/postFirstPerceptionAuditRef/helper/invocations: 两次 Projection 原始字节不一致")
first_diagnostics = first_invocation.get("diagnostics") or {}
second_diagnostics = second_invocation.get("diagnostics") or {}
first_config = (first_diagnostics.get("config") or {}).get("selectionOverlay")
second_config = (second_diagnostics.get("config") or {}).get("selectionOverlay")
first_result = first_diagnostics.get("selectionOverlay")
second_result = second_diagnostics.get("selectionOverlay")
if len(audit_invocations) != 2 or first_invocation.get("ordinal") != 1 \
or second_invocation.get("ordinal") != 2 \
or first_config != second_config or first_result != second_result:
errors.append("#/postFirstPerceptionAuditRef/helper/invocations: 双跑配置与归一化结果必须一致")
first_config = first_config if isinstance(first_config, dict) else {}
first_result = first_result if isinstance(first_result, dict) else {}
perception_config = first_diagnostics.get("config") or {}
perception_config_hash = _canonical_domain_hash("match3-perception-config/1", perception_config)
overlay_config_hash = _canonical_domain_hash("match3-selection-overlay-config/1", first_config or {})
if overlay_config_hash != _SELECTION_OVERLAY_CONFIG_HASH:
errors.append(
"#/postFirstPerceptionAuditRef/helper/diagnostics/config/selectionOverlay: "
"必须逐字使用 canonical overlay config",
)
initial_classifier = initial_projection.get("classifier") or {}
post_classifier = post_projection.get("classifier") or {}
if initial_classifier != post_classifier \
or post_classifier.get("id") != "deterministic-lab-cell-cluster" \
or post_classifier.get("version") != "1.1.0" \
or post_classifier.get("configHash") != perception_config_hash:
errors.append("#/postFirstPerceptionAuditRef/helper/diagnostics/config: classifier/configHash 不可比较")
support_hash = first_result.get("supportMaskHash") if isinstance(first_result, dict) else None
if isinstance(first_result, dict) and first_result.get("status") == "normalized":
side = (first_config or {}).get("cellSizePx")
spans = (first_config or {}).get("supportSpans")
mask = bytearray(side * side) if isinstance(side, int) and 0 < side <= 256 else None
try:
if mask is None or not isinstance(spans, list) or len(spans) != side:
raise ValueError("support mask shape")
for row, intervals in enumerate(spans):
for start, end in intervals:
if not (isinstance(start, int) and isinstance(end, int)
and 0 <= start <= end < side):
raise ValueError("support span")
mask[row * side + start:row * side + end + 1] = b"\x01" * (end - start + 1)
if hashlib.sha256(mask).hexdigest() != support_hash \
or sum(mask) != first_result.get("changedPixelCount"):
raise ValueError("support hash")
except (TypeError, ValueError):
errors.append("#/postFirstPerceptionAuditRef/helper/diagnostics/selectionOverlay/supportMaskHash: 复算失败")
# bundle 门不信 audit 自报的 normalized/not-needed直接从两张封存 PNG 复算像素事实。
try:
initial_width, initial_height, initial_rgb = _decode_png_rgb(
frame_bytes["initialFrameFileRef"],
)
post_width, post_height, post_rgb = _decode_png_rgb(
frame_bytes["postFirstFrameFileRef"],
)
if (initial_width, initial_height) != (post_width, post_height):
raise ValueError("frame dimensions")
selected_cell = (pair_resolution.get("firstEndpoint") or {}).get("cell") or {}
lattice = next(
(item for item in post_targets.get("targets") or []
if item.get("id") == post_projection.get("latticeId")),
{},
)
row_centers = lattice.get("rowCenters") or []
column_centers = lattice.get("columnCenters") or []
row = selected_cell.get("row")
column = selected_cell.get("column")
side = (first_config or {}).get("cellSizePx")
if not isinstance(row, int) or not isinstance(column, int) or not isinstance(side, int):
raise ValueError("selected cell")
center_y = row_centers[row - 1]
center_x = column_centers[column - 1]
top, left = center_y - side // 2, center_x - side // 2
expected_pixels = set()
for relative_row, intervals in enumerate((first_config or {}).get("supportSpans") or []):
for start, end in intervals:
expected_pixels.update(
(top + relative_row, left + relative_column)
for relative_column in range(start, end + 1)
)
changed_pixels = set()
maximum_alpha_spread = 0.0
maximum_replay_residual = 0
opaque_pixel_count = 0
for y in range(initial_height):
for x in range(initial_width):
offset = (y * initial_width + x) * 3
before = tuple(initial_rgb[offset:offset + 3])
after = tuple(post_rgb[offset:offset + 3])
if before == after:
continue
changed_pixels.add((y, x))
if any(current < previous for previous, current in zip(before, after)):
raise ValueError("channel decreased")
alphas = [
(current - previous) * 1000.0 / (255 - previous)
for previous, current in zip(before, after)
if previous <= (first_config or {}).get("maximumReferenceChannelForAlpha", -1)
]
if not alphas:
raise ValueError("alpha unresolved")
maximum_alpha_spread = max(maximum_alpha_spread, max(alphas) - min(alphas))
alpha = sum(alphas) / len(alphas)
if alpha >= (first_config or {}).get("opaqueAlphaThresholdPermille", 1001):
opaque_pixel_count += 1
replay = tuple(round(previous + (alpha / 1000.0) * (255 - previous)) for previous in before)
maximum_replay_residual = max(
maximum_replay_residual,
max(abs(replayed - current) for replayed, current in zip(replay, after)),
)
outside_count = len(changed_pixels - expected_pixels)
opaque_pixel_permille = (
opaque_pixel_count * 1000 // max(1, len(changed_pixels))
)
if not changed_pixels:
if (first_result or {}).get("status") != "not-needed" \
or (first_result or {}).get("changedPixelCount") != 0 \
or (first_result or {}).get("outsideSelectedCellCount") != 0:
raise ValueError("not-needed mismatch")
elif changed_pixels != expected_pixels \
or (first_result or {}).get("status") != "normalized" \
or (first_result or {}).get("changedPixelCount") != len(changed_pixels) \
or (first_result or {}).get("outsideSelectedCellCount") != outside_count \
or (first_result or {}).get("maximumAlphaSpreadPermille") != math.ceil(maximum_alpha_spread) \
or (first_result or {}).get("maximumReplayResidual") != maximum_replay_residual \
or (first_result or {}).get("opaquePixelPermille") != opaque_pixel_permille \
or opaque_pixel_permille < (first_config or {}).get("minimumOpaquePixelPermille", 1001):
raise ValueError("normalized pixel mismatch")
except (IndexError, KeyError, TypeError, ValueError, zlib.error, struct.error):
errors.append(
"#/postFirstPerceptionAuditRef/helper/diagnostics/selectionOverlay: "
"与 initial/post-first PNG 独立像素复算不一致",
)
expected_normalization = {
"schemaVersion": "OverlayNormalizationEvidence/1",
"auditRef": manifest.get("postFirstPerceptionAuditRef"),
"auditHash": raw_hashes.get("postFirstPerceptionAuditRef"),
"auditSchemaVersion": perception_audit.get("schemaVersion"),
"overlayReferenceFrameRef": initial_targets.get("sourceFrameRef"),
"overlayReferenceFrameHash": initial_targets.get("sourceFrameHash"),
"postFirstFrameRef": post_targets.get("sourceFrameRef"),
"postFirstFrameHash": post_targets.get("sourceFrameHash"),
"selectedCell": (pair_resolution.get("firstEndpoint") or {}).get("cell"),
"perceptionConfigHash": perception_config_hash,
"selectionOverlayConfigHash": overlay_config_hash,
"invocationCount": 2,
"deterministic": True,
"status": (first_result or {}).get("status"),
"changedPixelCount": (first_result or {}).get("changedPixelCount"),
"outsideSelectedCellCount": (first_result or {}).get("outsideSelectedCellCount"),
"supportMaskHash": (first_result or {}).get("supportMaskHash"),
"maximumAlphaSpreadPermille": (first_result or {}).get("maximumAlphaSpreadPermille"),
"maximumReplayResidual": (first_result or {}).get("maximumReplayResidual"),
"reasonCodes": (first_result or {}).get("reasonCodes"),
"resultHash": _canonical_domain_hash("match3-selection-overlay-result/1", first_result or {}),
}
if normalization != expected_normalization:
errors.append("#/boardIdentityCheckRef/overlayNormalizationEvidence: 必须从原始 audit 独立重建")
if manifest.get("rollAuditHash") != raw_hashes.get("rollAuditRef"):
errors.append("#/rollAuditHash: 与 roll audit 封存字节不一致")
audit_groups = roll_audit.get("groups") or []
audit_last = audit_groups[-1] if audit_groups else {}
audit_group_path = (
paths["rollAuditRef"].parent / str(audit_last.get("ref") or "")
).resolve()
if audit_group_path != paths["actionGroupRef"] \
or audit_last.get("hash") != raw_hashes["actionGroupRef"] \
or audit_last.get("actionGroupId") != group.get("actionGroupId"):
errors.append("#/rollAuditRef/groups: 最终 group 必须绑定 bundle actionGroup 原始文件")
if attempt.get("actorViewRef") != manifest.get("actorViewRef") \
or attempt.get("actorViewHash") != raw_hashes["actorViewRef"]:
errors.append("#/selectionAttemptRef/actorViewHash: 未绑定 ActorView 原始文件")
if attempt.get("selectionRef") != manifest.get("selectionRef") \
or attempt.get("selectionHash") != raw_hashes["selectionRef"] \
or attempt.get("selection") != selection or attempt.get("accepted") is not True:
errors.append("#/selectionAttemptRef: 必须绑定唯一 accepted Selection")
if pair_resolution.get("selectionRef") != manifest.get("selectionRef") \
or pair_resolution.get("selectionHash") != raw_hashes["selectionRef"]:
errors.append("#/pairResolutionRef/selectionHash: 未绑定 Selection")
if pair_resolution.get("swapSetRef") != manifest.get("swapSetRef") \
or pair_resolution.get("swapSetHash") != raw_hashes["swapSetRef"]:
errors.append("#/pairResolutionRef/swapSetHash: 未绑定 SwapSet")
candidate = next((row for row in swap_set.get("candidates") or []
if row.get("candidateId") == selection.get("candidateId")), None)
if candidate is None:
errors.append("#/selectionRef/candidateId: 未命中 sealed SwapSet")
else:
expected_pair = candidate.get("pair")
for field, expected in (
("candidateSetHash", swap_set.get("candidateSetHash")),
("candidateId", candidate.get("candidateId")), ("latticeId", swap_set.get("latticeId")),
("pair", expected_pair), ("firstEndpoint", selection.get("firstEndpoint")),
):
if pair_resolution.get(field) != expected:
errors.append(f"#/pairResolutionRef/{field}: 必须由 Selection + SwapSet 确定性解析")
identity = ("interactionProfileId", "interactionRegistryVersion", "interactionBindingHash")
for holder_name, holder in (
("swapSetRef", swap_set), ("initialProjectionRef", initial_projection),
("postFirstProjectionRef", post_projection), ("pairResolutionRef", pair_resolution),
("boardIdentityCheckRef", board),
("actionGroupRef", group),
):
for field in identity:
if holder.get(field) != binding.get(field):
errors.append(f"#/{holder_name}/{field}: 必须镜像 request binding")
def target_lattice(target_set: dict, lattice_id: str):
return next((row for row in target_set.get("targets") or [] if row.get("id") == lattice_id), None)
for side_name, target_set, projection, target_ref_field, projection_ref_field in (
("initial", initial_targets, initial_projection, "initialTargetSetRef", "initialProjectionRef"),
("postFirst", post_targets, post_projection, "postFirstTargetSetRef", "postFirstProjectionRef"),
):
side = board.get(side_name) or {}
bundle_check(
f"#/{projection_ref_field}/sourceFrameRef", projection.get("sourceFrameRef"),
target_set.get("sourceFrameRef"), "board-causality", f"{side_name}-projection-source-ref",
)
bundle_check(
f"#/{projection_ref_field}/sourceFrameHash", projection.get("sourceFrameHash"),
target_set.get("sourceFrameHash"), "board-causality", f"{side_name}-projection-source-hash",
)
bundle_check(
f"#/{projection_ref_field}/targetSetRef", projection.get("targetSetRef"),
manifest.get(target_ref_field), "board-causality", f"{side_name}-projection-target-ref",
)
bundle_check(
f"#/{projection_ref_field}/targetSetHash", projection.get("targetSetHash"),
target_set.get("targetSetHash"), "board-causality", f"{side_name}-projection-target-hash",
)
lattice = target_lattice(target_set, projection.get("latticeId"))
if lattice is None:
errors.append(f"#/boardIdentityCheckRef/{side_name}/lattice: TargetSet 不含 projection lattice")
continue
expected_geometry = {
"latticeId": lattice.get("id"), "dimensions": lattice.get("dimensions"),
"rowCenters": lattice.get("rowCenters"), "columnCenters": lattice.get("columnCenters"),
}
expected_values = {
"sourceFrameRef": target_set.get("sourceFrameRef"),
"sourceFrameHash": target_set.get("sourceFrameHash"),
"targetSetRef": manifest.get(target_ref_field),
"targetSetHash": target_set.get("targetSetHash"),
"projectionRef": manifest.get(projection_ref_field),
"projectionHash": raw_hashes[projection_ref_field],
"lattice": expected_geometry,
"equivalenceMatrixHash": _projection_equivalence_hash(projection),
}
for field, expected in expected_values.items():
if side.get(field) != expected:
errors.append(f"#/boardIdentityCheckRef/{side_name}/{field}: 必须由 TargetSet/Projection 重算")
first_pre = first_action.get("preFrameRef") or {}
first_post = first_action.get("postFrameRef") or {}
second_pre = second_action.get("preFrameRef") or {}
second_post = second_action.get("postFrameRef") or {}
for error_path, actual, expected, check in (
("#/initialTargetSetRef/sourceFrameRef", initial_targets.get("sourceFrameRef"), first_pre.get("path"), "initial-target-action-ref"),
("#/initialTargetSetRef/sourceFrameHash", initial_targets.get("sourceFrameHash"), first_pre.get("hash"), "initial-target-action-hash"),
("#/postFirstTargetSetRef/sourceFrameRef", post_targets.get("sourceFrameRef"), first_post.get("path"), "post-target-first-action-ref"),
("#/postFirstTargetSetRef/sourceFrameHash", post_targets.get("sourceFrameHash"), first_post.get("hash"), "post-target-first-action-hash"),
("#/secondActionRef/preFrameRef/path", second_pre.get("path"), first_post.get("path"), "second-pre-first-post-ref"),
("#/secondActionRef/preFrameRef/hash", second_pre.get("hash"), first_post.get("hash"), "second-pre-first-post-hash"),
("#/initialFrameHash", manifest.get("initialFrameHash"), first_pre.get("hash"), "initial-frame-bytes-action-hash"),
("#/postFirstFrameHash", manifest.get("postFirstFrameHash"), first_post.get("hash"), "post-frame-bytes-action-hash"),
("#/secondPostFrameHash", manifest.get("secondPostFrameHash"), second_post.get("hash"), "second-post-frame-bytes-action-hash"),
("#/secondPostFrameRef", manifest.get("secondPostFrameRef"), second_post.get("path"), "second-post-logical-ref"),
):
bundle_check(error_path, actual, expected, "frame-causality", check)
for field, expected in (
("sourceFrameRef", initial_targets.get("sourceFrameRef")),
("sourceFrameHash", initial_targets.get("sourceFrameHash")),
("targetSetRef", manifest.get("initialTargetSetRef")),
("targetSetHash", initial_targets.get("targetSetHash")),
("projectionRef", manifest.get("initialProjectionRef")),
("projectionHash", initial_projection.get("projectionHash")),
):
bundle_check(f"#/swapSetRef/{field}", swap_set.get(field), expected,
"board-causality", f"swap-{field}")
if board.get("firstActionRef") != manifest.get("firstActionRef") \
or board.get("firstActionHash") != raw_hashes["firstActionRef"]:
errors.append("#/boardIdentityCheckRef/firstActionHash: 未绑定第一原生 action")
if board.get("firstEndpoint") != pair_resolution.get("firstEndpoint"):
errors.append("#/boardIdentityCheckRef/firstEndpoint: 必须镜像 pair resolution")
for grouped_name, grouped, action_name, action, role, endpoint in (
("firstGroupedActionRef", first_grouped, "firstActionRef", first_action, "first", pair_resolution.get("firstEndpoint")),
("secondGroupedActionRef", second_grouped, "secondActionRef", second_action, "second", pair_resolution.get("secondEndpoint")),
):
if grouped.get("actionGroupId") != group.get("actionGroupId") or grouped.get("role") != role:
errors.append(f"#/{grouped_name}: actionGroupId/role 不一致")
if grouped.get("actionId") != action.get("actionId") or grouped.get("endpoint") != endpoint:
errors.append(f"#/{grouped_name}: 未绑定对应原生 action 与 endpoint")
if grouped.get("actionRef") != manifest.get(action_name) \
or grouped.get("actionHash") != raw_hashes[action_name]:
errors.append(f"#/{grouped_name}/actionHash: 未绑定原生 action 文件")
if int(re.sub(r"\D", "", second_action.get("actionId") or "0")) \
!= int(re.sub(r"\D", "", first_action.get("actionId") or "0")) + 1:
errors.append("#/secondActionRef/actionId: 双 tap 原生 action 必须连续")
if first_action.get("postFrameRef") != second_action.get("preFrameRef"):
errors.append("#/secondActionRef/preFrameRef: 必须承接第一 tap post frame")
if first_action.get("postEventSeq") != second_action.get("preEventSeq"):
errors.append("#/secondActionRef/preEventSeq: 必须承接第一 tap postEventSeq")
if second_action.get("postFrameRef", {}).get("path") != manifest.get("secondPostFrameRef"):
errors.append("#/secondPostFrameRef: 必须等于第二 tap post frame")
for action, endpoint, target_set, label in (
(first_action, pair_resolution.get("firstEndpoint"), initial_targets, "firstActionRef"),
(second_action, pair_resolution.get("secondEndpoint"), post_targets, "secondActionRef"),
):
lattice = target_lattice(target_set, endpoint.get("id")) or {}
cell = endpoint.get("cell") or {}
expected_xy = {
"type": "tap",
"x": (lattice.get("columnCenters") or [])[cell.get("column", 1) - 1],
"y": (lattice.get("rowCenters") or [])[cell.get("row", 1) - 1],
}
if action.get("normalized") != expected_xy:
errors.append(f"#/{label}/normalized: 必须由对应时刻 TargetSet lattice center 解析")
group_expected = {
"selectionAttemptRef": manifest.get("selectionAttemptRef"),
"selectionAttemptHash": raw_hashes["selectionAttemptRef"],
"pairResolutionRef": manifest.get("pairResolutionRef"),
"pairResolutionHash": raw_hashes["pairResolutionRef"],
"candidateSetHash": swap_set.get("candidateSetHash"),
"candidateId": pair_resolution.get("candidateId"),
"initialSourceFrameHash": swap_set.get("sourceFrameHash"),
"initialTargetSetHash": swap_set.get("targetSetHash"),
"latticeId": pair_resolution.get("latticeId"), "pair": pair_resolution.get("pair"),
"firstEndpoint": pair_resolution.get("firstEndpoint"),
"secondEndpoint": pair_resolution.get("secondEndpoint"),
"firstActionId": first_action.get("actionId"), "secondActionId": second_action.get("actionId"),
"firstGroupedActionRef": manifest.get("firstGroupedActionRef"),
"firstGroupedActionHash": raw_hashes["firstGroupedActionRef"],
"secondGroupedActionRef": manifest.get("secondGroupedActionRef"),
"secondGroupedActionHash": raw_hashes["secondGroupedActionRef"],
"boardIdentityCheckRef": manifest.get("boardIdentityCheckRef"),
"boardIdentityCheckHash": raw_hashes["boardIdentityCheckRef"],
}
for field, expected in group_expected.items():
if group.get(field) != expected:
errors.append(f"#/actionGroupRef/{field}: 未闭合 runner 审计引用")
if board.get("result") != "equivalent":
errors.append("#/boardIdentityCheckRef/result: 只有 equivalent 才能派发第二 tap")
if group.get("state") != "completed":
errors.append("#/actionGroupRef/state: 只有 completed 才能形成玩法证明")
if isinstance(events, list) and len(events) >= 2:
event_seqs = [event.get("seq") for event in events]
expected_slice = list(range(first_action.get("preEventSeq", 0) + 1, second_action.get("postEventSeq", 0) + 1))
if event_seqs != expected_slice:
errors.append("#/eventsRef/seq: 必须完整覆盖 firstAction.preEventSeq 到 secondAction.postEventSeq 的事件区间")
first_types = [
event.get("type") for event in events
if first_action.get("preEventSeq", 0) < event.get("seq", 0) <= first_action.get("postEventSeq", 0)
]
if any(event_type in ("puzzle.swap-committed", "puzzle.match-formed") for event_type in first_types):
errors.append("#/eventsRef: 第一 tap 区间不得提前产生 swap-committed/match-formed")
proof_events = [
event for event in events
if event.get("actionId") == second_action.get("actionId")
and event.get("type") in ("puzzle.swap-committed", "puzzle.match-formed")
]
if len(proof_events) != 2:
errors.append("#/eventsRef: second action 必须且只能产生一条 swap-committed 与一条 match-formed")
return errors
swap_event, match_event = proof_events
for index, event in enumerate((swap_event, match_event)):
if event.get("evidenceMode") != "native":
errors.append(f"#/eventsRef/{index}/evidenceMode: Match-3 proof 事件必须为 native")
if event.get("eventRole") != "profile-proof":
errors.append(f"#/eventsRef/{index}/eventRole: Match-3 proof 事件必须为 profile-proof")
if [swap_event.get("type"), match_event.get("type")] != [
"puzzle.swap-committed", "puzzle.match-formed",
]:
errors.append("#/eventsRef: 事件必须按 swap-committed→match-formed 排序")
swap_index = events.index(swap_event)
match_index = events.index(match_event)
if swap_index + 1 != match_index or swap_event.get("seq", 0) + 1 != match_event.get("seq"):
errors.append("#/eventsRef: 两条 proof 事件必须在完整事件流中相邻")
if swap_event.get("actionId") != second_action.get("actionId") \
or match_event.get("actionId") != second_action.get("actionId"):
errors.append("#/eventsRef/actionId: 两事件必须属于 secondActionId")
second_pre_seq = second_action.get("preEventSeq", 0)
second_post_seq = second_action.get("postEventSeq", 0)
target_event_seqs = [swap_event.get("seq"), match_event.get("seq")]
if not all(
isinstance(seq, int) and second_pre_seq < seq <= second_post_seq
for seq in target_event_seqs
):
errors.append("#/eventsRef/seq: 两事件必须位于 secondAction 的 (preEventSeq, postEventSeq] 区间")
if swap_event.get("virtualTimeMs") != match_event.get("virtualTimeMs"):
errors.append("#/eventsRef/virtualTimeMs: 两事件必须来自同一同步栈")
swap_payload, match_payload = swap_event.get("payload") or {}, match_event.get("payload") or {}
if swap_payload.get("moveId") != match_payload.get("moveId"):
errors.append("#/eventsRef/moveId: 两事件 moveId 必须一致")
if swap_payload.get("pair") != group.get("pair"):
errors.append("#/eventsRef/0/payload/pair: 必须等于 actionGroup canonical pair")
run_cells = [cell for run in match_payload.get("runs") or [] for cell in run.get("cells") or []]
if not any(cell in group.get("pair", []) for cell in run_cells):
errors.append("#/eventsRef/1/payload/runs: 至少一个 run 必须包含交换端点")
expected_proof = {
"id": "puzzle.match3-valid-swap", "required": True, "status": "satisfied",
"sequenceRefs": [
{"stepId": "swap-committed", "eventRef": swap_event.get("seq"),
"actionRef": second_action.get("actionId"), "postFrameRef": manifest.get("secondPostFrameRef")},
{"stepId": "match-formed", "eventRef": match_event.get("seq"),
"actionRef": second_action.get("actionId"), "postFrameRef": manifest.get("secondPostFrameRef")},
],
"actionRefs": [second_action.get("actionId")],
"postFrameRefs": [manifest.get("secondPostFrameRef")],
"eventRefs": [swap_event.get("seq"), match_event.get("seq")],
"contradictions": [], "blockingProblems": [],
}
proof_registry = _load_proof_registry(request.get("proofRegistryVersion"))
proof_profile = (proof_registry.get("profiles") or {}).get(request.get("proofProfileId")) or {}
obligation_by_id = {row.get("id"): row for row in proof_profile.get("obligations") or []}
required_ids = resolution.get("requiredObligationIds") or []
if "puzzle.match3-valid-swap" not in required_ids:
errors.append("#/resolutionRef/requiredObligationIds: 必须提升 puzzle.match3-valid-swap")
expected_candidates = []
for obligation_id in required_ids:
canonical = obligation_by_id.get(obligation_id) or {}
complete = obligation_id == "puzzle.match3-valid-swap"
sequence_refs = deepcopy(expected_proof["sequenceRefs"]) if complete else []
action_refs = deepcopy(expected_proof["actionRefs"]) if complete else []
post_refs = deepcopy(expected_proof["postFrameRefs"]) if complete else []
event_refs = deepcopy(expected_proof["eventRefs"]) if complete else []
steps = ((canonical.get("evidence") or {}).get("sequence") or [])
expected_candidates.append({
"id": obligation_id,
"title": canonical.get("title", ""),
"description": canonical.get("description", ""),
"required": True,
"evidence": deepcopy(canonical.get("evidence") or {}),
"candidateOnly": True,
"candidateState": "complete" if complete else "missing",
"candidateProblems": [],
"sequenceRefs": sequence_refs,
"actionRefs": action_refs,
"postFrameRefs": post_refs,
"eventRefs": event_refs,
"evidenceRefs": action_refs + post_refs + [f"event:{seq}" for seq in event_refs],
"seenStepTitles": [step.get("title") for step in steps] if complete else [],
"missingStepTitles": [] if complete else [step.get("title") for step in steps],
})
expected_proof_obligations = [_proof_obligation_from_candidate(row) for row in expected_candidates]
request_hash = _canonical_domain_hash("acceptance-request/2", request)
brief_hash = hashlib.sha256(brief_bytes).hexdigest()
for holder_name, holder in (
("collectionProofRef", collection), ("singleRollProofRef", proof), ("judgePackageRef", judge_package),
):
for field, expected in (
("gameId", request.get("gameId")), ("genre", request.get("genre")),
("templateRoute", request.get("templateRoute")), ("proofProfileId", request.get("proofProfileId")),
("proofRegistryVersion", request.get("proofRegistryVersion")),
("taskBindingHash", request.get("taskBindingHash")),
("acceptanceRequestHash", request_hash), ("artifactHash", provenance.get("artifactHash")),
("briefHash", brief_hash),
):
bundle_check(f"#/{holder_name}/{field}", holder.get(field), expected,
"proof-identity", f"{holder_name}-{field}")
resolution_raw_hash = raw_hashes["resolutionRef"]
collection_raw_hash = raw_hashes["collectionProofRef"]
judge_raw_hash = raw_hashes["judgePackageRef"]
for field, expected in (
("proofProfileId", request.get("proofProfileId")),
("proofRegistryVersion", request.get("proofRegistryVersion")),
("taskBindingHash", request.get("taskBindingHash")),
("acceptanceRequestHash", request_hash),
("interactionBindingRef", manifest.get("bindingRef")),
("interactionBindingFileHash", raw_hashes["bindingRef"]),
("interactionBindingHash", binding.get("interactionBindingHash")),
):
bundle_check(f"#/resolutionRef/{field}", resolution.get(field), expected,
"resolution", field)
try:
brief_text = brief_bytes.decode("utf-8", errors="strict")
except UnicodeDecodeError:
errors.append("#/briefRef: brief 必须是严格 UTF-8")
brief_text = ""
bundle_check("#/judgePackageRef/brief", judge_package.get("brief"), brief_text,
"judge-projection", "brief-bytes")
bundle_check("#/singleRollProofRef/proofObligationResolutionRef",
proof.get("proofObligationResolutionRef"), manifest.get("resolutionRef"),
"proof-hash", "resolution-ref")
bundle_check("#/singleRollProofRef/proofObligationResolutionHash",
proof.get("proofObligationResolutionHash"), resolution_raw_hash,
"proof-hash", "resolution-bytes")
bundle_check("#/singleRollProofRef/collectionProofRef", proof.get("collectionProofRef"),
manifest.get("collectionProofRef"), "proof-hash", "collection-ref")
bundle_check("#/singleRollProofRef/collectionProofHash", proof.get("collectionProofHash"),
collection_raw_hash, "proof-hash", "collection-bytes")
bundle_check("#/singleRollProofRef/judgePackageRef", proof.get("judgePackageRef"),
manifest.get("judgePackageRef"), "proof-hash", "judge-ref")
bundle_check("#/singleRollProofRef/judgePackageHash", proof.get("judgePackageHash"),
judge_raw_hash, "proof-hash", "judge-bytes")
bundle_check("#/singleRollProofRef/judge/packageHash", (proof.get("judge") or {}).get("packageHash"),
judge_raw_hash, "proof-hash", "judge-consensus-package")
bundle_check("#/judgePackageRef/proofRef", judge_package.get("proofRef"),
manifest.get("collectionProofRef"), "judge-projection", "collection-ref")
bundle_check("#/judgePackageRef/proofHash", judge_package.get("proofHash"),
collection_raw_hash, "judge-projection", "collection-bytes")
expected_actions = [first_action, second_action]
expected_proof_actions = [_proof_action_projection(action) for action in expected_actions]
expected_judge_actions = [_judge_action_projection(action, events) for action in expected_actions]
bundle_check("#/collectionProofRef/actions", collection.get("actions"), expected_actions,
"collection", "native-actions")
bundle_check("#/collectionProofRef/events", collection.get("events"), events,
"collection", "native-events")
bundle_check("#/collectionProofRef/obligationCandidates", collection.get("obligationCandidates"),
expected_candidates, "collection", "obligation-candidates")
bundle_check("#/collectionProofRef/provenance/requestHash",
(collection.get("provenance") or {}).get("requestHash"), request_hash,
"collection", "request-hash")
bundle_check("#/collectionProofRef/provenance/manifestHash",
(collection.get("provenance") or {}).get("manifestHash"),
raw_hashes["actionGroupRef"], "collection", "action-group-manifest-hash")
bundle_check("#/singleRollProofRef/actions", proof.get("actions"), expected_proof_actions,
"proof-projection", "proof-actions")
bundle_check("#/singleRollProofRef/events", proof.get("events"), proof_events,
"proof-projection", "proof-events")
bundle_check("#/singleRollProofRef/proofObligations", proof.get("proofObligations"),
expected_proof_obligations, "proof-projection", "proof-obligations")
bundle_check("#/judgePackageRef/proofObligations", judge_package.get("proofObligations"),
expected_candidates, "judge-projection", "obligation-candidates")
bundle_check("#/judgePackageRef/actions", judge_package.get("actions"), expected_judge_actions,
"judge-projection", "actions")
bundle_check("#/judgePackageRef/gameEvents", judge_package.get("gameEvents"), events,
"judge-projection", "events")
expected_frames = [
{"actionId": first_action.get("actionId"), "kind": "pre", "ref": first_pre.get("path"),
"hash": first_pre.get("hash"), "artifactHash": provenance.get("artifactHash")},
{"actionId": first_action.get("actionId"), "kind": "post", "ref": first_post.get("path"),
"hash": first_post.get("hash"), "artifactHash": provenance.get("artifactHash")},
{"actionId": second_action.get("actionId"), "kind": "pre", "ref": second_pre.get("path"),
"hash": second_pre.get("hash"), "artifactHash": provenance.get("artifactHash")},
{"actionId": second_action.get("actionId"), "kind": "post", "ref": second_post.get("path"),
"hash": second_post.get("hash"), "artifactHash": provenance.get("artifactHash")},
]
bundle_check("#/collectionProofRef/frames", collection.get("frames"), expected_frames,
"collection", "sealed-frames")
bundle_check("#/singleRollProofRef/frames", proof.get("frames"), expected_frames,
"proof-projection", "sealed-frames")
bundle_check("#/judgePackageRef/frames", judge_package.get("frames"), expected_frames,
"judge-projection", "sealed-frames")
expected_scope = {
"actionRefs": [first_action.get("actionId"), second_action.get("actionId")],
"frameRefs": list(dict.fromkeys(row["ref"] for row in expected_frames)),
"eventRefs": [f"event:{event.get('seq')}" for event in events],
}
bundle_check("#/judgePackageRef/textReferenceScope", judge_package.get("textReferenceScope"),
expected_scope, "judge-projection", "text-reference-scope")
return errors
def _validate_match3_roll_audit(audit_path: Path) -> list:
"""回读 roll audit 引用的 group/reset禁止只靠自报摘要通过恢复预算门。"""
audit = _load_sample_instance(audit_path)
errors = _schema_errors("match3-roll-interaction-audit.schema.json", audit)
_trace("roll-audit", "manifest-schema", path=audit_path,
result="pass" if not errors else "fail", error=errors[0] if errors else None)
if errors:
return errors
def trace_sealed(stage: str, check: str, path: Path, declared: str, value) -> str:
"""记录 roll audit 引用工件的原始字节身份,不写业务载荷。"""
computed = _raw_sample_hash(path, value)
ok = declared == computed
_trace(stage, check, path=path, declared_hash=declared, computed_hash=computed,
result="pass" if ok else "fail", error=None if ok else "封存字节 hash 漂移")
return computed
recovery = (((_load(_HERE / "interaction-profiles.v2.json").get("profiles") or {})
.get(audit.get("interactionProfileId")) or {}).get("recoveryPolicy") or {})
expected_budget = recovery.get("maxActionGroupsPerRoll", 0) * 2 + recovery.get("maxResetPerRoll", 0)
_trace("roll-audit", "recovery-budget", declared_hash=None, computed_hash=None,
result="pass" if audit.get("initialRemainingBudget") == expected_budget else "fail",
error=None if audit.get("initialRemainingBudget") == expected_budget else "initialRemainingBudget 非配置推导值")
preflight_path = (audit_path.parent / audit.get("producerPreflightRef", "")).resolve()
if preflight_path.is_symlink() or not preflight_path.is_file():
errors.append("#/producerPreflightRef: 引用 preflight 文件不存在或不是普通文件")
_trace("roll-preflight", "regular-file", ref=audit.get("producerPreflightRef"),
path=preflight_path, result="fail", error="preflight 文件不存在或不是普通文件")
else:
preflight = _load_sample_instance(preflight_path)
preflight_errors = _schema_errors("match3-producer-preflight.schema.json", preflight)
errors += [f"#/producerPreflightRef{error[1:]}" for error in preflight_errors]
_trace("roll-preflight", "schema-and-semantic", ref=audit.get("producerPreflightRef"),
path=preflight_path, result="pass" if not preflight_errors else "fail",
error=preflight_errors[0] if preflight_errors else None)
computed_preflight_hash = trace_sealed(
"roll-preflight", "sealed-bytes", preflight_path,
audit.get("producerPreflightHash"), preflight,
)
if audit.get("producerPreflightHash") != computed_preflight_hash:
errors.append("#/producerPreflightHash: 与 preflight 原始字节不一致")
if preflight.get("result") != "compatible" or preflight.get("reasonCode") != "none":
errors.append("#/producerPreflightRef/result: 正式 roll 只能绑定 compatible/none preflight")
preflight_identity = {
"gameId": "gameId",
"artifactHash": "artifactHash",
"taskBindingHash": "taskBindingHash",
"acceptanceRequestHash": "acceptanceRequestHash",
"seed": "seed",
"interactionProfileId": "interactionProfileId",
"interactionRegistryVersion": "interactionRegistryVersion",
"interactionBindingHash": "interactionBindingHash",
}
for audit_field, preflight_field in preflight_identity.items():
if audit.get(audit_field) != preflight.get(preflight_field):
errors.append(
f"#/{audit_field}: 必须镜像 compatible preflight 的 {preflight_field}",
)
binding_path = (audit_path.parent / audit.get("interactionBindingRef", "")).resolve()
if binding_path.is_symlink() or not binding_path.is_file():
errors.append("#/interactionBindingRef: 引用 binding 文件不存在或不是普通文件")
_trace("roll-binding", "regular-file", ref=audit.get("interactionBindingRef"),
path=binding_path, result="fail", error="binding 文件不存在或不是普通文件")
else:
binding = _load_sample_instance(binding_path)
binding_errors = _schema_errors("interaction-binding.schema.json", binding)
errors += [f"#/interactionBindingRef{error[1:]}" for error in binding_errors]
_trace("roll-binding", "schema-and-semantic", ref=audit.get("interactionBindingRef"),
path=binding_path, result="pass" if not binding_errors else "fail",
error=binding_errors[0] if binding_errors else None)
computed_binding_hash = trace_sealed(
"roll-binding", "sealed-bytes", binding_path,
audit.get("interactionBindingFileHash"), binding,
)
if audit.get("interactionBindingFileHash") != computed_binding_hash:
errors.append("#/interactionBindingFileHash: 与 binding 原始字节不一致")
for field in ("interactionProfileId", "interactionRegistryVersion", "interactionBindingHash"):
if audit.get(field) != binding.get(field):
errors.append(f"#/{field}: 必须镜像真实 binding 文件")
if audit.get("taskBindingHash") != binding.get("taskBindingHash"):
errors.append("#/taskBindingHash: 必须镜像真实 binding 文件")
identity = {
"interactionProfileId": audit.get("interactionProfileId"),
"interactionRegistryVersion": audit.get("interactionRegistryVersion"),
"interactionBindingHash": audit.get("interactionBindingHash"),
}
groups = []
pair_resolutions = []
for index, group_ref in enumerate(audit.get("groups") or []):
path = (audit_path.parent / group_ref.get("ref", "")).resolve()
if not path.is_file():
errors.append(f"#/groups/{index}/ref: 引用 actionGroup 不存在")
continue
group = _load_sample_instance(path)
groups.append(group)
group_errors = [
f"#/groups/{index}{error[1:]}"
for error in _schema_errors("match3-action-group.schema.json", group)
]
errors += group_errors
_trace("roll-group", "schema-and-semantic", path=path,
result="pass" if not group_errors else "fail",
error=group_errors[0] if group_errors else None)
if group_ref.get("hash") != trace_sealed(
"roll-group", "sealed-bytes", path, group_ref.get("hash"), group,
):
errors.append(f"#/groups/{index}/hash: 与 actionGroup 封存字节不一致")
for field in ("actionGroupId", "candidateSetHash", "candidateId", "state"):
if group_ref.get(field) != group.get(field):
errors.append(f"#/groups/{index}/{field}: 必须镜像 actionGroup")
for field, expected in identity.items():
if group.get(field) != expected:
errors.append(f"#/groups/{index}/{field}: 必须镜像 roll audit 身份")
nested_specs = (
("selectionAttemptRef", "selectionAttemptHash", "actor-selection-attempt.schema.json"),
("pairResolutionRef", "pairResolutionHash", "match3-pair-resolution.schema.json"),
("firstGroupedActionRef", "firstGroupedActionHash", "grouped-action-ref.schema.json"),
("boardIdentityCheckRef", "boardIdentityCheckHash", "board-identity-check.schema.json"),
)
if group_ref.get("secondGroupedActionRef") is not None:
nested_specs += (("secondGroupedActionRef", "secondGroupedActionHash", "grouped-action-ref.schema.json"),)
pair_resolution = None
for ref_field, hash_field, schema_name in nested_specs:
if group_ref.get(ref_field) != group.get(ref_field) or group_ref.get(hash_field) != group.get(hash_field):
errors.append(f"#/groups/{index}/{ref_field}: 必须镜像 actionGroup 完整引用")
nested_path = (audit_path.parent / str(group_ref.get(ref_field) or "")).resolve()
if not nested_path.is_file():
errors.append(f"#/groups/{index}/{ref_field}: 引用工件不存在")
continue
nested = _load_sample_instance(nested_path)
nested_errors = [
f"#/groups/{index}/{ref_field}{error[1:]}"
for error in _schema_errors(schema_name, nested)
]
errors += nested_errors
_trace("roll-group-ref", f"{ref_field}-schema", path=nested_path,
result="pass" if not nested_errors else "fail",
error=nested_errors[0] if nested_errors else None)
if group_ref.get(hash_field) != trace_sealed(
"roll-group-ref", f"{ref_field}-bytes", nested_path,
group_ref.get(hash_field), nested,
):
errors.append(f"#/groups/{index}/{hash_field}: 与封存工件原始字节不一致")
if ref_field == "selectionAttemptRef":
for attempt_ref, attempt_hash, schema in (
(nested.get("actorViewRef"), nested.get("actorViewHash"), "actor-view-v4.schema.json"),
(nested.get("selectionRef"), nested.get("selectionHash"), "actor-selection-v2.schema.json"),
):
attempt_path = (audit_path.parent / str(attempt_ref or "")).resolve()
if not attempt_path.is_file():
errors.append(f"#/groups/{index}/{ref_field}: attempt 子工件不存在")
continue
attempt_value = _load_sample_instance(attempt_path)
attempt_errors = [
f"#/groups/{index}/{ref_field}{error[1:]}"
for error in _schema_errors(schema, attempt_value)
]
errors += attempt_errors
_trace("roll-selection", f"{schema}-schema", path=attempt_path,
result="pass" if not attempt_errors else "fail",
error=attempt_errors[0] if attempt_errors else None)
if attempt_hash != trace_sealed(
"roll-selection", f"{schema}-bytes", attempt_path,
attempt_hash, attempt_value,
):
errors.append(f"#/groups/{index}/{ref_field}: attempt 子工件 hash 不一致")
selection_path = (audit_path.parent / str(nested.get("selectionRef") or "")).resolve()
if selection_path.is_file() and nested.get("selection") != _load_sample_instance(selection_path):
errors.append(f"#/groups/{index}/{ref_field}/selection: 必须镜像 selection 文件")
elif ref_field == "pairResolutionRef":
pair_resolution = nested
for field in ("candidateSetHash", "candidateId", "latticeId", "pair", "firstEndpoint", "secondEndpoint"):
if group.get(field) != nested.get(field):
errors.append(f"#/groups/{index}/{ref_field}/{field}: 必须镜像 actionGroup")
for pair_ref, pair_hash, schema in (
(nested.get("selectionRef"), nested.get("selectionHash"), "actor-selection-v2.schema.json"),
(nested.get("swapSetRef"), nested.get("swapSetHash"), "match3-swap-set.schema.json"),
):
pair_path = (audit_path.parent / str(pair_ref or "")).resolve()
if not pair_path.is_file():
errors.append(f"#/groups/{index}/{ref_field}: pair 子工件不存在")
continue
pair_value = _load_sample_instance(pair_path)
pair_errors = [
f"#/groups/{index}/{ref_field}{error[1:]}"
for error in _schema_errors(schema, pair_value)
]
errors += pair_errors
_trace("roll-pair", f"{schema}-schema", path=pair_path,
result="pass" if not pair_errors else "fail",
error=pair_errors[0] if pair_errors else None)
if pair_hash != trace_sealed(
"roll-pair", f"{schema}-bytes", pair_path, pair_hash, pair_value,
):
errors.append(f"#/groups/{index}/{ref_field}: pair 子工件 hash 不一致")
elif "GroupedAction" in schema_name or schema_name == "grouped-action-ref.schema.json":
expected_role = "first" if ref_field == "firstGroupedActionRef" else "second"
expected_action_id = group.get("firstActionId") if expected_role == "first" else group.get("secondActionId")
if nested.get("actionGroupId") != group.get("actionGroupId") \
or nested.get("role") != expected_role \
or nested.get("actionId") != expected_action_id:
errors.append(f"#/groups/{index}/{ref_field}: 必须绑定 actionGroup 对应原生动作")
native_path = (audit_path.parent / str(nested.get("actionRef") or "")).resolve()
if not native_path.is_file():
errors.append(f"#/groups/{index}/{ref_field}/actionRef: 原生 action 不存在")
else:
native = _load_sample_instance(native_path)
action_root = _load(_HERE / "playtest-evidence-v3.schema.json")
native_errors = validate(action_root["$defs"]["actionRecord"], native, action_root)
if not native_errors:
native_errors += _semantic_validate_v3_action(native, "#")
errors += [f"#/groups/{index}/{ref_field}/actionRef{error[1:]}" for error in native_errors]
if nested.get("actionHash") != _raw_sample_hash(native_path, native):
errors.append(f"#/groups/{index}/{ref_field}/actionHash: 与原生 action 封存字节不一致")
trace_sealed("roll-action", f"{ref_field}-native-action", native_path,
nested.get("actionHash"), native)
elif ref_field == "boardIdentityCheckRef":
if nested.get("actionGroupId") != group.get("actionGroupId"):
errors.append(f"#/groups/{index}/{ref_field}/actionGroupId: 必须镜像 actionGroup")
if group.get("state") == "completed" and nested.get("result") != "equivalent":
errors.append(f"#/groups/{index}/{ref_field}/result: completed group 必须 equivalent")
if group.get("state") == "aborted" and nested.get("result") == "equivalent":
errors.append(f"#/groups/{index}/{ref_field}/result: aborted group 不得 equivalent")
for side_name in ("initial", "postFirst"):
side = nested.get(side_name) or {}
target_path = (audit_path.parent / str(side.get("targetSetRef") or "")).resolve()
projection_path = (audit_path.parent / str(side.get("projectionRef") or "")).resolve()
if not target_path.is_file() or not projection_path.is_file():
errors.append(f"#/groups/{index}/{ref_field}/{side_name}: TargetSet/Projection 不存在")
continue
target_set = _load_sample_instance(target_path)
projection = _load_sample_instance(projection_path)
errors += [f"#/groups/{index}/{ref_field}/{side_name}{error[1:]}" for error in _schema_errors("visual-target-set.schema.json", target_set)]
errors += [f"#/groups/{index}/{ref_field}/{side_name}{error[1:]}" for error in _schema_errors("match3-board-projection.schema.json", projection)]
if side.get("targetSetHash") != target_set.get("targetSetHash") \
or side.get("projectionHash") != _raw_sample_hash(projection_path, projection) \
or side.get("equivalenceMatrixHash") != _projection_equivalence_hash(projection):
errors.append(f"#/groups/{index}/{ref_field}/{side_name}: 必须从 TargetSet/Projection 重算")
target_identity_ok = side.get("targetSetHash") == target_set.get("targetSetHash")
_trace("roll-board", f"{side_name}-target-set-identity", path=target_path,
declared_hash=side.get("targetSetHash"),
computed_hash=target_set.get("targetSetHash"),
result="pass" if target_identity_ok else "fail",
error=None if target_identity_ok else "TargetSet 身份漂移")
trace_sealed("roll-board", f"{side_name}-projection", projection_path,
side.get("projectionHash"), projection)
first_action_path = (audit_path.parent / str(nested.get("firstActionRef") or "")).resolve()
if not first_action_path.is_file():
errors.append(f"#/groups/{index}/{ref_field}/firstActionRef: 原生 action 不存在")
else:
first_action_value = _load_sample_instance(first_action_path)
if nested.get("firstActionHash") != _raw_sample_hash(first_action_path, first_action_value):
errors.append(f"#/groups/{index}/{ref_field}/firstActionHash: 与原生 action 字节不一致")
pair_resolutions.append(pair_resolution)
reset = audit.get("reset")
if isinstance(reset, dict):
reset_path = (audit_path.parent / reset.get("groupedActionRef", "")).resolve()
if not reset_path.is_file():
errors.append("#/reset/groupedActionRef: 引用 reset action 不存在")
else:
grouped = _load_sample_instance(reset_path)
errors += [
f"#/reset{error[1:]}"
for error in _schema_errors("grouped-action-ref.schema.json", grouped)
]
if reset.get("groupedActionHash") != _raw_sample_hash(reset_path, grouped):
errors.append("#/reset/groupedActionHash: 与 reset action 封存字节不一致")
trace_sealed("roll-reset", "grouped-action", reset_path,
reset.get("groupedActionHash"), grouped)
if grouped.get("role") != "reset" or grouped.get("endpoint") is not None:
errors.append("#/reset/groupedActionRef: 必须引用 endpoint=null 的 reset action")
if groups and grouped.get("actionGroupId") != groups[0].get("actionGroupId"):
errors.append("#/reset/groupedActionRef/actionGroupId: reset 必须归属首个 aborted group")
native_path = (audit_path.parent / reset.get("nativeActionRef", "")).resolve()
if not native_path.is_file():
errors.append("#/reset/nativeActionRef: reset 原生 action 不存在")
else:
native_action = _load_sample_instance(native_path)
action_root = _load(_HERE / "playtest-evidence-v3.schema.json")
action_errors = validate(action_root["$defs"]["actionRecord"], native_action, action_root)
if not action_errors:
action_errors += _semantic_validate_v3_action(native_action, "#")
errors += [f"#/reset/nativeActionRef{error[1:]}" for error in action_errors]
if reset.get("nativeActionHash") != _raw_sample_hash(native_path, native_action):
errors.append("#/reset/nativeActionHash: 与 reset 原生 action 封存字节不一致")
trace_sealed("roll-reset", "native-action", native_path,
reset.get("nativeActionHash"), native_action)
if grouped.get("actionRef") != reset.get("nativeActionRef") \
or grouped.get("actionHash") != reset.get("nativeActionHash") \
or grouped.get("actionId") != native_action.get("actionId"):
errors.append("#/reset/groupedActionRef: 必须绑定同一个 reset 原生 action")
reset_target_path = (audit_path.parent / reset.get("resetTargetSetRef", "")).resolve()
frame_path = (audit_path.parent / reset.get("postResetFrameFileRef", "")).resolve()
target_path = (audit_path.parent / reset.get("postResetTargetSetRef", "")).resolve()
projection_path = (audit_path.parent / reset.get("postResetProjectionRef", "")).resolve()
swap_path = (audit_path.parent / reset.get("postResetSwapSetRef", "")).resolve()
if (not reset_target_path.is_file() or not target_path.is_file()
or not projection_path.is_file() or not swap_path.is_file()):
errors.append("#/reset: reset 前后 TargetSet/Projection/SwapSet 引用不存在")
else:
reset_target_set = _load_sample_instance(reset_target_path)
target_set = _load_sample_instance(target_path)
projection = _load_sample_instance(projection_path)
swap_set = _load_sample_instance(swap_path)
errors += [f"#/reset/resetTargetSetRef{error[1:]}" for error in _schema_errors("visual-target-set.schema.json", reset_target_set)]
errors += [f"#/reset/postResetTargetSetRef{error[1:]}" for error in _schema_errors("visual-target-set.schema.json", target_set)]
errors += [f"#/reset/postResetProjectionRef{error[1:]}" for error in _schema_errors("match3-board-projection.schema.json", projection)]
errors += [f"#/reset/postResetSwapSetRef{error[1:]}" for error in _schema_errors("match3-swap-set.schema.json", swap_set)]
if reset.get("resetTargetSetHash") != _raw_sample_hash(reset_target_path, reset_target_set):
errors.append("#/reset/resetTargetSetHash: 与 reset 前 TargetSet 封存字节不一致")
if reset.get("postResetTargetSetHash") != _raw_sample_hash(target_path, target_set):
errors.append("#/reset/postResetTargetSetHash: 与 TargetSet 封存字节不一致")
if reset.get("postResetProjectionHash") != _raw_sample_hash(projection_path, projection):
errors.append("#/reset/postResetProjectionHash: 与 Projection 封存字节不一致")
if reset.get("postResetSwapSetHash") != _raw_sample_hash(swap_path, swap_set):
errors.append("#/reset/postResetSwapSetHash: 与 SwapSet 封存字节不一致")
trace_sealed("roll-reset", "pre-target-set", reset_target_path,
reset.get("resetTargetSetHash"), reset_target_set)
trace_sealed("roll-reset", "post-target-set", target_path,
reset.get("postResetTargetSetHash"), target_set)
trace_sealed("roll-reset", "post-projection", projection_path,
reset.get("postResetProjectionHash"), projection)
trace_sealed("roll-reset", "post-swap-set", swap_path,
reset.get("postResetSwapSetHash"), swap_set)
native_post = native_action.get("postFrameRef") or {}
if frame_path.is_symlink() or not frame_path.is_file():
errors.append("#/reset/postResetFrameFileRef: 必须是存在的普通非符号链接文件")
_trace("roll-reset", "post-frame-regular-file", ref=reset.get("postResetFrameFileRef"),
path=frame_path, result="fail", error="frame 不存在或不是普通文件")
else:
frame_hash = hashlib.sha256(frame_path.read_bytes()).hexdigest()
if reset.get("postResetFrameHash") != frame_hash or native_post.get("hash") != frame_hash:
errors.append("#/reset/postResetFrameHash: 必须等于实际 frame 字节与 reset action postFrameHash")
_trace("roll-reset", "post-frame-bytes", ref=reset.get("postResetFrameFileRef"), path=frame_path,
declared_hash=reset.get("postResetFrameHash"), computed_hash=frame_hash,
result="fail", error="reset post frame hash 漂移")
else:
_trace("roll-reset", "post-frame-bytes", ref=reset.get("postResetFrameFileRef"), path=frame_path,
declared_hash=reset.get("postResetFrameHash"), computed_hash=frame_hash, result="pass")
for actual, expected, message in (
(target_set.get("sourceFrameRef"), native_post.get("path"), "postReset TargetSet sourceFrameRef"),
(target_set.get("sourceFrameHash"), native_post.get("hash"), "postReset TargetSet sourceFrameHash"),
(projection.get("sourceFrameRef"), target_set.get("sourceFrameRef"), "postReset Projection sourceFrameRef"),
(projection.get("sourceFrameHash"), target_set.get("sourceFrameHash"), "postReset Projection sourceFrameHash"),
(projection.get("targetSetRef"), reset.get("postResetTargetSetRef"), "postReset Projection targetSetRef"),
(projection.get("targetSetHash"), target_set.get("targetSetHash"), "postReset Projection targetSetHash"),
(swap_set.get("sourceFrameRef"), target_set.get("sourceFrameRef"), "postReset SwapSet sourceFrameRef"),
(swap_set.get("sourceFrameHash"), target_set.get("sourceFrameHash"), "postReset SwapSet sourceFrameHash"),
(swap_set.get("targetSetRef"), reset.get("postResetTargetSetRef"), "postReset SwapSet targetSetRef"),
(swap_set.get("targetSetHash"), target_set.get("targetSetHash"), "postReset SwapSet targetSetHash"),
(swap_set.get("projectionRef"), reset.get("postResetProjectionRef"), "postReset SwapSet projectionRef"),
(swap_set.get("projectionHash"), projection.get("projectionHash"), "postReset SwapSet projectionHash"),
):
if actual != expected:
errors.append(f"#/reset: {message} 因果身份不一致")
if reset.get("resultingCandidateSetHash") != swap_set.get("candidateSetHash"):
errors.append("#/reset/resultingCandidateSetHash: 必须来自 reset 后 sealed SwapSet")
resolution = native_action.get("targetResolution") or {}
if resolution.get("targetSetHash") != reset_target_set.get("targetSetHash") \
or resolution.get("sourceFrameRef") != reset_target_set.get("sourceFrameRef") \
or resolution.get("sourceFrameHash") != reset_target_set.get("sourceFrameHash"):
errors.append("#/reset/nativeActionRef/targetResolution: 必须绑定 reset 前 TargetSet")
endpoint = (pair_resolutions[0] or {}).get("firstEndpoint") if pair_resolutions else None
lattice = next((row for row in reset_target_set.get("targets") or [] if row.get("id") == (endpoint or {}).get("id")), None)
cell = (endpoint or {}).get("cell") or {}
if lattice is None:
errors.append("#/reset/postResetTargetSetRef: 不含首组 firstEndpoint lattice")
else:
expected_action = {
"type": "tap",
"x": (lattice.get("columnCenters") or [])[cell.get("column", 1) - 1],
"y": (lattice.get("rowCenters") or [])[cell.get("row", 1) - 1],
}
if native_action.get("normalized") != expected_action or native_action.get("virtualTimeBudgetMs") != 600:
errors.append("#/reset/nativeActionRef: 必须以 600ms repeat-first-endpoint 原生 tap 执行")
events_path = (audit_path.parent / reset.get("eventSliceRef", "")).resolve()
if not events_path.is_file():
errors.append("#/reset/eventSliceRef: reset 完整事件切片不存在")
else:
reset_events = _load_sample_instance(events_path)
if reset.get("eventSliceHash") != _raw_sample_hash(events_path, reset_events):
errors.append("#/reset/eventSliceHash: 与 reset 事件切片原始字节不一致")
trace_sealed("roll-reset", "event-slice", events_path,
reset.get("eventSliceHash"), reset_events)
if not isinstance(reset_events, list):
errors.append("#/reset/eventSliceRef: 必须是完整事件数组")
else:
expected_seqs = list(range(native_action.get("preEventSeq", 0) + 1, native_action.get("postEventSeq", 0) + 1))
if [event.get("seq") for event in reset_events] != expected_seqs:
errors.append("#/reset/eventSliceRef: 必须完整覆盖 reset action 事件区间")
for event in reset_events:
errors += [f"#/reset/eventSliceRef{error[1:]}" for error in _schema_errors("game-event.schema.json", event)]
if any(event.get("type") in ("puzzle.swap-committed", "puzzle.match-formed") for event in reset_events):
errors.append("#/reset/eventSliceRef: reset 不得产生 swap-committed/match-formed")
return errors
def _validate_match3_specialty_proof_bundle(manifest_path: Path) -> list:
"""回读 Proof/3→Audit/2→全部 sidecar复算原始字节 hash 与跨文件身份。"""
errors = []
manifest = _load_sample_instance(manifest_path)
if manifest.get("schemaVersion") != "Match3SpecialtyProofBundle/1":
return ["#/schemaVersion: 必须为 Match3SpecialtyProofBundle/1"]
allowed_manifest_keys = {
"schemaVersion", "singleRollProofRef", "singleRollProofHash", "expectedErrorPattern",
}
extra_keys = set(manifest) - allowed_manifest_keys
if extra_keys:
errors.append(f"#: specialty bundle 含未知字段 {sorted(extra_keys)}")
def resolve_ref(owner: Path, ref, field: str) -> Path | None:
if not isinstance(ref, str) or not ref or ref.startswith("/"):
errors.append(f"#/{field}: 必须是非空相对引用")
return None
target = (owner.parent / ref).resolve()
try:
target.relative_to(_HERE)
except ValueError:
errors.append(f"#/{field}: 引用越出 contracts/play-loop")
return None
if target.is_symlink() or not target.is_file():
errors.append(f"#/{field}: 引用不存在或不是普通文件")
return None
return target
def load_json_ref(owner: Path, ref, declared_hash, field: str, schema_name=None,
canonical_hash_field=None):
target = resolve_ref(owner, ref, field)
if target is None:
return None, None
try:
value = _load_sample_instance(target)
except Exception as exc: # noqa: BLE001 —— 引用文件损坏必须保留原始根因。
errors.append(f"#/{field}: JSON 不可读:{exc}")
return None, target
if canonical_hash_field is None:
computed = _raw_sample_hash(target, value)
if declared_hash != computed:
errors.append(f"#/{field}: 声明 hash 与封存原始字节不一致")
elif declared_hash != value.get(canonical_hash_field):
errors.append(f"#/{field}: 声明 hash 必须等于 Audit/2 canonical auditHash")
if schema_name:
schema_errors = _schema_errors(schema_name, value)
errors.extend(f"#/{field}{error[1:]}" for error in schema_errors)
return value, target
proof, proof_path = load_json_ref(
manifest_path, manifest.get("singleRollProofRef"),
manifest.get("singleRollProofHash"), "singleRollProofRef",
"single-roll-proof-v3.schema.json",
)
if proof is None or proof_path is None:
return errors
proof_schema = _load(_HERE / "single-roll-proof-v3.schema.json")
proof_schema_errors = validate(proof_schema, proof, proof_schema)
if not proof_schema_errors:
errors += [
f"#/singleRollProofRef{error[1:]}"
for error in _semantic_validate_single_roll_proof_v3(proof)
]
if proof.get("packageType") != "SingleRollProof/3":
errors.append("#/singleRollProofRef/packageType: 旧 Proof/2 不得冒充视觉音频闭包")
audit, audit_path = load_json_ref(
proof_path, proof.get("match3RollInteractionAuditRef"),
proof.get("match3RollInteractionAuditHash"), "match3RollInteractionAuditRef",
"match3-roll-interaction-audit-v2.schema.json", "auditHash",
)
if audit is None or audit_path is None:
return errors
audit_schema = _load(_HERE / "match3-roll-interaction-audit-v2.schema.json")
audit_schema_errors = validate(audit_schema, audit, audit_schema)
if not audit_schema_errors:
errors += [
f"#/match3RollInteractionAuditRef{error[1:]}"
for error in _semantic_validate_roll_audit_v2(audit)
]
for proof_field, audit_field in (
("gameId", "gameId"), ("artifactHash", "artifactHash"),
("taskBindingHash", "taskBindingHash"),
("acceptanceRequestHash", "acceptanceRequestHash"),
):
if proof.get(proof_field) != audit.get(audit_field):
errors.append(f"#/singleRollProofRef/{proof_field}: 必须镜像 Audit/2")
if f"roll-{proof.get('roll')}" != audit.get("rollId") \
or proof.get("requestedSeed") != audit.get("seed"):
errors.append("#/singleRollProofRef: roll/seed 必须镜像 Audit/2")
sidecars = {}
for key, schema_name in (
("actionAudit", "match3-roll-interaction-audit.schema.json"),
("buildManifest", "build-manifest.schema.json"),
("visualTransaction", "match3-visual-transaction.schema.json"),
("visualAudit", "match3-visual-audit.schema.json"),
("effectAudit", "match3-effect-audit.schema.json"),
("audioAudit", "match3-audio-audit.schema.json"),
("inputLockProbe", "input-lock-probe.schema.json"),
("filmstripManifest", "match3-filmstrip-analyzer.schema.json"),
):
value, target = load_json_ref(
audit_path, audit.get(f"{key}Ref"), audit.get(f"{key}Hash"), f"{key}Ref", schema_name,
)
sidecars[key] = (value, target)
settlement_path = resolve_ref(
audit_path, audit.get("visualSettlementFrameRef"), "visualSettlementFrameRef",
)
if settlement_path is not None:
settlement_hash = hashlib.sha256(settlement_path.read_bytes()).hexdigest()
if settlement_hash != audit.get("visualSettlementFrameHash"):
errors.append("#/visualSettlementFrameHash: 与稳定终盘 PNG 原始字节不一致")
action_audit, action_path = sidecars["actionAudit"]
if action_audit is not None and action_path is not None:
errors += [f"#/actionAuditRef{error[1:]}" for error in _validate_match3_roll_audit(action_path)]
if action_audit.get("schemaVersion") != "Match3RollInteractionAudit/1":
errors.append("#/actionAuditRef/schemaVersion: 必须显式引用冻结 /1")
if action_audit.get("finalState") != "completed":
errors.append("#/actionAuditRef/finalState: Audit/2 只能提升 completed 动作账")
for field in (
"rollId", "gameId", "artifactHash", "taskBindingHash", "acceptanceRequestHash",
"seed", "interactionProfileId", "interactionRegistryVersion", "interactionBindingHash",
):
if action_audit.get(field) != audit.get(field):
errors.append(f"#/actionAuditRef/{field}: 必须镜像 Audit/2")
build, _ = sidecars["buildManifest"]
transaction, _ = sidecars["visualTransaction"]
visual, _ = sidecars["visualAudit"]
effect, effect_path = sidecars["effectAudit"]
audio, _ = sidecars["audioAudit"]
lock, _ = sidecars["inputLockProbe"]
filmstrip, _ = sidecars["filmstripManifest"]
expected_versions = {
"buildManifest": "BuildManifest/1",
"visualTransaction": "Match3VisualTransaction/1",
"visualAudit": "Match3VisualAudit/1",
"effectAudit": "Match3EffectAudit/1",
"audioAudit": "Match3AudioAudit/1",
"inputLockProbe": "InputLockProbe/1",
"filmstripManifest": "Match3FilmstripAnalyzer/1",
}
for key, expected_version in expected_versions.items():
value = sidecars[key][0] or {}
if value.get("schemaVersion") != expected_version:
errors.append(f"#/{key}Ref/schemaVersion: 必须为 {expected_version}")
if build:
if build.get("artifactHash") != audit.get("artifactHash") \
or build.get("gameId") != audit.get("gameId"):
errors.append("#/buildManifestRef: game/artifact 身份与 Audit/2 分叉")
bgm_rows = [row for row in build.get("files") or [] if row.get("role") == "bgm"]
if len(bgm_rows) != 1:
errors.append("#/buildManifestRef/files: 必须且只能绑定一份 BGM 原始字节")
else:
bgm_rows = []
for key, value in (
("visualTransaction", transaction), ("visualAudit", visual),
("effectAudit", effect), ("inputLockProbe", lock),
("filmstripManifest", filmstrip),
):
value = value or {}
if value.get("transactionId") != audit.get("transactionId") \
or value.get("transactionHash") != audit.get("transactionHash"):
errors.append(f"#/{key}Ref: transaction 身份与 Audit/2 分叉")
if transaction:
if transaction.get("moveId") != audit.get("moveId") \
or transaction.get("visualComplete") is not True \
or transaction.get("degradedReason") is not None:
errors.append("#/visualTransactionRef: 必须是同 move 的完整非降级视觉事务")
if visual:
if visual.get("artifactHash") != audit.get("artifactHash") \
or visual.get("status") != "pass" \
or visual.get("moveId") != audit.get("moveId"):
errors.append("#/visualAuditRef: artifact/move/status 与 Audit/2 不闭合")
if visual.get("visualTransactionRef") != audit.get("visualTransactionRef") \
or visual.get("visualTransactionHash") != audit.get("visualTransactionHash"):
errors.append("#/visualAuditRef: 未单向绑定同一 VisualTransaction")
if visual.get("visualSettlementFrameRef") != audit.get("visualSettlementFrameRef") \
or visual.get("visualSettlementFrameHash") != audit.get("visualSettlementFrameHash"):
errors.append("#/visualAuditRef: 未绑定 Audit/2 稳定终盘")
if effect:
if effect.get("artifactHash") != audit.get("artifactHash") or effect.get("status") != "pass":
errors.append("#/effectAuditRef: artifact/status 与 Audit/2 不闭合")
if effect.get("buildManifestRef") != audit.get("buildManifestRef") \
or effect.get("buildManifestHash") != audit.get("buildManifestHash"):
errors.append("#/effectAuditRef: 未绑定同一 build manifest")
if effect.get("filmstripManifestRef") != audit.get("filmstripManifestRef") \
or effect.get("filmstripManifestHash") != audit.get("filmstripManifestHash"):
errors.append("#/effectAuditRef: 未绑定同一 filmstrip")
if not effect.get("effectIds") or not effect.get("renderReceiptRefs"):
errors.append("#/effectAuditRef: 必须有局部 effect 与可信主画布 receipt")
for index, receipt_ref in enumerate(effect.get("renderReceiptRefs") or []):
receipt, _ = load_json_ref(
effect_path, receipt_ref.get("ref"), receipt_ref.get("hash"),
f"effectAuditRef/renderReceiptRefs/{index}",
"match3-effect-render-receipt.schema.json",
)
if not receipt or receipt.get("schemaVersion") != "Match3EffectRenderReceipt/1" \
or receipt.get("transactionId") != audit.get("transactionId") \
or receipt.get("mainContextIdentity") != "plugin-init-main-context":
errors.append(f"#/effectAuditRef/renderReceiptRefs/{index}: 主画布 receipt 不可信")
if lock:
if lock.get("artifactHash") != audit.get("artifactHash") \
or lock.get("probeSeed") != audit.get("seed") \
or lock.get("lockedStateUnchanged") is not True \
or lock.get("settleInputRestored") is not True \
or lock.get("status") != "pass":
errors.append("#/inputLockProbeRef: 输入锁 control/probe 未闭合")
if filmstrip:
if filmstrip.get("artifactHash") != audit.get("artifactHash") \
or filmstrip.get("status") != "pass":
errors.append("#/filmstripManifestRef: artifact/status 与 Audit/2 不闭合")
if filmstrip.get("settlementFrameRef") != audit.get("visualSettlementFrameRef") \
or filmstrip.get("settlementFrameHash") != audit.get("visualSettlementFrameHash"):
errors.append("#/filmstripManifestRef: 未绑定 Audit/2 稳定终盘")
phases = [row.get("phase") for row in filmstrip.get("frames") or []]
cursor = -1
for phase in ("swap", "clear", "fall_refill", "settle"):
try:
cursor = phases.index(phase, cursor + 1)
except ValueError:
errors.append("#/filmstripManifestRef/frames: 缺 swap→clear→fall_refill→settle 顺序")
break
if audio:
audio_schema = _load(_HERE / "match3-audio-audit.schema.json")
audio_errors = validate(audio_schema, audio, audio_schema)
if not audio_errors:
errors += [
f"#/audioAuditRef{error[1:]}"
for error in _semantic_validate_match3_audio_audit(audio)
]
if not audio.get("samples") or audio.get("pendingOnsetCueReceiptRefs"):
errors.append("#/audioAuditRef: Audit/2 只接受 populated 且无 pending onset 的完整音频账")
bgm_hash = bgm_rows[0].get("sha256") if len(bgm_rows) == 1 else None
if (audio.get("audioArtifactBinding") or {}).get("sha256") != bgm_hash:
errors.append("#/audioAuditRef/audioArtifactBinding: 未绑定 build manifest 的 BGM 原始字节")
for sample in audio.get("samples") or []:
for transition in sample.get("transitionRefs") or []:
if transition.get("transactionId") != audit.get("transactionId") \
or transition.get("transactionHash") != audit.get("transactionHash"):
errors.append("#/audioAuditRef/transitionRefs: transaction 与 Audit/2 分叉")
def has_reverse_ref(value) -> bool:
if isinstance(value, dict):
if "singleRollProofRef" in value or "match3RollInteractionAuditRef" in value:
return True
return any(has_reverse_ref(child) for child in value.values())
if isinstance(value, list):
return any(has_reverse_ref(child) for child in value)
return False
for key, (value, _) in sidecars.items():
if key != "actionAudit" and has_reverse_ref(value):
errors.append(f"#/{key}Ref: sidecar 不得反向引用 Audit/2 或 Proof/3")
return errors
def _validate_proof_resolution_bundle(manifest_path: Path) -> list:
"""跨文件核对 request→binding→Resolution→SingleRollProof→evidence 闭包。"""
errors = []
manifest_schema = _load(_HERE / "proof-resolution-bundle.schema.json")
manifest = _load_sample_instance(manifest_path)
errors += validate(manifest_schema, manifest, manifest_schema)
if errors:
return errors
def ref_path(field: str, *, nullable: bool = False):
ref = manifest.get(field)
if ref is None and nullable:
return None
path = (manifest_path.parent / str(ref)).resolve()
if not path.is_file():
errors.append(f"#/{field}: 引用文件不存在 {ref}")
return None
return path
request_path = ref_path("requestRef")
provenance_path = ref_path("provenanceRef")
parent_request_path = ref_path("parentRequestRef", nullable=True)
parent_provenance_path = ref_path("parentProvenanceRef", nullable=True)
brief_path = ref_path("briefRef", nullable=True)
proof_path = ref_path("singleRollProofRef")
binding_path = ref_path("interactionBindingRef", nullable=True)
resolution_path = ref_path("resolutionRef", nullable=True)
if not request_path or not provenance_path or not proof_path:
return errors
request = _load_sample_instance(request_path)
provenance = _load_sample_instance(provenance_path)
proof = _load_sample_instance(proof_path)
request_version = request.get("schemaVersion")
if request_version == "acceptance-request/1":
request_schema = _load(_HERE / "acceptance-request.schema.json")
provenance_schema = _load(_HERE / "acceptance-provenance.schema.json")
errors += validate(request_schema, request, request_schema)
errors += _semantic_validate_acceptance_profile(request)
errors += validate(provenance_schema, provenance, provenance_schema)
errors += _semantic_validate_acceptance_profile(provenance)
legacy_proof_schema = _load(_HERE / "single-roll-proof-v1.schema.json")
legacy_proof_errors = validate(legacy_proof_schema, proof, legacy_proof_schema)
errors += [f"#/singleRollProofRef{error[1:]}" for error in legacy_proof_errors]
if not legacy_proof_errors:
errors += [
f"#/singleRollProofRef{error[1:]}"
for error in _semantic_validate_single_roll_proof_v1(proof)
]
legacy_mirror_fields = (
"gameId", "briefHash", "genre", "templateRoute", "proofProfileId",
"proofRegistryVersion", "taskBindingHash", "sourceArtifactHash",
"parentAcceptanceRequestHash", "repairOrdinal",
)
for field in legacy_mirror_fields:
if provenance.get(field) != request.get(field):
errors.append(f"#/provenanceRef/{field}: 必须逐字段镜像 legacy request")
for field in (
"gameId", "briefHash", "genre", "templateRoute", "proofProfileId",
"proofRegistryVersion", "taskBindingHash",
):
if proof.get(field) != request.get(field):
errors.append(f"#/singleRollProofRef/{field}: 必须镜像 legacy request")
if proof.get("acceptanceRequestHash") != provenance.get("acceptanceRequestHash"):
errors.append(
"#/singleRollProofRef/acceptanceRequestHash: "
"必须绑定 legacy provenance 的 opaque request hash",
)
if proof.get("artifactHash") != provenance.get("artifactHash"):
errors.append("#/singleRollProofRef/artifactHash: 必须等于 legacy provenance artifactHash")
if provenance.get("schemaVersion") != "acceptance-provenance/1":
errors.append("#/provenanceRef: legacy request 必须配 acceptance-provenance/1")
if binding_path or resolution_path:
errors.append("#: legacy /1 不得读取 binding 或生成 Resolution sidecar")
if any(value is not None for value in (manifest.get("runnerBinding") or {}).values()):
errors.append("#/runnerBinding: legacy /1 不得传新 interaction path/scalars")
if manifest.get("publishFrozen") is not True:
errors.append("#/publishFrozen: legacy /1 必须冻结发布")
if parent_request_path is not None or parent_provenance_path is not None:
errors.append("#/parentRequestRef: legacy /1 不走 /2 repair 继承校验")
elif request_version == "acceptance-request/2":
request_schema = _load(_HERE / "acceptance-request-v2.schema.json")
provenance_schema = _load(_HERE / "acceptance-provenance-v2.schema.json")
proof_schema = _load(_HERE / "single-roll-proof-v2.schema.json")
resolution_schema = _load(_HERE / "proof-obligation-resolution.schema.json")
errors += validate(request_schema, request, request_schema)
errors += _semantic_validate_acceptance_profile(request)
errors += validate(provenance_schema, provenance, provenance_schema)
errors += _semantic_validate_acceptance_profile(provenance)
errors += validate(proof_schema, proof, proof_schema)
if not validate(proof_schema, proof, proof_schema):
errors += _semantic_validate_single_roll_proof_v2(proof)
if provenance.get("schemaVersion") != "acceptance-provenance/2":
errors.append("#/provenanceRef: /2 request 必须配 acceptance-provenance/2")
if resolution_path is None:
errors.append("#/resolutionRef: 新 /2 强制 Resolution sidecar")
return errors
resolution = _load_sample_instance(resolution_path)
errors += validate(resolution_schema, resolution, resolution_schema)
if not errors:
errors += _semantic_validate_proof_resolution(resolution)
request_hash = _canonical_domain_hash("acceptance-request/2", request)
if provenance.get("acceptanceRequestHash") != request_hash:
errors.append("#/provenanceRef/acceptanceRequestHash: 与完整 canonical request 不一致")
mirror_fields = [
"gameId", "briefHash", "genre", "templateRoute", "proofProfileId",
"proofRegistryVersion", "taskBindingHash", "interactionBinding",
"sourceArtifactHash", "parentAcceptanceRequestHash", "repairOrdinal",
]
for field in mirror_fields:
if provenance.get(field) != request.get(field):
errors.append(f"#/provenanceRef/{field}: 必须逐字镜像 request")
resolution_identity = {
"acceptanceRequestHash": request_hash,
"proofProfileId": request.get("proofProfileId"),
"proofRegistryVersion": request.get("proofRegistryVersion"),
"taskBindingHash": request.get("taskBindingHash"),
}
for field, expected in resolution_identity.items():
if resolution.get(field) != expected:
errors.append(
f"#/resolutionRef/{field}: 必须与 canonical request 身份一致",
)
if request.get("repairOrdinal") == 0:
if parent_request_path is not None:
errors.append("#/parentRequestRef: repairOrdinal=0 时必须为 null")
if parent_provenance_path is not None:
errors.append("#/parentProvenanceRef: repairOrdinal=0 时必须为 null")
else:
parent_request = None
if parent_request_path is None:
errors.append("#/parentRequestRef: repairOrdinal=1 时必须引用 parent request/2")
else:
parent_request = _load_sample_instance(parent_request_path)
parent_errors = validate(request_schema, parent_request, request_schema)
if parent_errors:
errors.append(f"#/parentRequestRef: parent 不是合法 acceptance-request/2{parent_errors[0]}")
else:
errors += _semantic_validate_acceptance_profile(parent_request)
parent_hash = _canonical_domain_hash("acceptance-request/2", parent_request)
if request.get("parentAcceptanceRequestHash") != parent_hash:
errors.append("#/parentAcceptanceRequestHash: 与 parent canonical request hash 不一致")
if parent_request.get("repairOrdinal") != 0:
errors.append("#/parentRequestRef/repairOrdinal: parent 必须是初始请求")
for field in (
"gameId", "briefHash", "genre", "templateRoute", "proofProfileId",
"proofRegistryVersion", "taskBindingHash", "interactionBinding",
):
if request.get(field) != parent_request.get(field):
errors.append(f"#/parentRequestRef/{field}: repair 必须逐字段继承")
if parent_provenance_path is None:
errors.append("#/parentProvenanceRef: repairOrdinal=1 时必须引用 parent provenance/2")
else:
parent_provenance = _load_sample_instance(parent_provenance_path)
parent_provenance_errors = validate(
provenance_schema, parent_provenance, provenance_schema,
)
if parent_provenance_errors:
errors.append(
"#/parentProvenanceRef: parent 不是合法 acceptance-provenance/2"
f"{parent_provenance_errors[0]}",
)
else:
errors += _semantic_validate_acceptance_profile(parent_provenance)
if parent_request is not None:
parent_request_hash = _canonical_domain_hash(
"acceptance-request/2", parent_request,
)
if parent_provenance.get("acceptanceRequestHash") != parent_request_hash:
errors.append(
"#/parentProvenanceRef/acceptanceRequestHash: "
"必须绑定 parent canonical request",
)
for field in (
"gameId", "briefHash", "genre", "templateRoute", "proofProfileId",
"proofRegistryVersion", "taskBindingHash", "interactionBinding",
"sourceArtifactHash", "parentAcceptanceRequestHash", "repairOrdinal",
):
if parent_provenance.get(field) != parent_request.get(field):
errors.append(
f"#/parentProvenanceRef/{field}: "
"必须逐字段镜像 parent request",
)
if request.get("sourceArtifactHash") != parent_provenance.get("artifactHash"):
errors.append(
"#/parentProvenanceRef/artifactHash: repair sourceArtifactHash "
"必须等于 parent artifactHash",
)
if brief_path is None:
errors.append("#/briefRef: acceptance-request/2 必须引用原始 brief 文件")
brief_text = ""
else:
brief_raw = brief_path.read_bytes()
try:
brief_text = brief_raw.decode("utf-8", errors="strict")
except UnicodeDecodeError as exc:
brief_text = ""
errors.append(f"#/briefRef: brief 不是严格 UTF-8{exc}")
brief_hash = hashlib.sha256(brief_raw).hexdigest()
for holder_path, holder in (
("requestRef", request), ("provenanceRef", provenance),
("singleRollProofRef", proof),
):
if holder.get("briefHash") != brief_hash:
errors.append(f"#/{holder_path}/briefHash: 与 brief 原始字节 SHA-256 不一致")
proof_registry = _load_proof_registry(request.get("proofRegistryVersion"))
profile = (proof_registry.get("profiles") or {}).get(request.get("proofProfileId")) or {}
normalized_brief = brief_text.lower()
expected_brief_matches = []
for rule in profile.get("briefOptionalRules") or []:
when = rule.get("when") or {}
any_keywords = when.get("anyKeywords") or []
all_keywords = when.get("allKeywords") or []
matched = (
(not any_keywords or any(str(keyword).lower() in normalized_brief for keyword in any_keywords))
and all(str(keyword).lower() in normalized_brief for keyword in all_keywords)
)
if matched:
expected_brief_matches.append(rule.get("id"))
for holder_path, actual in (
("resolutionRef", resolution.get("briefRuleMatches")),
("singleRollProofRef", proof.get("briefRuleMatches")),
("evidenceProjection", (manifest.get("evidenceProjection") or {}).get("briefRuleMatches")),
):
if actual != expected_brief_matches:
errors.append(f"#/{holder_path}/briefRuleMatches: 与原始 brief 确定性匹配结果不一致")
runner = manifest.get("runnerBinding") or {}
binding = request.get("interactionBinding")
if binding is None:
if binding_path is not None or any(value is not None for value in runner.values()):
errors.append("#: absent 时不得存在 binding 文件或 runner path/scalars")
if resolution.get("interactionBindingStatus") != "absent":
errors.append("#/resolutionRef/interactionBindingStatus: absent request 必须为 absent")
expected_interaction_matches = []
else:
if binding_path is None:
errors.append("#/interactionBindingRef: verified 时 binding 文件必需")
else:
binding_raw = binding_path.read_bytes()
binding_file_hash = hashlib.sha256(binding_raw).hexdigest()
try:
binding_file_value = _load_payload_canonical(binding_raw.decode("utf-8"))
except Exception as exc: # noqa: BLE001 —— 文件损坏必须稳定归契约错误
binding_file_value = None
errors.append(f"#/interactionBindingRef: 非严格 JSON{exc}")
if not isinstance(binding_file_value, dict):
errors.append("#/interactionBindingRef: 必须是 InteractionBinding/1 对象JSON null 非法")
elif binding_file_value != binding or provenance.get("interactionBinding") != binding:
errors.append("#/interactionBindingRef: request/provenance/file 三方对象不一致")
expected_runner = {
"bindingPath": manifest.get("interactionBindingRef"),
"interactionProfileId": binding.get("interactionProfileId"),
"interactionRegistryVersion": binding.get("interactionRegistryVersion"),
"interactionBindingHash": binding.get("interactionBindingHash"),
"taskBindingHash": binding.get("taskBindingHash"),
}
if runner != expected_runner:
errors.append("#/runnerBinding: 必须由 verified binding 确定性派生")
if resolution.get("interactionBindingRef") != manifest.get("interactionBindingRef"):
errors.append("#/resolutionRef/interactionBindingRef: 必须镜像 manifest 与 runner path")
if resolution.get("interactionBindingStatus") != "verified":
errors.append("#/resolutionRef/interactionBindingStatus: 完整 binding 必须为 verified")
if resolution.get("interactionBindingFileHash") != binding_file_hash:
errors.append("#/resolutionRef/interactionBindingFileHash: 与 binding 原始字节不一致")
if resolution.get("interactionBindingHash") != binding.get("interactionBindingHash"):
errors.append("#/resolutionRef/interactionBindingHash: 与语义 binding hash 不一致")
expected_interaction_matches = [
rule.get("id") for rule in profile.get("interactionOptionalRules") or []
if rule.get("interactionProfileId") == binding.get("interactionProfileId")
]
if resolution.get("interactionRuleMatches") != expected_interaction_matches:
errors.append("#/resolutionRef/interactionRuleMatches: 与 verified binding 精确匹配结果不一致")
resolution_bytes = _sealed_sample_bytes(resolution_path, resolution)
resolution_file_hash = hashlib.sha256(resolution_bytes).hexdigest()
if proof.get("proofObligationResolutionRef") != manifest.get("resolutionRef"):
errors.append("#/singleRollProofRef/proofObligationResolutionRef: 与 manifest 不一致")
if proof.get("proofObligationResolutionHash") != resolution_file_hash:
errors.append("#/singleRollProofRef/proofObligationResolutionHash: 未绑定 sidecar 原始字节")
for field in (
"gameId", "genre", "templateRoute", "proofProfileId", "proofRegistryVersion",
"taskBindingHash", "briefHash",
):
if proof.get(field) != request.get(field):
errors.append(f"#/singleRollProofRef/{field}: 必须镜像 request")
if proof.get("acceptanceRequestHash") != request_hash:
errors.append("#/singleRollProofRef/acceptanceRequestHash: 必须镜像 canonical request hash")
if proof.get("artifactHash") != provenance.get("artifactHash"):
errors.append("#/singleRollProofRef/artifactHash: 必须等于 provenance artifactHash")
for field in (
"acceptanceRequestHash", "proofProfileId", "proofRegistryVersion", "taskBindingHash",
):
if proof.get(field) != resolution.get(field):
errors.append(f"#/singleRollProofRef/{field}: 必须逐字镜像 Resolution 身份")
if proof.get("briefRuleMatches") != resolution.get("briefRuleMatches"):
errors.append("#/singleRollProofRef/briefRuleMatches: 必须镜像 Resolution")
judge_ref = proof.get("judgePackageRef")
judge_path = (proof_path.parent / str(judge_ref)).resolve()
if not judge_path.is_file():
errors.append(f"#/singleRollProofRef/judgePackageRef: 引用文件不存在 {judge_ref}")
else:
judge_schema = _load(_HERE / "judge-package.schema.json")
judge_package = _load_sample_instance(judge_path)
judge_errors = validate(judge_schema, judge_package, judge_schema)
# 隔离泄漏要先于结构噪声报告,便于审计确认被拦的是越权输入而非偶然字段错误。
errors += [
f"#/judgePackageRef{error[1:]}"
for error in _semantic_validate_judge_package(judge_package)
]
errors += [f"#/judgePackageRef{error[1:]}" for error in judge_errors]
judge_hash = hashlib.sha256(_sealed_sample_bytes(judge_path, judge_package)).hexdigest()
if proof.get("judgePackageHash") != judge_hash:
errors.append("#/singleRollProofRef/judgePackageHash: 与 JudgePackage 原始字节不一致")
if (proof.get("judge") or {}).get("packageHash") != judge_hash:
errors.append("#/singleRollProofRef/judge/packageHash: 必须镜像 JudgePackage hash")
for field in (
"gameId", "genre", "templateRoute", "proofProfileId", "proofRegistryVersion",
"taskBindingHash", "acceptanceRequestHash", "briefHash",
):
if judge_package.get(field) != proof.get(field):
errors.append(f"#/judgePackageRef/{field}: 必须镜像 SingleRollProof/2")
if judge_package.get("brief") != brief_text:
errors.append("#/judgePackageRef/brief: 必须逐字等于原始 brief")
if (judge_package.get("registry") or {}).get("briefRuleMatches") != expected_brief_matches:
errors.append("#/judgePackageRef/registry/briefRuleMatches: 必须镜像重算结果")
projection = manifest.get("evidenceProjection") or {}
if projection.get("acceptanceRequestHash") != request_hash:
errors.append("#/evidenceProjection/acceptanceRequestHash: 必须镜像 canonical request hash")
if projection.get("briefRuleMatches") != resolution.get("briefRuleMatches"):
errors.append("#/evidenceProjection/briefRuleMatches: 必须镜像 Resolution")
required_map = {
row.get("id"): row.get("required") for row in projection.get("proofObligations") or []
}
expected_required = set(resolution.get("requiredObligationIds") or [])
if {oid for oid, required in required_map.items() if required is True} != expected_required:
errors.append("#/evidenceProjection/proofObligations: required 集与 Resolution 不一致")
proof_projection = [
{"id": row.get("id"), "required": row.get("required")}
for row in proof.get("proofObligations") or []
]
if projection.get("proofObligations") != proof_projection:
errors.append("#/evidenceProjection/proofObligations: 必须由 SingleRollProof/2 逐项投影")
proof_required = {
row.get("id") for row in proof.get("proofObligations") or []
if row.get("required") is True
}
if proof_required != expected_required:
errors.append("#/singleRollProofRef/proofObligations: required 集与 Resolution 不一致")
else:
errors.append(f"#/requestRef/schemaVersion: 未知 request 版本 {request_version}")
proof_bytes = _sealed_sample_bytes(proof_path, proof)
proof_hash = hashlib.sha256(proof_bytes).hexdigest()
if manifest.get("proofPackageHash") != proof_hash:
errors.append("#/proofPackageHash: 与 SingleRollProof 封存字节不一致")
return errors
def _validate_file(schema: dict, sample_path: Path) -> list:
"""校验单个样本文件返回错误列表JSON 解析失败也算一条错误)。"""
try:
instance = _load_sample_instance(sample_path)
except Exception as e: # noqa: BLE001 —— 解析失败即样本不合格,如实报,不抛断链
return [f"#: JSON 解析失败:{e}"]
errors = validate(schema, instance, schema)
# 先保证结构成立再跑跨引用语义,避免结构缺字段时产生大量次生噪声。
if not errors:
errors += _semantic_validate(schema, instance)
if not errors and schema.get("title") == "ReferenceAssetConsumptionManifestV1":
errors += _semantic_validate_reference_asset_consumption_manifest_bytes(sample_path, instance)
return errors
def _run_suite(samples_root: Path) -> int:
"""套件模式正样本应全过、负样本应全拦。返回进程退出码0=期望全满足)。"""
contracts = [
("play-spec", _HERE / "play-spec.schema.json", []),
("verdict-feedback", _HERE / "verdict-feedback.schema.json", []),
("game-event", _HERE / "game-event.schema.json", []),
("playtest-evidence", _HERE / "playtest-evidence.schema.json", []),
("proof-obligation-registry", _HERE / "proof-obligation-registry.schema.json",
[_HERE / "proof-obligations.v2.json"]),
("proof-obligation-registry-legacy-v2",
_HERE / "proof-obligation-registry.2026-07-14.v2.schema.json",
[_HERE / "proof-obligations.2026-07-14.v2.json"]),
("legacy-game-log-mapping", _HERE / "legacy-game-log-mapping.schema.json",
[_HERE / "legacy-game-log-mapping.v1.json"]),
("single-roll-fact", _HERE / "single-roll-fact.schema.json", []),
("acceptance-request", _HERE / "acceptance-request.schema.json", []),
("acceptance-provenance", _HERE / "acceptance-provenance.schema.json", []),
("acceptance-request-v2", _HERE / "acceptance-request-v2.schema.json", []),
("acceptance-provenance-v2", _HERE / "acceptance-provenance-v2.schema.json", []),
("acceptance-request-v3", _HERE / "acceptance-request-v3.schema.json", []),
("acceptance-provenance-v3", _HERE / "acceptance-provenance-v3.schema.json", []),
("reference-asset-record", _HERE / "reference-asset-record.schema.json", []),
("reference-asset-registry", _HERE / "reference-asset-registry.schema.json",
[_HERE / "reference-asset-registry.initial.json"]),
("reference-asset-record-v2", _HERE / "reference-asset-record-v2.schema.json", []),
("reference-asset-registry-v2", _HERE / "reference-asset-registry-v2.schema.json", []),
("reference-asset-consumption-manifest",
_HERE / "reference-asset-consumption-manifest.schema.json", []),
("reference-asset-consumption-policy",
_HERE / "reference-asset-consumption-policy.schema.json", []),
("reference-asset-release", _HERE / "reference-asset-release.schema.json", []),
("reference-asset-verification-receipt",
_HERE / "reference-asset-verification-receipt.schema.json", []),
("acceptance-provenance-v4", _HERE / "acceptance-provenance-v4.schema.json", []),
("actor-selection", _HERE / "actor-selection.schema.json", []),
("judge-result", _HERE / "judge-result.schema.json", []),
("visual-target-set", _HERE / "visual-target-set.schema.json", []),
("target-resolution", _HERE / "target-resolution.schema.json", []),
("cost-reservation", _HERE / "cost-reservation.schema.json", []),
("playtest-evidence-v3", _HERE / "playtest-evidence-v3.schema.json", []),
("single-roll-fact-v3", _HERE / "single-roll-fact-v3.schema.json", []),
("interaction-profile-registry", _HERE / "interaction-profile-registry.schema.json",
[_HERE / "interaction-profiles.v1.json"]),
("interaction-profile-registry-v2", _HERE / "interaction-profile-registry-v2.schema.json",
[_HERE / "interaction-profiles.v2.json"]),
("match3-producer-preflight", _HERE / "match3-producer-preflight.schema.json", []),
("interaction-binding", _HERE / "interaction-binding.schema.json", []),
("proof-obligation-resolution", _HERE / "proof-obligation-resolution.schema.json", []),
("single-roll-proof-v1", _HERE / "single-roll-proof-v1.schema.json",
[_HERE / "samples/proof-resolution-bundle/fixtures/legacy-single-roll-proof.json"]),
("single-roll-proof-v2", _HERE / "single-roll-proof-v2.schema.json", []),
("single-roll-proof-v3", _HERE / "single-roll-proof-v3.schema.json", [
_HERE / "samples/match3-action-bundle/fixtures/single-roll-proof-v3.json",
]),
("judge-package", _HERE / "judge-package.schema.json", []),
("match3-board-projection", _HERE / "match3-board-projection.schema.json", []),
("match3-swap-set", _HERE / "match3-swap-set.schema.json", []),
("actor-selection-v2", _HERE / "actor-selection-v2.schema.json", []),
("actor-view-v3", _HERE / "actor-view-v3.schema.json", []),
("actor-view-v4", _HERE / "actor-view-v4.schema.json", []),
("actor-selection-context", _HERE / "actor-selection-context.schema.json", []),
("actor-protocol-bundle", _HERE / "actor-protocol-bundle.schema.json", []),
("actor-selection-attempt", _HERE / "actor-selection-attempt.schema.json", []),
("match3-pair-resolution", _HERE / "match3-pair-resolution.schema.json", []),
("grouped-action-ref", _HERE / "grouped-action-ref.schema.json", []),
("board-identity-check", _HERE / "board-identity-check.schema.json", []),
("match3-action-group", _HERE / "match3-action-group.schema.json", []),
("match3-roll-interaction-audit-unit", _HERE / "match3-roll-interaction-audit.schema.json", []),
("match3-audio-audit", _HERE / "match3-audio-audit.schema.json", []),
("match3-roll-interaction-audit-v2", _HERE / "match3-roll-interaction-audit-v2.schema.json", [
_HERE / "samples/match3-action-bundle/fixtures/match3-roll-interaction-audit-v2.json",
]),
("build-manifest", _HERE / "build-manifest.schema.json", [
_HERE / "samples/match3-action-bundle/fixtures/specialty-build-manifest.json",
]),
("match3-visual-transaction", _HERE / "match3-visual-transaction.schema.json", [
_HERE / "samples/match3-action-bundle/fixtures/specialty-visual-transaction.json",
]),
("match3-visual-audit", _HERE / "match3-visual-audit.schema.json", [
_HERE / "samples/match3-action-bundle/fixtures/specialty-visual-audit.json",
]),
("match3-effect-audit", _HERE / "match3-effect-audit.schema.json", [
_HERE / "samples/match3-action-bundle/fixtures/specialty-effect-audit.json",
]),
("match3-effect-render-receipt", _HERE / "match3-effect-render-receipt.schema.json", [
_HERE / "samples/match3-action-bundle/fixtures/specialty-effect-receipt.json",
]),
("input-lock-probe", _HERE / "input-lock-probe.schema.json", [
_HERE / "samples/match3-action-bundle/fixtures/specialty-input-lock.json",
]),
("match3-filmstrip-analyzer", _HERE / "match3-filmstrip-analyzer.schema.json", [
_HERE / "samples/match3-action-bundle/fixtures/specialty-filmstrip.json",
]),
]
total_pass = total_block = 0
all_ok = True
for name, schema_path, canonical_valids in contracts:
schema = _load(schema_path)
base = samples_root / name
valid_dir, invalid_dir = base / "valid", base / "invalid"
valids = list(canonical_valids)
if valid_dir.is_dir():
valids += sorted(valid_dir.glob("*.json"))
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
bundle_base = samples_root / "proof-resolution-bundle"
bundle_valids = sorted((bundle_base / "valid").glob("[0-9][0-9]-*.json"))
bundle_invalids = sorted((bundle_base / "invalid").glob("[0-9][0-9]-*.json"))
print("\n══ proof-resolution-bundle跨文件闭包══")
bundle_pass = bundle_block = 0
for manifest in bundle_valids:
errs = _validate_proof_resolution_bundle(manifest)
if errs:
all_ok = False
print(f" ✗ 正样本本应过却被拦:{manifest.name}")
for error in errs[:4]:
print(f" {error}")
else:
bundle_pass += 1
print(f" ✓ 正样本过:{manifest.name}")
for manifest in bundle_invalids:
errs = _validate_proof_resolution_bundle(manifest)
if errs:
descriptor = _load(manifest)
expected_pattern = descriptor.get("expectedErrorPattern")
matched = (
expected_pattern is None
or any(re.search(expected_pattern, error) for error in errs)
)
if matched:
bundle_block += 1
print(f" ✓ 负样本拦:{manifest.name} —— {errs[0]}")
else:
all_ok = False
print(
f" ✗ 负样本虽被拦但未命中预期错误 {expected_pattern!r}"
f"{manifest.name} —— {errs[0]}",
)
else:
all_ok = False
print(f" ✗ 负样本本应拦却放过:{manifest.name}")
print(
f" 小计:正样本 {bundle_pass}/{len(bundle_valids)} 过,"
f"负样本 {bundle_block}/{len(bundle_invalids)}",
)
total_pass += bundle_pass
total_block += bundle_block
for title, directory, validator in (
("match3-action-bundle双 tap 跨文件闭包)", "match3-action-bundle", _validate_match3_action_bundle),
("match3-roll-interaction-audit恢复预算跨文件闭包", "match3-roll-interaction-audit", _validate_match3_roll_audit),
("match3-specialty-proof视觉音频跨文件闭包", "match3-specialty-proof", _validate_match3_specialty_proof_bundle),
):
base = samples_root / directory
valids = sorted((base / "valid").glob("[0-9][0-9]-*.json"))
invalids = sorted((base / "invalid").glob("[0-9][0-9]-*.json"))
print(f"\n══ {title} ══")
valid_count = invalid_count = 0
for sample in valids:
errs = validator(sample)
if errs:
all_ok = False
print(f" ✗ 正样本本应过却被拦:{sample.name}")
for error in errs[:4]:
print(f" {error}")
else:
valid_count += 1
print(f" ✓ 正样本过:{sample.name}")
for sample in invalids:
errs = validator(sample)
descriptor = _load(sample)
pattern = descriptor.get("expectedErrorPattern") if isinstance(descriptor, dict) else None
if errs and (pattern is None or any(re.search(pattern, error) for error in errs)):
invalid_count += 1
print(f" ✓ 负样本拦:{sample.name} —— {errs[0]}")
elif errs:
all_ok = False
print(f" ✗ 负样本未命中预期错误 {pattern!r}{sample.name} —— {errs[0]}")
else:
all_ok = False
print(f" ✗ 负样本本应拦却放过:{sample.name}")
print(f" 小计:正样本 {valid_count}/{len(valids)} 过,负样本 {invalid_count}/{len(invalids)}")
total_pass += valid_count
total_block += invalid_count
print(f"\n══ 总计:正样本 {total_pass} 过 / 负样本 {total_block} 拦 —— "
f"{'全部符合预期' if all_ok else '存在未达预期项(见上 ✗)'}")
return 0 if all_ok else 1
def main(argv: list) -> int:
try:
argv = _configure_trace(argv)
except ValueError as exc:
print(f"FAIL {exc}")
return 2
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 and argv[1] == "--proof-package-dir":
errors = _validate_proof_resolution_bundle(Path(argv[2]).resolve())
if errors:
for error in errors:
print(f"FAIL {error}")
return 1
print(f"PASS {Path(argv[2]).name}")
return 0
if len(argv) == 3 and argv[1] in (
"--match3-action-bundle", "--match3-roll-audit", "--match3-specialty-proof",
):
validator = {
"--match3-action-bundle": _validate_match3_action_bundle,
"--match3-roll-audit": _validate_match3_roll_audit,
"--match3-specialty-proof": _validate_match3_specialty_proof_bundle,
}[argv[1]]
target = Path(argv[2]).resolve()
errors = validator(target)
if errors:
for error in errors:
print(f"FAIL {error}")
return 1
print(f"PASS {target.name}")
return 0
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))