docs(#10): UX interaction spec plus remaining POC issue write-back

UAT is POC, so #10 is in DOC COMPLETE. Capture knobs live on-screen, review
confidence presentation, 409/422 field recovery, taxonomy cache, portable
Call 1→Call 2, shared validation. U2Net Try harder deferred, not deleted.
This commit is contained in:
2026-09-02 00:30:47 +00:00
parent 3921a9dfa2
commit 7b7ed712c2
10 changed files with 329 additions and 79 deletions
+1 -1
View File
@@ -32,7 +32,7 @@ Archived PRD lines that conflict with the files above are non-authoritative (see
### Open doc defects ### 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.
--- ---
+2 -1
View File
@@ -28,6 +28,7 @@ early Phase 1 first draft listing).
| `docs/PROMPTS.md` | Vision + content prompts (server files) | | `docs/PROMPTS.md` | Vision + content prompts (server files) |
| `docs/DEPLOYMENT.md` | Env-agnostic compose, Caddy, backup/restore | | `docs/DEPLOYMENT.md` | Env-agnostic compose, Caddy, backup/restore |
| `deploy/INVENTORY.md` | Host inventory worksheet (Gate B) | | `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/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) | | `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: 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. - **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. - **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). - **Woo UAT:** draft default; `SLAB-UAT-*` force draft (AGENTS §5).
+66 -31
View File
@@ -10,22 +10,24 @@ Companion: `docs/TECH-SPEC-PIPELINE.md`. Historical PRD: `docs/archive/PRD-2026-
## 1. Topology (hybrid) ## 1. Topology (hybrid)
Phone does the happy-path pipeline (mask, length axis, sqft/bdft, 3:4 PNG). Phone does the happy-path pipeline (mask knobs, length axis, sqft/bdft, 3:4 PNG).
FastAPI stores drafts, runs U2Net only on demand, proxies inference, talks to Woo. FastAPI stores drafts, proxies inference, talks to Woo. reverse proxy terminates HTTPS.
reverse proxy terminates HTTPS. Server U2Net (“Try harder”) is **deferred from POC**, not deleted.
``` ```
phone (SvelteKit) phone (SvelteKit)
capture, mask, sliders, length axis, sqft/bdft, 3:4 PNG capture, four knobs inline, live mask overlay on the source photo,
if mask still bad → POST one photo → FastAPI U2Net → mask back → user continues length axis, sqft/bdft, 3:4 PNG
(POC: retake if mask still bad — no U2Net control)
server server
reverse proxy → frontend (static) 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. - 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. - Woo + LLM keys stay on the server. Browser does not call Woo or the vision endpoint directly.
## 2. Swimlane ## 2. Swimlane
@@ -45,7 +47,7 @@ flowchart TB
end end
subgraph API["server — FastAPI"] subgraph API["server — FastAPI"]
A1[U2Net: one photo in, mask out] A1[U2Net reserved post-POC]
A2[Store draft: originals + PNGs + numbers] A2[Store draft: originals + PNGs + numbers]
A3[Call 1 proxy — vision] A3[Call 1 proxy — vision]
A4[Call 2 proxy — text] A4[Call 2 proxy — text]
@@ -63,8 +65,7 @@ flowchart TB
end end
P1 --> P2 --> P3 P1 --> P2 --> P3
P3 -->|no — Try harder| A1 P3 -->|no — retune knobs or retake| P2
A1 --> P2
P3 -->|no — Retake| P1 P3 -->|no — Retake| P1
P3 -->|yes| P4 --> P5 --> P6 P3 -->|yes| P4 --> P5 --> P6
P6 --> A2 P6 --> A2
@@ -77,34 +78,34 @@ flowchart TB
## 3. Clarifications ## 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). 1. **Happy path** stays on the phone through PNG + numbers. FastAPI is idle until the user has a confirmed mask.
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. 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. 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 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 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: is skipped; user can manually fill taxonomy and still trigger Call 2. Body:
all originals downscaled to 1024 + Woo taxonomy snapshot + SKU/length/thickness. all originals downscaled to 1024 + **Woo-synced taxonomy only** + SKU/length/thickness.
Timeout 45s → error + retry on the phone. When inference is on, Call 1 results Timeout 45s → error + retry on the phone. ≥0.7 confidence **pre-populates**
**pre-populate** the review screen: species, wood categories, edge/figure/grade species, wood categories, edge/figure/grade, feat-* (mutable). Below 0.7 those
attributes, `fig-*`/`feat-*` tags (from character/inclusions/voids/checks), with fields stay **empty**. Manual entry stays authoritative. Presentation: `docs/UX.md`.
confidence. User reviews and overrides. Manual entry stays authoritative; 5. **Call 2** does not auto-fire. User taps Generate text. Server must send
Call 1 is assist-only. **portable Call 1 context** (default: full conversation resend). Stateful
5. **Call 2** does not auto-fire. User taps Generate text. Server sends curated `previous_response_id` only after `test-inference` proves the provider supports
Call 1 results (or manually-entered taxonomy if Call 1 failed) + deterministic it. Call 2 inputs distinguish confirmed user values from inferred ones. Per-slab
numbers + brand/GEO for prose generation; assembles title/description/short thread; delete on submit or abandon.
description from deterministic templates with the LLM prose injected.
6. **Woo** is only FastAPI. Browser never sees the WordPress application 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, 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 ## 4. What runs where
| Job | Where | | Job | Where |
|---|---| |---|---|
| Camera, 1-5 photos | Client | | Camera, 1-5 photos | Client |
| Sheet detect, chroma-key / black threshold, sliders, flood-fill | Client | | Sheet detect, chroma-key / black threshold, four knobs, live re-run, flood-fill | Client |
| U2Net | Server, on demand | | U2Net “Try harder” | Server, **deferred from POC** |
| Length axis overlay, 6" widths, sqft, bdft | Client | | Length axis overlay, 6" widths, sqft, bdft | Client |
| 3:4 PNG, 80% fill (configurable aspect) | Client | | 3:4 PNG, 80% fill (configurable aspect) | Client |
| Draft + originals + processed PNGs | Server (after user continues) | | 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. 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, 3. **ready** — all mandatory fields populated (species, wood category, edge type,
figure, grade, thickness, price, title/desc/short desc, at least one photo). 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 Values can come from Call 1 (≥ 0.7 pre-fill the user kept), manual entry, or a mix.
entry, or a mix. **Review gate:** if Call 1 returned below-threshold for a **Review gate:** below-threshold Call 1 leaves that field empty. Mandatory before
field, that field stays empty until the user fills it. At minimum **species** ready: **exactly one species, ≥1 wood category, ≥1 figure**. Inline nudges, not
and **≥1 figure attribute** must be set before ready. Inference is optional; submit-only (`docs/UX.md`). Inference is optional; with inference OFF the user
with inference OFF the user fills everything by hand. fills everything by hand.
4. **publishing** — Woo create in flight. Poll for result. 4. **publishing** — Woo create in flight. Poll for result.
5. **published** — Woo product created, woo_product_id stored. Images purged. 5. **published** — Woo product created, woo_product_id stored. Images purged.
6. **failed** — pipeline, inference, or publish error. Error detail stored. Retry available. 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 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 → results do not auto-fill. If the user skips inference, the path is: calibrated →
ready → publishing → published. 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`.
+20 -4
View File
@@ -84,7 +84,14 @@ and the final stored content.
1. `GET /products/categories?per_page=100` (paginate) → `woo_taxonomy(kind=category)` 1. `GET /products/categories?per_page=100` (paginate) → `woo_taxonomy(kind=category)`
2. `GET /products/tags?per_page=100` → `kind=tag` 2. `GET /products/tags?per_page=100` → `kind=tag`
3. `GET /products/attributes` → `kind=attribute` 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). Failure → `502 woo_error`; previous cache retained (never delete-then-fail).
### 2.3 Taxonomy assignment (FR26) ### 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. mutable. The user confirms or overrides; manual entry always wins.
**Below threshold:** those fields stay empty. The review screen gates continue **Below threshold:** those fields stay empty. The review screen gates continue
until the user manually selects at least **species** and **one figure attribute** until the user manually selects **exactly one species, ≥1 wood category, and ≥1
(and the rest of the ready mandatory set). Do not auto-fill below-threshold figure**. Inline nudges: `docs/UX.md`. Do not auto-fill below-threshold
guesses. Full rules: AGENTS §6. guesses. Full functional rules: AGENTS §6.
**Deterministic name-matching** resolves Call 1 names (or manual picks) to synced **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 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. payload assembly, dedupe, logging, title assembly.
- Scoped inference: optional LLM prose generation for description only. The LLM - Scoped inference: optional LLM prose generation for description only. The LLM
never touches dimensions. 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.
+5 -1
View File
@@ -50,6 +50,7 @@ Draft-only. Deleted on successful publish. WooCommerce is the source of truth fo
| `server_rev` | INTEGER NOT NULL DEFAULT 1 | | | `server_rev` | INTEGER NOT NULL DEFAULT 1 | |
| `created_at` | TEXT NOT NULL | | | `created_at` | TEXT NOT NULL | |
| `updated_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`. Indexes: `sku` (unique), `status`, `updated_at`.
@@ -64,7 +65,7 @@ any → failed (with error)
1. **draft** — photos taken, no mask confirmed. 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. 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. 4. **publishing** — Woo create in flight. Poll for result.
5. **published** — Woo product created. This row + all photos deleted. sync_log retained. 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. 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 | | | `synced_at` | TEXT | |
Only species + wood category. Sync pulls all Woo categories; the app filters to the two we use. 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)`. `UNIQUE(woo_id)`.
@@ -183,8 +185,10 @@ Keys (server settings; secrets encrypted at rest):
- `inference_enabled`, `content_llm_enabled` - `inference_enabled`, `content_llm_enabled`
- `brand_voice`, `geo_context` - `brand_voice`, `geo_context`
- `woo_create_status` ('draft' | 'publish'), default `draft` — Woo product create status key only; safety policy lives in AGENTS.md §5 - `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) - `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 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). 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. Optional server sync of those prefs is out of scope for POC.
+20 -17
View File
@@ -116,7 +116,7 @@ Deliverables:
settings (AES-GCM + test stubs), taxonomy (cache read/write). settings (AES-GCM + test stubs), taxonomy (cache read/write).
- Optimistic concurrency (`client_rev` / `server_rev`, 409 paths). - Optimistic concurrency (`client_rev` / `server_rev`, 409 paths).
- Status machine only: `draft → calibrated → ready → publishing → published | failed`. - 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); - Integration tests (httpx TestClient): create calibrated → poll → ready (manual fields);
revision conflict; duplicate SKU; settings encryption round-trip (secret never returned). revision conflict; duplicate SKU; settings encryption round-trip (secret never returned).
@@ -162,15 +162,16 @@ Deliverables:
re-crop UI as SoT. re-crop UI as SoT.
- Inference UI (only meaningful after Gate C and when `inference_enabled`): Call 1 - Inference UI (only meaningful after Gate C and when `inference_enabled`): Call 1
results with confidence. **≥ 0.7** (configurable default) → pre-fill mutable results with confidence. **≥ 0.7** (configurable default) → pre-fill mutable
fields. **< 0.7** → leave empty; block continue until species + ≥1 figure fields. **< 0.7** → leave empty; inline nudges; block continue until exactly
(and other mandatories) are set by hand. "Generate text" triggers Call 2; one species, ≥1 wood category, and ≥1 figure (`docs/UX.md`). "Generate text"
never auto. 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 - Publish button (online-only), poll `publishing` → `published` | `failed`, surface
error detail from sync_log. error detail from sync_log.
- Settings UI: WP username + application password + test-woo, inference endpoint + - Settings UI: WP username + application password + test-woo, inference endpoint +
test (vision check), pricing rules editor, brand voice/GEO, `woo_create_status`. test (vision + portable-context probe), pricing rules editor, brand voice/GEO,
Sheet/slider prefs stay in client localStorage. Prompts are server files, not UI `woo_create_status`. Store URL read-only from `WOO_BASE_URL`. Sheet/slider prefs
settings. stay in client localStorage. Prompts are server files, not UI settings.
- Secrets never appear in GET settings responses (masked `*_configured` only). - Secrets never appear in GET settings responses (masked `*_configured` only).
Acceptance: Acceptance:
@@ -228,14 +229,16 @@ Deliverables:
- Call 1 (vision) proxy: **all inventory photos** downscaled to 1024 + Woo taxonomy - Call 1 (vision) proxy: **all inventory photos** downscaled to 1024 + Woo taxonomy
snapshot + user metadata; OpenAI-compatible endpoint; model constrained to cached snapshot + user metadata; OpenAI-compatible endpoint; model constrained to cached
taxonomy; confidence threshold **0.7** (configurable default; not a POC Settings taxonomy **from the Woo cache only**; confidence threshold **0.7** (configurable
slider). Auto-run on calibrated create only if `inference_enabled`. Not top-down-only. 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/ - Apply AGENTS §6 gate: ≥ threshold → pre-fill mutable species/categories/attributes/
tags; < threshold → empty fields + review gate requiring species + ≥1 figure tags; < threshold → empty fields + review gate requiring species + wood category
before ready. + figure before ready (`docs/UX.md`).
- Call 2 (text) proxy: never auto. User taps Generate text. LLM prose only; templates - Call 2 (text) proxy: never auto. Portable Call 1 context (full resend default).
inject deterministic numbers. See `docs/PROMPTS.md`. LLM prose only; templates inject deterministic numbers. See `docs/PROMPTS.md`.
- `test-inference` endpoint (vision capability check) before Call 2 is allowed. - `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. - Feature flags; `inference_log` audit rows; prompt files read fresh each call.
- No numeric/dimension invention by the LLM. LoRA deferred. - 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 - Local vision model configured: ≥ 0.7 fields pre-fill and remain editable; user
override sticks. override sticks.
- Below 0.7: fields stay empty; UI blocks continue/ready until species and ≥1 - Below 0.7: fields stay empty; UI blocks continue/ready until exactly one
figure attribute are set manually (asserted in integration or UI test). 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 - No doc/code path auto-fills below-threshold guesses or treats inference as a
publish gate. publish gate.
- Inference OFF: app works end-to-end; tests assert zero calls to the inference endpoint. - 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 | | 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 | | 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 | | 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 | | Local vision endpoint not up | inference off | configurable endpoint; deploy-time config; app works without it |
+29 -17
View File
@@ -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). - `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). - 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`. - 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` → `200` `[Slab]` (most recent first).
- `GET /api/v1/slabs/{id}` → `200` `Slab` / `404`. - `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). - 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`). - `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. - 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. - `DELETE /api/v1/slabs/{id}` → `204`. Soft: only unpublished drafts. Published slabs cannot be deleted from the app.
### Photo operations ### 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. - Returns `202` `{ mask_url: "..." }`. Client uses the mask to re-compute area/widths locally.
- `404` if photo not found. - `404` if photo not found.
- `GET /api/v1/slabs/{id}/photos/{pid}/original` → `image/*` (original, while retained). - `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. - Returns `202` `{ "status": "inferring" }`. Client polls `GET /api/v1/slabs/{id}` for results.
- Timeout > 45s → `504 gateway_timeout` with error detail. - Timeout > 45s → `504 gateway_timeout` with error detail.
- `POST /api/v1/slabs/{id}/infer-content` — Trigger Call 2 (text). - `POST /api/v1/slabs/{id}/infer-content` — Trigger Call 2 (text).
- Server reads the Call 2 prompt file fresh, uses available taxonomy - Server reads the Call 2 prompt file fresh. Default: **resend full Call 1
(from Call 1 results or manually-entered values) + deterministic numbers context** (prompt + assistant JSON + confirmed vs inferred labels). Use
+ brand voice + GEO context, sends to the LLM for prose generation, then `previous_response_id` only if test-inference reported stateful support.
assembles title/description/short description from deterministic templates Per-slab thread; delete on publish or abandon.
with the LLM prose injected. - Taxonomy + deterministic numbers + brand/GEO; LLM prose then templates.
- Requires that inference was previously tested as connected - Requires `test-inference` `ok: true`. Else `422` (not connected / not portable).
(`test-inference` succeeded at least once). If not connected, returns `422`.
- Returns `202` `{ "status": "inferring" }`. Client polls for results. - Returns `202` `{ "status": "inferring" }`. Client polls for results.
- Timeout > 45s → `504` with error detail. - Timeout > 45s → `504` with error detail.
### Publish ### Publish
- `POST /api/v1/slabs/{id}/publish` — Publish to WooCommerce. - `POST /api/v1/slabs/{id}/publish` — Publish to WooCommerce.
- Pre-conditions: status in `ready`, all required fields set. Else `422 unpublishable`. - 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`. - 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. - Returns `202` `{ "status": "publishing" }`. Client polls.
- Woo failure → slab `failed`, `sync_log` written, poll returns `502 woo_error`. - Woo failure → slab `failed`, `sync_log` written, poll returns `502 woo_error`.
- Success → slab `published`, `woo_product_id` set, images purged, draft row deleted. - 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: - `GET /api/v1/admin/taxonomy` → `200` taxonomy grouped by kind:
```jsonc ```jsonc
{ "categories": [...], "attributes": [{ name, slug, terms: [...] }], { "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": "..." }`. Client re-pulls if `taxonomy_updated_at` is newer than its snapshot. Species/attribute/tag
- Pulls from Woo, rebuilds the cache. `502 woo_error` on failure; previous cache retained. 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 ### Pricing
- `GET /api/v1/admin/pricing` → `[PriceRule]` - `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`. `woo_consumer_key` / `woo_consumer_secret`.
- Secrets re-encrypted server-side. `200` `SettingsView` returned (password - Secrets re-encrypted server-side. `200` `SettingsView` returned (password
never echoed). 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 - 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. `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.
--- ---
+7 -2
View File
@@ -85,8 +85,9 @@ Return valid JSON matching this schema:
``` ```
### Notes ### Notes
- The taxonomy list (species, edge type, figure, grade) is pulled from the - The taxonomy list (species, wood categories, edge type, figure, grade, feat-*) is
synced Woo cache each time, so the options always match what's in the store. 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 - Brief definitions help the model distinguish ambiguous cases
(e.g., "quarter-sawn" vs "cathedral"). (e.g., "quarter-sawn" vs "cathedral").
- Character/inclusions/voids/checks are free-text observations. The server - 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. - No numeric-accuracy guardrail needed — dimensions never pass through the LLM.
- If `content_llm_enabled` is false, the LLM call is skipped and templates - If `content_llm_enabled` is false, the LLM call is skipped and templates
are used with a generic description paragraph. 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.
--- ---
+7 -5
View File
@@ -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. 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. 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. 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. - 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. - 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. 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. - Show the slab edge clearly — that edge is the measurement boundary.
- User confirms the edge before length / area run. - User confirms the edge before length / area run.
### 3.3 Persisted settings (reset-to-default available) ### 3.3 Capture knobs (inline, live, persisted)
Factory values live in code. If the user changes them, the new values persist until reset. 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. 1. **Sheet** — auto / green / black.
2. **Sensitivity** — one slider; tighter vs looser detection (green: HSV range width; black: brightness cutoff). 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 ### 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). - **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). - **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. - Morphological close, then feather per the setting.
### 3.5 Edge quality ### 3.5 Edge quality
- The mask must be one contiguous slab contour. - 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 ## 4. Length axis, widths, sqft, bdft
+172
View File
@@ -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 &lt; 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. |
| **&lt; 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 &lt; 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.