MB-X Bilibili Pipeline
6 days ago eeaf4e682d2700ab695c62b7b7869538334eb2c7
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
#!/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()