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).
8.0 KiB
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.
sqftandbdftare stored to 2 decimals half-up.priceis 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 envSLAB_AES_KEYand is never in the DB. - On successful publish the local working slab row is discarded (photos purged, device draft
cleared);
sync_logandinference_logare 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_photosBLOBs; the/imagesvolume 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
sqlite3module and a checked-inschema.sql(CREATE TABLE IF NOT EXISTS); there is no ORM and no migration framework. Pragmas arejournal_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_idFK,created_at,updated_at.prompt_revisions:id,prompt_idFK,contenttext,content_hashtext,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.