from __future__ import annotations import argparse import getpass import hashlib import json import os import re from datetime import datetime from typing import Iterable import pymysql ARCHIVE_BATCH_ID = "DL_FULL_ARCHIVE_20260624_V1" SOURCE_ID = "SOURCE_LEGACY_EXTERNAL_INFORMATION_CASEBOOK" SOURCE_PATH = "legacy_source/external_information_casebook.md" def sha256_text(text: str) -> str: return hashlib.sha256(text.encode("utf-8")).hexdigest() def get_password(args: argparse.Namespace) -> str: if args.password: return args.password if os.environ.get("DARKLINE_MYSQL_PASSWORD"): return os.environ["DARKLINE_MYSQL_PASSWORD"] if os.environ.get("TIANXIA_MYSQL_PASSWORD"): return os.environ["TIANXIA_MYSQL_PASSWORD"] return getpass.getpass("MySQL password: ") def connect(args: argparse.Namespace): return pymysql.connect( host=args.host, port=args.port, user=args.user, password=get_password(args), database=args.database, charset="utf8mb4", autocommit=True, cursorclass=pymysql.cursors.DictCursor, ) def normalize_text(text: str) -> str: return re.sub(r"\s+", " ", text.strip()) def compact(text: str, limit: int = 480) -> str: text = normalize_text(text) return text if len(text) <= limit else text[: limit - 3] + "..." def split_reasoning_units(body: str) -> list[dict[str, str]]: units: list[dict[str, str]] = [] current_heading = "" for raw in body.splitlines(): line = raw.strip() if not line: continue if line.startswith("#"): current_heading = line.strip("# ").strip() units.append({"heading": current_heading, "text": line, "is_heading": "1"}) continue # Preserve bullets, numbered lines and dense paragraphs. They usually carry # the reasoning chain in our darkline casebook. if len(line) >= 8: units.append({"heading": current_heading, "text": line, "is_heading": "0"}) return units def classify_step_type(text: str) -> str: lower = text.lower() if re.search(r"为什么|為什麼|why", text, flags=re.I): return "WHY_NODE" if re.search(r"暗线|意图|目标|人心|组织意志|反推|操纵|主力", text): return "DARKLINE_INTENTION" if re.search(r"新闻|公告|事件|看到|叶子|b/c|初始", lower): return "INITIAL_LEAF" if re.search(r"之前|前置|铺垫|早已|提前|m节点|M:|M:", text): return "PRIOR_EXPECTED_LINE" if re.search(r"后续|接下来|将来|下一步|应该发生|n/l|N:|L:|N:|L:", text): return "FOLLOWUP_EXPECTED_LINE" if re.search(r"K线|股价|涨停|跌停|上涨|下跌|放量|缩量|换手|市场|显影|输出", text, flags=re.I): return "MARKET_MANIFESTATION" if re.search(r"替代解释|也可能|可能是|不是|风险|缺口|不确定|held|review", lower): return "ALTERNATIVE_OR_GAP" if re.search(r"总结|结论|启发|当前读法|收口", text): return "SUMMARY_READOUT" if text.startswith("#"): return "SECTION_HEADING" return "BODY_REASONING" def evidence_type_for_step(step_type: str) -> str: mapping = { "INITIAL_LEAF": "INITIAL_INFORMATION_LEAF", "DARKLINE_INTENTION": "INTENTION_REASONING", "WHY_NODE": "WHY_REASONING", "PRIOR_EXPECTED_LINE": "PRIOR_NODE_REASONING", "FOLLOWUP_EXPECTED_LINE": "FOLLOWUP_NODE_REASONING", "MARKET_MANIFESTATION": "MARKET_MANIFESTATION_TEXT", "ALTERNATIVE_OR_GAP": "ALTERNATIVE_EXPLANATION_TEXT", "SUMMARY_READOUT": "SUMMARY_TEXT", } return mapping.get(step_type, "CASEBOOK_TEXT") def extract_symbols(text: str) -> list[str]: symbols = sorted(set(re.findall(r"\b(?:[036]\d{5}|688\d{3}|8\d{5})\.(?:SZ|SH|BJ)\b", text))) return symbols def has_event_signal(text: str) -> bool: return bool( re.search( r"\d{4}[-/年]\d{1,2}[-/月]\d{0,2}|公告|发布|披露|立案|减持|回购|增持|诉讼|仲裁|并购|重组|订单|涨停|跌停", text, ) ) def has_market_signal(text: str) -> bool: return bool(re.search(r"K线|股价|涨停|跌停|上涨|下跌|放量|缩量|换手|D[0-9]+|收益|显影|拉升|砸盘", text, flags=re.I)) def contains_expected_line(step_type: str, text: str) -> bool: if step_type in {"PRIOR_EXPECTED_LINE", "FOLLOWUP_EXPECTED_LINE", "WHY_NODE"}: return True return bool(re.search(r"如果.*应该|理论上.*会|后续.*会|之前.*应该|预期|应有之线", text)) def create_tables(cur) -> None: cur.execute( """ CREATE TABLE IF NOT EXISTS dl_source_document ( source_id VARCHAR(128) PRIMARY KEY, source_title VARCHAR(512) NOT NULL, source_url_or_path VARCHAR(512) NOT NULL, source_type VARCHAR(64) NOT NULL, publish_time VARCHAR(64) NULL, available_time VARCHAR(64) NULL, raw_text_hash CHAR(64) NOT NULL, source_reliability VARCHAR(64) NOT NULL, archive_status VARCHAR(64) NOT NULL ) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci """ ) cur.execute( """ CREATE TABLE IF NOT EXISTS dl_case_reasoning_step ( reasoning_step_id VARCHAR(128) PRIMARY KEY, case_id VARCHAR(96) NOT NULL, darkline_hypothesis_id VARCHAR(96) NOT NULL, step_order INT NOT NULL, step_type VARCHAR(64) NOT NULL, section_heading VARCHAR(512) NULL, step_text LONGTEXT NOT NULL, extraction_rule VARCHAR(128) NOT NULL, archive_batch_id VARCHAR(96) NOT NULL, KEY idx_reason_case (case_id), KEY idx_reason_type (step_type) ) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci """ ) cur.execute( """ CREATE TABLE IF NOT EXISTS dl_expected_line ( expected_line_id VARCHAR(128) PRIMARY KEY, darkline_hypothesis_id VARCHAR(96) NOT NULL, case_id VARCHAR(96) NOT NULL, expected_line_type VARCHAR(64) NOT NULL, expected_event LONGTEXT NOT NULL, expected_timing VARCHAR(128) NULL, expected_direction VARCHAR(64) NOT NULL, observed_flag TINYINT NOT NULL, observed_event_node_id VARCHAR(128) NULL, validation_status VARCHAR(64) NOT NULL, source_reasoning_step_id VARCHAR(128) NULL, archive_batch_id VARCHAR(96) NOT NULL, KEY idx_expected_case (case_id), KEY idx_expected_hypothesis (darkline_hypothesis_id) ) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci """ ) cur.execute( """ CREATE TABLE IF NOT EXISTS dl_event_node ( event_node_id VARCHAR(128) PRIMARY KEY, darkline_hypothesis_id VARCHAR(96) NOT NULL, case_id VARCHAR(96) NOT NULL, event_title VARCHAR(512) NOT NULL, event_date VARCHAR(64) NULL, available_time VARCHAR(64) NULL, actor_list TEXT NULL, target_list TEXT NULL, event_type VARCHAR(96) NOT NULL, event_level VARCHAR(32) NOT NULL, source_id VARCHAR(128) NOT NULL, node_status VARCHAR(64) NOT NULL, source_reasoning_step_id VARCHAR(128) NULL, archive_batch_id VARCHAR(96) NOT NULL, KEY idx_event_case (case_id), KEY idx_event_hypothesis (darkline_hypothesis_id) ) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci """ ) cur.execute( """ CREATE TABLE IF NOT EXISTS dl_evidence ( evidence_id VARCHAR(128) PRIMARY KEY, source_id VARCHAR(128) NOT NULL, case_id VARCHAR(96) NOT NULL, raw_excerpt LONGTEXT NOT NULL, evidence_type VARCHAR(96) NOT NULL, evidence_strength VARCHAR(64) NOT NULL, extracted_time DATETIME NOT NULL, evidence_hash CHAR(64) NOT NULL, source_reasoning_step_id VARCHAR(128) NULL, archive_batch_id VARCHAR(96) NOT NULL, KEY idx_evidence_case (case_id), KEY idx_evidence_type (evidence_type) ) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci """ ) cur.execute( """ CREATE TABLE IF NOT EXISTS dl_evidence_node_link ( evidence_node_link_id VARCHAR(160) PRIMARY KEY, evidence_id VARCHAR(128) NOT NULL, event_node_id VARCHAR(128) NULL, darkline_hypothesis_id VARCHAR(96) NOT NULL, expected_line_id VARCHAR(128) NULL, link_role VARCHAR(64) NOT NULL, archive_batch_id VARCHAR(96) NOT NULL, KEY idx_link_evidence (evidence_id), KEY idx_link_event (event_node_id), KEY idx_link_hypothesis (darkline_hypothesis_id) ) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci """ ) cur.execute( """ CREATE TABLE IF NOT EXISTS dl_impact_target ( impact_target_id VARCHAR(128) PRIMARY KEY, darkline_hypothesis_id VARCHAR(96) NOT NULL, case_id VARCHAR(96) NOT NULL, target_type VARCHAR(64) NOT NULL, target_id VARCHAR(96) NOT NULL, target_name VARCHAR(256) NULL, expected_impact_direction VARCHAR(64) NOT NULL, expected_window VARCHAR(128) NULL, archive_batch_id VARCHAR(96) NOT NULL, KEY idx_target_case (case_id), KEY idx_target_hypothesis (darkline_hypothesis_id) ) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci """ ) cur.execute( """ CREATE TABLE IF NOT EXISTS dl_manifestation_bridge ( bridge_id VARCHAR(128) PRIMARY KEY, darkline_hypothesis_id VARCHAR(96) NOT NULL, case_id VARCHAR(96) NOT NULL, expected_line_id VARCHAR(128) NULL, impact_target_id VARCHAR(128) NULL, output_layer VARCHAR(96) NOT NULL, manifestation_time VARCHAR(64) NULL, manifestation_value LONGTEXT NOT NULL, control_group_id VARCHAR(128) NULL, support_status VARCHAR(64) NOT NULL, source_reasoning_step_id VARCHAR(128) NULL, archive_batch_id VARCHAR(96) NOT NULL, KEY idx_bridge_case (case_id), KEY idx_bridge_hypothesis (darkline_hypothesis_id) ) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci """ ) cur.execute( """ CREATE TABLE IF NOT EXISTS dl_alternative_explanation ( alternative_id VARCHAR(128) PRIMARY KEY, darkline_hypothesis_id VARCHAR(96) NOT NULL, case_id VARCHAR(96) NOT NULL, bridge_id VARCHAR(128) NULL, alternative_type VARCHAR(96) NOT NULL, explanation LONGTEXT NOT NULL, strength VARCHAR(64) NOT NULL, current_status VARCHAR(64) NOT NULL, source_reasoning_step_id VARCHAR(128) NULL, archive_batch_id VARCHAR(96) NOT NULL, KEY idx_alt_case (case_id), KEY idx_alt_hypothesis (darkline_hypothesis_id) ) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci """ ) def clear_archive_rows(cur) -> None: for table in [ "dl_case_reasoning_step", "dl_expected_line", "dl_event_node", "dl_evidence", "dl_evidence_node_link", "dl_impact_target", "dl_manifestation_bridge", "dl_alternative_explanation", ]: cur.execute(f"DELETE FROM `{table}` WHERE archive_batch_id=%s", (ARCHIVE_BATCH_ID,)) cur.execute("DELETE FROM dl_source_document WHERE source_id=%s", (SOURCE_ID,)) def first_event_date(text: str) -> str | None: m = re.search(r"(\d{4}[-/年]\d{1,2}(?:[-/月]\d{1,2})?)", text) return m.group(1) if m else None def insert_many(cur, sql: str, rows: Iterable[tuple]) -> int: rows = list(rows) if rows: cur.executemany(sql, rows) return len(rows) def main() -> None: parser = argparse.ArgumentParser(description="Build full darkline archive tables from imported case records.") parser.add_argument("--host", default=os.environ.get("DARKLINE_MYSQL_HOST", "127.0.0.1")) parser.add_argument("--port", type=int, default=int(os.environ.get("DARKLINE_MYSQL_PORT", "3306"))) parser.add_argument("--user", default=os.environ.get("DARKLINE_MYSQL_USER", "root")) parser.add_argument("--password", default=None) parser.add_argument("--database", default=os.environ.get("DARKLINE_MYSQL_DATABASE", "tianxia")) args = parser.parse_args() now = datetime.now().strftime("%Y-%m-%d %H:%M:%S") conn = connect(args) counts: dict[str, int] = {} with conn.cursor() as cur: create_tables(cur) clear_archive_rows(cur) cur.execute("SELECT COALESCE(MAX(source_hash), '') AS source_hash FROM dl_case_import_batch") source_hash = cur.fetchone()["source_hash"] or "UNKNOWN_SOURCE_HASH" cur.execute( """ INSERT INTO dl_source_document ( source_id, source_title, source_url_or_path, source_type, publish_time, available_time, raw_text_hash, source_reliability, archive_status ) VALUES (%s,%s,%s,%s,%s,%s,%s,%s,%s) """, ( SOURCE_ID, "Legacy external information casebook", SOURCE_PATH, "MARKDOWN_CASEBOOK", None, None, source_hash, "HISTORICAL_PROJECT_SOURCE", "READY", ), ) counts["dl_source_document"] = 1 cur.execute( """ SELECT case_id, case_title, case_level, case_family, case_body_md, case_body_hash FROM dl_case_record ORDER BY case_order """ ) cases = cur.fetchall() reasoning_rows = [] expected_rows = [] event_rows = [] evidence_rows = [] link_rows = [] target_rows = [] bridge_rows = [] alt_rows = [] for case in cases: case_id = case["case_id"] hypothesis_id = case_id body = case["case_body_md"] or "" units = split_reasoning_units(body) symbols = extract_symbols(case["case_title"] + "\n" + body) if not symbols: symbols = ["CASE_SCOPE_UNKNOWN"] for target_order, symbol in enumerate(symbols, start=1): target_rows.append( ( f"{case_id}_TARGET_{target_order:03d}", hypothesis_id, case_id, "SYMBOL" if symbol != "CASE_SCOPE_UNKNOWN" else "CASE_SCOPE", symbol, symbol if symbol != "CASE_SCOPE_UNKNOWN" else None, "UNKNOWN", "CASE_DEFINED_WINDOW", ARCHIVE_BATCH_ID, ) ) expected_id_for_case: str | None = None event_id_for_step: dict[str, str] = {} for order, unit in enumerate(units, start=1): text = unit["text"] step_type = classify_step_type(text) step_id = f"{case_id}_STEP_{order:04d}" reasoning_rows.append( ( step_id, case_id, hypothesis_id, order, step_type, compact(unit.get("heading", ""), 480) or None, text, "MARKDOWN_LINE_HEURISTIC_V1", ARCHIVE_BATCH_ID, ) ) evidence_id = f"{case_id}_EVID_{order:04d}" evidence_rows.append( ( evidence_id, SOURCE_ID, case_id, text, evidence_type_for_step(step_type), "REVIEW", now, sha256_text(f"{case_id}|{order}|{text}"), step_id, ARCHIVE_BATCH_ID, ) ) expected_line_id = None if contains_expected_line(step_type, text): expected_line_id = f"{case_id}_EXPECTED_{len([r for r in expected_rows if r[2] == case_id]) + 1:03d}" expected_id_for_case = expected_id_for_case or expected_line_id expected_rows.append( ( expected_line_id, hypothesis_id, case_id, step_type, compact(text, 1000), "TEXT_INFERRED_WINDOW", "UNKNOWN", 0, None, "REVIEW_EXTRACTED", step_id, ARCHIVE_BATCH_ID, ) ) event_node_id = None if has_event_signal(text): event_node_id = f"{case_id}_EVENT_{order:04d}" event_id_for_step[step_id] = event_node_id event_rows.append( ( event_node_id, hypothesis_id, case_id, compact(text, 480), first_event_date(text), None, None, json.dumps(symbols, ensure_ascii=False), step_type, case["case_level"] or "UNKNOWN", SOURCE_ID, "REVIEW_EXTRACTED", step_id, ARCHIVE_BATCH_ID, ) ) link_rows.append( ( f"{case_id}_LINK_{order:04d}", evidence_id, event_node_id, hypothesis_id, expected_line_id or expected_id_for_case, "SUPPORT" if step_type != "ALTERNATIVE_OR_GAP" else "REVIEW", ARCHIVE_BATCH_ID, ) ) if has_market_signal(text): bridge_rows.append( ( f"{case_id}_BRIDGE_{len([r for r in bridge_rows if r[2] == case_id]) + 1:03d}", hypothesis_id, case_id, expected_line_id or expected_id_for_case, f"{case_id}_TARGET_001", "KLINE_OR_MARKET_TEXT", first_event_date(text), text, None, "REVIEW_EXTRACTED", step_id, ARCHIVE_BATCH_ID, ) ) if step_type == "ALTERNATIVE_OR_GAP": alt_rows.append( ( f"{case_id}_ALT_{len([r for r in alt_rows if r[2] == case_id]) + 1:03d}", hypothesis_id, case_id, None, "TEXT_ALTERNATIVE_OR_GAP", text, "REVIEW", "OPEN", step_id, ARCHIVE_BATCH_ID, ) ) if not expected_id_for_case: expected_rows.append( ( f"{case_id}_EXPECTED_001", hypothesis_id, case_id, "CASE_IMPLIED_EXPECTED_LINE", compact(case["case_title"], 1000), "CASE_REVIEW_WINDOW", "UNKNOWN", 0, None, "REVIEW_EXTRACTED", None, ARCHIVE_BATCH_ID, ) ) counts["dl_case_reasoning_step"] = insert_many( cur, """ INSERT INTO dl_case_reasoning_step ( reasoning_step_id, case_id, darkline_hypothesis_id, step_order, step_type, section_heading, step_text, extraction_rule, archive_batch_id ) VALUES (%s,%s,%s,%s,%s,%s,%s,%s,%s) """, reasoning_rows, ) counts["dl_expected_line"] = insert_many( cur, """ INSERT INTO dl_expected_line ( expected_line_id, darkline_hypothesis_id, case_id, expected_line_type, expected_event, expected_timing, expected_direction, observed_flag, observed_event_node_id, validation_status, source_reasoning_step_id, archive_batch_id ) VALUES (%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s) """, expected_rows, ) counts["dl_event_node"] = insert_many( cur, """ INSERT INTO dl_event_node ( event_node_id, darkline_hypothesis_id, case_id, event_title, event_date, available_time, actor_list, target_list, event_type, event_level, source_id, node_status, source_reasoning_step_id, archive_batch_id ) VALUES (%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s) """, event_rows, ) counts["dl_evidence"] = insert_many( cur, """ INSERT INTO dl_evidence ( evidence_id, source_id, case_id, raw_excerpt, evidence_type, evidence_strength, extracted_time, evidence_hash, source_reasoning_step_id, archive_batch_id ) VALUES (%s,%s,%s,%s,%s,%s,%s,%s,%s,%s) """, evidence_rows, ) counts["dl_evidence_node_link"] = insert_many( cur, """ INSERT INTO dl_evidence_node_link ( evidence_node_link_id, evidence_id, event_node_id, darkline_hypothesis_id, expected_line_id, link_role, archive_batch_id ) VALUES (%s,%s,%s,%s,%s,%s,%s) """, link_rows, ) counts["dl_impact_target"] = insert_many( cur, """ INSERT INTO dl_impact_target ( impact_target_id, darkline_hypothesis_id, case_id, target_type, target_id, target_name, expected_impact_direction, expected_window, archive_batch_id ) VALUES (%s,%s,%s,%s,%s,%s,%s,%s,%s) """, target_rows, ) counts["dl_manifestation_bridge"] = insert_many( cur, """ INSERT INTO dl_manifestation_bridge ( bridge_id, darkline_hypothesis_id, case_id, expected_line_id, impact_target_id, output_layer, manifestation_time, manifestation_value, control_group_id, support_status, source_reasoning_step_id, archive_batch_id ) VALUES (%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s) """, bridge_rows, ) counts["dl_alternative_explanation"] = insert_many( cur, """ INSERT INTO dl_alternative_explanation ( alternative_id, darkline_hypothesis_id, case_id, bridge_id, alternative_type, explanation, strength, current_status, source_reasoning_step_id, archive_batch_id ) VALUES (%s,%s,%s,%s,%s,%s,%s,%s,%s,%s) """, alt_rows, ) conn.close() print( json.dumps( { "archive_batch_id": ARCHIVE_BATCH_ID, "database": args.database, "counts": counts, "status": "PASS", }, ensure_ascii=False, indent=2, ) ) if __name__ == "__main__": main()