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 source_candidates(row): fact_id = row.get("fact_id", "") if fact_id == "YS-B3-F-02056": return [ { "company_name": "洛阳钼业", "stock_code": "603993.SH / 03993.HK", "source_type": "company_official_report_entry", "source_name": "洛阳钼业业绩与报告入口", "source_url": "https://www.cmoc.com/html/InvestorMedia/PerformanceReport/Quarterly/", "source_period_or_date": "2025/2026", "source_access_status": "SOURCE_ENTRY_LOCATED", "source_relevance": "official report entry; annual report row and tungsten detail still need extraction", "next_action": "open official annual report entry and archive annual report page or PDF", }, { "company_name": "洛阳钼业", "stock_code": "603993.SH / 03993.HK", "source_type": "company_official_news", "source_name": "洛阳钼业2025年盈利203亿元新闻发布", "source_url": "https://www.cmoc.com/html/2026/News_0327/398.html", "source_period_or_date": "2026-03-27", "source_access_status": "SOURCE_ENTRY_LOCATED", "source_relevance": "official company news confirms 2025 performance context; tungsten segment detail still needs annual report/table row", "next_action": "use as company context only; verify tungsten business value against annual report or exchange filing", }, { "company_name": "洛阳钼业", "stock_code": "603993.SH / 03993.HK", "source_type": "disclosure_mirror_annual_report_pdf", "source_name": "洛阳钼业2025年年度报告披露镜像", "source_url": "https://pdf.dfcfw.com/pdf/H2_AN202603271820812761_1.pdf", "source_period_or_date": "2025", "source_access_status": "SOURCE_ENTRY_LOCATED_NEEDS_ARCHIVE", "source_relevance": "annual report mirror candidate; must archive and extract exact tungsten row before formal use", "next_action": "archive PDF and search tungsten segment/production guidance row", }, ] if fact_id in {"YS-B3-F-00805", "YS-B3-F-02391"}: return [ { "company_name": "湖南黄金", "stock_code": "002155.SZ", "source_type": "disclosure_pdf_project_announcement", "source_name": "湖南黄金关于控股子公司新邵四维取得采矿许可证的公告", "source_url": "https://xinpi.zqrb.cn/pdf/SZ/002155/2025/1128/f3d81db0a446b7501b6b0e4e00fbc0de.pdf", "source_period_or_date": "2025-11-28", "source_access_status": "SOURCE_ENTRY_LOCATED_NEEDS_ARCHIVE", "source_relevance": "project announcement candidate for Tanxi tungsten mine mining license and 990kt/year scale", "next_action": "archive PDF and extract license name, mineral type, production scale and validity period", }, { "company_name": "湖南黄金", "stock_code": "002155.SZ", "source_type": "company_annual_report_disclosure_mirror", "source_name": "湖南黄金2025年年度报告披露镜像", "source_url": "https://money.finance.sina.com.cn/corp/view/vCB_AllBulletinDetail.php?id=12073043&stockid=002155", "source_period_or_date": "2025", "source_access_status": "SOURCE_ENTRY_LOCATED_NEEDS_ARCHIVE", "source_relevance": "annual report candidate for company resource, tungsten output and business context", "next_action": "archive annual report and extract tungsten resource/output table rows", }, { "company_name": "湖南黄金", "stock_code": "002155.SZ", "source_type": "disclosure_media_entry", "source_name": "上证报湖南黄金控股子公司新邵四维取得采矿许可证", "source_url": "https://www.cnstock.com/commonDetail/594295", "source_period_or_date": "2025-11-27", "source_access_status": "SOURCE_ENTRY_LOCATED", "source_relevance": "disclosure media confirms announcement summary; use only as secondary pointer before formal evidence upgrade", "next_action": "use as pointer; formal evidence should rely on announcement PDF or annual report", }, ] return [] def main(): parser = argparse.ArgumentParser() parser.add_argument("--project-root", default=".") parser.add_argument("--gap-input", 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-OFFICIAL-SOURCE-012") args = parser.parse_args() project_root = Path(args.project_root).resolve() gaps = read_csv(project_root / args.gap_input) target = [row for row in gaps if row.get("gap_type") == "COMPANY_OFFICIAL_SOURCE_GAP"] created_at = datetime.now(timezone(timedelta(hours=8))).isoformat(timespec="seconds") rows = [] for gap in target: for source in source_candidates(gap): rows.append( { "company_official_source_id": f"YS-COMPANY-OFFICIAL-SRC-012-{len(rows) + 1:04d}", "case_id": gap.get("case_id", ""), "batch_id": gap.get("batch_id", ""), "run_id": args.run_id, "gap_priority_id": gap.get("gap_priority_id", ""), "evidence_card_id": gap.get("evidence_card_id", ""), "fact_id": gap.get("fact_id", ""), "doc_id": gap.get("doc_id", ""), "metal_tags": gap.get("metal_tags", ""), "theme_tags": gap.get("theme_tags", ""), "gap_type": gap.get("gap_type", ""), "preferred_source_type": gap.get("preferred_source_type", ""), "company_name": source["company_name"], "stock_code": source["stock_code"], "source_type": source["source_type"], "source_name": source["source_name"], "source_url": source["source_url"], "source_period_or_date": source["source_period_or_date"], "source_access_status": source["source_access_status"], "source_relevance": source["source_relevance"], "value_unit_candidates": gap.get("value_unit_candidates", ""), "date_candidates": gap.get("date_candidates", ""), "evidence_status": "SOURCE_ENTRY_DRAFT_NOT_VALUE_VERIFIED", "next_action": source["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_official_source_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_official_source_supplement", "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, } ], ) company_counts = Counter(row["company_name"] for row in rows) source_type_counts = Counter(row["source_type"] for row in rows) status_counts = Counter(row["source_access_status"] for row in rows) summary_path = project_root / args.summary lines = [ "# 公司官方源补证 PASS-012 摘要", "", "状态: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(target)}", f"- 来源候选记录:{len(rows)}", f"- sha256:`{output_sha}`", "", "## 公司分布", "", "| 公司 | 数量 |", "|---|---:|", ] for key, count in company_counts.most_common(): lines.append(f"| {key} | {count} |") lines.extend(["", "## 来源类型", "", "| 来源类型 | 数量 |", "|---|---:|"]) for key, count in source_type_counts.most_common(): lines.append(f"| {key} | {count} |") lines.extend(["", "## 来源状态", "", "| 状态 | 数量 |", "|---|---:|"]) for key, count in status_counts.most_common(): lines.append(f"| {key} | {count} |") lines.extend( [ "", "## 边界", "", "本轮只补充公司官方源或披露入口候选,尚未完成网页/PDF 归档、原文表格行抽取、数值一致性核验或正式证据升级;不作为正式公司结论或正式指标。", ] ) 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(company_counts)) print(dict(source_type_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()