[object Object]

← back to Wallco Ai

promote_edge_heal: dry-run-default tool, re-scans + flips only fresh-PASS non-ghost edge-heals

dcdbbed4bfa6fc6c5dd611a8d835f1fade0743f1 · 2026-05-26 19:58:18 -0700 · Steve Abrams

Files touched

Diff

commit dcdbbed4bfa6fc6c5dd611a8d835f1fade0743f1
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Tue May 26 19:58:18 2026 -0700

    promote_edge_heal: dry-run-default tool, re-scans + flips only fresh-PASS non-ghost edge-heals
---
 scripts/promote_edge_heal.py | 105 +++++++++++++++++++++++++++++++++++++++++++
 1 file changed, 105 insertions(+)

diff --git a/scripts/promote_edge_heal.py b/scripts/promote_edge_heal.py
new file mode 100644
index 0000000..908fa4f
--- /dev/null
+++ b/scripts/promote_edge_heal.py
@@ -0,0 +1,105 @@
+#!/usr/bin/env python3
+"""promote_edge_heal — promote edge-heal copies that pass a FRESH edges-agent scan.
+
+Safe by design:
+  - DRY-RUN by default. Prints what it WOULD promote. Flips nothing.
+  - --apply is required to actually set is_published=TRUE.
+  - Re-scans every candidate live (does NOT trust a stale verdict) — only a
+    fresh edges-agent PASS gets promoted, per "only clean designs welcome".
+  - Excludes any row whose self OR parent carries a BLEED_GHOST flag (a clean
+    edge feather can't fix a ghost source).
+  - Never deploys. After --apply it PRINTS the two commands Steve runs next
+    (refresh snapshot + deploy-kamatera.sh) — it does not run them.
+
+Reversible: each promoted row can be flipped back via
+  POST /api/fix-decision/<id>  verdict=reject  (or a manual is_published=FALSE).
+
+Usage:
+  python3 scripts/promote_edge_heal.py              # dry-run preview
+  python3 scripts/promote_edge_heal.py --apply       # actually promote PASSes
+  python3 scripts/promote_edge_heal.py --limit 50    # cap (for a staged rollout)
+"""
+import argparse, subprocess, sys
+from pathlib import Path
+
+sys.path.insert(0, str(Path.home() / '.claude/skills/edges-agent/scripts'))
+import scan as edges
+
+
+def psql(sql):
+    r = subprocess.run(['psql', 'dw_unified', '-At', '-F', '\x1f', '-c', sql],
+                       capture_output=True, text=True, check=True)
+    return [ln.split('\x1f') for ln in r.stdout.splitlines() if ln.strip()]
+
+
+def candidates(limit):
+    sql = (
+        "SELECT a.id, a.local_path, a.parent_design_id "
+        "FROM all_designs a "
+        "WHERE a.generator='pil-edge-heal' "
+        "  AND (a.is_published IS NULL OR a.is_published=FALSE) "
+        "  AND a.local_path IS NOT NULL "
+        "  AND a.notes NOT LIKE '%BLEED_GHOST%' "
+        "  AND (a.settlement_verdict IS NULL OR a.settlement_verdict NOT LIKE '%GHOST%') "
+        "ORDER BY a.id"
+    )
+    if limit:
+        sql += f" LIMIT {int(limit)}"
+    rows = psql(sql + ";")
+    # parent ghost exclusion
+    pids = sorted({int(r[2]) for r in rows if r[2] not in ('', None)})
+    parent_ghost = set()
+    if pids:
+        pg = psql("SELECT id FROM all_designs WHERE id IN (" +
+                  ",".join(str(p) for p in pids) +
+                  ") AND (notes LIKE '%BLEED_GHOST%' OR settlement_verdict LIKE '%GHOST%');")
+        parent_ghost = {int(x[0]) for x in pg}
+    return [(int(r[0]), r[1]) for r in rows
+            if r[2] in ('', None) or int(r[2]) not in parent_ghost]
+
+
+def main():
+    ap = argparse.ArgumentParser()
+    ap.add_argument('--apply', action='store_true', help='actually flip is_published=TRUE')
+    ap.add_argument('--limit', type=int, default=None)
+    args = ap.parse_args()
+
+    cands = candidates(args.limit)
+    print(f"candidates (pil-edge-heal, unpublished, non-ghost): {len(cands)}", flush=True)
+
+    passed = []
+    for i, (rid, path) in enumerate(cands, 1):
+        try:
+            res = edges.scan_path(Path(path))
+            if res.get('ok') and res['verdict'] == 'PASS':
+                passed.append(rid)
+        except Exception:
+            pass
+        if i % 200 == 0 or i == len(cands):
+            print(f"  re-scanned {i}/{len(cands)}  PASS so far={len(passed)}", flush=True)
+
+    print(f"\nfresh-scan PASS (promotable): {len(passed)}")
+    if not passed:
+        print("nothing to promote"); return
+
+    if not args.apply:
+        print("\nDRY-RUN — nothing changed. Re-run with --apply to promote.")
+        print(f"first 15 ids: {passed[:15]}")
+        return
+
+    # --apply: flip in one transaction
+    idlist = ",".join(str(x) for x in passed)
+    r = subprocess.run(
+        ['psql', 'dw_unified', '-q', '-v', 'ON_ERROR_STOP=1', '-c',
+         f"UPDATE all_designs SET is_published=TRUE WHERE id IN ({idlist});"],
+        capture_output=True, text=True)
+    if r.returncode != 0:
+        print(f"PSQL FAILED: {r.stderr[:400]}", file=sys.stderr); sys.exit(1)
+    print(f"\nPROMOTED {len(passed)} rows to is_published=TRUE.")
+    print("\nNEXT (run these yourself — deploy is Steve-gated):")
+    print("  python3 scripts/refresh_designs_snapshot.py")
+    print("  ./deploy-kamatera.sh")
+
+
+if __name__ == '__main__':
+    main()

← fdb9464 Add style-aware-sample-verify.js — settlement + edges-scan +  ·  back to Wallco Ai  ·  snapshot before reject sweep (preserve parallel-session in-p 5778b04 →