394d5dae3f
- 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)
101 lines
4.3 KiB
Python
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()
|