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:
2026-09-18 21:02:06 -07:00
parent a8cc2f7596
commit 01aa4c52c3
53 changed files with 3186 additions and 817 deletions
+191
View File
@@ -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.
+86
View File
@@ -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`).
+142
View File
@@ -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.
+91
View File
@@ -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.
+5 -4
View File
@@ -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`.
+152
View File
@@ -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.
+97
View File
@@ -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.
+142
View File
@@ -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.