#!/usr/bin/env python3 """Apply the independently approved terminal status sync for NEWENERGY BATCH-001. This is an L0 state-only transition. It changes review/document/package state, recomputes receipt/output/artifact hashes affected by that state transition, and creates a terminal acceptance receipt. It does not change source content, evidence text, eligibility, ranking, mappings, or research conclusions. """ from __future__ import annotations import csv import json import re from collections import Counter from datetime import datetime from pathlib import Path from typing import Any from zoneinfo import ZoneInfo import newenergy_batch001_repair as base import newenergy_batch001_repair004 as r4 TASK_ID = "TASK-NEWENERGY-FOUR-TRACK-ATLAS-20260805-001" CASE_ID = "ANA-NEWENERGY-FOUR-TRACK-ATLAS-20260805-001" BATCH_ID = "BATCH-001" RUN_ID = "RUN-ANA-NEWENERGY-FOUR-TRACK-ATLAS-20260805-001-BATCH-001-001" FINAL_AUDIT = "AUDIT-ANA-NEWENERGY-FOUR-TRACK-ATLAS-BATCH001-EXECUTION-OUTPUT-REPAIR005-REREVIEW-20260806-001" PARENT_STATUS = "COMPLETED_ACCEPTED_AND_CLOSED" ACCEPTED = "ACCEPTED_BY_INDEPENDENT_REVIEW" ARTIFACT_STATUS = "FINAL_ACCEPTED_BY_INDEPENDENT_REVIEW" TOOL_VERSION = "REPAIR-005" PROJECT_ROOT = Path(__file__).resolve().parents[2] INDUSTRY_ROOT = PROJECT_ROOT / "ana-data/cases/新能源案例" CASE_ROOT = INDUSTRY_ROOT / CASE_ID CASE_OUTPUTS = CASE_ROOT / "outputs" CASE_MANIFEST = CASE_ROOT / "manifest" RESULT_ROOT = PROJECT_ROOT / "ana-data/result/新能源案例" / CASE_ID ACCEPTANCE_RECORD = RESULT_ROOT / "acceptance_record.md" ACCEPTANCE_VALIDATION = CASE_MANIFEST / "acceptance_validation_receipt.json" def sha256_file(path: Path) -> str: return r4.sha256_file(path) def stable_accepted_at() -> str: if ACCEPTANCE_RECORD.exists(): text = ACCEPTANCE_RECORD.read_text(encoding="utf-8") match = re.search(r"^- `accepted_at=([^`]+)`$", text, flags=re.M) if match: return match.group(1) return datetime.now(ZoneInfo("Asia/Shanghai")).replace(microsecond=0).isoformat() def read_csv_with_header(path: Path) -> tuple[list[str], list[dict[str, str]]]: with path.open("r", encoding="utf-8-sig", newline="") as handle: reader = csv.DictReader(handle) return list(reader.fieldnames or []), list(reader) def canonical_receipt_hash(row: dict[str, str]) -> str: core = {key: value for key, value in row.items() if key != "receipt_sha256"} payload = json.dumps(core, ensure_ascii=False, sort_keys=True, separators=(",", ":")).encode("utf-8") return r4.sha256_bytes(payload) def sync_csv_review_statuses() -> dict[str, int]: counters: Counter[str] = Counter() artifact_manifest = INDUSTRY_ROOT / "manifest" / "artifact_manifest.csv" for path in sorted(INDUSTRY_ROOT.rglob("*.csv")): if path.resolve() == artifact_manifest.resolve(): continue headers, rows = read_csv_with_header(path) if "review_status" not in headers: continue for row in rows: row["review_status"] = ACCEPTED if path.name == "human_doc_validation_receipt.csv": row["document_status"] = ACCEPTED if path.name == "output_manifest.csv": row["data_status"] = "FINAL_ACCEPTED" if "receipt_sha256" in headers: row["receipt_sha256"] = canonical_receipt_hash(row) base.write_csv(path, headers, rows) counters["csv_files"] += 1 counters["csv_rows"] += len(rows) counters[path.name] += len(rows) return dict(counters) def replace_required(path: Path, old: str, new: str, *, minimum: int = 1) -> int: text = path.read_text(encoding="utf-8") count = text.count(old) if count < minimum and new not in text: raise RuntimeError(f"required acceptance transition marker missing in {path}: {old!r}") if count: path.write_text(text.replace(old, new), encoding="utf-8", newline="\n") return count def sync_markdown_statuses() -> dict[str, int]: counts: Counter[str] = Counter() for path in sorted(CASE_OUTPUTS.rglob("*.md")): counts["output_status_markers"] += replace_required( path, "> 状态:`DRAFT_FOR_REVIEW`", f"> 状态:`{ACCEPTED}`" ) counts["output_files"] += 1 summary = CASE_OUTPUTS / "summary.md" old = ( "输出仍为 `DRAFT_FOR_REVIEW`。在独立执行/输出审核 PASS 前,不标记完成、" "不回写父级终态、不对外宣称体系成果已经验收。" ) new = ( f"本批执行/输出已由独立审核 `{FINAL_AUDIT}` 以 `PASS / 0 / 0` 接受," f"父级状态已回写为 `{PARENT_STATUS}`。接受仅覆盖本 case/BATCH/run 的现有公开资料成果," "不放行下一批、范围扩展、完整覆盖声明、估值、行情、交易、收益、市场反向补漏或核电敏感内容。" ) counts["summary_terminal_paragraph"] += replace_required(summary, old, new) batch_summary = CASE_MANIFEST / "batch_summary.md" counts["candidate_count_fix"] += replace_required( batch_summary, "candidate_state_count=INCLUDED_T1:16,INCLUDED_T2:16,HELD_BY_EVIDENCE_GAP:4479", "candidate_state_count=INCLUDED_T1:16,INCLUDED_T2:16,ELIGIBLE_NOT_SELECTED_BATCH001:40,HELD_BY_EVIDENCE_GAP:4479", ) counts["batch_output_status"] += replace_required( batch_summary, "output_status=DRAFT_FOR_REVIEW", "output_status=FINAL_ACCEPTED" ) counts["batch_review_status"] += replace_required( batch_summary, "review_status=PENDING_REPAIR005_FOCUSED_REREVIEW", f"review_status={ACCEPTED}" ) result_index = RESULT_ROOT / "result_index.md" counts["result_header"] += replace_required( result_index, "> 状态:`DRAFT_FOR_REVIEW`", f"> 状态:`{ACCEPTED}`" ) counts["result_current"] += replace_required( result_index, "当前状态:`DRAFT_FOR_REVIEW`", f"当前状态:`{ACCEPTED}`" ) counts["result_review"] += replace_required( result_index, "执行/输出独立审核:`PENDING`", f"执行/输出独立审核:`PASS`(`{FINAL_AUDIT}`)" ) counts["result_parent"] += replace_required( result_index, "父级终态回写:`NOT_ALLOWED_BEFORE_REVIEW_PASS`", f"父级终态回写:`{PARENT_STATUS}`" ) return dict(counts) def rebuild_output_manifest() -> list[dict[str, str]]: path = CASE_MANIFEST / "output_manifest.csv" headers, rows = read_csv_with_header(path) for row in rows: output = PROJECT_ROOT / row["output_path"] if not output.exists(): raise FileNotFoundError(output) row["output_sha256"] = sha256_file(output) row["data_status"] = "FINAL_ACCEPTED" row["review_status"] = ACCEPTED base.write_csv(path, headers, rows) return rows def write_acceptance_record(accepted_at: str, output_manifest_hash: str) -> None: RESULT_ROOT.mkdir(parents=True, exist_ok=True) text = f"""# {CASE_ID} 正式接受记录 - `task_id={TASK_ID}` - `case_id={CASE_ID}` - `batch_id={BATCH_ID}` - `run_id={RUN_ID}` - `status={ACCEPTED}` - `accepted_at={accepted_at}` - `final_audit={FINAL_AUDIT}` - `audit_result=PASS/0/0/0` - `closed_blockers=B1-R004-A,B1-R004-B` - `parent_status={PARENT_STATUS}` - `accepted_output_manifest=ana-data/cases/新能源案例/{CASE_ID}/manifest/output_manifest.csv` - `accepted_output_manifest_sha256={output_manifest_hash.upper()}` - `governance=OK_PROJECTS1_WARNINGS0` ## 接受范围 本记录接受本 case/BATCH/run 的锂电、光伏、风电、核电四赛道基础行业图谱、16 份子行业正文、三类顶层视图、4,551 条唯一候选映射账本、32 条正式 selected 公司—赛道映射、786/786 份 source/conversion、82 条 evidence fact、110 条结论映射和 24 项 output manifest。候选终态为 `INCLUDED_T1=16 / INCLUDED_T2=16 / ELIGIBLE_NOT_SELECTED_BATCH001=40 / HELD_BY_EVIDENCE_GAP=4479`。 执行/输出独立复审 `{FINAL_AUDIT}` 已关闭 B1-R004-A/B,并给出 `PASS / blocker 0 / non-blocking 0`;此前设计及执行审核的所有 HOLD/PASS 记录和冻结哈希继续保留。审核后仅执行获准的 L0 状态同步,未修改来源内容、证据原文、资格判断、机械排序、结论强度或研究边界。 ## 父级关闭与边界 项目管理员已在 `ana-doc/案例总纲.md` append-only 回写 `{PARENT_STATUS}`。本接受不放行下一批或扩域;继续保持 `primary_region=MAINLAND_CHINA`、全球/境外分账、`coverage_claim=NONE_INITIAL_CANDIDATE_POOL_ONLY`,不形成完整覆盖、估值、行情、交易、收益、市场反向补漏或核电敏感结论。 """ ACCEPTANCE_RECORD.write_text(text, encoding="utf-8", newline="\n") def count_csv(path: Path) -> tuple[int, list[dict[str, str]]]: _, rows = read_csv_with_header(path) return len(rows), rows def write_acceptance_validation(accepted_at: str, output_rows: list[dict[str, str]]) -> dict[str, Any]: candidate_path = INDUSTRY_ROOT / "extracted" / "company_track_candidate_ledger.csv" evidence_path = INDUSTRY_ROOT / "evidence" / "evidence_fact_table.csv" case_map_path = CASE_ROOT / "evidence" / "case_evidence_map.csv" role_path = INDUSTRY_ROOT / "extracted" / "candidate_role_adjudication_receipt.csv" page_path = INDUSTRY_ROOT / "extracted" / "candidate_page_qualification_receipt.csv" source_path = INDUSTRY_ROOT / "manifest" / "source_document.csv" conversion_path = INDUSTRY_ROOT / "manifest" / "conversion_status.csv" _, candidates = count_csv(candidate_path) source_count, _ = count_csv(source_path) conversion_count, _ = count_csv(conversion_path) evidence_count, _ = count_csv(evidence_path) map_count, _ = count_csv(case_map_path) _, roles = count_csv(role_path) _, pages = count_csv(page_path) candidate_states = dict(sorted(Counter(row["candidate_state"] for row in candidates).items())) errors: list[str] = [] expected_states = { "ELIGIBLE_NOT_SELECTED_BATCH001": 40, "HELD_BY_EVIDENCE_GAP": 4479, "INCLUDED_T1": 16, "INCLUDED_T2": 16, } if candidate_states != expected_states: errors.append(f"candidate state mismatch: {candidate_states}") if (source_count, conversion_count, evidence_count, map_count, len(output_rows)) != (786, 786, 82, 110, 24): errors.append( f"accepted package counts mismatch: source={source_count} conversion={conversion_count} " f"evidence={evidence_count} map={map_count} output={len(output_rows)}" ) for label, rows in (("role", roles), ("page", pages)): invalid = sum(row.get("receipt_sha256") != canonical_receipt_hash(row) for row in rows) if invalid: errors.append(f"{label} receipt hash mismatch={invalid}") if any(row["review_status"] != ACCEPTED or row["data_status"] != "FINAL_ACCEPTED" for row in output_rows): errors.append("output manifest terminal status mismatch") output_hash_errors = sum( row["output_sha256"] != sha256_file(PROJECT_ROOT / row["output_path"]) for row in output_rows ) if output_hash_errors: errors.append(f"output hash mismatch={output_hash_errors}") receipt = { "task_id": TASK_ID, "case_id": CASE_ID, "batch_id": BATCH_ID, "run_id": RUN_ID, "accepted_at": accepted_at, "status": ACCEPTED, "final_audit": FINAL_AUDIT, "parent_status": PARENT_STATUS, "transition_type": "L0_REVIEW_STATUS_AND_MANIFEST_HASH_SYNC_ONLY", "candidate_state_distribution": candidate_states, "source_count": source_count, "conversion_count": conversion_count, "evidence_fact_count": evidence_count, "case_map_count": map_count, "output_count": len(output_rows), "role_receipt_count": len(roles), "page_receipt_count": len(pages), "core_hashes": { "candidate_ledger": sha256_file(candidate_path), "evidence_fact_table": sha256_file(evidence_path), "case_evidence_map": sha256_file(case_map_path), "role_adjudication_receipt": sha256_file(role_path), "candidate_page_qualification_receipt": sha256_file(page_path), "output_manifest": sha256_file(CASE_MANIFEST / "output_manifest.csv"), "acceptance_record": sha256_file(ACCEPTANCE_RECORD), }, "historical_frozen_receipt": "manifest/repair005_validation_receipt.json (pre-acceptance audit snapshot; intentionally unchanged)", "validation_errors": errors, "validation_status": "PASS" if not errors else "FAIL", } ACCEPTANCE_VALIDATION.write_text( json.dumps(receipt, ensure_ascii=False, sort_keys=True, indent=2) + "\n", encoding="utf-8", newline="\n", ) if errors: raise RuntimeError("acceptance validation failed: " + "; ".join(errors)) return receipt def rebuild_artifact_manifest(output_rows: list[dict[str, str]]) -> list[dict[str, str]]: source_rows = base.read_csv(INDUSTRY_ROOT / "manifest" / "source_document.csv") collected = [row.get("collected_at", "") for row in source_rows if row.get("collected_at")] base.COLLECTED_AT = max(collected, default="2026-08-06T00:00:00+08:00") base.ARTIFACT_TOOL_VERSION = TOOL_VERSION base.ARTIFACT_PARAMETERS_SUMMARY = ( "B1 REPAIR005 independently accepted; terminal L0 review/document/package status sync; " "no source, evidence, eligibility, ranking, mapping, or conclusion change" ) base.ARTIFACT_STATUS = ARTIFACT_STATUS audited_replay_tool = Path(__file__).with_name("newenergy_batch001_repair005.py") return base.build_artifact_manifest(source_rows, output_rows, audited_replay_tool) def validate_artifact_manifest(rows: list[dict[str, str]]) -> dict[str, Any]: audited_replay_tool = Path(__file__).with_name("newenergy_batch001_repair005.py") formal = base.formal_files(audited_replay_tool) by_path = {row["relative_path"]: row for row in rows} errors: list[str] = [] if len(by_path) != len(rows): errors.append("duplicate artifact relative path") if set(by_path) != {base.rel(path) for path in formal}: errors.append("artifact/formal exact-set mismatch") for path in formal: row = by_path.get(base.rel(path)) if row is None: continue if row["file_size"] != str(path.stat().st_size) or row["sha256"] != sha256_file(path): errors.append(f"artifact hash mismatch: {base.rel(path)}") break if any(row["artifact_status"] != ARTIFACT_STATUS or row["tool_version"] != TOOL_VERSION for row in rows): errors.append("artifact terminal status/tool version mismatch") if errors: raise RuntimeError("artifact validation failed: " + "; ".join(errors)) return { "artifact_exact_set": len(rows), "artifact_manifest_sha256": sha256_file(INDUSTRY_ROOT / "manifest" / "artifact_manifest.csv"), "package_exact_set_receipt_sha256": sha256_file(CASE_MANIFEST / "package_exact_set_receipt.md"), } def assert_no_pending_markers() -> None: forbidden = ("DRAFT_FOR_REVIEW", "PENDING_REPAIR005_FOCUSED_REREVIEW", "NOT_ALLOWED_BEFORE_REVIEW_PASS") files = list(CASE_OUTPUTS.rglob("*.md")) + [CASE_MANIFEST / "batch_summary.md", RESULT_ROOT / "result_index.md"] hits = [(path, marker) for path in files for marker in forbidden if marker in path.read_text(encoding="utf-8")] if hits: raise RuntimeError(f"terminal Markdown still contains pending marker: {hits[:3]}") def run() -> dict[str, Any]: accepted_at = stable_accepted_at() csv_sync = sync_csv_review_statuses() markdown_sync = sync_markdown_statuses() output_rows = rebuild_output_manifest() output_manifest_hash = sha256_file(CASE_MANIFEST / "output_manifest.csv") write_acceptance_record(accepted_at, output_manifest_hash) acceptance = write_acceptance_validation(accepted_at, output_rows) artifacts = rebuild_artifact_manifest(output_rows) artifact_validation = validate_artifact_manifest(artifacts) assert_no_pending_markers() return { "status": "ACCEPTED_BY_INDEPENDENT_REVIEW_AND_PARENT_CLOSED", "final_audit": FINAL_AUDIT, "parent_status": PARENT_STATUS, "accepted_at": accepted_at, "candidate_state_distribution": acceptance["candidate_state_distribution"], "csv_sync": csv_sync, "markdown_sync": markdown_sync, "output_count": len(output_rows), "output_manifest_sha256": output_manifest_hash, "acceptance_record_sha256": sha256_file(ACCEPTANCE_RECORD), "acceptance_validation_sha256": sha256_file(ACCEPTANCE_VALIDATION), **artifact_validation, } def main() -> None: print(json.dumps(run(), ensure_ascii=False, sort_keys=True, indent=2)) if __name__ == "__main__": main()