docs(phase2): reconcile requirements and migrate capabilities/contracts
Surface-matrix sweep of every source doc; added nine missed behaviors (METADATA-115, REVIEW-50, APP-55/60/65, SETTINGS-45 mode, Generate-text availability, defaults-off). 153 requirements, 0 coverage gaps. Migrated capabilities to docs/capabilities/ and contracts to docs/contracts/; added WOO-API, API, DATA-MODEL, PROMPTS, UX, TEST-SPEC. Populated MILESTONES; branch-neutral trunk docs; renamed umbrella term to Woo Slab Taxonomy. Archived reconciliation working records under .audit/archive/.
This commit is contained in:
@@ -0,0 +1,191 @@
|
||||
# App API contract — client ↔ FastAPI
|
||||
|
||||
Status: SPEC · 2026-09-18.
|
||||
Base path `/api/v1`. Health at `/api/health`. All JSON unless noted. Timestamps UTC ISO-8601.
|
||||
The outbound WooCommerce payload is owned by `docs/contracts/WOO-API.md`.
|
||||
|
||||
## Requirements impact
|
||||
|
||||
| Requirement | Impact on this design |
|
||||
|---|---|
|
||||
| APP-5 | slab status values and transitions |
|
||||
| APP-20 | client ids, UTC timestamps, `client_rev`/`server_rev` |
|
||||
| APP-25 | FastAPI is the only Woo and inference caller |
|
||||
| APP-30 | `/api/v1` base path, `/api/health` |
|
||||
| APP-35 | error envelope, full lists, JSON |
|
||||
| APP-40 | server stores client numbers, never recomputes |
|
||||
| APP-45 | health body and non-200 condition |
|
||||
| APP-50 | upstream Woo 4xx mapped into the envelope |
|
||||
| CAPTURE-15, CAPTURE-165 | offline and U2Net are out of scope |
|
||||
| CAPTURE-25, CAPTURE-30, CAPTURE-115, CAPTURE-130 | calibration choice and batch photos |
|
||||
| CAPTURE-70, CAPTURE-80, CAPTURE-85, CAPTURE-90 | length/thickness units and derived numbers |
|
||||
| CAPTURE-95, CAPTURE-105, CAPTURE-135, CAPTURE-141, CAPTURE-145, CAPTURE-150, CAPTURE-155 | PNG output, upload, preview/controls |
|
||||
| METADATA-5, METADATA-10, METADATA-15, METADATA-30 | taxonomy cardinality and scoping |
|
||||
| METADATA-35, METADATA-40, METADATA-45, METADATA-50, METADATA-55 | Call 1 / Call 2 |
|
||||
| METADATA-60, METADATA-65, METADATA-70, METADATA-75, METADATA-100, METADATA-105, METADATA-110, METADATA-115 | context, constraints, fallback, prompts, failure |
|
||||
| OPS-55 | no client auth |
|
||||
| PUBLISH-5, PUBLISH-10, PUBLISH-15, PUBLISH-20 | create sequence, draft rule, duplicate, stale |
|
||||
| PUBLISH-25, PUBLISH-30, PUBLISH-31, PUBLISH-35, PUBLISH-40, PUBLISH-45, PUBLISH-50, PUBLISH-55, PUBLISH-60 | create outcomes, images, payload |
|
||||
| REVIEW-5, REVIEW-10, REVIEW-15, REVIEW-20, REVIEW-25, REVIEW-30, REVIEW-40, REVIEW-45, REVIEW-50 | ready set, fields, limits |
|
||||
| SETTINGS-5, SETTINGS-10, SETTINGS-15, SETTINGS-20, SETTINGS-25, SETTINGS-30 | env URL, credentials, crypto |
|
||||
| SETTINGS-35, SETTINGS-40, SETTINGS-45, SETTINGS-50, SETTINGS-51, SETTINGS-55, SETTINGS-60, SETTINGS-65, SETTINGS-75, SETTINGS-80, SETTINGS-85, SETTINGS-90 | rates, tests, taxonomy, flags |
|
||||
|
||||
## 1. Conventions
|
||||
|
||||
- **Auth:** no client auth; the app relies on the trusted LAN/Tailscale network plus HTTPS.
|
||||
- **Envelope:** every error is `{ "error": { "code": string, "message": string, "detail": object|null } }`.
|
||||
- **Lists:** returned in full, no pagination.
|
||||
- **Revisions:** create/update carry `client_rev`; the server echoes `server_rev`; a mismatch is
|
||||
`409 revision_conflict`.
|
||||
- **IDs and time:** slab ids are client-generated UUIDv4; timestamps are UTC ISO-8601.
|
||||
- **Photos:** `multipart/form-data` with `data` (JSON), `files[]` (original images), and `meta[]`
|
||||
(`{ "seq": n }`). One photo is the calibration photo, chosen by the owner.
|
||||
- **Secrets:** never returned; only a configured boolean.
|
||||
- **Error display:** raw status codes, stack traces, and internal codes never reach the owner.
|
||||
|
||||
## 2. Error codes
|
||||
|
||||
| Status | Code | Meaning |
|
||||
|---|---|---|
|
||||
| 400 | `validation` | payload failed validation |
|
||||
| 404 | `not_found` | unknown slab, photo, or setting |
|
||||
| 409 | `revision_conflict` | `client_rev`/`server_rev` mismatch; `detail.current_server_rev` |
|
||||
| 409 | `duplicate_sku` | SKU exists in Woo under any status |
|
||||
| 422 | `unpublishable` | slab not publishable or missing a required field |
|
||||
| 422 | `stale_field` | a picked taxonomy id vanished; returns current options |
|
||||
| 502 | `woo_error` | WooCommerce rejected the request; `detail` carries the Woo body |
|
||||
| 500 | `internal` | unexpected |
|
||||
|
||||
There is no `412 validation_module_stale` path.
|
||||
|
||||
## 3. Health and meta
|
||||
|
||||
- `GET /api/health` → `{ "status": "ok", "version": string, "woo_reachable": bool }`. Non-200 when
|
||||
the database or image volume is not writable.
|
||||
- `GET /api/v1/meta` → `{ "version": string }`.
|
||||
|
||||
## 4. Slabs
|
||||
|
||||
### `POST /api/v1/slabs`
|
||||
Multipart create of a calibrated draft. Returns `201` with the slab at `status: "calibrated"`.
|
||||
Stores the client-sent derived numbers and never recomputes them. Auto-runs Call 1 only when
|
||||
`inference_enabled` is true; inferred values persist only at confidence ≥ 0.7. `409 duplicate_sku`
|
||||
if the SKU already exists.
|
||||
|
||||
`SlabCreate` fields:
|
||||
|
||||
| Field | Type | Notes |
|
||||
|---|---|---|
|
||||
| `id` | uuid | client-generated |
|
||||
| `sku` | string | 3–24, alphanumeric and hyphen |
|
||||
| `length_in`, `thickness_in` | number | 1/8 inch steps |
|
||||
| `sqft`, `bdft`, `width_min_in`, `width_max_in`, `width_avg_in` | number | client-computed |
|
||||
| `species_id` | int | one species leaf |
|
||||
| `wood_category_ids` | int[] | one or more |
|
||||
| `edge_type_term_id` | int | one |
|
||||
| `figure_term_ids` | int[] | one or more |
|
||||
| `grade_term_id` | int | exactly one |
|
||||
| `thickness_term_id` | int | round-up band |
|
||||
| `moisture_term_id` | int | default kiln-dried |
|
||||
| `fig_tag_ids` | int[] | optional |
|
||||
| `feat_tag_ids` | int[] | optional |
|
||||
| `price` | number | 2 decimals |
|
||||
| `price_source` | string | `recommendation` \| `override` |
|
||||
| `title`, `short_description`, `description` | string\|null | prose |
|
||||
| `content_source` | string | `manual` \| `llm` \| `template` |
|
||||
| `client_rev` | int | optimistic concurrency |
|
||||
|
||||
Grade, edge, thickness, and moisture are singular; figure and the tag lists are arrays.
|
||||
|
||||
### `GET /api/v1/slabs`
|
||||
Returns `[Slab]`, most recent first.
|
||||
|
||||
### `GET /api/v1/slabs/{id}`
|
||||
Returns the `Slab`. Also the poll endpoint for inference and publish. The client backs off
|
||||
(1s, 2s, 4s, cap 10s).
|
||||
|
||||
### `PUT /api/v1/slabs/{id}`
|
||||
Partial update of editable fields plus `client_rev`. Stores client-sent derived fields and does
|
||||
not recompute them. `409 revision_conflict`, `409 duplicate_sku` (SKU field actions `edit_sku` /
|
||||
`open_existing`), `422 stale_field` (current options included).
|
||||
|
||||
### `DELETE /api/v1/slabs/{id}`
|
||||
Soft delete of unpublished drafts only; `204`. Published slabs cannot be deleted from the app.
|
||||
|
||||
## 5. Photos
|
||||
|
||||
- `GET /api/v1/slabs/{id}/photos/{pid}/original` → `image/*` while retained.
|
||||
- `GET /api/v1/slabs/{id}/photos/{pid}/processed` → `image/*` (transparent PNG) while retained.
|
||||
- After publish both return `410 Gone`.
|
||||
- `POST /api/v1/slabs/{id}/u2net/{pid}` is reserved post-POC and is not exposed.
|
||||
|
||||
## 6. Inference
|
||||
|
||||
### `POST /api/v1/slabs/{id}/infer-taxon`
|
||||
Runs or retries Call 1. Sends the clean processed photos at 1024 px long edge or less, the synced
|
||||
taxonomy, and the user metadata. Returns `202`; the client polls. A failure or timeout follows the
|
||||
runtime failure rule (recoverable offers Retry, unrecoverable explains).
|
||||
|
||||
### `POST /api/v1/slabs/{id}/infer-content`
|
||||
Runs Call 2 on user action only. Fresh request carrying the full Call 1 context; no provider
|
||||
thread id. Requires `test-inference` `ok`. Returns `202`; the client polls. The model writes prose
|
||||
only and never touches dimensions.
|
||||
|
||||
Call 1 result fields: `species_id`, `species_confidence`, `wood_category_ids`,
|
||||
`edge_type_term_id`, `figure_term_ids`, `figure_confidence`, `grade_term_id`, `grade_confidence`,
|
||||
`character`, `inclusions`, `voids`, `checks`. Free-text observations map to synced `feat-*` tags
|
||||
only.
|
||||
|
||||
Call 2 result fields: `title`, `short_description`, `description`.
|
||||
|
||||
## 7. Publish
|
||||
|
||||
### `POST /api/v1/slabs/{id}/publish`
|
||||
Preconditions are the full ready set, else `422 unpublishable` with field keys. Sequence: validate
|
||||
the ready set; look up the SKU and stop if present; refresh taxonomy when the last sync is over
|
||||
one hour old; resolve categories, tags, and attributes; upload the processed PNGs to Woo media;
|
||||
`POST /products` per `docs/contracts/WOO-API.md`; finalize. `SLAB-UAT-*` always creates a draft.
|
||||
Returns `202`; the client polls.
|
||||
|
||||
- Success: `woo_product_id` set, photos purged, local slab row deleted, device draft cleared.
|
||||
- Failure: attempt marked failed, photos retained, `sync_log` written, plain message on the
|
||||
affected field or action, retry from the same screen.
|
||||
- Partial failure (media uploaded, product POST failed): orphaned media left, logged, manual
|
||||
cleanup note surfaced.
|
||||
|
||||
## 8. Taxonomy
|
||||
|
||||
- `GET /api/v1/admin/taxonomy` → grouped categories, attributes with terms, and tags, plus
|
||||
`taxonomy_anchor` and `taxonomy_last_sync_at`. Only fig-* and feat-* tag families are kept.
|
||||
These lists are the only legal picker options.
|
||||
- `POST /api/v1/admin/taxonomy/sync` → pulls from Woo, rebuilds the cache in one transaction, sets
|
||||
`taxonomy_last_sync_at`, bumps `taxonomy_anchor` only when the stored-subset hash changes, and
|
||||
on failure returns `502` while keeping the previous cache.
|
||||
|
||||
## 9. Pricing
|
||||
|
||||
- `GET /api/v1/admin/pricing` → `[PriceRule]`
|
||||
- `POST /api/v1/admin/pricing` → `201`
|
||||
- `PUT /api/v1/admin/pricing/{id}` → `200`
|
||||
- `DELETE /api/v1/admin/pricing/{id}` → `204`
|
||||
|
||||
One flat `price_per_bdft` per species leaf, no tiers.
|
||||
|
||||
## 10. Settings
|
||||
|
||||
- `GET /api/v1/settings` → `SettingsView` with secrets masked.
|
||||
- `PUT /api/v1/settings` — partial update. `woo_base_url` is not writable; the server rejects the
|
||||
write. Woo credentials are write-only and stored AES-GCM. Inference base URL and key are
|
||||
encrypted; flags, brand voice, and geo context are settings. Secrets are never echoed.
|
||||
- `POST /api/v1/settings/test-woo` → `{ "ok": bool, "detail": string, "taxonomy_anchor": string,
|
||||
"taxonomy_last_sync_at": string }`. Fails closed on missing credentials or non-HTTPS; an auth
|
||||
rejection is distinct from an unreachable store; on success always resyncs.
|
||||
- `POST /api/v1/settings/test-inference` → `{ "ok": bool, "mode": "stateful"|"stateless"|"fail",
|
||||
"vision_capable": bool, "detail": string }`. `fail` blocks Call 2 only.
|
||||
|
||||
## 11. Removed from v1
|
||||
|
||||
- The shared validation module and its `412` path.
|
||||
- Provider thread ids and `previous_response_id` (Call 2 is a fresh full-context call).
|
||||
- The U2Net control; the endpoint is reserved only.
|
||||
- Offline/IndexedDB sync.
|
||||
@@ -0,0 +1,86 @@
|
||||
# Listing copy and payload intent
|
||||
|
||||
Status: SPEC · 2026-09-18.
|
||||
Owns how listing copy is produced and the intent of the create payload. The wire shape is
|
||||
`docs/contracts/WOO-API.md`; the create sequence is `docs/contracts/API.md`; prompts are
|
||||
`docs/contracts/PROMPTS.md`.
|
||||
|
||||
## Requirements impact
|
||||
|
||||
| Requirement | Impact on this design |
|
||||
|---|---|
|
||||
| REVIEW-10, REVIEW-15, REVIEW-40, REVIEW-50 | required prose, primary category, editability, limits |
|
||||
| METADATA-40, METADATA-55, METADATA-60, METADATA-65, METADATA-66 | model scope, Call 2, context, dimension safety, logging |
|
||||
| METADATA-70, METADATA-75, METADATA-80, METADATA-85, METADATA-90, METADATA-95, METADATA-110 | template fallback, non-blocking, prompt selection, fact block, overlay exclusion, control |
|
||||
| PUBLISH-5, PUBLISH-10, PUBLISH-15, PUBLISH-20, PUBLISH-25, PUBLISH-30, PUBLISH-31, PUBLISH-40, PUBLISH-45, PUBLISH-50, PUBLISH-55, PUBLISH-60 | payload and create outcomes |
|
||||
| SETTINGS-30, SETTINGS-65 | brand voice and geo context; create status |
|
||||
| METADATA-10, METADATA-15, METADATA-30 | categories, attributes, tags on the payload |
|
||||
|
||||
## 1. Copy sources
|
||||
|
||||
Titles and descriptions come from one of three sources (`REVIEW-10`):
|
||||
|
||||
1. **Typed** by the owner.
|
||||
2. **Model** when Call 2 runs; the owner triggers it (`METADATA-55`).
|
||||
3. **Template** as the fallback when Call 2 does not run (`METADATA-70`).
|
||||
|
||||
The template guides style; when the model runs it writes the prose (`METADATA-80`). The model
|
||||
writes prose only and never touches a dimension; every number comes from the dimension fact block
|
||||
(`METADATA-60`, `METADATA-65`, `METADATA-90`). The dimension-overlay image is never sent
|
||||
(`METADATA-95`). The owner can edit any result (`REVIEW-40`). All text respects the field limits
|
||||
in `docs/contracts/WOO-API.md` §9 (`REVIEW-50`).
|
||||
|
||||
The Generate text control appears only when content generation is enabled and the endpoint is
|
||||
reachable (`METADATA-110`); a failed generation never blocks create (`METADATA-75`). Every
|
||||
generation is logged (`METADATA-66`).
|
||||
|
||||
## 2. Default templates
|
||||
|
||||
Placeholders use `{field}`; all math fields come from the deterministic pipeline, never the model
|
||||
(`METADATA-90`).
|
||||
|
||||
**Title** (hyphen separator; `{character}` is the primary figure term name, `METADATA-80`):
|
||||
|
||||
```
|
||||
{species} {length_ft}ft {length_in_remainder}in x {width_avg_in}in Wood Slab - {character}
|
||||
```
|
||||
|
||||
`length_ft = floor(length_in / 12)`, `length_in_remainder = length_in mod 12` (1 dp).
|
||||
|
||||
**Short description:**
|
||||
|
||||
```
|
||||
{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}
|
||||
```
|
||||
|
||||
`geo_sentence` comes from Settings and is omitted when unset (`SETTINGS-30`). The primary wood
|
||||
category selects a specialized Call 2 prompt (`METADATA-85`).
|
||||
|
||||
## 3. Payload intent
|
||||
|
||||
The exact wire shape is `docs/contracts/WOO-API.md`. Intent:
|
||||
|
||||
- Only non-null fields are sent (`PUBLISH-55`).
|
||||
- `status` comes from `woo_create_status` (default draft); `SLAB-UAT-*` always draft
|
||||
(`SETTINGS-65`, `PUBLISH-10`).
|
||||
- Categories are the species leaf plus each wood-category leaf; attributes are the five locked
|
||||
attributes by synced term; tags are fig-* and feat-* (`METADATA-10`, `METADATA-15`,
|
||||
`METADATA-30`). Missing attributes or terms are skipped, never created (`METADATA-20`).
|
||||
- Images are the processed transparent PNGs in photos-ready order, with no cap (`PUBLISH-40`,
|
||||
`PUBLISH-45`).
|
||||
- `sold_individually` is true; each product is one slab (`PUBLISH-60`).
|
||||
- Create checks the SKU first and blocks on a duplicate (`PUBLISH-15`); a stale value is a
|
||||
field-level error (`PUBLISH-20`). Success purges photos and deletes the local row
|
||||
(`PUBLISH-25`); failure retains photos and logs (`PUBLISH-30`), including the orphaned-media
|
||||
cleanup note (`PUBLISH-31`). Published means the create succeeded, not that it is live
|
||||
(`PUBLISH-50`).
|
||||
@@ -0,0 +1,142 @@
|
||||
# Data model contract
|
||||
|
||||
Status: SPEC · 2026-09-18. Store: SQLite.
|
||||
|
||||
## Requirements impact
|
||||
|
||||
| Requirement | Impact on this design |
|
||||
|---|---|
|
||||
| APP-5 | slab status enum and transitions |
|
||||
| APP-20 | ids, timestamps, revisions |
|
||||
| APP-40 | derived numbers stored, never recomputed |
|
||||
| CAPTURE-25, CAPTURE-30, CAPTURE-70, CAPTURE-80, CAPTURE-85, CAPTURE-90, CAPTURE-95, CAPTURE-105, CAPTURE-115, CAPTURE-130, CAPTURE-135, CAPTURE-160 | slab and photo columns, units, PNG |
|
||||
| METADATA-5, METADATA-10, METADATA-15, METADATA-30 | taxonomy cache tables and slab taxonomy columns |
|
||||
| METADATA-35, METADATA-45, METADATA-55, METADATA-66, METADATA-105 | inference columns, prompts tables, logs |
|
||||
| PUBLISH-25, PUBLISH-30 | purge on success, append-only log |
|
||||
| REVIEW-10, REVIEW-25, REVIEW-45, REVIEW-50 | prose, price, SKU, field limits |
|
||||
| SETTINGS-5, SETTINGS-10, SETTINGS-20, SETTINGS-25, SETTINGS-30, SETTINGS-35, SETTINGS-50, SETTINGS-51, SETTINGS-55, SETTINGS-65, SETTINGS-90 | settings, crypto, rates, taxonomy stamps |
|
||||
|
||||
## 1. Conventions
|
||||
|
||||
- Slab ids are client-generated UUIDv4; all other ids are integer autoincrement.
|
||||
- Timestamps are UTC ISO-8601 text.
|
||||
- Slab status is one of `draft`, `calibrated`, `ready`, `publishing`, `published`, `failed`.
|
||||
- Inches are stored as the rounded 1/8-inch value the user entered.
|
||||
- `sqft` and `bdft` are stored to 2 decimals half-up.
|
||||
- `price` is stored to 2 decimals half-up.
|
||||
- Multi-select taxonomy values are JSON arrays of Woo ids.
|
||||
- Secrets are stored as `value_enc = nonce(12) || ciphertext`, AES-GCM; the master key is the env
|
||||
`SLAB_AES_KEY` and is never in the DB.
|
||||
- The local slab row is deleted on successful publish; `sync_log` and `inference_log` are
|
||||
append-only and retained.
|
||||
|
||||
## 2. `slabs`
|
||||
|
||||
| Column | Type | Constraint / default |
|
||||
|---|---|---|
|
||||
| `id` | text | PK, client UUIDv4 |
|
||||
| `status` | text | enum, default `draft` |
|
||||
| `sku` | text | 3–24, alphanumeric and hyphen; unique locally |
|
||||
| `length_in` | real | 1/8-inch step |
|
||||
| `thickness_in` | real | 1/8-inch step |
|
||||
| `sqft` | real | 2 dp |
|
||||
| `bdft` | real | 2 dp |
|
||||
| `width_min_in`, `width_max_in`, `width_avg_in` | real | 6-inch stations |
|
||||
| `species_id` | int | FK `woo_categories`, one leaf |
|
||||
| `wood_category_ids` | json | one or more leaf ids |
|
||||
| `edge_type_term_id` | int | one term |
|
||||
| `figure_term_ids` | json | one or more terms |
|
||||
| `grade_term_id` | int | exactly one term |
|
||||
| `thickness_term_id` | int | round-up band term |
|
||||
| `moisture_term_id` | int | default kiln-dried term |
|
||||
| `fig_tag_ids` | json | optional |
|
||||
| `feat_tag_ids` | json | optional |
|
||||
| `price` | real | 2 dp |
|
||||
| `price_source` | text | `recommendation` \| `override` |
|
||||
| `title`, `short_description`, `description` | text | required non-empty; title warn >60, short_description warn >160 |
|
||||
| `content_source` | text | `manual` \| `llm` \| `template` |
|
||||
| `species_confidence`, `figure_confidence`, `grade_confidence` | real | null below 0.7 |
|
||||
| `inference_status` | text | null \| `inferring` \| `done` \| `failed` |
|
||||
| `calibration_photo_id` | text | FK `slab_photos` |
|
||||
| `client_rev`, `server_rev` | int | default 1 |
|
||||
| `woo_product_id` | int | null until publish |
|
||||
| `published_at` | text | null until publish |
|
||||
| `created_at`, `updated_at` | text | UTC ISO-8601 |
|
||||
|
||||
No `inference_thread_id` column exists; provider threads are not used.
|
||||
|
||||
## 3. `slab_photos`
|
||||
|
||||
| Column | Type | Constraint |
|
||||
|---|---|---|
|
||||
| `id` | text | PK, UUID |
|
||||
| `slab_id` | text | FK `slabs` |
|
||||
| `seq` | int | capture order |
|
||||
| `is_calibration` | int (bool) | exactly one true per slab |
|
||||
| `original_blob` | blob | retained while draft/retry |
|
||||
| `processed_blob` | blob | transparent PNG, alpha preserved |
|
||||
| `width_px`, `height_px` | int | processed dimensions |
|
||||
| `created_at` | text | UTC ISO-8601 |
|
||||
|
||||
The calibration photo exists; other batch photos are visual inventory only. Rows are purged on
|
||||
successful publish.
|
||||
|
||||
## 4. `pricing_rules`
|
||||
|
||||
| Column | Type | Constraint |
|
||||
|---|---|---|
|
||||
| `id` | int | PK |
|
||||
| `species_id` | int | FK `woo_categories`, UNIQUE |
|
||||
| `price_per_bdft` | real | flat, no tiers |
|
||||
| `created_at`, `updated_at` | text | UTC ISO-8601 |
|
||||
|
||||
## 5. Taxonomy cache
|
||||
|
||||
- `woo_categories` — species and wood-category leaves only; parents group and are never assigned.
|
||||
- `woo_attributes` / `woo_attribute_terms` — exactly the five locked attributes Edge Type, Figure,
|
||||
Grade, Thickness, Moisture, with their terms.
|
||||
- `woo_tags` — only the fig-* and feat-* families; other store tags are ignored.
|
||||
|
||||
Rebuild is one transaction; on failure the previous cache is kept.
|
||||
|
||||
## 6. `settings`
|
||||
|
||||
| Key | Storage | Default |
|
||||
|---|---|---|
|
||||
| `woo_wp_username` | plain | — |
|
||||
| `woo_app_password` | `value_enc` | — |
|
||||
| `inference_base_url` | `value_enc` | — |
|
||||
| `inference_api_key` | `value_enc` | — |
|
||||
| `inference_model` | plain | — |
|
||||
| `inference_enabled` | plain | `false` |
|
||||
| `content_llm_enabled` | plain | `false` |
|
||||
| `brand_voice` | plain | — |
|
||||
| `geo_context` | plain | — |
|
||||
| `woo_create_status` | plain | `draft` |
|
||||
| `taxonomy_last_sync_at` | plain | — |
|
||||
| `taxonomy_anchor` | plain | — |
|
||||
|
||||
`WOO_BASE_URL` is not a settings row; it is env, HTTPS only, read-only.
|
||||
|
||||
## 7. `prompts` and `prompt_revisions`
|
||||
|
||||
Prompts live in the database with all revisions; each call uses the active revision and the
|
||||
history is retained.
|
||||
|
||||
- `prompts`: `id`, `name` (`call1` | `call2`), `active_revision_id`, `created_at`, `updated_at`.
|
||||
- `prompt_revisions`: `id`, `prompt_id` FK, `content` text, `created_at`, `created_by`.
|
||||
|
||||
## 8. `sync_log`
|
||||
|
||||
Append-only; never deleted on publish. Columns: `id`, `slab_id`, `woo_product_id`, `status`
|
||||
(`success` | `failed`), `payload_hash` (sha256 of the outbound payload), `message`, `created_at`.
|
||||
|
||||
## 9. `inference_log`
|
||||
|
||||
Append-only audit. Columns: `id`, `slab_id`, `call` (1 | 2), `prompt_revision_id` FK, `model`,
|
||||
`request_payload`, `response_payload`, `latency_ms`, `created_at`.
|
||||
|
||||
## 10. Status transitions
|
||||
|
||||
`draft` → `calibrated` → `ready` → `publishing` → `published`, with `failed` reachable from any
|
||||
step and retryable. `calibrated` is set when the draft uploads; `ready` requires the full set.
|
||||
@@ -0,0 +1,91 @@
|
||||
# Prompts contract
|
||||
|
||||
Status: SPEC · 2026-09-18.
|
||||
Owns prompt storage, prompt inputs, and the two inference calls.
|
||||
|
||||
## Requirements impact
|
||||
|
||||
| Requirement | Impact on this design |
|
||||
|---|---|
|
||||
| METADATA-5, METADATA-10, METADATA-15, METADATA-30 | taxonomy choices and cardinality |
|
||||
| METADATA-35, METADATA-40, METADATA-45, METADATA-50 | Call 1 inputs, constraints, pre-fill, precedence |
|
||||
| METADATA-55, METADATA-60, METADATA-65, METADATA-70 | Call 2 trigger, context, constraints, fallback |
|
||||
| METADATA-75, METADATA-80, METADATA-85, METADATA-90, METADATA-95 | non-blocking, fallback title, prompt selection, fact block, overlay exclusion |
|
||||
| METADATA-100, METADATA-105, METADATA-110, METADATA-115 | feat mapping, prompt storage, button, failure |
|
||||
| METADATA-66 | generation logging |
|
||||
| SETTINGS-30 | brand voice and geo context |
|
||||
| REVIEW-40, REVIEW-50 | editability and field limits |
|
||||
|
||||
## 1. Storage and revisions
|
||||
|
||||
- Prompts live in the database, not in text files, with all revisions retained. Tables are in
|
||||
`docs/contracts/DATA-MODEL.md` §7.
|
||||
- Each call reads the active revision fresh.
|
||||
- Editing a prompt creates a new revision; history is never overwritten.
|
||||
- Every generation is logged with its inputs, the request/response when a model was used, and the
|
||||
final stored content.
|
||||
|
||||
## 2. Call 1 — vision classification
|
||||
|
||||
**When.** Runs automatically after a calibrated draft uploads only when `inference_enabled` is
|
||||
true; also on demand or retry. With inference off it is skipped and the fields are filled by hand.
|
||||
|
||||
**Inputs.**
|
||||
- The clean processed photos at 1024 px long edge or less.
|
||||
- The synced taxonomy: species leaves, Edge Type, Figure, Grade, Thickness, Moisture terms, each
|
||||
with its Woo id. These are the only legal choices.
|
||||
- User metadata: SKU, length, thickness.
|
||||
|
||||
**Constraints.**
|
||||
- The model may only choose values from the supplied synced lists; it never invents categories or
|
||||
terms.
|
||||
- Free-text observations (character, inclusions, voids, checks) are mapped to existing synced
|
||||
`feat-*` tags only; Woo is authoritative for those tags.
|
||||
|
||||
**Result fields.** `species_id` + `species_confidence`; `wood_category_ids`;
|
||||
`edge_type_term_id`; `figure_term_ids` + `figure_confidence`; `grade_term_id` +
|
||||
`grade_confidence`; `character`, `inclusions`, `voids`, `checks`. Grade, edge, thickness, and
|
||||
moisture are singular.
|
||||
|
||||
**Pre-fill.** At or above the threshold (default 0.7) the field is pre-filled and remains
|
||||
editable; below it the field stays empty and is not shown as a low-confidence guess. Typed values
|
||||
always win. Call 1 is assist-only and never gates publish. A runtime failure is classified
|
||||
recoverable or unrecoverable for the owner.
|
||||
|
||||
## 3. Call 2 — content prose
|
||||
|
||||
**When.** User-triggered only, via the Generate text control; never automatic. The control appears
|
||||
only when content generation is enabled and the endpoint is reachable.
|
||||
|
||||
**Inputs.**
|
||||
- A fresh, full Call 1 context: prompt, Call 1 photos, and the Call 1 result. No provider thread
|
||||
id is used.
|
||||
- The confirmed or changed metadata from review.
|
||||
- The literal dimension fact block, exact strings for species, figure term, length, thickness,
|
||||
average width, width min–max, and board feet; no length-by-width-by-thickness rectangle
|
||||
assumption.
|
||||
- Brand voice and geo context.
|
||||
|
||||
**Constraints.**
|
||||
- The model writes prose only and never invents or alters a dimension; every number comes from the
|
||||
fact block.
|
||||
- The dimension-overlay image is never sent.
|
||||
|
||||
**Output.** `title`, `short_description`, `description`. The owner can edit the result.
|
||||
|
||||
## 4. Fallback and templates
|
||||
|
||||
- With inference off, deterministic templates produce name, short_description, and description so
|
||||
create never lacks prose.
|
||||
- The template is the fallback only; when Call 2 runs, the model writes and the template guides
|
||||
style.
|
||||
- The fallback title uses the primary figure term name as the character, with the hyphen
|
||||
separator.
|
||||
- The primary wood category selects a specialized Call 2 prompt.
|
||||
- All generated text stays within the field limits in `docs/contracts/WOO-API.md` §9.
|
||||
|
||||
## 5. Prompt management
|
||||
|
||||
- The active Call 1 and Call 2 revisions are editable; saving creates a new revision.
|
||||
- A revision identifies its content, author, and timestamp; `inference_log` records the revision
|
||||
used for each call.
|
||||
@@ -4,13 +4,14 @@ Cross-cutting interface detail shared by capabilities.
|
||||
|
||||
Rules (`docs/STRUCTURE.md`): a contract doc **cites** requirement IDs from `docs/REQUIREMENTS.md`; it never defines or restates them.
|
||||
|
||||
Expected files (Phase 2 migration):
|
||||
Contract docs (migrated):
|
||||
|
||||
- `WOO-API.md` — external WooCommerce REST API v3 surface (new; the dependency contract)
|
||||
- `API.md` — FastAPI HTTP contract (from tag `OPENAPI.md`)
|
||||
- `DATA-MODEL.md` — SQLite schema and status machine (from tag `DATA-MODEL.md`)
|
||||
- `UX.md` — screens, field recovery, outcomes (from tag `UX.md`)
|
||||
- `PROMPTS.md` — Call 1 / Call 2 prompt text (from tag `PROMPTS.md`)
|
||||
- `TEST-SPEC.md` — test pyramid, vectors, dishonest proxies (from tag `TEST-SPEC.md`)
|
||||
- `CONTENT-WOO.md` — listing copy and Woo payload detail (migrated from `docs/CONTENT-WOO.md`)
|
||||
- `PROMPTS.md` — Call 1 / Call 2 prompt text and inputs (from tag `PROMPTS.md`)
|
||||
- `CONTENT-WOO.md` — listing copy and Woo payload intent (from `docs/CONTENT-WOO.md`)
|
||||
- `TEST-SPEC.md` — test pyramid, vectors, dishonest proxies (from tag `TEST-SPEC.md`) — pending
|
||||
|
||||
Template: `docs/_templates/contract.md`.
|
||||
|
||||
@@ -0,0 +1,152 @@
|
||||
# Test spec contract
|
||||
|
||||
Status: SPEC · 2026-09-18 · reconciled against the finished PoR.
|
||||
Defines the affirmative test bar. It cites requirement IDs; `docs/REQUIREMENTS.md` is normative.
|
||||
The human walkthrough is `docs/UAT-PRACTICE.md`.
|
||||
|
||||
## Requirements impact
|
||||
|
||||
| Requirement | Impact on this design |
|
||||
|---|---|
|
||||
| QUALITY-5, QUALITY-10, QUALITY-15 | TDD for the deterministic core; vectors TV-1..TV-10 |
|
||||
| QUALITY-20, QUALITY-35, QUALITY-50 | inference-off suite, Woo-mock integration, Compose UI E2E |
|
||||
| QUALITY-25, QUALITY-30 | evidence rules; Ty owns gate exit |
|
||||
| QUALITY-40, QUALITY-45 | property checks and bdft identity |
|
||||
| QUALITY-55 .. QUALITY-100 | pipeline vectors |
|
||||
| QUALITY-105, QUALITY-110 | dishonest proxies; M3 exit evidence |
|
||||
| QUALITY-115, QUALITY-120, QUALITY-125 | browser E2E, no raw codes, AES round-trip |
|
||||
| APP-45, APP-55, APP-60, APP-65 | health, logging, accessibility, log redaction |
|
||||
| SETTINGS-80, PUBLISH-30/31/35 | failure handling under test |
|
||||
|
||||
## 1. Layers and harness
|
||||
|
||||
| Layer | Written | Location | Rule source |
|
||||
|---|---|---|---|
|
||||
| Unit | TDD before the code exists | `backend/tests/unit/`, `frontend/src/__tests__/` | deterministic core and validation |
|
||||
| API contract | with the feature | `backend/tests/contract/` (pytest + httpx TestClient) | `docs/contracts/API.md` |
|
||||
| Integration | with the feature | `backend/tests/integration/` (respx Woo mock) | `docs/contracts/WOO-API.md`, `docs/contracts/CONTENT-WOO.md` |
|
||||
| UI / E2E | with the feature | `frontend/e2e/` (Playwright, Compose stack, mocked Woo) | `docs/contracts/UX.md` |
|
||||
| Ops | with deploy config | `deploy/tests/` (shell) | `deploy/` |
|
||||
|
||||
Allowed results are `pass`, `fail`, `not run`, and optionally `blocked`. Every `pass` or `fail`
|
||||
needs runner evidence; no evidence means `not run`. The only bug log is Gitea issues. Ty reads raw
|
||||
results and owns gate exit; an agent never declares a gate green (`QUALITY-25`, `QUALITY-30`).
|
||||
|
||||
## 2. Fundamentals (FUND)
|
||||
|
||||
- FUND-1..4 — no browser/DOM/network deps in pure tests; semantic selectors; real Tab navigation;
|
||||
reactivity flush. (test practice)
|
||||
- FUND-5, FUND-6, FUND-7 — Tab order, 320 px reflow, visible focus ring → `APP-60`
|
||||
- FUND-8..11 — app factory per test, lifespan, override cleanup, no real network (test practice)
|
||||
- FUND-12, FUND-13, FUND-16 — FK and constraint enforcement, migration integrity (test practice)
|
||||
- FUND-14 — WAL with no stale reads (test practice)
|
||||
- FUND-15 — illegal status transitions rejected → `APP-5`
|
||||
- FUND-17 — fresh nonce per encrypt; tamper fails → `SETTINGS-25`
|
||||
- FUND-18 — ciphertext absent from GET; only configured booleans → `SETTINGS-75`
|
||||
- FUND-19 — missing key refuses startup → `SETTINGS-25`
|
||||
- FUND-20 — Basic Auth with application passwords over HTTPS, not consumer keys → `SETTINGS-15`
|
||||
- FUND-21 — 401/400/404 mapped, not passed raw → `APP-50`
|
||||
- FUND-22 — pre-create SKU lookup, duplicate race → `PUBLISH-15`
|
||||
- FUND-23 — timeouts/network errors graceful, no stack → `PUBLISH-30`, `PUBLISH-35`
|
||||
- FUND-24 — no secret material in logs → `APP-65`
|
||||
- FUND-25 — HTTPS, health through proxy, port collision → `OPS-5`, `OPS-15`, `OPS-20`
|
||||
|
||||
## 3. Deterministic vectors (TV)
|
||||
|
||||
- TV-1 — green patch not touching the border survives; U2Net not invoked → `QUALITY-55`
|
||||
- TV-2 — dark interior wood survives; flood fill isolates border-connected regions → `QUALITY-60`
|
||||
- TV-3 — 96 in, 1.5 in, 0.64 sqft yields 0.96 bdft; 1 sqft at 1 in equals 1 bdft → `QUALITY-65`
|
||||
- TV-4 — 6 in stations on a 96 in axis report min, max, average → `QUALITY-70`
|
||||
- TV-5 — one square end, one 45 degree cut; axis follows prevailing length → `QUALITY-75`
|
||||
- TV-6 — 3:4 crop, 90 percent fill at extremes, transparent PNG, 800 px floor → `QUALITY-80`
|
||||
- TV-7 — undersized source warns, is not upscaled, not publishable until retake → `QUALITY-85`
|
||||
- TV-8 — 0.96 bdft at 12.50 gives 12.00; a 150.00 override is stored → `QUALITY-90`
|
||||
- TV-9 — 1 in rounds up into the synced Woo thickness band → `QUALITY-95`
|
||||
- TV-10 — half-up 2 decimal rounding cases → `QUALITY-100`
|
||||
|
||||
Validation:
|
||||
- SKU charset 3–24 alphanumeric and hyphen → `REVIEW-45`
|
||||
- title/short_description/description within limits → `REVIEW-50`
|
||||
|
||||
AES:
|
||||
- round-trip decrypt; ciphertext never in GET → `QUALITY-125`, `SETTINGS-75`
|
||||
- missing key refuses startup → `SETTINGS-25`
|
||||
|
||||
API contract / health / draft:
|
||||
- `GET /api/health` shape and non-200 on DB/volume failure → `APP-45`
|
||||
- `POST /api/v1/slabs` multipart returns 201 calibrated → `docs/contracts/API.md`, `APP-5`
|
||||
|
||||
## 4. Gate C and Woo
|
||||
|
||||
- `SLAB-UAT-*` creates a draft regardless of setting → `PUBLISH-10`
|
||||
- duplicate SKU before create returns 409 with `edit_sku` / `open_existing` → `PUBLISH-15`
|
||||
- test-woo ok, resyncs taxonomy → `SETTINGS-40`
|
||||
- WOO_BASE_URL read-only, PUT rejected → `SETTINGS-5`, `SETTINGS-85`
|
||||
- startup refuses missing or http WOO_BASE_URL → `SETTINGS-10`
|
||||
- Woo create stores `woo_product_id`, purges images → `PUBLISH-25`
|
||||
- Woo 409 mapped to field recovery, draft retained → `PUBLISH-15`
|
||||
- taxonomy anchor unchanged on no-op, bumps on change → `SETTINGS-55`
|
||||
- failed publish marks failed, keeps photos, writes sync_log, retries to success → `PUBLISH-30`
|
||||
|
||||
## 5. Inference
|
||||
|
||||
- with inference off: Call 1 skipped, no inference calls, suite passes → `METADATA-75`, `QUALITY-20`
|
||||
- test-inference returns ok, mode stateful/stateless, vision_capable → `SETTINGS-45`
|
||||
- test-inference fail blocks Call 2 only → `SETTINGS-45`
|
||||
- Call 1 at or above 0.7 pre-fills; below stays empty; override wins → `METADATA-45`, `METADATA-50`
|
||||
- Call 1 sends clean processed photos at 1024, synced taxonomy, user metadata → `METADATA-35`
|
||||
- Call 2 user-triggered only, fresh full context → `METADATA-55`, `METADATA-60`
|
||||
- model does not invent dimensions → `METADATA-65`
|
||||
- generation logged → `METADATA-66`
|
||||
- prompts stored with revisions → `METADATA-105`
|
||||
|
||||
## 6. UI end-to-end (UIE / E2E)
|
||||
|
||||
- capture controls, overlay, axis, numbers, ready gates → `CAPTURE-40`, `CAPTURE-45`, `CAPTURE-50`,
|
||||
`CAPTURE-60`, `CAPTURE-70`, `CAPTURE-80`, `CAPTURE-85`, `CAPTURE-90`, `REVIEW-5`, `REVIEW-20`
|
||||
- review pre-fill and editability → `METADATA-45`, `REVIEW-40`
|
||||
- duplicate SKU, stale value, success, failure messages → `PUBLISH-15`, `PUBLISH-20`, `PUBLISH-25`,
|
||||
`PUBLISH-30`, `PUBLISH-35`
|
||||
- inference-off content path, Generate text disabled → `METADATA-70`, `METADATA-110`
|
||||
- settings screen and read-only URL → `SETTINGS-5`, `SETTINGS-15`, `SETTINGS-20`, `SETTINGS-30`,
|
||||
`SETTINGS-35`, `SETTINGS-65`
|
||||
- cross-browser capture through publish → `QUALITY-115`
|
||||
|
||||
## 7. API surface
|
||||
|
||||
- revisions and conflict → `APP-20`
|
||||
- PUT stores derived, no recompute → `APP-40`
|
||||
- secrets absent from GET → `SETTINGS-75`
|
||||
- infer-taxon/content 202 and poll; timeout → `METADATA-35`, `METADATA-115`
|
||||
- publish preconditions → `REVIEW-5`
|
||||
- taxonomy sync, anchor, cache retention → `SETTINGS-50`, `SETTINGS-55`
|
||||
- delete soft, published undeletable → `docs/contracts/API.md`
|
||||
- test-woo fails closed → `SETTINGS-80`
|
||||
- taxonomy re-pull and 1 hour publish refresh → `SETTINGS-60`
|
||||
- species picker synced only → `METADATA-5`
|
||||
|
||||
## 8. Ops and instrumentation
|
||||
|
||||
- compose health, inventory, test-woo, draft status → `OPS-15`, `SETTINGS-40`, `SETTINGS-65`, `APP-45`
|
||||
- backup and restore retention and cycle → `OPS-30`, `OPS-35`
|
||||
- health non-200 on DB/volume, `woo_reachable` false → `APP-45`
|
||||
- request id in logs → `APP-55`
|
||||
- inference_log and sync_log written → `METADATA-66`, `PUBLISH-30`
|
||||
|
||||
## 9. Purged cases
|
||||
|
||||
Removed because the behavior was superseded: four-knob cases (edge offset, feather), the shared
|
||||
validation module and its 412 path, `previous_response_id` and the per-slab thread, "inventory
|
||||
photos" as the Call 1 input, prompt files edited without restart, 80 percent / 1600 px crop values,
|
||||
and manual override of the computed fields.
|
||||
|
||||
## 10. New cases
|
||||
|
||||
- keyboard tab order, focus ring, and 320 px reflow → `APP-60`
|
||||
- no secret material in any log output → `APP-65`
|
||||
|
||||
## 11. Rules that do not change
|
||||
|
||||
Inference is optional and the full suite passes with it off. `bdft = sqft × thickness_in`, no
|
||||
divide-by-12. The client is authoritative for deterministic math; the server stores the numbers.
|
||||
No new product locks live in this document.
|
||||
@@ -0,0 +1,97 @@
|
||||
# UX contract — mill-owner flow
|
||||
|
||||
Status: SPEC · 2026-09-18. Presentation spec for the mill-owner flow on phone and PC browsers.
|
||||
|
||||
## Requirements impact
|
||||
|
||||
| Requirement | Impact on this design |
|
||||
|---|---|
|
||||
| CAPTURE-10 | supported browsers, hard stop |
|
||||
| CAPTURE-15 | online only |
|
||||
| CAPTURE-20, CAPTURE-25, CAPTURE-30 | photo intake and calibration choice |
|
||||
| CAPTURE-40, CAPTURE-41, CAPTURE-45, CAPTURE-50, CAPTURE-55 | sheet/sensitivity controls, overlay, retake |
|
||||
| CAPTURE-60, CAPTURE-65, CAPTURE-70, CAPTURE-80, CAPTURE-85, CAPTURE-90 | axis, entry, derived numbers |
|
||||
| CAPTURE-105, CAPTURE-135, CAPTURE-141, CAPTURE-145, CAPTURE-150, CAPTURE-155 | crop guard, upload, preview/controls, failure |
|
||||
| METADATA-10, METADATA-15, METADATA-30 | required taxonomy fields and cardinality |
|
||||
| METADATA-45, METADATA-55, METADATA-110 | pre-fill, Generate text availability |
|
||||
| PUBLISH-10, PUBLISH-15, PUBLISH-20, PUBLISH-25, PUBLISH-30, PUBLISH-35, PUBLISH-36, PUBLISH-37, PUBLISH-38, PUBLISH-50 | publish outcomes and messaging |
|
||||
| REVIEW-5, REVIEW-10, REVIEW-15, REVIEW-20, REVIEW-25, REVIEW-30, REVIEW-40, REVIEW-45, REVIEW-50 | ready set, gating, fields, limits |
|
||||
| SETTINGS-65 | create-status default |
|
||||
|
||||
## 1. Shape
|
||||
|
||||
One column of screens: capture → review → publish. No dashboard and no batch view. The reader is a
|
||||
mill owner photographing one slab at a time, online. Capture often starts on a phone; the same
|
||||
flow runs on a PC browser.
|
||||
|
||||
## 2. Capture
|
||||
|
||||
1. **Photos.** Take one or more photos; the first is top-down with the lens parallel to the broad
|
||||
face. The owner chooses which photo is the calibration photo.
|
||||
2. **Mask overlay and controls.** The app draws the mask on the source photo. The controls are
|
||||
exactly **Sheet** (auto, green, black) and **Sensitivity** (0–100). A Sheet change runs
|
||||
immediately; a Sensitivity change debounces 2000 ms; a worker recomputes and the last good
|
||||
overlay stays visible. There is no apply step and no server round trip; values persist until
|
||||
reset.
|
||||
3. **Confirm the mask.** The owner confirms the edge; approval is blocked when the mask has no
|
||||
slab pixels. A broken or non-contiguous mask resolves as a retake.
|
||||
4. **Length axis.** The app draws the bounding rectangle and the length axis; the owner rotates it
|
||||
to the prevailing length and confirms. On a nearly square slab the owner chooses the axis.
|
||||
5. **Numbers.** The owner enters length and thickness in 1/8-inch steps and the SKU. The browser
|
||||
computes widths, sqft, and bdft. These are display only; the server never recomputes them.
|
||||
6. **Continue.** Uploads originals, processed PNGs, and the numbers as a calibrated draft; status
|
||||
becomes `calibrated`.
|
||||
|
||||
The preview is display-only and never drives the math. Failures such as a broken mask or an
|
||||
undersized source never guess; the resolution is retake.
|
||||
|
||||
## 3. Review
|
||||
|
||||
- **Vision pre-fill.** At or above the threshold (default 0.7) a field is pre-filled and shown
|
||||
with an "AI" badge; below it the field starts empty. The badge is presentation only and clears
|
||||
on edit.
|
||||
- **Required set.** Exactly one species and at least one wood-category leaf; one Edge Type, at
|
||||
least one Figure, exactly one Grade, one Thickness band, one Moisture; fig-* and feat-* tags
|
||||
optional; price; title, short_description, and description.
|
||||
- **Primary category.** The owner declares one primary wood category and any secondary ones; the
|
||||
default is the first selected.
|
||||
- **Inline nudges.** Every empty required field shows an inline nudge as the owner works; there is
|
||||
no single submit-time gap list.
|
||||
- **Calculated fields.** Widths, sqft, and bdft are read-only and not overridable.
|
||||
- **Generate text.** The control appears only when content generation is enabled and the endpoint
|
||||
is reachable; it is disabled when the text service is down; it never runs automatically.
|
||||
|
||||
## 4. Publish
|
||||
|
||||
- A normal SKU uses `woo_create_status` (default draft); a `SLAB-UAT-*` SKU always creates a draft.
|
||||
"Published" means the store create succeeded, not that it is live.
|
||||
- **Success.** The local draft and photos are gone; the store product reference is shown.
|
||||
- **Duplicate SKU.** "This SKU already exists in the store (any status)." with Edit SKU and Open
|
||||
Existing.
|
||||
- **Stale value.** "This value is no longer valid. Pick from the current list," with current
|
||||
options.
|
||||
- **Failure.** A plain message on the affected field or action, classified recoverable or
|
||||
unrecoverable, with Retry while the draft and photos are retained; no raw code.
|
||||
|
||||
## 5. Browser gate
|
||||
|
||||
Supported families are iOS Safari and Android Chrome, plus desktop Chrome, Edge, and Firefox. An
|
||||
unsupported client gets a hard stop with an update or switch link; there are no polyfills.
|
||||
|
||||
## 6. Canonical error and empty copy
|
||||
|
||||
| Situation | Copy |
|
||||
|---|---|
|
||||
| Mask failure | "Cannot isolate the slab edge. Adjust the controls or retake." |
|
||||
| Axis check | "Rotate the axis to match the prevailing length of the slab." |
|
||||
| Source too small | "Photo will be too small after cropping. Please retake with a closer shot." |
|
||||
| Duplicate SKU | "This SKU already exists in the store (any status)." |
|
||||
| Stale value | "This value is no longer valid. Pick from the current list." |
|
||||
| Store unreachable | "Cannot reach the store. Check your connection and settings." |
|
||||
| Missing field nudge | per-field, in place (e.g. "Select species") |
|
||||
| Browser gate | "This app requires a recent browser. Please update or switch, then try again." |
|
||||
|
||||
## 7. Presentation-only details
|
||||
|
||||
First-run guidance card, "AI" badge styling, and the separate title/description step are
|
||||
presentation detail, not requirements.
|
||||
@@ -0,0 +1,142 @@
|
||||
# WooCommerce API contract
|
||||
|
||||
Status: SPEC · 2026-09-18 · external dependency: WooCommerce REST API v3.
|
||||
Source: official WooCommerce REST API v3 documentation
|
||||
(https://woocommerce.github.io/woocommerce-rest-api-docs/).
|
||||
|
||||
This contract pins the exact WooCommerce surface the app codes against.
|
||||
`docs/contracts/CONTENT-WOO.md` owns the listing copy and payload intent; this file owns the wire
|
||||
shape and the external constraints.
|
||||
|
||||
## Requirements impact
|
||||
|
||||
| Requirement | Impact on this design |
|
||||
|---|---|
|
||||
| SETTINGS-5, SETTINGS-10, SETTINGS-15 | base URL, HTTPS, app-password auth |
|
||||
| SETTINGS-35, SETTINGS-50, SETTINGS-51, SETTINGS-65 | pricing, taxonomy sync, tag scope, create status |
|
||||
| METADATA-5, METADATA-10, METADATA-15, METADATA-30 | categories, attributes, tags shapes |
|
||||
| REVIEW-10, REVIEW-25, REVIEW-45, REVIEW-50 | name, price, SKU, field limits |
|
||||
| PUBLISH-5, PUBLISH-10, PUBLISH-15, PUBLISH-20, PUBLISH-25, PUBLISH-30, PUBLISH-35 | create sequence, draft rule, errors |
|
||||
| PUBLISH-40, PUBLISH-45, PUBLISH-55, PUBLISH-60 | images, payload fields, sold-individually |
|
||||
| APP-35, APP-50 | error envelope mapping |
|
||||
|
||||
## 1. Transport and auth
|
||||
|
||||
- Base path: `${WOO_BASE_URL}/wp-json/wc/v3`.
|
||||
- `WOO_BASE_URL` is HTTPS only and injected from env.
|
||||
- Auth: WordPress application password over HTTP Basic. The consumer-key example in the Woo docs
|
||||
is **not** used.
|
||||
- Content type `application/json`. Dates ISO8601 (`YYYY-MM-DDTHH:MM:SS`). Monetary values are
|
||||
strings with two decimals.
|
||||
|
||||
## 2. Endpoints used
|
||||
|
||||
| Purpose | Method and path |
|
||||
|---|---|
|
||||
| Sync categories | `GET /products/categories?per_page=100&page=N` |
|
||||
| Sync tags | `GET /products/tags?per_page=100&page=N` |
|
||||
| Sync attributes | `GET /products/attributes?per_page=100` |
|
||||
| Sync attribute terms | `GET /products/attributes/{id}/terms?per_page=100` |
|
||||
| SKU lookup | `GET /products?sku={sku}` |
|
||||
| Create product | `POST /products` |
|
||||
| Upload media | `POST /wp-json/wp/v2/media` (WordPress core) |
|
||||
|
||||
## 3. Product create field alignment
|
||||
|
||||
| Our value | Woo field | Woo type | Enum / constraint |
|
||||
|---|---|---|---|
|
||||
| title | `name` | string | none documented |
|
||||
| — | `type` | string | `simple` (default) |
|
||||
| create status | `status` | string | `draft`, `pending`, `private`, `publish`; default `publish` |
|
||||
| sku | `sku` | string | unique; no documented format |
|
||||
| price | `regular_price` | string | monetary string, 2 decimals |
|
||||
| description | `description` | string | none documented |
|
||||
| short_description | `short_description` | string | none documented |
|
||||
| species + wood categories | `categories` | array of `{id: integer}` | leaf ids |
|
||||
| fig-* / feat-* tags | `tags` | array of `{id: integer}` | tag ids |
|
||||
| uploaded PNGs | `images` | array of `{id: integer}` | attachment ids, lineup order |
|
||||
| five locked attributes | `attributes` | array of attribute elements | see §4 |
|
||||
| — | `virtual` | boolean | default `false` |
|
||||
| — | `downloadable` | boolean | default `false` |
|
||||
| — | `catalog_visibility` | string | `visible`, `catalog`, `search`, `hidden`; default `visible` |
|
||||
| — | `sold_individually` | boolean | default `false`; set `true` |
|
||||
|
||||
`sold_individually: true` is the Woo mechanism that enforces "one slab is one unit."
|
||||
|
||||
## 4. `attributes` element shape
|
||||
|
||||
Global attributes are referenced by `id` (synced from Woo), never created by name. Each element:
|
||||
|
||||
```json
|
||||
{
|
||||
"id": 8,
|
||||
"position": 0,
|
||||
"visible": true,
|
||||
"variation": false,
|
||||
"options": ["Live Edge"]
|
||||
}
|
||||
```
|
||||
|
||||
| Attribute | `options` |
|
||||
|---|---|
|
||||
| Edge Type | `[term name]` |
|
||||
| Figure | `[term name, ...]` |
|
||||
| Grade | `[term name]` |
|
||||
| Thickness | `[term name]` |
|
||||
| Moisture | `[term name]` |
|
||||
|
||||
`options` carries term **names**, not ids. `visible` is `true`; `variation` is `false`.
|
||||
|
||||
## 5. Categories, tags, images shapes
|
||||
|
||||
- `categories`: `[{"id": 9}, {"id": 14}]`
|
||||
- `tags`: `[{"id": 34}]`
|
||||
- `images`: `[{"id": 42}, {"id": 793}]`, in photos-ready lineup order.
|
||||
- Media upload: `POST /wp-json/wp/v2/media` as `multipart/form-data` with the PNG bytes; the
|
||||
response `id` is the attachment id used in `images`. Keep alpha; PNG only.
|
||||
|
||||
## 6. Error shape
|
||||
|
||||
```json
|
||||
{ "code": "woocommerce_rest_term_invalid", "message": "Resource doesn't exist.", "data": { "status": 404 } }
|
||||
```
|
||||
|
||||
`401` auth, `400` bad request, `404` missing, `500` server. These map into the app's uniform
|
||||
envelope and never reach the owner raw.
|
||||
|
||||
## 7. Pagination
|
||||
|
||||
`per_page` max 100, `page` 1-based. Read `X-WP-Total` and `X-WP-TotalPages`; follow pages until
|
||||
exhausted. Taxonomy lists return in full to the client.
|
||||
|
||||
## 8. No documented maximum lengths
|
||||
|
||||
The Woo v3 docs state no maximum length for `name`, `description`, `short_description`, `sku`, or
|
||||
`regular_price`. Do not invent caps. The app enforces non-empty required fields and its own local
|
||||
formats.
|
||||
|
||||
## 9. Field length limits
|
||||
|
||||
Hard limits are verified from WordPress/WooCommerce source; the display caps are conventions
|
||||
(Google states no numeric limit).
|
||||
|
||||
| Field | Storage | Hard limit | Recommended display cap | Rationale |
|
||||
|---|---|---|---|---|
|
||||
| `name` | `wp_posts.post_title` (`text`) | 65,535 | 60 | on-screen title-tag width |
|
||||
| `short_description` | `wp_posts.post_excerpt` (`text`) | 65,535 | 160 | search snippet / meta description width |
|
||||
| `description` | `wp_posts.post_content` (`longtext`) | ~16.7M | none | no word-count target; write for the page |
|
||||
| `sku` | `wc_product_meta_lookup.sku` (`varchar(100)`, index `sku(50)`) | 100 | 50 | index prefix length |
|
||||
|
||||
Sources: WordPress `schema.php`, WooCommerce `class-wc-install.php`,
|
||||
`class-wc-product-data-store-cpt.php`, and Google Search Central (title links, snippets, SEO
|
||||
starter guide).
|
||||
|
||||
## 10. Media size
|
||||
|
||||
The store exposes its upload cap read-only as `max_upload_size` on system status. Validate the
|
||||
processed PNG against it before upload and surface a plain, actionable error if exceeded.
|
||||
|
||||
## 11. Versioning
|
||||
|
||||
Woo `wc/v3` requires WooCommerce 3.5+ and WordPress 4.4+ with pretty permalinks. Breaking Woo API
|
||||
changes are handled by the app's own `/api/v1` version, not by pinning the store.
|
||||
Reference in New Issue
Block a user