← 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()