Files
SlabUploader/docs/contracts/API.md
T
Ty 2ba5428269 docs(m3): place create decisions in the contracts
- DATA-MODEL.md: storage engine (stdlib sqlite3 + schema.sql, no ORM/migrations),
  pragmas, per-test isolation.
- API.md: revision coupling, async job model, PUT check order, server-derived ready.
- PROMPTS.md: no prompt files ship; the DB is the only prompt source.
2026-09-19 10:12:32 -07:00

10 KiB
Raw Blame History

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.

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.

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 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 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

  • 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.