# Data model contract Status: SPEC · 2026-09-18. Store: SQLite. ## Requirements impact | Requirement | Impact on this design | |---|---| | APP-5 | slab status enum and transitions | | APP-20 | ids, timestamps, revisions | | APP-40 | derived numbers stored, never recomputed | | CAPTURE-25, CAPTURE-30, CAPTURE-70, CAPTURE-80, CAPTURE-85, CAPTURE-90, CAPTURE-95, CAPTURE-105, CAPTURE-115, CAPTURE-130, CAPTURE-135, CAPTURE-160 | slab and photo columns, units, PNG | | METADATA-5, METADATA-10, METADATA-15, METADATA-30 | taxonomy cache tables and slab taxonomy columns | | METADATA-35, METADATA-45, METADATA-55, METADATA-66, METADATA-105, METADATA-106 | inference columns, prompts tables, logs, revision interface | | PUBLISH-25, PUBLISH-30 | purge on success, append-only log | | REVIEW-10, REVIEW-25, REVIEW-45, REVIEW-50 | prose, price, SKU, field limits | | SETTINGS-5, SETTINGS-10, SETTINGS-20, SETTINGS-25, SETTINGS-30, SETTINGS-35, SETTINGS-50, SETTINGS-51, SETTINGS-55, SETTINGS-65, SETTINGS-90 | settings, crypto, rates, taxonomy stamps | ## 1. Conventions - Slab ids are client-generated UUIDv4; all other ids are integer autoincrement. - Timestamps are UTC ISO-8601 text. - Slab status is one of `draft`, `calibrated`, `ready`, `publishing`, `published`, `failed`. - Inches are stored as the rounded 1/8-inch value the user entered. - `sqft` and `bdft` are stored to 2 decimals half-up. - `price` is stored to 2 decimals half-up. - Multi-select taxonomy values are JSON arrays of Woo ids. - Secrets are stored as `value_enc = nonce(12) || ciphertext`, AES-GCM; the master key is the env `SLAB_AES_KEY` and is never in the DB. - On successful publish the local working slab row is discarded (photos purged, device draft cleared); `sync_log` and `inference_log` are append-only and retained. Nothing is deleted from WooCommerce. - Soft delete sets `deleted_at` (text, null until then) on unpublished drafts; deleted rows are excluded from all GET responses. - Photo bytes live in `slab_photos` BLOBs; the `/images` volume is not a second store. It holds a health-probe file and short-lived staging for 1024 px Call-1 JPEGs and Woo media uploads, purged after use. - The store is SQLite through the stdlib `sqlite3` module and a checked-in `schema.sql` (`CREATE TABLE IF NOT EXISTS`); there is no ORM and no migration framework. Pragmas are `journal_mode=WAL`, `foreign_keys=ON`, `busy_timeout`, `synchronous=NORMAL`, with one process-wide connection behind a lock. The database path and image directory are explicit so a test runs against isolated storage. ## 2. `slabs` | Column | Type | Constraint / default | |---|---|---| | `id` | text | PK, client UUIDv4 | | `status` | text | enum, default `draft` | | `sku` | text | 3–24, alphanumeric and hyphen; unique locally | | `length_in` | real | 1/8-inch step | | `thickness_in` | real | 1/8-inch step | | `sqft` | real | 2 dp | | `bdft` | real | 2 dp | | `width_min_in`, `width_max_in`, `width_avg_in` | real | 6-inch stations | | `species_id` | int | FK `woo_categories`, one leaf | | `wood_category_ids` | json | one or more leaf ids | | `edge_type_term_id` | int | one term | | `figure_term_ids` | json | one or more terms | | `grade_term_id` | int | exactly one term | | `thickness_term_id` | int | round-up band term | | `moisture_term_id` | int | default kiln-dried term | | `fig_tag_ids` | json | optional | | `feat_tag_ids` | json | optional | | `price` | real | 2 dp | | `price_source` | text | `recommendation` \| `override` | | `title`, `short_description`, `description` | text | required non-empty; title warn >60, short_description warn >160 | | `content_source` | text | `manual` \| `llm` \| `template` | | `species_confidence`, `figure_confidence`, `grade_confidence` | real | null below 0.7 | | `inference_status` | text | null \| `inferring` \| `done` \| `failed` | | `calibration_photo_id` | text | FK `slab_photos` | | `client_rev`, `server_rev` | int | default 1 | | `woo_product_id` | int | null until publish | | `published_at` | text | null until publish | | `deleted_at` | text | null until soft-deleted; unpublished drafts only | | `created_at`, `updated_at` | text | UTC ISO-8601 | No `inference_thread_id` column exists; provider threads are not used. ## 3. `slab_photos` | Column | Type | Constraint | |---|---|---| | `id` | text | PK, UUID | | `slab_id` | text | FK `slabs` | | `seq` | int | capture order | | `is_calibration` | int (bool) | exactly one true per slab | | `original_blob` | blob | retained while draft/retry | | `processed_blob` | blob | transparent PNG, alpha preserved | | `width_px`, `height_px` | int | processed dimensions | | `created_at` | text | UTC ISO-8601 | The calibration photo exists; other batch photos are visual inventory only. Rows are purged on successful publish. ## 4. `pricing_rules` | Column | Type | Constraint | |---|---|---| | `id` | int | PK | | `species_id` | int | FK `woo_categories`, UNIQUE | | `price_per_bdft` | real | flat, no tiers | | `created_at`, `updated_at` | text | UTC ISO-8601 | ## 5. Taxonomy cache Cites METADATA-5, SETTINGS-50, SETTINGS-51, SETTINGS-55. - `woo_categories` — species and wood-category leaves only; parents group and are never assigned. - `woo_attributes` / `woo_attribute_terms` — exactly the five locked attributes Edge Type, Figure, Grade, Thickness, Moisture, with their terms. - `woo_tags` — only tags whose slug starts with `fig-` or `feat-`; other store tags are ignored. Rebuild is one transaction; on failure the previous cache is kept. ## 6. `settings` | Key | Storage | Default | |---|---|---| | `woo_wp_username` | plain | — | | `woo_app_password` | `value_enc` | — | | `inference_base_url` | `value_enc` | — | | `inference_api_key` | `value_enc` | — | | `inference_model` | plain | — | | `inference_enabled` | plain | `false` | | `content_llm_enabled` | plain | `false` | | `brand_voice` | plain | — | | `geo_context` | plain | — | | `woo_create_status` | plain | `draft` | | `taxonomy_last_sync_at` | plain | — | | `taxonomy_anchor` | plain | — | `WOO_BASE_URL` is not a settings row; it is env, HTTPS only, read-only. ## 7. `prompts` and `prompt_revisions` `docs/contracts/PROMPTS.md` owns the prompt structure, the seed text, and the revision interface; this section owns the storage shape (`METADATA-105`, `METADATA-106`). - `prompts`: `id`, `name` (`call1` | `call2`), `active_revision_id` FK, `created_at`, `updated_at`. - `prompt_revisions`: `id`, `prompt_id` FK, `content` text, `content_hash` text, `created_at`, `created_by`. Revisions are append-only: a save inserts a new row and moves `active_revision_id`; a row is never updated or deleted, so reset-to-revision and reset-to-default are pointer moves. Each prompt has a revision 0 seeded on first run from the seed text in `docs/contracts/PROMPTS.md` §6. `inference_log.call` records which call ran, and `inference_log.prompt_revision_id` records the revision it used (`METADATA-66`). ## 8. `sync_log` Cites PUBLISH-25, PUBLISH-30. Append-only; never deleted on publish. Columns: `id`, `slab_id`, `woo_product_id`, `status` (`success` | `failed`), `payload_hash` (sha256 of the outbound payload), `message`, `created_at`. ## 9. `inference_log` Append-only audit. Columns: `id`, `slab_id`, `call` (1 | 2), `prompt_revision_id` FK, `model`, `request_payload`, `response_payload`, `latency_ms`, `created_at`. `request_payload` is auditable JSON of the model request **without media bytes** (`METADATA-66`). Text parts (active prompt, LISTS / fact block, SKU/dims) are stored in full. Each vision attachment is an `image_ref` (`photo_id`, `seq`, `bytes`, optional `infer_long_edge_px`, `label`) pointing at `slab_photos` — never a `data:image/...;base64,...` URL. The wire request to the inference API still includes images; only the log is redacted. A byte cap remains as a safety net after redaction; normal Call 1/2 rows are far under it. ## 10. Status transitions Cites APP-5. `draft` → `calibrated` → `ready` → `publishing` → `published`, with `failed` reachable from any step and retryable. `calibrated` is set when the draft uploads; `ready` requires the full set.