docs: full implementation plan (PRD decisions locked + specs 1-4 + impl plan)
- PRD: 20 locked decisions, open items cleared, fence fixed - docs/TECH-SPEC-PIPELINE.md: deterministic math + 9 test vectors (deliverable #1) - docs/ARCHITECTURE.md, DATA-MODEL.md, OPENAPI.md (deliverable #2), CONTENT-WOO.md, DEPLOYMENT.md (deliverable #4), IMPL-PLAN.md - README rewritten; .gitignore added AGENTS.md pending Ty's approval (protected file write was blocked).
This commit is contained in:
@@ -0,0 +1,166 @@
|
||||
# Data Model (SQLite, v1)
|
||||
|
||||
Status: SPEC · 2026-08-24 · SlabUploader
|
||||
Storage: single SQLite file (volume `slab_db`). Schema kept portable to Postgres
|
||||
(DATETIME→TIMESTAMPTZ, TEXT blobs→bytea/objects). All math values stored full
|
||||
precision (float64); display rounding is a presentation concern (TECH-SPEC §2).
|
||||
|
||||
## Conventions
|
||||
- IDs: `TEXT` UUIDv4 (client-generated for slabs so the PWA can reference a slab
|
||||
before it exists server-side).
|
||||
- Timestamps: UTC ISO-8601 `TEXT` (`YYYY-MM-DDTHH:MM:SSZ`).
|
||||
- Soft state via `status` column; no hard deletes for slabs (audit). Images purged
|
||||
on publish (Decision 12) but the slab row + metadata retained.
|
||||
- Money stored as float64 USD, rounded half-up to cents at write (TECH-SPEC §2).
|
||||
- `client_rev` / `server_rev`: monotonic ints for optimistic concurrency (ARCH §3.2).
|
||||
|
||||
## ERD (text)
|
||||
```
|
||||
slabs 1─N slab_photos
|
||||
slabs 1─0..1 sync_log (latest) [sync_log is an append-only history; 1 per publish attempt]
|
||||
pricing_rules N─1 (species, character) [unique pair]
|
||||
woo_taxonomy (cache) categories / tags / attributes
|
||||
settings 1 row (key→encrypted value)
|
||||
```
|
||||
|
||||
## Tables
|
||||
|
||||
### slabs
|
||||
| col | type | notes |
|
||||
|---|---|---|
|
||||
| id | TEXT PK | client-generated UUID |
|
||||
| sku | TEXT UNIQUE NOT NULL | from OCR or manual; dedupe key (FR24a) |
|
||||
| status | TEXT NOT NULL | `draft` `processing` `ready` `review` `publishing` `published` `failed` |
|
||||
| species | TEXT | controlled value (from woo_taxonomy categories) or free until confirmed |
|
||||
| species_confidence | REAL NULL | from inference, if used |
|
||||
| species_source | TEXT | `inference` `manual` |
|
||||
| character | TEXT | controlled value |
|
||||
| character_confidence | REAL NULL | |
|
||||
| character_source | TEXT | `inference` `manual` |
|
||||
| length_in | REAL NOT NULL | authoritative (ruler-derived or manual) |
|
||||
| length_source | TEXT | `ruler` `manual` |
|
||||
| width_avg_in | REAL | average of sampled widths (TECH-SPEC §5.2) |
|
||||
| width_source | TEXT | `sampled` `manual` |
|
||||
| thickness_in | REAL NOT NULL | always manual (FR8) |
|
||||
| sqft | REAL | (L×w_avg)/144 |
|
||||
| bdft | REAL | (L×w_avg×t)/12 (Decision 18) |
|
||||
| price_per_bdft | REAL NULL | copied from pricing rule at compute time (snapshot) |
|
||||
| price | REAL NOT NULL | final authoritative price (override or recommendation) |
|
||||
| price_source | TEXT | `recommendation` `override` |
|
||||
| title | TEXT | generated/edited |
|
||||
| short_title | TEXT | |
|
||||
| description | TEXT | |
|
||||
| content_source | TEXT | `template` `llm` `manual` |
|
||||
| normalized_fill_ok | INTEGER | 0/1 — met ~80% fill target (TECH-SPEC TV-6) |
|
||||
| woo_product_id | INTEGER NULL | set on successful publish (FR28) |
|
||||
| published_at | TEXT NULL | |
|
||||
| client_rev | INTEGER NOT NULL DEFAULT 1 | |
|
||||
| server_rev | INTEGER NOT NULL DEFAULT 1 | |
|
||||
| created_at | TEXT NOT NULL | |
|
||||
| updated_at | TEXT NOT NULL | |
|
||||
|
||||
Index: `sku` (unique), `status`, `updated_at`.
|
||||
|
||||
### slab_photos
|
||||
| col | type | notes |
|
||||
|---|---|---|
|
||||
| id | TEXT PK | |
|
||||
| slab_id | TEXT FK→slabs.id ON DELETE CASCADE | |
|
||||
| kind | TEXT NOT NULL | `calibration_sku` `calibration_ruler` `inventory` |
|
||||
| role | TEXT | for inventory: `topdown` (the width-sampling photo) or `extra` |
|
||||
| seq | INTEGER | display order (FR23 reorder) |
|
||||
| original_path | TEXT | relative to images volume |
|
||||
| normalized_path | TEXT NULL | set after normalization |
|
||||
| is_customer_facing | INTEGER | calibration=0, inventory=1 (FR2: calibration never uploaded) |
|
||||
| created_at | TEXT | |
|
||||
|
||||
Rule: only `kind=inventory` photos are uploaded to WooCommerce. Calibration photos
|
||||
are internal-only (FR2).
|
||||
|
||||
### pricing_rules
|
||||
| col | type | notes |
|
||||
|---|---|---|
|
||||
| id | TEXT PK | uuid (PRD pricing schema) |
|
||||
| species | TEXT NOT NULL | |
|
||||
| character | TEXT NOT NULL | |
|
||||
| price_per_bdft | REAL NOT NULL | flat (Decision 5; no tiers) |
|
||||
| created_at | TEXT | |
|
||||
| updated_at | TEXT | |
|
||||
|
||||
`UNIQUE (species, character)`. Seeded from synced Woo categories (Decision 4);
|
||||
user sets `price_per_bdft` per value in Settings (FR13).
|
||||
|
||||
### woo_taxonomy (cache)
|
||||
| col | type | notes |
|
||||
|---|---|---|
|
||||
| kind | TEXT | `category` `tag` `attribute` |
|
||||
| woo_id | INTEGER | Woo term id |
|
||||
| name | TEXT | |
|
||||
| parent_id | INTEGER NULL | categories |
|
||||
| slug | TEXT | |
|
||||
| synced_at | TEXT | |
|
||||
|
||||
`UNIQUE (kind, woo_id)`. Rebuilt by `POST /api/admin/taxonomy/sync` (FR25). This is
|
||||
the controlled list for species (Decision 4 — species drawn from categories) and
|
||||
the taxonomy auto-assign source (FR26).
|
||||
|
||||
### sync_log (append-only history)
|
||||
| col | type | notes |
|
||||
|---|---|---|
|
||||
| id | INTEGER PK AUTOINCREMENT | |
|
||||
| slab_id | TEXT FK | |
|
||||
| attempt | INTEGER | 1,2,… per slab |
|
||||
| action | TEXT | `create` |
|
||||
| woo_product_id | INTEGER NULL | |
|
||||
| status | TEXT | `success` `failed` `blocked_duplicate_sku` |
|
||||
| http_status | INTEGER NULL | |
|
||||
| error | TEXT NULL | machine-readable code |
|
||||
| detail | TEXT NULL | human-readable message / Woo error body |
|
||||
| payload_hash | TEXT | sha256 of the Woo product payload sent (audit, FR28) |
|
||||
| created_at | TEXT | |
|
||||
|
||||
FR28: log sync results, surface errors, store Woo product ID on success.
|
||||
|
||||
### settings (key/value, encrypted values)
|
||||
| col | type | notes |
|
||||
|---|---|---|
|
||||
| key | TEXT PK | |
|
||||
| value_enc | BLOB | AES-GCM ciphertext (see Security) |
|
||||
| updated_at | TEXT | |
|
||||
|
||||
Keys:
|
||||
- `woo_base_url` (plaintext-OK, but encrypted for uniformity)
|
||||
- `woo_consumer_key`, `woo_consumer_secret` (secret)
|
||||
- `inference_base_url`, `inference_api_key` (secret), `inference_model`
|
||||
- `inference_enabled` (bool), `content_llm_enabled` (bool)
|
||||
- `brand_voice` (text), `geo_context` (text)
|
||||
- `content_templates` (JSON: title/short_title/description templates, FR16)
|
||||
- `aspect_ratio` = `1:1`, `output_px` = 1600, `output_px_min` = 1000 (Decision 20)
|
||||
- `fill_target` = 0.80
|
||||
|
||||
The AES-GCM master key is an env var (`SLAB_AES_KEY`, 32 bytes base64) injected by
|
||||
compose — never stored in the DB. Each `value_enc` = `nonce(12) || ciphertext`.
|
||||
|
||||
## Status state machine
|
||||
```
|
||||
draft → processing → ready → review → publishing → published
|
||||
│ ↑ (woo_product_id set)
|
||||
└→ failed (retry → publishing)
|
||||
any → failed (with error)
|
||||
```
|
||||
- `processing`: server pipeline running.
|
||||
- `ready`: enriched, awaiting user review.
|
||||
- `review`: user has opened/edited the review screen.
|
||||
- `publishing`: Woo create in flight.
|
||||
- `published`: Woo product created (draft/pending on the store — never live without
|
||||
human action, per test-target decision).
|
||||
- `failed`: pipeline or publish error; `sync_log.detail` has the reason.
|
||||
|
||||
## Concurrency
|
||||
- Single owner. Optimistic locking via `server_rev`: an update must send the `server_rev`
|
||||
it last read; server rejects on mismatch (409) → client re-fetches. This is the
|
||||
server-side counterpart to the client `client_rev` (ARCH §3.2).
|
||||
|
||||
## Backup
|
||||
- SQLite file + images volume are the only durable state. Backup = `sqlite3 .backup`
|
||||
of the DB file (WAL-safe) + `tar` of the images volume. See DEPLOYMENT.md.
|
||||
Reference in New Issue
Block a user