← back to Novasuede Onboard
qa_scan.py
55 lines
#!/usr/bin/env python3
"""QA scan of data/enriched.ndjson: flag color-name vs measured-depth mismatches
and summarize the color/style distribution before the live write.
Optionally --fix snaps clear mismatches to the hue-family's depth-appropriate name."""
import sys, json, collections, pathlib, colorsys
DATA = pathlib.Path(__file__).resolve().parent / "data"
FIX = "--fix" in sys.argv
# colorway names that are inherently PALE/LIGHT (should not name a deep/dark swatch)
PALE_NAMES = {"chalk","alabaster","cotton","marshmallow","swiss-coffee","ivory","eggshell","bone",
"cream","linen","porcelain","antique-white","powder","buttercream","champagne",
"blush","lavender","lilac","celadon","pistachio","sky","dove","mauve","oyster"}
# family deep-name fallback by measured hue family
# NOTE: hue_family returns "pink" for 290-345, which includes magenta/purple — so a
# deep swatch there is usually purple, not red. Map pink-deep to Plum (purple), never
# Garnet (a red), and run AFTER descriptions so tag+body never disagree (lesson 2026-06-24).
DEEP_BY_FAMILY = {"purple":"Aubergine","blue":"Navy","green":"Forest","red":"Oxblood",
"orange":"Espresso","yellow":"Ochre","pink":"Plum",
"neutral_dark":"Charcoal","neutral_mid":"Slate"}
def hsl(hexstr):
h=hexstr.lstrip("#"); r,g,b=(int(h[i:i+2],16)/255 for i in (0,2,4))
H,L,S=colorsys.rgb_to_hls(r,g,b); return H*360,S,L
def main():
rows=[json.loads(l) for l in (DATA/"enriched.ndjson").read_text().splitlines()]
ok=[r for r in rows if r.get("_ok")]
print(f"{len(ok)} ok / {len(rows)} total\n")
cw=collections.Counter(r["primary_colorway"] for r in ok)
st=collections.Counter(s for r in ok for s in (r.get("styles") or []))
print("=== colorway distribution ===")
for c,n in cw.most_common(): print(f" {n:3d} {c}")
print("\n=== style distribution ===")
for s,n in st.most_common(): print(f" {n:3d} {s}")
print("\n=== depth/name mismatches (deep|dark swatch given a pale name) ===")
fixes=0
for r in ok:
depth=r.get("depth"); name=(r.get("primary_colorway") or "").lower().replace(" ","-")
if depth in ("deep","dark") and name in PALE_NAMES:
fam=r.get("hue_family","")
newname=DEEP_BY_FAMILY.get(fam) or DEEP_BY_FAMILY.get(fam.split("_")[0]) or "Charcoal"
print(f" ⚠ {r['vendor_name']:>16} {r['dominant_hex']} depth={depth} "
f"name={r['primary_colorway']} -> {newname if FIX else '(suggest '+newname+')'}")
if FIX: r["primary_colorway"]=newname; fixes+=1
if FIX and fixes:
with (DATA/"enriched.ndjson").open("w") as f:
for r in rows: f.write(json.dumps(r)+"\n")
print(f"\napplied {fixes} fixes -> enriched.ndjson")
elif FIX:
print("\nno fixes needed")
if __name__=="__main__":
main()