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.
|
||||
Reference in New Issue
Block a user