41401a7266
Log full text + image_ref (photo_id/seq/bytes) only; wire path still sends data-URLs. Call 1/2 SELECT id+seq for refs. Cap is safety net after redaction (METADATA-66).
177 lines
8.0 KiB
Markdown
177 lines
8.0 KiB
Markdown
# 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 the fig-* and feat-* families; 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.
|