881 lines
25 KiB
Python
881 lines
25 KiB
Python
#!/usr/bin/env python3
|
||
# -*- coding: utf-8 -*-
|
||
"""
|
||
CM93 Importer Normalization 接入点只读调查
|
||
Version: v12.0
|
||
|
||
用途
|
||
====
|
||
V12 不修改数据库,也不修改源码。
|
||
|
||
它只做两件事:
|
||
|
||
Gate A:
|
||
读取 V11 输出目录或 ZIP,要求:
|
||
IMPLEMENTATION PRECHECK PASS = YES
|
||
或 report JSON 中:
|
||
summary.implementation_precheck_pass = true
|
||
|
||
Gate B:
|
||
只读扫描当前 importer 项目源码,自动定位:
|
||
|
||
1. CM93 Feature / Area geometry 构造入口
|
||
2. chart_area_geometry / chart_coverage 写入位置
|
||
3. geometry_issue = "geometry missing" 或同义逻辑
|
||
4. geometry_invalid_count / geometry_repaired_count 更新位置
|
||
5. raw_geotype / geometry_type 判定位置
|
||
6. bbox_min_lon / bbox_min_lat / bbox_max_lon / bbox_max_lat 计算位置
|
||
7. Shapely / GEOS / WKB / Polygon / MultiPolygon 相关代码
|
||
8. chart_feature / chart_area_geometry / chart_coverage SQL 写入语句
|
||
9. raw_feature_blob / chart_raw_record 保留逻辑
|
||
10. importer job summary / counters
|
||
|
||
输出的目标是让下一阶段可以基于当前真实源码生成精准修改版,
|
||
而不是凭文件名或架构猜测。
|
||
|
||
重要:
|
||
- 不修改项目文件
|
||
- 不修改 MySQL
|
||
- 不开始 PBF
|
||
- 不生成自动 apply patch
|
||
- 只输出调查报告和代码片段
|
||
|
||
执行
|
||
====
|
||
假设当前就在 importer 项目根目录:
|
||
|
||
python .\\cm93_importer_normalization_source_audit_v12.py ^
|
||
--v11-input ".\\cm93_normalization_implementation_precheck_v11_dataset_1_20260901_084147" ^
|
||
--project-root "."
|
||
|
||
也可以给 V11 ZIP:
|
||
|
||
python .\\cm93_importer_normalization_source_audit_v12.py ^
|
||
--v11-input ".\\cm93_normalization_implementation_precheck_v11_dataset_1_20260901_084147.zip" ^
|
||
--project-root "."
|
||
|
||
输出
|
||
====
|
||
- importer_source_audit_summary_v12.txt
|
||
- importer_source_audit_report_v12.json
|
||
- importer_candidate_files_v12.csv
|
||
- importer_code_hits_v12.csv
|
||
- importer_sql_hits_v12.csv
|
||
- importer_function_inventory_v12.csv
|
||
- importer_dependency_inventory_v12.csv
|
||
- importer_normalization_integration_points_v12.md
|
||
- importer_source_audit_v12.zip
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
import argparse
|
||
import ast
|
||
import csv
|
||
import json
|
||
import os
|
||
import re
|
||
import shutil
|
||
import sys
|
||
import tempfile
|
||
import zipfile
|
||
from collections import Counter, defaultdict
|
||
from datetime import datetime
|
||
from pathlib import Path
|
||
from typing import Any, Dict, Iterable, List, Optional, Sequence, Tuple
|
||
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# Configuration
|
||
# ---------------------------------------------------------------------------
|
||
|
||
TEXT_EXTENSIONS = {
|
||
".py", ".sql", ".toml", ".ini", ".cfg", ".json",
|
||
".yaml", ".yml", ".md", ".txt",
|
||
}
|
||
|
||
SKIP_DIRS = {
|
||
".git", ".hg", ".svn",
|
||
"__pycache__", ".pytest_cache", ".mypy_cache",
|
||
".venv", "venv", "env",
|
||
"node_modules", "dist", "build",
|
||
".idea", ".vscode",
|
||
}
|
||
|
||
SEARCH_GROUPS = {
|
||
"NORMALIZATION": [
|
||
r"\bmake_valid\b",
|
||
r"\bpolygonize_full\b",
|
||
r"\bunary_union\b",
|
||
r"\bnormalize\b",
|
||
r"\bMultiPolygon\b",
|
||
r"\bPolygon\b",
|
||
r"\bShapely\b",
|
||
r"\bshapely\b",
|
||
r"\bWKB\b",
|
||
r"\bwkb\b",
|
||
],
|
||
"GEOMETRY_ISSUE": [
|
||
r"geometry_issue",
|
||
r"geometry missing",
|
||
r"geometry_missing",
|
||
r"invalid geometry",
|
||
r"geometry_invalid",
|
||
r"geometry_repaired",
|
||
],
|
||
"GEOTYPE": [
|
||
r"raw_geotype",
|
||
r"geotype",
|
||
r"geometry_type",
|
||
r"\bMETA\b",
|
||
r"\bAREA\b",
|
||
],
|
||
"BBOX": [
|
||
r"bbox_min_lon",
|
||
r"bbox_min_lat",
|
||
r"bbox_max_lon",
|
||
r"bbox_max_lat",
|
||
r"\bbounds\b",
|
||
r"\bbbox\b",
|
||
],
|
||
"COUNTERS": [
|
||
r"geometry_invalid_count",
|
||
r"geometry_repaired_count",
|
||
r"invalid_count",
|
||
r"repaired_count",
|
||
r"summary_json",
|
||
r"chart_import_job",
|
||
],
|
||
"DB_WRITE": [
|
||
r"chart_area_geometry",
|
||
r"chart_coverage",
|
||
r"chart_feature",
|
||
r"INSERT\s+INTO",
|
||
r"UPDATE\s+",
|
||
r"executemany",
|
||
r"execute\(",
|
||
],
|
||
"RAW_SOURCE": [
|
||
r"raw_feature_blob",
|
||
r"chart_raw_record",
|
||
r"source_record_index",
|
||
r"raw_blob",
|
||
r"raw_metadata_json",
|
||
],
|
||
}
|
||
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# Helpers
|
||
# ---------------------------------------------------------------------------
|
||
|
||
def write_csv(
|
||
path: Path,
|
||
rows: Iterable[Dict[str, Any]],
|
||
fieldnames: Optional[List[str]] = None,
|
||
):
|
||
rows = list(rows)
|
||
|
||
if fieldnames is None:
|
||
seen = set()
|
||
fieldnames = []
|
||
for row in rows:
|
||
for k in row.keys():
|
||
if k not in seen:
|
||
seen.add(k)
|
||
fieldnames.append(k)
|
||
|
||
path.parent.mkdir(parents=True, exist_ok=True)
|
||
|
||
with path.open("w", encoding="utf-8-sig", newline="") as f:
|
||
if not fieldnames:
|
||
return
|
||
w = csv.DictWriter(f, fieldnames=fieldnames, extrasaction="ignore")
|
||
w.writeheader()
|
||
for row in rows:
|
||
out = {}
|
||
for k in fieldnames:
|
||
v = row.get(k)
|
||
if isinstance(v, (dict, list, tuple)):
|
||
v = json.dumps(v, ensure_ascii=False, default=str)
|
||
out[k] = "" if v is None else v
|
||
w.writerow(out)
|
||
|
||
|
||
def safe_read_text(path: Path) -> Optional[str]:
|
||
for enc in ("utf-8", "utf-8-sig", "cp932", "latin-1"):
|
||
try:
|
||
return path.read_text(encoding=enc)
|
||
except Exception:
|
||
pass
|
||
return None
|
||
|
||
|
||
def relpath(path: Path, root: Path) -> str:
|
||
try:
|
||
return str(path.resolve().relative_to(root.resolve()))
|
||
except Exception:
|
||
return str(path)
|
||
|
||
|
||
def should_skip(path: Path) -> bool:
|
||
return any(part in SKIP_DIRS for part in path.parts)
|
||
|
||
|
||
def list_source_files(root: Path) -> List[Path]:
|
||
out = []
|
||
for p in root.rglob("*"):
|
||
if not p.is_file():
|
||
continue
|
||
if should_skip(p):
|
||
continue
|
||
if p.suffix.lower() in TEXT_EXTENSIONS:
|
||
out.append(p)
|
||
return sorted(out)
|
||
|
||
|
||
def open_input(src: Path) -> Tuple[Path, Optional[Path]]:
|
||
if src.is_dir():
|
||
return src, None
|
||
|
||
if src.is_file() and src.suffix.lower() == ".zip":
|
||
temp = Path(tempfile.mkdtemp(prefix="cm93_v12_v11_"))
|
||
with zipfile.ZipFile(src, "r") as z:
|
||
z.extractall(temp)
|
||
return temp, temp
|
||
|
||
raise ValueError("V11 输入必须是目录或 ZIP")
|
||
|
||
|
||
def find_file(root: Path, filename: str) -> Optional[Path]:
|
||
direct = root / filename
|
||
if direct.exists():
|
||
return direct
|
||
hits = list(root.rglob(filename))
|
||
return hits[0] if hits else None
|
||
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# V11 gate
|
||
# ---------------------------------------------------------------------------
|
||
|
||
def load_v11_gate(root: Path) -> Dict[str, Any]:
|
||
result = {
|
||
"source": None,
|
||
"implementation_precheck_pass": False,
|
||
"db_invalid_count": None,
|
||
"golden_revalidation_pass_count": None,
|
||
"golden_revalidation_fail_count": None,
|
||
"area_geometry_column_type": None,
|
||
"coverage_geometry_column_type": None,
|
||
"future_area_bbox_update_needed_count": None,
|
||
"raw_geotype16_meta_count": None,
|
||
"raw_geotype16_with_issue_count": None,
|
||
"total_feature_geometry_issue_count": None,
|
||
"import_job_geometry_invalid_sum": None,
|
||
"errors": [],
|
||
}
|
||
|
||
report_path = find_file(root, "implementation_precheck_report_v11.json")
|
||
summary_path = find_file(root, "implementation_precheck_summary_v11.txt")
|
||
|
||
if report_path:
|
||
report = json.loads(report_path.read_text(encoding="utf-8"))
|
||
s = report.get("summary", {})
|
||
result["source"] = "implementation_precheck_report_v11.json"
|
||
result["implementation_precheck_pass"] = bool(
|
||
s.get("implementation_precheck_pass")
|
||
)
|
||
for key in (
|
||
"db_invalid_count",
|
||
"golden_revalidation_pass_count",
|
||
"golden_revalidation_fail_count",
|
||
"area_geometry_column_type",
|
||
"coverage_geometry_column_type",
|
||
"future_area_bbox_update_needed_count",
|
||
"raw_geotype16_meta_count",
|
||
"raw_geotype16_with_issue_count",
|
||
"total_feature_geometry_issue_count",
|
||
"import_job_geometry_invalid_sum",
|
||
):
|
||
result[key] = s.get(key)
|
||
|
||
elif summary_path:
|
||
text = summary_path.read_text(encoding="utf-8", errors="replace")
|
||
result["source"] = "implementation_precheck_summary_v11.txt"
|
||
result["implementation_precheck_pass"] = bool(
|
||
re.search(
|
||
r"IMPLEMENTATION\s+PRECHECK\s+PASS:\s*YES",
|
||
text,
|
||
flags=re.I,
|
||
)
|
||
)
|
||
|
||
else:
|
||
result["errors"].append(
|
||
"找不到 implementation_precheck_report_v11.json "
|
||
"或 implementation_precheck_summary_v11.txt"
|
||
)
|
||
|
||
return result
|
||
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# Source analysis
|
||
# ---------------------------------------------------------------------------
|
||
|
||
def scan_hits(
|
||
path: Path,
|
||
text: str,
|
||
root: Path,
|
||
) -> List[Dict[str, Any]]:
|
||
rows = []
|
||
lines = text.splitlines()
|
||
|
||
compiled = {
|
||
group: [re.compile(pat, re.I) for pat in patterns]
|
||
for group, patterns in SEARCH_GROUPS.items()
|
||
}
|
||
|
||
for lineno, line in enumerate(lines, start=1):
|
||
stripped = line.strip()
|
||
|
||
for group, patterns in compiled.items():
|
||
matched = []
|
||
for pat in patterns:
|
||
if pat.search(line):
|
||
matched.append(pat.pattern)
|
||
|
||
if not matched:
|
||
continue
|
||
|
||
start = max(1, lineno - 2)
|
||
end = min(len(lines), lineno + 2)
|
||
context = "\n".join(
|
||
f"{n:05d}: {lines[n-1]}"
|
||
for n in range(start, end + 1)
|
||
)
|
||
|
||
rows.append({
|
||
"file": relpath(path, root),
|
||
"line": lineno,
|
||
"group": group,
|
||
"matched_patterns": matched,
|
||
"code": stripped,
|
||
"context": context,
|
||
})
|
||
|
||
return rows
|
||
|
||
|
||
def python_function_inventory(
|
||
path: Path,
|
||
text: str,
|
||
root: Path,
|
||
) -> List[Dict[str, Any]]:
|
||
if path.suffix.lower() != ".py":
|
||
return []
|
||
|
||
try:
|
||
tree = ast.parse(text)
|
||
except Exception:
|
||
return []
|
||
|
||
rows = []
|
||
|
||
for node in ast.walk(tree):
|
||
if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
|
||
rows.append({
|
||
"file": relpath(path, root),
|
||
"name": node.name,
|
||
"line": node.lineno,
|
||
"end_line": getattr(node, "end_lineno", None),
|
||
"async": int(isinstance(node, ast.AsyncFunctionDef)),
|
||
"args": [
|
||
arg.arg
|
||
for arg in (
|
||
list(node.args.posonlyargs)
|
||
+ list(node.args.args)
|
||
+ list(node.args.kwonlyargs)
|
||
)
|
||
],
|
||
})
|
||
|
||
return rows
|
||
|
||
|
||
def python_dependency_inventory(
|
||
path: Path,
|
||
text: str,
|
||
root: Path,
|
||
) -> List[Dict[str, Any]]:
|
||
if path.suffix.lower() != ".py":
|
||
return []
|
||
|
||
try:
|
||
tree = ast.parse(text)
|
||
except Exception:
|
||
return []
|
||
|
||
rows = []
|
||
|
||
for node in ast.walk(tree):
|
||
if isinstance(node, ast.Import):
|
||
for alias in node.names:
|
||
rows.append({
|
||
"file": relpath(path, root),
|
||
"line": node.lineno,
|
||
"module": alias.name,
|
||
"import_type": "import",
|
||
})
|
||
|
||
elif isinstance(node, ast.ImportFrom):
|
||
rows.append({
|
||
"file": relpath(path, root),
|
||
"line": node.lineno,
|
||
"module": node.module or "",
|
||
"import_type": "from",
|
||
"names": [x.name for x in node.names],
|
||
})
|
||
|
||
return rows
|
||
|
||
|
||
def candidate_score(
|
||
path: Path,
|
||
hits: List[Dict[str, Any]],
|
||
root: Path,
|
||
) -> Dict[str, Any]:
|
||
groups = Counter(r["group"] for r in hits)
|
||
score = 0
|
||
|
||
weights = {
|
||
"GEOMETRY_ISSUE": 8,
|
||
"GEOTYPE": 5,
|
||
"BBOX": 5,
|
||
"COUNTERS": 7,
|
||
"DB_WRITE": 7,
|
||
"RAW_SOURCE": 4,
|
||
"NORMALIZATION": 3,
|
||
}
|
||
|
||
for group, count in groups.items():
|
||
score += weights.get(group, 1) * min(count, 10)
|
||
|
||
name = path.name.lower()
|
||
if "import" in name:
|
||
score += 8
|
||
if "geometry" in name or "geom" in name:
|
||
score += 6
|
||
if "cm93" in name:
|
||
score += 6
|
||
if "db" in name or "mysql" in name:
|
||
score += 3
|
||
|
||
return {
|
||
"file": relpath(path, root),
|
||
"score": score,
|
||
"hit_count": len(hits),
|
||
**{f"hits_{g.lower()}": groups.get(g, 0) for g in SEARCH_GROUPS},
|
||
}
|
||
|
||
|
||
def choose_integration_points(
|
||
hits: List[Dict[str, Any]]
|
||
) -> Dict[str, List[Dict[str, Any]]]:
|
||
buckets = {
|
||
"area_geometry_build_or_write": [],
|
||
"coverage_geometry_build_or_write": [],
|
||
"geotype16_issue_logic": [],
|
||
"geometry_invalid_counter": [],
|
||
"bbox_update": [],
|
||
"raw_source_preservation": [],
|
||
}
|
||
|
||
for r in hits:
|
||
code = r["code"].lower()
|
||
file = r["file"].lower()
|
||
|
||
if (
|
||
"chart_area_geometry" in code
|
||
or (
|
||
r["group"] == "NORMALIZATION"
|
||
and "area" in file
|
||
)
|
||
):
|
||
buckets["area_geometry_build_or_write"].append(r)
|
||
|
||
if "chart_coverage" in code:
|
||
buckets["coverage_geometry_build_or_write"].append(r)
|
||
|
||
if (
|
||
"raw_geotype" in code
|
||
or "geotype" in code
|
||
or "geometry missing" in code
|
||
):
|
||
buckets["geotype16_issue_logic"].append(r)
|
||
|
||
if "geometry_invalid_count" in code:
|
||
buckets["geometry_invalid_counter"].append(r)
|
||
|
||
if (
|
||
"bbox_min_lon" in code
|
||
or "bbox_max_lon" in code
|
||
or ".bounds" in code
|
||
):
|
||
buckets["bbox_update"].append(r)
|
||
|
||
if (
|
||
"raw_feature_blob" in code
|
||
or "chart_raw_record" in code
|
||
):
|
||
buckets["raw_source_preservation"].append(r)
|
||
|
||
for key in buckets:
|
||
buckets[key] = buckets[key][:30]
|
||
|
||
return buckets
|
||
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# Report
|
||
# ---------------------------------------------------------------------------
|
||
|
||
def build_integration_md(
|
||
gate: Dict[str, Any],
|
||
candidates: List[Dict[str, Any]],
|
||
integration: Dict[str, List[Dict[str, Any]]],
|
||
) -> str:
|
||
lines = []
|
||
|
||
lines.append("# CM93 Importer Normalization 接入点调查 v12")
|
||
lines.append("")
|
||
lines.append("## V11 门控")
|
||
lines.append("")
|
||
lines.append(
|
||
f"- IMPLEMENTATION PRECHECK:"
|
||
f"{'PASS' if gate['implementation_precheck_pass'] else 'FAIL'}"
|
||
)
|
||
|
||
for key in (
|
||
"db_invalid_count",
|
||
"golden_revalidation_pass_count",
|
||
"golden_revalidation_fail_count",
|
||
"area_geometry_column_type",
|
||
"coverage_geometry_column_type",
|
||
"future_area_bbox_update_needed_count",
|
||
"raw_geotype16_meta_count",
|
||
"raw_geotype16_with_issue_count",
|
||
"import_job_geometry_invalid_sum",
|
||
):
|
||
lines.append(f"- {key}: `{gate.get(key)}`")
|
||
|
||
lines.append("")
|
||
lines.append("## 高优先级候选源码文件")
|
||
lines.append("")
|
||
|
||
for row in candidates[:15]:
|
||
lines.append(
|
||
f"- `{row['file']}` — score={row['score']}, "
|
||
f"hits={row['hit_count']}"
|
||
)
|
||
|
||
title_map = {
|
||
"area_geometry_build_or_write": "Area geometry 构造/写入",
|
||
"coverage_geometry_build_or_write": "Coverage geometry 构造/写入",
|
||
"geotype16_issue_logic": "raw_geotype=16 / geometry missing 判定",
|
||
"geometry_invalid_counter": "geometry_invalid_count 统计",
|
||
"bbox_update": "bbox 计算/写入",
|
||
"raw_source_preservation": "raw source 保留",
|
||
}
|
||
|
||
for key, title in title_map.items():
|
||
lines.append("")
|
||
lines.append(f"## {title}")
|
||
lines.append("")
|
||
|
||
rows = integration.get(key, [])
|
||
|
||
if not rows:
|
||
lines.append("- 未自动定位,需要人工继续查。")
|
||
continue
|
||
|
||
seen = set()
|
||
for row in rows:
|
||
marker = (row["file"], row["line"])
|
||
if marker in seen:
|
||
continue
|
||
seen.add(marker)
|
||
lines.append(
|
||
f"- `{row['file']}:{row['line']}` "
|
||
f"`{row['code']}`"
|
||
)
|
||
|
||
lines.append("")
|
||
lines.append("## 下一阶段修改边界")
|
||
lines.append("")
|
||
lines.append("下一阶段只应修改真实定位到的 importer 代码,不应重新设计数据库。")
|
||
lines.append("")
|
||
lines.append("冻结要求:")
|
||
lines.append("")
|
||
lines.append("1. valid geometry 原样 PASS-THROUGH。")
|
||
lines.append("2. invalid Area/Coverage 才执行 odd-even face selection。")
|
||
lines.append("3. normalization 后执行 GEOS normalize canonicalization。")
|
||
lines.append("4. 输出必须为 valid Polygon/MultiPolygon。")
|
||
lines.append("5. Area normalized 后同步重新计算 feature bbox。")
|
||
lines.append("6. retraced connector 不保存为 Area 面积,但 raw source 必须保留。")
|
||
lines.append("7. raw_geotype=16 / META 不生成 `geometry missing`。")
|
||
lines.append("8. raw_geotype=16 不增加 `geometry_invalid_count`。")
|
||
lines.append("9. 不删除 `raw_feature_blob` 或 `chart_raw_record`。")
|
||
lines.append("10. 当前仍不进入 PBF。")
|
||
|
||
return "\n".join(lines)
|
||
|
||
|
||
def parse_args():
|
||
p = argparse.ArgumentParser(
|
||
description="CM93 importer normalization source audit v12"
|
||
)
|
||
p.add_argument("--v11-input", required=True)
|
||
p.add_argument("--project-root", default=".")
|
||
p.add_argument("--output", default=None)
|
||
return p.parse_args()
|
||
|
||
|
||
def main():
|
||
args = parse_args()
|
||
|
||
v11_src = Path(args.v11_input).expanduser().resolve()
|
||
project_root = Path(args.project_root).expanduser().resolve()
|
||
|
||
if not v11_src.exists():
|
||
print(f"V11 输入不存在:{v11_src}")
|
||
sys.exit(2)
|
||
|
||
if not project_root.is_dir():
|
||
print(f"项目目录不存在:{project_root}")
|
||
sys.exit(2)
|
||
|
||
temp_v11 = None
|
||
|
||
try:
|
||
v11_root, temp_v11 = open_input(v11_src)
|
||
|
||
print("[1/6] V11 implementation precheck gate ...")
|
||
gate = load_v11_gate(v11_root)
|
||
|
||
print(json.dumps(gate, ensure_ascii=False, indent=2))
|
||
|
||
if not gate["implementation_precheck_pass"]:
|
||
print("")
|
||
print("STOP:V11 IMPLEMENTATION PRECHECK 未通过。")
|
||
print("V12 不扫描 importer 源码。")
|
||
sys.exit(3)
|
||
|
||
print(" V11 gate: PASS")
|
||
|
||
stamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
||
|
||
outdir = Path(
|
||
args.output
|
||
or project_root / f"cm93_importer_source_audit_v12_{stamp}"
|
||
)
|
||
outdir.mkdir(parents=True, exist_ok=True)
|
||
|
||
print("[2/6] 扫描项目文本源码 ...")
|
||
files = list_source_files(project_root)
|
||
print(f" files={len(files)}")
|
||
|
||
all_hits = []
|
||
function_rows = []
|
||
dependency_rows = []
|
||
candidate_rows = []
|
||
|
||
for i, path in enumerate(files, start=1):
|
||
# 避免扫描当前输出目录自身
|
||
try:
|
||
path.resolve().relative_to(outdir.resolve())
|
||
continue
|
||
except Exception:
|
||
pass
|
||
|
||
text = safe_read_text(path)
|
||
if text is None:
|
||
continue
|
||
|
||
hits = scan_hits(path, text, project_root)
|
||
all_hits.extend(hits)
|
||
|
||
function_rows.extend(
|
||
python_function_inventory(path, text, project_root)
|
||
)
|
||
|
||
dependency_rows.extend(
|
||
python_dependency_inventory(path, text, project_root)
|
||
)
|
||
|
||
if hits:
|
||
candidate_rows.append(
|
||
candidate_score(path, hits, project_root)
|
||
)
|
||
|
||
if i % 100 == 0:
|
||
print(f" scanned={i}/{len(files)}")
|
||
|
||
candidate_rows.sort(
|
||
key=lambda r: (-int(r["score"]), r["file"])
|
||
)
|
||
|
||
print("[3/6] 分类 SQL 命中与函数 ...")
|
||
sql_hits = [
|
||
r for r in all_hits
|
||
if (
|
||
r["group"] == "DB_WRITE"
|
||
or re.search(
|
||
r"\b(SELECT|INSERT|UPDATE|DELETE|CREATE|ALTER)\b",
|
||
r["code"],
|
||
re.I,
|
||
)
|
||
)
|
||
]
|
||
|
||
print("[4/6] 推导 normalization 接入点 ...")
|
||
integration = choose_integration_points(all_hits)
|
||
|
||
print("[5/6] 写报告 ...")
|
||
write_csv(
|
||
outdir / "importer_candidate_files_v12.csv",
|
||
candidate_rows
|
||
)
|
||
write_csv(
|
||
outdir / "importer_code_hits_v12.csv",
|
||
all_hits
|
||
)
|
||
write_csv(
|
||
outdir / "importer_sql_hits_v12.csv",
|
||
sql_hits
|
||
)
|
||
write_csv(
|
||
outdir / "importer_function_inventory_v12.csv",
|
||
function_rows
|
||
)
|
||
write_csv(
|
||
outdir / "importer_dependency_inventory_v12.csv",
|
||
dependency_rows
|
||
)
|
||
|
||
integration_md = build_integration_md(
|
||
gate,
|
||
candidate_rows,
|
||
integration,
|
||
)
|
||
|
||
(
|
||
outdir / "importer_normalization_integration_points_v12.md"
|
||
).write_text(
|
||
integration_md,
|
||
encoding="utf-8"
|
||
)
|
||
|
||
report = {
|
||
"version": "v12.0",
|
||
"generated_at": datetime.now().isoformat(timespec="seconds"),
|
||
"project_root": str(project_root),
|
||
"read_only": True,
|
||
"v11_gate": gate,
|
||
"source_file_count": len(files),
|
||
"candidate_file_count": len(candidate_rows),
|
||
"code_hit_count": len(all_hits),
|
||
"sql_hit_count": len(sql_hits),
|
||
"function_count": len(function_rows),
|
||
"dependency_count": len(dependency_rows),
|
||
"top_candidate_files": candidate_rows[:25],
|
||
"integration_point_counts": {
|
||
k: len(v)
|
||
for k, v in integration.items()
|
||
},
|
||
}
|
||
|
||
(
|
||
outdir / "importer_source_audit_report_v12.json"
|
||
).write_text(
|
||
json.dumps(
|
||
report,
|
||
ensure_ascii=False,
|
||
indent=2,
|
||
default=str,
|
||
),
|
||
encoding="utf-8"
|
||
)
|
||
|
||
lines = []
|
||
lines.append("CM93 Importer Normalization 接入点只读调查 v12")
|
||
lines.append("=" * 88)
|
||
lines.append(f"Generated: {report['generated_at']}")
|
||
lines.append(f"Project: {project_root}")
|
||
lines.append("")
|
||
lines.append("V11 GATE")
|
||
lines.append(" PASS")
|
||
lines.append("")
|
||
lines.append("SOURCE SCAN")
|
||
lines.append(f" files: {len(files)}")
|
||
lines.append(f" candidate files:{len(candidate_rows)}")
|
||
lines.append(f" code hits: {len(all_hits)}")
|
||
lines.append(f" SQL hits: {len(sql_hits)}")
|
||
lines.append(f" functions: {len(function_rows)}")
|
||
lines.append("")
|
||
lines.append("TOP CANDIDATE FILES")
|
||
|
||
for row in candidate_rows[:15]:
|
||
lines.append(
|
||
f" {row['score']:4d} {row['file']}"
|
||
)
|
||
|
||
lines.append("")
|
||
lines.append("INTEGRATION POINT COUNTS")
|
||
|
||
for key, rows in integration.items():
|
||
lines.append(
|
||
f" {key:36s} {len(rows)}"
|
||
)
|
||
|
||
lines.append("")
|
||
lines.append(
|
||
"NO SOURCE WRITE / NO DATABASE WRITE / NO PBF"
|
||
)
|
||
|
||
(
|
||
outdir / "importer_source_audit_summary_v12.txt"
|
||
).write_text(
|
||
"\n".join(lines),
|
||
encoding="utf-8"
|
||
)
|
||
|
||
print("[6/6] 压缩 ...")
|
||
zpath = outdir.with_suffix(".zip")
|
||
|
||
with zipfile.ZipFile(
|
||
zpath,
|
||
"w",
|
||
compression=zipfile.ZIP_DEFLATED
|
||
) as z:
|
||
for p in sorted(outdir.rglob("*")):
|
||
if p.is_file():
|
||
z.write(
|
||
p,
|
||
p.relative_to(outdir.parent)
|
||
)
|
||
|
||
print("")
|
||
print("SOURCE AUDIT PASS: YES")
|
||
print(f"ZIP: {zpath.resolve()}")
|
||
print("把 V12 ZIP 上传给 ChatGPT。")
|
||
|
||
finally:
|
||
if temp_v11 and temp_v11.exists():
|
||
shutil.rmtree(temp_v11, ignore_errors=True)
|
||
|
||
|
||
if __name__ == "__main__":
|
||
main()
|