Files
veripath/implementation/auditing/audit_diff.py
T
Leonard 394d5dae3f feat(auditing): pipeline v1.1 — report generation + scripted gate + temporal delta
- report_generate.py: REPORT-final.md + VALIDATION.md from findings + raw
  capture; per-surface status from summary.surfaces_ok; Data Limitations
- report_gate.py: §6 gate scripted (count/evidence/identity/material
  support); non-zero exit blocks delivery
- audit_diff.py: before/after capture comparison -> DELTA.md
- audit_pipeline.sh: 2 steps -> 4; collision-safe capture copy (re-runs
  preserve baseline); relative output dir resolved to absolute
- beta-audit-process.md: locked v1.0 -> v1.1 (+ dated decision record)
- all 9 runs of the 2026-08-15 batch regenerated + gated (9/9 PASS)
- live end-to-end proof: Gilmore re-run, DELTA.md 0 changes (same day)
2026-08-15 16:52:23 +00:00

101 lines
4.3 KiB
Python

#!/usr/bin/env python3
"""Locked process v1.1: temporal delta — compare two raw captures, emit DELTA.md.
Usage:
audit_diff.py BEFORE_RUN_DIR AFTER_RUN_DIR
Compares the LATEST capture in each run dir across the fields the audit
scores (name, phone, address, hours per day, rating, reviews, website,
category, description, schema presence) and writes DELTA.md into the AFTER
dir. This is the proof-of-fix artifact: after the client applies the
recommendations, re-run the pipeline and the delta is the invoice evidence.
Exit 0 always (a diff is a result, not an error); DELTA.md lists every
change and confirms unchanged fields.
"""
import json, os, sys, glob
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
import report_generate as rg # latest_capture + normalization
DAYS = ["monday", "tuesday", "wednesday", "thursday", "friday", "saturday", "sunday"]
def load(d):
if os.path.isfile(d):
f = d
else:
f = rg.latest_capture(d)
raw = json.load(open(f))
src = raw.get("sources", {})
g = src.get("google_business_profile") or {}
a = src.get("apple_maps") or {}
w = src.get("website") if isinstance(src.get("website"), dict) else {}
return raw, g, a, w
def row(label, before, after, fmt=str):
b, n = fmt(before), fmt(after)
if b == n:
return None, (b, b)
return (label, b, n), (b, n)
def schema_present(w):
return bool(w.get("schema")) if isinstance(w, dict) else False
def compare(before_dir, after_dir):
raw_b, g_b, a_b, w_b = load(before_dir)
raw_a, g_a, a_a, w_a = load(after_dir)
changes, same = [], []
def add(label, b, n, fmt=str):
r, vals = row(label, b, n, fmt)
(changes if r else same).append(r or (label, vals[0]))
add("Name (GBP)", g_b.get("name"), g_a.get("name"))
add("Phone (GBP)", rg.phone_core(g_b.get("phone")), rg.phone_core(g_a.get("phone")))
add("Phone (Apple)", rg.phone_core(a_b.get("phone")), rg.phone_core(a_a.get("phone")))
add("Address (GBP)", rg.addr_core(g_b.get("address")), rg.addr_core(g_a.get("address")))
add("Address (Apple)", rg.addr_core(a_b.get("address")), rg.addr_core(a_a.get("address")))
add("Website (GBP)", g_b.get("website"), g_a.get("website"))
add("Rating (GBP)", g_b.get("rating"), g_a.get("rating"))
add("Reviews (GBP)", g_b.get("reviews"), g_a.get("reviews"))
add("Category (GBP)", g_b.get("category"), g_a.get("category"))
add("Description set (GBP)", bool(g_b.get("description")), bool(g_a.get("description")))
add("Schema (website)", schema_present(w_b), schema_present(w_a))
for day in DAYS:
gh_b, gh_a = (g_b.get("hours") or {}).get(day), (g_a.get("hours") or {}).get(day)
if gh_b != gh_a:
changes.append((f"Hours {day} (GBP)", gh_b or "", gh_a or ""))
ah_b, ah_a = (a_b.get("hours") or {}).get(day), (a_a.get("hours") or {}).get(day)
if ah_b != ah_a:
changes.append((f"Hours {day} (Apple)", ah_b or "", ah_a or ""))
return changes, same, raw_b, raw_a
def main():
if len(sys.argv) != 3:
sys.exit("usage: audit_diff.py BEFORE_RUN_DIR AFTER_RUN_DIR")
before_dir, after_dir = sys.argv[1], sys.argv[2]
changes, same, raw_b, raw_a = compare(before_dir, after_dir)
L = [f"# Delta: {raw_a.get('name', '?')}", "",
f"**Before:** {rg.run_date(raw_b)} (`{os.path.basename(before_dir)}`) ",
f"**After:** {rg.run_date(raw_a)} (`{os.path.basename(after_dir)}`) ",
"", f"**{len(changes)} field(s) changed** across the audited surfaces.", ""]
if changes:
L += ["## Changed", "", "| Field | Before | After |", "|-------|--------|-------|"]
for label, b, n in changes:
L.append(f"| {label} | {b} | {n} |")
L.append("")
else:
L += ["No changes detected between the two captures.", ""]
L += ["## Unchanged", ""]
L += [f"- {label}: {v}" for label, v in same]
L += ["", "*Generated by audit_diff.py (locked process v1.1). Compare only — no judgment.*", ""]
out = os.path.join(after_dir, "DELTA.md") if os.path.isdir(after_dir) else \
os.path.join(os.path.dirname(os.path.abspath(after_dir)), "DELTA.md")
open(out, "w").write("\n".join(L))
print(f"{os.path.basename(after_dir)}: {len(changes)} change(s) -> {out}")
if __name__ == "__main__":
main()