feat(greenfield): rebuild SlabUploader M1-M3 on clean foundation, all gates green

Clean re-implementation of the full product (not a port of the old M3 tree):
- P0 foundation: 13-table schema + per-call sqlite factory (thread-safe),
  FastAPI app (health, JSON logging + secret redaction, CORS, startup guard).
- P1 M1 Capture: mask/sheetKeep, geometry/rect (min-area rect), measure,
  png crop/inventory/overlay, EXIF decodeOrientedBitmap.
- P2 M2 Photos: stateful capture session (batch, pick-cal, apply mask/crop
  to batch) + photos-ready lineup.
- P3 M3 Create: real backend (crypto, woo client, taxonomy sync, pricing,
  inference Call1/Call2, review, publish, settings, slabs, photos) + real FE
  review/publish/settings screens consuming the API (no hardcoded Woo ids).
- P4 gate exit: check_requirements + check_coverage both exit 0
  (137/155 IDs, 0 gaps); backend 130/130, frontend 150/150, build green.

Cite: docs/REQUIREMENTS.md (155 rows), docs/MILESTONES.md M1-M3.
This commit is contained in:
2026-09-25 06:57:29 -07:00
parent 4ca2ef57a7
commit 49c22c9385
106 changed files with 5502 additions and 8863 deletions
+23 -25
View File
@@ -1,9 +1,11 @@
"""Taxonomy sync: pull Woo categories/tags/attributes/terms into the local cache.
Rebuild happens in one transaction; on any failure the prior cache is kept.
Only fig-* / feat-* tag families are retained (METADATA-15). taxonomy_anchor is a
stable hash of the stored subset, recorded only when it changes.
Rebuild happens in one transaction; on any failure the prior cache is kept
(commit only on success). Only fig-* / feat-* tag families are retained
(METADATA-15). taxonomy_anchor is a stable hash of the stored subset, recorded
only when it changes.
"""
from __future__ import annotations
import hashlib
import json
@@ -23,14 +25,12 @@ def _scoped_tags(tags: list[dict]) -> list[dict]:
def _normalize_category(c: dict) -> tuple:
"""Map a Woo REST category row into woo_categories insert values.
WooCommerce REST v3 returns `parent` (0 = top-level). Older fixtures may
already use `parent_id`. Never invent ids — only store what the payload carries.
WooCommerce REST v3 returns `parent` (0 = top-level); older fixtures may
already use `parent_id`. Never invent ids — only store what the payload
carries.
"""
raw_parent = c.get("parent_id", c.get("parent"))
if raw_parent in (0, "0", None, ""):
parent_id = None
else:
parent_id = int(raw_parent)
parent_id = None if raw_parent in (0, "0", None, "") else int(raw_parent)
woo_id = c.get("woo_id", c.get("id"))
if woo_id is not None:
woo_id = int(woo_id)
@@ -49,21 +49,24 @@ def _subset_hash(categories: list[dict], attributes: list[dict],
def sync(client: WooClient, conn) -> dict:
"""Rebuild the taxonomy cache from the store. Raises WooError on failure (caller
maps to 502; prior cache is retained because we only commit on success)."""
"""Rebuild the taxonomy cache from the store. Raises WooError on failure
(caller maps to 502; the prior cache survives because we commit on success)."""
categories = client.get_categories()
tags = client.get_tags()
attributes = client.get_attributes()
terms_by_attr = {a["id"]: client.get_attribute_terms(a["id"]) for a in attributes}
scoped_tags = _scoped_tags(tags)
# One explicit transaction: the rebuild is atomic, and on any failure the
# prior cache survives (autocommit mode — see db factory).
conn.execute("BEGIN")
# Attributes/tags/terms are not referenced by slabs FKs — safe to rebuild.
conn.execute("DELETE FROM woo_attribute_terms")
conn.execute("DELETE FROM woo_attributes")
conn.execute("DELETE FROM woo_tags")
# Categories: upsert then drop stale rows. A full DELETE fails when slabs or
# pricing_rules still reference species_id (live UAT IntegrityError on test-woo).
# Categories: upsert, then drop stale rows. A blanket DELETE fails when
# slabs or pricing_rules still reference species_id (live UAT IntegrityError).
cat_rows = [_normalize_category(c) for c in categories]
cat_ids = [r[0] for r in cat_rows]
for row in cat_rows:
@@ -73,20 +76,15 @@ def sync(client: WooClient, conn) -> dict:
"name=excluded.name, slug=excluded.slug, parent_id=excluded.parent_id, woo_id=excluded.woo_id",
row,
)
keep = (
f"AND id NOT IN (SELECT species_id FROM slabs WHERE species_id IS NOT NULL) "
f"AND id NOT IN (SELECT species_id FROM pricing_rules)"
)
if cat_ids:
placeholders = ",".join("?" * len(cat_ids))
conn.execute(
f"DELETE FROM woo_categories WHERE id NOT IN ({placeholders}) "
"AND id NOT IN (SELECT species_id FROM slabs WHERE species_id IS NOT NULL) "
"AND id NOT IN (SELECT species_id FROM pricing_rules)",
cat_ids,
)
conn.execute(f"DELETE FROM woo_categories WHERE id NOT IN ({placeholders}) {keep}", cat_ids)
else:
conn.execute(
"DELETE FROM woo_categories "
"WHERE id NOT IN (SELECT species_id FROM slabs WHERE species_id IS NOT NULL) "
"AND id NOT IN (SELECT species_id FROM pricing_rules)"
)
conn.execute(f"DELETE FROM woo_categories {keep}")
conn.executemany(
"INSERT INTO woo_attributes(id, name, slug, woo_id) VALUES (?,?,?,?)",
@@ -106,7 +104,7 @@ def sync(client: WooClient, conn) -> dict:
now = _now_iso()
conn.execute("INSERT OR REPLACE INTO settings(key, value, value_enc) VALUES ('taxonomy_last_sync_at', ?, NULL)", (now,))
conn.execute("INSERT OR REPLACE INTO settings(key, value, value_enc) VALUES ('taxonomy_anchor', ?, NULL)", (anchor,))
conn.commit()
conn.execute("COMMIT")
return {"taxonomy_anchor": anchor, "taxonomy_last_sync_at": now}