- PRD: 20 locked decisions, open items cleared, fence fixed - docs/TECH-SPEC-PIPELINE.md: deterministic math + 9 test vectors (deliverable #1) - docs/ARCHITECTURE.md, DATA-MODEL.md, OPENAPI.md (deliverable #2), CONTENT-WOO.md, DEPLOYMENT.md (deliverable #4), IMPL-PLAN.md - README rewritten; .gitignore added AGENTS.md pending Ty's approval (protected file write was blocked).
7.3 KiB
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 Optional LLM refinement (FR17, toggleable)
- Triggered only if
content_llm_enabledis true AND the user taps "Refine with AI" on the review screen (never automatic — FR18). - Prompt (server-side, assembled): system = brand voice + GEO context; user = the template-generated title/description + slab facts; task = "improve the title and description for a premium one-of-a-one wood slab listing. Keep all measurements exact. Do not invent features. Return JSON: {title, short_title, description}."
- Guardrail (deterministic): after the LLM returns, the server re-checks that every numeric dimension in the LLM text still matches the pipeline values (regex on numbers). Mismatch → reject the LLM output, keep the template version, log the rejection. This keeps measurement accuracy deterministic even when the LLM is on.
- Output is always editable (FR18);
content_source=llmwhen 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 (PRD "inference calls logged"), and the final stored content.
2. WooCommerce integration (FR24–28)
2.1 Auth & client
- REST v3:
GET/POST/PATCH/DELETE {woo_base_url}/wp-json/wc/v3/… - Basic auth:
woo_consumer_key:woo_consumer_secret(Decision 10; the "username" is the consumer key, the "app password" is the consumer secret). - Client:
httpxwith timeouts (connect 10s, read 60s for image-heavy create). woo_base_urldefault:https://www.whidbeywoodstore.com.
2.2 Taxonomy sync (FR25)
POST /api/admin/taxonomy/sync pulls, in order:
GET /products/categories?per_page=100(paginate) →woo_taxonomy(kind=category)GET /products/tags?per_page=100→kind=tagGET /products/attributes→kind=attributeRebuilds the cache table (delete + insert, single transaction). Returns counts. Failure →502 woo_error; previous cache retained (never delete-then-fail).
2.3 Auto-assign taxonomy (FR26)
- Category: species value is matched to a Woo category by name (case-insensitive,
trimmed). Match → that category id. No match → the slab's
category_override(user picks in review) or empty (Woo required-field validation will surface it). - Tags:
[character]plus optional[species]if present as a tag. - Attributes: species/character/length/thickness as product attributes if the corresponding Woo attributes exist in the cache (by name). Missing attributes are skipped, not created (v1: no attribute creation).
- Every assignment is overridable in the review screen (FR26).
2.4 Product create payload (FR27)
POST /products — body (only non-null fields sent):
{
"name": "{title}",
"type": "simple",
"status": "pending", // test-target decision: NEVER "publish" automatically
"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": <char 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:
- For each customer-facing (inventory, normalized) photo in
seqorder:POST /mediawith the JPEG bytes → get attachmentsource_url. - Reference those
source_urlvalues inimages(≤5, per PRD 1–5 inventory photos). - Calibration photos are excluded (FR2).
Status = pending by default (test-target decision: test products must never go
live without human action). The Settings key woo_publish_status (default pending)
controls this; Ty flips it to publish only when intentionally going live.
2.5 Dedupe & idempotency (FR24a)
Before create: GET /products?sku={sku}.
- Found →
409 duplicate_sku(v1 has no update path; human resolves). - Not found → proceed. (Single-owner, slab-by-slab, so no TOCTOU concern in practice; the check is still done for correctness.)
2.6 Sync result & logging (FR28)
- On success: set
slabs.woo_product_id,status=published,published_at, writesync_log(status=success, woo_product_id, payload_hash). - On Woo HTTP error:
status=failed,sync_log(status=failed, http_status, error, detail=<Woo error body>). Surface in UI + poll response. - 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; a manual cleanup note is surfaced. Not auto-deleted (could delete a real image).
3. End-to-end publish sequence (server side)
1. validate slab publishable (length,width,thickness,price,species,char,≥1 photo)
2. dedupe by SKU (GET /products?sku=) → 409 duplicate_sku if exists
3. (re)sync taxonomy if cache older than 1h → best-effort; use existing cache on fail
4. resolve category/tags/attributes (auto-assign + overrides)
5. upload normalized inventory images → media → source_urls
6. POST /products (status=pending) → woo_product_id
7. write sync_log(success), set slab published, purge images
on any failure at 2–6: write sync_log(failed), slab=failed, (images retained for retry)
4. What is deterministic vs scoped-inference here
- Deterministic: templates, all dimension text, category/tag/attribute resolution, payload assembly, dedupe, logging.
- Scoped inference: optional LLM title/description refinement only, with the numeric-accuracy guardrail (§1.2).