Files
SlabUploader/docs/contracts/API.md
T
Ty 7287a55707 fix(inference): #86 tighten Call 1 prompt and closed lists
Ship locked expert Call 1 seed: order cats before edge, species=best
confidence, mill-count edge rules, Wild for cookies/burl, multi figure,
feature_tag_ids only (no free-text bags, no grade). LISTS keys match the
seed; feat- prefix stripped for the model. Gate and review UI apply
feature_tag_ids. Seed auto-activates on deploy when still seed-authored.
2026-09-20 22:54:36 -07:00

208 lines
11 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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`. A background inference write bumps `server_rev`; the client adopts the
polled `server_rev` before its next write.
- **Async work:** inference and publish run through one in-process job queue with a single
consumer; a status transition is compare-and-set inside one write transaction, so a poll never
observes a half-applied transition. Tests run the queue inline so contract tests are
deterministic.
- **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. Cites APP-20, CAPTURE-135, METADATA-45, PUBLISH-15.
`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). On publish success the terminal response carries `woo_product_id`; the
working row is then discarded, so a later GET may return `404`, which the client treats as
completion.
### `PUT /api/v1/slabs/{id}`
Partial update of editable fields plus `client_rev`. Stores client-sent derived fields and does
not recompute them. Checks run in order: `409 revision_conflict`, then `409 duplicate_sku` (SKU
field actions `edit_sku` / `open_existing`), then `422 stale_field` (current options included).
`status` becomes `ready` only when the full required set is present; it is server-derived and a
client never sets it. Cites APP-20, REVIEW-40, PUBLISH-15, PUBLISH-20.
### `DELETE /api/v1/slabs/{id}`
Soft delete of unpublished drafts only (sets `deleted_at`); `204`. Deleted rows are excluded from
all GET responses. 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 processed photos, the review-confirmed
metadata, the primary and secondary wood categories, the parent-category path, the dimension fact
block, and brand/geo context; no provider thread id and no raw Call 1 result. Requires
`test-inference` `ok`. Returns `202`; the client polls. The model writes the long description only
and never touches dimensions. Cites METADATA-55, METADATA-65.
Call 1 result fields: `species_id`, `species_confidence`, `wood_category_ids`,
`wood_category_confidence`, `edge_type_term_id`, `edge_confidence`, `figure_term_ids`,
`figure_confidence`, `feature_tag_ids`, `feature_tag_confidence`. Feature tags are ids from the
synced `feat-*` set only (no free-text bags). Grade is never suggested.
Call 2 result field: `description`. The name and short description are assembled by the server from
the primary wood category's patterns, not returned by the call.
## 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. Cites PUBLISH-5, PUBLISH-10, PUBLISH-15, PUBLISH-25.
- Success: `woo_product_id` set and returned on the terminal poll, photos purged, the local working
slab row discarded, device draft cleared. Nothing is deleted from WooCommerce.
- 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
Cites SETTINGS-15, SETTINGS-20, SETTINGS-25, SETTINGS-80, SETTINGS-85.
- `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.