"""SGV UI oracle: snapshot/diff of ONE C000025 row (READ-ONLY; never writes to Firebird). python scripts/sgv_product_snapshot.py before -> reads the whole row and saves docs/runs/snapshots/_before.json (operator changes exactly ONE field in the SGV product screen and saves) python scripts/sgv_product_snapshot.py diff [--ui-label "CFOP saĆ­da"] -> re-reads, prints FIELD/BEFORE/AFTER for every changed column, and (with --ui-label) records the result in docs/runs/sgv_ui_diff_ledger.json. A label is CONFIRMED only when exactly ONE non-technical column changed. """ import argparse import json import os import sys from datetime import date, datetime from decimal import Decimal from pathlib import Path ROOT = Path(__file__).resolve().parents[1] sys.path.insert(0, str(ROOT / "agent" / "src")) SNAP_DIR = ROOT / "docs" / "runs" / "snapshots" LEDGER = ROOT / "docs" / "runs" / "sgv_ui_diff_ledger.json" # Columns the ERP rewrites by itself on every save; ignored ONLY because they are clearly automatic. TECHNICAL = {"ULTIMA_ALTERACAO", "DATA_ALTERACAO", "DATA_ULTIMA_ALTERACAO", "USUARIO_ALTERACAO"} def norm(v): if isinstance(v, (bytes, bytearray)): import hashlib return "blob:" + hashlib.sha256(bytes(v)).hexdigest()[:16] if isinstance(v, (datetime, date)): return v.isoformat() if isinstance(v, Decimal): return str(v) return v.strip() if isinstance(v, str) else v def read_row(env, code): from gti_fiscal_agent.firebird_reader import _connect # read-only isolation conn = _connect(env) try: cur = conn.cursor() cur.execute("SELECT * FROM C000025 WHERE CODIGO = ?", (code,)) row = cur.fetchone() if row is None: raise SystemExit(f"product {code} not found") names = [c[0].strip() for c in cur.description] blob = {} return {n: norm(v) for n, v in zip(names, row)} finally: conn.close() def compute_diff(before, after): out = [] for f in sorted(set(before) | set(after)): if before.get(f) != after.get(f): out.append({"FIELD": f, "BEFORE": before.get(f), "AFTER": after.get(f), "technical": f in TECHNICAL}) return out def load_env(): for line in (ROOT / ".env").read_text(encoding="utf-8").splitlines(): if "=" in line and not line.startswith("#"): k, _, v = line.partition("="); os.environ.setdefault(k.strip(), v.strip()) def record(label, code, changes): real = [c for c in changes if not c["technical"]] ledger = json.loads(LEDGER.read_text(encoding="utf-8")) if LEDGER.exists() else [] entry = {"ui_label": label, "product": code, "recorded": datetime.now().isoformat(timespec="seconds"), "changed_columns": [c["FIELD"] for c in real], "before": {c["FIELD"]: c["BEFORE"] for c in real}, "after": {c["FIELD"]: c["AFTER"] for c in real}, "confirmed": len(real) == 1, "note": "" if len(real) == 1 else ("no column changed" if not real else "more than one column changed; repeat changing a single field")} ledger.append(entry) LEDGER.write_text(json.dumps(ledger, ensure_ascii=False, indent=1), encoding="utf-8") return entry def main(argv=None): ap = argparse.ArgumentParser() ap.add_argument("action", choices=["before", "diff"]); ap.add_argument("codigo"); ap.add_argument("--ui-label") a = ap.parse_args(argv) load_env() SNAP_DIR.mkdir(parents=True, exist_ok=True) path = SNAP_DIR / f"{a.codigo}_before.json" if a.action == "before": path.write_text(json.dumps(read_row(os.environ, a.codigo), ensure_ascii=False, indent=1, default=str), encoding="utf-8") print(f"snapshot saved: {path}"); return if not path.exists(): raise SystemExit("run 'before' first") changes = compute_diff(json.loads(path.read_text(encoding="utf-8")), read_row(os.environ, a.codigo)) print(f"{'FIELD':<28}{'BEFORE':<28}AFTER") for c in changes: print(f"{c['FIELD']:<28}{str(c['BEFORE']):<28}{c['AFTER']}{' (technical, ignored)' if c['technical'] else ''}") if a.ui_label: print(json.dumps(record(a.ui_label, a.codigo, changes), ensure_ascii=False)) if __name__ == "__main__": main()