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.
This commit is contained in:
@@ -0,0 +1,201 @@
|
|||||||
|
# Listing copy and the store
|
||||||
|
|
||||||
|
> On branch `m3-ds`, Milestone 3 exit and scope live in `docs/CREATE.md`. This file is the listing-copy and Woo create/payload reference brought from `main` so coding does not need to open `main`. If anything conflicts with a Ty conversation lock in `CREATE.md`, `CREATE.md` wins.
|
||||||
|
|
||||||
|
|
||||||
|
How titles and descriptions are built, how we talk to WooCommerce, and the exact create payload. Templates always run. Generate text is optional and never publishes on its own.
|
||||||
|
|
||||||
|
## 1. Titles and descriptions
|
||||||
|
|
||||||
|
### 1.1 Templates (always on)
|
||||||
|
Templates live in Settings. Defaults below. Placeholders use `{field}`.
|
||||||
|
|
||||||
|
**title** (default):
|
||||||
|
```
|
||||||
|
{species} {length_ft}ft {length_in_remainder}in × {width_avg_in}in Wood Slab — {character}
|
||||||
|
```
|
||||||
|
Example: `Black Walnut 16ft 0in × 18.2in Wood Slab — Cathedral`
|
||||||
|
- `length_ft` = floor(length_in / 12), `length_in_remainder` = length_in mod 12 (1 dp).
|
||||||
|
|
||||||
|
**short_title** (default):
|
||||||
|
```
|
||||||
|
{species} Slab — {character}
|
||||||
|
```
|
||||||
|
Example: `Black Walnut Slab — Cathedral`
|
||||||
|
|
||||||
|
**description** (default, 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}
|
||||||
|
```
|
||||||
|
`geo_sentence` is the geography sentence from Settings, e.g.
|
||||||
|
`"Locally milled and shipped from the Pacific Northwest."` (default if unset: none).
|
||||||
|
|
||||||
|
All template math fields come from the deterministic pipeline (never from the LLM).
|
||||||
|
|
||||||
|
### 1.2 Generate text (optional)
|
||||||
|
The mill owner taps Generate text on review. It never runs by itself. It is off unless `content_llm_enabled` is true.
|
||||||
|
|
||||||
|
The model writes prose only (character, color, grain, uses). Measured length, width, thickness, square feet, and board feet are facts in the prompt. The model does not invent numbers. Templates inject those numbers into the title and description after the prose comes back.
|
||||||
|
|
||||||
|
The mill owner can always edit the result. `content_source` is `llm` when this path ran.
|
||||||
|
|
||||||
|
If the text endpoint is missing or down, disable the button. Templates still work. Publish is never blocked because Generate text failed.
|
||||||
|
|
||||||
|
### 1.3 Audit
|
||||||
|
Every content generation (template or LLM) is logged with inputs (template version,
|
||||||
|
slab facts), the LLM request/response payload when used (inference calls logged for audit),
|
||||||
|
and the final stored content.
|
||||||
|
|
||||||
|
## 2. WooCommerce
|
||||||
|
|
||||||
|
### 2.1 Auth & client
|
||||||
|
- REST v3 surface unchanged: `GET/POST/PATCH/DELETE {WOO_BASE_URL}/wp-json/wc/v3/…`
|
||||||
|
- Auth is **WordPress Application Passwords**, not WooCommerce consumer keys
|
||||||
|
(`ck_`/`cs_`) and not the user's login password.
|
||||||
|
- Create a **dedicated low-privilege WordPress user** (shop manager, or a custom
|
||||||
|
role with product create/edit + media upload). A dedicated low-privilege user, not the admin account, is used.
|
||||||
|
- Generate the application password under **Users → Profile → Application
|
||||||
|
Passwords**. WordPress requires **HTTPS** for that UI; the store must already
|
||||||
|
be on TLS (Caddy enforces HTTPS on the app side too).
|
||||||
|
- Basic auth shape: `woo_wp_username:woo_app_password` (username + application
|
||||||
|
password). Spaces in the generated password are ignored by WordPress; store
|
||||||
|
the value as entered.
|
||||||
|
- FastAPI is the sole Woo caller. Credentials live encrypted at rest in SQLite
|
||||||
|
(AES-GCM, `SLAB_AES_KEY` env). The browser never holds them.
|
||||||
|
- Rotation: revoke the application password in the WP user profile, enter a new
|
||||||
|
one in Settings UI, then `POST /api/v1/settings/test-woo`.
|
||||||
|
- Client: `httpx` with timeouts (connect 10s, read 60s for image-heavy create).
|
||||||
|
- Store URL is **`WOO_BASE_URL`** from Docker Compose / host `.env` (HTTPS only).
|
||||||
|
FastAPI refuses `http://` at startup. Not a Settings field; UI shows it read-only.
|
||||||
|
|
||||||
|
### 2.2 Store lists (species, tags, attributes)
|
||||||
|
`POST /api/v1/admin/taxonomy/sync` pulls, in order:
|
||||||
|
1. `GET /products/categories?per_page=100` (paginate) → species + wood-category leaves
|
||||||
|
2. `GET /products/tags?per_page=100` → `fig-*` / `feat-*` only (other tags ignored)
|
||||||
|
3. `GET /products/attributes` (+ terms) → five locked attributes
|
||||||
|
|
||||||
|
Rebuilds cache tables in one transaction. Failure → `502 woo_error`; previous
|
||||||
|
cache retained (never delete-then-fail).
|
||||||
|
|
||||||
|
**Manufactured stamps.** Woo REST has **no** usable last-modified on categories,
|
||||||
|
attributes, or tags. FastAPI persists `taxonomy_last_sync_at` (every successful
|
||||||
|
pull) and `taxonomy_anchor` (bump only when stored-subset hash changes, including
|
||||||
|
deletions). Client re-pulls the full taxonomy iff the anchor advanced. No Woo-side
|
||||||
|
taxonomy timestamp; no per-navigation sync.
|
||||||
|
|
||||||
|
**Triggers:** (1) successful `test-woo` **always** resyncs; (2) publish if
|
||||||
|
`now - taxonomy_last_sync_at > 1 hour` then reconcile (bump only on real change);
|
||||||
|
(3) optional app launch / Settings refresh. If a picked id vanished after a refresh, show that field as no longer valid. The selection is not silently swapped.
|
||||||
|
|
||||||
|
**Species exclusivity.** UI pickers, pricing seeds, and Call 1 species lists are
|
||||||
|
**exactly** the synced species-category leaves. No hardcoded, invented, or fallback
|
||||||
|
species set. A species new in Woo is selectable only after the next sync stores it.
|
||||||
|
|
||||||
|
### 2.3 What goes on the listing
|
||||||
|
|
||||||
|
When vision is on and it is at least 0.7 sure, it fills species, wood categories,
|
||||||
|
edge, figure, grade, and feature tags (character, inclusions, voids, checks map
|
||||||
|
to existing `feat-*` tags only). The mill owner can change every filled value.
|
||||||
|
Typed values always win.
|
||||||
|
|
||||||
|
If it is less sure, those fields stay empty. That empty-start subset is not
|
||||||
|
the submit list. Review will not continue until the full ready set is filled:
|
||||||
|
SKU, length, thickness, price, species, wood category, edge type, figure,
|
||||||
|
grade, thickness band, moisture, fig-* tags, and inventory PNG(s). Species
|
||||||
|
plus one figure is not enough. Presentation: `docs/UX.md`.
|
||||||
|
Low-confidence guesses are not filled.
|
||||||
|
|
||||||
|
Names from vision or from typing match store ids case-insensitive, trimmed. No
|
||||||
|
match leaves the field empty so they can pick from the list. The store will still
|
||||||
|
reject a missing required field at publish.
|
||||||
|
|
||||||
|
|
||||||
|
Field rules:
|
||||||
|
- **Categories**: species leaf (1 required) plus wood-category leaf ids (1+).
|
||||||
|
- **Tags**: `fig-*` (1+ required) and `feat-*` (0+). Only documented
|
||||||
|
character or species product tags are used. Woo is authoritative for tag definitions.
|
||||||
|
- **Attributes**: the five locked Woo attributes only — Edge Type, Figure, Grade,
|
||||||
|
Thickness, Moisture — using term ids from the cache. Species,
|
||||||
|
character, length, or free-text thickness as product attributes. Missing
|
||||||
|
attributes are skipped, not created (v1: no attribute creation).
|
||||||
|
|
||||||
|
Vision only suggests. It never blocks publish. With vision off, everything is typed.
|
||||||
|
|
||||||
|
### 2.4 Create payload
|
||||||
|
`POST /products` — body (only non-null fields sent):
|
||||||
|
```jsonc
|
||||||
|
{
|
||||||
|
"name": "{title}",
|
||||||
|
"type": "simple",
|
||||||
|
"status": "{woo_create_status}", // draft | publish; default draft; SLAB-UAT-* forced draft
|
||||||
|
"sku": "{sku}",
|
||||||
|
"regular_price": "{price}", // 2 dp string, e.g. "2730.00"
|
||||||
|
"description": "{description}",
|
||||||
|
"short_description": "{short_title}",
|
||||||
|
"categories": [ { "id": <species category id> } ],
|
||||||
|
"tags": [ { "id": <fig-* tag id> }, { "id": <feat-* tag id> }, … ],
|
||||||
|
"images": [
|
||||||
|
{ "src": "<data URL or pre-uploaded attachment URL>", "alt": "{species} {character} slab" }
|
||||||
|
],
|
||||||
|
"attributes": [ … if present … ],
|
||||||
|
"virtual": false,
|
||||||
|
"downloadable": false,
|
||||||
|
"catalog_visibility": "visible"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
**Image upload strategy** (Woo v3): `images[].src` accepts a URL or a data URL.
|
||||||
|
For reliability the server:
|
||||||
|
1. For each customer-facing inventory photo in `seq` order (processed transparent PNGs
|
||||||
|
from the client TECH-SPEC path): `POST /media` with the **PNG bytes** → get attachment
|
||||||
|
`source_url`. Woo accepts `image/*`; keep alpha. JPEG re-encoding drops alpha and must not happen.
|
||||||
|
2. Reference those `source_url` values in `images` (≤5 inventory photos).
|
||||||
|
3. Only `kind=inventory` photos. Calibration photos do not exist in this design.
|
||||||
|
|
||||||
|
**Status** comes from Settings `woo_create_status` (`draft` or `publish`, default
|
||||||
|
`draft`). Practice SKUs `SLAB-UAT-*` are always created as drafts, even if Settings
|
||||||
|
say publish. Existing SKUs are not edited in this version. After a successful create,
|
||||||
|
the server deletes the original and processed photos. A slab marked published in the
|
||||||
|
app means the store create succeeded. That is not the same as WooCommerce "publish."
|
||||||
|
The force-draft rule is in `AGENTS.md`. This file only owns the payload and the steps.
|
||||||
|
|
||||||
|
### 2.5 Duplicate SKU
|
||||||
|
Before create: `GET /products?sku={sku}` (any store status).
|
||||||
|
- Found: stop. The phone shows the SKU field with Edit SKU and Open existing listing. This version does not update the other product.
|
||||||
|
- Not found: create. Still do the check every time.
|
||||||
|
|
||||||
|
### 2.6 After a failed publish
|
||||||
|
|
||||||
|
The mill owner sees a message on the field, not a status number or a stack. The form stays on the phone. The server marks the slab failed so they can try again.
|
||||||
|
|
||||||
|
If the SKU already exists: under SKU, "This SKU already exists in the store (any status)." Edit SKU, or open the existing listing (admin link when we have it).
|
||||||
|
|
||||||
|
If a category, attribute, or price is no longer valid: under that field, "This value is no longer valid. Pick from the current list," with the current store options.
|
||||||
|
|
||||||
|
The server still writes `sync_log`. Success still sets `woo_product_id`, marks published locally, and deletes photos.
|
||||||
|
|
||||||
|
- **Partial-failure note** (risk, see IMPL-PLAN): if media uploads succeed but the
|
||||||
|
final product POST fails, orphaned Woo attachments may exist. v1: log it; surface
|
||||||
|
a manual cleanup note. Not auto-deleted.
|
||||||
|
|
||||||
|
## 3. End-to-end publish sequence (server side)
|
||||||
|
```
|
||||||
|
1. check the listing against the shared rules (current copy of the rules on submit)
|
||||||
|
2. look up the SKU in the store (any status). Stop if it already exists.
|
||||||
|
3. refresh store lists if the last successful pull is older than 1 hour. Only bump the client list-version if something actually changed.
|
||||||
|
4. resolve categories, tags, and attributes from the synced lists (vision names or typed names)
|
||||||
|
5. upload processed PNG inventory photos to media, keep the URLs
|
||||||
|
6. POST /products with draft/publish per Settings, always draft for SLAB-UAT-* SKUs
|
||||||
|
7. on success: log, mark published locally, delete originals and processed photos
|
||||||
|
on failure: log, mark failed, keep photos, put the error on the field (§2.6)
|
||||||
|
```
|
||||||
|
|
||||||
|
## 4. What the model may and may not do
|
||||||
|
|
||||||
|
Templates, numbers, matching names to store ids, payload, dedupe, logging: code.
|
||||||
|
Generate text may write description prose only. It must not invent dimensions. It may only pick species and attributes that exist in the synced store lists.
|
||||||
|
|
||||||
|
Presentation: `docs/UX.md`. The wire uses `{ field, code, message, actions[] }`. The mill owner never sees those codes. If the phone's copy of the rules is stale, refresh the copy and retry. That is not a duplicate-SKU conflict.
|
||||||
+19
-9
@@ -15,7 +15,7 @@ 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`).
|
- **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`.
|
- **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`. Product overview: `docs/PRODUCT.md`. Folder map: `docs/README.md`.
|
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)
|
## Preconditions (already shipped)
|
||||||
|
|
||||||
@@ -217,7 +217,7 @@ FastAPI proxies all inference calls. The browser never holds the key.
|
|||||||
|
|
||||||
### Templates (always available)
|
### Templates (always available)
|
||||||
|
|
||||||
Defaults from `main` CONTENT-WOO:
|
Full defaults and Woo payload text live in `docs/CONTENT-WOO.md`. Summary here:
|
||||||
|
|
||||||
**title**
|
**title**
|
||||||
|
|
||||||
@@ -225,6 +225,8 @@ Defaults from `main` CONTENT-WOO:
|
|||||||
{species} {length_ft}ft {length_in_remainder}in × {width_avg_in}in Wood Slab - {character}
|
{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**
|
**short_title**
|
||||||
|
|
||||||
```
|
```
|
||||||
@@ -241,6 +243,12 @@ Each slab is naturally shaped. Width varies along the length at {width_min_in}in
|
|||||||
{width_max_in}in. Sold by the slab; dimensions as measured. {geo_sentence}
|
{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.
|
All template math fields come from the deterministic pipeline, never from the LLM.
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -279,7 +287,7 @@ Mill-owner path after Photos-ready:
|
|||||||
"regular_price": "{price}",
|
"regular_price": "{price}",
|
||||||
"description": "{description}",
|
"description": "{description}",
|
||||||
"short_description": "{short_title}",
|
"short_description": "{short_title}",
|
||||||
"categories": [ { "id": <species and wood-category ids> } ],
|
"categories": [ { "id": <species leaf id> }, { "id": <wood-category leaf id> }, ... ],
|
||||||
"tags": [ { "id": <fig-* and feat-* ids> } ],
|
"tags": [ { "id": <fig-* and feat-* ids> } ],
|
||||||
"images": [ { "src": "<media source_url>", "alt": "{species} {character} slab" } ],
|
"images": [ { "src": "<media source_url>", "alt": "{species} {character} slab" } ],
|
||||||
"attributes": [ /* Edge, Figure, Grade, Thickness, Moisture term ids when present */ ],
|
"attributes": [ /* Edge, Figure, Grade, Thickness, Moisture term ids when present */ ],
|
||||||
@@ -404,17 +412,19 @@ Full request and response field lists on historical `main` `docs/OPENAPI.md` are
|
|||||||
|
|
||||||
## Open decisions (must lock before task breakdown)
|
## Open decisions (must lock before task breakdown)
|
||||||
|
|
||||||
These came from comparing `CREATE.md` to the technical requirements on `main`. Do not invent Woo API shapes to fill them.
|
Locked from Ty conversation or from `docs/CONTENT-WOO.md`:
|
||||||
|
|
||||||
1. **`categories` on `POST /products`.** Ready set requires species plus wood-category leaf ids. The `main` CONTENT-WOO example only shows a species id in `categories[]`. Confirm: send **all** ready-set category ids (species + every wood category) as `[{"id": n}, ...]` per Woo REST product `categories`.
|
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. **`attributes` element shape on `POST /products`.** `main` leaves `"attributes": [ … if present … ]` without fields. Need the exact Woo REST object fields for the five locked attributes (typically attribute `id`, `options` as term names, `visible`, `variation`). Prefer a live product sample from Whidbey Wood Store or the official Woo REST product-attribute docs Ty points us at. Do not guess.
|
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):** include in Woo upload. Order is (1) clean calibration PNG, (2) dimension-overlay copy of that calibration PNG, (3+) other inventory PNGs in the order they were added in the web app. Cap remains ≤5.
|
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.
|
||||||
|
|
||||||
4. **Template `{character}`.** Default title/short/description templates need a `{character}` string. Confirm the source when `feat-*` is empty, one tag, or many (for example first feat tag name, figure term, or a dedicated field).
|
Still open (CONTENT-WOO does not fully settle these):
|
||||||
|
|
||||||
5. **Server persistence.** `main` OpenAPI assumes a full server slab row and status machine. This branch still centers the device draft. Confirm M3 server persistence: full slab store as on `main`, or a thinner path (Settings + taxonomy + pricing + create from an uploaded ready package) with device draft as the client source of truth until create.
|
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.
|
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.
|
||||||
|
|
||||||
|
|||||||
@@ -48,6 +48,7 @@ Technical detail for Woo sync, metadata, pricing, and inference is copied into `
|
|||||||
| `CAPTURE.md` | Milestone 1 Capture (shipped). |
|
| `CAPTURE.md` | Milestone 1 Capture (shipped). |
|
||||||
| `PHOTOS.md` | Milestone 2 Photos (shipped). |
|
| `PHOTOS.md` | Milestone 2 Photos (shipped). |
|
||||||
| `CREATE.md` | Milestone 3 Create (current work). Read this before task breakdown or coding. |
|
| `CREATE.md` | Milestone 3 Create (current work). Read this before task breakdown or coding. |
|
||||||
|
| `CONTENT-WOO.md` | Listing copy templates and Woo create/payload detail (from `main`). |
|
||||||
| `UAT-PRACTICE.md` | Human practice walk for a `SLAB-UAT-*` listing. Defers to `CREATE.md` on conflict. |
|
| `UAT-PRACTICE.md` | Human practice walk for a `SLAB-UAT-*` listing. Defers to `CREATE.md` on conflict. |
|
||||||
| `archive/` | Historical only. Not M3 scope. |
|
| `archive/` | Historical only. Not M3 scope. |
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user