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
+66 -31
View File
@@ -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`.
+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)`
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.
+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 | |
| `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.
+20 -17
View File
@@ -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 |
+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).
- 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.
---
+7 -2
View File
@@ -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.
---
+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.
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
+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.