← back to Dw Color Image Audit
scripts/select_push_targets.py
95 lines
#!/usr/bin/env python3
"""DTD D1=(c) target selector — build dw_color_staging.push_targets.
Policy (DTD 3/3, 2026-06-20): push a product's corrected color metafield ONLY when
- v2 real_hex is a valid #rrggbb, AND
- v2 image_family is known (not null / 'unknown'), AND
- product is ACTIVE, AND
- the cached hex was the near-white defect OR v2 differs MATERIALLY from cached
(cached null/empty, or RGB euclidean distance(cached, real) > 40).
Never overwrite an already-good cached hex with a low-confidence v2 guess.
All local + reversible (isolated dw_color_staging DB)."""
import subprocess, math, sys
WHITE_SET = {'#F2F2F2', '#FFFFFF', '#FAFAFA', '#F5F5F5', '#FFFFFE', '#FEFEFE'}
MATERIAL_DIST = 40.0
def rgb(h):
if not h or len(h) != 7 or h[0] != '#':
return None
try:
return (int(h[1:3], 16), int(h[3:5], 16), int(h[5:7], 16))
except ValueError:
return None
def is_valid_hex(h):
return rgb(h) is not None
def near_white(h):
if not h:
return False
H = h.upper()
if H in WHITE_SET:
return True
# pattern #FxFxFx (every other nibble F) — matches analyze.sql near-white capture
return len(H) == 7 and H[1] == 'F' and H[3] == 'F' and H[5] == 'F'
def dist(a, b):
ra, rb = rgb(a), rgb(b)
if ra is None or rb is None:
return None
return math.sqrt(sum((x - y) ** 2 for x, y in zip(ra, rb)))
def q(sql, inp=None):
p = subprocess.run(['psql', '-d', 'dw_color_staging', '-v', 'ON_ERROR_STOP=1', '-c', sql],
input=inp, text=True, capture_output=True)
if p.returncode:
sys.stderr.write(p.stdout + p.stderr); sys.exit(1)
return p.stdout
# pull candidates
rows = subprocess.run(
['psql', '-d', 'dw_color_staging', '-tAF', '\t', '-c',
"SELECT shopify_product_id, vendor, status, declared, cached_hex, real_hex, image_family "
"FROM product_colors_v2 WHERE lower(status)='active'"],
text=True, capture_output=True).stdout.strip().split('\n')
targets, skipped = [], {'bad_hex': 0, 'unknown_fam': 0, 'good_cached_kept': 0}
for line in rows:
if not line:
continue
spid, vendor, status, declared, cached, real, fam = (line.split('\t') + [''] * 7)[:7]
if not is_valid_hex(real):
skipped['bad_hex'] += 1; continue
if not fam or fam.lower() == 'unknown':
skipped['unknown_fam'] += 1; continue
nw = near_white(cached)
d = dist(cached, real)
material = (not cached) or nw or (d is not None and d > MATERIAL_DIST) or (d is None)
if not material:
skipped['good_cached_kept'] += 1; continue
targets.append((spid, vendor, status, declared, cached or '', real, fam,
'near_white' if nw else ('cached_missing' if not cached else f'dist={d:.0f}')))
# (re)build push_targets
q("""DROP TABLE IF EXISTS push_targets;
CREATE TABLE push_targets(
shopify_product_id text PRIMARY KEY, vendor text, status text, declared text,
cached_hex text, color_hex text NOT NULL, color_name text, reason text,
backup_color_hex text, backup_color_details text, backed_up_at timestamptz,
push_status text NOT NULL DEFAULT 'pending', pushed_at timestamptz,
response text, is_canary boolean DEFAULT false);""")
buf = []
for spid, vendor, status, declared, cached, real, fam, reason in targets:
name = fam[:1].upper() + fam[1:] if fam else ''
cells = [spid, vendor, status, declared, cached, real, name, reason]
buf.append('\t'.join((c or '').replace('\\', '\\\\').replace('\t', ' ').replace('\n', ' ') for c in cells))
p = subprocess.run(
['psql', '-d', 'dw_color_staging', '-c',
"COPY push_targets(shopify_product_id,vendor,status,declared,cached_hex,color_hex,color_name,reason) FROM STDIN"],
input='\n'.join(buf) + '\n', text=True, capture_output=True)
sys.stderr.write(p.stdout + p.stderr)
print(f"targets={len(targets)} skipped={skipped}")