docs: add TEST-SPEC engineer bar (pyramid through PROD)

This commit is contained in:
2026-09-07 02:36:31 +00:00
parent a06a4a0123
commit 72d1cfa920
3 changed files with 292 additions and 0 deletions
+1
View File
@@ -19,6 +19,7 @@ Status: **Gate A freeze** · signed 2026-08-31 PT · docs cut to `main` 2026-09-
| `docs/PROMPTS.md` | Call 1 / Call 2 prompt files |
| `docs/DEPLOYMENT.md` | Compose, Caddy, secrets placement, ops |
| `docs/UX.md` | Presentation: what the mill owner sees and does |
| `docs/TEST-SPEC.md` | Engineer test bar: pyramid cases, phased by gate, through PROD |
| `docs/UAT-PRACTICE.md` | Walkable practice UAT for `SLAB-UAT-*` |
| `docs/IMPL-PLAN.md` | Phases 0–6. Phase 2 is a deferred stub (offline PWA / OCR / ruler). |
| `README.md` | Orientation only |
+2
View File
@@ -16,6 +16,8 @@ Ground rules (from AGENTS.md):
- Inference stays toggleable; the full suite must pass with it disabled.
- One phase = a reviewable, shippable increment.
Test specification: `docs/TEST-SPEC.md` — pyramid cases, phased by gate, affirmative only.
---
## Phase 0 — Hybrid skeleton and client deterministic core (= Gates A+B)
+289
View File
@@ -0,0 +1,289 @@
# TEST-SPEC — Engineer Bar
Status: SPEC · engineer bar · SoT gitea-atd main · 2026-09-07
Purpose: this document defines every affirmative test case for the SlabUploader through PROD. Each case states what must happen, where the rule lives, and how it is verified. The human UAT walk at `docs/UAT-PRACTICE.md` remains the walkable practice script. Tests are organized by pyramid layer and phased by gate. The full suite passes with inference OFF.
---
## Pyramid overview
| Layer | Cadence | Primary SoT |
|---|---|---|
| Unit | TDD before UI consumes | TECH-SPEC-PIPELINE, issue #17 |
| API / contract | with-code | OPENAPI, DATA-MODEL, issue #5/#11 |
| Integration | with-code | CONTENT-WOO, issue #2/#3/#14/#18 |
| UI / E2E | with-code | UX, TECH-SPEC, issue #8/#9/#15 |
| Ops | with-code | DEPLOYMENT, IMPL-PLAN Phase 6 |
---
## Phase 0 / Gate B — Hybrid skeleton + deterministic core
**Exit evidence:** Ty confirms TV-3, TV-6, TV-7, TV-10 output on real fixtures; `GET /api/health` up; AES round-trip; draft store stub.
### Unit — deterministic core (TDD)
| Case | Expected behavior | SoT | Cadence |
|---|---|---|---|
| TV-1 | Green-sheet synthetic photo: interior green-ish patch that does not touch the border stays after chroma-key; U2Net is not invoked | TECH-SPEC §3.1, §3.4 | TDD |
| TV-2 | Black-sheet synthetic photo: dark interior wood that does not touch the border stays after threshold; flood-fill isolates border-connected pixels only | TECH-SPEC §3.1, §3.4 | TDD |
| TV-3 | Slab 96" long, 1.5" thick, mask 0.64 sqft → bdft = 0.96. Identity: 1 sqft at 1" = 1 bdft. No divide-by-12 | TECH-SPEC §4.6 | TDD |
| TV-4 | Width samples at 6" stations on a 96" length axis report min, max, avg perpendicular to the confirmed axis | TECH-SPEC §4.4 | TDD |
| TV-5 | Slab with one square end and one 45° cut: length axis follows prevailing length, widths stay perpendicular after user confirm | TECH-SPEC §4.1, §4.2 | TDD |
| TV-6 | 3:4 crop, slab centered, 80% fill at extremes, transparent PNG, shorter side ≥ 1600 when source allows | TECH-SPEC §5, §7.1 | TDD |
| TV-7 | Undersized source: after 80% crop shorter side would be below 1600 and source cannot supply it → warn, no upscaling, photo not publishable until retake | TECH-SPEC §5, §7.1 | TDD |
| TV-8 | Pricing: bdft 0.96, species $12.50/bdft → rec 12.00; override 150.00 stored as 150.00; no rule → rec empty, manual 99.99 stored | TECH-SPEC §6, §8 TV-8 | TDD |
| TV-9 | Thickness 1" → Woo range rounds up (e.g. 1"–1 1/2") using synced attribute term, not a hardcoded label | TECH-SPEC §8 TV-9, DATA-MODEL slabs | TDD |
| TV-10 | Rounding: 96.04" → 96"; sqft 0.641 → 0.64; bdft 11.525 → 11.53; price 12.345 → 12.35. Half-up to 2 decimals | TECH-SPEC §8 TV-10 | TDD |
### Unit — shared validation (issue #17 substance)
| Case | Expected behavior | SoT | Cadence |
|---|---|---|---|
| V-1 | SKU charset `[A-Z0-9-]{3,24}` accepted; values outside the charset rejected | TECH-SPEC §2, issue #17 | TDD |
| V-2 | Title, short_title, description: UTF-8 within Woo maxLength enforced at field exit and at submit | TECH-SPEC §2, issue #17 | TDD |
| V-3 | Validation module version hash: client caches module; submit carries hash; mismatch returns 412 with fresh module, not 409 | OPENAPI §validation, issue #17 | TDD |
### Unit — AES-GCM settings (issue #5 substance)
| Case | Expected behavior | SoT | Cadence |
|---|---|---|---|
| AES-1 | Encrypt then decrypt round-trips for woo_wp_username and woo_app_password; ciphertext never appears in GET settings | ARCHITECTURE §5, issue #5 | TDD |
| AES-2 | Missing SLAB_AES_KEY env at startup: app refuses to start; no fallback to plaintext | AGENTS §4, DATA-MODEL settings | TDD |
### API/contract — health + draft store stub
| Case | Expected behavior | SoT | Cadence |
|---|---|---|---|
| H-1 | `GET /api/health` returns 200 `{ "status":"ok" }` with `woo_reachable` false when credentials not configured | OPENAPI §health, IMPL-PLAN Phase 0 | with-code |
| D-1 | `POST /api/v1/slabs` accepts multipart with data JSON + files + meta; returns 201 with `status: calibrated` and `server_rev` | OPENAPI §slabs, DATA-MODEL slabs | with-code |
---
## Gate C — Early Phase 1 (first draft listing, inference OFF)
**Exit evidence:** Ty opens one `SLAB-UAT-*` Woo draft listing in the store admin. Inference remains OFF.
### API/contract — create + draft
| Case | Expected behavior | SoT | Cadence |
|---|---|---|---|
| GC-1 | `POST /api/v1/slabs` with `SLAB-UAT-*` SKU creates Woo product with `status: "draft"` regardless of `woo_create_status` setting | AGENTS §5, CONTENT-WOO §2.4 | with-code |
| GC-2 | Duplicate SKU before create: `GET /products?sku={sku}` finds existing → 409 `duplicate_sku` with `{ field:"sku", actions:["edit_sku","open_existing"] }` | OPENAPI §slabs, issue #3 | with-code |
| GC-3 | `POST /api/v1/settings/test-woo` returns ok with populated taxonomy; always resyncs taxonomy on success | OPENAPI §settings, issue #14 | with-code |
| GC-4 | `WOO_BASE_URL` is compose-only HTTPS; Settings GET returns it read-only; PUT rejects write attempts with 422 | AGENTS §5, issue #11 | with-code |
### Integration — Woo mock (draft create, SLAB-UAT-*, duplicate SKU, taxonomy)
| Case | Expected behavior | SoT | Cadence |
|---|---|---|---|
| W-1 | Woo mock returns a new product with status draft; server stores `woo_product_id` and purges images | CONTENT-WOO §2.4, IMPL-PLAN Phase 4 | with-code |
| W-2 | Woo mock returns 409 for duplicate SKU; server maps to `{ field:"sku", actions:["edit_sku","open_existing"] }`; draft retained | OPENAPI §slabs, issue #3 | with-code |
| W-3 | Taxonomy sync returns cached data; anchor stays unchanged on no-op sync; anchor bumps only when stored-subset hash changes | ARCHITECTURE §7, issue #14 | with-code |
| W-4 | `test-woo` always runs taxonomy resync after successful connection test | ARCHITECTURE §7, issue #14 | with-code |
### Integration — inference OFF path + test-inference probe (issue #2 substance)
| Case | Expected behavior | SoT | Cadence |
|---|---|---|---|
| INF-1 | With `inference_enabled: false`: slab create skips Call 1; `inference_status` is null; full suite passes end-to-end with no inference network calls | AGENTS §6, IMPL-PLAN Phase 5 | with-code |
| INF-2 | `POST /api/v1/settings/test-inference` returns `{ ok: true, mode: "stateful", vision_capable: true }` on a stateful provider | OPENAPI §settings, issue #2 | with-code |
| INF-3 | `test-inference` returns `{ ok: true, mode: "stateless", vision_capable: true }` on a stateless provider; Call 2 uses full resend | OPENAPI §settings, issue #2 | with-code |
| INF-4 | `test-inference` returns `{ ok: false, mode: "fail" }`: Call 2 enablement blocked; Gate C path remains green with inference OFF | OPENAPI §settings, issue #2 | with-code |
### UI/E2E — capture knobs, mask/axis, ready gates (phone + PC)
| Case | Expected behavior | SoT | Cadence |
|---|---|---|---|
| UI-1 | Four inline knobs (sheet, sensitivity, edge offset, feather) on capture screen: changing any knob immediately re-runs mask and refreshes overlay without leaving the screen | TECH-SPEC §3.3, UX §Capture, issue #15 | with-code |
| UI-2 | Mask overlay on source photo: user confirms edge; below-threshold edge after tuning → retake; no U2Net control in POC | TECH-SPEC §3.2, §3.5, UX §Capture | with-code |
| UI-3 | Length axis overlay: user rotates if skewed; samples and sqft update live; nearly-square slab: user picks length direction | TECH-SPEC §4.2, UX §Capture | with-code |
| UI-4 | Numbers entry: length 1/8" steps, thickness pick, SKU typed; computed sqft/bdft/widths displayed | TECH-SPEC §4, UX §Capture | with-code |
| UI-5 | Ready-set gates: slab cannot reach `ready` until full named set is filled (SKU, length, thickness, sqft/bdft/widths, ≥1 PNG, species, wood category, edge, figure, grade, thickness band, moisture, price); inline nudges per empty required field, not a submit-time list | AGENTS §7, UX §Review, issue #9 | with-code |
### Ops — compose health
| Case | Expected behavior | SoT | Cadence |
|---|---|---|---|
| OPS-1 | `GET https://${APP_HOSTNAME}/api/health` returns ok after compose up with filled INVENTORY | DEPLOYMENT §8, IMPL-PLAN Phase 0 | with-code |
---
## Phase 1 remainder — Full API surface
**Exit evidence:** Ty drives curl through a full calibrated → ready cycle; OPENAPI endpoints match; revision conflict handled; taxonomy anchor logic verified.
### API/contract — all routes, concurrency, secrets redaction
| Case | Expected behavior | SoT | Cadence |
|---|---|---|---|
| API-1 | `PUT /api/v1/slabs/{id}` updates fields with `client_rev`; server echoes new `server_rev`; mismatch returns 409 `revision_conflict` with `current_server_rev` | OPENAPI §conventions, DATA-MODEL slabs | with-code |
| API-2 | `PUT /api/v1/slabs/{id}` stores client-sent derived fields (sqft, bdft, widths); server does not recompute them as SoT | OPENAPI §slabs PUT, ARCHITECTURE §3 | with-code |
| API-3 | `GET /api/v1/settings` returns `SettingsView` with `woo_credentials_configured` boolean; `woo_app_password` never returned | OPENAPI §settings, issue #5 | with-code |
| API-4 | `POST /api/v1/slabs/{id}/infer-taxon` returns 202 `{ status: "inferring" }`; client polls `GET /api/v1/slabs/{id}` for results; timeout > 45s → 504 | OPENAPI §inference, issue #8 | with-code |
| API-5 | `POST /api/v1/slabs/{id}/infer-content` returns 202; requires `test-inference` ok; below-threshold Call 1 (confidence < 0.7): fields stay empty | OPENAPI §inference, AGENTS §6, issue #8/#9 | with-code |
| API-6 | `POST /api/v1/slabs/{id}/publish`: pre-condition check returns 422 with field keys for missing required fields; UI maps fields, no raw code | OPENAPI §publish, issue #18 | with-code |
| API-7 | `POST /api/v1/admin/taxonomy/sync`: pulls categories/attributes/tags; rebuilds cache; anchor bumps only on stored-subset hash change; 502 retains previous cache | OPENAPI §taxonomy, ARCHITECTURE §7, issue #14 | with-code |
| API-8 | `GET /api/v1/validation/module` returns hash and rules; client caches; submit mismatch returns 412 with fresh module | OPENAPI §validation, issue #17 | with-code |
### Integration — taxonomy sync + anchor
| Case | Expected behavior | SoT | Cadence |
|---|---|---|---|
| TAX-1 | Sync on publish when `now - taxonomy_last_sync_at > 1 hour`; bump anchor only on real change; stale picked id → 422 with current options | ARCHITECTURE §7, CONTENT-WOO §2.2, issue #14 | with-code |
| TAX-2 | Species picker offers only Woo-synced species; no hardcoded fallback; new species selectable only after sync stores it | ARCHITECTURE §7, issue #12 | with-code |
---
## Phase 3 — Frontend review + publish UI
**Exit evidence:** Ty publishes a test slab to the real store as a Woo draft with `SLAB-UAT-*`.
### UI/E2E — review, field recovery, publish outcome (phone + PC)
| Case | Expected behavior | SoT | Cadence |
|---|---|---|---|
| UIE-1 | Review screen: all fields editable; client-computed sqft/bdft/widths shown with manual override; typed override persists across poll/refresh | IMPL-PLAN Phase 3, UX §Review | with-code |
| UIE-2 | Call 1 prefill ≥ 0.7: fields pre-filled and mutable; user override sticks; below 0.7 fields empty; inline nudges for every still-required control | AGENTS §6, issue #8/#9, UX §Review | with-code |
| UIE-3 | Duplicate SKU at publish: under SKU field, "This SKU already exists in the store (any status)"; edit SKU or open existing; draft retained; no raw 409 | AGENTS §5, CONTENT-WOO §2.5, issue #3, issue #18 | with-code |
| UIE-4 | Stale field at publish: field shows "This value is no longer valid. Pick from the current list"; draft retained; no raw 422 | CONTENT-WOO §2.6, issue #18 | with-code |
| UIE-5 | Publish success: screen shows listing created, woo_product_id when available; local draft and photos gone; slab status `published` | CONTENT-WOO §2.6, DATA-MODEL slabs | with-code |
| UIE-6 | Publish failure: plain message, no status number, no stack; retry from same screen without re-photographing | CONTENT-WOO §2.6, issue #18 | with-code |
| UIE-7 | Inference OFF: full manual taxonomy + content path works; no inference network calls; Generate text disabled; templates still work | IMPL-PLAN Phase 3, UX §Review | with-code |
| UIE-8 | Settings UI: WP username + app password + test-woo; inference endpoint + test; pricing rules; `woo_create_status`; store URL read-only | IMPL-PLAN Phase 3, issue #11 | with-code |
### UI/E2E — capture knobs live re-run (issue #15 substance)
| Case | Expected behavior | SoT | Cadence |
|---|---|---|---|
| UIE-9 | Changing sensitivity slider immediately refreshes mask overlay without leaving screen or server round trip | TECH-SPEC §3.3, issue #15, UX §Capture | with-code |
| UIE-10 | Changing edge offset pulls cut in/out; feather changes alpha blend; sheet switch (auto/green/black) re-runs; all persisted in localStorage until reset | TECH-SPEC §3.3, issue #15 | with-code |
---
## Phase 5 — Inference (after Gate C is green)
**Exit evidence:** Ty runs species detection on 3 real slabs; results acceptable or manually corrected.
### Integration — Call 1/2, portable context, confidence gating
| Case | Expected behavior | SoT | Cadence |
|---|---|---|---|
| C1-1 | Call 1 sends all inventory photos @1024 + Woo taxonomy only + SKU/length/thickness; model constrained to cached taxonomy from Woo cache | IMPL-PLAN Phase 5, issue #12 | with-code |
| C1-2 | Confidence ≥ 0.7: species, wood categories, edge, figure, grade, feat-* pre-filled and mutable; user override wins | AGENTS §6, issue #8/#9 | with-code |
| C1-3 | Confidence < 0.7: those fields stay empty; ready blocked until full named set is filled; species + one figure is not the gate | AGENTS §6, issue #8/#9 | with-code |
| C2-1 | Call 2 triggered only by user tap; portable Call 1 context: full resend default; confirmed vs inferred labels carried | ARCHITECTURE §9, issue #2 | with-code |
| C2-2 | Test-inference stateful mode: `previous_response_id` used when available; stateless mode: full resend; fail mode: Call 2 blocked | OPENAPI §settings, issue #2 | with-code |
| C2-3 | LLM does not invent dimensions; templates inject deterministic numbers into title/description | PROMPTS §Call 2, AGENTS §6 | with-code |
| C2-4 | Audit: inference_log stores request_payload and response_payload per call; prompt file edits apply without restart | DATA-MODEL inference_log, PROMPTS §Management | with-code |
### UI/E2E — inference OFF path
| Case | Expected behavior | SoT | Cadence |
|---|---|---|---|
| OFF-1 | Full suite (calibrated → ready → publish → success) passes with `inference_enabled: false`; zero calls to inference endpoint asserted | AGENTS §6, IMPL-PLAN Phase 5 | with-code |
| OFF-2 | `test-inference` fail mode blocks Call 2 enablement only; Gate C inference-OFF path unaffected | OPENAPI §settings, issue #2 | with-code |
---
## Phase 6 / Ops — Deployment, UAT, hardening
**Exit evidence:** Ty signs off on UAT results; v1 declared done.
### Ops — compose health, deploy path, backup/restore
| Case | Expected behavior | SoT | Cadence |
|---|---|---|---|
| OPS-2 | INVENTORY.md filled: APP_HOST, APP_HOSTNAME, ports verified free, Caddy location confirmed, DNS reachable from phone on LAN/Tailscale | DEPLOYMENT §1, IMPL-PLAN Phase 6 | with-code |
| OPS-3 | Compose health: `GET https://${APP_HOSTNAME}/api/health` ok; `test-woo` ok with species populated; `woo_create_status=draft`; inference OFF for early gates | DEPLOYMENT §8, UAT-PRACTICE §1 | with-code |
| OPS-4 | Backup: `deploy/backup.sh` creates WAL-safe `.backup` of slab.db and tar of images volume; retention 7 daily + 4 weekly | DEPLOYMENT §6 | with-code |
| OPS-5 | Restore: stop compose → restore DB + untar images → start compose → health ok; tested at least once before declaring backup done | DEPLOYMENT §6, IMPL-PLAN Phase 6 | with-code |
### Ops — instrumentation (issue #6 substance)
| Case | Expected behavior | SoT | Cadence |
|---|---|---|---|
| INS-1 | Health non-200 on DB or volume failure; `woo_reachable` false on Woo unreachable | OPENAPI §health, issue #6 | with-code |
| INS-2 | Request ID logged on every server request | issue #6 | with-code |
| INS-3 | Inference latency, model name, and confidence recorded in inference_log | DATA-MODEL inference_log, issue #6 | with-code |
| INS-4 | Sync_log written with payload_hash on every publish attempt; retains success and failure | DATA-MODEL sync_log, issue #6 | with-code |
### Ops — E2E on all target browsers
| Case | Expected behavior | SoT | Cadence |
|---|---|---|---|
| E2E-1 | Capture → mask/axis confirm → calibrated upload → review → publish → Woo draft `SLAB-UAT-*` works on iOS Safari (phone) | UX §Browser compatibility, IMPL-PLAN Phase 6 | with-code |
| E2E-2 | Same flow works on Android Chrome (phone) | UX §Browser compatibility, IMPL-PLAN Phase 6 | with-code |
| E2E-3 | Same flow works on desktop Chrome, Edge, Firefox (PC) | UX §Browser compatibility, IMPL-PLAN Phase 6 | with-code |
| E2E-4 | Manual length override recalculates widths/sqft/bdft correctly on all three browser families | TECH-SPEC §4.3, IMPL-PLAN Phase 6 | with-code |
| E2E-5 | Client crop: slab ~80% frame; shorter side ≥ 1600 when source allows; undersized source warns and requests retake | TECH-SPEC §5, IMPL-PLAN Phase 6 | with-code |
---
## Cadence summary
- **Deterministic core (TV-1…TV-10, validation, AES):** TDD. Unit tests written before any UI consumes the functions. Property checks: bdft monotonic in thickness/sqft; fill band; no upscale path.
- **UI, FastAPI, Woo/inference adapters:** tests-with-code. Each feature commit includes its test and shown execution output.
- **Integration tests:** httpx TestClient with Woo mock (respx) for success, error, duplicate, stale-field, and taxonomy-anchor paths.
- **Full suite must pass with inference OFF.** Call 1/Call 2 cases gated after Gate C is green.
- **bdft = sqft × thickness_in (no /12).** TV-3 is the identity proof. Gate B requires TV-3, TV-6, TV-7, TV-10 show-output on real fixtures.
---
## Cross-reference
| This spec | Live doc |
|---|---|
| TV-1…TV-10 | `docs/TECH-SPEC-PIPELINE.md` §8 |
| V-1…V-3 | `docs/TECH-SPEC-PIPELINE.md` §2, `docs/OPENAPI.md` §validation |
| AES-1…AES-2 | `docs/ARCHITECTURE.md` §5, `docs/AGENTS.md` §4 |
| GC-1…GC-4, W-1…W-4 | `docs/CONTENT-WOO.md` §2.4-2.5, `docs/OPENAPI.md` §slabs/§settings |
| INF-1…INF-4 | `docs/OPENAPI.md` §settings, `docs/AGENTS.md` §6 |
| UI-1…UI-5 | `docs/TECH-SPEC-PIPELINE.md` §3-§4, `docs/UX.md` §Capture/§Review |
| UIE-1…UIE-10 | `docs/UX.md` §Review/§Publish, `docs/CONTENT-WOO.md` §2.5-2.6 |
| C1-1…C2-4 | `docs/AGENTS.md` §6-§7, `docs/PROMPTS.md` §Call 1/2 |
| OPS-1…OPS-5, E2E-1…E2E-5 | `docs/DEPLOYMENT.md` §1-§8, `docs/IMPL-PLAN.md` Phase 6 |
| INS-1…INS-4 | `docs/AGENTS.md` §6, `docs/DATA-MODEL.md` §sync_log/inference_log |
---
## Issue #19 mapping
This spec folds affirmative case substance from issue #19 where the rule exists in a live doc. Issue #19 is not closed by this work.
| Issue #19 item | Cases | SoT doc(s) |
|---|---|---|
| #2 portable multi-turn | INF-2, INF-3, INF-4, C2-1, C2-2 | OPENAPI §settings, ARCHITECTURE §9 |
| #3 duplicate SKU wording | GC-2, W-2, UIE-3 | OPENAPI §slabs, CONTENT-WOO §2.5 |
| #5 Application Passwords | AES-1, AES-2, API-3 | ARCHITECTURE §5, DATA-MODEL settings |
| #6 instrumentation | INS-1, INS-2, INS-3, INS-4 | DATA-MODEL sync_log/inference_log |
| #8/#9 Call 1 prefill + 0.7 | UIE-2, C1-2, C1-3 | AGENTS §6, PROMPTS §Call 1 |
| #11 WOO_BASE_URL | GC-4, UIE-8 | AGENTS §5, DEPLOYMENT §3 |
| #12 synced-species-only | C1-1, TAX-2 | ARCHITECTURE §7, CONTENT-WOO §2.2 |
| #14 taxonomy_anchor | TAX-1, W-3, W-4, API-7 | ARCHITECTURE §7, OPENAPI §taxonomy |
| #15 four inline knobs | UI-1, UIE-9, UIE-10 | TECH-SPEC §3.3, UX §Capture |
| #17 shared validation | V-1, V-2, V-3, API-8 | OPENAPI §validation |
| #18 409/422 recovery | UIE-3, UIE-4, UIE-5, UIE-6 | CONTENT-WOO §2.5-2.6, OPENAPI §slabs |
---
## Gates and phase map
| Phase / Gate | Cases | Exit evidence |
|---|---|---|
| Phase 0 / Gate B | TV-1…TV-10, V-1…V-3, AES-1…AES-2, H-1, D-1, OPS-1 | Ty confirms TV-3, TV-6, TV-7, TV-10 on real fixtures; health up; AES round-trip; draft store |
| Gate C (early Phase 1) | GC-1…GC-4, W-1…W-4, INF-1…INF-4, UI-1…UI-5 | Ty opens one `SLAB-UAT-*` Woo draft; inference OFF |
| Phase 1 remainder | API-1…API-8, TAX-1, TAX-2 | Ty drives curl calibrated → ready; OPENAPI matches; revision + taxonomy green |
| Phase 3 | UIE-1…UIE-10 | Ty publishes test slab as Woo draft `SLAB-UAT-*` |
| Phase 5 | C1-1…C2-4, OFF-1, OFF-2 | Ty runs species detection on 3 real slabs; inference OFF path green |
| Phase 6 / PROD | OPS-2…OPS-5, INS-1…INS-4, E2E-1…E2E-5 | Ty signs off on UAT; v1 done |
---
## Rules that do not change
- Inference is optional. The full suite passes with inference OFF. Call 1/2 cases are gated after Gate C.
- `bdft = sqft × thickness_in`. No divide-by-12. TV-3 proves the identity.
- Client is authoritative for deterministic math; server stores client-sent numbers.
- No new product locks in this document. All rules reference existing live docs or issue #19 substance.