diff --git a/AGENTS.md b/AGENTS.md index c2c893e..0748ea3 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -2,25 +2,25 @@ ## Agent context -Read this file, `docs/CAPTURE.md`, `docs/PHOTOS.md`, and `docs/CREATE.md`. -Implement **Milestone 3** only. Specification: `docs/CREATE.md`. -PRD overview: `docs/PRODUCT.md` for destination and field names. +Read `docs/README.md` first, then this file, `docs/CAPTURE.md`, `docs/PHOTOS.md`, and `docs/CREATE.md`. -**M3 scope SoT is `docs/CREATE.md` and the Ty conversations that produced it.** -Milestone / task breakdowns on `main` are obsolete for M3. Do not cross-reference `main` for scope once coding on this branch. +Implement **Milestone 3** only. Specification: `docs/CREATE.md`. +Product overview: `docs/PRODUCT.md`. + +**M3 scope** (what is in vs out) comes from Ty conversations and `docs/CREATE.md`. +**Behavior** for Woo sync, metadata, pricing, and inference is written into `docs/CREATE.md` from the technical requirements on `main`. Do not treat older milestone splits on `main` as scope authority. Do not code against `main` docs by cross-reference. Use `docs/CREATE.md` on this branch. Do not implement work outside `docs/CREATE.md` unless Ty expands scope in chat. Do not touch deploy port `:10880`. UAT redeploy target is overnight Docker on ubudual5060 `:10881`. ## Milestone 3 -Create — Woo attributes sync (WordPress application password), proper use of the synced taxonomy for the ready set and product create, plus OpenAI-compatible inference endpoint dependency in Settings. -Acceptance: see Done when in `docs/CREATE.md`. Practice SKU `SLAB-UAT-*` exists in the store as draft; create works with inference off. +Create. Exit criteria: a Photos-ready slab is **submission-ready for WooCommerce**, proven by a `SLAB-UAT-*` Woo draft that matches the reviewed ready set. Inference-off path required. -## Milestone 1–2 (shipped inputs) +## Milestone 1 and 2 (shipped inputs) -- Capture: `docs/CAPTURE.md` — calibration math and PNG contract. -- Photos: `docs/PHOTOS.md` — batch inventory PNGs + dimension overlay. Base tip for this branch: shipped M2 on `:10881`. +- Capture: `docs/CAPTURE.md` +- Photos: `docs/PHOTOS.md` (UAT on `:10881`) ## Invariants @@ -31,8 +31,8 @@ Acceptance: see Done when in `docs/CREATE.md`. Practice SKU `SLAB-UAT-*` exists - `bdft = sqft × thickness_in` - Price proposal is `bdft × species $/bdft`. User may replace it. Missing rate leaves price empty. - User-entered length, thickness, SKU, price, and taxonomy override computed or inferred values. -- **Woo attributes** = synced species + wood categories; Edge / Figure / Grade / Thickness / Moisture; `fig-*` / `feat-*`. Store is source of truth. -- Woo auth is WordPress application password (server-side). `WOO_BASE_URL` is compose/env; inference URL is Settings. +- **Woo attributes** means synced species and wood categories; Edge, Figure, Grade, Thickness, Moisture; `fig-*` and `feat-*`. Store is source of truth. +- Woo auth is WordPress application password (server-side). `WOO_BASE_URL` is compose/env. Inference URL is Settings. - SKUs matching `SLAB-UAT-*` create as Woo draft. - Create works with inference off. - Source below the PNG size floor requires a new photo. diff --git a/README.md b/README.md index 5cf252d..4021e12 100644 --- a/README.md +++ b/README.md @@ -2,12 +2,17 @@ Lists one-of-a-kind wood slabs on Whidbey Wood Store. +Start at [`docs/README.md`](docs/README.md) for goals, what already shipped, and Milestone 3. + | Artifact | File | |---|---| -| PRD | `docs/PRODUCT.md` | +| Docs map | `docs/README.md` | +| Product overview | `docs/PRODUCT.md` | | Invariants and agent context | `AGENTS.md` | -| Milestone 1 | `docs/CAPTURE.md` | -| Milestone 2 | `docs/PHOTOS.md` | +| Milestone 1 Capture (shipped) | `docs/CAPTURE.md` | +| Milestone 2 Photos (shipped) | `docs/PHOTOS.md` | +| Milestone 3 Create (current) | `docs/CREATE.md` | +| UAT practice walk | `docs/UAT-PRACTICE.md` | | Hosting | `deploy/` | Owner: Ty. Store: https://www.whidbeywoodstore.com/ diff --git a/docs/CREATE.md b/docs/CREATE.md index 5508c94..757272e 100644 --- a/docs/CREATE.md +++ b/docs/CREATE.md @@ -1,112 +1,407 @@ -# Milestone 3 — Create +# Milestone 3 Create -Builds on Milestones 1–2 (`docs/CAPTURE.md`, `docs/PHOTOS.md`). -Destination and field names also live in `docs/PRODUCT.md`. +## Purpose -**Scope source of truth:** this file and the Ty conversations that wrote it. -Task and milestone breakdowns on `main` (including older PRODUCT / IMPL-PLAN / CONTENT-WOO splits) are **obsolete for M3**. Do not cross-reference `main` for what M3 includes once this branch is the working set. +Milestone 3 takes a Photos-ready device draft and makes the slab **submission-ready for WooCommerce**. -## Goal +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. -Phone path from Photos-ready device draft → store-synced **Woo attributes** filled and reviewed → WooCommerce product create (practice SKUs as draft) → clear device draft. +**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. -M3 also wires the **OpenAI-compatible inference endpoint** as a server dependency so inference ops can run when enabled. Create must still work 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 and never invents the lists. +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) +### Categories (product categories, leaf only) -| Field | Cardinality | Notes | +| Field | Cardinality | Rule | |---|---|---| -| Species | exactly 1 required | Synced species leaves only; no hardcoded / fallback set | -| Wood category | 1+ required | Parents are grouping only; picks are leaves | +| 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 | Notes | +| Attribute | Cardinality | Rule | |---|---|---| -| Edge Type | 1 required | Term id from sync | -| Figure | 1+ required | Term ids from sync | -| Grade | 1+ required | Term ids from sync | -| Thickness | 1 required | Measured thickness rounded **up** into the store band | -| Moisture | 1 required | Default kiln-dried; user sets/keeps; **never inferred** | +| 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: missing attributes are skipped, not created. Species, character, length, and free-text thickness are **not** product attributes. +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 -| Tag prefix | Cardinality | Notes | +| Prefix | Cardinality | Rule | |---|---|---| -| `fig-*` | 1+ required | Other Woo tags ignored | -| `feat-*` | 0+ optional | Character / inclusions / voids / checks map onto existing feat tags only | +| `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. | -### Listing facts beside taxonomy +--- -- User: SKU, length, thickness (and app-computed widths / sqft / bdft from M1/M2) -- Price: propose `bdft × species $/bdft` from pricing rules; user may override; missing rate → empty price -- Copy: title / short title / description from templates (optional Generate text when inference is on; not required for ready) +## Auth and store URL -## Sync (Woo) +From `main` deploy and agents rules, adopted here: -- FastAPI is the sole Woo client. Credentials never in the browser. -- Auth: WordPress **application password** for a dedicated low-privilege WP user (not Woo `ck_`/`cs_` consumer keys). -- Store URL: `WOO_BASE_URL` from `deploy/.env` / compose (`https://` only). Settings shows it read-only. Not a SQLite settings row. -- Woo username + application password: Settings UI → AES-GCM in SQLite (`SLAB_AES_KEY` env). -- Taxonomy sync pulls, in order: categories → tags (`fig-*` / `feat-*` only) → attributes + terms (five locked). -- Sync failure keeps the previous cache (never delete-then-fail). Stale picks show “no longer valid”; never silently swap. +- 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`. -## Inference dependency +--- -- OpenAI-compatible endpoint: `inference_base_url`, API key, model in **Settings** (encrypted), not compose env. -- Server proxies all inference calls. -- Feature-flagged. **Create / ready path must pass with inference off** (manual taxonomy + deterministic numbers + Woo draft). -- Vision may prefill taxonomy at ≥0.7 confidence when on; typed values always win; below threshold → leave empty. +## Taxonomy sync -## Ready set (submission ready) +Endpoint and behavior from `main` OPENAPI / CONTENT-WOO / ARCHITECTURE: -Review does not continue until the full ready set is filled. Species + one figure is not enough. +`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: -- ≥1 inventory PNG (from M2) -- SKU, length, thickness; widths / sqft / bdft present -- Species, ≥1 wood category, edge type, ≥1 figure, ≥1 grade, thickness band, moisture, ≥1 `fig-*`, price -- `feat-*` optional (0+) +- 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) -## Create +Optional: -- `POST` product to Woo with categories, attributes, tags, price, copy, inventory PNG images. -- Duplicate SKU (any store status) blocks create; no update path in this version. -- `SLAB-UAT-*` always creates as Woo **draft**, even if Settings say publish. -- Device draft ≠ Woo draft. After successful create, clear the device draft. -- Default create status from Settings `woo_create_status` (`draft` \| `publish`, default `draft`), subject to the UAT force-draft rule. +- `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. -## Acceptance (done when) +Inline per-field nudges show empty required fields as the owner works. The slab cannot reach ready until the full set is filled. -Product state induced: overnight UAT app on ubudual5060 `http://100.64.0.9:10881` (or the current M3 deploy of this branch), with Woo credentials and `WOO_BASE_URL` pointed at the intended store. +Server status name for this state: `ready` (see data model below). Publish/create is blocked with `422 unpublishable` until then. -Evidence from that state: +--- -1. Taxonomy sync via application password retrieves Woo attributes into the app cache; pickers offer only synced ids. -2. A Photos-ready slab can be filled to the full ready set using those synced lists (inference off is a required path). -3. Create with `SLAB-UAT-*` yields a WooCommerce **draft** product whose categories / attributes / tags / price / 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 / model; create still succeeds with inference disabled. +## 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: + +```jsonc +{ + "name": "{title}", + "type": "simple", + "status": "{woo_create_status}", + "sku": "{sku}", + "regular_price": "{price}", + "description": "{description}", + "short_description": "{short_title}", + "categories": [ { "id": } ], + "tags": [ { "id": } ], + "images": [ { "src": "", "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 or in-process fake that never hit the real store -- Claiming ready / create green from unit tests alone without a real draft in the store -- Consumer-key auth instead of application password -- Inference “wired” only as dead config with no server proxy path, or create that requires inference on -- Touching `:10880` (Ty Node/SvelteKit) or dumping work onto `m2-ds` / doc-simplification branches +- 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. -## Out of scope (unless Ty expands M3 in chat) +## Open decisions -- Full Assist UX beyond the OpenAI endpoint dependency and optional prefill rules above -- Creating new Woo attributes / terms / tags from the app -- Update-in-place of an existing store SKU -- Bulk create +None blocking the exit definition above. If store-specific term labels differ from examples in this file, the synced store lists win. diff --git a/docs/PHOTOS.md b/docs/PHOTOS.md index 375726b..65ba246 100644 --- a/docs/PHOTOS.md +++ b/docs/PHOTOS.md @@ -70,6 +70,6 @@ On a phone UAT build: ## Out of scope - Complete-listing fields (SKU, species, category, edge, figure, grade, thickness band, moisture, `fig-*` / `feat-*`, title, short title, description, price) -- WooCommerce create (Milestone 3) -- Inference / Assist (Milestone 4) +- WooCommerce create and submission-ready taxonomy (Milestone 3: `docs/CREATE.md`) +- Work beyond `docs/CREATE.md` unless Ty expands M3 in chat - Changing Milestone 1 mask/axis/measure math except to reuse its outputs for batch apply and overlays diff --git a/docs/PRODUCT.md b/docs/PRODUCT.md index e87e580..2ba2ef0 100644 --- a/docs/PRODUCT.md +++ b/docs/PRODUCT.md @@ -1,4 +1,4 @@ -# Product requirements +# Product overview SlabUploader creates one WooCommerce listing per wood slab for Whidbey Wood Store. @@ -6,8 +6,12 @@ Each slab is one unit and one listing. Photos and metadata are the inventory rec Photographs are taken of the slab on a green or black sheet. That is the capture starting state. +Owner: Ty. Store: https://www.whidbeywoodstore.com/ + ## Complete listing +A finished listing has: + - Inventory photo(s), uniform 3:4 transparent PNG - SKU, length, thickness, price - Species, wood category, edge type, figure, grade @@ -16,7 +20,9 @@ Photographs are taken of the slab on a green or black sheet. That is the capture - Title, short title, description - Zero or more `feat-*` tags -Store categories, attributes, and `fig-*` / `feat-*` tags are the filters. Measured values feed title and description templates. +Store categories, attributes, and `fig-*` / `feat-*` tags are the filters shoppers use. Measured values feed title and description templates. + +The locked name for that synced taxonomy package is **Woo attributes**. Details live in `docs/CREATE.md`. ## Decision rights @@ -30,41 +36,33 @@ Store categories, attributes, and `fig-*` / `feat-*` tags are the filters. Measu | Price | App proposal `bdft × species $/bdft`; user may replace | | Title, short title, description | Templates from listing facts; user may edit | +User-entered length, thickness, SKU, price, and taxonomy override computed or inferred values. + ## System seams -Milestones 1 and 2 run in the browser. No backend and no inference URL. +Milestones 1 and 2 run in the browser. No backend and no inference URL on the shipped path. + Milestone 1 handoff is the device draft: source photo, approved mask, inventory PNG, length, thickness, widths, sqft, bdft. + Milestone 2 extends the draft with the photo batch, calibration choice, per-photo inventory PNGs, and the dimension-overlay calibration copy. -Milestone 3 uses FastAPI as the Woo client. Store credentials stay on the server. -## Create +Milestone 3 introduces FastAPI as the Woo client and inference proxy. Store credentials and inference secrets stay on the server. Spec: `docs/CREATE.md`. -Spec: `docs/CREATE.md` (Milestone 3 SoT on this branch). +## Create rules (summary) -`SLAB-UAT-*` creates as a WooCommerce draft (store status). -Duplicate SKU blocks create. -A listing can be created with inference off. -A device draft in the app is separate from a WooCommerce draft. -Woo attributes (synced taxonomy) and the OpenAI-compatible inference Settings dependency are defined in `docs/CREATE.md`. +Full behavior is in `docs/CREATE.md`. -## Milestones +- `SLAB-UAT-*` creates as a WooCommerce draft (store status). +- Duplicate SKU blocks create. +- A listing can be created with inference off. +- A device draft in the app is separate from a WooCommerce draft. -M1 and M2 are shipped inputs. **M3 scope is defined in `docs/CREATE.md` and Ty conversations — not by older splits on `main`.** +## Milestone status on `m3-ds` -### Milestone 1 — Capture +| Milestone | Spec | Status | +|---|---|---| +| 1 Capture | `docs/CAPTURE.md` | Shipped | +| 2 Photos | `docs/PHOTOS.md` | Shipped (UAT `:10881`) | +| 3 Create | `docs/CREATE.md` | Current work. Exit: submission-ready for WooCommerce (`CREATE.md` Done when). Scope from Ty conversations; behavior for sync/metadata/pricing/inference taken from `main` into `CREATE.md`. | -Spec: `docs/CAPTURE.md`. Shipped; handoff into Milestone 2. - -### Milestone 2 — Photos - -Spec: `docs/PHOTOS.md`. Shipped on UAT `:10881`. No Woo create. No Complete-listing taxonomy in this milestone. - -### Milestone 3 — Create - -Spec: `docs/CREATE.md`. Current work on branch `m3-ds`. -Woo attributes sync via application password, ready-set use of that taxonomy, product create, inference endpoint dependency. -Acceptance: see Done when in `docs/CREATE.md`. - -### Later work - -Anything beyond `docs/CREATE.md` (including fuller Assist UX) is out of M3 unless Ty expands scope in chat. +Anything beyond `docs/CREATE.md` is out of M3 unless Ty expands scope in chat. diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 0000000..e9339b4 --- /dev/null +++ b/docs/README.md @@ -0,0 +1,59 @@ +# Docs map (branch `m3-ds`) + +Read this file first. It tells you what SlabUploader is for, what already ships, and what Milestone 3 must build. + +**Who this is for.** Mill owner (Ty), humans reviewing scope, and agents implementing on this branch. + +**Source of truth for Milestone 3.** `docs/CREATE.md` plus the Ty conversations that wrote it. Older milestone and task splits on `main` are obsolete for M3. Do not open `main` for scope once you are coding on `m3-ds`. + +## High-level project goal + +SlabUploader lists one-of-a-kind wood slabs on Whidbey Wood Store (WooCommerce). + +One slab is one unit and one listing. Photos and metadata are the inventory record. The mill owner photographs a slab on a green or black sheet, gets store-ready transparent PNGs and measurements on the phone, fills the store's taxonomy and price, then creates a WooCommerce product. + +Store: https://www.whidbeywoodstore.com/ + +## What is already implemented (pre-M3) + +| Milestone | Spec | Status on this branch | +|---|---|---| +| 1 Capture | `docs/CAPTURE.md` | Shipped. Browser-only mask, length axis, measure, inventory PNG, device draft. | +| 2 Photos | `docs/PHOTOS.md` | Shipped. Batch photos, pick calibration, apply mask/crop to the batch, dimension overlay, Photos-ready lineup. UAT on `:10881`. | + +There is no Woo create yet. There is no Complete-listing taxonomy UI yet. There is no FastAPI Woo client on this branch (`backend/` is a stub). Inference is not wired. + +## What Milestone 3 builds + +Spec: `docs/CREATE.md`. + +**Exit criteria.** A Photos-ready slab becomes **submission-ready for WooCommerce**, and a practice `SLAB-UAT-*` draft exists in the store that matches the reviewed ready set. Create must still work with inference off. + +In plain terms, M3 connects the Photos-ready device draft to the real store: + +1. Talk to WooCommerce with a WordPress application password (server-side only). +2. Sync and use **Woo attributes** (categories, five locked attributes, `fig-*` / `feat-*` tags) per the requirements taken from `main` and written into `CREATE.md`. +3. Pricing from species `$/bdft` rules, with user override. +4. Inference Settings (OpenAI-compatible URL, key, model) and Call 1 / Call 2 rules when enabled. +5. Review to the full ready set, then create the Woo product (force draft for `SLAB-UAT-*`). + +Technical detail for Woo sync, metadata, pricing, and inference is copied into `docs/CREATE.md` from `main` so you do not need to open `main` while coding. Older milestone splits on `main` are still not the scope authority for what M3 includes. + +## How to read the folder + +| File | Role | +|---|---| +| `README.md` (this file) | Map: goals, shipped, M3. | +| `PRODUCT.md` | Product overview: complete listing, decision rights, seams. | +| `CAPTURE.md` | Milestone 1 Capture (shipped). | +| `PHOTOS.md` | Milestone 2 Photos (shipped). | +| `CREATE.md` | Milestone 3 Create (current work). Read this before task breakdown or coding. | +| `UAT-PRACTICE.md` | Human practice walk for a `SLAB-UAT-*` listing. Defers to `CREATE.md` on conflict. | +| `archive/` | Historical only. Not M3 scope. | + +## Deploy reminders for this branch + +- UAT overnight Docker: ubudual5060 `http://100.64.0.9:10881` +- Do not touch `:10880` (Ty Node/SvelteKit) +- Store URL (`WOO_BASE_URL`) lives in compose / host `.env` +- Woo application password and inference URL live in encrypted Settings, not in compose diff --git a/docs/UAT-PRACTICE.md b/docs/UAT-PRACTICE.md new file mode 100644 index 0000000..56af9f1 --- /dev/null +++ b/docs/UAT-PRACTICE.md @@ -0,0 +1,230 @@ +# UAT Practice Walk: one `SLAB-UAT-*` listing + +Status: WALK · for Ty to run · SlabUploader + +This is the walkable practice plan. It proves one listing end to end without touching a live product. +Follow the sections in order. Each one names what you do, what you should see, and where the rule +lives. + +**Inference is OFF for this walk.** The optional ON path is only valid after Gate C is green and +the inference endpoint is already configured. Inference stays off here. + +--- + +> On branch `m3-ds`, Milestone 3 exit criteria and field rules live in `docs/CREATE.md`. This walk is the human practice path for a `SLAB-UAT-*` listing. Prefer `CREATE.md` if anything conflicts. + + +## 1. Host and deploy preconditions + +Before this walk, the app must already be on the inventoried host. This section only checks that. +Host IPs and hostnames come from `deploy/INVENTORY.md`. + +1. **Inventory is filled.** `deploy/INVENTORY.md` has `APP_HOST`, `APP_HOSTNAME`, ports, Caddy + location, and the smoke results. It is the only source for what runs where on this host. + See `docs/DEPLOYMENT.md` §1 for the worksheet rules. +2. **Reachable over LAN/Tailscale.** Your phone reaches `https://${APP_HOSTNAME}` on the same + network (LAN or Tailscale). No public exposure. Verify with a browser, not `curl`. +3. **HTTPS only.** The URL is `https://`. The browser shows a valid certificate. If the phone + warns, the Caddy TLS trust is not set up yet. That is a deploy problem, not a listing problem. +4. **Health passes.** In a browser on the phone, open `https://${APP_HOSTNAME}/api/health`. + You should see an ok response. This is the same check in `docs/DEPLOYMENT.md` §8 step 1. +5. **Store URL is set.** `WOO_BASE_URL` is in the host `.env` and points at the store over + `https://`. It is not something you type in the app. See `docs/DEPLOYMENT.md` §3 and §5. + +If any of these fail, fix the deploy before continuing. This walk does not cover Gate B host work. +It assumes the app is up and reachable. + +--- + +## 2. WordPress Application Password setup + +You do this once per store, in the store's WordPress admin. Do it before you open the app's +Settings. + +1. **Store is on HTTPS.** WordPress only offers Application Passwords over HTTPS. The store must + already be on TLS. If it is not, stop and fix the store first. +2. **Dedicated low-privilege user.** Create or use a WordPress user that can create and edit + products and upload media. The store admin login is not reused. A shop manager role or a custom + role with product create/edit plus media upload is enough. +3. **Generate the password.** Sign in as that user. Go to **Users → Profile → Application + Passwords**. Generate a password and label it "SlabUploader". Copy the password. WordPress + shows it once. +4. **Enter it in the app.** Open the app's Settings on the phone. Enter the WordPress username and + the application password. The server stores them encrypted. The browser does not keep them. +5. **Run `test-woo`.** In Settings, tap the test. It should report ok. This also resyncs the + store's species, categories, tags, and attributes into the app. If it fails, the username or + password is wrong, or the store is unreachable. + +Rules: this is a WordPress Application Password, not WooCommerce consumer keys (`ck_`/`cs_`). +The app never calls the store from the browser. FastAPI is the only caller. Rotate by revoking +the old password in the WP profile and entering a new one in Settings, then re-running +`test-woo`. Full lock: `AGENTS.md` §4 and §5, `docs/DEPLOYMENT.md` §5. + +--- + +## 3. Practice build rules + +These rules keep practice runs invisible to customers. They are locked. Changes are not made for +the walk. + +- **SKU prefix `SLAB-UAT-*`.** Every practice listing uses a SKU that starts with `SLAB-UAT-`. + Pick a unique one each time, for example `SLAB-UAT-001`. +- **Always a Woo draft.** A `SLAB-UAT-*` SKU creates a WooCommerce draft even if Settings say + publish. Customers cannot open it. This is hard rule in `AGENTS.md` §5. +- **`woo_create_status` defaults to draft.** The Settings field that controls create status + defaults to `draft`. Practice uses the default. If it is `publish`, the + `SLAB-UAT-*` rule still forces a draft. +- **Customers never see practice listings.** A Woo draft is hidden from the storefront. If you + can see the product on the live store, something is wrong. + +If a practice SKU is not `SLAB-UAT-*`, it is not a practice run. Real SKUs are not published in +this walk. + +--- + +## 4. Prove one listing end to end + +This is the main walk. Run it on a phone browser first, then repeat on a PC browser. Both are in +the signed compatibility matrix (`docs/UX.md`, signed 2026-09-06). + +Supported browsers: iOS Safari, Android Chrome on phones; desktop Chrome, Edge, Firefox on PC. +Exact version pins are tracked under issue #4. If the browser is not supported, the app shows a +hard stop. Switch to a supported browser. + +### 4.1 Capture + +1. Open the app on the phone. Start a new slab. +2. Take 1 to 5 photos of the slab. The first should be top-down, lens parallel to the broad + face. A tilted shot makes the numbers wrong. Retake if it is tilted. +3. **Mask overlay and four knobs.** The app draws a mask over the photo. You see which pixels are + slab and which are not. Tune the four knobs: **Sheet** (auto/green/black), **Sensitivity**, + **Edge offset**, **Feather**. Each change re-runs the mask immediately. Look at the edge + against the real wood. If it is still off, retake the photo. +4. **Confirm the mask.** Tap confirm. +5. **Length axis.** The app draws the bounding rectangle and the length axis. Confirm it follows + the slab's long direction. Rotate if it is skewed. On a nearly square slab, you say which way + is length. +6. **Enter numbers by hand.** Length in 1/8" steps. Thickness as a 1/8" pick. SKU: type + `SLAB-UAT-001` (or your chosen practice SKU). +7. **Check the computed numbers.** The browser shows square feet, board feet, and width samples. + Board feet = square feet × thickness in inches. No divide-by-12. If the numbers look wrong, + go back and fix the mask or the length. +8. **Continue.** This uploads the originals, the processed PNGs, and the numbers to the server. + The slab status becomes `calibrated`. + +### 4.2 Review + +1. You are on the review screen. Fill every required field. Inference is OFF, so everything starts + empty. +2. **Species.** Pick exactly one from the synced store list. The list came from `test-woo`. + Only species in the list may be typed. +3. **Wood categories.** Pick at least one from the synced list. +4. **Edge type, figure, grade.** Pick from the synced lists. At least one figure, at least one + grade. +5. **Thickness band.** Pick the Woo band that the measured thickness rounds up into. +6. **Moisture.** Keep the default (kiln-dried) or change it. +7. **fig-* tags.** Pick at least one. +8. **feat-* tags.** Zero or more. Only pick existing `feat-*` tags. +9. **Price.** A recommendation fills from the species $/bdft rule times board feet. You can + override it. If there is no rule for the species, the price starts empty. Type it. Publish + is still allowed with a manual price. +10. **Title and description.** You can type them, or leave them for templates at publish. + Templates inject the deterministic numbers. Generate text is OFF for this walk. + +Every empty required field shows an inline nudge where it sits. The slab cannot reach `ready` +until the full set is filled. + +### 4.3 Publish + +1. Tap **Publish**. +2. The server checks the SKU against the store. If `SLAB-UAT-001` is new, it creates the + product. +3. **What you should see:** a success message on the screen. The local draft and its photos are + gone from the app. The slab status in the app is `published`. +4. **What that does NOT mean:** the product is not live in the store. It is a WooCommerce draft. + The app's "published" means the store create succeeded and the local row was cleaned up. + +### 4.4 Confirm in the store + +1. Open the store's WooCommerce admin. +2. Find the product by SKU `SLAB-UAT-001`. +3. **It must be a draft.** Status is draft, not published. +4. **Check the fields:** SKU, price, category (species), tags (fig-*, feat-*), attributes + (Edge Type, Figure, Grade, Thickness, Moisture), and images (the processed transparent PNGs). +5. **Open the live storefront.** The product must not appear. A draft is invisible to customers. + +### 4.5 Repeat on a PC browser + +Repeat 4.1 through 4.4 on a desktop browser (Chrome, Edge, or Firefox). Use a different +practice SKU, for example `SLAB-UAT-002`. The flow must work the same way. This is the signed +PC-browser compatibility check. + +--- + +## 5. Pass and fail checks + +You can see all of these without reading code. + +| Check | Where | Pass looks like | +|---|---|---| +| Health endpoint | Phone browser, `https://${APP_HOSTNAME}/api/health` | ok response | +| `test-woo` ok | App Settings | ok; species list is populated | +| `woo_create_status` is draft | App Settings | shows `draft` (or `publish`, but `SLAB-UAT-*` forces draft anyway) | +| Slab reaches `calibrated` | App, after capture | status is `calibrated` | +| Slab reaches `ready` | App, after review | no empty required fields; inline nudges gone | +| Publish succeeds | App, after Publish | success message; local draft is gone | +| Woo product exists as draft | Store admin, products | SKU `SLAB-UAT-*`, status draft | +| SKU prefix correct | Store admin | starts with `SLAB-UAT-` | +| Store not live | Live storefront URL | product does not appear | +| Board feet identity | App review screen, or a fixture | bdft = sqft × thickness in inches; no divide-by-12 | +| Images are processed PNGs | Store admin, product images | transparent PNGs, not the original JPEGs | + +### Duplicate SKU recovery + +If you publish the same SKU twice, the second time the app stops before creating a second product. +Under the SKU field you see: "This SKU already exists in the store (any status)." Two paths: +edit the SKU to a new value, or open the existing listing. The draft stays in the app. Nothing is +lost. This is the expected behavior, not a bug. See `docs/CONTENT-WOO.md` §2.5. + +### What a fail looks like + +- **No success message after Publish.** The app shows a plain message that the listing could not + be created. No status number, no stack trace. The server kept a log. You can retry from the + same screen without re-photographing. +- **A value is no longer valid.** If a category, attribute, or price was removed from the store + between sync and publish, that field shows "This value is no longer valid. Pick from the + current list." Re-pick and retry. +- **Store unreachable.** A plain message says the store could not be reached. Check the network + and retry. + +--- + +## 6. Cleanup + +After the walk, decide what to do with the Woo draft UAT product. + +- **Leave it or delete it.** Both are fine. It is a draft. Customers cannot see it. If you delete + it, the store admin is the place to do it. The app does not delete store products. +- **Never customer-visible.** If the product is visible on the live storefront, it is not a + draft. That is a bug. Stop and report it. + +**Real live publish is a separate step.** It is not part of this walk. A real product uses a +non-UAT SKU (no `SLAB-UAT-` prefix) and is published only after you review it. That is a +different decision. That step is not run here. The name of that step is **final POC publish**, and it +is gated on your review of one real listing. + +--- + +## Where each rule lives + +| Rule | Source | +|---|---| +| Practice SKU forces draft | `AGENTS.md` §5 | +| `woo_create_status` default draft | `AGENTS.md` §5, `docs/CONTENT-WOO.md` §2.4 | +| App Password setup, not consumer keys | `AGENTS.md` §4, `docs/DEPLOYMENT.md` §5 | +| Host inventory before deploy | `docs/DEPLOYMENT.md` §1, `deploy/INVENTORY.md` | +| Capture → review → publish flow | `docs/UX.md` (SIGNED 2026-09-06) | +| Board feet identity | `docs/TECH-SPEC-PIPELINE.md` | +| Duplicate SKU recovery | `docs/CONTENT-WOO.md` §2.5, `docs/UX.md` Publish | +| PC browser compatibility | `docs/UX.md` Browser compatibility | +| Gate C = early Phase 1 | `docs/IMPL-PLAN.md` Gates map and Phase 1 | diff --git a/docs/archive/README.md b/docs/archive/README.md index 298473a..08d2fe1 100644 --- a/docs/archive/README.md +++ b/docs/archive/README.md @@ -19,3 +19,7 @@ Live spec on this branch: ## M3 note (2026-09-16) For Milestone 3 on branch `m3-ds`, do **not** treat docs frozen on `main` as scope SoT. Use `docs/CREATE.md` on `m3-ds` (conversation-locked). Archive / `main` files remain historical reference only. + +## M3 note (updated 2026-09-16) + +M3 exit on `m3-ds` is submission-ready for WooCommerce (`docs/CREATE.md`). Technical requirements for Woo sync, metadata, pricing, and inference were taken from historical `main` docs and written into `CREATE.md`. Do not use `main` milestone splits as M3 scope.