fix(m3): Option A must-fix #79 #82 #83 #84 #89 #98

- #79 publish SKU/create via WooClient (User-Agent + typed 403 unrecoverable)
- #82 stale_field current_options from live taxonomy + owner labels
- #83 create allowlist/extra-key safe; taxonomy+enum 4xx on create
- #84 health woo_reachable real probe (non-failing); version 0.5.0-m3
- #89 review persists assembled title/short after taxonomy lock; UI applies
- #98 live-edge from synced term; attrs resolved by slug then name
This commit is contained in:
Ty
2026-09-21 12:49:53 -07:00
parent bdd03afce4
commit f6eb9e352d
8 changed files with 408 additions and 94 deletions
+89 -16
View File
@@ -42,6 +42,20 @@ TAXONOMY_TABLES = {
"fig_tag_ids": "woo_tags",
"feat_tag_ids": "woo_tags",
}
# Owner-facing labels for stale_field messages (QUALITY-120 — never raw column names).
FIELD_LABELS = {
"species_id": "Species",
"wood_category_ids": "Wood category",
"edge_type_term_id": "Edge type",
"figure_term_ids": "Figure",
"grade_term_id": "Grade",
"thickness_term_id": "Thickness",
"moisture_term_id": "Moisture",
"fig_tag_ids": "Figure tags",
"feat_tag_ids": "Feature tags",
}
PRICE_SOURCES = {"recommendation", "override", "manual"}
INFERENCE_STATUSES = {"idle", "inferring", "done", "failed", "skipped"}
def _now_iso() -> str:
@@ -95,6 +109,58 @@ def _check_taxonomy(conn, table: str, val: Any) -> bool:
return conn.execute(f"SELECT 1 FROM {table} WHERE id=?", (val,)).fetchone() is not None
def _current_options(conn, table: str) -> list[dict[str, Any]]:
"""Live taxonomy options for a stale_field response (PUBLISH-20 / #82)."""
rows = conn.execute(f"SELECT id, name FROM {table} ORDER BY name, id").fetchall()
return [{"id": r["id"], "name": r["name"]} for r in rows]
def _normalize_tax_val(val: Any) -> Any:
if isinstance(val, str):
try:
return json.loads(val)
except (ValueError, TypeError):
return []
return val
def _validate_taxonomy_fields(conn, payload: Dict[str, Any]) -> JSONResponse | None:
"""Return a 422 stale_field response, or None if all present taxonomy ids are live."""
for field, table in TAXONOMY_TABLES.items():
if field not in payload or payload[field] is None:
continue
val = _normalize_tax_val(payload[field])
if not _check_taxonomy(conn, table, val):
label = FIELD_LABELS.get(field, field)
return _err(
"stale_field",
f"{label} is no longer in the store taxonomy",
{"field": field, "current_options": _current_options(conn, table)},
status=422,
)
return None
def _validate_enums(payload: Dict[str, Any]) -> JSONResponse | None:
if "price_source" in payload and payload["price_source"] is not None:
if payload["price_source"] not in PRICE_SOURCES:
return _err(
"validation",
"price_source is not a known value",
{"field": "price_source", "allowed": sorted(PRICE_SOURCES)},
status=400,
)
if "inference_status" in payload and payload["inference_status"] is not None:
if payload["inference_status"] not in INFERENCE_STATUSES:
return _err(
"validation",
"inference_status is not a known value",
{"field": "inference_status", "allowed": sorted(INFERENCE_STATUSES)},
status=400,
)
return None
# Capture-only fields required to create a slab. The Review screen fills the
# taxonomy/content fields after create (CREATE.md §1: "every other field stays
# editable until create"), so create must accept a capture-math payload.
@@ -110,14 +176,22 @@ async def create_slab(payload: Dict[str, Any]) -> Dict[str, Any]:
missing = [f for f in REQUIRED_CREATE_FIELDS if f not in payload]
if missing:
return _err("validation", "missing fields", {"fields": missing}, status=400)
# Drop unknown keys (QUALITY-120 / #83) — never let client keys expand the INSERT.
payload = {k: v for k, v in payload.items() if k in ALLOWED_FIELDS}
# Business rule (middle tier, not the DB): an empty/blank SKU at create means
# "not set yet" (the owner fills it in Review). Store NULL — the DB CHECK only
# governs present SKUs, and NULL is the honest "absent" representation.
sku = payload["sku"]
sku = payload.get("sku")
if isinstance(sku, str) and sku.strip() == "":
payload = {**payload, "sku": None}
enum_err = _validate_enums(payload)
if enum_err is not None:
return enum_err
conn = get_connection()
if payload["sku"] is not None and _sku_taken(conn, payload["sku"]):
tax_err = _validate_taxonomy_fields(conn, payload)
if tax_err is not None:
return tax_err
if payload.get("sku") is not None and _sku_taken(conn, payload["sku"]):
return _err("duplicate_sku", "SKU already exists", {"sku": payload["sku"]}, status=409)
now = _now_iso()
data = _encode(payload)
@@ -129,8 +203,12 @@ async def create_slab(payload: Dict[str, Any]) -> Dict[str, Any]:
vals = [data[f] for f in allowed_cols] + server_vals
ph = ",".join(["?"] * len(cols))
assert len(vals) == len(cols), f"create_slab binding mismatch: {len(vals)} vals vs {len(cols)} cols"
conn.execute(f"INSERT INTO slabs ({', '.join(cols)}) VALUES ({ph})", vals)
conn.commit()
try:
conn.execute(f"INSERT INTO slabs ({', '.join(cols)}) VALUES ({ph})", vals)
conn.commit()
except Exception:
# QUALITY-120: never surface a driver stack to the owner.
return _err("validation", "Could not create the slab. Check the fields and try again.", status=400)
row = conn.execute("SELECT * FROM slabs WHERE id=?", (payload["id"],)).fetchone()
return _row_to_slab(row)
@@ -155,6 +233,9 @@ async def get_slab(id: str) -> Dict[str, Any]:
async def update_slab(id: str, payload: Dict[str, Any]) -> Dict[str, Any]:
if "client_rev" not in payload:
return _err("validation", "client_rev required", status=400)
enum_err = _validate_enums(payload)
if enum_err is not None:
return enum_err
conn = get_connection()
current = conn.execute("SELECT * FROM slabs WHERE id=? AND deleted_at IS NULL", (id,)).fetchone()
if current is None:
@@ -164,19 +245,11 @@ async def update_slab(id: str, payload: Dict[str, Any]) -> Dict[str, Any]:
return _err("revision_conflict", "client_rev mismatch",
{"current_server_rev": current["server_rev"]}, status=409)
if "sku" in payload and payload["sku"] != current["sku"]:
if _sku_taken(conn, payload["sku"], exclude_id=id):
if payload["sku"] is not None and _sku_taken(conn, payload["sku"], exclude_id=id):
return _err("duplicate_sku", "SKU already exists", {"sku": payload["sku"]}, status=409)
for field, table in TAXONOMY_TABLES.items():
if field in payload and payload[field] is not None:
val = payload[field]
if isinstance(val, str):
try:
val = json.loads(val)
except (ValueError, TypeError):
val = []
if not _check_taxonomy(conn, table, val):
return _err("stale_field", f"{field} not in current taxonomy",
{"current_options": val}, status=422)
tax_err = _validate_taxonomy_fields(conn, payload)
if tax_err is not None:
return tax_err
data = _encode(payload)
cols = [f for f in ALLOWED_UPDATE_FIELDS if f in data]
if not cols: