Files
SlabUploader/docs/ARCHITECTURE.md
T
grokbot_ty 0c5ee36a7a docs: split list-ready into capture, submit, and confidence-gate
Name every DATA-MODEL submit field in mill-owner English. Issue #9 empty-below-0.7 stays a subset; species plus one figure is not enough to list.
2026-09-02 02:40:28 +00:00

11 KiB
Raw Blame History

Architecture

Status: SPEC · hybrid topology locked at Gate A freeze (2026-08-31) · SlabUploader

Normative for hybrid split, swimlane, what-runs-where, secrets why, and status transitions. Ops depth lives in DEPLOYMENT. Schema enums live in DATA-MODEL. Agent-facing hard rules live in AGENTS.md (verbatim locks stay there).

Companion: docs/TECH-SPEC-PIPELINE.md. Historical PRD: docs/archive/PRD-2026-08.txt (frozen; not a source for new work). Agent locks: AGENTS.md.

1. Topology (hybrid)

Phone does the happy-path pipeline (mask knobs, length axis, sqft/bdft, 3:4 PNG). FastAPI stores drafts, proxies inference, talks to Woo. reverse proxy terminates HTTPS. Server U2Net (“Try harder”) is deferred from POC, not deleted.

phone (SvelteKit)
  capture, four knobs inline, live mask overlay on the source photo,
  length axis, sqft/bdft, 3:4 PNG
  (POC: retake if mask still bad — no U2Net control)

server
  reverse proxy → frontend (static)
        → fastapi : store, settings, taxonomy cache, Woo, inference proxy, prompt files
        → U2Net endpoint reserved post-POC
  • Happy path never leaves the phone.
  • Mask knobs live on capture; changing a knob re-runs client BG removal immediately.
  • Woo + LLM keys stay on the server. Browser does not call Woo or the vision endpoint directly.

2. Swimlane

flowchart TB
  subgraph Phone["Phone — SvelteKit"]
    P1[New Wood: 1–5 photos]
    P2[BG overlay + sheet / sensitivity / edge / feather]
    P3{Edge OK?}
    P4[Length axis overlay — rotate / confirm]
    P5[User length + thickness + SKU]
    P6[sqft, bdft, 6in widths, 3:4 PNGs]
    P7[Review: Call 1 pre-fill or manual taxonomy]
    P8[Tap Generate text]
    P9[Review title / desc — publish]
  end

  subgraph API["server — FastAPI"]
    A1[U2Net reserved post-POC]
    A2[Store draft: originals + PNGs + numbers]
    A3[Call 1 proxy — vision]
    A4[Call 2 proxy — text]
    A5[Woo: taxonomy, SKU check, create product]
  end

  subgraph LLM["Your OpenAI-compatible endpoint"]
    L1[Wood expert — all photos at 1024]
    L2[Title / desc / short desc]
  end

  subgraph Woo["WooCommerce store"]
    W1[Categories / attributes / fig-* tags]
    W2[New product live or draft per setting]
  end

  P1 --> P2 --> P3
  P3 -->|no — retune knobs or retake| P2
  P3 -->|no — Retake| P1
  P3 -->|yes| P4 --> P5 --> P6
  P6 --> A2
  A2 --> A3 --> L1 --> A3 --> P7
  P7 --> P8 --> A4 --> L2 --> A4 --> P9
  P9 --> A5
  A5 --> W1
  A5 --> W2

