Files
SlabUploader/docs/CONTENT-WOO.md
T
grokbot_ty d413e2f935 docs(#12,#14,#18): synced species, taxonomy anchor, 409/422 recovery
Species UI and Call 1 use only Woo-synced leaves. Middle tier manufactures a
taxonomy last-modified anchor (bump on stored-subset change only; test-woo
always resyncs; publish if >1h). CONTENT-WOO field-level 409/422 recovery.
IMPL acceptance and AGENTS locks updated.
2026-09-02 00:33:05 +00:00

222 lines
12 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.
# Content Generation & WooCommerce Integration
Status: SPEC · 2026-08-24 · SlabUploader
Covers: FR16 (deterministic content templates + optional LLM refinement), FR17
(brand voice / GEO), FR18 (editable, never auto-published), FR25–28 (taxonomy sync,
product create, sync log), and the exact WooCommerce REST v3 payload.
## 1. Content generation (FR16–18)
### 1.1 Deterministic templates (always on)
Templates are configured in Settings (`content_templates`, editable per FR32).
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` = GEO context sentence from Settings (FR17), 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 Call 2 content generation (toggleable)
- Triggered only if `content_llm_enabled` is true AND the user taps "Generate text"
on the review screen (never automatic).
- **LLM writes prose only**: the prompt sends the slab's deterministic measurements
as facts, and asks the model to write compelling product description prose
(character, color, warmth, grain, use cases). Dimensions are never generated
by the LLM — they are injected by deterministic templates.
- **Template assembly**: after the LLM returns, the server assembles the final
title, short description, and full description from deterministic templates
with the LLM prose injected into the description.
- No numeric guardrail needed — dimensions never pass through the LLM.
- Output is always editable (FR18); `content_source` = `llm` when used.
- If the inference endpoint is text-only or unreachable → the button is disabled,
templates still work. No failure mode blocks publish.
### 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 integration (FR24–28)
### 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). Do not reuse an admin account.
- 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 Taxonomy sync (FR25)
`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 anchor.** Woo REST has **no** usable last-modified on categories,
attributes, or tags. FastAPI manufactures one `taxonomy_updated_at` /
`cache_anchor` for the whole cache. Bump **only** when a row-level change hits the
**stored subset** of cache columns (schema-driven: new columns auto-enter the
diff). Catch adds, edits, and deletions. A no-op sync does **not** bump. Client
re-pulls the full taxonomy iff the anchor advanced past its last value. One
timestamp governs all metadata kinds. Do not invent a Woo-side taxonomy timestamp.
**Triggers:** (1) successful `test-woo` **always** resyncs; (2) publish path if last
successful sync older than **1 hour** (bump only on real change); (3) optional app
launch / Settings refresh. Not every navigation.
**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 Taxonomy assignment (FR26)
**Primary source when inference is on and confidence ≥ threshold (default 0.7):**
Call 1 vision results pre-fill the review screen for species, wood categories,
edge type, figure, grade, and mapped `fig-*` / `feat-*` tags (character /
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 **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
required-field validation still surfaces gaps at publish.
Field rules:
- **Categories**: species leaf (1 required) plus wood-category leaf ids (1+).
- **Tags**: `fig-*` (1+ required) and `feat-*` (0+). Do not invent generic
character or species product tags. 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. Do not publish species,
character, length, or free-text thickness as product attributes. Missing
attributes are skipped, not created (v1: no attribute creation).
Call 1 is assist-only and never gates publish. Inference OFF → full manual path.
### 2.4 Product create payload (FR27)
`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. Do not re-encode to JPEG and drop alpha.
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 key **`woo_create_status`** (`draft` | `publish`, default
**`draft`**). Woo **safety policy** (UAT draft default, `SLAB-UAT-*` force draft even when
Settings say publish, no edit-same-SKU, purge originals + processed images after successful
publish) is owned by **AGENTS.md §5**. This file owns payload shape and publish sequence
only. Do not diverge from AGENTS on force-draft. Do not conflate Woo create status with
slab lifecycle status `published` (means Woo create succeeded locally).
### 2.5 Dedupe & idempotency (FR24a)
Before create: `GET /products?sku={sku}` (any Woo status).
- Found → `409 duplicate_sku` (v1 has no update path).
- Not found → proceed. (Single-owner, slab-by-slab; check still required.)
### 2.6 Post-submission recovery (409 / 422) and logging (FR28)
Business-logic failures after the user taps publish map to **field-level recovery**
on the review screen. No raw HTTP codes or stack traces in the UI. The draft stays
on the phone across error-and-retry.
| Class | When | UI recovery |
|---|---|---|
| **409 Conflict** | SKU already exists in Woo, any status | Inline on the **SKU** field. Copy names the conflict. Two paths: **edit SKU** and retry, or **open the existing listing** (store admin URL / product id). Never "update in place" of the other product in MVP. |
| **422 Validation** | Stale or rejected value (category/attribute deleted since sync, price rule, etc.) | Inline on the **affected field**. Copy: value is no longer valid; show **current options** from the synced taxonomy cache (or re-pull if anchor advanced). |
Server still writes `sync_log` and sets slab `failed` with machine codes for pollers.
Success path unchanged: set `woo_product_id`, local `published`, purge images.
- **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. validate via shared validation module (hash check on submit; see ARCHITECTURE §8)
2. dedupe by SKU (GET /products?sku=, any status) → 409 duplicate_sku if exists
3. (re)sync taxonomy if last sync >1h; bump cache_anchor only on stored-subset change
4. resolve category/tags/attributes (Call 1 / manual + name-match; synced set only)
5. upload processed PNG inventory images → media → source_urls
6. POST /products (status per AGENTS §5 + woo_create_status) → woo_product_id
7. write sync_log(success), set slab published, purge originals + processed images
on failure: sync_log(failed), slab=failed, images retained; map 409/422 to fields (§2.6)
```
## 4. What is deterministic vs scoped-inference here
- Deterministic: templates, all dimension text, category/tag/attribute resolution
against the Woo-synced cache only, payload assembly, dedupe, logging, title assembly.
- Scoped inference: optional LLM prose generation for description only. The LLM
never touches dimensions and may only choose species/attributes present in the
synced taxonomy lists.
## 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.