122 lines
4.2 KiB
Python
122 lines
4.2 KiB
Python
#!/usr/bin/env python3
|
|
"""Compare local regenerated model inputs with committed inputs.
|
|
|
|
This script is intentionally read-only with respect to data/. It writes a report
|
|
under _local/reports/ and exits nonzero when files differ or are missing.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import csv
|
|
import hashlib
|
|
import os
|
|
from pathlib import Path
|
|
from typing import Iterable
|
|
|
|
|
|
MODEL_INPUTS = [
|
|
"text_data.csv",
|
|
"expert.csv",
|
|
"lr_data.csv",
|
|
"union_mapping.csv",
|
|
"party_families.csv",
|
|
]
|
|
|
|
|
|
def normalized_rows(path: Path) -> Iterable[list[str]]:
|
|
with path.open(newline="", encoding="utf-8") as fh:
|
|
reader = csv.reader(fh)
|
|
for row in reader:
|
|
yield ["" if value is None else value.strip() for value in row]
|
|
|
|
|
|
def normalized_hash(path: Path) -> str:
|
|
h = hashlib.sha256()
|
|
for row in normalized_rows(path):
|
|
h.update(("\x1f".join(row) + "\n").encode("utf-8"))
|
|
return h.hexdigest()
|
|
|
|
|
|
def csv_shape(path: Path) -> tuple[int, int, list[str]]:
|
|
with path.open(newline="", encoding="utf-8") as fh:
|
|
reader = csv.reader(fh)
|
|
try:
|
|
header = next(reader)
|
|
except StopIteration:
|
|
return 0, 0, []
|
|
rows = sum(1 for _ in reader)
|
|
return rows, len(header), header
|
|
|
|
|
|
def first_difference(left: Path, right: Path, max_scan: int = 100000) -> str:
|
|
for idx, (lrow, rrow) in enumerate(zip(normalized_rows(left), normalized_rows(right)), start=1):
|
|
if lrow != rrow:
|
|
return f"line {idx}: committed={lrow[:8]!r} generated={rrow[:8]!r}"
|
|
if idx >= max_scan:
|
|
break
|
|
return "no differing row found in scan window; row counts may differ"
|
|
|
|
|
|
def main() -> int:
|
|
repo_root = Path(__file__).resolve().parents[1]
|
|
committed_dir = Path(os.environ.get("PARTY2D_COMMITTED_INPUT_DIR", repo_root / "data"))
|
|
generated_dir = Path(os.environ.get("PARTY2D_GENERATED_INPUT_DIR", repo_root / "_local" / "generated-inputs"))
|
|
report_dir = Path(os.environ.get("PARTY2D_REPORT_DIR", repo_root / "_local" / "reports"))
|
|
report_dir.mkdir(parents=True, exist_ok=True)
|
|
report_path = report_dir / "input_comparison.md"
|
|
|
|
lines: list[str] = []
|
|
lines.append("# Generated input comparison")
|
|
lines.append("")
|
|
lines.append(f"Committed input dir: `{committed_dir}`")
|
|
lines.append(f"Generated input dir: `{generated_dir}`")
|
|
lines.append("")
|
|
lines.append("| file | status | committed rows | generated rows | committed hash | generated hash | notes |")
|
|
lines.append("| --- | --- | ---: | ---: | --- | --- | --- |")
|
|
|
|
ok = True
|
|
for name in MODEL_INPUTS:
|
|
committed = committed_dir / name
|
|
generated = generated_dir / name
|
|
if not committed.exists() or not generated.exists():
|
|
ok = False
|
|
lines.append(
|
|
f"| `{name}` | missing | | | | | committed exists={committed.exists()}, generated exists={generated.exists()} |"
|
|
)
|
|
continue
|
|
|
|
c_rows, c_cols, c_header = csv_shape(committed)
|
|
g_rows, g_cols, g_header = csv_shape(generated)
|
|
c_hash = normalized_hash(committed)
|
|
g_hash = normalized_hash(generated)
|
|
same = c_hash == g_hash
|
|
status = "match" if same else "diff"
|
|
if not same:
|
|
ok = False
|
|
notes = []
|
|
if c_cols != g_cols:
|
|
notes.append(f"columns {c_cols}!={g_cols}")
|
|
if c_header != g_header:
|
|
notes.append("header differs")
|
|
if c_rows != g_rows:
|
|
notes.append(f"rows {c_rows}!={g_rows}")
|
|
if not same and not notes:
|
|
notes.append(first_difference(committed, generated))
|
|
lines.append(
|
|
f"| `{name}` | {status} | {c_rows} | {g_rows} | `{c_hash[:12]}` | `{g_hash[:12]}` | {'; '.join(notes)} |"
|
|
)
|
|
|
|
lines.append("")
|
|
lines.append("Committed inputs were not modified by this comparison.")
|
|
report_path.write_text("\n".join(lines) + "\n", encoding="utf-8")
|
|
print(f"Wrote comparison report: {report_path}")
|
|
if ok:
|
|
print("Generated inputs match committed inputs.")
|
|
return 0
|
|
print("Generated inputs differ from committed inputs or are missing.")
|
|
return 1
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|