3. Clarifications

  1. Happy path stays on the phone through PNG + numbers. FastAPI is idle until the user has a confirmed mask.
  2. Mask knobs live on capture; each change re-runs client BG removal. Server U2Net “Try harder” is deferred from POC (keep the one-photo contract; do not ship the control).
  3. Draft upload sends originals + processed PNGs + length/thickness/SKU/sqft/bdft/widths. Originals are needed if Call 1 or a later retry must not depend on the tab still being open. After successful Woo publish, server deletes both.
  4. Call 1 is server-side so the vision key never sits in the browser. Auto-run on slab create only if inference_enabled is true. If disabled, Call 1 is skipped; user can manually fill taxonomy and still trigger Call 2. Body: all originals downscaled to 1024 + Woo-synced taxonomy only + SKU/length/thickness. Timeout 45s → error + retry on the phone. ≥0.7 confidence pre-populates species, wood categories, edge/figure/grade, feat-* (mutable). Below 0.7 those fields stay empty. Manual entry stays authoritative. Presentation: docs/UX.md.
  5. Call 2 does not auto-fire. User taps Generate text. Server must send portable Call 1 context (default: full conversation resend). Stateful previous_response_id only after test-inference proves the provider supports it. Call 2 inputs distinguish confirmed user values from inferred ones. Per-slab thread; delete on submit or abandon.
  6. Woo is only FastAPI. Browser never sees the WordPress application password. Duplicate SKU → 409 mapped on the SKU field (edit SKU or open existing). Stale value → 422 on that field. Draft stays on the phone.
  7. Settings (species $/bdft, prompts, inference endpoint, Woo credentials, publish status) live on the server. Store URL is env, displayed read-only.

4. What runs where

Job Where
Camera, 1-5 photos Client
Sheet detect, chroma-key / black threshold, four knobs, live re-run, flood-fill Client
U2Net “Try harder” Server, deferred from POC
Length axis overlay, 6" widths, sqft, bdft Client
3:4 PNG, 80% fill (configurable aspect) Client
Draft + originals + processed PNGs Server (after user continues)
Species $/bdft, Woo creds, prompts, brand/GEO Server
Call 1 (vision, all photos @1024) Server proxy
Call 2 (title/desc) Server proxy, after Call 1 curation
Woo taxonomy sync + publish Server

5. Secrets and settings

Server-side only (never in browser JS):

  1. WordPress username + application password for a dedicated low-privilege WP user. Server uses HTTP Basic Auth (username:application_password) against /wp-json/wc/v3/. Stored encrypted at rest in SQLite (AES-GCM). Not Woo consumer keys. Not the account login password.
  2. Inference endpoint base URL + API key. Stored encrypted. Server proxies all inference calls.
  3. AES master key. Env var on server (SLAB_AES_KEY), never in the DB.
  4. WOO_BASE_URL env (compose). HTTPS only. Not user-editable; Settings displays it read-only.

Browser-side (no secrets):

  1. User preferences: sheet mode, sensitivity, edge offset, feather. Persisted in localStorage, reset-to-default available.
  2. Species list with $/bdft. Read from server settings sync.
  3. Taxonomy snapshot (categories, attributes, tags). Read from server. No auth needed.

Flow:

  1. Deploy sets WOO_BASE_URL (https://) in compose. Operator creates a low-privilege WP user on that store (HTTPS required for Application Passwords UI). Generates an application password under Users → Profile.
  2. User enters that username + application password in Settings UI (store URL shown read-only from env).
  3. Browser POSTs them to /api/v1/settings on FastAPI. Server encrypts with AES-GCM and stores ciphertext only.
  4. Server syncs taxonomy from Woo, caches in SQLite.
  5. Client reads taxonomy from /api/v1/admin/taxonomy — no auth needed.
  6. All Woo calls go server-to-Woo. Browser never calls Woo REST directly.

Browser-to-Woo REST would need CORS on the store, which this design does not use.

6. Status machine

draft → calibrated → ready → publishing → published
         │              │                ↑
         └→ failed  ────┴────────────────┘ (woo_product_id set)
any → failed (with error)
  1. draft — user took photos, no mask confirmed yet.
  2. calibrated — user confirmed the BG edge, confirmed length axis, entered length/thickness/SKU. Client computed sqft/bdft/widths. Draft uploaded to server.
  3. ready — SKU, length, thickness, client sqft/bdft/widths, ≥1 inventory PNG, exactly one species, ≥1 wood category, 1 edge type, ≥1 figure term, ≥1 fig-* tag, ≥1 grade, thickness store band (round-up), 1 moisture (default kiln-dried), price. feat-* 0+. Title/description may be typed, generated, or templated at publish. Values can come from Call 1 (≥ 0.7 pre-fill the user kept), manual entry, or a mix. Review gate: below-threshold Call 1 leaves species, wood category, figure, edge, grade, and feat-* empty. That subset is not the submit list. Inline nudges on every still-required control, not submit-only (docs/UX.md). Inference is optional; with inference OFF the user fills everything by hand.
  4. publishing — Woo create in flight. Poll for result.
  5. published — Woo product created, woo_product_id stored. Images purged.
  6. failed — pipeline, inference, or publish error. Error detail stored. Retry available.

Call 1 and Call 2 are assist-only. They do not gate publish. Low-confidence Call 1 results do not auto-fill. If the user skips inference, the path is: calibrated → ready → publishing → published.

7. Taxonomy cache (manufactured last-modified)

Woo REST exposes no usable last-modified on categories, attributes, or tags (only products carry date_modified). The client never talks to Woo. FastAPI is the sole freshness source. It persists two UTC stamps: taxonomy_last_sync_at (every successful Woo pull) and taxonomy_anchor (bumps only when the stored-subset hash changes, including deletions).

  • Anchor bumps only when a row-level change is detected in the stored subset of cache fields (whatever columns the cache tables actually keep: species/wood-category leaves, five attributes + terms, fig-*/feat-* tags). Scope is schema-driven: adding a cache column later automatically includes it.
  • Spelling/name edits, additions, and deletions all bump. Diff must catch missing rows (deletions), not only changed rows.
  • A no-op sync (stored subset unchanged) does not bump, so the client does not churn.
  • Client reads the anchor. If it advanced past the client's last value, re-pull the full taxonomy cache. One timestamp governs categories, attributes, and tags together.
  • Do not invent a Woo-side taxonomy timestamp or a per-table anchor.

Triggers

  1. POST /api/v1/settings/test-woo always resyncs taxonomy after a successful connection test (unlocks the species list).
  2. Publish / product submit: opportunistic resync if the last successful sync is older than 1 hour; bump anchor only if the stored subset changed.
  3. Optional: app launch check (sync only if stale) and a Settings “Refresh taxonomy” control. Not required for POC.
  4. Do not sync on every screen navigation.

Species pickers and Call 1 option lists are exactly this cached set. No hardcoded, invented, or fallback species list. A new Woo species is selectable only after the next sync that lands it in the cache.

8. Shared validation module

One FastAPI module is source of truth (GET /api/v1/validation/module → { hash, rules[] }). Two consumers: review UI and Call 2 prompt builder. Client caches the module.

  • SKU charset: [A-Z0-9-]{3,24} (printable ASCII subset). Title / short_title / description: UTF-8 within Woo maxLength. Not “ASCII-only” and not “extended ASCII”.
  • Field-exit: local check against the cache. Empty optional is valid.
  • Length: maxLength is a hard input cap; server still enforces.
  • Submit carries validation_hash. Match → validate. Mismatch → HTTP 412 + fresh module; UI “Updating rules”; re-validate; retry. Never show 412. 409 is duplicate SKU only. Woo-shaped 422 is store rejection (#18).

9. Call 1 → Call 2 context

Providers differ. Default portable path: resend the full Call 1 turn (prompt + images metadata + assistant JSON) with Call 2. Opt into previous_response_id only after probe.

POST /api/v1/settings/test-inference two-turn probe records stateful | stateless | fail. Fail blocks Call 2 enablement only (Gate C inference-OFF still works). Never require previous_response_id. Do not ship a Call 2 that silently drops Call 1 context.

Per-slab thread on the server (client does not hold provider IDs). Best-effort delete on publish / abandon / fail; log and continue.

Presentation of the review screen: docs/UX.md.