diff --git a/AGENTS.md b/AGENTS.md index c828bc5..bc7ddd7 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -32,7 +32,7 @@ Archived PRD lines that conflict with the files above are non-authoritative (see ### Open doc defects -POC issue wave (docs on this branch): Application Passwords (#5), Call 1 confidence gate (#9), `WOO_BASE_URL` compose-only (#11), Call 1 prefill (#8), synced-species-only (#12), inline mask knobs (#15), taxonomy cache anchor (#14), Call1→Call2 portable context (#2), shared validation (#17), 409/422 recovery (#18), UX spec (#10 → `docs/UX.md`). +Landed on `docs/refactor-v2`: #5 Application Passwords, #8 Call 1 prefill, #9 confidence gate, #11 `WOO_BASE_URL`, #10 UX spec (`docs/UX.md`), #12 synced-species-only, #15 inline mask knobs (U2Net deferred), #14 taxonomy manufactured last-modified, #2 portable Call1→Call2, #17 shared validation module, #18 409/422 field recovery. Grok Build owns remaining OPENAPI #3 wording if still open. --- diff --git a/README.md b/README.md index 8014e2a..c032501 100644 --- a/README.md +++ b/README.md @@ -28,6 +28,7 @@ early Phase 1 first draft listing). | `docs/PROMPTS.md` | Vision + content prompts (server files) | | `docs/DEPLOYMENT.md` | Env-agnostic compose, Caddy, backup/restore | | `deploy/INVENTORY.md` | Host inventory worksheet (Gate B) | +| `docs/UX.md` | POC interaction spec (review, capture knobs, errors, first-run) | | `docs/IMPL-PLAN.md` | Phased build plan with acceptance criteria + exit gates | | `docs/archive/PRD-2026-08.txt` | Frozen historical PRD only (not a source for new work) | @@ -35,7 +36,7 @@ early Phase 1 first draft listing). Hard locks live in `AGENTS.md`. Short map: -- **Hybrid:** phone owns mask/sliders/sqft/bdft/3:4 PNG; server stores, Woo, inference proxy, on-demand U2Net. +- **Hybrid:** phone owns mask knobs / sqft / bdft / 3:4 PNG; server stores, Woo, inference proxy. U2Net “Try harder” deferred from POC. - **bdft** = sqft × thickness_in (no `/12`). TV-1 through TV-10 on the client. - **Secrets** never in browser; AES-GCM on server; `SLAB_AES_KEY` env only. Woo auth = WP application password (dedicated low-priv user), not consumer keys. - **Woo UAT:** draft default; `SLAB-UAT-*` force draft (AGENTS §5). diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 240c60d..b5d852e 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -10,22 +10,24 @@ Companion: `docs/TECH-SPEC-PIPELINE.md`. Historical PRD: `docs/archive/PRD-2026- ## 1. Topology (hybrid) -Phone does the happy-path pipeline (mask, length axis, sqft/bdft, 3:4 PNG). -FastAPI stores drafts, runs U2Net only on demand, proxies inference, talks to Woo. -reverse proxy terminates HTTPS. +Phone does the happy-path pipeline (mask knobs, length axis, sqft/bdft, 3:4 PNG). +FastAPI stores drafts, proxies inference, talks to Woo. reverse proxy terminates HTTPS. +Server U2Net (“Try harder”) is **deferred from POC**, not deleted. ``` phone (SvelteKit) - capture, mask, sliders, length axis, sqft/bdft, 3:4 PNG - if mask still bad → POST one photo → FastAPI U2Net → mask back → user continues + capture, four knobs inline, live mask overlay on the source photo, + length axis, sqft/bdft, 3:4 PNG + (POC: retake if mask still bad — no U2Net control) server reverse proxy → frontend (static) - → fastapi : store, settings, taxonomy, Woo, inference proxy, U2Net, prompt files + → fastapi : store, settings, taxonomy cache, Woo, inference proxy, prompt files + → U2Net endpoint reserved post-POC ``` - Happy path never leaves the phone. -- U2Net is explicit “Try harder” (or coverage failure), not every slab. +- Mask knobs live on capture; changing a knob re-runs client BG removal immediately. - Woo + LLM keys stay on the server. Browser does not call Woo or the vision endpoint directly. ## 2. Swimlane @@ -45,7 +47,7 @@ flowchart TB end subgraph API["server — FastAPI"] - A1[U2Net: one photo in, mask out] + A1[U2Net reserved post-POC] A2[Store draft: originals + PNGs + numbers] A3[Call 1 proxy — vision] A4[Call 2 proxy — text] @@ -63,8 +65,7 @@ flowchart TB end P1 --> P2 --> P3 - P3 -->|no — Try harder| A1 - A1 --> P2 + P3 -->|no — retune knobs or retake| P2 P3 -->|no — Retake| P1 P3 -->|yes| P4 --> P5 --> P6 P6 --> A2 @@ -77,34 +78,34 @@ flowchart TB ## 3. Clarifications -1. **Happy path** stays on the phone through PNG + numbers. FastAPI is idle until the user has a confirmed mask (or taps Try harder). -2. **Try harder** is U2Net on **one photo** (the one on screen). Mask comes back; sliders still apply. Not a full re-pipeline on the server. +1. **Happy path** stays on the phone through PNG + numbers. FastAPI is idle until the user has a confirmed mask. +2. **Mask knobs** live on capture; each change re-runs client BG removal. Server U2Net “Try harder” is deferred from POC (keep the one-photo contract; do not ship the control). 3. **Draft upload** sends **originals + processed PNGs + length/thickness/SKU/sqft/bdft/widths**. Originals are needed if Call 1 or a later retry must not depend on the tab still being open. After successful Woo publish, server deletes both. 4. **Call 1** is server-side so the vision key never sits in the browser. Auto-run on slab create **only if** `inference_enabled` is true. If disabled, Call 1 is skipped; user can manually fill taxonomy and still trigger Call 2. Body: - all originals downscaled to 1024 + Woo taxonomy snapshot + SKU/length/thickness. - Timeout 45s → error + retry on the phone. When inference is on, Call 1 results - **pre-populate** the review screen: species, wood categories, edge/figure/grade - attributes, `fig-*`/`feat-*` tags (from character/inclusions/voids/checks), with - confidence. User reviews and overrides. Manual entry stays authoritative; - Call 1 is assist-only. -5. **Call 2** does not auto-fire. User taps Generate text. Server sends curated - Call 1 results (or manually-entered taxonomy if Call 1 failed) + deterministic - numbers + brand/GEO for prose generation; assembles title/description/short - description from deterministic templates with the LLM prose injected. + all originals downscaled to 1024 + **Woo-synced taxonomy only** + SKU/length/thickness. + Timeout 45s → error + retry on the phone. ≥0.7 confidence **pre-populates** + species, wood categories, edge/figure/grade, feat-* (mutable). Below 0.7 those + fields stay **empty**. Manual entry stays authoritative. Presentation: `docs/UX.md`. +5. **Call 2** does not auto-fire. User taps Generate text. Server must send + **portable Call 1 context** (default: full conversation resend). Stateful + `previous_response_id` only after `test-inference` proves the provider supports + it. Call 2 inputs distinguish confirmed user values from inferred ones. Per-slab + thread; delete on submit or abandon. 6. **Woo** is only FastAPI. Browser never sees the WordPress application - password. Duplicate SKU → stop, show error, no edit-same-SKU in MVP. + password. Duplicate SKU → 409 mapped on the SKU field (edit SKU or open existing). + Stale value → 422 on that field. Draft stays on the phone. 7. **Settings** (species $/bdft, prompts, inference endpoint, Woo credentials, - publish status) live on the server; the phone is just the form. + publish status) live on the server. Store URL is env, displayed read-only. ## 4. What runs where | Job | Where | |---|---| | Camera, 1-5 photos | Client | -| Sheet detect, chroma-key / black threshold, sliders, flood-fill | Client | -| U2Net | Server, on demand | +| Sheet detect, chroma-key / black threshold, four knobs, live re-run, flood-fill | Client | +| U2Net “Try harder” | Server, **deferred from POC** | | Length axis overlay, 6" widths, sqft, bdft | Client | | 3:4 PNG, 80% fill (configurable aspect) | Client | | Draft + originals + processed PNGs | Server (after user continues) | @@ -157,11 +158,11 @@ any → failed (with error) 2. **calibrated** — user confirmed the BG edge, confirmed length axis, entered length/thickness/SKU. Client computed sqft/bdft/widths. Draft uploaded to server. 3. **ready** — all mandatory fields populated (species, wood category, edge type, figure, grade, thickness, price, title/desc/short desc, at least one photo). - Values can come from Call 1 (≥ threshold pre-fill the user kept), manual - entry, or a mix. **Review gate:** if Call 1 returned below-threshold for a - field, that field stays empty until the user fills it. At minimum **species** - and **≥1 figure attribute** must be set before ready. Inference is optional; - with inference OFF the user fills everything by hand. + Values can come from Call 1 (≥ 0.7 pre-fill the user kept), manual entry, or a mix. + **Review gate:** below-threshold Call 1 leaves that field empty. Mandatory before + ready: **exactly one species, ≥1 wood category, ≥1 figure**. Inline nudges, not + submit-only (`docs/UX.md`). Inference is optional; with inference OFF the user + fills everything by hand. 4. **publishing** — Woo create in flight. Poll for result. 5. **published** — Woo product created, woo_product_id stored. Images purged. 6. **failed** — pipeline, inference, or publish error. Error detail stored. Retry available. @@ -169,3 +170,37 @@ any → failed (with error) Call 1 and Call 2 are assist-only. They do not gate publish. Low-confidence Call 1 results do not auto-fill. If the user skips inference, the path is: calibrated → ready → publishing → published. + +## 7. Taxonomy cache (manufactured last-modified) + +Woo category/tag/attribute `date_modified` is not a reliable client cache key (the store does not bump it for every edit we care about). FastAPI **manufactures** a `taxonomy_updated_at` timestamp: + +- Bump **only** when a stored-subset row actually changes (species/wood-category leaves, five attributes + terms, `fig-*`/`feat-*` tags). +- A no-op sync (same rows) **does not** bump. +- Client re-pulls if the returned anchor is newer than its snapshot. + +**Triggers** + +1. `POST /api/v1/settings/test-woo` always runs a taxonomy resync after a successful connection test. +2. Publish path: opportunistic resync if the last successful bump is older than **1 hour**. +3. Optional: app launch and a Settings “Refresh taxonomy” control. + +Species pickers and Call 1 option lists are **exactly** this cached set. No hardcoded mill list. + +## 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 }`. + +- 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. + +## 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. + +Per-slab thread. Delete stored thread on successful publish or user abandon. + +Presentation of the review screen: `docs/UX.md`. diff --git a/docs/CONTENT-WOO.md b/docs/CONTENT-WOO.md index ec0b455..8e08ae5 100644 --- a/docs/CONTENT-WOO.md +++ b/docs/CONTENT-WOO.md @@ -84,7 +84,14 @@ and the final stored content. 1. `GET /products/categories?per_page=100` (paginate) → `woo_taxonomy(kind=category)` 2. `GET /products/tags?per_page=100` → `kind=tag` 3. `GET /products/attributes` → `kind=attribute` -Rebuilds the cache table (delete + insert, single transaction). Returns counts. +Rebuilds the cache table (delete + insert, single transaction). Returns counts plus +`taxonomy_updated_at`. **Bump that timestamp only if a stored-subset row changed.** +A no-op sync (identical rows) leaves the anchor alone. Client re-pulls when the +anchor is newer than its snapshot. + +**When to sync:** (1) always after successful `test-woo`; (2) opportunistic on +publish if last bump is older than 1 hour; (3) optional launch / Settings refresh. + Failure → `502 woo_error`; previous cache retained (never delete-then-fail). ### 2.3 Taxonomy assignment (FR26) @@ -96,9 +103,9 @@ inclusions / voids / checks → existing `feat-*`). All pre-filled values are mutable. The user confirms or overrides; manual entry always wins. **Below threshold:** those fields stay empty. The review screen gates continue -until the user manually selects at least **species** and **one figure attribute** -(and the rest of the ready mandatory set). Do not auto-fill below-threshold -guesses. Full rules: AGENTS §6. +until the user manually selects **exactly one species, ≥1 wood category, and ≥1 +figure**. Inline nudges: `docs/UX.md`. Do not auto-fill below-threshold +guesses. Full functional rules: AGENTS §6. **Deterministic name-matching** resolves Call 1 names (or manual picks) to synced Woo IDs case-insensitive, trimmed. No match → empty; user picks in review. Woo @@ -184,3 +191,12 @@ Before create: `GET /products?sku={sku}`. payload assembly, dedupe, logging, title assembly. - Scoped inference: optional LLM prose generation for description only. The LLM never touches dimensions. + +## 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. + +- **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. + +The shared validation module (`GET /api/v1/validation-module`) is the client cache for required/length/charset. Hash check on submit only. diff --git a/docs/DATA-MODEL.md b/docs/DATA-MODEL.md index c93dedb..ffac7c9 100644 --- a/docs/DATA-MODEL.md +++ b/docs/DATA-MODEL.md @@ -50,6 +50,7 @@ Draft-only. Deleted on successful publish. WooCommerce is the source of truth fo | `server_rev` | INTEGER NOT NULL DEFAULT 1 | | | `created_at` | TEXT NOT NULL | | | `updated_at` | TEXT NOT NULL | | +| `inference_thread_id` | TEXT | optional stateful provider id; delete on publish or abandon | Indexes: `sku` (unique), `status`, `updated_at`. @@ -64,7 +65,7 @@ any → failed (with error) 1. **draft** — photos taken, no mask confirmed. 2. **calibrated** — BG edge confirmed, length axis confirmed, length/thickness/SKU entered. Client computed sqft/bdft/widths. Draft uploaded to server. -3. **ready** — all mandatory fields populated. Values from Call 1, manual, or mix. Inference is optional. Publishable. +3. **ready** — exactly one species, ≥1 wood category, ≥1 figure, plus remaining listing fields. Call 1 may prefill only when confidence ≥ 0.7. Inference optional. Publishable. 4. **publishing** — Woo create in flight. Poll for result. 5. **published** — Woo product created. This row + all photos deleted. sync_log retained. 6. **failed** — pipeline, inference, or publish error. Error detail stored. Retry available. @@ -113,6 +114,7 @@ Rule: only inventory photos. Calibration photos do not exist in this design. | `synced_at` | TEXT | | Only species + wood category. Sync pulls all Woo categories; the app filters to the two we use. +Species leaves from this table (after filter) are the **only** species the UI and Call 1 may offer. `UNIQUE(woo_id)`. @@ -183,8 +185,10 @@ 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`. - `aspect_ratio` ('3:4'), `output_px` (1600), `output_px_min` (1600), `fill_target` (0.80) + Sheet/slider prefs (`sheet_mode`, sensitivity, edge offset, feather) are **not** server settings keys in POC. Client localStorage is source of truth (AGENTS §4 / ARCHITECTURE §5). Optional server sync of those prefs is out of scope for POC. diff --git a/docs/IMPL-PLAN.md b/docs/IMPL-PLAN.md index d551a0b..e25e637 100644 --- a/docs/IMPL-PLAN.md +++ b/docs/IMPL-PLAN.md @@ -116,7 +116,7 @@ Deliverables: settings (AES-GCM + test stubs), taxonomy (cache read/write). - Optimistic concurrency (`client_rev` / `server_rev`, 409 paths). - Status machine only: `draft → calibrated → ready → publishing → published | failed`. - Job queue only for publish, inference (later), and U2Net-on-demand. Never for measure/crop. + Job queue only for publish and inference (later). U2Net-on-demand is deferred from POC. Never for measure/crop. - Integration tests (httpx TestClient): create calibrated → poll → ready (manual fields); revision conflict; duplicate SKU; settings encryption round-trip (secret never returned). @@ -162,15 +162,16 @@ Deliverables: re-crop UI as SoT. - Inference UI (only meaningful after Gate C and when `inference_enabled`): Call 1 results with confidence. **≥ 0.7** (configurable default) → pre-fill mutable - fields. **< 0.7** → leave empty; block continue until species + ≥1 figure - (and other mandatories) are set by hand. "Generate text" triggers Call 2; - never auto. + fields. **< 0.7** → leave empty; inline nudges; block continue until exactly + one species, ≥1 wood category, and ≥1 figure (`docs/UX.md`). "Generate text" + triggers Call 2; never auto. +- Capture: four knobs on the mask screen with live re-run. No U2Net control in POC. - Publish button (online-only), poll `publishing` → `published` | `failed`, surface error detail from sync_log. - Settings UI: WP username + application password + test-woo, inference endpoint + - test (vision check), pricing rules editor, brand voice/GEO, `woo_create_status`. - Sheet/slider prefs stay in client localStorage. Prompts are server files, not UI - settings. + test (vision + portable-context probe), pricing rules editor, brand voice/GEO, + `woo_create_status`. Store URL read-only from `WOO_BASE_URL`. Sheet/slider prefs + stay in client localStorage. Prompts are server files, not UI settings. - Secrets never appear in GET settings responses (masked `*_configured` only). Acceptance: @@ -228,14 +229,16 @@ Deliverables: - Call 1 (vision) proxy: **all inventory photos** downscaled to 1024 + Woo taxonomy snapshot + user metadata; OpenAI-compatible endpoint; model constrained to cached - taxonomy; confidence threshold **0.7** (configurable default; not a POC Settings - slider). Auto-run on calibrated create only if `inference_enabled`. Not top-down-only. + taxonomy **from the Woo cache only**; confidence threshold **0.7** (configurable + default; not a POC Settings slider). Auto-run on calibrated create only if + `inference_enabled`. Not top-down-only. - Apply AGENTS §6 gate: ≥ threshold → pre-fill mutable species/categories/attributes/ - tags; < threshold → empty fields + review gate requiring species + ≥1 figure - before ready. -- Call 2 (text) proxy: never auto. User taps Generate text. LLM prose only; templates - inject deterministic numbers. See `docs/PROMPTS.md`. -- `test-inference` endpoint (vision capability check) before Call 2 is allowed. + tags; < threshold → empty fields + review gate requiring species + wood category + + figure before ready (`docs/UX.md`). +- Call 2 (text) proxy: never auto. Portable Call 1 context (full resend default). + LLM prose only; templates inject deterministic numbers. See `docs/PROMPTS.md`. +- `test-inference` endpoint (vision + portable vs stateful probe). Fail closed if + neither Call 1→Call 2 path works. - Feature flags; `inference_log` audit rows; prompt files read fresh each call. - No numeric/dimension invention by the LLM. LoRA deferred. @@ -243,8 +246,8 @@ Acceptance: - Local vision model configured: ≥ 0.7 fields pre-fill and remain editable; user override sticks. -- Below 0.7: fields stay empty; UI blocks continue/ready until species and ≥1 - figure attribute are set manually (asserted in integration or UI test). +- Below 0.7: fields stay empty; UI blocks continue/ready until exactly one + species, ≥1 wood category, and ≥1 figure are set manually (asserted). - No doc/code path auto-fills below-threshold guesses or treats inference as a publish gate. - Inference OFF: app works end-to-end; tests assert zero calls to the inference endpoint. @@ -288,7 +291,7 @@ Exit gate: Ty signs off on UAT results; v1 declared done. | Risk | Impact | Mitigation | |---|---|---| -| Green-sheet color varies / U2Net CPU latency high | slow or failed “Try harder” | chroma-key / black threshold primary on client; U2Net on demand only; never drop `output_px_min` below 1600 (undersized → retake) | +| Green-sheet color varies | bad mask, wrong sqft | chroma-key / black threshold + inline knobs + live re-run; retake; U2Net deferred from POC; never drop `output_px_min` below 1600 | | No vision model available at deploy | species inference off | inference is toggleable; manual species/character fully functional — v1 not blocked | | Woo partial publish (media up, product POST fails) | orphaned images, confusing state | sync_log + retained images for retry; manual cleanup note; no auto-delete | | Local vision endpoint not up | inference off | configurable endpoint; deploy-time config; app works without it | diff --git a/docs/OPENAPI.md b/docs/OPENAPI.md index 9b1f896..a396e3e 100644 --- a/docs/OPENAPI.md +++ b/docs/OPENAPI.md @@ -148,17 +148,20 @@ Sheet/slider prefs are client localStorage only in POC (not in SettingsView). - `POST /api/v1/slabs` — Create a calibrated draft. Body: multipart with `data` (SlabCreate JSON) + `files[]` (photos) + `meta[]` (photo metadata). - Returns `201` `Slab` with `status: "calibrated"` (mask + axis confirmed; client-sent sqft/bdft/widths stored). - Server auto-runs Call 1 **only if** `inference_enabled` is true. `inference_status` becomes `inferring`, then `done`/`failed`. - - `409 duplicate_sku` if SKU already exists in Woo, any status. + - After Call 1, persist inferred values onto the slab **only** for fields with confidence ≥ 0.7. Below 0.7 leave those columns null so GET does not prefill. + - `409 duplicate_sku` if SKU already exists in Woo, any status (payload as PUT). - `GET /api/v1/slabs` → `200` `[Slab]` (most recent first). - `GET /api/v1/slabs/{id}` → `200` `Slab` / `404`. - Also used as the poll endpoint for inference and publish. Long-running ops set status to `inferring`/`publishing`; client backs off (1s, 2s, 4s, cap 10s). - `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`. + - `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. - `DELETE /api/v1/slabs/{id}` → `204`. Soft: only unpublished drafts. Published slabs cannot be deleted from the app. ### Photo operations -- `POST /api/v1/slabs/{id}/u2net/{pid}` — "Try harder." Server runs U2Net on this one photo, returns a better mask. +- `POST /api/v1/slabs/{id}/u2net/{pid}` — **post-POC.** "Try harder." Server runs U2Net on this one photo, returns a better mask. Do not implement or expose in Gate C / first listing. Keep the contract; do not delete. - Returns `202` `{ mask_url: "..." }`. Client uses the mask to re-compute area/widths locally. - `404` if photo not found. - `GET /api/v1/slabs/{id}/photos/{pid}/original` → `image/*` (original, while retained). @@ -173,20 +176,20 @@ Sheet/slider prefs are client localStorage only in POC (not in SettingsView). - Returns `202` `{ "status": "inferring" }`. Client polls `GET /api/v1/slabs/{id}` for results. - Timeout > 45s → `504 gateway_timeout` with error detail. - `POST /api/v1/slabs/{id}/infer-content` — Trigger Call 2 (text). - - Server reads the Call 2 prompt file fresh, uses available taxonomy - (from Call 1 results or manually-entered values) + deterministic numbers - + brand voice + GEO context, sends to the LLM for prose generation, then - assembles title/description/short description from deterministic templates - with the LLM prose injected. - - Requires that inference was previously tested as connected - (`test-inference` succeeded at least once). If not connected, returns `422`. + - Server reads the Call 2 prompt file fresh. Default: **resend full Call 1 + context** (prompt + assistant JSON + confirmed vs inferred labels). Use + `previous_response_id` only if test-inference reported stateful support. + Per-slab thread; delete on publish or abandon. + - Taxonomy + deterministic numbers + brand/GEO; LLM prose then templates. + - Requires `test-inference` `ok: true`. Else `422` (not connected / not portable). - Returns `202` `{ "status": "inferring" }`. Client polls for results. - Timeout > 45s → `504` with error detail. ### Publish - `POST /api/v1/slabs/{id}/publish` — Publish to WooCommerce. - - Pre-conditions: status in `ready`, all required fields set. Else `422 unpublishable`. - - Dedupe: if SKU already exists in Woo under any status → `409 duplicate_sku`. + - Pre-conditions: status in `ready`, mandatory set (exactly one species, ≥1 wood category, ≥1 figure, remaining listing fields). Else `422 unpublishable` with field keys — UI maps, no raw code. + - Dedupe: if SKU already exists in Woo under any status → `409 duplicate_sku` (same payload as PUT). + - Stale Woo value → `422 stale_field` as above. Draft stays on the phone. - Returns `202` `{ "status": "publishing" }`. Client polls. - Woo failure → slab `failed`, `sync_log` written, poll returns `502 woo_error`. - Success → slab `published`, `woo_product_id` set, images purged, draft row deleted. @@ -195,10 +198,13 @@ Sheet/slider prefs are client localStorage only in POC (not in SettingsView). - `GET /api/v1/admin/taxonomy` → `200` taxonomy grouped by kind: ```jsonc { "categories": [...], "attributes": [{ name, slug, terms: [...] }], - "tags": [...] } + "tags": [...], + "taxonomy_updated_at": "ISO-8601" } ``` -- `POST /api/v1/admin/taxonomy/sync` → `200` `{ "categories": n, "attributes": n, "tags": n, "synced_at": "..." }`. - - Pulls from Woo, rebuilds the cache. `502 woo_error` on failure; previous cache retained. + Client re-pulls if `taxonomy_updated_at` is newer than its snapshot. 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. ### Pricing - `GET /api/v1/admin/pricing` → `[PriceRule]` @@ -216,10 +222,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": "..." }` +- `POST /api/v1/settings/test-woo` → `200` `{ "ok": bool, "detail": "...", "taxonomy_updated_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. -- `POST /api/v1/settings/test-inference` → `200` `{ "ok": bool, "vision_capable": bool, "detail": "..." }` + - 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. --- diff --git a/docs/PROMPTS.md b/docs/PROMPTS.md index d6ebce3..1eff3a3 100644 --- a/docs/PROMPTS.md +++ b/docs/PROMPTS.md @@ -85,8 +85,9 @@ Return valid JSON matching this schema: ``` ### Notes -- The taxonomy list (species, edge type, figure, grade) is pulled from the - synced Woo cache each time, so the options always match what's in the store. +- The taxonomy list (species, wood categories, edge type, figure, grade, feat-*) is + pulled from the **synced Woo cache only**. No hardcoded mill species list. A term + not in the cache cannot be suggested or selected. - Brief definitions help the model distinguish ambiguous cases (e.g., "quarter-sawn" vs "cathedral"). - Character/inclusions/voids/checks are free-text observations. The server @@ -175,6 +176,10 @@ dimensions as measured. {geo_sentence}` - No numeric-accuracy guardrail needed — dimensions never pass through the LLM. - If `content_llm_enabled` is false, the LLM call is skipped and templates are used with a generic description paragraph. +- **Portable Call 1 context (issue #2):** default is full resend of Call 1 (user + prompt + assistant JSON). Label each taxonomy value **confirmed** (user-edited) + vs **inferred** (still from Call 1). Stateful `previous_response_id` is opt-in + after `test-inference`. Fail closed if neither path works. --- diff --git a/docs/TECH-SPEC-PIPELINE.md b/docs/TECH-SPEC-PIPELINE.md index 62bbcd1..5af049b 100644 --- a/docs/TECH-SPEC-PIPELINE.md +++ b/docs/TECH-SPEC-PIPELINE.md @@ -12,7 +12,7 @@ Inference, Woo payload, API, and schema live in other docs. 1. **Determinism** — same inputs → bit-identical outputs. No randomness, no timestamps in math, no network calls for the math. 2. **Manual entry is authoritative** — user-entered length, thickness, SKU, and price always win over any computed value. 3. **Fallback chain is explicit** — every computed value has a defined fallback; the system never silently guesses. - - BG removal: chroma-key / threshold → user slider / sheet toggle → U2Net → retake. + - BG removal: chroma-key / threshold → four knobs (sheet, sensitivity, edge offset, feather) live on capture → retake. U2Net last-resort is deferred from POC (do not delete from architecture). - Length axis: min-area bounding rect → user rotate/confirm → retake if the photo is unusable. - Area: requires a single contiguous slab mask; broken mask → flag and retake. 4. **Units** — inches in 1/8" steps; sqft and bdft to 2 decimals; price half-up to 2 decimals. Those rounded values are what get stored. No sqin in the UI, tests, or product copy. @@ -48,8 +48,10 @@ The user must see where the cut will land and be able to fix it. Edge quality is - Show the slab edge clearly — that edge is the measurement boundary. - User confirms the edge before length / area run. -### 3.3 Persisted settings (reset-to-default available) -Factory values live in code. If the user changes them, the new values persist until reset. +### 3.3 Capture knobs (inline, live, persisted) +Factory values live in code. If the user changes them, the new values persist in localStorage until reset (Settings). + +All four knobs are **on the capture screen next to the overlay**, not in Settings. Changing any knob **immediately re-runs** §3.4 on the current photo and refreshes the overlay on the source image. No apply/save step. No server round trip in POC. 1. **Sheet** — auto / green / black. 2. **Sensitivity** — one slider; tighter vs looser detection (green: HSV range width; black: brightness cutoff). @@ -59,12 +61,12 @@ Factory values live in code. If the user changes them, the new values persist un ### 3.4 Algorithms - **Green:** chroma-key in HSV. Border flood-fill so only sheet pixels connected to the image edge are removed (protects green-ish grain inside the slab). - **Black:** grayscale threshold. Same flood-fill (protects dark grain). -- **Fallback:** if the edge is still wrong after adjustment, U2Net matting. Last resort. +- **Fallback (POC):** retake. U2Net matting is last resort **post-POC** (one photo in, mask out; sliders still apply). Do not implement in Gate C. - Morphological close, then feather per the setting. ### 3.5 Edge quality - The mask must be one contiguous slab contour. -- If the user cannot get a clean edge, retake is the resolution. +- If the user cannot get a clean edge with knobs, retake is the POC resolution. ## 4. Length axis, widths, sqft, bdft diff --git a/docs/UX.md b/docs/UX.md new file mode 100644 index 0000000..40fa0ce --- /dev/null +++ b/docs/UX.md @@ -0,0 +1,172 @@ +# SlabUploader UX — POC interaction spec + +Status: SPEC · 2026-09-01 · issue #10 (presentation) · functional rules in #9 / AGENTS +This is the UAT-facing interface contract. Wireframes are described as screens and states, not pixels. Implementers and testers use this file plus `AGENTS.md` and `docs/TECH-SPEC-PIPELINE.md`. + +Functional numbers (0.7 threshold, empty vs prefill) live in AGENTS §6. This file only says how those rules look and feel. + +--- + +## 0. Principles + +- One-handed phone, outdoor or mill-floor lighting, thumb reach. +- The user always sees what is missing **on the field**, not only after Submit. +- Manual values always beat inference. Empty is honest; fake prefill is not. +- No raw HTTP codes or stack traces. Recovery stays on the same screen as the field. + +Mandatory before **ready** (continue to publishable): + +- exactly **one species** +- **at least one wood category** +- **at least one figure** (`fig-*` or the Figure attribute — same locked set as DATA-MODEL) + +Issue #9 named species + ≥1 figure as the confidence-gate floor. Wood category is also required to list (original product). Presentation treats all three as mandatory with inline nudges (#10). + +--- + +## 1. First-run (calibration trick) + +Not OpenCV ruler detection (that path is deferred). First launch of capture shows a short overlay, dismissible, remembered in localStorage: + +1. Lay the slab on a **green or black sheet**. +2. Shoot **top-down**, lens parallel to the broad face. +3. Put a tape or known length along the prevailing grain so *you* can type length in inches. The app does not read the tape. +4. Take 1–5 photos. Confirm the mask before trusting numbers. + +Skip control: “Don’t show again.” Reset lives in Settings, not on the capture chrome. + +--- + +## 2. Capture — mask is the load-bearing step + +Background removal defines the pixel footprint. Length, widths, sqft, and bdft all sit on that mask. A clipped or fat mask silently moves the bounding rectangle and the 6" width samples. + +### Layout (one screen) + +- Full-bleed photo with **mask overlay** on the original (included vs excluded), not only the cropped PNG. +- Four knobs **on this screen**, next to the preview — never a trip to Settings: + - Sheet: auto / green / black + - Sensitivity + - Edge offset + - Feather +- Confirm edge. Then length-axis overlay (rotate / confirm). Then length, thickness, SKU. +- Retake photo. POC does **not** show “Try harder” (server U2Net). That control is deferred, not deleted — do not remove it from architecture docs. + +### Live re-run + +Changing any knob re-runs client background removal on the current photo and refreshes the overlay immediately. No save button for knobs. No server round trip for mask tuning in POC. + +### Empty / error states (capture) + +| Situation | What the user sees | +|---|---| +| Sheet color not auto-detected | Prompt: pick green or black. Stay on this screen. | +| Mask not one contiguous slab | Banner on the photo: “Cut is broken — retake or tighten the sliders.” Confirm stays disabled. | +| Axis looks wrong | Rotate control; if still wrong, retake. | +| After crop, shorter side would be < 1600px | Warn: this photo cannot list until retake. No fake upscale. | +| User tries to continue without confirming mask | Confirm is the gate; Continue is disabled until confirm. | + +Knobs persist in localStorage (reset-to-default in Settings). They are not server secrets. + +--- + +## 3. Review screen + +After calibrated upload (and Call 1 if inference is on). + +### Field rendering + +| Call 1 confidence | Species, wood categories, edge, figure, grade, feat-* | +|---|---| +| **≥ 0.7** (default; configurable in server settings later, not a POC slider) | Prefill from Call 1. Every value is editable. Show a quiet “suggested” affordance, not a lock. | +| **< 0.7** | Leave **empty**. Do not stash the low-confidence guess in a hidden field. | +| Inference OFF | Empty; user fills by hand. Same mandatory set. | + +Pickers for species, wood category, and figure list **only** Woo-synced leaves. No “other”, no hardcoded mill list. A species that appeared in Woo after last sync is invisible until the next successful sync. + +Price: recommendation from species $/bdft × bdft when a rate exists; otherwise empty. Override always allowed. + +Title / description: empty until the user taps **Generate text** (Call 2) or types. Call 2 never auto-runs. + +### Per-field nudges (not submit-only) + +While a mandatory field is empty, that control shows an inline state (mark + short line under the field): + +- Species: “Pick the species from the store list.” +- Wood category: “Pick at least one wood category.” +- Figure: “Pick at least one figure.” + +Nudges appear as soon as the screen loads with empties, and clear when the field is valid. Submit/Continue to ready stays disabled while any of the three is empty — **and** the user can still see which one without scrolling to a toast after tap. + +Do not use a single post-submit modal as the only signal. + +### Overrides + +Changing a prefilled species/figure is a normal edit. No confirm dialog. Manual value is what publishes. + +--- + +## 4. Settings (relevant bits) + +- **Store URL**: read-only text from `WOO_BASE_URL` (compose). Not an input. +- Woo username + application password: inputs. Password never round-trips on GET. +- Inference endpoint + key + model; enable flags. +- Species $/bdft table (seeded from synced species only). +- `woo_create_status` draft | publish. +- Reset capture knobs to factory. +- Manual “Refresh taxonomy” is optional for POC; test-connection always syncs. + +--- + +## 5. Validation (field exit and submit) + +Shared module on FastAPI (`docs/OPENAPI.md`). Client caches it; hash checked **on submit**, not on every blur. + +- 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. + +Presentation: errors sit under the field. No top-of-screen dump for field problems. + +--- + +## 6. Publish errors (after commit) + +Draft stays on the phone. + +**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”). +- Never show `409` as the message. + +**422 stale value** — inline on the field Woo rejected (category, attribute, price, …): + +- “This value is no longer valid. Pick from the current list.” +- Options from the synced cache after the submission-time refresh. +- Never show `422` as the message. + +Server 5xx / timeout / auth: one recoverable banner (“Couldn’t reach the store — try again”), draft intact. Not a field nudge. + +--- + +## 7. UAT can write cases against + +1. First-run overlay appears once; skip persists. +2. Knobs on capture; each slider live-reruns mask overlay on the source photo. +3. Confirm disabled until mask is contiguous. +4. Review with inference ON and confidence ≥ 0.7: fields filled, editable. +5. Review with confidence < 0.7: species/wood category/figure empty; inline nudges; cannot mark ready until all three satisfied. +6. Species picker contains only synced names; typing a non-synced name is impossible. +7. Duplicate SKU: inline SKU recovery, two actions, no raw code. +8. Stale category after sync: 422 copy on that field with current options. + +--- + +## 8. Non-goals (this spec) + +- Visual branding, type scale, color tokens. +- Server U2Net “Try harder” control (deferred). +- Configurable 0.7 slider in the UI. +- Offline / airplane-mode chrome.