# M3 Create — build plan (working record) Status: WORKING RECORD · 2026-09-19 · **not normative**. `docs/REQUIREMENTS.md` is the only normative text; `docs/MILESTONES.md` holds the M3 ID subset; behavior is in `docs/capabilities/CREATE.md` and `docs/contracts/`. This file records decisions made on 2026-09-19 so a new session does not re-litigate or drift from them. It **cites** requirement IDs; it defines none. ## 1. Scope Implement Milestone 3 only, per the M3 ID subset in `docs/MILESTONES.md` and `docs/capabilities/CREATE.md`. Do not implement outside that subset unless the owner expands scope in chat. ## 2. Settled contract rulings (owner, 2026-09-19) | # | Question | Ruling | Now lives in | |---|---|---|---| | R1 | Soft delete representation | Add `deleted_at TEXT NULL`; deleted rows excluded from all GET responses; status enum unchanged | `docs/contracts/DATA-MODEL.md` §1–2, `docs/contracts/API.md` §4 | | R2 | Publish completion vs the poll endpoint | The terminal poll returns `woo_product_id`; the local working slab row is then discarded; the device draft is cleared client-side; the app **never deletes from WooCommerce** | `docs/REQUIREMENTS.md` PUBLISH-25, `docs/contracts/API.md` §4 + §7, `docs/contracts/DATA-MODEL.md` §1 | | R3 | Prompt storage | SQLite is the point of record (`prompts` + `prompt_revisions`). File-based prompts are **superseded**; no prompt files ship and the `backend/app/prompts:ro` compose mount was removed | `docs/contracts/PROMPTS.md` (unchanged), `deploy/docker-compose.yml`, `docs/DEPLOYMENT.md` | | R4 | `/images` volume | Photo bytes are SQLite BLOBs (source of truth). `/images` holds only a health-probe file and short-lived staging (1024 px Call-1 JPEGs, Woo media uploads), purged after use. Health goes non-200 when the DB or `/images` is unwritable; `woo_reachable:false` stays 200 | `docs/contracts/DATA-MODEL.md` §1, APP-45 | ## 3. Implementation decisions **Stack.** FastAPI + stdlib `sqlite3` + `schema.sql` (`CREATE TABLE IF NOT EXISTS`). No SQLAlchemy, no Alembic. Pragmas `journal_mode=WAL`, `foreign_keys=ON`, `busy_timeout`, `synchronous=NORMAL`; one process-wide connection behind a lock. The app factory takes `db_path` and `image_dir` so tests get per-test isolation. Revisit an ORM/migrations only after real UAT data exists. **Async (202 + poll).** One in-process job queue with a single consumer — **not** FastAPI `BackgroundTasks` (Starlette background tasks make TestClient 202 tests accidentally synchronous). Status transitions CAS inside `BEGIN IMMEDIATE`. Tests run the queue in an inline mode so contract tests stay deterministic. **Photos.** `slab_photos.original_blob` / `processed_blob` are the source of truth; `/images` per R4. **Prompts.** DB-only with revisions per R3. **Publish.** Per R2 and `docs/contracts/API.md` §7 + `docs/contracts/CONTENT-WOO.md`. On success: set `woo_product_id`, purge photos, delete the working row after the terminal poll is served, clear the device draft. `SLAB-UAT-*` always creates a draft. `max_upload_size` is checked before media upload (`docs/contracts/WOO-API.md` §10). **Other.** - `ready` is server-derived from the REVIEW-5 set. - SKU is entered at the Measure step (UX §2.5); do not widen the Capture union. - The server stores client-sent derived numbers and never recomputes them as source of truth (APP-40). - PUT order: 404 → 409 `revision_conflict` → 409 `duplicate_sku` → 422 `stale_field` → apply → `server_rev++`. - Call 1 fills empty fields only; background inference writes bump `server_rev` and the client adopts the polled rev. - Frontend additions stay outside the Capture union: `frontend/src/api/*`, `frontend/src/create/*`. ## 4. Slice order | Slice | Delivers | M3 IDs | |---|---|---| | S0 | factory, config/startup guards, health/meta, JSON logs, Dockerfile | APP-30, APP-45, APP-55, APP-65, SETTINGS-10, SETTINGS-25, OPS-25 | | S1 | AES-GCM crypto + Settings (masked, write-only secrets) | SETTINGS-5/15/20/25/30/35/75/85/90, QUALITY-125 | | S2 | slabs CRUD, optimistic concurrency, soft delete | APP-5/15/20/35/40, REVIEW-45 | | S3 | photos (multipart → BLOBs, original/processed, 410 after purge) | CAPTURE-95/105/130, PUBLISH-40 | | S4 | Woo client (app-password Basic) + taxonomy sync + test-woo | METADATA-5/10/15/20/30, SETTINGS-40/50/51/55/60/80, QUALITY-35 | | S5 | pricing, deterministic templates, server-derived ready | REVIEW-5/10/15/20/25/30/35/40/50, METADATA-70/80/85/90, QUALITY-90/95 | | S6 | publish (sequence, UAT draft, duplicate/stale/partial-failure) | PUBLISH-5/10/15/20/25/30/31/35/36/37/38/40/45/50/55/60 | | S7 | inference last (Call 1/Call 2, prompt revisions, templates fallback) | METADATA-35/40/45/50/55/60/65/66/75/95/100/105/110/115, SETTINGS-45/70 | | Cross | frontend API client + Review / Settings / Publish screens | (UI for the slices above) | | Exit | inference-off suite, Woo-mock integration, Compose UI E2E, real UAT draft | QUALITY-20/35/50/105/110/120 | TDD every slice: the failing test precedes its implementation; never parallelize RED and GREEN of the same scenario. ## 5. Guardrails - Subagents run on a single local GPU: **one at a time, never in parallel**. - Agents never declare a gate green; Ty owns gate exit (QUALITY-25, QUALITY-30). - Run `python tools/check_requirements.py` after any docs change; it must pass. - Ports `10880`–`10889` are this project's; never bind `:10880`. Dev-test runs on `:10881`, bound `0.0.0.0`. - Create must work with inference off (zero inference calls) — QUALITY-20. - No raw status codes, stack traces, or internal codes reach the owner — QUALITY-120. ## 6. Not yet validated - Long-horizon multi-step agentic loops (more than one or two tool round-trips). - M3 implementation has not started; this file is the pre-build record.