zizi 091b66a9bb 重构: 收敛 Agent/Skill 运行时与创作质量闭环
将角色与 Skill 从 .claude 迁入 .agent,移除 Claude CLI 运行时并接入固定 Opus 角色 profile、完整 schema、预算 deadline、raw 与回执证据链。

同步拆分 Skill 职责、复利 lesson、Gate 回放、Dashboard 人审入口、数据库登记和机械门禁;候选设计正文不包含在本提交中。
2026-08-22 02:12:32 +08:00

613 lines
28 KiB
Python

#!/usr/bin/env python3
"""回放快照的 fail-closed 校验。
输入是冻结脚本产出的结构化 manifest 和运行时登记信息;本模块不访问数据库、不调用模型。
"""
from __future__ import annotations
import argparse
import json
import re
from datetime import datetime, timezone
from pathlib import Path
from typing import Any, Mapping, Sequence
from build_snapshot import normalize_chapter, normalize_chapter_range
STATUS_READY = "ready"
STATUS_BLOCKED_AUTHORIZATION = "blocked_authorization"
STATUS_INVALID_SNAPSHOT = "invalid_snapshot"
STATUS_INVALID_ARM_DIFF = "invalid_arm_diff"
STATUS_TARGET_SOURCE_FORBIDDEN = "target_source_forbidden"
STATUS_SCHEMA_INVALID = "schema_invalid"
FORBIDDEN_SOURCE_STATUSES = frozenset(
{"revoked", "delisted", "recalled", "blocked", "owner_missing", "unauthorized"}
)
ALLOWED_SOURCE_STATUSES = frozenset({"active", "approved", "authorized", "licensed"})
ALLOWED_COPYRIGHT_STATUSES = frozenset({"licensed", "public_domain", "research_only"})
FORBIDDEN_COPYRIGHT_STATUSES = frozenset(
{"unauthorized", "unlicensed", "revoked", "expired", "blocked"}
)
SOURCE_HASH_PATTERN = re.compile(r"^sha256:[0-9a-f]{64}$")
SOURCE_VERSION_PATTERN = re.compile(r"^raw-file-v1:sha256:[0-9a-f]{64}$")
SANITIZED_FIXTURE_VERSION_PATTERN = re.compile(
r"^sanitized-fixture-v1:sha256:[0-9a-f]{64}$"
)
CARD_KEYS = frozenset(
{
"arm",
"card",
"cards",
"cardInjection",
"cardInjectionSha256",
"cardInjectionCount",
"cardSection",
"cardSections",
"cardManifest",
"cardIds",
"cardSourceIds",
"cardStrategy",
"l2-card",
"l2-placebo",
"card_injection",
"card_injection_sha256",
"card_injection_count",
"card_section",
"card_sections",
"card_manifest",
"card_ids",
"card_source_ids",
"card_strategy",
"l2_card",
"l2_placebo",
}
)
NORMALIZED_CARD_KEYS = frozenset(key.lower() for key in CARD_KEYS)
REQUIRED_CANDIDATE_FIELDS = (
"targetChapter",
"chapterGoal",
"keyEvents",
"entities",
"foreshadowing",
"stateChanges",
"hook",
"unknowns",
"assumptions",
)
ALLOWED_CANDIDATE_FIELDS = frozenset((*REQUIRED_CANDIDATE_FIELDS, "sourceRefs"))
EVENT_FIELDS = {
"id": str,
"order": int,
"event": str,
"participants": list,
"trigger": str,
"resultDirection": str,
}
ENTITY_FIELDS = {"name": str, "type": str, "role": str}
FORESHADOWING_FIELDS = {"action": str, "subject": str, "evidence": str}
FORBIDDEN_CANDIDATE_FIELDS = frozenset(
{"body", "raw", "rawText", "content", "正文", "原文", "正文全文", "完整目标细纲"}
)
def _result(status: str, errors: Sequence[str] = (), warnings: Sequence[str] = ()) -> dict[str, Any]:
return {"status": status, "ok": status == STATUS_READY, "errors": list(errors), "warnings": list(warnings)}
def _field(value: Mapping[str, Any], *keys: str) -> Any:
for key in keys:
if key in value:
return value[key]
return None
def _parse_utc_time(value: Any, field: str) -> tuple[datetime | None, str | None]:
"""解析带时区的 ISO-8601 时间;格式含糊时按失败关闭处理。"""
if not isinstance(value, str) or not value.strip():
return None, f"授权快照 {field} 不是有效时间"
try:
parsed = datetime.fromisoformat(value.strip().replace("Z", "+00:00"))
except ValueError:
return None, f"授权快照 {field} 不是有效 ISO-8601 时间"
if parsed.tzinfo is None:
return None, f"授权快照 {field} 缺少时区"
return parsed.astimezone(timezone.utc), None
def check_authorization(authorization: Mapping[str, Any] | None) -> dict[str, Any]:
"""授权信息缺失、用途不符或来源进入危险状态时关闭评测。"""
if not isinstance(authorization, Mapping):
return _result(STATUS_BLOCKED_AUTHORIZATION, ["缺少不可变授权快照"])
snapshot = _field(authorization, "authorizationSnapshot", "authorization_snapshot")
if not isinstance(snapshot, Mapping) or not snapshot:
return _result(STATUS_BLOCKED_AUTHORIZATION, ["授权快照为空"])
snapshot_required = (
"id",
"version",
"immutable",
"sourceHash",
"sourceVersion",
"sourceStatus",
"copyrightStatus",
"authorizationBasis",
"allowedPurpose",
"checkedAt",
)
missing_snapshot = [field for field in snapshot_required if not snapshot.get(field)]
if missing_snapshot:
return _result(
STATUS_BLOCKED_AUTHORIZATION,
[f"授权快照缺少字段: {','.join(missing_snapshot)}"],
)
if snapshot.get("immutable") is not True:
return _result(STATUS_BLOCKED_AUTHORIZATION, ["授权快照不是不可变快照"])
if not snapshot.get("expiresAt") and not snapshot.get("revalidationAt"):
return _result(STATUS_BLOCKED_AUTHORIZATION, ["授权快照缺少过期或重验时间"])
now = datetime.now(timezone.utc)
checked_at, error = _parse_utc_time(snapshot.get("checkedAt"), "checkedAt")
if error:
return _result(STATUS_BLOCKED_AUTHORIZATION, [error])
if checked_at is not None and checked_at > now:
return _result(STATUS_BLOCKED_AUTHORIZATION, ["授权快照 checkedAt 晚于当前时间"])
for field in ("expiresAt", "revalidationAt"):
raw_time = snapshot.get(field)
if not raw_time:
continue
deadline, error = _parse_utc_time(raw_time, field)
if error:
return _result(STATUS_BLOCKED_AUTHORIZATION, [error])
if deadline is not None and deadline <= now:
return _result(STATUS_BLOCKED_AUTHORIZATION, [f"授权快照 {field} 已到期"])
source_status = str(_field(authorization, "sourceStatus", "source_status") or "").lower()
if not source_status:
return _result(STATUS_BLOCKED_AUTHORIZATION, ["缺少 sourceStatus"])
if source_status in FORBIDDEN_SOURCE_STATUSES:
return _result(STATUS_BLOCKED_AUTHORIZATION, [f"来源状态禁止评测: {source_status}"])
if source_status not in ALLOWED_SOURCE_STATUSES:
return _result(STATUS_BLOCKED_AUTHORIZATION, [f"来源状态未登记,拒绝评测: {source_status}"])
copyright_status = str(
_field(authorization, "copyrightStatus", "copyright_status") or ""
).lower()
if not copyright_status:
return _result(STATUS_BLOCKED_AUTHORIZATION, ["缺少 copyrightStatus"])
if copyright_status in FORBIDDEN_COPYRIGHT_STATUSES:
return _result(STATUS_BLOCKED_AUTHORIZATION, [f"版权状态禁止评测: {copyright_status}"])
if copyright_status not in ALLOWED_COPYRIGHT_STATUSES:
return _result(STATUS_BLOCKED_AUTHORIZATION, [f"版权状态未登记,拒绝评测: {copyright_status}"])
if str(snapshot["copyrightStatus"]).lower() != copyright_status:
return _result(STATUS_BLOCKED_AUTHORIZATION, ["授权快照 copyrightStatus 不一致"])
source_hash = str(_field(authorization, "sourceHash", "source_hash") or "")
if not SOURCE_HASH_PATTERN.fullmatch(source_hash):
return _result(STATUS_BLOCKED_AUTHORIZATION, ["sourceHash 必须是 sha256:<64位小写十六进制>"])
if str(snapshot.get("sourceHash")) != source_hash:
return _result(STATUS_BLOCKED_AUTHORIZATION, ["授权快照 sourceHash 不一致"])
authorization_basis = str(snapshot.get("authorizationBasis") or "")
source_version = str(_field(authorization, "sourceVersion", "source_version") or "")
if authorization_basis == "sanitized_contract_fixture":
if not SANITIZED_FIXTURE_VERSION_PATTERN.fullmatch(source_version):
return _result(
STATUS_BLOCKED_AUTHORIZATION,
["脱敏夹具 sourceVersion 必须是 sanitized-fixture-v1:sha256:<64位小写十六进制>"],
)
if source_version != f"sanitized-fixture-v1:{source_hash}":
return _result(STATUS_BLOCKED_AUTHORIZATION, ["脱敏夹具 sourceVersion 与 sourceHash 不一致"])
else:
if not SOURCE_VERSION_PATTERN.fullmatch(source_version):
return _result(
STATUS_BLOCKED_AUTHORIZATION,
["sourceVersion 必须是 raw-file-v1:sha256:<64位小写十六进制>"],
)
if source_version != f"raw-file-v1:{source_hash}":
return _result(STATUS_BLOCKED_AUTHORIZATION, ["sourceVersion 与 sourceHash 不一致"])
if str(snapshot.get("sourceVersion")) != source_version:
return _result(STATUS_BLOCKED_AUTHORIZATION, ["授权快照 sourceVersion 不一致"])
if str(snapshot["sourceStatus"]).lower() != source_status:
return _result(STATUS_BLOCKED_AUTHORIZATION, ["授权快照 sourceStatus 不一致"])
allowed = _field(authorization, "allowedPurpose", "allowed_purpose")
if isinstance(allowed, str):
allowed = [allowed]
if not isinstance(allowed, Sequence) or isinstance(allowed, (str, bytes)):
return _result(STATUS_BLOCKED_AUTHORIZATION, ["allowedPurpose 不是用途列表"])
if "offline_evaluation" not in allowed:
return _result(STATUS_BLOCKED_AUTHORIZATION, ["allowedPurpose 不包含 offline_evaluation"])
snapshot_allowed = snapshot.get("allowedPurpose", snapshot.get("allowed_purpose"))
if not isinstance(snapshot_allowed, Sequence) or isinstance(snapshot_allowed, (str, bytes)):
return _result(STATUS_BLOCKED_AUTHORIZATION, ["授权快照缺少 allowedPurpose"])
if "offline_evaluation" not in snapshot_allowed:
return _result(STATUS_BLOCKED_AUTHORIZATION, ["授权快照用途不包含 offline_evaluation"])
if set(snapshot_allowed) != set(allowed):
return _result(STATUS_BLOCKED_AUTHORIZATION, ["授权快照用途与运行用途不一致"])
forbidden = _field(authorization, "forbiddenPurpose", "forbidden_purpose")
if not isinstance(forbidden, list):
return _result(STATUS_BLOCKED_AUTHORIZATION, ["forbiddenPurpose 不是用途列表"])
snapshot_forbidden = snapshot.get("forbiddenPurpose", snapshot.get("forbidden_purpose"))
if not isinstance(snapshot_forbidden, list):
return _result(STATUS_BLOCKED_AUTHORIZATION, ["授权快照 forbiddenPurpose 不是用途列表"])
if set(snapshot_forbidden) != set(forbidden):
return _result(STATUS_BLOCKED_AUTHORIZATION, ["授权快照禁止用途与运行禁止用途不一致"])
if "offline_evaluation" in forbidden:
return _result(STATUS_BLOCKED_AUTHORIZATION, ["forbiddenPurpose 禁止 offline_evaluation"])
if set(allowed) & set(forbidden):
return _result(STATUS_BLOCKED_AUTHORIZATION, ["allowedPurpose 与 forbiddenPurpose 存在冲突"])
if authorization_basis == "user_authorization":
if copyright_status != "research_only":
return _result(STATUS_BLOCKED_AUTHORIZATION, ["用户授权不能登记为 licensed"])
if list(allowed) != ["offline_evaluation"]:
return _result(STATUS_BLOCKED_AUTHORIZATION, ["用户授权仅允许 offline_evaluation"])
if authorization_basis == "sanitized_contract_fixture":
if copyright_status != "research_only":
return _result(STATUS_BLOCKED_AUTHORIZATION, ["脱敏合同夹具只能登记为 research_only"])
if list(allowed) != ["offline_evaluation"]:
return _result(STATUS_BLOCKED_AUTHORIZATION, ["脱敏合同夹具仅允许 offline_evaluation"])
return _result(STATUS_READY)
def check_target_sources(target_chapter: int, sources: Sequence[Mapping[str, Any]]) -> dict[str, Any]:
"""规划器侧来源不得包含目标章或更晚章号。"""
target = normalize_chapter(target_chapter)
if target is None:
return _result(STATUS_INVALID_SNAPSHOT, ["目标章号无效"])
if not isinstance(sources, Sequence) or isinstance(sources, (str, bytes)) or not sources:
return _result(STATUS_INVALID_SNAPSHOT, ["缺少规划来源"])
errors: list[str] = []
for index, source in enumerate(sources):
if not isinstance(source, Mapping):
errors.append(f"source[{index}] 不是对象")
continue
if not source.get("sourceId") or not source.get("sourceVersion"):
errors.append(f"source[{index}] 缺少 sourceId/sourceVersion")
continue
value = _field(source, "chapter", "chapterNo", "chapter_no", "章", "章号")
bounds = normalize_chapter_range(value)
if bounds is None:
value = _field(
source,
"chapterRange",
"chapter_range",
"range",
"章节范围",
)
bounds = normalize_chapter_range(value)
if bounds is None and "from_order" in source:
value = f"{source.get('from_order')}-{source.get('to_order')}"
bounds = normalize_chapter_range(value)
if bounds is None:
scope = str(source.get("scope") or "chapter").lower()
if scope not in {"work", "authorization", "metadata"}:
errors.append(f"source[{index}] 缺少可验证绝对章号或完整区间")
continue
if bounds is not None and bounds[1] >= target:
errors.append(f"source[{index}] 包含目标章或未来章: {bounds[0]}-{bounds[1]}")
return _result(STATUS_TARGET_SOURCE_FORBIDDEN if errors else STATUS_READY, errors)
def _without_card_fields(value: Any) -> Any:
"""去掉三臂允许变化的 arm/card 分区,保留所有公共输入用于字节级比较。"""
if isinstance(value, list):
return [_without_card_fields(item) for item in value]
if not isinstance(value, Mapping):
return value
result: dict[str, Any] = {}
for key, item in value.items():
key_text = str(key)
if key_text.lower() in NORMALIZED_CARD_KEYS or key_text in {"卡", "知识卡", "卡片"}:
continue
result[key_text] = _without_card_fields(item)
return result
def _canonical_hash(value: Any) -> str:
"""用 JSON 类型保真的规范序列化比较公共区。"""
import hashlib
encoded = json.dumps(value, ensure_ascii=False, sort_keys=True, separators=(",", ":")).encode("utf-8")
return hashlib.sha256(encoded).hexdigest()
def _check_arm_semantics(name: str, manifest: Mapping[str, Any]) -> list[str]:
"""校验生产三臂的无卡/正确卡/placebo 语义。"""
count = manifest.get("cardInjectionCount", 0)
source_ids = manifest.get("cardSourceIds", [])
strategy = str(manifest.get("cardStrategy") or "none")
has_payload = bool(manifest.get("cardInjection"))
if name == "outline_only":
if has_payload or count not in (0, None) or source_ids or strategy not in {"none", ""}:
return ["outline_only 必须不含卡注入"]
return []
if name not in {"outline_plus_cards", "outline_plus_placebo_cards"}:
return [f"未登记评测臂: {name}"]
if has_payload or not isinstance(count, int) or count <= 0 or not source_ids:
return [f"{name} 必须包含带来源的卡注入元数据"]
expected_strategy = "correct" if name == "outline_plus_cards" else "placebo"
if strategy != expected_strategy:
return [f"{name} 的 cardStrategy 必须是 {expected_strategy}"]
return []
def check_arm_manifests(
manifests: Mapping[str, Mapping[str, Any]],
required_arms: Sequence[str] | None = None,
*,
as_of_chapter: int | None = None,
target_chapter: int | None = None,
enforce_semantics: bool = False,
smoke: bool = False,
) -> dict[str, Any]:
"""确保三臂除卡注入区外完全一致。"""
required = set(required_arms or ("outline_only", "outline_plus_cards", "outline_plus_placebo_cards"))
if not smoke and required != {"outline_only", "outline_plus_cards", "outline_plus_placebo_cards"}:
return _result(STATUS_INVALID_ARM_DIFF, ["生产回放必须使用完整三臂;双臂只允许 smoke"])
actual = set(manifests)
missing = sorted(required - actual)
if missing:
return _result(STATUS_INVALID_ARM_DIFF, [f"缺少评测臂: {','.join(missing)}"])
unexpected = sorted(actual - required)
if unexpected:
return _result(STATUS_INVALID_ARM_DIFF, [f"存在未登记评测臂: {','.join(unexpected)}"])
if any(not isinstance(manifest, Mapping) for manifest in manifests.values()):
return _result(STATUS_INVALID_ARM_DIFF, ["评测臂 manifest 必须是对象"])
if as_of_chapter is not None or target_chapter is not None:
expected_as_of = normalize_chapter(as_of_chapter)
expected_target = normalize_chapter(target_chapter)
if expected_as_of is None or expected_target != expected_as_of + 1:
return _result(STATUS_INVALID_SNAPSHOT, ["as_of/target 不是连续章节"])
for name in sorted(required):
manifest = manifests[name]
if normalize_chapter(manifest.get("asOfChapter")) != expected_as_of:
return _result(STATUS_INVALID_SNAPSHOT, [f"{name} 的 asOfChapter 不一致"])
if normalize_chapter(manifest.get("targetChapter")) != expected_target:
return _result(STATUS_INVALID_SNAPSHOT, [f"{name} 的 targetChapter 不一致"])
if enforce_semantics:
semantic_errors = [
f"{name}: {error}"
for name in sorted(required)
for error in _check_arm_semantics(name, manifests[name])
]
if semantic_errors:
return _result(STATUS_INVALID_ARM_DIFF, semantic_errors)
names = sorted(required)
baseline = _without_card_fields(manifests[names[0]])
baseline_hash = _canonical_hash(baseline)
differences = [
name for name in names[1:]
if _canonical_hash(_without_card_fields(manifests[name])) != baseline_hash
]
if differences:
return _result(STATUS_INVALID_ARM_DIFF, [f"公共输入区不一致: {','.join(differences)}"])
return _result(STATUS_READY)
def check_candidate_output(
candidate: Mapping[str, Any],
target_chapter: int,
source_catalog: Sequence[Mapping[str, Any]] | None = None,
) -> dict[str, Any]:
"""校验细纲候选的结构边界,不判断内容是否命中标准答案。"""
if not isinstance(candidate, Mapping):
return _result(STATUS_SCHEMA_INVALID, ["候选不是对象"])
expected_target = normalize_chapter(target_chapter)
if expected_target is None:
return _result(STATUS_INVALID_SNAPSHOT, ["目标章号无效"])
errors = [f"缺少字段: {field}" for field in REQUIRED_CANDIDATE_FIELDS if field not in candidate]
unexpected = sorted(set(candidate) - ALLOWED_CANDIDATE_FIELDS)
if unexpected:
errors.append(f"候选包含未登记字段: {','.join(unexpected)}")
forbidden = sorted(set(candidate) & FORBIDDEN_CANDIDATE_FIELDS)
if forbidden:
errors.append(f"候选包含正文/原文字段: {','.join(forbidden)}")
for field in ("keyEvents", "entities", "foreshadowing", "stateChanges", "unknowns", "assumptions"):
if field in candidate and not isinstance(candidate[field], list):
errors.append(f"字段必须是数组: {field}")
for field in ("chapterGoal", "hook"):
if field in candidate and not isinstance(candidate[field], str):
errors.append(f"字段必须是字符串: {field}")
events = candidate.get("keyEvents")
if isinstance(events, list):
if not events:
errors.append("keyEvents 不能为空")
event_ids = [item.get("id") for item in events if isinstance(item, Mapping) and item.get("id")]
if len(event_ids) != len(set(event_ids)):
errors.append("keyEvents 包含重复事件 id")
if any(not isinstance(item, Mapping) for item in events):
errors.append("keyEvents 每项必须是对象")
if any(isinstance(item, Mapping) and not item.get("id") for item in events):
errors.append("keyEvents 每项必须有 id")
for index, event in enumerate(events):
if not isinstance(event, Mapping):
continue
unexpected_event_fields = sorted(set(event) - set(EVENT_FIELDS))
if unexpected_event_fields:
errors.append(f"keyEvents[{index}] 包含未登记字段: {','.join(unexpected_event_fields)}")
for field, expected_type in EVENT_FIELDS.items():
if field not in event:
errors.append(f"keyEvents[{index}] 缺少字段: {field}")
elif expected_type is int and (isinstance(event[field], bool) or not isinstance(event[field], int)):
errors.append(f"keyEvents[{index}].{field} 必须是整数")
elif expected_type is not int and not isinstance(event[field], expected_type):
errors.append(f"keyEvents[{index}].{field} 类型错误")
event_orders = [
event.get("order")
for event in events
if isinstance(event, Mapping)
and isinstance(event.get("order"), int)
and not isinstance(event.get("order"), bool)
]
if len(event_orders) == len(events) and event_orders != list(range(1, len(events) + 1)):
errors.append("keyEvents.order 必须唯一且从 1 开始连续严格递增")
entities = candidate.get("entities")
if isinstance(entities, list):
for index, entity in enumerate(entities):
if not isinstance(entity, Mapping):
errors.append(f"entities[{index}] 必须是对象")
continue
for field, expected_type in ENTITY_FIELDS.items():
if field not in entity:
errors.append(f"entities[{index}] 缺少字段: {field}")
elif not isinstance(entity[field], expected_type):
errors.append(f"entities[{index}].{field} 类型错误")
unexpected_entity_fields = sorted(set(entity) - set(ENTITY_FIELDS))
if unexpected_entity_fields:
errors.append(f"entities[{index}] 包含未登记字段: {','.join(unexpected_entity_fields)}")
foreshadowing = candidate.get("foreshadowing")
if isinstance(foreshadowing, list):
for index, item in enumerate(foreshadowing):
if not isinstance(item, Mapping):
errors.append(f"foreshadowing[{index}] 必须是对象")
continue
for field, expected_type in FORESHADOWING_FIELDS.items():
if field not in item:
errors.append(f"foreshadowing[{index}] 缺少字段: {field}")
elif not isinstance(item[field], expected_type):
errors.append(f"foreshadowing[{index}].{field} 类型错误")
unexpected_foreshadowing_fields = sorted(set(item) - set(FORESHADOWING_FIELDS))
if unexpected_foreshadowing_fields:
errors.append(
f"foreshadowing[{index}] 包含未登记字段: {','.join(unexpected_foreshadowing_fields)}"
)
for field in ("stateChanges", "unknowns", "assumptions"):
values = candidate.get(field)
if isinstance(values, list) and any(not isinstance(item, str) for item in values):
errors.append(f"{field} 每项必须是字符串")
source_errors: list[str] = []
source_refs = candidate.get("sourceRefs")
if source_refs is not None:
if not isinstance(source_refs, list):
errors.append("sourceRefs 必须是数组")
else:
if source_catalog is None:
return _result(STATUS_SCHEMA_INVALID, ["sourceRefs 校验缺少冻结来源目录"])
catalog = {
str(source.get("sourceId")): source
for source in source_catalog
if isinstance(source, Mapping) and source.get("sourceId")
}
for index, reference in enumerate(source_refs):
if not isinstance(reference, str):
errors.append(f"sourceRefs[{index}] 必须是 sourceId 字符串")
continue
source = catalog.get(reference)
if source is None:
errors.append(f"sourceRefs[{index}] 未登记来源")
continue
source_value = _field(
source,
"chapter",
"chapterNo",
"chapter_no",
"章",
"章号",
"chapterRange",
"chapter_range",
"range",
)
bounds = normalize_chapter_range(source_value)
if bounds is None and "from_order" in source:
bounds = normalize_chapter_range(f"{source.get('from_order')}-{source.get('to_order')}")
if bounds is None:
scope = str(source.get("scope") or "chapter").lower()
if scope not in {"work", "authorization", "metadata"}:
source_errors.append(f"sourceRefs[{index}] 缺少可验证绝对章号或完整区间")
elif bounds[1] >= expected_target:
source_errors.append(
f"sourceRefs[{index}] 包含目标章或未来章: {bounds[0]}-{bounds[1]}"
)
actual_target = normalize_chapter(candidate.get("targetChapter"))
if actual_target != expected_target:
errors.append(f"候选目标章错误: expected={expected_target}, actual={actual_target}")
if source_errors:
return _result(STATUS_TARGET_SOURCE_FORBIDDEN, source_errors)
if errors:
return _result(STATUS_SCHEMA_INVALID, errors)
return _result(STATUS_READY)
def check_replay(
*,
authorization: Mapping[str, Any] | None,
as_of_chapter: int,
target_chapter: int,
planner_sources: Sequence[Mapping[str, Any]],
arm_manifests: Mapping[str, Mapping[str, Any]],
required_arms: Sequence[str] | None = None,
smoke: bool = False,
) -> dict[str, Any]:
"""执行回放前置门,任何一项失败都不允许进入模型调用。"""
as_of = normalize_chapter(as_of_chapter)
target = normalize_chapter(target_chapter)
if as_of is None or target != as_of + 1:
return _result(STATUS_INVALID_SNAPSHOT, ["as_of/target 不是连续章节"])
checks = [
check_authorization(authorization),
check_target_sources(target_chapter, planner_sources),
check_arm_manifests(
arm_manifests,
required_arms,
as_of_chapter=as_of,
target_chapter=target,
enforce_semantics=True,
smoke=smoke,
),
]
failures = [check for check in checks if not check["ok"]]
if failures:
return _result(
failures[0]["status"],
[error for check in failures for error in check["errors"]],
[warning for check in failures for warning in check["warnings"]],
)
return _result(STATUS_READY)
def _parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(description="校验回放快照和三臂输入")
parser.add_argument("--authorization", type=Path, required=True)
parser.add_argument("--sources", type=Path, required=True)
parser.add_argument("--arms", type=Path, required=True)
parser.add_argument("--as-of-chapter", type=int, required=True)
parser.add_argument("--target-chapter", type=int, required=True)
parser.add_argument("--output", type=Path, required=True)
return parser.parse_args()
def main() -> int:
args = _parse_args()
authorization = json.loads(args.authorization.read_text(encoding="utf-8"))
sources = json.loads(args.sources.read_text(encoding="utf-8"))
arms = json.loads(args.arms.read_text(encoding="utf-8"))
result = check_replay(
authorization=authorization,
as_of_chapter=args.as_of_chapter,
target_chapter=args.target_chapter,
planner_sources=sources,
arm_manifests=arms,
)
args.output.write_text(json.dumps(result, ensure_ascii=False, sort_keys=True) + "\n", encoding="utf-8")
return 0 if result["ok"] else 2
if __name__ == "__main__":
raise SystemExit(main())