CM93 MySQL importer validated baseline
This commit is contained in:
475
cm93_v10_1_result_digest_v10_2.py
Normal file
475
cm93_v10_1_result_digest_v10_2.py
Normal file
@@ -0,0 +1,475 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
CM93 V10.1 结果汇总与完整性验证器
|
||||
Version: v10.2-result-digest
|
||||
|
||||
用途:
|
||||
- 不连接 MySQL。
|
||||
- 不重新运行 normalization。
|
||||
- 直接读取 V10.1 的输出目录或 ZIP。
|
||||
- 自动判断 V10.1 是否整体 PASS。
|
||||
- 生成一个很小的结果摘要 ZIP,便于上传给 ChatGPT。
|
||||
|
||||
支持输入:
|
||||
1. V10.1 完整输出目录,例如:
|
||||
cm93_normalization_canonical_v10_1_dataset_1_20260831_215602
|
||||
|
||||
2. V10.1 完整 ZIP,例如:
|
||||
cm93_normalization_canonical_v10_1_dataset_1_20260831_215602.zip
|
||||
|
||||
3. 只有部分 CSV 的 ZIP。
|
||||
此时脚本会明确显示“INCOMPLETE”,不会误判整体 PASS。
|
||||
|
||||
执行:
|
||||
python cm93_v10_1_result_digest_v10_2.py ^
|
||||
--input "D:\...\cm93_normalization_canonical_v10_1_dataset_1_20260831_215602"
|
||||
|
||||
或:
|
||||
|
||||
python cm93_v10_1_result_digest_v10_2.py ^
|
||||
--input "D:\...\cm93_normalization_canonical_v10_1_dataset_1_20260831_215602.zip"
|
||||
|
||||
输出:
|
||||
- V10_1_结果摘要_v10_2.txt
|
||||
- V10_1_结果摘要_v10_2.json
|
||||
- cm93_v10_1_result_digest_v10_2.zip
|
||||
|
||||
只读,不修改原始结果。
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import csv
|
||||
import json
|
||||
import shutil
|
||||
import sys
|
||||
import tempfile
|
||||
import zipfile
|
||||
from collections import Counter
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
|
||||
EXPECTED_FILES = {
|
||||
"summary": "normalization_canonical_summary_v10_1.txt",
|
||||
"report": "normalization_canonical_report_v10_1.json",
|
||||
"golden_csv": "normalization_golden_vectors_v10_1.csv",
|
||||
"golden_json": "normalization_golden_vectors_v10_1.json",
|
||||
"variant": "normalization_variant_results_v10_1.csv",
|
||||
"face_order": "normalization_face_order_results_v10_1.csv",
|
||||
"failures": "normalization_canonical_failures_v10_1.csv",
|
||||
"bbox": "normalization_bbox_metadata_impact_v10_1.csv",
|
||||
}
|
||||
|
||||
|
||||
def parse_args():
|
||||
p = argparse.ArgumentParser(
|
||||
description="CM93 V10.1 result digest verifier v10.2"
|
||||
)
|
||||
p.add_argument("--input", required=True, help="V10.1 输出目录或 ZIP")
|
||||
p.add_argument("--output-dir", default=None)
|
||||
return p.parse_args()
|
||||
|
||||
|
||||
def find_file(root: Path, filename: str) -> Optional[Path]:
|
||||
direct = root / filename
|
||||
if direct.exists():
|
||||
return direct
|
||||
|
||||
hits = list(root.rglob(filename))
|
||||
if hits:
|
||||
return hits[0]
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def read_csv(path: Path) -> List[Dict[str, str]]:
|
||||
with path.open("r", encoding="utf-8-sig", newline="") as f:
|
||||
return list(csv.DictReader(f))
|
||||
|
||||
|
||||
def intv(v: Any, default: int = 0) -> int:
|
||||
try:
|
||||
return int(v)
|
||||
except Exception:
|
||||
return default
|
||||
|
||||
|
||||
def boolv(v: Any) -> bool:
|
||||
if isinstance(v, bool):
|
||||
return v
|
||||
if v is None:
|
||||
return False
|
||||
s = str(v).strip().lower()
|
||||
return s in {"1", "true", "yes", "y", "pass"}
|
||||
|
||||
|
||||
def main():
|
||||
args = parse_args()
|
||||
src = Path(args.input).expanduser().resolve()
|
||||
|
||||
if not src.exists():
|
||||
print(f"输入不存在:{src}")
|
||||
sys.exit(2)
|
||||
|
||||
temp_dir = None
|
||||
|
||||
try:
|
||||
if src.is_file() and src.suffix.lower() == ".zip":
|
||||
temp_dir = Path(tempfile.mkdtemp(prefix="cm93_v10_1_digest_"))
|
||||
with zipfile.ZipFile(src, "r") as z:
|
||||
z.extractall(temp_dir)
|
||||
root = temp_dir
|
||||
elif src.is_dir():
|
||||
root = src
|
||||
else:
|
||||
print("输入必须是 V10.1 输出目录或 ZIP。")
|
||||
sys.exit(2)
|
||||
|
||||
found: Dict[str, Optional[Path]] = {
|
||||
key: find_file(root, filename)
|
||||
for key, filename in EXPECTED_FILES.items()
|
||||
}
|
||||
|
||||
present = {k: bool(v) for k, v in found.items()}
|
||||
|
||||
digest: Dict[str, Any] = {
|
||||
"version": "v10.2-result-digest",
|
||||
"generated_at": datetime.now().isoformat(timespec="seconds"),
|
||||
"input": str(src),
|
||||
"files_present": present,
|
||||
"complete_core_set": all(
|
||||
present[k]
|
||||
for k in (
|
||||
"report",
|
||||
"golden_csv",
|
||||
"variant",
|
||||
"face_order",
|
||||
"failures",
|
||||
"bbox",
|
||||
)
|
||||
),
|
||||
"checks": {},
|
||||
"overall": "INCOMPLETE",
|
||||
}
|
||||
|
||||
checks = digest["checks"]
|
||||
|
||||
# --------------------------------------------------------------
|
||||
# Report JSON
|
||||
# --------------------------------------------------------------
|
||||
if found["report"]:
|
||||
report = json.loads(
|
||||
found["report"].read_text(encoding="utf-8")
|
||||
)
|
||||
summary = report.get("summary", {})
|
||||
|
||||
checks["report"] = {
|
||||
"overall_pass": summary.get("overall_pass"),
|
||||
"total_polygon_records": summary.get("total_polygon_records"),
|
||||
"valid_polygon_records": summary.get("valid_polygon_records"),
|
||||
"invalid_polygon_records": summary.get("invalid_polygon_records"),
|
||||
"valid_policy_changed_count":
|
||||
summary.get("valid_policy_changed_count"),
|
||||
"valid_hash_changed_count":
|
||||
summary.get("valid_hash_changed_count"),
|
||||
"golden_vector_count":
|
||||
summary.get("golden_vector_count"),
|
||||
"golden_record_status_counts":
|
||||
summary.get("golden_record_status_counts"),
|
||||
"input_variant_case_count":
|
||||
summary.get("input_variant_case_count"),
|
||||
"input_variant_status_counts":
|
||||
summary.get("input_variant_status_counts"),
|
||||
"face_order_case_count":
|
||||
summary.get("face_order_case_count"),
|
||||
"face_order_hash_mismatch_count":
|
||||
summary.get("face_order_hash_mismatch_count"),
|
||||
"canonical_topology_failure_count":
|
||||
summary.get("canonical_topology_failure_count"),
|
||||
"repeat_hash_failure_count":
|
||||
summary.get("repeat_hash_failure_count"),
|
||||
"input_variant_failure_record_count":
|
||||
summary.get("input_variant_failure_record_count"),
|
||||
"future_area_feature_bbox_update_needed_count":
|
||||
summary.get("future_area_feature_bbox_update_needed_count"),
|
||||
}
|
||||
|
||||
# --------------------------------------------------------------
|
||||
# Golden vectors
|
||||
# --------------------------------------------------------------
|
||||
if found["golden_csv"]:
|
||||
rows = read_csv(found["golden_csv"])
|
||||
statuses = Counter(
|
||||
str(r.get("record_status", "")).strip()
|
||||
for r in rows
|
||||
)
|
||||
hashes = [
|
||||
r.get("canonical_wkb_sha256", "")
|
||||
for r in rows
|
||||
if r.get("canonical_wkb_sha256")
|
||||
]
|
||||
|
||||
checks["golden"] = {
|
||||
"rows": len(rows),
|
||||
"status_counts": dict(statuses),
|
||||
"unique_hash_count": len(set(hashes)),
|
||||
"missing_hash_count":
|
||||
sum(not bool(r.get("canonical_wkb_sha256")) for r in rows),
|
||||
"variant_failure_records":
|
||||
sum(
|
||||
intv(r.get("input_variant_failure_count")) > 0
|
||||
for r in rows
|
||||
),
|
||||
"face_order_unstable_records":
|
||||
sum(
|
||||
not boolv(r.get("face_order_hash_stable"))
|
||||
for r in rows
|
||||
),
|
||||
"repeat_unstable_records":
|
||||
sum(
|
||||
not boolv(r.get("repeat_hash_stable"))
|
||||
for r in rows
|
||||
),
|
||||
"canonical_topology_changed_records":
|
||||
sum(
|
||||
not boolv(r.get("canonical_topology_unchanged"))
|
||||
for r in rows
|
||||
),
|
||||
}
|
||||
|
||||
# --------------------------------------------------------------
|
||||
# Variant
|
||||
# --------------------------------------------------------------
|
||||
if found["variant"]:
|
||||
rows = read_csv(found["variant"])
|
||||
statuses = Counter(
|
||||
str(r.get("status", "")).strip()
|
||||
for r in rows
|
||||
)
|
||||
|
||||
checks["variant"] = {
|
||||
"rows": len(rows),
|
||||
"status_counts": dict(statuses),
|
||||
"fail_rows":
|
||||
sum(str(r.get("status", "")).strip() != "PASS" for r in rows),
|
||||
"hash_mismatch_rows":
|
||||
sum(
|
||||
not boolv(r.get("matches_baseline_hash"))
|
||||
for r in rows
|
||||
if str(r.get("status", "")).strip() != "EXCEPTION"
|
||||
),
|
||||
"topology_mismatch_rows":
|
||||
sum(
|
||||
not boolv(r.get("topology_equal_baseline"))
|
||||
for r in rows
|
||||
if str(r.get("status", "")).strip() != "EXCEPTION"
|
||||
),
|
||||
}
|
||||
|
||||
# --------------------------------------------------------------
|
||||
# Face order
|
||||
# --------------------------------------------------------------
|
||||
if found["face_order"]:
|
||||
rows = read_csv(found["face_order"])
|
||||
checks["face_order"] = {
|
||||
"rows": len(rows),
|
||||
"hash_mismatch_rows":
|
||||
sum(
|
||||
not boolv(r.get("matches_baseline_hash"))
|
||||
for r in rows
|
||||
),
|
||||
}
|
||||
|
||||
# --------------------------------------------------------------
|
||||
# Failure CSV
|
||||
# --------------------------------------------------------------
|
||||
if found["failures"]:
|
||||
rows = read_csv(found["failures"])
|
||||
# 只有表头时 DictReader 返回 0 rows,正是期望。
|
||||
checks["failures"] = {
|
||||
"rows": len(rows),
|
||||
}
|
||||
|
||||
# --------------------------------------------------------------
|
||||
# BBox metadata impact
|
||||
# --------------------------------------------------------------
|
||||
if found["bbox"]:
|
||||
rows = read_csv(found["bbox"])
|
||||
checks["bbox"] = {
|
||||
"rows": len(rows),
|
||||
"bbox_changed_rows":
|
||||
sum(
|
||||
boolv(r.get("bbox_changed_over_tolerance"))
|
||||
for r in rows
|
||||
),
|
||||
"future_feature_bbox_update_needed_rows":
|
||||
sum(
|
||||
boolv(r.get("future_feature_bbox_update_needed"))
|
||||
for r in rows
|
||||
),
|
||||
"rows_with_connector":
|
||||
sum(
|
||||
float(r.get("discarded_connector_length_deg") or 0) > 0
|
||||
for r in rows
|
||||
),
|
||||
}
|
||||
|
||||
# --------------------------------------------------------------
|
||||
# Independent overall verdict
|
||||
# --------------------------------------------------------------
|
||||
required_for_pass = all(
|
||||
k in checks
|
||||
for k in (
|
||||
"report",
|
||||
"golden",
|
||||
"variant",
|
||||
"face_order",
|
||||
"failures",
|
||||
"bbox",
|
||||
)
|
||||
)
|
||||
|
||||
if required_for_pass:
|
||||
report_check = checks["report"]
|
||||
golden = checks["golden"]
|
||||
variant = checks["variant"]
|
||||
face = checks["face_order"]
|
||||
failures = checks["failures"]
|
||||
|
||||
pass_conditions = [
|
||||
boolv(report_check.get("overall_pass")),
|
||||
intv(report_check.get("invalid_polygon_records")) == 55,
|
||||
intv(report_check.get("valid_policy_changed_count")) == 0,
|
||||
intv(report_check.get("valid_hash_changed_count")) == 0,
|
||||
|
||||
golden["rows"] == 55,
|
||||
golden["status_counts"].get("PASS", 0) == 55,
|
||||
golden["status_counts"].get("FAIL", 0) == 0,
|
||||
golden["missing_hash_count"] == 0,
|
||||
golden["variant_failure_records"] == 0,
|
||||
golden["face_order_unstable_records"] == 0,
|
||||
golden["repeat_unstable_records"] == 0,
|
||||
golden["canonical_topology_changed_records"] == 0,
|
||||
|
||||
variant["rows"] == 55 * 6,
|
||||
variant["fail_rows"] == 0,
|
||||
variant["hash_mismatch_rows"] == 0,
|
||||
variant["topology_mismatch_rows"] == 0,
|
||||
|
||||
face["rows"] == 55 * 4,
|
||||
face["hash_mismatch_rows"] == 0,
|
||||
|
||||
failures["rows"] == 0,
|
||||
]
|
||||
|
||||
digest["overall"] = (
|
||||
"PASS"
|
||||
if all(pass_conditions)
|
||||
else "FAIL"
|
||||
)
|
||||
digest["pass_conditions"] = pass_conditions
|
||||
else:
|
||||
digest["overall"] = "INCOMPLETE"
|
||||
|
||||
# --------------------------------------------------------------
|
||||
# Output
|
||||
# --------------------------------------------------------------
|
||||
outdir = (
|
||||
Path(args.output_dir).expanduser().resolve()
|
||||
if args.output_dir
|
||||
else src.parent
|
||||
)
|
||||
outdir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
txt_path = outdir / "V10_1_结果摘要_v10_2.txt"
|
||||
json_path = outdir / "V10_1_结果摘要_v10_2.json"
|
||||
zip_path = outdir / "cm93_v10_1_result_digest_v10_2.zip"
|
||||
|
||||
json_path.write_text(
|
||||
json.dumps(digest, ensure_ascii=False, indent=2),
|
||||
encoding="utf-8"
|
||||
)
|
||||
|
||||
lines = []
|
||||
lines.append("CM93 V10.1 结果汇总与完整性验证 v10.2")
|
||||
lines.append("=" * 78)
|
||||
lines.append(f"Input: {src}")
|
||||
lines.append(f"Overall: {digest['overall']}")
|
||||
lines.append("")
|
||||
|
||||
lines.append("FILES")
|
||||
for key, filename in EXPECTED_FILES.items():
|
||||
lines.append(
|
||||
f" {key:12s} "
|
||||
f"{'YES' if present[key] else 'NO ':3s} "
|
||||
f"{filename}"
|
||||
)
|
||||
|
||||
for section, value in checks.items():
|
||||
lines.append("")
|
||||
lines.append(section.upper())
|
||||
for k, v in value.items():
|
||||
lines.append(f" {k}: {v}")
|
||||
|
||||
if digest["overall"] == "INCOMPLETE":
|
||||
lines.append("")
|
||||
lines.append(
|
||||
"INCOMPLETE:当前输入不足以判定 V10.1 整体 PASS。"
|
||||
)
|
||||
elif digest["overall"] == "PASS":
|
||||
lines.append("")
|
||||
lines.append(
|
||||
"PASS:V10.1 canonical normalization 全部门槛通过。"
|
||||
)
|
||||
else:
|
||||
lines.append("")
|
||||
lines.append(
|
||||
"FAIL:至少一个 V10.1 canonical validation 门槛未通过。"
|
||||
)
|
||||
|
||||
txt_path.write_text(
|
||||
"\n".join(lines),
|
||||
encoding="utf-8"
|
||||
)
|
||||
|
||||
with zipfile.ZipFile(
|
||||
zip_path,
|
||||
"w",
|
||||
compression=zipfile.ZIP_DEFLATED
|
||||
) as z:
|
||||
z.write(txt_path, txt_path.name)
|
||||
z.write(json_path, json_path.name)
|
||||
|
||||
# 把关键小文件一起放进 digest ZIP。
|
||||
for key in (
|
||||
"summary",
|
||||
"report",
|
||||
"golden_csv",
|
||||
"variant",
|
||||
"face_order",
|
||||
"failures",
|
||||
"bbox",
|
||||
):
|
||||
p = found.get(key)
|
||||
if p and p.exists():
|
||||
z.write(p, p.name)
|
||||
|
||||
print("")
|
||||
print(f"Overall: {digest['overall']}")
|
||||
print(f"TXT: {txt_path}")
|
||||
print(f"JSON: {json_path}")
|
||||
print(f"ZIP: {zip_path}")
|
||||
print("")
|
||||
print("把这个很小的 digest ZIP 上传给 ChatGPT。")
|
||||
|
||||
finally:
|
||||
if temp_dir and temp_dir.exists():
|
||||
shutil.rmtree(temp_dir, ignore_errors=True)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user