80af35721f
Contradictions resolved: - AGENTS.md: add 'Known stale PRD lines' section (FR17, FR19, FR40, NFR, FR42) so PRD's cross-reference resolves - AGENTS §6: remove 'tag sets' from Call 1 body description (Call 1 only uses species and attribute lists) - PROMPTS.md: shipped defaults path corrected to backend/app/prompts/ (matches DEPLOYMENT repo layout) - PROMPTS.md: Call 1 notes removed feat-* and fig-* from taxonomy list (Call 1 returns free-text observations mapped to feat-* tags by server, not fig-*) - PROMPTS.md: Call 2 template field names aligned with CONTENT-WOO defaults (width_avg_in, width_min_in, width_max_in, length_in_remainder) - UX.md §112: remove 'tag lists' from Call 1 body description - DATA-MODEL: prompt files location corrected from /data/prompts/ to backend/app/prompts/ (bind-mounted read-only to /app/prompts/) - DEPLOYMENT: backup script updated (prompts are version-controlled in repo, no separate backup needed) - docker-compose.yml: prompts mount corrected to ../backend/app/prompts
242 lines
8.5 KiB
Markdown
242 lines
8.5 KiB
Markdown
# Data Model
|
||
|
||
Status: SPEC · 2026-08-27 · SlabUploader
|
||
Storage: single SQLite file. Schema portable to Postgres.
|
||
|
||
Conventions:
|
||
- IDs: client-generated UUIDv4 for slabs so the SvelteKit client can reference a slab before it exists server-side.
|
||
- Timestamps: UTC ISO-8601 TEXT.
|
||
- Soft state via status column; no hard deletes for slabs (audit).
|
||
- Money: float64 USD, rounded half-up to cents at write.
|
||
- All rounded values are what gets stored. No full-precision internal storage.
|
||
- Inches stored as 1/8" increments. Sqft/bdft to 2 decimals.
|
||
- Multi-select fields stored as JSON arrays of Woo IDs.
|
||
|
||
---
|
||
|
||
## slabs
|
||
|
||
Draft-only. Deleted on successful publish. WooCommerce is the source of truth for product data.
|
||
|
||
| Column | Type | Notes |
|
||
|---|---|---|
|
||
| `id` | TEXT PK | client-generated UUID |
|
||
| `sku` | TEXT UNIQUE NOT NULL | `[A-Z0-9-]{3,24}` printable ASCII subset |
|
||
| `status` | TEXT NOT NULL | draft \| calibrated \| ready \| publishing \| published \| failed |
|
||
| `length_in` | REAL NOT NULL | 1/8" step |
|
||
| `thickness_in` | REAL NOT NULL | 1/8" step |
|
||
| `sqft` | REAL | 2 decimals |
|
||
| `bdft` | REAL | 2 decimals |
|
||
| `width_min_in` | REAL | min width at 6" stations, 2 decimals |
|
||
| `width_max_in` | REAL | max width at 6" stations, 2 decimals |
|
||
| `width_avg_in` | REAL | avg width at 6" stations, 2 decimals |
|
||
| `species_id` | INTEGER | → woo_categories (leaf only, 1 required) |
|
||
| `wood_category_ids` | INTEGER[] JSON | → woo_categories (leaf, 1+) |
|
||
| `edge_type_term_id` | INTEGER | → woo_attribute_terms (1 required) |
|
||
| `figure_term_ids` | INTEGER[] JSON | → woo_attribute_terms (1+) |
|
||
| `grade_term_ids` | INTEGER[] JSON | → woo_attribute_terms (1+) |
|
||
| `thickness_term_id` | INTEGER | → woo_attribute_terms (round-up) |
|
||
| `moisture_term_id` | INTEGER | → woo_attribute_terms (1 required, defaults to kiln-dried term; user overrides; not inferred) |
|
||
| `fig_tag_ids` | INTEGER[] JSON | → woo_tags (fig-*, 1+) |
|
||
| `feat_tag_ids` | INTEGER[] JSON | → woo_tags (feat-*, 0+) |
|
||
| `price_per_bdft` | REAL | from pricing_rules for species |
|
||
| `price` | REAL NOT NULL | 2 decimals |
|
||
| `price_source` | TEXT | 'recommendation' \| 'override' |
|
||
| `title` | TEXT | |
|
||
| `short_title` | TEXT | |
|
||
| `description` | TEXT | |
|
||
| `content_source` | TEXT | 'manual' \| 'llm' |
|
||
| `client_rev` | INTEGER NOT NULL DEFAULT 1 | |
|
||
| `server_rev` | INTEGER NOT NULL DEFAULT 1 | |
|
||
| `created_at` | TEXT NOT NULL | |
|
||
| `updated_at` | TEXT NOT NULL | |
|
||
| `inference_thread_id` | TEXT | optional stateful provider id; delete on publish or abandon |
|
||
|
||
Indexes: `sku` (unique), `status`, `updated_at`.
|
||
|
||
### Status state machine
|
||
|
||
```
|
||
draft → calibrated → ready → publishing → published
|
||
│ │ ↑
|
||
└→ failed ────┴────────────────┘ (woo_product_id in sync_log)
|
||
any → failed (with error)
|
||
```
|
||
|
||
1. **draft** — photos taken, no mask confirmed.
|
||
2. **calibrated** — BG edge confirmed, length axis confirmed, length/thickness/SKU entered. Client computed sqft/bdft/widths. Draft uploaded to server.
|
||
3. **ready** — SKU, length_in, thickness_in, 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_term_id (round-up from measured thickness), 1 moisture (default kiln-dried; user override; not inferred), price. feat-* 0+. Title/short_title/description may be typed, generated, or written by templates at publish. Call 1 may prefill only the vision subset when confidence ≥ 0.7. Inference optional. Publishable.
|
||
4. **publishing** — Woo create in flight. Poll for result.
|
||
5. **published** — Woo product created. This row + all photos deleted. sync_log retained.
|
||
6. **failed** — pipeline, inference, or publish error. Error detail stored. Retry available.
|
||
|
||
---
|
||
|
||
## slab_photos
|
||
|
||
| Column | Type | Notes |
|
||
|---|---|---|
|
||
| `id` | TEXT PK | |
|
||
| `slab_id` | TEXT FK → slabs.id ON DELETE CASCADE | |
|
||
| `kind` | TEXT NOT NULL | 'inventory' |
|
||
| `role` | TEXT | 'topdown' \| 'extra' |
|
||
| `seq` | INTEGER | display order |
|
||
| `original_path` | TEXT | relative to images volume |
|
||
| `processed_path` | TEXT | relative to images volume |
|
||
| `created_at` | TEXT | |
|
||
|
||
Rule: only inventory photos. Calibration photos do not exist in this design.
|
||
|
||
---
|
||
|
||
## pricing_rules
|
||
|
||
| Column | Type | Notes |
|
||
|---|---|---|
|
||
| `id` | TEXT PK UUID | |
|
||
| `species` | TEXT NOT NULL | matches woo_categories.name (leaf) |
|
||
| `price_per_bdft` | REAL NOT NULL | flat, no tiers |
|
||
| `created_at` | TEXT | |
|
||
| `updated_at` | TEXT | |
|
||
|
||
`UNIQUE(species)`. Seeded from synced Woo categories. User sets price per species.
|
||
|
||
---
|
||
|
||
## woo_categories
|
||
|
||
| Column | Type | Notes |
|
||
|---|---|---|
|
||
| `woo_id` | INTEGER PK | |
|
||
| `name` | TEXT | |
|
||
| `slug` | TEXT | |
|
||
| `parent_id` | INTEGER | NULL for root |
|
||
| `synced_at` | TEXT | |
|
||
|
||
Only species + wood category. Sync pulls all Woo categories; the app filters to the two we use.
|
||
Species leaves from this table (after filter) are the **only** species the UI and Call 1 may offer.
|
||
|
||
`UNIQUE(woo_id)`.
|
||
|
||
---
|
||
|
||
## woo_attributes
|
||
|
||
| Column | Type | Notes |
|
||
|---|---|---|
|
||
| `woo_id` | INTEGER PK | |
|
||
| `name` | TEXT | |
|
||
| `slug` | TEXT | |
|
||
| `type` | TEXT | 'select' |
|
||
| `synced_at` | TEXT | |
|
||
|
||
Only the five first-class attributes: Edge Type, Figure, Grade, Thickness, Moisture.
|
||
|
||
`UNIQUE(woo_id)`.
|
||
|
||
---
|
||
|
||
## woo_attribute_terms
|
||
|
||
| Column | Type | Notes |
|
||
|---|---|---|
|
||
| `woo_id` | INTEGER PK | |
|
||
| `attribute_id` | INTEGER FK → woo_attributes.woo_id | |
|
||
| `name` | TEXT | |
|
||
| `slug` | TEXT | |
|
||
| `synced_at` | TEXT | |
|
||
|
||
The actual selectable values. E.g., "Cathedral" under Figure, "1\"–1 1/2\"" under Thickness.
|
||
|
||
`UNIQUE(woo_id)`.
|
||
|
||
---
|
||
|
||
## woo_tags
|
||
|
||
| Column | Type | Notes |
|
||
|---|---|---|
|
||
| `woo_id` | INTEGER PK | |
|
||
| `name` | TEXT | |
|
||
| `slug` | TEXT | |
|
||
| `synced_at` | TEXT | |
|
||
|
||
Only `fig-*` and `feat-*` tags. Other Woo tags ignored. WooCommerce is the
|
||
authoritative source for tag definitions; this table caches synced Woo tags.
|
||
|
||
`UNIQUE(woo_id)`.
|
||
|
||
---
|
||
|
||
## settings
|
||
|
||
| Column | Type | Notes |
|
||
|---|---|---|
|
||
| `key` | TEXT PK | |
|
||
| `value_enc` | BLOB | AES-GCM ciphertext |
|
||
| `updated_at` | TEXT | |
|
||
|
||
Keys (server settings; secrets encrypted at rest):
|
||
- `woo_wp_username`, `woo_app_password`
|
||
(username + WordPress application password for a dedicated low-priv WP user;
|
||
not WooCommerce consumer keys)
|
||
- Store URL is **not** a settings row: `WOO_BASE_URL` env (compose), HTTPS only
|
||
- `inference_base_url`, `inference_api_key`, `inference_model`
|
||
- `inference_enabled`, `content_llm_enabled`
|
||
- `brand_voice`, `geo_context`
|
||
- `woo_create_status` ('draft' | 'publish'), default `draft` — Woo product create status key only; safety policy lives in AGENTS.md §5
|
||
- `taxonomy_last_sync_at` — UTC ISO; every successful Woo taxonomy pull
|
||
- `taxonomy_anchor` — UTC ISO; bump only when stored-subset hash changes (incl. deletions). No-op sync does not bump. Not Woo `date_modified`.
|
||
- `aspect_ratio` ('3:4'), `output_px` (1600), `output_px_min` (1600), `fill_target` (0.80)
|
||
|
||
|
||
Sheet/slider prefs (`sheet_mode`, sensitivity, edge offset, feather) are **not** server
|
||
settings keys in POC. Client localStorage is source of truth (AGENTS §4 / ARCHITECTURE §5).
|
||
Optional server sync of those prefs is out of scope for POC.
|
||
|
||
**Prompt files** — not in the settings table. Shipped as text files in
|
||
`backend/app/prompts/` (repo), bind-mounted read-only to `/app/prompts/` in the
|
||
FastAPI container. Read fresh on each invocation. Default prompts shipped with the
|
||
app; owner edits on the host; no restart needed. A reset-to-default script
|
||
restores originals from the app bundle.
|
||
|
||
This table is the ciphertext **key inventory** and schema only. The never-browser /
|
||
AES-GCM / `SLAB_AES_KEY` env policy lives in AGENTS.md §4; flow lives in ARCHITECTURE §5.
|
||
|
||
AES master key is an env var (`SLAB_AES_KEY`, 32 bytes base64). Never in the DB.
|
||
Each `value_enc` = `nonce(12) || ciphertext`.
|
||
|
||
---
|
||
|
||
## sync_log
|
||
|
||
Append-only history. Not deleted on publish.
|
||
|
||
| Column | Type | Notes |
|
||
|---|---|---|
|
||
| `id` | INTEGER PK AUTOINCREMENT | |
|
||
| `slab_id` | TEXT FK | |
|
||
| `sku` | TEXT | |
|
||
| `woo_product_id` | INTEGER | |
|
||
| `status` | TEXT | 'success' \| 'failed' |
|
||
| `http_status` | INTEGER | |
|
||
| `error` | TEXT | machine-readable code |
|
||
| `detail` | TEXT | Woo error body |
|
||
| `payload_hash` | TEXT | sha256 of Woo payload sent |
|
||
| `created_at` | TEXT | |
|
||
|
||
---
|
||
|
||
## inference_log
|
||
|
||
Append-only audit.
|
||
|
||
| Column | Type | Notes |
|
||
|---|---|---|
|
||
| `id` | INTEGER PK AUTOINCREMENT | |
|
||
| `slab_id` | TEXT FK | |
|
||
| `call` | TEXT | '1' \| '2' |
|
||
| `request_payload` | TEXT | JSON of what was sent |
|
||
| `response_payload` | TEXT | JSON of what came back |
|
||
| `created_at` | TEXT | |
|