docs: pair locks for #14 #2 #17 #18 (PPM product OK)

Two taxonomy stamps, validation/module + 412 hash mismatch, SKU charset
vs UTF-8 titles, 409 open_existing required, draft = phone form + failed row.
This commit is contained in:
2026-09-02 00:34:44 +00:00
parent d413e2f935
commit fc5df28b8c
8 changed files with 55 additions and 47 deletions
+2 -2
View File
@@ -159,8 +159,8 @@ Payload and publish sequence depth: `docs/CONTENT-WOO.md` (cites this section fo
- Call 1 auto only if `inference_enabled`; Call 2 never auto-fires (user taps Generate text).
- Call 1 body: all inventory photos @1024 (not top-down-only). Taxonomy lists sent to Call 1 are **exactly the Woo-synced species / attribute / tag sets**. No hardcoded or invented species.
- **Confidence 0.7 (default):** ≥ 0.7 → pre-populate species, wood categories, edge, figure, grade, and feat-* from Call 1 (**mutable**). < 0.7 → those fields stay **empty**; user is gated before ready. Mandatory floor: species + ≥1 figure (product also requires ≥1 wood category). Presentation: `docs/UX.md`. Inference never auto-publishes.
- **Taxonomy cache:** FastAPI manufactures one last-modified anchor; bump only on stored-subset change; client re-pulls if advanced. `test-woo` always resyncs; publish if >1h. No Woo taxonomy timestamp.
- Call 2 must run on a provider that supports **portable Call 1→Call 2 context** (full history resend default; stateful `previous_response_id` only if probe says so). `test-inference` fails closed if neither path works.
- **Taxonomy cache:** `taxonomy_last_sync_at` vs `taxonomy_anchor` (bump only on stored-subset hash change, including deletions). Client re-pulls iff the anchor advanced. `test-woo` always syncs; publish if last_sync >1h. No per-nav sync. No Woo taxonomy timestamp.
- Call 2 portable context: default full resend; never require `previous_response_id`. `test-inference` two-turn probe `stateful|stateless|fail`. Fail blocks **Call 2 enablement only**.
- LoRA deferred. No numeric/dimension invention by LLM; templates inject deterministic numbers.
Prompt file mechanics: `docs/PROMPTS.md`.
+12 -8
View File
@@ -175,8 +175,9 @@ ready → publishing → published.
Woo REST exposes **no usable last-modified** on categories, attributes, or tags
(only products carry `date_modified`). The client never talks to Woo. FastAPI is
the sole freshness source and **manufactures** one anchor timestamp
(`taxonomy_updated_at` / `cache_anchor`).
the sole freshness source. It persists two UTC stamps: `taxonomy_last_sync_at`
(every successful Woo pull) and `taxonomy_anchor` (bumps only when the stored-subset
hash changes, including deletions).
- Anchor bumps **only** when a row-level change is detected in the **stored
subset** of cache fields (whatever columns the cache tables actually keep:
@@ -207,18 +208,21 @@ only after the next sync that lands it in the cache.
## 8. Shared validation module
One FastAPI module is source of truth for field rules (required, length cap, charset, Woo option membership). Client caches `{ rules, hash }`.
One FastAPI module is source of truth (`GET /api/v1/validation/module` → `{ hash, rules[] }`).
Two consumers: review UI and Call 2 prompt builder. Client caches the module.
- Field-exit: local check against the cache.
- Length: hard input cap (cannot type past Woo max).
- Hash is compared **on submit only**. Mismatch → refresh module → re-validate → retry. Draft stays.
- SKU charset: `[A-Z0-9-]{3,24}` (printable ASCII subset). Title / short_title / description: UTF-8 within Woo `maxLength`. Not “ASCII-only” and not “extended ASCII”.
- Field-exit: local check against the cache. Empty optional is valid.
- Length: `maxLength` is a hard input cap; server still enforces.
- Submit carries `validation_hash`. Match → validate. Mismatch → HTTP **412** + fresh module; UI “Updating rules”; re-validate; retry. Never show 412. 409 is duplicate SKU only. Woo-shaped 422 is store rejection (#18).
## 9. Call 1 → Call 2 context
Providers differ. Default portable path: **resend the full Call 1 turn** (prompt + images metadata + assistant JSON) with Call 2. Opt into `previous_response_id` only after probe.
`POST /api/v1/settings/test-inference` fails closed (`ok: false`) if **neither** full-resend nor stateful id works. Do not ship a Call 2 that silently drops Call 1 context.
`POST /api/v1/settings/test-inference` two-turn probe records `stateful | stateless | fail`.
**Fail blocks Call 2 enablement only** (Gate C inference-OFF still works). Never require `previous_response_id`. Do not ship a Call 2 that silently drops Call 1 context.
Per-slab thread. Delete stored thread on successful publish or user abandon.
Per-slab thread on the **server** (client does not hold provider IDs). Best-effort delete on publish / abandon / fail; log and continue.
Presentation of the review screen: `docs/UX.md`.
+16 -15
View File
@@ -88,17 +88,16 @@ and the final stored content.
Rebuilds cache tables in one transaction. Failure → `502 woo_error`; previous
cache retained (never delete-then-fail).
**Manufactured anchor.** Woo REST has **no** usable last-modified on categories,
attributes, or tags. FastAPI manufactures one `taxonomy_updated_at` /
`cache_anchor` for the whole cache. Bump **only** when a row-level change hits the
**stored subset** of cache columns (schema-driven: new columns auto-enter the
diff). Catch adds, edits, and deletions. A no-op sync does **not** bump. Client
re-pulls the full taxonomy iff the anchor advanced past its last value. One
timestamp governs all metadata kinds. Do not invent a Woo-side taxonomy timestamp.
**Manufactured stamps.** Woo REST has **no** usable last-modified on categories,
attributes, or tags. FastAPI persists `taxonomy_last_sync_at` (every successful
pull) and `taxonomy_anchor` (bump only when stored-subset hash changes, including
deletions). Client re-pulls the full taxonomy iff the anchor advanced. Do not
invent a Woo-side taxonomy timestamp. Do not sync per navigation.
**Triggers:** (1) successful `test-woo` **always** resyncs; (2) publish path if last
successful sync older than **1 hour** (bump only on real change); (3) optional app
launch / Settings refresh. Not every navigation.
**Triggers:** (1) successful `test-woo` **always** resyncs; (2) publish if
`now - taxonomy_last_sync_at > 1 hour` then reconcile (bump only on real change);
(3) optional app launch / Settings refresh. Stale selected IDs after a bump are
#18 422, not a silent remap.
**Species exclusivity.** UI pickers, pricing seeds, and Call 1 species lists are
**exactly** the synced species-category leaves. No hardcoded, invented, or fallback
@@ -196,7 +195,7 @@ Success path unchanged: set `woo_product_id`, local `published`, purge images.
```
1. validate via shared validation module (hash check on submit; see ARCHITECTURE §8)
2. dedupe by SKU (GET /products?sku=, any status) → 409 duplicate_sku if exists
3. (re)sync taxonomy if last sync >1h; bump cache_anchor only on stored-subset change
3. (re)sync taxonomy if last_sync >1h; bump taxonomy_anchor only on stored-subset hash change
4. resolve category/tags/attributes (Call 1 / manual + name-match; synced set only)
5. upload processed PNG inventory images → media → source_urls
6. POST /products (status per AGENTS §5 + woo_create_status) → woo_product_id
@@ -213,9 +212,11 @@ Success path unchanged: set `woo_product_id`, local `published`, purge images.
## 4. Field-level Woo errors (presentation in `docs/UX.md`)
API codes stay on the wire. The phone never displays `409` or `422` as the message.
Wire shape: `{ field, code, message, actions[] }`. The phone never displays HTTP codes.
- **409 duplicate SKU** (exists in Woo under any status): inline on SKU. Actions: edit SKU, or open/copy the existing listing. Draft stays on the phone. No in-place update of the other product in POC.
- **422 stale or invalid field** (category/attribute/tag/price no longer valid): after a submission-time taxonomy refresh, return the field key plus the **current** allowed options. Inline on that field. Draft stays.
- **409 duplicate SKU**: actions `edit_sku` | `open_existing`. Include Woo id + admin URL when GET-by-sku works; if lookup fails, keep `open_existing` (SKU + “open this SKU in Woo admin”). No in-place update of the other product.
- **422 stale field**: current cache options after publish-time sync if #14 fired.
- Draft: phone keeps the form; server slab → `failed` (retryable). No IndexedDB.
- Module-stale hash is **412**, not 409.
The shared validation module (`GET /api/v1/validation-module`) is the client cache for required/length/charset. Hash check on submit only.
The shared validation module is `GET /api/v1/validation/module`. Hash check on submit only.
+3 -2
View File
@@ -21,7 +21,7 @@ Draft-only. Deleted on successful publish. WooCommerce is the source of truth fo
| Column | Type | Notes |
|---|---|---|
| `id` | TEXT PK | client-generated UUID |
| `sku` | TEXT UNIQUE NOT NULL | 3-24 [A-Z0-9-] |
| `sku` | TEXT UNIQUE NOT NULL | `[A-Z0-9-]{3,24}` printable ASCII subset |
| `status` | TEXT NOT NULL | draft \| calibrated \| ready \| publishing \| published \| failed |
| `length_in` | REAL NOT NULL | 1/8" step |
| `thickness_in` | REAL NOT NULL | 1/8" step |
@@ -185,7 +185,8 @@ Keys (server settings; secrets encrypted at rest):
- `inference_enabled`, `content_llm_enabled`
- `brand_voice`, `geo_context`
- `woo_create_status` ('draft' | 'publish'), default `draft` — Woo product create status key only; safety policy lives in AGENTS.md §5
- `taxonomy_updated_at` — manufactured ISO timestamp; bump only when a stored-subset taxonomy row changes. No-op sync does not bump. Not Woo's own `date_modified`.
- `taxonomy_last_sync_at` — UTC ISO; every successful Woo taxonomy pull
- `taxonomy_anchor` — UTC ISO; bump only when stored-subset hash changes (incl. deletions). No-op sync does not bump. Not Woo `date_modified`.
- `aspect_ratio` ('3:4'), `output_px` (1600), `output_px_min` (1600), `fill_target` (0.80)
+1 -1
View File
@@ -117,7 +117,7 @@ Deliverables:
- Routers per OPENAPI: slabs (CRUD + upload + poll), photos (serve), pricing-rules,
settings (AES-GCM + test stubs), taxonomy (cache read/write + manufactured
`taxonomy_updated_at` / `cache_anchor`).
`taxonomy_anchor` / `taxonomy_last_sync_at`).
- Optimistic concurrency (`client_rev` / `server_rev`, 409 paths).
- Status machine only: `draft → calibrated → ready → publishing → published | failed`.
Job queue only for publish and inference (later). U2Net-on-demand is deferred from POC. Never for measure/crop.
+16 -14
View File
@@ -156,8 +156,9 @@ Sheet/slider prefs are client localStorage only in POC (not in SettingsView).
- `PUT /api/v1/slabs/{id}` — Update draft fields. Body: partial `SlabCreate` (any subset of editable fields + `client_rev`).
- Server **stores** client-sent derived fields (`sqft`, `bdft`, widths). It may validate shape and ranges. It must **not** recompute mask, sqft, bdft, or widths as source of truth. Client re-runs TECH-SPEC math and PUTs the new numbers.
- `409 revision_conflict`.
- `409 duplicate_sku` → `{ "error": "duplicate_sku", "sku": "...", "woo_admin_url": "..."? }`. UI maps this to the SKU field (`docs/UX.md`). Never show the code.
- `422` stale/invalid field → `{ "error": "stale_field", "field": "species_id", "options": [...] }` after a submission-time taxonomy refresh. UI maps to that field. Draft stays.
- `409 duplicate_sku` → `{ "field":"sku", "code":"duplicate_sku", "message":"...", "actions":["edit_sku","open_existing"], "woo_product_id": "...?", "woo_admin_url": "...?" }`. `open_existing` is required even if lookup failed (then omit id/url). UI: `docs/UX.md`. Never show the code.
- `422` stale/invalid field → `{ "field":"species_id", "code":"stale_field", "message":"...", "actions":[], "options": [...] }` after a submission-time taxonomy refresh. Draft: phone form + server slab `failed` (retryable).
- `412` validation module stale → `{ "code":"validation_module_stale", "module": { "hash":"...", "rules":[] } }`. Not a 409.
- `DELETE /api/v1/slabs/{id}` → `204`. Soft: only unpublished drafts. Published slabs cannot be deleted from the app.
### Photo operations
@@ -199,12 +200,13 @@ Sheet/slider prefs are client localStorage only in POC (not in SettingsView).
```jsonc
{ "categories": [...], "attributes": [{ name, slug, terms: [...] }],
"tags": [...],
"taxonomy_updated_at": "ISO-8601" }
"taxonomy_anchor": "ISO-8601",
"taxonomy_last_sync_at": "ISO-8601" }
```
Client re-pulls if `taxonomy_updated_at` is newer than its snapshot. Species/attribute/tag
Client re-pulls iff `taxonomy_anchor` advanced. Species/attribute/tag
lists here are the **only** legal options for UI pickers and Call 1.
- `POST /api/v1/admin/taxonomy/sync` → `200` `{ "categories": n, "attributes": n, "tags": n, "taxonomy_updated_at": "..." }`.
- Pulls from Woo, rebuilds the cache. Bump `taxonomy_updated_at` **only** if a stored-subset row changed. `502 woo_error` on failure; previous cache retained.
- `POST /api/v1/admin/taxonomy/sync` → `200` `{ "categories": n, "attributes": n, "tags": n, "taxonomy_anchor": "...", "taxonomy_last_sync_at": "..." }`.
- Pulls from Woo, rebuilds the cache. Always set `taxonomy_last_sync_at` on success. Bump `taxonomy_anchor` **only** if stored-subset hash changed. `502 woo_error` on failure; previous cache retained.
### Pricing
- `GET /api/v1/admin/pricing` → `[PriceRule]`
@@ -222,16 +224,16 @@ Sheet/slider prefs are client localStorage only in POC (not in SettingsView).
`woo_consumer_key` / `woo_consumer_secret`.
- Secrets re-encrypted server-side. `200` `SettingsView` returned (password
never echoed).
- `POST /api/v1/settings/test-woo` → `200` `{ "ok": bool, "detail": "...", "taxonomy_updated_at": "..." }`
- `POST /api/v1/settings/test-woo` → `200` `{ "ok": bool, "detail": "...", "taxonomy_anchor": "...", "taxonomy_last_sync_at": "..." }`
- Uses stored username + application password over HTTPS Basic Auth against
`WOO_BASE_URL` REST v3. Fails closed if credentials missing or URL is not https.
- On success, **always** run a taxonomy resync (bump anchor only if rows changed).
- `POST /api/v1/settings/test-inference` → `200` `{ "ok": bool, "vision_capable": bool, "portable_resend": bool, "stateful_previous_response_id": bool, "detail": "..." }`
- Probe whether Call 1→Call 2 can use full-history resend and/or `previous_response_id`.
- **Fail closed** (`ok: false`) if neither portable resend nor stateful id works. Call 2 must not run.
- `GET /api/v1/validation-module` → `200` `{ "hash": "...", "rules": { ... } }`
- Shared FastAPI module. Client caches it. Compare `hash` **on submit only**.
- Length rules are hard input caps. Field-exit uses the cache locally.
- On success, **always** run a taxonomy resync (set last_sync; bump anchor only if hash changed).
- `POST /api/v1/settings/test-inference` → `200` `{ "ok": bool, "mode": "stateful"|"stateless"|"fail", "vision_capable": bool, "detail": "..." }`
- Two-turn probe. Default Call 1→Call 2 path is full resend; never require `previous_response_id`.
- `mode=fail` **blocks Call 2 enablement only**. Gate C inference-OFF still works.
- `GET /api/v1/validation/module` → `200` `{ "hash": "...", "rules": [ ... ] }`
- Shared FastAPI module (UI + Call 2). Client caches it. Submit sends `validation_hash`.
- Match → validate. Mismatch → **412** + fresh module. SKU `[A-Z0-9-]{3,24}`; titles UTF-8 + Woo maxLength.
---
+1 -1
View File
@@ -230,7 +230,7 @@ Price 12.345 → **12.35**.
| Portable Call 1→Call 2 context (full resend default) | ARCHITECTURE §9, PROMPTS |
| Woo payload, publish, 409/422 recovery | CONTENT-WOO, OPENAPI |
| SQLite tables, status machine | DATA-MODEL |
| HTTP contract, taxonomy `cache_anchor` | OPENAPI |
| HTTP contract, taxonomy `taxonomy_anchor` | OPENAPI |
| Client vs server topology, validation module | ARCHITECTURE |
| Auto-dimension overlay, figure-weighted price | IMPL-PLAN (v1.5) |
| Offline capture | non-goal (online-only POC; see AGENTS) |
+4 -4
View File
@@ -124,8 +124,8 @@ Shared module on FastAPI (`docs/OPENAPI.md`). Client caches it; hash checked **o
- Blur: local check with cached rules. Required fields block; optional empty is valid; optional with content must pass.
- Length: hard cap at the Woo limit (cannot type past it).
- Character set: invalid characters rejected on exit.
- Submit: full pass + hash check. Mismatch → “Updating rules” → re-validate → retry. No data loss.
- Character set: SKU `[A-Z0-9-]{3,24}`; title/description UTF-8 within Woo max. Invalid characters rejected on exit. Not “extended ASCII”.
- Submit: full pass + hash check. Mismatch → “Updating rules” (HTTP 412, never shown) → re-validate → retry. No data loss.
Presentation: errors sit under the field. No top-of-screen dump for field problems.
@@ -133,12 +133,12 @@ Presentation: errors sit under the field. No top-of-screen dump for field proble
## 6. Publish errors (after commit)
Draft stays on the phone.
The phone keeps the form. The server slab goes `failed` (retryable). No IndexedDB.
**409 duplicate SKU** — inline on SKU:
- “This SKU already exists in the store (any status).”
- Actions: **Edit SKU** (focus the field) · **Open existing listing** (admin URL if the server returned it; otherwise copy SKU and a one-line “look this SKU up in Woo admin”).
- Actions: **Edit SKU** (focus the field) · **Open existing listing** (required). Use the admin URL/id when the server returned them; otherwise keep the button and “look this SKU up in Woo admin”.
- Never show `409` as the message.
**422 stale value** — inline on the field Woo rejected (category, attribute, price, …):