591 lines
21 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
"""回放评测的冻结快照纯函数。
本模块只处理内存中的结构化数据,不访问数据库、不调用模型、不写正式数据。
它把「截至第 N 章」变成可重复、可审计的来源清单,避免把未来事实误当成卡的价值。
"""
from __future__ import annotations
import argparse
import copy
import hashlib
import json
import re
from pathlib import Path
from typing import Any, Iterable, Mapping, Sequence
class SnapshotError(ValueError):
"""快照输入违反冻结或全文留存边界。"""
TERMINAL_FIELDS = frozenset(
{
"final_summary",
"terminal_summary",
"final_state",
"finalState",
"current_state",
"currentState",
"future_arc",
"futureArc",
"future_plan",
"futurePlan",
"event_result",
"eventResult",
"relationship_plan",
"relationshipPlan",
"growth_arc",
"growthArc",
"terminalResult",
"成长弧线",
"当前态",
"终态摘要",
"终局状态",
"未来弧线",
"未来计划",
"事件结果",
"关系计划",
}
)
SNAPSHOT_TOP_LEVEL_KEYS = frozenset(
{
"milestones",
"outlineWindows",
"cards",
"chapters",
"events",
"relations",
"relationships",
"stateRecords",
"state_records",
"facts",
}
)
CHAPTER_SCOPED_COLLECTIONS = frozenset(
{
"milestones",
"outlineWindows",
"chapters",
"events",
"relations",
"relationships",
"stateRecords",
"state_records",
"facts",
}
)
FINAL_REPORT_ALLOWED_FIELDS = frozenset(
{
"status",
"runId",
"referenceWork",
"referenceWorkVersion",
"targetChapter",
"asOfChapter",
"snapshotVersion",
"evaluationSetVersion",
"strategyVersion",
"profile",
"arm",
"armComparison",
"candidateCount",
"scores",
"summary",
"chapterRefs",
"failureClass",
"failureReason",
"hashes",
"warnings",
"stability",
"proxyConfidence",
"goldUncertain",
}
)
FINAL_REPORT_FORBIDDEN_FIELDS = frozenset(
{
"raw",
"payload",
"raw_text",
"body",
"content",
"full_text",
"full_body",
"原文",
"正文",
"正文全文",
"完整目标细纲",
"target_chapter_text",
"prompt",
"response",
"fullPrompt",
"fullResponse",
}
)
_INTEGER_RE = re.compile(r"^\s*(\d+)\s*$")
_RANGE_RE = re.compile(r"^\s*(?:第\s*)?(\d+)\s*(?:-|–|—|~|至|到)\s*(?:第\s*)?(\d+)\s*(?:章)?\s*$")
def _normalized_key(key: Any) -> str:
"""统一英文终态字段的大小写、下划线和短横线写法。"""
return re.sub(r"[_-]", "", str(key)).lower()
TERMINAL_FIELD_NAMES = frozenset(_normalized_key(field) for field in TERMINAL_FIELDS)
def normalize_chapter(value: Any) -> int | None:
"""只接受明确的正整数章号;不把 bool、浮点或模糊文本猜成章号。"""
if isinstance(value, bool):
return None
if isinstance(value, int):
return value if value > 0 else None
if not isinstance(value, str):
return None
matched = _INTEGER_RE.fullmatch(value)
if not matched:
return None
chapter = int(matched.group(1))
return chapter if chapter > 0 else None
def normalize_chapter_range(value: Any) -> tuple[int, int] | None:
"""解析单章或有明确起止上界的章区间。"""
chapter = normalize_chapter(value)
if chapter is not None:
return chapter, chapter
if not isinstance(value, str):
return None
matched = _RANGE_RE.fullmatch(value)
if not matched:
return None
start, end = int(matched.group(1)), int(matched.group(2))
if start <= 0 or end < start:
return None
return start, end
def _chapter_value(record: Mapping[str, Any]) -> Any:
"""兼容实验台和 schema 中的中英文章号键。"""
for key in ("chapter", "chapter_no", "order_no", "章", "章号"):
if key in record:
return record[key]
if "from_order" in record or "to_order" in record:
start = record.get("from_order")
end = record.get("to_order")
if start is not None and end is not None:
return f"{start}-{end}"
return None
def _safe_json(value: Any) -> str:
"""用固定格式序列化,确保相同输入产生相同哈希。"""
return json.dumps(value, ensure_ascii=False, sort_keys=True, separators=(",", ":"))
def sha256_value(value: Any) -> str:
"""返回结构化值的稳定 SHA-256。"""
return hashlib.sha256(_safe_json(value).encode("utf-8")).hexdigest()
def _omitted(index: int, reason: str, record: Any) -> dict[str, Any]:
"""只记录定位和原因,不把被排除的原文复制到回显。"""
return {"index": index, "reason": reason, "recordHash": sha256_value(record)}
def filter_milestones(
milestones: Iterable[Mapping[str, Any]], as_of: int
) -> tuple[list[dict[str, Any]], list[dict[str, Any]]]:
"""保留完整落在 as_of 以前的里程碑,返回(保留项、排除项)。"""
as_of = normalize_chapter(as_of)
if as_of is None:
raise SnapshotError("as_of 必须是正整数章号")
kept: list[dict[str, Any]] = []
omitted: list[dict[str, Any]] = []
for index, item in enumerate(milestones):
if not isinstance(item, Mapping):
omitted.append(_omitted(index, "not_an_object", item))
continue
bounds = normalize_chapter_range(_chapter_value(item))
if bounds is None:
omitted.append(_omitted(index, "missing_or_unbounded_chapter", item))
continue
if bounds[1] > as_of:
omitted.append(_omitted(index, "future_or_crosses_as_of", item))
continue
kept.append(copy.deepcopy(dict(item)))
kept.sort(key=lambda item: normalize_chapter_range(_chapter_value(item)) or (0, 0))
return kept, omitted
def filter_outline_windows(
windows: Iterable[Mapping[str, Any]], as_of: int
) -> tuple[list[dict[str, Any]], list[dict[str, Any]]]:
"""只保留 to_order<=as_of 的完整窗口,并按 from_order 排序。"""
as_of = normalize_chapter(as_of)
if as_of is None:
raise SnapshotError("as_of 必须是正整数章号")
kept: list[dict[str, Any]] = []
omitted: list[dict[str, Any]] = []
for index, item in enumerate(windows):
if not isinstance(item, Mapping):
omitted.append(_omitted(index, "not_an_object", item))
continue
start = normalize_chapter(item.get("from_order"))
end = normalize_chapter(item.get("to_order"))
if start is None or end is None or start > end:
omitted.append(_omitted(index, "missing_or_invalid_window_bounds", item))
continue
if end > as_of:
omitted.append(_omitted(index, "future_or_crosses_as_of", item))
continue
kept.append(copy.deepcopy(dict(item)))
kept.sort(key=lambda item: (normalize_chapter(item["from_order"]) or 0, normalize_chapter(item["to_order"]) or 0))
return kept, omitted
def remove_terminal_fields(
value: Any,
omitted_fields: list[dict[str, Any]] | None = None,
path: str = "$",
) -> Any:
"""递归删除终态字段,并记录裁剪路径供审计。"""
if isinstance(value, list):
return [
remove_terminal_fields(item, omitted_fields, f"{path}[{index}]")
for index, item in enumerate(value)
]
if not isinstance(value, Mapping):
return copy.deepcopy(value)
projected: dict[str, Any] = {}
for key, item in value.items():
key_text = str(key)
if _normalized_key(key_text) in TERMINAL_FIELD_NAMES or key_text in TERMINAL_FIELDS:
if omitted_fields is not None:
omitted_fields.append({"path": f"{path}.{key_text}", "reason": "terminal_field_removed"})
continue
projected[key_text] = remove_terminal_fields(item, omitted_fields, f"{path}.{key_text}")
return projected
_DROP = object()
def _freeze_nested_chapter_records(
value: Any,
as_of: int,
omitted_sources: list[dict[str, Any]],
path: str = "$",
) -> Any:
"""递归冻结所有带绝对章号的嵌套记录,未标明章号的显式记录不猜测。"""
if isinstance(value, Mapping):
chapter_keys = {"chapter", "chapter_no", "order_no", "章", "章号", "from_order", "to_order"}
if chapter_keys.intersection(value):
bounds = normalize_chapter_range(_chapter_value(value))
if bounds is None:
omitted_sources.append({"path": path, "reason": "missing_or_invalid_chapter", "recordHash": sha256_value(value)})
return _DROP
if bounds[1] > as_of:
omitted_sources.append({"path": path, "reason": "future_or_crosses_as_of", "recordHash": sha256_value(value)})
return _DROP
projected: dict[str, Any] = {}
for key, item in value.items():
frozen = _freeze_nested_chapter_records(item, as_of, omitted_sources, f"{path}.{key}")
if frozen is not _DROP:
projected[str(key)] = frozen
return projected
if isinstance(value, list):
projected_list: list[Any] = []
for index, item in enumerate(value):
frozen = _freeze_nested_chapter_records(item, as_of, omitted_sources, f"{path}[{index}]")
if frozen is not _DROP:
projected_list.append(frozen)
return projected_list
return copy.deepcopy(value)
def _validate_final_report(value: Any, path: str = "finalReport", top_level: bool = True) -> None:
"""用顶层白名单和递归禁用字段保护最终报告边界。"""
if isinstance(value, Mapping):
for key, item in value.items():
key_text = str(key)
if key_text in FINAL_REPORT_FORBIDDEN_FIELDS:
raise SnapshotError(f"{path}.{key_text} 不得进入最终报告")
if top_level and key_text not in FINAL_REPORT_ALLOWED_FIELDS:
raise SnapshotError(f"{path}.{key_text} 不是允许的最终报告字段")
_validate_final_report(item, f"{path}.{key_text}", False)
elif isinstance(value, list):
for index, item in enumerate(value):
_validate_final_report(item, f"{path}[{index}]", False)
def _source_record(section: str, value: Any) -> dict[str, Any]:
"""将来源内容收敛成元数据,绝不把 payload 写入 manifest。"""
if isinstance(value, Mapping) and "payload" in value:
source_id = str(value.get("sourceId") or section)
source_version = str(value.get("sourceVersion") or "")
if not source_id or not source_version:
raise SnapshotError(f"来源 {section} 缺少 sourceId/sourceVersion")
payload = value["payload"]
omitted_fields = list(value.get("omittedFields") or [])
omitted_sources = list(value.get("omittedSources") or [])
else:
source_id = section
raise SnapshotError(f"来源 {section} 必须显式提供 sourceId/sourceVersion/payload")
serialized = _safe_json(payload)
return {
"section": section,
"sourceId": source_id,
"sourceVersion": source_version,
"sha256": hashlib.sha256(serialized.encode("utf-8")).hexdigest(),
"charCount": len(serialized),
"omittedFields": omitted_fields,
"omittedSources": omitted_sources,
}
def build_snapshot_manifest(
*,
as_of: int,
target_chapter: int,
snapshot_version: str,
reference_work: Mapping[str, Any],
evaluation_set_version: str,
strategy_version: str,
authorization_snapshot: Mapping[str, Any],
run_permissions: Mapping[str, Any],
arm_config: Mapping[str, Any],
sections: Mapping[str, Any],
omitted_fields: Sequence[Any] | None = None,
omitted_sources: Sequence[Any] | None = None,
final_report: Mapping[str, Any] | None = None,
) -> dict[str, Any]:
"""构造稳定 manifest;最终报告只允许摘要、定位、评分和哈希。"""
normalized_as_of = normalize_chapter(as_of)
if normalized_as_of is None:
raise SnapshotError("as_of 必须是正整数章号")
normalized_target = normalize_chapter(target_chapter)
if normalized_target != normalized_as_of + 1:
raise SnapshotError("target_chapter 必须等于 as_of+1")
if not snapshot_version.strip():
raise SnapshotError("snapshot_version 不能为空")
if not isinstance(reference_work, Mapping) or not reference_work.get("id") or not reference_work.get("version"):
raise SnapshotError("reference_work 必须包含 id/version")
if not evaluation_set_version.strip() or not strategy_version.strip():
raise SnapshotError("evaluation_set_version/strategy_version 不能为空")
if not isinstance(authorization_snapshot, Mapping):
raise SnapshotError("authorization_snapshot 必须是对象")
authorization_required = (
"id",
"version",
"immutable",
"sourceVersion",
"sourceStatus",
"allowedPurpose",
"checkedAt",
)
if any(not authorization_snapshot.get(field) for field in authorization_required):
raise SnapshotError("authorization_snapshot 缺少必填字段")
if authorization_snapshot.get("immutable") is not True:
raise SnapshotError("authorization_snapshot 必须是不可变快照")
if not authorization_snapshot.get("expiresAt") and not authorization_snapshot.get("revalidationAt"):
raise SnapshotError("authorization_snapshot 缺少过期或重验时间")
if not isinstance(run_permissions, Mapping) or not run_permissions.get("purpose"):
raise SnapshotError("run_permissions 必须包含 purpose")
if not isinstance(arm_config, Mapping) or not arm_config.get("arms"):
raise SnapshotError("arm_config 必须包含 arms")
if set(arm_config["arms"]) != {
"outline_only",
"outline_plus_cards",
"outline_plus_placebo_cards",
}:
raise SnapshotError("arm_config 必须是完整三臂")
if final_report is not None:
_validate_final_report(final_report)
auth_safe = {
"id": str(authorization_snapshot["id"]),
"version": str(authorization_snapshot.get("version") or ""),
"sha256": sha256_value(authorization_snapshot),
"checkedAt": authorization_snapshot.get("checkedAt"),
}
if not auth_safe["version"]:
raise SnapshotError("authorization_snapshot 必须包含 version")
manifest: dict[str, Any] = {
"snapshotVersion": snapshot_version,
"asOfChapter": normalized_as_of,
"targetChapter": normalized_target,
"referenceWork": {"id": str(reference_work["id"]), "version": str(reference_work["version"])},
"evaluationSetVersion": evaluation_set_version,
"strategyVersion": strategy_version,
"authorization": auth_safe,
"runPermissions": {
"purpose": str(run_permissions["purpose"]),
"mode": str(run_permissions.get("mode") or "offline"),
"sha256": sha256_value(run_permissions),
},
"armConfig": {
"arms": sorted(str(arm) for arm in arm_config["arms"]),
"sha256": sha256_value(arm_config),
},
"sections": {
str(section): _source_record(str(section), value)
for section, value in sorted(sections.items(), key=lambda pair: str(pair[0]))
},
"omittedFields": list(omitted_fields or []),
"omittedSources": list(omitted_sources or []),
"finalReport": copy.deepcopy(final_report or {}),
}
manifest["manifestSha256"] = sha256_value(manifest)
return manifest
def build_snapshot(
data: Mapping[str, Any],
as_of: int,
snapshot_version: str,
*,
target_chapter: int | None = None,
manifest_metadata: Mapping[str, Any] | None = None,
) -> dict[str, Any]:
"""从最小 JSON 输入生成冻结后的结构化快照和 manifest。"""
if not isinstance(data, Mapping):
raise SnapshotError("快照输入必须是对象")
unknown_sections = sorted(set(data) - SNAPSHOT_TOP_LEVEL_KEYS)
if unknown_sections:
raise SnapshotError(f"快照包含未登记顶层分区: {','.join(str(item) for item in unknown_sections)}")
safe = copy.deepcopy(dict(data))
all_omitted: list[dict[str, Any]] = []
metadata = dict(manifest_metadata or {})
normalized_as_of = normalize_chapter(as_of)
if normalized_as_of is None:
raise SnapshotError("as_of 必须是正整数章号")
normalized_target = normalize_chapter(target_chapter or metadata.get("targetChapter"))
if normalized_target != normalized_as_of + 1:
raise SnapshotError("target_chapter 必须等于 as_of+1")
for section in CHAPTER_SCOPED_COLLECTIONS:
if section in safe and not isinstance(safe[section], list):
raise SnapshotError(f"分区 {section} 必须是数组")
if isinstance(safe.get("milestones"), list):
milestones, omitted = filter_milestones(safe["milestones"], as_of)
safe["milestones"] = milestones
all_omitted.extend({"source": "milestones", **item} for item in omitted)
if isinstance(safe.get("outlineWindows"), list):
windows, omitted = filter_outline_windows(safe["outlineWindows"], as_of)
safe["outlineWindows"] = windows
all_omitted.extend({"source": "outlineWindows", **item} for item in omitted)
if isinstance(safe.get("cards"), list):
frozen_cards: list[dict[str, Any]] = []
for card_index, card in enumerate(safe["cards"]):
if not isinstance(card, Mapping):
all_omitted.append(_omitted(card_index, "card_not_an_object", card))
continue
projected = copy.deepcopy(dict(card))
history_key = next(
(key for key in ("milestones", "演变历程") if isinstance(projected.get(key), list)),
None,
)
if history_key is not None:
history, omitted = filter_milestones(projected[history_key], as_of)
projected[history_key] = history
all_omitted.extend(
{"source": f"cards[{card_index}].{history_key}", **item}
for item in omitted
)
frozen_cards.append(projected)
safe["cards"] = frozen_cards
safe = _freeze_nested_chapter_records(safe, normalized_as_of, all_omitted)
if safe is _DROP:
raise SnapshotError("快照根对象不能被冻结过滤")
omitted_fields: list[dict[str, Any]] = []
safe = remove_terminal_fields(safe, omitted_fields)
required_metadata = {
"referenceWork": metadata.get("referenceWork"),
"evaluationSetVersion": metadata.get("evaluationSetVersion"),
"strategyVersion": metadata.get("strategyVersion"),
"authorizationSnapshot": metadata.get("authorizationSnapshot"),
"runPermissions": metadata.get("runPermissions"),
"armConfig": metadata.get("armConfig"),
}
if any(value is None for value in required_metadata.values()):
raise SnapshotError("缺少完整回放 manifest 元数据")
manifest = build_snapshot_manifest(
as_of=normalized_as_of,
target_chapter=normalized_target,
snapshot_version=snapshot_version,
reference_work=required_metadata["referenceWork"],
evaluation_set_version=required_metadata["evaluationSetVersion"],
strategy_version=required_metadata["strategyVersion"],
authorization_snapshot=required_metadata["authorizationSnapshot"],
run_permissions=required_metadata["runPermissions"],
arm_config=required_metadata["armConfig"],
sections={"snapshot": {"sourceId": "frozen-snapshot", "sourceVersion": snapshot_version, "payload": safe}},
omitted_fields=omitted_fields,
omitted_sources=all_omitted,
)
return {"snapshot": safe, "manifest": manifest}
def _parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(description="构造 as_of 章号冻结快照")
parser.add_argument("--input", type=Path, required=True, help="结构化 JSON 输入")
parser.add_argument("--output", type=Path, required=True, help="输出 JSON 路径")
parser.add_argument("--as-of", type=int, required=True, dest="as_of")
parser.add_argument("--target-chapter", type=int, required=True, dest="target_chapter")
parser.add_argument("--metadata", type=Path, required=True, help="回放 manifest 元数据 JSON")
parser.add_argument("--snapshot-version", default="next_fine_outline_replay_v0")
return parser.parse_args()
def main() -> int:
args = _parse_args()
data = json.loads(args.input.read_text(encoding="utf-8"))
metadata = json.loads(args.metadata.read_text(encoding="utf-8"))
result = build_snapshot(
data,
args.as_of,
args.snapshot_version,
target_chapter=args.target_chapter,
manifest_metadata=metadata,
)
args.output.write_text(_safe_json(result) + "\n", encoding="utf-8")
return 0
if __name__ == "__main__":
raise SystemExit(main())