import argparse
|
import csv
|
import hashlib
|
from collections import Counter
|
from datetime import datetime, timedelta, timezone
|
from pathlib import Path
|
|
|
def read_csv(path):
|
with path.open("r", encoding="utf-8-sig", newline="") as handle:
|
return list(csv.DictReader(handle))
|
|
|
def write_csv(path, fieldnames, rows):
|
path.parent.mkdir(parents=True, exist_ok=True)
|
with path.open("w", encoding="utf-8-sig", newline="") as handle:
|
writer = csv.DictWriter(handle, fieldnames=fieldnames)
|
writer.writeheader()
|
writer.writerows(rows)
|
|
|
def sha256_file(path):
|
return hashlib.sha256(path.read_bytes()).hexdigest()
|
|
|
def main():
|
parser = argparse.ArgumentParser()
|
parser.add_argument("--project-root", default=".")
|
parser.add_argument("--source-input", required=True)
|
parser.add_argument("--archive-manifest", required=True)
|
parser.add_argument("--output", required=True)
|
parser.add_argument("--manifest", required=True)
|
parser.add_argument("--summary", required=True)
|
parser.add_argument("--run-id", default="RUN-ANA-YS-COMPANY-SOURCE-ARCHIVE-013")
|
args = parser.parse_args()
|
|
project_root = Path(args.project_root).resolve()
|
sources = read_csv(project_root / args.source_input)
|
archive_rows = read_csv(project_root / args.archive_manifest)
|
archive_by_url = {}
|
for row in archive_rows:
|
archive_by_url.setdefault(row.get("source_url", ""), []).append(row)
|
|
created_at = datetime.now(timezone(timedelta(hours=8))).isoformat(timespec="seconds")
|
rows = []
|
for source in sources:
|
url = source.get("source_url", "")
|
matches = archive_by_url.get(url, [])
|
match = matches.pop(0) if matches else {}
|
fetch_status = match.get("fetch_status", "NOT_ATTEMPTED")
|
if fetch_status == "FETCHED":
|
archive_status = "ARCHIVED_DRAFT"
|
evidence_status = "ARCHIVED_NOT_VALUE_VERIFIED"
|
next_action = "extract_original_page_or_table_row_and_compare_value"
|
else:
|
archive_status = "HELD_BY_ENV_NETWORK"
|
evidence_status = "SOURCE_ENTRY_DRAFT_ARCHIVE_FAILED"
|
next_action = "retry_archive_with_browser_or_approved_network_path"
|
rows.append(
|
{
|
"company_source_archive_review_id": f"YS-COMPANY-ARCHIVE-013-{len(rows) + 1:04d}",
|
"case_id": source.get("case_id", ""),
|
"batch_id": source.get("batch_id", ""),
|
"run_id": args.run_id,
|
"company_official_source_id": source.get("company_official_source_id", ""),
|
"gap_priority_id": source.get("gap_priority_id", ""),
|
"evidence_card_id": source.get("evidence_card_id", ""),
|
"fact_id": source.get("fact_id", ""),
|
"doc_id": source.get("doc_id", ""),
|
"company_name": source.get("company_name", ""),
|
"stock_code": source.get("stock_code", ""),
|
"source_type": source.get("source_type", ""),
|
"source_name": source.get("source_name", ""),
|
"source_url": url,
|
"source_period_or_date": source.get("source_period_or_date", ""),
|
"archive_source_id": match.get("source_id", ""),
|
"archive_fetch_status": fetch_status,
|
"archive_http_status": match.get("http_status", ""),
|
"archive_content_type": match.get("content_type", ""),
|
"archive_bytes": match.get("bytes", ""),
|
"archive_sha256": match.get("sha256", ""),
|
"archive_relative_path": match.get("relative_path", ""),
|
"archive_text_relative_path": match.get("text_relative_path", ""),
|
"archive_error": match.get("error", ""),
|
"archive_status": archive_status,
|
"original_line_extract_status": "NOT_STARTED",
|
"value_consistency_status": "NOT_VERIFIED",
|
"evidence_status": evidence_status,
|
"next_action": next_action,
|
"review_status": "DRAFT_FOR_REVIEW",
|
"created_at": created_at,
|
}
|
)
|
|
output_path = project_root / args.output
|
fields = list(rows[0].keys()) if rows else ["company_source_archive_review_id", "case_id", "run_id", "review_status"]
|
write_csv(output_path, fields, rows)
|
output_sha = sha256_file(output_path)
|
manifest_path = project_root / args.manifest
|
write_csv(
|
manifest_path,
|
["case_id", "batch_id", "run_id", "artifact_type", "artifact_path", "row_count", "sha256", "review_status", "created_at"],
|
[
|
{
|
"case_id": "ANA-YS-INDUSTRY-001",
|
"batch_id": "BATCH-003",
|
"run_id": args.run_id,
|
"artifact_type": "company_source_archive_review",
|
"artifact_path": output_path.relative_to(project_root).as_posix(),
|
"row_count": str(len(rows)),
|
"sha256": output_sha,
|
"review_status": "DRAFT_FOR_REVIEW",
|
"created_at": created_at,
|
}
|
],
|
)
|
|
archive_counts = Counter(row["archive_status"] for row in rows)
|
fetch_counts = Counter(row["archive_fetch_status"] for row in rows)
|
company_counts = Counter(row["company_name"] for row in rows)
|
summary_path = project_root / args.summary
|
lines = [
|
"# 公司官方源归档复核 PASS-013 摘要",
|
"",
|
"状态:DRAFT_FOR_REVIEW",
|
f"生成时间:{created_at}",
|
"",
|
"## 输出",
|
"",
|
f"- 归档复核表:`{output_path.relative_to(project_root).as_posix()}`",
|
f"- manifest:`{manifest_path.relative_to(project_root).as_posix()}`",
|
f"- 记录数:{len(rows)}",
|
f"- sha256:`{output_sha}`",
|
"",
|
"## 归档状态",
|
"",
|
"| 状态 | 数量 |",
|
"|---|---:|",
|
]
|
for key, count in archive_counts.most_common():
|
lines.append(f"| {key} | {count} |")
|
lines.extend(["", "## 抓取状态", "", "| 状态 | 数量 |", "|---|---:|"])
|
for key, count in fetch_counts.most_common():
|
lines.append(f"| {key} | {count} |")
|
lines.extend(["", "## 公司分布", "", "| 公司 | 数量 |", "|---|---:|"])
|
for key, count in company_counts.most_common():
|
lines.append(f"| {key} | {count} |")
|
lines.extend(
|
[
|
"",
|
"## 边界",
|
"",
|
"本轮执行公开 URL 归档尝试并记录状态;当前归档失败项只表示环境或网络路径缺口,不构成公司官方证据通过。正式证据升级前仍需网页/PDF 归档、hash、原文行或表格行抽取和数值一致性核验。",
|
]
|
)
|
summary_path.parent.mkdir(parents=True, exist_ok=True)
|
summary_path.write_text("\n".join(lines) + "\n", encoding="utf-8")
|
|
print(f"rows={len(rows)}")
|
print(dict(archive_counts))
|
print(dict(fetch_counts))
|
print(f"output={output_path.relative_to(project_root).as_posix()}")
|
print(f"manifest={manifest_path.relative_to(project_root).as_posix()}")
|
print(f"summary={summary_path.relative_to(project_root).as_posix()}")
|
|
|
if __name__ == "__main__":
|
main()
|