Files
SlabUploader/docs/CREATE.md
T
grokbot_ty d754cac9ef docs(m3): bring CONTENT-WOO onto branch; lock character and categories
Copy listing-copy/Woo payload spec from main. Lock {character} to the
primary Figure term name (Cathedral example). Lock categories[] to all
ready-set species and wood-category ids.
2026-09-16 21:11:07 +00:00

432 lines
20 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Milestone 3 Create
## Purpose
Milestone 3 takes a Photos-ready device draft and makes the slab **submission-ready for WooCommerce**.
Submission-ready means the mill owner has filled the full store listing package against **live synced Woo attributes**, with price set, and can create the product in the store (practice SKUs as Woo drafts). Photos and measures from Milestones 1 and 2 are inputs. They are not enough by themselves.
**Exit criteria.** On the M3 UAT deploy, a Photos-ready slab reaches submission-ready and a `SLAB-UAT-*` product exists in WooCommerce as a **draft** whose categories, attributes, tags, price, and inventory images match the reviewed ready set. Create still works with inference off.
## How to read this file
This file is the Milestone 3 source of truth on branch `m3-ds`.
- **What is in M3** comes from Ty conversations on this branch (not from older milestone splits on `main`).
- **How Woo sync, metadata, pricing, and inference behave** is taken from the technical requirements on `main` (`CONTENT-WOO`, `DATA-MODEL`, `UX`, `ARCHITECTURE`, `OPENAPI`, `AGENTS`, `UAT-PRACTICE`, `DEPLOYMENT`) and written here so coding does not need to open `main`.
Related shipped specs: `docs/CAPTURE.md`, `docs/PHOTOS.md`. Listing copy and Woo payload detail: `docs/CONTENT-WOO.md` (from `main`). Product overview: `docs/PRODUCT.md`. Folder map: `docs/README.md`.
## Preconditions (already shipped)
On this branch before M3 work:
- Milestone 1 Capture works in the browser (`docs/CAPTURE.md`).
- Milestone 2 Photos works in the browser (`docs/PHOTOS.md`). UAT evidence on `:10881`.
- Device draft holds Photos-ready inventory PNGs, calibration measures, and related state.
- `frontend/` holds that client path. `backend/` does not yet implement the Woo client or inference proxy.
M3 adds the server path, Settings, taxonomy sync, review/ready fields, pricing, inference dependency, and Woo create.
## In scope
1. WooCommerce auth with WordPress application password (server-side).
2. Sync of **Woo attributes** from the store and correct use of those lists for the listing.
3. Review UI and rules to reach submission-ready (full ready set).
4. Pricing proposal from species `$/bdft` rules and user override.
5. OpenAI-compatible inference endpoint as a Settings dependency, with Call 1 / Call 2 behavior when enabled.
6. Create product in WooCommerce (force draft for `SLAB-UAT-*`), then clear the device draft on success.
## Out of scope unless Ty expands M3 in chat
- Creating new Woo categories, attributes, terms, or tags from the app.
- Updating an existing store SKU in place.
- Bulk create.
- Touching deploy port `:10880`.
- Dumping work onto `m2-ds` or doc-simplification branches.
---
## Woo attributes
Locked name for the synced taxonomy package. WooCommerce is the source of truth. The app caches ids. It does not invent the lists.
### Categories (product categories, leaf only)
| Field | Cardinality | Rule |
|---|---|---|
| Species | exactly 1 | Synced species leaves only. No hardcoded or fallback species set. A new store species appears only after the next successful sync. |
| Wood category | 1 or more | Leaf ids. Parents are grouping only. |
### Attributes (exactly five locked Woo attributes)
| Attribute | Cardinality | Rule |
|---|---|---|
| Edge Type | 1 | Term id from sync. |
| Figure | 1 or more | Term ids from sync. |
| Grade | 1 or more | Term ids from sync. |
| Thickness | 1 | Measured thickness rounded **up** into the store band (example: 1" lands in the store band labeled like `1"-1 1/2"` when that is the store label). |
| Moisture | 1 | Defaults to kiln-dried. User sets or keeps it. Never inferred. |
v1 does not create missing attributes or terms. If a term is missing in Woo, skip it. Do not invent it.
Species, character, length, and free-text thickness are **not** Woo product attributes. Only the five locked attributes go on the create payload.
### Tags
| Prefix | Cardinality | Rule |
|---|---|---|
| `fig-*` | 1 or more | Required. Other Woo tags are ignored at sync. |
| `feat-*` | 0 or more | Optional. Character, inclusions, voids, and checks map onto existing `feat-*` tags only. |
---
## Auth and store URL
From `main` deploy and agents rules, adopted here:
- FastAPI is the only Woo caller. The browser never holds store credentials.
- Auth is a WordPress **application password** for a dedicated low-privilege WP user (shop manager or custom role with product create/edit and media upload). Not WooCommerce consumer keys (`ck_` / `cs_`). Not the store admin login password.
- Basic auth shape: `woo_wp_username` + `woo_app_password`.
- Store URL is **`WOO_BASE_URL`** from `deploy/.env` / Docker Compose (`https://` only). Injected into the FastAPI container. Settings shows it read-only. Not a SQLite settings row. Change store means edit `.env` and recreate the FastAPI container.
- Username and application password are entered in Settings UI, stored AES-GCM encrypted in SQLite (`SLAB_AES_KEY` env).
- REST surface: `{WOO_BASE_URL}/wp-json/wc/v3/…`
- Client: `httpx` with connect timeout 10s and read timeout 60s for image-heavy create.
- Rotate: revoke the application password in WP Users → Profile, enter a new one in Settings, then run `test-woo`.
---
## Taxonomy sync
Endpoint and behavior from `main` OPENAPI / CONTENT-WOO / ARCHITECTURE:
`POST /api/v1/admin/taxonomy/sync` pulls, in order:
1. `GET /products/categories?per_page=100` (paginate) → species and wood-category leaves after app filter.
2. `GET /products/tags?per_page=100` → keep only `fig-*` and `feat-*`.
3. `GET /products/attributes` plus terms → the five locked attributes.
Rebuild cache tables in one transaction. On failure return `502 woo_error` and **keep** the previous cache. Never delete-then-fail.
**Stamps.** Woo REST has no usable last-modified for categories, attributes, or tags. FastAPI stores:
- `taxonomy_last_sync_at` on every successful pull
- `taxonomy_anchor` bumped only when the stored-subset hash changes (including deletions)
Client re-pulls the full taxonomy only when the anchor advances.
**Triggers.**
1. Successful `POST /api/v1/settings/test-woo` always resyncs.
2. On publish, if `now - taxonomy_last_sync_at > 1 hour`, reconcile (bump anchor only on real change).
3. Optional app launch or Settings refresh.
If a picked id disappeared after refresh, show that field as no longer valid. Do not silently swap another value.
`GET /api/v1/admin/taxonomy` returns the cached groups plus `taxonomy_anchor` and `taxonomy_last_sync_at`.
---
## Submission-ready (ready set)
A slab is submission-ready only when **all** of the following are present. Species plus one figure is not enough.
Required:
- At least one inventory PNG (from Milestone 2)
- SKU matching `[A-Z0-9-]{3,24}` (practice runs use `SLAB-UAT-*`)
- Length and thickness (from capture, still editable on review)
- Client-computed widths, sqft, and bdft (user typed number wins if overridden)
- Exactly one species leaf
- At least one wood-category leaf
- One edge type term
- At least one figure term
- At least one grade term
- Thickness band term (round-up from measured thickness)
- One moisture term (default kiln-dried)
- At least one `fig-*` tag
- Price (see Pricing)
Optional:
- `feat-*` tags (zero or more)
- Title, short title, description (may be typed, filled by templates at create, or written by Generate text when inference is on). Not required to reach ready.
Inline per-field nudges show empty required fields as the owner works. The slab cannot reach ready until the full set is filled.
Server status name for this state: `ready` (see data model below). Publish/create is blocked with `422 unpublishable` until then.
---
## Pricing
From `main` DATA-MODEL / UX / AGENTS:
- Settings (or admin API) holds `pricing_rules`: one flat `$/bdft` per species name that matches a synced species leaf. No tiers in v1.
- Seed rule rows from synced species. The mill owner sets the rate per species.
- Proposal: `price = bdft × price_per_bdft` for the selected species.
- User may replace the proposal. Override sticks (`price_source` = `override` vs `recommendation`).
- If no rule exists for the species, price starts empty and the owner types it.
- Publish/create is allowed with a manual price.
- `bdft = sqft × thickness_in` (deterministic).
Admin API surface from `main` OPENAPI:
- `GET /api/v1/admin/pricing`
- `POST /api/v1/admin/pricing`
- `PUT /api/v1/admin/pricing/{id}`
- `DELETE /api/v1/admin/pricing/{id}`
---
## Inference
M3 includes the OpenAI-compatible endpoint as a real server dependency and the Call 1 / Call 2 rules below. Create must pass with inference **off**.
### Settings (not compose)
Stored encrypted in SQLite Settings (not `deploy/.env`):
- `inference_base_url`
- `inference_api_key`
- `inference_model`
- `inference_enabled`
- `content_llm_enabled`
FastAPI proxies all inference calls. The browser never holds the key.
`POST /api/v1/settings/test-inference` returns `{ ok, mode: stateful|stateless|fail, vision_capable, detail }`. Mode `fail` blocks Call 2 enablement only. Inference-off create still works.
### Call 1 (vision / taxonomy assist)
- Runs only when `inference_enabled` is true (auto after calibrated draft upload when on, or via `POST /api/v1/slabs/{id}/infer-taxon`).
- Input: inventory photos downscaled to max 1024px, user metadata, synced Woo taxonomy lists.
- Output: species, wood categories, edge, figure, grade, and feature-tag suggestions constrained to synced lists, each with confidence.
- Default confidence threshold 0.7 (configurable). At or above threshold: pre-fill on review, still editable. Below: leave empty. Do not show low-confidence guesses as defaults.
- Moisture is never inferred.
- Typed values always win over model values.
- Vision never blocks create and never auto-publishes.
### Call 2 (Generate text)
- User-triggered only (`POST /api/v1/slabs/{id}/infer-content`). Never auto.
- Requires successful `test-inference` (`ok: true`).
- Model writes prose only (character, color, grain, uses). It must not invent dimensions.
- Server assembles title, short title, and description from templates with prose injected.
- If the endpoint is down, disable the button. Templates still work. Create is never blocked by Generate text failure.
### Templates (always available)
Full defaults and Woo payload text live in `docs/CONTENT-WOO.md`. Summary here:
**title**
```
{species} {length_ft}ft {length_in_remainder}in × {width_avg_in}in Wood Slab - {character}
```
Example from CONTENT-WOO: `Black Walnut 16ft 0in × 18.2in Wood Slab - Cathedral`
**short_title**
```
{species} Slab - {character}
```
**description** (multi-paragraph)
```
A one-of-a-one {character} {species} slab, {length_ft}ft {length_in_remainder}in long with an
average width of {width_avg_in}in and {thickness_in}in thickness ({bdft} bdft).
Each slab is naturally shaped. Width varies along the length at {width_min_in}in to
{width_max_in}in. Sold by the slab; dimensions as measured. {geo_sentence}
```
**What `{character}` means (locked from CONTENT-WOO example + PROMPTS on `main`).**
The word after the dash in the example is `Cathedral`, which is a **Figure** attribute term.
Fill `{character}` with the primary (first) Figure term **name** from the ready set.
If somehow no figure is present, leave that slot empty (ready set normally requires ≥1 figure).
Do not confuse this with `feat-*` tags. Call 1 free-text "character / inclusions / voids / checks" map to `feat-*` tags, not to this title slot.
All template math fields come from the deterministic pipeline, never from the LLM.
---
## Review and create flow
Mill-owner path after Photos-ready:
1. Upload calibrated / Photos-ready draft to the server (`POST /api/v1/slabs` multipart) when the M3 client reaches that step. Status becomes `calibrated`. If inference is on, Call 1 may run.
2. **Review.** Owner fills or overrides every ready-set field. Pickers show only synced Woo attributes.
3. When the ready set is complete, status becomes `ready` (submission-ready).
4. Owner taps create/publish → `POST /api/v1/slabs/{id}/publish`.
5. Server runs the create sequence below. Client polls `GET /api/v1/slabs/{id}`.
### Create sequence (server)
1. Check the listing against the ready-set rules.
2. Look up SKU in the store (`GET /products?sku={sku}`, any status). Stop if found.
3. Refresh taxonomy if last successful pull is older than 1 hour.
4. Resolve categories, tags, and attributes from the synced lists.
5. Upload inventory PNGs via `POST /media`, keep alpha. Reference `source_url` values on the product. Cap at 5 images. **Woo image order (locked):** (1) clean calibration inventory PNG, (2) dimension-overlay copy of that calibration PNG, (3+) remaining batch inventory PNGs in the order they were added in the web app. The overlay is part of the submission. Do not upload non-inventory artifacts outside that lineup.
6. `POST /products` with status from Settings `woo_create_status` (`draft` or `publish`, default `draft`). **`SLAB-UAT-*` always forces draft.**
7. On success: log, set `woo_product_id`, mark published locally, delete original and processed photos, clear device draft.
8. On failure: log, mark failed, keep photos, put a field-level error on the phone. No raw status codes for the mill owner.
### Create payload (shape)
`POST /products` sends only non-null fields. Conceptual body:
```jsonc
{
"name": "{title}",
"type": "simple",
"status": "{woo_create_status}",
"sku": "{sku}",
"regular_price": "{price}",
"description": "{description}",
"short_description": "{short_title}",
"categories": [ { "id": <species leaf id> }, { "id": <wood-category leaf id> }, ... ],
"tags": [ { "id": <fig-* and feat-* ids> } ],
"images": [ { "src": "<media source_url>", "alt": "{species} {character} slab" } ],
"attributes": [ /* Edge, Figure, Grade, Thickness, Moisture term ids when present */ ],
"virtual": false,
"downloadable": false,
"catalog_visibility": "visible"
}
```
App "published" means the store create succeeded. That is not the same as the Woo product being live when status is draft.
### Duplicate SKU
Before create, if the SKU exists in any store status: block. Show Edit SKU and Open existing listing. This version does not update the other product. API: `409 duplicate_sku`.
### Stale taxonomy on submit
If a category, attribute, or price is no longer valid after refresh: under that field, tell the owner to pick from the current list and supply current options (`stale_field`).
---
## Settings keys (M3)
| Key | Where | Notes |
|---|---|---|
| `WOO_BASE_URL` | compose / `.env` | HTTPS store URL. Read-only in UI. |
| `SLAB_AES_KEY` | compose / `.env` | AES master key. Never in DB or repo. |
| `woo_wp_username` | Settings (encrypted) | Dedicated WP user. |
| `woo_app_password` | Settings (encrypted) | Application password. |
| `woo_create_status` | Settings | `draft` or `publish`. Default `draft`. |
| `inference_base_url` | Settings (encrypted) | OpenAI-compatible base URL. |
| `inference_api_key` | Settings (encrypted) | |
| `inference_model` | Settings | |
| `inference_enabled` | Settings | |
| `content_llm_enabled` | Settings | Gates Generate text. |
| `taxonomy_last_sync_at` | Settings / derived | |
| `taxonomy_anchor` | Settings / derived | |
| `brand_voice`, `geo_context` | Settings | Copy helpers. |
Relevant Settings API: `GET/PUT /api/v1/settings`, `POST /api/v1/settings/test-woo`, `POST /api/v1/settings/test-inference`.
---
## Data shape (submission-ready fields)
Core slab fields M3 must persist (from `main` DATA-MODEL, adopted here):
| Field | Rule |
|---|---|
| `sku` | Required. Unique. |
| `status` | `draft` → `calibrated` → `ready` → `publishing` → `published`, or `failed` |
| `length_in`, `thickness_in` | Required. 1/8" step. |
| `sqft`, `bdft`, widths | From client math. |
| `species_id` | 1 species leaf. |
| `wood_category_ids` | 1 or more. |
| `edge_type_term_id` | 1. |
| `figure_term_ids` | 1 or more. |
| `grade_term_ids` | 1 or more. |
| `thickness_term_id` | Round-up band. |
| `moisture_term_id` | 1. Default kiln-dried. |
| `fig_tag_ids` | 1 or more. |
| `feat_tag_ids` | 0 or more. |
| `price`, `price_per_bdft`, `price_source` | See Pricing. |
| `title`, `short_title`, `description` | Optional for ready. |
Cache tables: `woo_categories`, `woo_attributes`, `woo_attribute_terms`, `woo_tags`, `pricing_rules`.
Device draft (IndexedDB on the phone) is not a WooCommerce draft. Clear the device draft after successful create.
---
## Practice / UAT rules
- Every practice listing uses SKU prefix `SLAB-UAT-*`.
- `SLAB-UAT-*` always creates a WooCommerce draft, even if Settings say publish.
- Customers must not see practice listings on the storefront.
- Inference is **off** for the claimable exit walk. Optional ON path only after inference Settings pass `test-inference`.
- UAT deploy for this branch: ubudual5060 `http://100.64.0.9:10881`. Do not use `:10880`.
---
## API surface M3 must implement (summary)
Base path `/api/v1` (health may also be `/api/health`).
| Area | Methods |
|---|---|
| Health | `GET /api/health` |
| Slabs | `POST/GET/PUT/DELETE /api/v1/slabs`, `GET /api/v1/slabs/{id}` |
| Photos | original/processed GET while retained |
| Inference | `POST .../infer-taxon`, `POST .../infer-content` |
| Publish | `POST .../publish` → 202, poll slab |
| Taxonomy | `GET/POST /api/v1/admin/taxonomy`, `.../sync` |
| Pricing | CRUD `/api/v1/admin/pricing` |
| Settings | `GET/PUT /api/v1/settings`, `test-woo`, `test-inference` |
Full request and response field lists on historical `main` `docs/OPENAPI.md` are the detail reference if a field is missing here. Prefer extending this file over coding from `main` directly.
---
## Done when (exit criteria)
**Product state induced.** Overnight UAT app on ubudual5060 `http://100.64.0.9:10881` (or the current M3 image of this branch), with `WOO_BASE_URL` and Woo application password pointed at the intended store.
**Evidence from that state.**
1. `test-woo` succeeds and taxonomy sync fills Woo attributes into the app cache. Pickers offer only synced ids.
2. A Photos-ready slab can be filled to the **full ready set** (submission-ready) using those lists and pricing rules, with **inference off**.
3. Create with `SLAB-UAT-*` yields a WooCommerce **draft** whose categories, attributes, tags, price, and inventory images match the reviewed ready set.
4. Duplicate SKU is blocked. Device draft clears after success.
5. Inference Settings accept an OpenAI-compatible base URL, key, and model. `test-inference` runs. Create still succeeds with inference disabled.
## Dishonest proxies (do not count as pass)
- Hardcoded or fixture taxonomy lists treated as synced.
- Mock Woo client that never hits the real store for the exit walk.
- Unit tests alone without a real `SLAB-UAT-*` draft in the store.
- Consumer-key auth instead of application password.
- Inference "wired" as dead config with no server proxy, or create that requires inference on.
- Claiming submission-ready when required ready-set fields are still empty.
- Touching `:10880` or parking work on `m2-ds` / doc-simplification.
## Open decisions (must lock before task breakdown)
Locked from Ty conversation or from `docs/CONTENT-WOO.md`:
1. **`categories` on `POST /products`.** **Locked from CONTENT-WOO field rules:** send every ready-set category id (species leaf + each wood-category leaf) as `[{"id": n}, ...]`. The single-id example in CONTENT-WOO §2.4 is incomplete relative to those field rules.
2. **Template `{character}`.** **Locked from CONTENT-WOO example (`… Wood Slab - Cathedral`):** use the primary Figure term **name**. `feat-*` tags are separate (Call 1 free-text character/inclusions/voids/checks map there).
3. **Dimension-overlay PNG.** **Locked (Ty 2026-09-16):** Woo image order is (1) clean calibration PNG, (2) dimension-overlay copy, (3+) other inventory PNGs in add order. Cap ≤5.
Still open (CONTENT-WOO does not fully settle these):
4. **`attributes` element shape on `POST /products`.** CONTENT-WOO still has `"attributes": [ … if present … ]` with no fields. Need a live Whidbey product JSON sample, or Ty pointing at the official Woo REST fields to use. Do not invent.
5. **Server persistence.** CONTENT-WOO describes create sequence and payload. It does not choose full OpenAPI slab CRUD vs a thinner FastAPI path on this branch. Still need a lock before coding the server store.
Non-blocking notes (shipped M1/M2 win on this branch): fill target 90% and 800px floor (not main's 80%/1600); Vite + Svelte SPA (not SvelteKit docs); overlay draw spacing may differ from measure spacing by design.
If store-specific term labels differ from examples in this file, the synced store lists win.