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:
Hermes Agent
2026-08-24 12:27:20 -07:00
parent db659e1947
commit bee3bcc278
10 changed files with 1409 additions and 18 deletions
+166
View File
@@ -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.