#!/usr/bin/env python3 """REPAIR005: close the remaining B1 company-self / target-bucket role defects. This replay deliberately reuses the 729 CNINFO PDFs and the 858 page-search receipts accepted by the independent REPAIR004 review. It does not download, expand, or replace research sources. The only substantive change is a stricter semantic role adjudication of the 50 page-search positives, followed by a full 858-row state replay, non-eligible field cleanup, mechanical reranking, and deterministic downstream rebuild. """ from __future__ import annotations import argparse import json import sys from collections import Counter, defaultdict from pathlib import Path from typing import Any import newenergy_batch001_repair as base import newenergy_batch001_repair004 as r4 TOOL_VERSION = "REPAIR-005" ROLE_RULE_VERSION = "REPAIR005_COMPANY_SELF_TARGET_BUCKET_ROLE_GATE_V2_HUMAN_ADJUDICATED_DIRECT_SET" REVIEW_STATUS = "DRAFT_FOR_REVIEW" TASK_ID = r4.TASK_ID CASE_ID = r4.CASE_ID BATCH_ID = r4.BATCH_ID RUN_ID = r4.RUN_ID # The independent reviewer expressly allowed necessary human reading in # REPAIR005. Every one of the 50 prior regex-positive rows is therefore frozen # here with an explicit semantic outcome. Accepted rows prove a direct business # of the listed company or a controlled subsidiary in the target bucket. The # remaining 808 rows are replayed from the already accepted full-page negative # or context-only search receipt. APPROVED_ROLE_DECISIONS: dict[str, str] = { "QUAL-00019": "CONTROLLED_SUBSIDIARY_TARGET_PRODUCT_MANUFACTURE", "QUAL-00061": "DIRECT_TARGET_PRODUCT_RESEARCH_PRODUCTION_AND_SALE", "QUAL-00072": "DIRECT_TARGET_PRODUCT_RESEARCH_PRODUCTION_AND_SALE", "QUAL-00103": "CONTROLLED_SUBSIDIARY_TARGET_PRODUCT_MANUFACTURE", "QUAL-00128": "DIRECT_TARGET_PRODUCT_PRODUCTION_AND_SALE", "QUAL-00129": "DIRECT_TARGET_PRODUCT_BUSINESS", "QUAL-00136": "DIRECT_TARGET_PRODUCT_PRODUCTION_AND_SALE", "QUAL-00141": "DIRECT_TARGET_PRODUCT_SALE", "QUAL-00148": "DIRECT_TARGET_PRODUCT_PRODUCTION", "QUAL-00149": "DIRECT_TARGET_RESOURCE_DEVELOPMENT_PRODUCTION_AND_SALE", "QUAL-00213": "DIRECT_TARGET_PRODUCT_RESEARCH_PRODUCTION_AND_SALE", "QUAL-00226": "DIRECT_TARGET_PRODUCT_RESEARCH_PRODUCTION_AND_SALE", "QUAL-00227": "DIRECT_TARGET_PRODUCT_RESEARCH_PRODUCTION_AND_SALE", "QUAL-00348": "DIRECT_TARGET_CELL_RESEARCH_PRODUCTION_AND_SALE", "QUAL-01029": "DIRECT_TARGET_SYSTEM_PRODUCT", "QUAL-01137": "DIRECT_TARGET_SYSTEM_PRODUCT", "QUAL-01625": "DIRECT_TARGET_WAFER_PRODUCTION", "QUAL-01639": "DIRECT_TARGET_SILICON_INGOT_AND_WAFER_PRODUCTION_AND_SALE", "QUAL-01732": "DIRECT_TARGET_WAFER_PRODUCT_BUSINESS", "QUAL-01846": "CONTROLLED_SUBSIDIARY_TARGET_COMPONENT_MANUFACTURE", "QUAL-01904": "DIRECT_TARGET_SPECIALTY_COMPONENT_PRODUCTION_AND_SALE", "QUAL-02192": "DIRECT_TARGET_COMPONENT_PRODUCT_SALE", "QUAL-02265": "DIRECT_TARGET_COMPONENT_PRODUCT_SALE", "QUAL-02299": "CONTROLLED_SUBSIDIARY_TARGET_COMPONENT_PRODUCTION_AND_SALE", "QUAL-02364": "CONTROLLED_SUBSIDIARY_TARGET_COMPONENT_RESEARCH_PRODUCTION_AND_SALE", "QUAL-02648": "DIRECT_TARGET_INVERTER_PRODUCT", "QUAL-02692": "CONTROLLED_SUBSIDIARY_TARGET_STATION_INVESTMENT_AND_OPERATION", "QUAL-02760": "CONTROLLED_SUBSIDIARY_TARGET_STATION_INVESTMENT_AND_OPERATION", "QUAL-02761": "DIRECT_TARGET_STATION_PROJECT_CONSTRUCTION_AND_OPERATION", "QUAL-02875": "CONTROLLED_SUBSIDIARY_TARGET_STATION_CONSTRUCTION_AND_OPERATION", "QUAL-02888": "DIRECT_TARGET_STATION_CONSTRUCTION_AND_OPERATION_WITH_REVENUE", "QUAL-02900": "CONTROLLED_SUBSIDIARY_TARGET_STATION_OPERATION", "QUAL-02956": "DIRECT_TARGET_STATION_EPC_AND_OPERATION_SERVICE", "QUAL-02982": "DIRECT_TARGET_STATION_CONSTRUCTION_OPERATION_AND_EXTERNAL_EXPANSION", "QUAL-02991": "DIRECT_TARGET_STATION_INVESTMENT_AND_OPERATION", "QUAL-03042": "DIRECT_TARGET_STATION_CONSTRUCTION_AND_OPERATION_WITH_REVENUE", "QUAL-03055": "DIRECT_TARGET_STATION_INVESTMENT_AND_OPERATION_WITH_REVENUE", "QUAL-03067": "DIRECT_TARGET_STATION_EPC_SERVICE", "QUAL-03199": "DIRECT_TARGET_WIND_COMPONENT_MANUFACTURE", "QUAL-03750": "DIRECT_TARGET_WIND_PROJECT_CONSTRUCTION_AND_OPERATION", } REJECTED_ROLE_DECISIONS: dict[str, str] = { "QUAL-00063": "ADJACENT_EQUIPMENT_AND_EPC_SERVICE_NOT_BATTERY_RESOURCE_OR_MAIN_MATERIAL", "QUAL-00223": "ADJACENT_INDUSTRIAL_GAS_SUPPLY_TO_BATTERY_MATERIAL_PROJECT", "QUAL-01640": "CUTTING_WIRE_AUXILIARY_NOT_SILICON_OR_WAFER_MANUFACTURE", "QUAL-01889": "COMPONENT_BACKSHEET_FILM_AND_EPOXY_AUXILIARY_NOT_CELL_OR_COMPONENT_MANUFACTURE", "QUAL-01901": "PHOTOVOLTAIC_GLASS_AUXILIARY_OR_POWER_GENERATION_NOT_CELL_OR_COMPONENT_MANUFACTURE", "QUAL-01962": "COMPONENT_ENCAPSULATION_MATERIAL_NOT_CELL_OR_COMPONENT_MANUFACTURE", "QUAL-02159": "COMPANY_PROCUREMENT_OF_COMPONENTS_AND_INVERTERS_NOT_TARGET_PRODUCT_BUSINESS", "QUAL-02499": "COMPONENTS_LISTED_AS_CONSUMED_RAW_MATERIAL_NOT_TARGET_PRODUCT_BUSINESS", "QUAL-02828": "INTERNAL_ROOFTOP_AND_STATION_SUPPORT_SERVICE_ONLY_WITHOUT_EXTERNAL_TARGET_BUSINESS", "QUAL-02852": "SELF_USE_TEXTILE_PROJECT_ONLY_WITHOUT_STATION_BUSINESS", } ROLE_HEADERS = [ "role_adjudication_id", "qualification_row_id", "task_id", "case_id", "batch_id", "run_id", "company_id", "security_code", "security_name", "track_code", "selection_bucket", "prior_page_search_result", "prior_qualification_result", "prior_keyword_hit_count", "prior_direct_context_hit_count", "full_page_search_reused", "human_semantic_review", "company_self_gate", "target_bucket_role_gate", "adjacent_or_self_use_exclusion_gate", "final_role_decision", "decision_basis_code", "selected_attachment_id", "selected_page", "selected_exact_sentence", "rule_version", "adjudicated_at", "tool_version", "review_status", "receipt_sha256", ] PRESTATE_HEADERS = [ "snapshot_id", "blocker_id", "record_type", "company_id", "security_code", "track_code", "state", "subject_type", "direct_business_source_id", "direct_business_locator", "evidence_grade", "exposure_specificity", "evidence_fact_id", "chain_node_id", "classification_reason", "snapshot_status", ] def role_receipt_path(industry_root: Path) -> Path: return industry_root / "extracted" / "candidate_role_adjudication_receipt.csv" def prestate_path(industry_root: Path) -> Path: return industry_root / "manifest" / "repair005_pollution_prestate_snapshot.csv" def validation_path(industry_root: Path) -> Path: return industry_root / "manifest" / "repair005_validation_receipt.json" def stable_adjudicated_at(industry_root: Path) -> str: path = role_receipt_path(industry_root) if path.exists(): rows = r4.read_csv(path) values = sorted({row.get("adjudicated_at", "") for row in rows if row.get("adjudicated_at")}) if len(values) == 1: return values[0] return r4.now_iso() def build_role_adjudication_receipt( industry_root: Path, pair_rows: list[dict[str, str]], candidates: list[dict[str, str]], ) -> list[dict[str, str]]: direct_ids = {row["qualification_row_id"] for row in pair_rows if int(row["direct_context_hit_count"]) > 0} frozen_decisions = set(APPROVED_ROLE_DECISIONS) | set(REJECTED_ROLE_DECISIONS) if len(pair_rows) != 858 or len({row["qualification_row_id"] for row in pair_rows}) != 858: raise RuntimeError("REPAIR005 requires the frozen 858 unique pair receipts") if len(direct_ids) != 50 or direct_ids != frozen_decisions: raise RuntimeError( f"the frozen 50 direct-context rows changed: direct={len(direct_ids)} " f"missing={sorted(direct_ids - frozen_decisions)} extra={sorted(frozen_decisions - direct_ids)}" ) if set(APPROVED_ROLE_DECISIONS) & set(REJECTED_ROLE_DECISIONS): raise RuntimeError("approved and rejected semantic decision sets overlap") candidate_by_pair = {(row["company_id"], row["track_code"]): row for row in candidates} adjudicated_at = stable_adjudicated_at(industry_root) results: list[dict[str, str]] = [] for source in sorted(pair_rows, key=lambda row: row["qualification_row_id"]): qid = source["qualification_row_id"] candidate = candidate_by_pair[(source["company_id"], source["track_code"])] attachment_id = "" page = "" sentence = "" if qid in APPROVED_ROLE_DECISIONS: attachment_id, page_number, sentence = r4.parse_direct_context(source) page = str(page_number) company_gate = "PASS" target_gate = "PASS" exclusion_gate = "PASS_NO_EXCLUSION_TRIGGERED" decision = "ELIGIBLE_DIRECT_COMPANY_SELF_TARGET_BUCKET_ROLE" basis = APPROVED_ROLE_DECISIONS[qid] human_review = "YES_ALL_PRIOR_DIRECT_CONTEXTS_REVIEWED" elif qid in REJECTED_ROLE_DECISIONS: attachment_id, page_number, sentence = r4.parse_direct_context(source) page = str(page_number) company_gate = "FAIL_OR_NOT_SAME_TARGET_ROLE" target_gate = "FAIL" exclusion_gate = "FAIL_EXCLUSION_TRIGGERED" decision = "HELD_ROLE_MISMATCH_ADJACENT_PROCUREMENT_OR_SELF_USE" basis = REJECTED_ROLE_DECISIONS[qid] human_review = "YES_ALL_PRIOR_DIRECT_CONTEXTS_REVIEWED" else: company_gate = "FAIL_NO_PRIOR_DIRECT_SELF_CONTEXT" target_gate = "FAIL_NO_PRIOR_DIRECT_TARGET_ROLE_CONTEXT" exclusion_gate = "NOT_APPLICABLE_NO_DIRECT_CONTEXT" if source["page_search_result"] == "ALL_REFERENCED_READABLE_PDFS_SEARCHED_NO_FROZEN_KEYWORD_HIT": decision = "HELD_TRUE_NEGATIVE_AFTER_FULL_PAGE_SEARCH" basis = "NO_FROZEN_KEYWORD_HIT_IN_REFERENCED_READABLE_PDFS" elif source["page_search_result"] == "FROZEN_KEYWORD_FOUND_BUT_NO_DIRECT_SELF_BUSINESS_PAGE_CONTEXT": decision = "HELD_KEYWORD_CONTEXT_ONLY_AFTER_FULL_PAGE_SEARCH" basis = "KEYWORD_CONTEXT_DID_NOT_PASS_COMPANY_SELF_AND_TARGET_ROLE_GATE" else: decision = "HELD_RETRIEVAL_OR_PARSE_GAP" basis = "REPAIR004_REVIEWABLE_RETRIEVAL_OR_PARSE_FAILURE" human_review = "NOT_REQUIRED_PRIOR_FULL_PAGE_GATE_FOUND_NO_DIRECT_CONTEXT" core: dict[str, str] = { "role_adjudication_id": f"ROLE-R005-{qid}", "qualification_row_id": qid, "task_id": TASK_ID, "case_id": CASE_ID, "batch_id": BATCH_ID, "run_id": RUN_ID, "company_id": source["company_id"], "security_code": source["security_code"], "security_name": candidate["security_name"], "track_code": source["track_code"], "selection_bucket": source["selection_bucket"], "prior_page_search_result": source["page_search_result"], "prior_qualification_result": source["qualification_result"], "prior_keyword_hit_count": source["keyword_hit_count"], "prior_direct_context_hit_count": source["direct_context_hit_count"], "full_page_search_reused": "YES_REPAIR004_ACCEPTED_729_PDFS_858_PAIR_SEARCH", "human_semantic_review": human_review, "company_self_gate": company_gate, "target_bucket_role_gate": target_gate, "adjacent_or_self_use_exclusion_gate": exclusion_gate, "final_role_decision": decision, "decision_basis_code": basis, "selected_attachment_id": attachment_id, "selected_page": page, "selected_exact_sentence": sentence, "rule_version": ROLE_RULE_VERSION, "adjudicated_at": adjudicated_at, "tool_version": TOOL_VERSION, "review_status": REVIEW_STATUS, } payload = json.dumps(core, ensure_ascii=False, sort_keys=True, separators=(",", ":")).encode("utf-8") core["receipt_sha256"] = r4.sha256_bytes(payload) results.append({key: core.get(key, "") for key in ROLE_HEADERS}) r4.write_csv(role_receipt_path(industry_root), ROLE_HEADERS, results) return results def write_prestate_snapshot_once( industry_root: Path, candidates: list[dict[str, str]], classifications: list[dict[str, str]], ) -> list[dict[str, str]]: path = prestate_path(industry_root) if path.exists(): prior = r4.read_csv(path) rows = [ row for row in prior if ( row["record_type"] == "CANDIDATE_LEDGER" and bool(row["direct_business_source_id"]) and row["evidence_grade"] in {"S", "A"} ) or ( row["record_type"] == "CLASSIFICATION_SUMMARY" and ( row["subject_type"] == "COMPANY_DIRECT_BUSINESS" or bool(row["evidence_fact_id"]) ) ) ] if len(rows) != len(prior): r4.write_csv(path, PRESTATE_HEADERS, rows) return rows rows: list[dict[str, str]] = [] for candidate in candidates: held = candidate["candidate_state"] == "HELD_BY_EVIDENCE_GAP" polluted = ( held and bool(candidate.get("direct_business_source_id", "")) and candidate.get("evidence_grade", "") in {"S", "A"} ) if not polluted: continue rows.append({ "snapshot_id": f"PRE-R005-LEDGER-{candidate['track_code']}-{candidate['security_code']}", "blocker_id": "B1-R004-B", "record_type": "CANDIDATE_LEDGER", "company_id": candidate["company_id"], "security_code": candidate["security_code"], "track_code": candidate["track_code"], "state": candidate["candidate_state"], "subject_type": "", "direct_business_source_id": candidate["direct_business_source_id"], "direct_business_locator": candidate["direct_business_locator"], "evidence_grade": candidate["evidence_grade"], "exposure_specificity": candidate["exposure_specificity"], "evidence_fact_id": "", "chain_node_id": candidate["chain_nodes"], "classification_reason": candidate["include_or_exclude_reason"], "snapshot_status": "FROZEN_PRE_REPAIR005_POLLUTION_EVIDENCE", }) for row in classifications: held = row["data_status"] == "HELD_BY_EVIDENCE_GAP" polluted = held and ( row.get("subject_type", "") == "COMPANY_DIRECT_BUSINESS" or bool(row.get("evidence_fact_id", "")) ) if not polluted: continue rows.append({ "snapshot_id": f"PRE-R005-CLASS-{row['track_code']}-{row['company_id']}", "blocker_id": "B1-R004-B", "record_type": "CLASSIFICATION_SUMMARY", "company_id": row["company_id"], "security_code": row["company_id"].split(":")[-1], "track_code": row["track_code"], "state": row["data_status"], "subject_type": row["subject_type"], "direct_business_source_id": row["source_doc_id"], "direct_business_locator": "", "evidence_grade": "", "exposure_specificity": "", "evidence_fact_id": row["evidence_fact_id"], "chain_node_id": row["chain_node_id"], "classification_reason": row["classification_reason"], "snapshot_status": "FROZEN_PRE_REPAIR005_POLLUTION_EVIDENCE", }) if not rows: raise RuntimeError("expected REPAIR004 polluted prestate was not found before REPAIR005") r4.write_csv(path, PRESTATE_HEADERS, rows) return rows def clear_noneligible_candidate(candidate: dict[str, str], reason: str) -> None: candidate["chain_nodes"] = "" candidate["direct_business_source_id"] = "" candidate["direct_business_locator"] = "" candidate["evidence_grade"] = "" candidate["exposure_specificity"] = "" candidate["latest_disclosed_period"] = "" candidate["candidate_state"] = "HELD_BY_EVIDENCE_GAP" candidate["selection_rank"] = "" candidate["tier"] = "" candidate["tie_break_rule"] = "" candidate["include_or_exclude_reason"] = reason def build_evidence_and_candidates( industry_root: Path, role_rows: list[dict[str, str]], source_rows: list[dict[str, str]], ) -> tuple[list[dict[str, str]], list[dict[str, str]]]: evidence_path = industry_root / "evidence" / "evidence_fact_table.csv" prior_evidence = r4.read_csv(evidence_path) evidence_headers = list(prior_evidence[0]) evidence_rows = [row for row in prior_evidence if not row["evidence_fact_id"].startswith("EVF-QUAL-")] candidate_path = industry_root / "extracted" / "company_track_candidate_ledger.csv" candidates = r4.read_csv(candidate_path) source_by_id = {row["doc_id"]: row for row in source_rows} candidate_by_pair = {(row["company_id"], row["track_code"]): row for row in candidates} role_by_pair = {(row["company_id"], row["track_code"]): row for row in role_rows} for role in role_rows: if role["final_role_decision"] != "ELIGIBLE_DIRECT_COMPANY_SELF_TARGET_BUCKET_ROLE": continue pair = (role["company_id"], role["track_code"]) candidate = candidate_by_pair[pair] attachment_id = role["selected_attachment_id"] page = role["selected_page"] doc_id = f"S-QUAL-AR-{attachment_id}" source = source_by_id[doc_id] evidence_id = f"EVF-QUAL-{role['track_code']}-{role['security_code']}-R005" evidence = { "evidence_fact_id": evidence_id, "task_id": TASK_ID, "case_id": CASE_ID, "batch_id": BATCH_ID, "run_id": RUN_ID, "doc_id": doc_id, "industry_case": "新能源案例", "industry_id": "IND-NEWENERGY", "subindustry_id": role["track_code"], "company_id": role["company_id"], "track_code": role["track_code"], "chain_node_id": r4.CHAIN_NODES[(role["track_code"], role["selection_bucket"])], "subject_type": "COMPANY", "subject_id": role["company_id"], "source_text_path": source["converted_text_path"], "raw_pool_path": source["raw_pool_path"], "raw_file_sha256": source["file_sha256"], "source_page": page, "source_table_id": "", "source_sentence_index": "", "locator_type": "PDF_PAGE_AND_EXACT_CONTEXT", "locator_value": f"attachment_id={attachment_id};page={page};receipt={role['qualification_row_id']};role_receipt={role['role_adjudication_id']}", "evidence_text": role["selected_exact_sentence"], "evidence_type": "OFFICIAL_ANNUAL_REPORT", "statement_type": "FACT", "business_dimension": "COMPANY_EXPOSURE", "research_dimension": "COMPANY_TRACK_QUALIFICATION", "numeric_value_raw": "", "metric_candidate_name": "", "metric_candidate_unit": "", "metric_period": "2025", "metric_date": "2025-12-31", "geography": "CN", "original_qualifier": ( "REPAIR005复用已审核的CNINFO年度报告与全页检索回执,并对全部50条先前direct-context做公司自身/" "受控子公司与目标桶角色人读裁决;仅支持直接业务事实,不作份额、质量或投资判断。" ), "related_company_id": "", "viewpoint_id": "", "darkline_signal_flag": "NO", "confidence_level": "HIGH", "conclusion_strength": "DIRECT_FACT", "sensitivity_screen": "LEGAL_PUBLIC_SCREENED_HIGH_LEVEL_ONLY", "contradicts_evidence_fact_id": "", "normalization_status": "NORMALIZED", "processing_status": "READY", "data_status": "VERIFIED_PUBLIC", "review_status": REVIEW_STATUS, } evidence_rows.append({key: evidence.get(key, "") for key in evidence_headers}) candidate.update({ "chain_nodes": r4.CHAIN_NODES[(role["track_code"], role["selection_bucket"])], "direct_business_source_id": doc_id, "direct_business_locator": f"2025年报第{page}页;attachment_id={attachment_id};role_receipt={role['role_adjudication_id']}", "evidence_grade": "S", "exposure_specificity": "NAMED_PRODUCT_PROJECT_AND_OPERATING_FACT", "latest_disclosed_period": "2025-12-31", }) evidence_by_pair: dict[tuple[str, str], list[dict[str, str]]] = defaultdict(list) for evidence in evidence_rows: if evidence["subject_type"] == "COMPANY" and evidence["company_id"] and evidence["track_code"]: evidence_by_pair[(evidence["company_id"], evidence["track_code"])].append(evidence) eligible_groups: dict[tuple[str, str], list[dict[str, str]]] = defaultdict(list) valid_specificity = { "SEGMENT_REVENUE_OR_ASSET_DISCLOSED", "NAMED_PRODUCT_PROJECT_AND_OPERATING_FACT", "GENERAL_DIRECT_BUSINESS_DESCRIPTION", } for candidate in candidates: pair = (candidate["company_id"], candidate["track_code"]) matching = [ evidence for evidence in evidence_by_pair.get(pair, []) if candidate["direct_business_source_id"] and evidence["doc_id"] == candidate["direct_business_source_id"] ] eligible = bool(matching) and candidate["evidence_grade"] in {"S", "A"} and candidate["exposure_specificity"] in valid_specificity if eligible: candidate["candidate_state"] = "ELIGIBLE" candidate["selection_rank"] = "" candidate["tier"] = "" candidate["tie_break_rule"] = "source_grade>exposure_specificity>latest_period>publish_date>exchange_code>security_code" if pair in role_by_pair: candidate["include_or_exclude_reason"] = ( "REPAIR005复用年度报告PDF与全页检索,对先前direct-context执行公司自身/受控子公司和目标桶角色语义裁决;" "S级主源、页码原句与全部资格gate通过,进入同桶ELIGIBLE机械排序。" ) else: candidate["include_or_exclude_reason"] = ( "设计前序已登记直接业务主源与证据事实在REPAIR005重放中保持有效;全部资格gate通过,进入同桶ELIGIBLE机械排序。" ) eligible_groups[(candidate["track_code"], candidate["selection_bucket"])].append(candidate) else: role = role_by_pair.get(pair) if role: reason = ( f"REPAIR005全量重放结论={role['final_role_decision']};" f"依据={role['decision_basis_code']};未通过公司自身及目标桶直接业务证据闸门,保持HELD。" ) else: reason = "REPAIR005重放未找到可与候选直接业务字段闭合的S/A级公司证据,清除全部资格派生字段并保持HELD。" clear_noneligible_candidate(candidate, reason) grade_order = {"S": 0, "A": 1} specificity_order = { "SEGMENT_REVENUE_OR_ASSET_DISCLOSED": 0, "NAMED_PRODUCT_PROJECT_AND_OPERATING_FACT": 1, "GENERAL_DIRECT_BUSINESS_DESCRIPTION": 2, } for group_rows in eligible_groups.values(): group_rows.sort(key=lambda row: ( grade_order.get(row["evidence_grade"], 9), specificity_order.get(row["exposure_specificity"], 9), r4.descending_date(row["latest_disclosed_period"]), r4.descending_date(source_by_id[row["direct_business_source_id"]]["publish_date"]), row["exchange_code"], row["security_code"], )) for rank, candidate in enumerate(group_rows, 1): candidate["selection_rank"] = str(rank) if rank == 1: candidate["candidate_state"] = "INCLUDED_T1" candidate["tier"] = "T1_PRIMARY" elif rank == 2: candidate["candidate_state"] = "INCLUDED_T2" candidate["tier"] = "T2_CANDIDATE" else: candidate["candidate_state"] = "ELIGIBLE_NOT_SELECTED_BATCH001" candidate["tier"] = "" candidate["include_or_exclude_reason"] += " 同桶排名超过2,保留为ELIGIBLE_NOT_SELECTED_BATCH001。" track_order = {"BATTERY": 0, "SOLAR": 1, "WIND": 2, "NUCLEAR": 3} bucket_order = {key: index for index, key in enumerate(r4.TRACK_KEYWORDS)} state_order = { "INCLUDED_T1": 0, "INCLUDED_T2": 1, "ELIGIBLE_NOT_SELECTED_BATCH001": 2, "HELD_BY_EVIDENCE_GAP": 3, } candidates.sort(key=lambda row: ( track_order[row["track_code"]], bucket_order[(row["track_code"], row["selection_bucket"])], state_order.get(row["candidate_state"], 9), int(row["selection_rank"] or 999999), row["exchange_code"], row["security_code"], )) r4.write_csv(evidence_path, evidence_headers, evidence_rows) r4.write_csv(candidate_path, list(candidates[0]), candidates) return evidence_rows, candidates def update_funnel( industry_root: Path, role_rows: list[dict[str, str]], candidates: list[dict[str, str]], evidence_rows: list[dict[str, str]], source_rows: list[dict[str, str]], ) -> list[dict[str, str]]: path = industry_root / "extracted" / "candidate_qualification_funnel.csv" funnel = r4.read_csv(path) role_by_pair = {(row["company_id"], row["track_code"]): row for row in role_rows} candidate_by_pair = {(row["company_id"], row["track_code"]): row for row in candidates} source_by_id = {row["doc_id"]: row for row in source_rows} evidence_by_pair: dict[tuple[str, str], list[dict[str, str]]] = defaultdict(list) for evidence in evidence_rows: if evidence["subject_type"] == "COMPANY": evidence_by_pair[(evidence["company_id"], evidence["track_code"])].append(evidence) eligible_states = {"INCLUDED_T1", "INCLUDED_T2", "ELIGIBLE_NOT_SELECTED_BATCH001"} for row in funnel: pair = (row["company_id"], row["track_code"]) candidate = candidate_by_pair[pair] role = role_by_pair.get(pair) eligible = candidate["candidate_state"] in eligible_states matching = [ evidence for evidence in evidence_by_pair.get(pair, []) if candidate["direct_business_source_id"] and evidence["doc_id"] == candidate["direct_business_source_id"] ] if role: row["page_level_verification_attempt"] = "REUSE_REPAIR004_FULL_PAGE_SEARCH_PLUS_REPAIR005_COMPANY_SELF_TARGET_BUCKET_ROLE_ADJUDICATION" row["page_level_verification_result"] = role["final_role_decision"] row["business_context_rule_result"] = ( "REPAIR005_COMPANY_SELF_TARGET_BUCKET_ROLE_CONFIRMED" if role["final_role_decision"] == "ELIGIBLE_DIRECT_COMPANY_SELF_TARGET_BUCKET_ROLE" else "REPAIR005_HELD_BY_ROLE_CONTEXT_OR_SEARCH_RESULT" ) row["replay_status"] = "REPAIR005_FULL_858_ROLE_STATE_REPLAY_COMPLETED" row["direct_business_source_id"] = candidate["direct_business_source_id"] if eligible else "" for gate in ["direct_source_gate", "locator_gate", "company_evidence_fact_gate", "source_grade_gate", "exposure_specificity_gate"]: row[gate] = "PASS" if eligible else "FAIL" row["evidence_fact_ids"] = ";".join(sorted(e["evidence_fact_id"] for e in matching)) if eligible else "" row["failed_gates"] = "" if eligible else "DIRECT_BUSINESS_SOURCE;PAGE_OR_TEXT_LOCATOR;COMPANY_EVIDENCE_FACT;SOURCE_GRADE_S_OR_A;EXPOSURE_SPECIFICITY" row["eligibility_result"] = "ELIGIBLE" if eligible else "HELD_BY_EVIDENCE_GAP" row["eligible_rank_in_bucket"] = candidate["selection_rank"] if eligible else "" row["final_candidate_state"] = candidate["candidate_state"] if eligible: source = source_by_id[candidate["direct_business_source_id"]] row["mechanical_sort_key"] = "|".join([ candidate["evidence_grade"], candidate["exposure_specificity"], candidate["latest_disclosed_period"], source["publish_date"], candidate["exchange_code"], candidate["security_code"], ]) else: row["mechanical_sort_key"] = "" row["funnel_rule_version"] = ROLE_RULE_VERSION row["review_status"] = REVIEW_STATUS r4.write_csv(path, list(funnel[0]), funnel) return funnel def update_classification_and_matrix( industry_root: Path, candidates: list[dict[str, str]], evidence_rows: list[dict[str, str]], source_rows: list[dict[str, str]], ) -> tuple[list[dict[str, str]], list[dict[str, str]]]: candidate_by_pair = {(row["company_id"], row["track_code"]): row for row in candidates} source_by_id = {row["doc_id"]: row for row in source_rows} evidence_by_pair: dict[tuple[str, str], list[dict[str, str]]] = defaultdict(list) for evidence in evidence_rows: if evidence["subject_type"] == "COMPANY": evidence_by_pair[(evidence["company_id"], evidence["track_code"])].append(evidence) classification_path = industry_root / "extracted" / "classification_summary.csv" classifications = r4.read_csv(classification_path) eligible_states = {"INCLUDED_T1", "INCLUDED_T2", "ELIGIBLE_NOT_SELECTED_BATCH001"} for row in classifications: pair = (row["company_id"], row["track_code"]) candidate = candidate_by_pair[pair] eligible = candidate["candidate_state"] in eligible_states matching = [ evidence for evidence in evidence_by_pair.get(pair, []) if candidate["direct_business_source_id"] and evidence["doc_id"] == candidate["direct_business_source_id"] ] row["classification_reason"] = candidate["include_or_exclude_reason"] row["data_status"] = candidate["candidate_state"] row["review_status"] = REVIEW_STATUS if eligible and matching: evidence = matching[0] source = source_by_id[evidence["doc_id"]] row["subject_type"] = "COMPANY_DIRECT_BUSINESS" row["source_doc_id"] = evidence["doc_id"] row["evidence_fact_id"] = evidence["evidence_fact_id"] row["chain_node_id"] = candidate["chain_nodes"] row["raw_pool_path"] = source["raw_pool_path"] row["raw_file_sha256"] = source["file_sha256"] else: discovery_source = source_by_id.get(candidate["candidate_source_id"], {}) row["subject_type"] = "COMPANY_QUERY_HIT" row["source_doc_id"] = discovery_source.get("doc_id", "") row["evidence_fact_id"] = "" row["chain_node_id"] = "" row["raw_pool_path"] = discovery_source.get("raw_pool_path", "") row["raw_file_sha256"] = discovery_source.get("file_sha256", "") r4.write_csv(classification_path, list(classifications[0]), classifications) selected = [row for row in candidates if row["candidate_state"] in {"INCLUDED_T1", "INCLUDED_T2"}] matrix_path = industry_root / "extracted" / "newenergy_company_exposure_matrix.csv" matrix_headers = list(r4.read_csv(matrix_path)[0]) matrix_rows: list[dict[str, str]] = [] for candidate in selected: pair = (candidate["company_id"], candidate["track_code"]) evidence = next( evidence for evidence in evidence_by_pair[pair] if evidence["doc_id"] == candidate["direct_business_source_id"] ) row = { "mapping_id": f"MAP-{candidate['track_code']}-{candidate['security_code']}", "task_id": TASK_ID, "case_id": CASE_ID, "batch_id": BATCH_ID, "run_id": RUN_ID, "company_id": candidate["company_id"], "security_code": candidate["security_code"], "security_name": candidate["security_name"], "legal_name": candidate["legal_name"], "exchange_code": candidate["exchange_code"], "track_code": candidate["track_code"], "chain_nodes": candidate["chain_nodes"], "selection_bucket": candidate["selection_bucket"], "tier": candidate["tier"], "candidate_state": candidate["candidate_state"], "direct_business_source_id": candidate["direct_business_source_id"], "direct_business_locator": candidate["direct_business_locator"], "evidence_fact_id": evidence["evidence_fact_id"], "evidence_grade": candidate["evidence_grade"], "exposure_specificity": candidate["exposure_specificity"], "latest_disclosed_period": candidate["latest_disclosed_period"], "primary_region": "MAINLAND_CHINA", "scope_status": "CORE_SCOPE_DIRECT_BUSINESS", "coverage_claim": "NONE_INITIAL_CANDIDATE_POOL_ONLY", "data_status": "VERIFIED_PUBLIC", "review_status": REVIEW_STATUS, } matrix_rows.append({key: row.get(key, "") for key in matrix_headers}) r4.write_csv(matrix_path, matrix_headers, matrix_rows) return classifications, matrix_rows def build_manifests( industry_root: Path, source_rows: list[dict[str, str]], conversion_rows: list[dict[str, str]], evidence_rows: list[dict[str, str]], candidates: list[dict[str, str]], case_map: list[dict[str, str]], ) -> tuple[list[dict[str, str]], list[dict[str, str]]]: base.COLLECTED_AT = max( (row["collected_at"] for row in source_rows if row.get("collected_at")), default="2026-08-06T00:00:00+08:00", ) base.ARTIFACT_TOOL_VERSION = TOOL_VERSION base.ARTIFACT_PARAMETERS_SUMMARY = ( "B1 REPAIR005; reuse 729 acquired PDFs; replay 858 pairs through the company-self and " "target-bucket semantic role gate; clear non-eligible derived fields and dangling evidence FKs; " "mechanical rerank; exact-set hash coverage" ) base.ARTIFACT_STATUS = "READY_FOR_REPAIR005_FOCUSED_REREVIEW" base.build_input_manifests(source_rows) base.build_source_gap_audit(source_rows, evidence_rows) universe_rows = r4.read_csv(industry_root / "extracted" / "a_share_universe.csv") base.rewrite_batch_summary(candidates, len(source_rows), len(conversion_rows), universe_rows) base.build_human_receipt(case_map, len(source_rows)) output_rows = base.rebuild_output_manifest() artifact_rows = base.build_artifact_manifest(source_rows, output_rows, Path(__file__)) return output_rows, artifact_rows def split_evidence_ids(value: str) -> list[str]: return [item.strip() for item in (value or "").split(";") if item.strip()] def validate_and_write_receipt( industry_root: Path, role_rows: list[dict[str, str]], prestate_rows: list[dict[str, str]], source_rows: list[dict[str, str]], conversion_rows: list[dict[str, str]], evidence_rows: list[dict[str, str]], candidates: list[dict[str, str]], funnel: list[dict[str, str]], classifications: list[dict[str, str]], matrix_rows: list[dict[str, str]], case_map: list[dict[str, str]], ) -> dict[str, Any]: errors: list[str] = [] eligible_states = {"INCLUDED_T1", "INCLUDED_T2", "ELIGIBLE_NOT_SELECTED_BATCH001"} evidence_ids = {row["evidence_fact_id"] for row in evidence_rows} source_ids = {row["doc_id"] for row in source_rows} conversion_ids = {row["source_doc_id"] for row in conversion_rows} candidate_by_pair = {(row["company_id"], row["track_code"]): row for row in candidates} role_counts = Counter(row["final_role_decision"] for row in role_rows) expected_role_counts = { "ELIGIBLE_DIRECT_COMPANY_SELF_TARGET_BUCKET_ROLE": 40, "HELD_ROLE_MISMATCH_ADJACENT_PROCUREMENT_OR_SELF_USE": 10, "HELD_KEYWORD_CONTEXT_ONLY_AFTER_FULL_PAGE_SEARCH": 221, "HELD_TRUE_NEGATIVE_AFTER_FULL_PAGE_SEARCH": 587, } if dict(role_counts) != expected_role_counts: errors.append(f"role distribution mismatch: {dict(role_counts)}") if len(role_rows) != 858 or len({row["qualification_row_id"] for row in role_rows}) != 858: errors.append("role adjudication receipt is not exact 858") if any(row["rule_version"] != ROLE_RULE_VERSION or row["tool_version"] != TOOL_VERSION for row in role_rows): errors.append("role receipt rule/tool version mismatch") candidate_counts = Counter(row["candidate_state"] for row in candidates) expected_candidate_counts = { "INCLUDED_T1": 16, "INCLUDED_T2": 16, "ELIGIBLE_NOT_SELECTED_BATCH001": 40, "HELD_BY_EVIDENCE_GAP": 4479, } if dict(candidate_counts) != expected_candidate_counts: errors.append(f"candidate distribution mismatch: {dict(candidate_counts)}") if len(candidates) != 4551 or len({(row["company_id"], row["track_code"]) for row in candidates}) != 4551: errors.append("candidate ledger pair set is not exact 4551") noneligible_derived_fields = [ "chain_nodes", "direct_business_source_id", "direct_business_locator", "evidence_grade", "exposure_specificity", "latest_disclosed_period", "selection_rank", "tier", "tie_break_rule", ] polluted_candidates = [ row for row in candidates if row["candidate_state"] not in eligible_states and any(row.get(field, "") for field in noneligible_derived_fields) ] if polluted_candidates: errors.append(f"noneligible candidate derived-field pollution={len(polluted_candidates)}") polluted_classifications = [ row for row in classifications if row["data_status"] not in eligible_states and ( row["subject_type"] == "COMPANY_DIRECT_BUSINESS" or bool(row["evidence_fact_id"]) or row["source_doc_id"].startswith("S-QUAL-") or bool(row["chain_node_id"]) ) ] if polluted_classifications: errors.append(f"noneligible classification pollution={len(polluted_classifications)}") for qid in REJECTED_ROLE_DECISIONS: role = next(row for row in role_rows if row["qualification_row_id"] == qid) candidate = candidate_by_pair[(role["company_id"], role["track_code"])] if candidate["candidate_state"] != "HELD_BY_EVIDENCE_GAP" or any( candidate.get(field, "") for field in noneligible_derived_fields ): errors.append(f"rejected role candidate not clean: {qid}") selected_groups: dict[tuple[str, str], list[dict[str, str]]] = defaultdict(list) for candidate in candidates: if candidate["candidate_state"] in {"INCLUDED_T1", "INCLUDED_T2"}: selected_groups[(candidate["track_code"], candidate["selection_bucket"])].append(candidate) if len(selected_groups) != 16 or any( sorted(row["candidate_state"] for row in rows) != ["INCLUDED_T1", "INCLUDED_T2"] for rows in selected_groups.values() ): errors.append("16-bucket T1/T2 coverage mismatch") if source_ids != conversion_ids or len(source_rows) != 786 or len(conversion_rows) != 786: errors.append(f"source/conversion exact set mismatch {len(source_rows)}/{len(conversion_rows)}") if any(row["direct_business_source_id"] not in source_ids for row in candidates if row["candidate_state"] in eligible_states): errors.append("eligible candidate source FK dangling") if any(evidence_id not in evidence_ids for row in funnel for evidence_id in split_evidence_ids(row["evidence_fact_ids"])): errors.append("funnel evidence FK dangling") if any(row["evidence_fact_id"] and row["evidence_fact_id"] not in evidence_ids for row in classifications): errors.append("classification evidence FK dangling") if any(row["evidence_fact_id"] not in evidence_ids for row in matrix_rows): errors.append("exposure matrix evidence FK dangling") if any(row["evidence_fact_id"] not in evidence_ids for row in case_map): errors.append("case map evidence FK dangling") new_qual_ids = {row["evidence_fact_id"] for row in evidence_rows if row["evidence_fact_id"].startswith("EVF-QUAL-")} if len(new_qual_ids) != 40 or any(not evidence_id.endswith("-R005") for evidence_id in new_qual_ids): errors.append(f"REPAIR005 qualification evidence exact set mismatch={len(new_qual_ids)}") if len(matrix_rows) != 32 or len(case_map) != 110: errors.append(f"matrix/map count mismatch {len(matrix_rows)}/{len(case_map)}") if len(funnel) != 4551: errors.append(f"funnel count mismatch={len(funnel)}") if len(prestate_rows) != 8: errors.append(f"direct-claim pollution prestate snapshot mismatch={len(prestate_rows)}") core_hashes = { "role_adjudication_receipt": r4.sha256_file(role_receipt_path(industry_root)), "candidate_ledger": r4.sha256_file(industry_root / "extracted" / "company_track_candidate_ledger.csv"), "qualification_funnel": r4.sha256_file(industry_root / "extracted" / "candidate_qualification_funnel.csv"), "classification_summary": r4.sha256_file(industry_root / "extracted" / "classification_summary.csv"), "evidence_fact_table": r4.sha256_file(industry_root / "evidence" / "evidence_fact_table.csv"), "exposure_matrix": r4.sha256_file(industry_root / "extracted" / "newenergy_company_exposure_matrix.csv"), "case_evidence_map": r4.sha256_file(industry_root / CASE_ID / "evidence" / "case_evidence_map.csv"), "pollution_prestate_snapshot": r4.sha256_file(prestate_path(industry_root)), } receipt = { "task_id": TASK_ID, "case_id": CASE_ID, "batch_id": BATCH_ID, "run_id": RUN_ID, "tool_version": TOOL_VERSION, "rule_version": ROLE_RULE_VERSION, "generated_at": role_rows[0]["adjudicated_at"], "review_status": REVIEW_STATUS, "scope": "B1_REPAIR005_ONLY_REUSE_729_PDFS_FULL_858_ROLE_STATE_REPLAY", "role_status_distribution": dict(sorted(role_counts.items())), "candidate_state_distribution": dict(sorted(candidate_counts.items())), "prestate_direct_claim_pollution_rows": len(prestate_rows), "poststate_noneligible_candidate_pollution_rows": len(polluted_candidates), "poststate_noneligible_classification_pollution_rows": len(polluted_classifications), "dangling_fk_count": sum("FK dangling" in error for error in errors), "selected_bucket_count": len(selected_groups), "selected_mapping_count": len(matrix_rows), "source_count": len(source_rows), "conversion_count": len(conversion_rows), "evidence_fact_count": len(evidence_rows), "case_map_count": len(case_map), "core_hashes": core_hashes, "validation_errors": errors, "validation_status": "PASS" if not errors else "FAIL", } path = validation_path(industry_root) path.parent.mkdir(parents=True, exist_ok=True) path.write_text(json.dumps(receipt, ensure_ascii=False, sort_keys=True, indent=2) + "\n", encoding="utf-8") if errors: raise RuntimeError("REPAIR005 validation failed: " + "; ".join(errors)) return receipt def validate_artifacts( industry_root: Path, source_rows: list[dict[str, str]], output_rows: list[dict[str, str]], artifact_rows: list[dict[str, str]], ) -> dict[str, Any]: formal = base.formal_files(Path(__file__)) formal_by_rel = {base.rel(path): path for path in formal} artifact_by_rel = {row["relative_path"]: row for row in artifact_rows} errors: list[str] = [] if set(formal_by_rel) != set(artifact_by_rel): errors.append( f"artifact exact set diff missing={sorted(set(formal_by_rel) - set(artifact_by_rel))[:5]} " f"extra={sorted(set(artifact_by_rel) - set(formal_by_rel))[:5]}" ) for relative_path, path in formal_by_rel.items(): artifact = artifact_by_rel.get(relative_path) if not artifact: continue if artifact["file_size"] != str(path.stat().st_size) or artifact["sha256"] != r4.sha256_file(path): errors.append(f"artifact hash mismatch {relative_path}") break if any(row["tool_version"] != TOOL_VERSION for row in artifact_rows): errors.append("artifact tool version is not REPAIR-005") for row in output_rows: path = base.PROJECT_ROOT / row["output_path"] if not path.exists() or row["output_sha256"] != r4.sha256_file(path): errors.append(f"output hash mismatch {row['output_path']}") break if len(source_rows) != 786 or len(output_rows) != 24: errors.append(f"source/output count mismatch {len(source_rows)}/{len(output_rows)}") if errors: raise RuntimeError("REPAIR005 artifact validation failed: " + "; ".join(errors)) return { "formal_exact_set": len(formal), "artifact_rows": len(artifact_rows), "output_rows": len(output_rows), "artifact_manifest_sha256": r4.sha256_file(industry_root / "manifest" / "artifact_manifest.csv"), "output_manifest_sha256": r4.sha256_file( industry_root / CASE_ID / "manifest" / "output_manifest.csv" ), } def run() -> dict[str, Any]: project_root = Path.cwd().resolve() industry_root = r4.find_industry_root(project_root) source_rows = r4.read_csv(industry_root / "manifest" / "source_document.csv") conversion_rows = r4.read_csv(industry_root / "manifest" / "conversion_status.csv") pair_rows = r4.read_csv(industry_root / "extracted" / "candidate_page_qualification_receipt.csv") candidates_before = r4.read_csv(industry_root / "extracted" / "company_track_candidate_ledger.csv") classifications_before = r4.read_csv(industry_root / "extracted" / "classification_summary.csv") prestate_rows = write_prestate_snapshot_once(industry_root, candidates_before, classifications_before) role_rows = build_role_adjudication_receipt(industry_root, pair_rows, candidates_before) evidence_rows, candidates = build_evidence_and_candidates(industry_root, role_rows, source_rows) funnel = update_funnel(industry_root, role_rows, candidates, evidence_rows, source_rows) classifications, matrix_rows = update_classification_and_matrix( industry_root, candidates, evidence_rows, source_rows, ) case_map = r4.write_company_outputs_and_map(industry_root, candidates, evidence_rows, source_rows) validation = validate_and_write_receipt( industry_root, role_rows, prestate_rows, source_rows, conversion_rows, evidence_rows, candidates, funnel, classifications, matrix_rows, case_map, ) output_rows, artifact_rows = build_manifests( industry_root, source_rows, conversion_rows, evidence_rows, candidates, case_map, ) artifact_validation = validate_artifacts(industry_root, source_rows, output_rows, artifact_rows) return { "status": "REPAIR005_FORMAL_PACKAGE_REBUILT_DRAFT_FOR_REVIEW", "role_status_distribution": validation["role_status_distribution"], "candidate_state_distribution": validation["candidate_state_distribution"], "dangling_fk_count": validation["dangling_fk_count"], "prestate_pollution_rows": validation["prestate_direct_claim_pollution_rows"], "poststate_pollution_rows": ( validation["poststate_noneligible_candidate_pollution_rows"] + validation["poststate_noneligible_classification_pollution_rows"] ), "sources": len(source_rows), "conversions": len(conversion_rows), "evidence_facts": len(evidence_rows), "candidates": len(candidates), "selected": len(matrix_rows), "case_map": len(case_map), **artifact_validation, "role_receipt_sha256": r4.sha256_file(role_receipt_path(industry_root)), "validation_receipt_sha256": r4.sha256_file(validation_path(industry_root)), } def main() -> None: parser = argparse.ArgumentParser() parser.parse_args() result = run() print(json.dumps(result, ensure_ascii=False, sort_keys=True, indent=2)) if __name__ == "__main__": main()