Files
SlabUploader/docs/CREATE.md
T
grokbot_ty 22599c924f docs(m3): expand Create SoT for submission-ready exit
Reorganize docs for humans: README map (goals / shipped / M3), PRODUCT
overview, and a specific CREATE.md that pulls Woo sync, metadata, pricing,
and inference requirements from main. Exit criteria is a slab submission-
ready for WooCommerce, proven with a SLAB-UAT-* draft. Add UAT-PRACTICE.
2026-09-16 21:03:46 +00:00

18 KiB
Raw Blame History

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. 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)

Defaults from main CONTENT-WOO:

title

{species} {length_ft}ft {length_in_remainder}in × {width_avg_in}in Wood Slab - {character}

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}

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 each inventory PNG (kind=inventory only) via POST /media, keep alpha. Reference source_url values on the product. Cap at 5 inventory photos. Calibration-only artifacts are not store images.
  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:

{
  "name": "{title}",
  "type": "simple",
  "status": "{woo_create_status}",
  "sku": "{sku}",
  "regular_price": "{price}",
  "description": "{description}",
  "short_description": "{short_title}",
  "categories": [ { "id": <species and wood-category ids> } ],
  "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

None blocking the exit definition above. If store-specific term labels differ from examples in this file, the synced store lists win.