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)
This commit is contained in:
@@ -0,0 +1,100 @@
|
||||
#!/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()
|
||||
Reference in New Issue
Block a user