|
|
@@ -0,0 +1,164 @@
|
|
|
+"""Production 0.7 State 与 Run 记录的完整观测投影。"""
|
|
|
+
|
|
|
+from __future__ import annotations
|
|
|
+
|
|
|
+import hashlib
|
|
|
+from pathlib import Path
|
|
|
+from typing import Any, Mapping
|
|
|
+
|
|
|
+from .full_capture import (
|
|
|
+ collect_state_records,
|
|
|
+ jsonable,
|
|
|
+ load_observation_record,
|
|
|
+)
|
|
|
+
|
|
|
+
|
|
|
+_RUN_TEXT_SUFFIXES = frozenset({".json", ".md", ".mmd", ".txt"})
|
|
|
+
|
|
|
+
|
|
|
+def _inside(path: Path, root: Path) -> bool:
|
|
|
+ try:
|
|
|
+ path.resolve().relative_to(root.resolve())
|
|
|
+ except (OSError, ValueError):
|
|
|
+ return False
|
|
|
+ return True
|
|
|
+
|
|
|
+
|
|
|
+def collect_production_run_records(
|
|
|
+ run_dir: Path,
|
|
|
+ *,
|
|
|
+ seen_hashes: dict[str, str] | None = None,
|
|
|
+ changed_only: bool = False,
|
|
|
+) -> dict[str, Any]:
|
|
|
+ """读取 Run 内正式文本记录。
|
|
|
+
|
|
|
+ ``changed_only`` 为真时只返回本节点新增或变化的文件。
|
|
|
+ """
|
|
|
+
|
|
|
+ root = run_dir.resolve()
|
|
|
+ records: dict[str, Any] = {}
|
|
|
+ if not root.is_dir():
|
|
|
+ return records
|
|
|
+ for path in sorted(root.rglob("*")):
|
|
|
+ if (
|
|
|
+ not path.is_file()
|
|
|
+ or path.suffix.lower() not in _RUN_TEXT_SUFFIXES
|
|
|
+ or not _inside(path, root)
|
|
|
+ ):
|
|
|
+ continue
|
|
|
+ relative = path.resolve().relative_to(root).as_posix()
|
|
|
+ try:
|
|
|
+ digest = hashlib.sha256(path.read_bytes()).hexdigest()
|
|
|
+ except OSError as exc:
|
|
|
+ records[relative] = {
|
|
|
+ "path": str(path.resolve()),
|
|
|
+ "exists": path.is_file(),
|
|
|
+ "read_error": f"{type(exc).__name__}: {exc}",
|
|
|
+ }
|
|
|
+ continue
|
|
|
+ previous = seen_hashes.get(relative) if seen_hashes is not None else None
|
|
|
+ if seen_hashes is not None:
|
|
|
+ seen_hashes[relative] = digest
|
|
|
+ if changed_only and previous == digest:
|
|
|
+ continue
|
|
|
+ record = load_observation_record(path.resolve())
|
|
|
+ if record is not None:
|
|
|
+ records[relative] = {**record, "sha256": digest}
|
|
|
+ return records
|
|
|
+
|
|
|
+
|
|
|
+def summarize_production_state(state: Mapping[str, Any]) -> dict[str, Any]:
|
|
|
+ """脱敏回退只保留运行身份、调度状态、计数和记录存在性。"""
|
|
|
+
|
|
|
+ counter_fields = (
|
|
|
+ "planner_calls",
|
|
|
+ "planner_review_calls",
|
|
|
+ "executor_calls",
|
|
|
+ "segment_validator_calls",
|
|
|
+ "assembly_calls",
|
|
|
+ "production_validator_calls",
|
|
|
+ "replan_count",
|
|
|
+ "max_plan_revisions",
|
|
|
+ )
|
|
|
+ segment_records = state.get("segment_records")
|
|
|
+ segments = (
|
|
|
+ {
|
|
|
+ str(segment_id): {
|
|
|
+ "status": record.get("status"),
|
|
|
+ "active_plan_version": record.get("active_plan_version"),
|
|
|
+ "accepted_plan_version": record.get("accepted_plan_version"),
|
|
|
+ }
|
|
|
+ for segment_id, record in segment_records.items()
|
|
|
+ if isinstance(record, Mapping)
|
|
|
+ }
|
|
|
+ if isinstance(segment_records, Mapping)
|
|
|
+ else {}
|
|
|
+ )
|
|
|
+ return {
|
|
|
+ "run_id": str(state.get("run_id") or "")[:160],
|
|
|
+ "protocol_version": state.get("protocol_version"),
|
|
|
+ "status": state.get("status"),
|
|
|
+ "phase": state.get("phase"),
|
|
|
+ "failure_code": state.get("failure_code"),
|
|
|
+ "replan_scope": state.get("replan_scope"),
|
|
|
+ "current_segment_id": state.get("current_segment_id"),
|
|
|
+ "counters": {
|
|
|
+ field: state.get(field)
|
|
|
+ for field in counter_fields
|
|
|
+ if field in state
|
|
|
+ },
|
|
|
+ "segments": segments,
|
|
|
+ "record_fields_present": sorted(collect_state_records(state)),
|
|
|
+ "event_count": len(state.get("event_log") or []),
|
|
|
+ }
|
|
|
+
|
|
|
+
|
|
|
+def expand_production_state(state: Mapping[str, Any]) -> dict[str, Any]:
|
|
|
+ return {
|
|
|
+ "state": jsonable(state),
|
|
|
+ "records": collect_state_records(state),
|
|
|
+ }
|
|
|
+
|
|
|
+
|
|
|
+def expand_production_node_output(
|
|
|
+ phase: str,
|
|
|
+ state: Mapping[str, Any],
|
|
|
+ update: Mapping[str, Any] | None,
|
|
|
+ *,
|
|
|
+ run_records: Mapping[str, Any],
|
|
|
+ exception: BaseException | None = None,
|
|
|
+) -> dict[str, Any]:
|
|
|
+ merged = dict(state)
|
|
|
+ if update is not None:
|
|
|
+ merged.update(update)
|
|
|
+ return {
|
|
|
+ "node": phase,
|
|
|
+ "update": jsonable(update or {}),
|
|
|
+ "state_after": jsonable(merged),
|
|
|
+ "referenced_records": collect_state_records(merged),
|
|
|
+ "new_or_changed_run_records": jsonable(run_records),
|
|
|
+ "exception": (
|
|
|
+ {
|
|
|
+ "type": type(exception).__name__,
|
|
|
+ "message": str(exception),
|
|
|
+ }
|
|
|
+ if exception is not None
|
|
|
+ else None
|
|
|
+ ),
|
|
|
+ }
|
|
|
+
|
|
|
+
|
|
|
+def expand_production_result(
|
|
|
+ result: Mapping[str, Any],
|
|
|
+ *,
|
|
|
+ execution_mode: str,
|
|
|
+ graph_invoked: bool,
|
|
|
+ run_records: Mapping[str, Any],
|
|
|
+) -> dict[str, Any]:
|
|
|
+ return {
|
|
|
+ "execution_mode": execution_mode,
|
|
|
+ "graph_invoked": graph_invoked,
|
|
|
+ "result": jsonable(result),
|
|
|
+ "referenced_records": collect_state_records(result),
|
|
|
+ "run_records": jsonable(run_records),
|
|
|
+ }
|