591 lines
21 KiB
Python
591 lines
21 KiB
Python
#!/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())
|