feat(gate-b): Gate B mask + BDFT + length axis (#21–#23) #31

Closed
grokbot_ty wants to merge 41 commits from gate-b into main
2 changed files with 356 additions and 215 deletions
Showing only changes of commit 302d69463b - Show all commits
+305 -186
View File
@@ -51,7 +51,125 @@ Other abbreviations used in this document:
--- ---
## 2. How cases are executed ## 2. Evidence & process contract
This contract is the mill-owner enforcement bar for test evidence. Soft words do not count. Tables below are the rules.
### Evidence class vocabulary
Every case row and every Results row must declare one Evidence class. No class means the row cannot be marked `pass`.
| Evidence class | What it proves | Honest sources |
|---|---|---|
| `product-state` | Real product behavior on real fixtures or the real deployed path | Real slab photos, real store draft opened by Ty, real host HTTPS health |
| `deploy` | The inventoried host and compose path work as specified | Compose up, Caddy HTTPS, inventory filled, backup/restore on the real host |
| `e2e` | A real browser session through the product UI | Playwright (or equivalent) against a started stack |
| `human-walk` | A person walked the product and recorded what they saw | Ty or delegate walk notes tied to named case IDs |
| `proxy-synthetic` | Synthetic doubles only. Unit fixtures, in-process fakes, stub UIs, mocked Woo/inference, wrong-interface curls | Vitest/pytest fixtures, respx mocks, stub frontend |
| `api-contract` | HTTP request/response shape and status through the app API | httpx TestClient or equivalent against FastAPI |
Extend this list only when an existing table needs a clear new class. Do not invent classes ad hoc in a Results file.
### Proxy isolation
Proxy evidence may fill only cells whose declared Evidence class is `proxy-synthetic`, or a cell that explicitly names a proxy class.
Proxy evidence never fills `product-state`, `deploy`, `e2e`, or `human-walk`.
Examples of proxy evidence include synthetic image fixtures, in-process fakes, stub UIs, mocked Woo or inference, and curls that hit the wrong interface.
### Fail-closed results
Allowed result vocabulary only:
| Result | Meaning |
|---|---|
| `pass` | The named case ran, evidence matches the declared Evidence class, and the expected outcome held |
| `fail` | The named case ran and the expected outcome did not hold. Blocker pointer required |
| `blocked` | The named case could not be run to a verdict. Blocker pointer required |
Soft or ambiguous words do not count as `pass`. That includes "looks good", "mostly", "deferred", "follow-up", and treating an unrun case as quiet success.
`not run` is not an allowed result. Use `blocked` with a blocker pointer.
No Evidence class on a Results row means that row cannot be marked `pass`.
### Gate exit is its own verifiable unit
Three different things:
| Unit | What it is | Who closes it |
|---|---|---|
| Coding tip | Code or docs landed on the branch | Anyone with write access |
| Test complete | Named case rows have Results with honest Evidence class and `pass`/`fail`/`blocked` | The test runner report |
| Gate ready | Gate exit checklist is filled with the required Evidence classes, and Ty confirms | Ty only |
Coding tip is not test complete. Test complete is not Gate ready.
Gate or UAT exit needs a mill-owner Results table whose proof classes match the declared exit checklist for that gate.
Bots never declare Gate B or Gate C green.
### Dual proof for product-state TV cases
Some TV cases list `product-state` as the case Evidence class because Verified by requires real fixtures (TV-3, TV-6, TV-7, TV-10).
A separate Results row may record a `proxy-synthetic` `pass` for the unit runner. That row is honest proxy proof only. It does not satisfy Gate B exit for those cases.
Gate B exit still requires `product-state` or `human-walk` evidence on real fixtures for TV-3, TV-6, TV-7, and TV-10.
### Build brief required fields
A product or gate Build brief must name all three fields below. If the ticket claims product or gate proof and any field is missing, refuse the brief.
| Required field | What to write |
|---|---|
| `product state induced` | Which real product state this case creates or observes |
| `dishonest proxies for this case` | Which doubles would look like a pass but do not count |
| `does NOT count as pass` | Explicit non-pass list for this case |
### SoT mutation permit
Personas, skills, `docs/TEST-SPEC.md`, and Builds that change Source of Truth need explicit Ty go (or PM relaying that go) recorded before the write.
| Field | Required |
|---|---|
| What SoT artifact changes | yes |
| Ty go (or PM relay of Ty go) | yes, dated, before the write |
| Why the change is needed | yes |
Approval sits before the write, not after the apology.
---
## 3. Results recording & report-out
Every report maps to named case IDs from this spec.
Allowed results only: `pass`, `fail`, `blocked`. Never invent `pass`. Soft words do not count.
Every Results row must include Evidence class. Allowed classes are listed in §2.
Evidence is required for every `pass` or `fail`. Use a path or paste of real runner output (pytest, httpx, compose logs, curl, walk notes). No evidence means `blocked` with a blocker pointer.
Report table columns:
| Column | Required |
|---|---|
| Case | yes |
| What this proves (mill-owner English) | yes for gate Results |
| Evidence class | yes |
| Result | yes (`pass` \| `fail` \| `blocked`) |
| Evidence | yes for `pass`/`fail`; pointer or blocker for `blocked` |
| Blocker | required when result is `fail` or `blocked` |
Gitea issues are the only mechanism for reporting issues found during a test pass. Chat, Slack, and PR comments are not the bug log. On fail, open or update a Gitea issue that includes the case id and evidence pointer. The report table blocker column links that issue. Do not invent issues that lack runner evidence.
The Test Bar may only summarize what runners printed. Ty also reads raw results and owns gate exit. Gate or phase exit evidence in the Gates section still requires Ty confirmation where IMPL-PLAN mandates it. Bots never declare Gate B or Gate C green.
When code does not exist yet, Phase 0 and earlier cases stay `blocked` with a blocker that names the missing surface. Do not speculate.
## 4. How cases are executed
Test automation lives in the repo under `backend/tests/` and `frontend/e2e/`. The harness uses: Test automation lives in the repo under `backend/tests/` and `frontend/e2e/`. The harness uses:
@@ -65,21 +183,7 @@ Test dependencies are pinned in `backend/tests/requirements-test.txt` and `front
--- ---
## Results recording & report-out ## 5. Test pyramid overview
Every report maps to named case IDs from this spec. Allowed results only: `pass`, `fail`, `not run`, and optionally `blocked` with a blocker pointer. Never invent pass.
Evidence is required for every `pass` or `fail`: a path or paste of real runner output (pytest, httpx, compose logs, curl). No evidence → `not run`. Report table columns are: case id · result · evidence pointer · blocker (if fail).
Gitea issues are the only mechanism for reporting issues found during a test pass. Chat, Slack, and PR comments are not the bug log. On fail, open or update a Gitea issue that includes the case id and evidence pointer; the report table's blocker column links that issue. Do not invent issues that lack runner evidence.
The Test Bar may only summarize what runners printed. Ty also reads raw results and owns gate exit. Gate or phase exit evidence in the Gates section still requires Ty confirmation where IMPL-PLAN mandates it — bots never declare Gate B or Gate C green.
When code does not exist yet, Phase 0 and earlier cases stay `not run`. Do not speculate.
---
## 3. Test pyramid overview
Each row states the test layer, when tests in that layer are written, and what document drives the expected behavior. Each row states the test layer, when tests in that layer are written, and what document drives the expected behavior.
@@ -97,272 +201,287 @@ Each row states the test layer, when tests in that layer are written, and what d
--- ---
## 4. Fundamentals — cross-cutting bar ## 6. Fundamentals — cross-cutting bar
These cases are the white-box bar for every dependency that touches the product. They run across all phases. These cases are the white-box bar for every dependency that touches the product. They run across all phases.
### SvelteKit / Vitest (frontend/src/__tests__/) ### SvelteKit / Vitest (frontend/src/__tests__/)
| Case | What must happen | Where the rule lives | Verified by | | Case | What must happen | Where the rule lives | Verified by | Evidence class |
|---|---|---|---| |---|---|---|---|---|
| FUND-1 | Pure logic tests have zero browser, DOM, or network dependencies | SvelteKit/Vitest convention | Vitest unit test: `vi.mock` any runtime import, assert no browser globals | | FUND-1 | Pure logic tests have zero browser, DOM, or network dependencies | SvelteKit/Vitest convention | Vitest unit test: `vi.mock` any runtime import, assert no browser globals | `proxy-synthetic` |
| FUND-2 | Component/browser tests use semantic `getByRole` selectors; class or data-testid alone is not sufficient | SvelteKit testing-library convention | Vitest component test: assert role-based query succeeds | | FUND-2 | Component/browser tests use semantic `getByRole` selectors; class or data-testid alone is not sufficient | SvelteKit testing-library convention | Vitest component test: assert role-based query succeeds | `proxy-synthetic` |
| FUND-3 | Real Tab key navigates focus; click-only tests are not sufficient for keyboard paths | UX.md §Browser compatibility | Playwright E2E: `page.keyboard.press('Tab')` followed by `toBeFocused` | | FUND-3 | Real Tab key navigates focus; click-only tests are not sufficient for keyboard paths | UX.md §Browser compatibility | Playwright E2E: `page.keyboard.press('Tab')` followed by `toBeFocused` | `e2e` |
| FUND-4 | Shared rune/state changes flush before DOM assertion; stale store values do not pass | Svelte rune reactivity | Vitest component test: flush/store tick before `expect` | | FUND-4 | Shared rune/state changes flush before DOM assertion; stale store values do not pass | Svelte rune reactivity | Vitest component test: flush/store tick before `expect` | `proxy-synthetic` |
### Playwright (frontend/e2e/) ### Playwright (frontend/e2e/)
| Case | What must happen | Where the rule lives | Verified by | | Case | What must happen | Where the rule lives | Verified by | Evidence class |
|---|---|---|---| |---|---|---|---|---|
| FUND-5 | Tab order: press Tab, assert `toBeFocused` after each stop; no keyboard trap | Playwright convention; UX.md §Browser compatibility | Playwright E2E: sequential `keyboard.press('Tab')` + `toBeFocused` | | FUND-5 | Tab order: press Tab, assert `toBeFocused` after each stop; no keyboard trap | Playwright convention; UX.md §Browser compatibility | Playwright E2E: sequential `keyboard.press('Tab')` + `toBeFocused` | `e2e` |
| FUND-6 | Viewport resize/reflow at ~320 px and phone/PC sizes: layout does not clip, overlap, or trap content | UX.md §Browser compatibility | Playwright E2E: `setViewportSize` then assert no clip/overlap | | FUND-6 | Viewport resize/reflow at ~320 px and phone/PC sizes: layout does not clip, overlap, or trap content | UX.md §Browser compatibility | Playwright E2E: `setViewportSize` then assert no clip/overlap | `e2e` |
| FUND-7 | Focus ring visible on touch and keyboard; focus ring is not hidden by `outline: none` on interactive elements | Playwright convention; WCAG 2.4.7 | Playwright E2E: assert focus ring CSS present on focused interactive element | | FUND-7 | Focus ring visible on touch and keyboard; focus ring is not hidden by `outline: none` on interactive elements | Playwright convention; WCAG 2.4.7 | Playwright E2E: assert focus ring CSS present on focused interactive element | `e2e` |
### FastAPI / pytest TestClient (backend/tests/) ### FastAPI / pytest TestClient (backend/tests/)
| Case | What must happen | Where the rule lives | Verified by | | Case | What must happen | Where the rule lives | Verified by | Evidence class |
|---|---|---|---| |---|---|---|---|---|
| FUND-8 | App factory pattern creates a fresh app instance per test; no shared mutable state between tests | FastAPI testing convention | pytest TestClient: each test calls the factory, not a module-level app | | FUND-8 | App factory pattern creates a fresh app instance per test; no shared mutable state between tests | FastAPI testing convention | pytest TestClient: each test calls the factory, not a module-level app | `api-contract` |
| FUND-9 | `with TestClient(app)` manages lifespan; startup and shutdown hooks run and clean up | FastAPI testing convention | pytest TestClient: context manager exits with clean lifespan | | FUND-9 | `with TestClient(app)` manages lifespan; startup and shutdown hooks run and clean up | FastAPI testing convention | pytest TestClient: context manager exits with clean lifespan | `api-contract` |
| FUND-10 | Clear `dependency_overrides` after every test; no override bleeds to the next test | FastAPI testing convention | pytest TestClient: assert `dependency_overrides` empty after each test | | FUND-10 | Clear `dependency_overrides` after every test; no override bleeds to the next test | FastAPI testing convention | pytest TestClient: assert `dependency_overrides` empty after each test | `api-contract` |
| FUND-11 | Override Woo/inference dependencies with mocks; no real network call leaks | FastAPI testing convention | pytest TestClient: assert zero HTTP calls to real Woo/inference endpoints | | FUND-11 | Override Woo/inference dependencies with mocks; no real network call leaks | FastAPI testing convention | pytest TestClient: assert zero HTTP calls to real Woo/inference endpoints | `api-contract` |
### SQLite + Alembic (backend/tests/) ### SQLite + Alembic (backend/tests/)
| Case | What must happen | Where the rule lives | Verified by | | Case | What must happen | Where the rule lives | Verified by | Evidence class |
|---|---|---|---| |---|---|---|---|---|
| FUND-12 | `PRAGMA foreign_keys=ON` enforced in every test database; FK violation rejected | SQLite testing convention; DATA-MODEL.md | pytest + SQLite: insert orphan row, assert IntegrityError | | FUND-12 | `PRAGMA foreign_keys=ON` enforced in every test database; FK violation rejected | SQLite testing convention; DATA-MODEL.md | pytest + SQLite: insert orphan row, assert IntegrityError | `proxy-synthetic` |
| FUND-13 | Unique and check constraints enforced; duplicate or out-of-range values rejected | SQLite testing convention; DATA-MODEL.md | pytest + SQLite: insert violating row, assert IntegrityError | | FUND-13 | Unique and check constraints enforced; duplicate or out-of-range values rejected | SQLite testing convention; DATA-MODEL.md | pytest + SQLite: insert violating row, assert IntegrityError | `proxy-synthetic` |
| FUND-14 | Concurrent poll / WAL stale-read risk handled; WAL mode with proper isolation level prevents stale reads | SQLite WAL convention; DATA-MODEL.md | pytest + SQLite: concurrent reader/writer, assert no stale read | | FUND-14 | Concurrent poll / WAL stale-read risk handled; WAL mode with proper isolation level prevents stale reads | SQLite WAL convention; DATA-MODEL.md | pytest + SQLite: concurrent reader/writer, assert no stale read | `proxy-synthetic` |
| FUND-15 | Illegal status transitions rejected by the application layer; status machine enforced | AGENTS.md §7; DATA-MODEL.md slabs | pytest TestClient: attempt illegal transition, assert 422 | | FUND-15 | Illegal status transitions rejected by the application layer; status machine enforced | AGENTS.md §7; DATA-MODEL.md slabs | pytest TestClient: attempt illegal transition, assert 422 | `api-contract` |
| FUND-16 | Migrations must not silently drop integrity indexes or constraints; post-migration assertions verify schema integrity | Alembic convention; DATA-MODEL.md | pytest + SQLite: run migration, assert indexes/constraints preserved | | FUND-16 | Migrations must not silently drop integrity indexes or constraints; post-migration assertions verify schema integrity | Alembic convention; DATA-MODEL.md | pytest + SQLite: run migration, assert indexes/constraints preserved | `proxy-synthetic` |
### AES-GCM (settings) ### AES-GCM (settings)
| Case | What must happen | Where the rule lives | Verified by | | Case | What must happen | Where the rule lives | Verified by | Evidence class |
|---|---|---|---| |---|---|---|---|---|
| FUND-17 | Fresh nonce per encrypt; tampered ciphertext or wrong key/nonce raises authentication failure | AES-GCM specification; AGENTS.md §4 | Unit test: tamper ciphertext, assert auth failure; encrypt twice, assert nonces differ | | FUND-17 | Fresh nonce per encrypt; tampered ciphertext or wrong key/nonce raises authentication failure | AES-GCM specification; AGENTS.md §4 | Unit test: tamper ciphertext, assert auth failure; encrypt twice, assert nonces differ | `proxy-synthetic` |
| FUND-18 | Ciphertext never appears in any GET response; only the configured username and `woo_credentials_configured` boolean returned | OPENAPI.md §settings; DATA-MODEL.md settings | API contract test: assert `woo_app_password` absent from GET /api/v1/settings | | FUND-18 | Ciphertext never appears in any GET response; only the configured username and `woo_credentials_configured` boolean returned | OPENAPI.md §settings; DATA-MODEL.md settings | API contract test: assert `woo_app_password` absent from GET /api/v1/settings | `api-contract` |
| FUND-19 | Missing `SLAB_AES_KEY` refuses startup; no fallback to plaintext storage | AGENTS.md §4; DATA-MODEL.md settings | Unit test: unset env var, assert startup rejection | | FUND-19 | Missing `SLAB_AES_KEY` refuses startup; no fallback to plaintext storage | AGENTS.md §4; DATA-MODEL.md settings | Unit test: unset env var, assert startup rejection | `proxy-synthetic` |
### Woo REST client (httpx + App Passwords over HTTPS) ### Woo REST client (httpx + App Passwords over HTTPS)
| Case | What must happen | Where the rule lives | Verified by | | Case | What must happen | Where the rule lives | Verified by | Evidence class |
|---|---|---|---| |---|---|---|---|---|
| FUND-20 | Basic Auth with App Passwords over HTTPS; consumer keys not implemented | AGENTS.md §4; DEPLOYMENT.md §3; OPENAPI.md §settings | Unit test: assert HTTP Basic header, HTTPS-only URL | | FUND-20 | Basic Auth with App Passwords over HTTPS; consumer keys not implemented | AGENTS.md §4; DEPLOYMENT.md §3; OPENAPI.md §settings | Unit test: assert HTTP Basic header, HTTPS-only URL | `proxy-synthetic` |
| FUND-21 | 401/400/404 response shapes handled; server maps to actionable error, not raw passthrough | Woo REST convention; CONTENT-WOO.md §2.6 | Integration test with respx: mock 401/400/404, assert mapped error response | | FUND-21 | 401/400/404 response shapes handled; server maps to actionable error, not raw passthrough | Woo REST convention; CONTENT-WOO.md §2.6 | Integration test with respx: mock 401/400/404, assert mapped error response | `proxy-synthetic` |
| FUND-22 | Pre-create SKU lookup mandatory; Woo concurrent duplicate-SKU race handled by checking before create | AGENTS.md §5; CONTENT-WOO.md §2.5 | Integration test with respx: create duplicate SKU, assert 409 before create | | FUND-22 | Pre-create SKU lookup mandatory; Woo concurrent duplicate-SKU race handled by checking before create | AGENTS.md §5; CONTENT-WOO.md §2.5 | Integration test with respx: create duplicate SKU, assert 409 before create | `proxy-synthetic` |
| FUND-23 | Timeouts and network errors handled gracefully; server returns actionable error, not stack trace | Woo REST convention; CONTENT-WOO.md §2.6 | Integration test with respx: mock timeout/connection error, assert graceful error | | FUND-23 | Timeouts and network errors handled gracefully; server returns actionable error, not stack trace | Woo REST convention; CONTENT-WOO.md §2.6 | Integration test with respx: mock timeout/connection error, assert graceful error | `proxy-synthetic` |
| FUND-24 | Secrets redacted in logs; no Woo username/password, inference key, or AES key in any log output | AGENTS.md §4; ARCHITECTURE.md §5 | Unit test: assert log output contains no secret material | | FUND-24 | Secrets redacted in logs; no Woo username/password, inference key, or AES key in any log output | AGENTS.md §4; ARCHITECTURE.md §5 | Unit test: assert log output contains no secret material | `proxy-synthetic` |
### Compose / Caddy (deploy/tests/) ### Compose / Caddy (deploy/tests/)
| Case | What must happen | Where the rule lives | Verified by | | Case | What must happen | Where the rule lives | Verified by | Evidence class |
|---|---|---|---| |---|---|---|---|---|
| FUND-25 | HTTPS-only `WOO_BASE_URL`; health through proxy; inventory/port collision checked at deploy time | DEPLOYMENT.md §3; AGENTS.md §9 | Shell script: assert HTTPS, health through Caddy, port conflict detection | | FUND-25 | HTTPS-only `WOO_BASE_URL`; health through proxy; inventory/port collision checked at deploy time | DEPLOYMENT.md §3; AGENTS.md §9 | Shell script: assert HTTPS, health through Caddy, port conflict detection | `deploy` |
--- ---
## Phase 0 / Gate B — Hybrid skeleton + deterministic core ## Phase 0 / Gate B — Hybrid skeleton + deterministic core
**Exit evidence**: Ty confirms TV-3, TV-6, TV-7, and TV-10 output on real fixtures; health returns 200; AES round-trip passes; draft store stub persists and retrieves; unit and API cases automated. ### Gate B exit checklist
Coding tip is not test complete. Test complete is not Gate ready. Bots never declare Gate B green.
| Exit condition | Required Evidence class | Status owner |
|---|---|---|
| TV-3 bdft identity on real fixtures | `product-state` or `human-walk` | Ty |
| TV-6 crop/PNG rules on real fixtures | `product-state` or `human-walk` | Ty |
| TV-7 undersized-source warn/retake on real fixtures | `product-state` or `human-walk` | Ty |
| TV-10 rounding on real fixtures | `product-state` or `human-walk` | Ty |
| Health returns 200 on the specified HTTPS deploy path | `deploy` | Ty |
| AES credentials round-trip without ciphertext leak | `api-contract` | Ty |
| Draft store create/persist/retrieve | `api-contract` | Ty |
| Unit and API automation recorded for Gate B cases | `proxy-synthetic` and/or `api-contract` as declared per case | Test report |
A prior `proxy-synthetic` `pass` for TV-3, TV-6, TV-7, or TV-10 is not Gate B product-state proof.
### Unit — deterministic core (test-driven) ### Unit — deterministic core (test-driven)
| Case | What must happen | Where the rule lives | Verified by | | Case | What must happen | Where the rule lives | Verified by | Evidence class |
|---|---|---|---| |---|---|---|---|---|
| TV-1 | Green-sheet synthetic photo: interior green-ish patch that does not touch the border stays after chroma-key; U2Net is not invoked | `docs/TECH-SPEC-PIPELINE.md` §3.1, §3.4 | Unit test with synthetic image fixture | | TV-1 | Green-sheet synthetic photo: interior green-ish patch that does not touch the border stays after chroma-key; U2Net is not invoked | `docs/TECH-SPEC-PIPELINE.md` §3.1, §3.4 | Unit test with synthetic image fixture | `proxy-synthetic` |
| 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 | `docs/TECH-SPEC-PIPELINE.md` §3.1, §3.4 | Unit test with synthetic image fixture | | 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 | `docs/TECH-SPEC-PIPELINE.md` §3.1, §3.4 | Unit test with synthetic image fixture | `proxy-synthetic` |
| TV-3 | Slab 96" long, 1.5" thick, mask 0.64 square feet → bdft = 0.96. Identity: 1 square foot at 1" thick equals 1 bdft. No divide-by-12. | `docs/TECH-SPEC-PIPELINE.md` §4.6 | Unit test, shown output on real fixtures | | TV-3 | Slab 96" long, 1.5" thick, mask 0.64 square feet → bdft = 0.96. Identity: 1 square foot at 1" thick equals 1 bdft. No divide-by-12. | `docs/TECH-SPEC-PIPELINE.md` §4.6 | Unit test, shown output on real fixtures | `product-state` |
| TV-4 | Width samples at 6" stations on a 96" length axis report min, max, and average perpendicular to the confirmed axis | `docs/TECH-SPEC-PIPELINE.md` §4.4 | Unit test with fixture mask | | TV-4 | Width samples at 6" stations on a 96" length axis report min, max, and average perpendicular to the confirmed axis | `docs/TECH-SPEC-PIPELINE.md` §4.4 | Unit test with fixture mask | `proxy-synthetic` |
| TV-5 | Slab with one square end and one 45° cut: length axis follows prevailing length; widths stay perpendicular after user confirm | `docs/TECH-SPEC-PIPELINE.md` §4.1, §4.2 | Unit test with angled-end fixture | | TV-5 | Slab with one square end and one 45° cut: length axis follows prevailing length; widths stay perpendicular after user confirm | `docs/TECH-SPEC-PIPELINE.md` §4.1, §4.2 | Unit test with angled-end fixture | `proxy-synthetic` |
| TV-6 | 3:4 crop, slab centered, 80 percent fill at extremes, transparent PNG, shorter side ≥ 1600 when source allows | `docs/TECH-SPEC-PIPELINE.md` §5, §7.1 | Unit test, shown output on real fixtures | | TV-6 | 3:4 crop, slab centered, 80 percent fill at extremes, transparent PNG, shorter side ≥ 1600 when source allows | `docs/TECH-SPEC-PIPELINE.md` §5, §7.1 | Unit test, shown output on real fixtures | `product-state` |
| TV-7 | Undersized source: after 80 percent crop shorter side would be below 1600 and source cannot supply it → warn, no upscaling, photo not publishable until retake | `docs/TECH-SPEC-PIPELINE.md` §5, §7.1 | Unit test, shown output on real fixtures | | TV-7 | Undersized source: after 80 percent crop shorter side would be below 1600 and source cannot supply it → warn, no upscaling, photo not publishable until retake | `docs/TECH-SPEC-PIPELINE.md` §5, §7.1 | Unit test, shown output on real fixtures | `product-state` |
| TV-8 | Pricing: bdft 0.96, species $12.50/bdft → recommended price 12.00; override 150.00 stored as 150.00; no rule → recommended price empty, manual 99.99 stored | `docs/TECH-SPEC-PIPELINE.md` §6, §8 TV-8 | Unit test with pricing rules | | TV-8 | Pricing: bdft 0.96, species $12.50/bdft → recommended price 12.00; override 150.00 stored as 150.00; no rule → recommended price empty, manual 99.99 stored | `docs/TECH-SPEC-PIPELINE.md` §6, §8 TV-8 | Unit test with pricing rules | `proxy-synthetic` |
| TV-9 | Thickness 1" → Woo range rounds up (for example 1"–1 1/2") using synced attribute term, not a hardcoded label | `docs/TECH-SPEC-PIPELINE.md` §8 TV-9, `docs/DATA-MODEL.md` slabs | Unit test with mock taxonomy | | TV-9 | Thickness 1" → Woo range rounds up (for example 1"–1 1/2") using synced attribute term, not a hardcoded label | `docs/TECH-SPEC-PIPELINE.md` §8 TV-9, `docs/DATA-MODEL.md` slabs | Unit test with mock taxonomy | `proxy-synthetic` |
| TV-10 | Rounding: 96.04" → 96"; square feet 0.641 → 0.64; bdft 11.525 → 11.53; price 12.345 → 12.35. Half-up to 2 decimals. | `docs/TECH-SPEC-PIPELINE.md` §8 TV-10 | Unit test, shown output on real fixtures | | TV-10 | Rounding: 96.04" → 96"; square feet 0.641 → 0.64; bdft 11.525 → 11.53; price 12.345 → 12.35. Half-up to 2 decimals. | `docs/TECH-SPEC-PIPELINE.md` §8 TV-10 | Unit test, shown output on real fixtures | `product-state` |
### Unit — shared validation ### Unit — shared validation
| Case | What must happen | Where the rule lives | Verified by | | Case | What must happen | Where the rule lives | Verified by | Evidence class |
|---|---|---|---| |---|---|---|---|---|
| V-1 | SKU charset `[A-Z0-9-]{3,24}` accepted; values outside the charset rejected | `docs/TECH-SPEC-PIPELINE.md` §2, issue #17 | Unit test with valid and invalid SKU strings | | V-1 | SKU charset `[A-Z0-9-]{3,24}` accepted; values outside the charset rejected | `docs/TECH-SPEC-PIPELINE.md` §2, issue #17 | Unit test with valid and invalid SKU strings | `proxy-synthetic` |
| V-2 | Title, short title, description: UTF-8 within WooCommerce maxLength enforced at field exit and at submit | `docs/TECH-SPEC-PIPELINE.md` §2, issue #17 | Unit test with boundary-length strings | | V-2 | Title, short title, description: UTF-8 within WooCommerce maxLength enforced at field exit and at submit | `docs/TECH-SPEC-PIPELINE.md` §2, issue #17 | Unit test with boundary-length strings | `proxy-synthetic` |
| V-3 | Validation module version hash: client caches module; submit carries hash; mismatch returns 412 with fresh module, not 409 | `docs/OPENAPI.md` §validation, issue #17 | Unit test of validation module hash comparison | | V-3 | Validation module version hash: client caches module; submit carries hash; mismatch returns 412 with fresh module, not 409 | `docs/OPENAPI.md` §validation, issue #17 | Unit test of validation module hash comparison | `proxy-synthetic` |
### Unit — AES-GCM settings ### Unit — AES-GCM settings
| Case | What must happen | Where the rule lives | Verified by | | Case | What must happen | Where the rule lives | Verified by | Evidence class |
|---|---|---|---| |---|---|---|---|---|
| AES-1 | Encrypt then decrypt round-trips for woo_wp_username and woo_app_password; ciphertext never appears in GET settings | `docs/ARCHITECTURE.md` §5, issue #5 | Unit test with round-trip encryption | | AES-1 | Encrypt then decrypt round-trips for woo_wp_username and woo_app_password; ciphertext never appears in GET settings | `docs/ARCHITECTURE.md` §5, issue #5 | Unit test with round-trip encryption | `api-contract` |
| AES-2 | Missing SLAB_AES_KEY environment variable at startup: app refuses to start; no fallback to plaintext | `docs/AGENTS.md` §4, `docs/DATA-MODEL.md` settings | Unit test with missing env var | | AES-2 | Missing SLAB_AES_KEY environment variable at startup: app refuses to start; no fallback to plaintext | `docs/AGENTS.md` §4, `docs/DATA-MODEL.md` settings | Unit test with missing env var | `proxy-synthetic` |
### API / contract — health + draft store stub ### API / contract — health + draft store stub
| Case | What must happen | Where the rule lives | Verified by | | Case | What must happen | Where the rule lives | Verified by | Evidence class |
|---|---|---|---| |---|---|---|---|---|
| H-1 | `GET /api/health` returns 200 `{ "status":"ok" }` with `woo_reachable` false when credentials are not configured | `docs/OPENAPI.md` §health, `docs/IMPL-PLAN.md` Phase 0 | API contract test via httpx TestClient | | H-1 | `GET /api/health` returns 200 `{ "status":"ok" }` with `woo_reachable` false when credentials are not configured | `docs/OPENAPI.md` §health, `docs/IMPL-PLAN.md` Phase 0 | API contract test via httpx TestClient | `api-contract` |
| D-1 | `POST /api/v1/slabs` accepts multipart with data JSON + files + metadata; returns 201 with `status: calibrated` and `server_rev` | `docs/OPENAPI.md` §slabs, `docs/DATA-MODEL.md` slabs | API contract test via httpx TestClient | | D-1 | `POST /api/v1/slabs` accepts multipart with data JSON + files + metadata; returns 201 with `status: calibrated` and `server_rev` | `docs/OPENAPI.md` §slabs, `docs/DATA-MODEL.md` slabs | API contract test via httpx TestClient | `api-contract` |
--- ---
## Gate C — Early Phase 1 (first draft listing, inference disabled) ## Gate C — Early Phase 1 (first draft listing, inference disabled)
**Exit evidence**: Ty opens one `SLAB-UAT-*` WooCommerce draft listing in the real store admin; inference OFF; mocked-store automation is not Gate C exit. **Exit evidence (Evidence class).** Ty opens one `SLAB-UAT-*` WooCommerce draft listing in the real store admin (`human-walk` or `product-state`). Inference OFF. Mocked-store automation is `proxy-synthetic` only and is not Gate C exit. Bots never declare Gate C green.
### API / contract — create + draft ### API / contract — create + draft
| Case | What must happen | Where the rule lives | Verified by | | Case | What must happen | Where the rule lives | Verified by | Evidence class |
|---|---|---|---| |---|---|---|---|---|
| GC-1 | `POST /api/v1/slabs` with `SLAB-UAT-*` SKU creates a WooCommerce product with `status: "draft"` regardless of the `woo_create_status` setting | `docs/AGENTS.md` §5, `docs/CONTENT-WOO.md` §2.4 | API contract test with mocked Woo | | GC-1 | `POST /api/v1/slabs` with `SLAB-UAT-*` SKU creates a WooCommerce product with `status: "draft"` regardless of the `woo_create_status` setting | `docs/AGENTS.md` §5, `docs/CONTENT-WOO.md` §2.4 | API contract test with mocked Woo | `api-contract` |
| GC-2 | Duplicate SKU before create: `GET /products?sku={sku}` finds existing → 409 `duplicate_sku` with `{ field:"sku", actions:["edit_sku","open_existing"] }` | `docs/CONTENT-WOO.md` §2.5, `docs/OPENAPI.md` §slabs, issue #3 | API contract test with pre-existing SKU in mock | | GC-2 | Duplicate SKU before create: `GET /products?sku={sku}` finds existing → 409 `duplicate_sku` with `{ field:"sku", actions:["edit_sku","open_existing"] }` | `docs/CONTENT-WOO.md` §2.5, `docs/OPENAPI.md` §slabs, issue #3 | API contract test with pre-existing SKU in mock | `api-contract` |
| GC-3 | `POST /api/v1/settings/test-woo` returns ok with populated taxonomy; always resyncs taxonomy on success | `docs/OPENAPI.md` §settings, issue #14 | API contract test with mocked Woo taxonomy | | GC-3 | `POST /api/v1/settings/test-woo` returns ok with populated taxonomy; always resyncs taxonomy on success | `docs/OPENAPI.md` §settings, issue #14 | API contract test with mocked Woo taxonomy | `api-contract` |
| GC-4 | `WOO_BASE_URL` is compose-only HTTPS; Settings GET returns it read-only; PUT rejects write attempts with 422 | `docs/AGENTS.md` §5, issue #11 | API contract test with PUT attempt | | GC-4 | `WOO_BASE_URL` is compose-only HTTPS; Settings GET returns it read-only; PUT rejects write attempts with 422 | `docs/AGENTS.md` §5, issue #11 | API contract test with PUT attempt | `api-contract` |
| GC-5 | FastAPI refuses to start when `WOO_BASE_URL` environment variable is missing or set to `http://` | `docs/AGENTS.md` §4, `docs/DEPLOYMENT.md` §3 | Unit test for startup config validation | | GC-5 | FastAPI refuses to start when `WOO_BASE_URL` environment variable is missing or set to `http://` | `docs/AGENTS.md` §4, `docs/DEPLOYMENT.md` §3 | Unit test for startup config validation | `proxy-synthetic` |
### Integration — WooCommerce mock (draft create, SLAB-UAT-*, duplicate SKU, taxonomy) ### Integration — WooCommerce mock (draft create, SLAB-UAT-*, duplicate SKU, taxonomy)
| Case | What must happen | Where the rule lives | Verified by | | Case | What must happen | Where the rule lives | Verified by | Evidence class |
|---|---|---|---| |---|---|---|---|---|
| W-1 | Woo mock returns a new product with status draft; server stores `woo_product_id` and purges images | `docs/CONTENT-WOO.md` §2.4, `docs/IMPL-PLAN.md` Phase 4 | Integration test with respx mock | | W-1 | Woo mock returns a new product with status draft; server stores `woo_product_id` and purges images | `docs/CONTENT-WOO.md` §2.4, `docs/IMPL-PLAN.md` Phase 4 | Integration test with respx mock | `proxy-synthetic` |
| W-2 | Woo mock returns 409 for duplicate SKU; server maps to `{ field:"sku", actions:["edit_sku","open_existing"] }`; draft retained | `docs/OPENAPI.md` §slabs, issue #3 | Integration test with respx mock returning 409 | | W-2 | Woo mock returns 409 for duplicate SKU; server maps to `{ field:"sku", actions:["edit_sku","open_existing"] }`; draft retained | `docs/OPENAPI.md` §slabs, issue #3 | Integration test with respx mock returning 409 | `proxy-synthetic` |
| W-3 | Taxonomy sync returns cached data; anchor stays unchanged on no-op sync; anchor bumps only when stored-subset hash changes | `docs/ARCHITECTURE.md` §7, issue #14 | Integration test with taxonomy mock | | W-3 | Taxonomy sync returns cached data; anchor stays unchanged on no-op sync; anchor bumps only when stored-subset hash changes | `docs/ARCHITECTURE.md` §7, issue #14 | Integration test with taxonomy mock | `proxy-synthetic` |
| W-4 | `test-woo` always runs taxonomy resync after successful connection test | `docs/ARCHITECTURE.md` §7, issue #14 | Integration test: assert taxonomy resync call | | W-4 | `test-woo` always runs taxonomy resync after successful connection test | `docs/ARCHITECTURE.md` §7, issue #14 | Integration test: assert taxonomy resync call | `proxy-synthetic` |
| W-5 | Failed publish: slab status `failed`, photos retained, sync_log written; retry from same screen without re-photographing → success → photos purged | `docs/CONTENT-WOO.md` §2.6, `docs/TECH-SPEC-PIPELINE.md` §5, `docs/DATA-MODEL.md` slabs | Integration test with retryable mock failure | | W-5 | Failed publish: slab status `failed`, photos retained, sync_log written; retry from same screen without re-photographing → success → photos purged | `docs/CONTENT-WOO.md` §2.6, `docs/TECH-SPEC-PIPELINE.md` §5, `docs/DATA-MODEL.md` slabs | Integration test with retryable mock failure | `proxy-synthetic` |
### Integration — inference disabled path + test-inference probe (issue #2 substance) ### Integration — inference disabled path + test-inference probe (issue #2 substance)
| Case | What must happen | Where the rule lives | Verified by | | Case | What must happen | Where the rule lives | Verified by | Evidence class |
|---|---|---|---| |---|---|---|---|---|
| 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 | `docs/AGENTS.md` §6, `docs/IMPL-PLAN.md` Phase 5 | Integration test asserting zero inference calls | | 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 | `docs/AGENTS.md` §6, `docs/IMPL-PLAN.md` Phase 5 | Integration test asserting zero inference calls | `proxy-synthetic` |
| INF-2 | `POST /api/v1/settings/test-inference` returns `{ ok: true, mode: "stateful", vision_capable: true }` on a stateful provider | `docs/OPENAPI.md` §settings, issue #2 | Integration test with mocked inference endpoint | | INF-2 | `POST /api/v1/settings/test-inference` returns `{ ok: true, mode: "stateful", vision_capable: true }` on a stateful provider | `docs/OPENAPI.md` §settings, issue #2 | Integration test with mocked inference endpoint | `proxy-synthetic` |
| INF-3 | `test-inference` returns `{ ok: true, mode: "stateless", vision_capable: true }` on a stateless provider; Call 2 uses full resend | `docs/OPENAPI.md` §settings, issue #2 | Integration test with mocked inference endpoint | | INF-3 | `test-inference` returns `{ ok: true, mode: "stateless", vision_capable: true }` on a stateless provider; Call 2 uses full resend | `docs/OPENAPI.md` §settings, issue #2 | Integration test with mocked inference endpoint | `proxy-synthetic` |
| INF-4 | `test-inference` returns `{ ok: false, mode: "fail" }`: Call 2 enablement blocked; Gate C path remains green with inference disabled | `docs/OPENAPI.md` §settings, issue #2 | Integration test: assert Call 2 blocked | | INF-4 | `test-inference` returns `{ ok: false, mode: "fail" }`: Call 2 enablement blocked; Gate C path remains green with inference disabled | `docs/OPENAPI.md` §settings, issue #2 | Integration test: assert Call 2 blocked | `proxy-synthetic` |
### UI end-to-end — capture knobs, mask, axis, ready gates (phone + PC) ### UI end-to-end — capture knobs, mask, axis, ready gates (phone + PC)
| Case | What must happen | Where the rule lives | Verified by | | Case | What must happen | Where the rule lives | Verified by | Evidence class |
|---|---|---|---| |---|---|---|---|---|
| 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 | `docs/TECH-SPEC-PIPELINE.md` §3.3, `docs/UX.md` §Capture, issue #15 | Playwright E2E test: knob change → overlay update, no navigation | | 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 | `docs/TECH-SPEC-PIPELINE.md` §3.3, `docs/UX.md` §Capture, issue #15 | Playwright E2E test: knob change → overlay update, no navigation | `e2e` |
| UI-2 | Mask overlay on source photo: user confirms edge; below-threshold edge after tuning → retake; no U2Net control in the proof of concept | `docs/TECH-SPEC-PIPELINE.md` §3.2, §3.5, `docs/UX.md` §Capture | Playwright E2E test: mask overlay visible, confirm/retake paths | | UI-2 | Mask overlay on source photo: user confirms edge; below-threshold edge after tuning → retake; no U2Net control in the proof of concept | `docs/TECH-SPEC-PIPELINE.md` §3.2, §3.5, `docs/UX.md` §Capture | Playwright E2E test: mask overlay visible, confirm/retake paths | `e2e` |
| UI-3 | Length axis overlay: user rotates if skewed; samples and square feet update live; nearly-square slab: user picks length direction | `docs/TECH-SPEC-PIPELINE.md` §4.2, `docs/UX.md` §Capture | Playwright E2E test: axis rotation, live number update | | UI-3 | Length axis overlay: user rotates if skewed; samples and square feet update live; nearly-square slab: user picks length direction | `docs/TECH-SPEC-PIPELINE.md` §4.2, `docs/UX.md` §Capture | Playwright E2E test: axis rotation, live number update | `e2e` |
| UI-4 | Numbers entry: length 1/8" steps, thickness pick, SKU typed; computed square feet/bdft/widths displayed | `docs/TECH-SPEC-PIPELINE.md` §4, `docs/UX.md` §Capture | Playwright E2E test: type inputs, assert computed output | | UI-4 | Numbers entry: length 1/8" steps, thickness pick, SKU typed; computed square feet/bdft/widths displayed | `docs/TECH-SPEC-PIPELINE.md` §4, `docs/UX.md` §Capture | Playwright E2E test: type inputs, assert computed output | `e2e` |
| UI-5 | Ready-set gates: slab cannot reach `ready` until full named set is filled (SKU, length, thickness, square feet/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 | `docs/AGENTS.md` §7, `docs/UX.md` §Review, issue #9 | Playwright E2E test: attempt ready with missing fields, assert nudges | | UI-5 | Ready-set gates: slab cannot reach `ready` until full named set is filled (SKU, length, thickness, square feet/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 | `docs/AGENTS.md` §7, `docs/UX.md` §Review, issue #9 | Playwright E2E test: attempt ready with missing fields, assert nudges | `e2e` |
### Ops — compose health ### Ops — compose health
| Case | What must happen | Where the rule lives | Verified by | | Case | What must happen | Where the rule lives | Verified by | Evidence class |
|---|---|---|---| |---|---|---|---|---|
| OPS-1 | `GET https://${APP_HOSTNAME}/api/health` returns ok after compose up with filled inventory | `docs/DEPLOYMENT.md` §8, `docs/IMPL-PLAN.md` Phase 0 | Shell script: compose up → curl health endpoint | | OPS-1 | `GET https://${APP_HOSTNAME}/api/health` returns ok after compose up with filled inventory | `docs/DEPLOYMENT.md` §8, `docs/IMPL-PLAN.md` Phase 0 | Shell script: compose up → curl health endpoint | `deploy` |
--- ---
## Phase 1 remainder — Full API surface ## Phase 1 remainder — Full API surface
**Exit evidence**: agent-written test automation drives a full calibrated to ready cycle via the API; all OPENAPI endpoints match expected responses; revision conflict handled; taxonomy anchor logic verified. **Exit evidence (Evidence class).** Agent-written test automation drives a full calibrated to ready cycle via the API (`api-contract`). OPENAPI responses match. Revision conflict and taxonomy anchor logic verified. Gate-ready still needs Ty where IMPL-PLAN says so.
### API / contract — all routes, concurrency, secrets redaction ### API / contract — all routes, concurrency, secrets redaction
| Case | What must happen | Where the rule lives | Verified by | | Case | What must happen | Where the rule lives | Verified by | Evidence class |
|---|---|---|---| |---|---|---|---|---|
| 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` | `docs/OPENAPI.md` §conventions, `docs/DATA-MODEL.md` slabs | API contract test with httpx TestClient | | 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` | `docs/OPENAPI.md` §conventions, `docs/DATA-MODEL.md` slabs | API contract test with httpx TestClient | `api-contract` |
| API-2 | `PUT /api/v1/slabs/{id}` stores client-sent derived fields (square feet, bdft, widths); server does not recompute them as source of truth | `docs/OPENAPI.md` §slabs PUT, `docs/ARCHITECTURE.md` §3 | API contract test: assert no server-side recompute | | API-2 | `PUT /api/v1/slabs/{id}` stores client-sent derived fields (square feet, bdft, widths); server does not recompute them as source of truth | `docs/OPENAPI.md` §slabs PUT, `docs/ARCHITECTURE.md` §3 | API contract test: assert no server-side recompute | `api-contract` |
| API-3 | `GET /api/v1/settings` returns `SettingsView` with `woo_credentials_configured` boolean; `woo_app_password` never returned | `docs/OPENAPI.md` §settings, issue #5 | API contract test: assert password absent from response | | API-3 | `GET /api/v1/settings` returns `SettingsView` with `woo_credentials_configured` boolean; `woo_app_password` never returned | `docs/OPENAPI.md` §settings, issue #5 | API contract test: assert password absent from response | `api-contract` |
| API-4 | `POST /api/v1/slabs/{id}/infer-taxon` returns 202 `{ status: "inferring" }`; client polls `GET /api/v1/slabs/{id}` for results; timeout greater than 45 seconds returns 504 | `docs/OPENAPI.md` §inference, issue #8 | API contract test with mocked inference | | API-4 | `POST /api/v1/slabs/{id}/infer-taxon` returns 202 `{ status: "inferring" }`; client polls `GET /api/v1/slabs/{id}` for results; timeout greater than 45 seconds returns 504 | `docs/OPENAPI.md` §inference, issue #8 | API contract test with mocked inference | `api-contract` |
| API-5 | `POST /api/v1/slabs/{id}/infer-content` returns 202; requires `test-inference` ok; below-threshold Call 1 (confidence below 0.7): fields stay empty | `docs/OPENAPI.md` §inference, `docs/AGENTS.md` §6, issue #8/#9 | API contract test with confidence-gated mock | | API-5 | `POST /api/v1/slabs/{id}/infer-content` returns 202; requires `test-inference` ok; below-threshold Call 1 (confidence below 0.7): fields stay empty | `docs/OPENAPI.md` §inference, `docs/AGENTS.md` §6, issue #8/#9 | API contract test with confidence-gated mock | `api-contract` |
| 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 | `docs/OPENAPI.md` §publish, issue #18 | API contract test with incomplete slab | | 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 | `docs/OPENAPI.md` §publish, issue #18 | API contract test with incomplete slab | `api-contract` |
| 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 | `docs/OPENAPI.md` §taxonomy, `docs/ARCHITECTURE.md` §7, issue #14 | Integration test with taxonomy mock | | 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 | `docs/OPENAPI.md` §taxonomy, `docs/ARCHITECTURE.md` §7, issue #14 | Integration test with taxonomy mock | `api-contract` |
| API-8 | `GET /api/v1/validation/module` returns hash and rules; client caches; submit mismatch returns 412 with fresh module | `docs/OPENAPI.md` §validation, issue #17 | API contract test: assert 412 on stale hash | | API-8 | `GET /api/v1/validation/module` returns hash and rules; client caches; submit mismatch returns 412 with fresh module | `docs/OPENAPI.md` §validation, issue #17 | API contract test: assert 412 on stale hash | `api-contract` |
| API-9 | `DELETE /api/v1/slabs/{id}`: soft-delete returns 204; unpublished drafts only; published slabs undeletable | `docs/OPENAPI.md` §slabs DELETE | API contract test with published slab | | API-9 | `DELETE /api/v1/slabs/{id}`: soft-delete returns 204; unpublished drafts only; published slabs undeletable | `docs/OPENAPI.md` §slabs DELETE | API contract test with published slab | `api-contract` |
| API-10 | `test-woo` fails closed: missing credentials or non-HTTPS URL returns `{ ok: false, detail: "..." }` without crashing | `docs/OPENAPI.md` §settings test-woo | API contract test with bad credentials | | API-10 | `test-woo` fails closed: missing credentials or non-HTTPS URL returns `{ ok: false, detail: "..." }` without crashing | `docs/OPENAPI.md` §settings test-woo | API contract test with bad credentials | `api-contract` |
### Integration — taxonomy sync + anchor ### Integration — taxonomy sync + anchor
| Case | What must happen | Where the rule lives | Verified by | | Case | What must happen | Where the rule lives | Verified by | Evidence class |
|---|---|---|---| |---|---|---|---|---|
| TAX-1 | Sync on publish when `now - taxonomy_last_sync_at > 1 hour`; bump anchor only on real change; stale picked id returns 422 with current options | `docs/ARCHITECTURE.md` §7, `docs/CONTENT-WOO.md` §2.2, issue #14 | Integration test with time-skewed sync | | TAX-1 | Sync on publish when `now - taxonomy_last_sync_at > 1 hour`; bump anchor only on real change; stale picked id returns 422 with current options | `docs/ARCHITECTURE.md` §7, `docs/CONTENT-WOO.md` §2.2, issue #14 | Integration test with time-skewed sync | `api-contract` |
| TAX-2 | Species picker offers only WooCommerce-synced species; no hardcoded fallback; new species selectable only after sync stores it | `docs/ARCHITECTURE.md` §7, issue #12 | Integration test: assert species list matches cache | | TAX-2 | Species picker offers only WooCommerce-synced species; no hardcoded fallback; new species selectable only after sync stores it | `docs/ARCHITECTURE.md` §7, issue #12 | Integration test: assert species list matches cache | `api-contract` |
| TAX-3 | Client reads taxonomy anchor; re-pulls full taxonomy only when anchor advances past client's last value; no-per-navigation sync | `docs/ARCHITECTURE.md` §7, `docs/OPENAPI.md` §taxonomy | Integration test with anchor versioning | | TAX-3 | Client reads taxonomy anchor; re-pulls full taxonomy only when anchor advances past client's last value; no-per-navigation sync | `docs/ARCHITECTURE.md` §7, `docs/OPENAPI.md` §taxonomy | Integration test with anchor versioning | `api-contract` |
--- ---
## Phase 3 — Frontend review + publish UI ## Phase 3 — Frontend review + publish UI
**Exit evidence**: Ty publishes a test slab to the real store as a WooCommerce draft `SLAB-UAT-*`; automation covers UI cases; Ty is the exit. **Exit evidence (Evidence class).** Ty publishes a test slab to the real store as a WooCommerce draft `SLAB-UAT-*` (`human-walk` or `product-state`). Automation covers UI cases (`e2e`). Ty is the exit.
### UI end-to-end — review, field recovery, publish outcome (phone + PC) ### UI end-to-end — review, field recovery, publish outcome (phone + PC)
| Case | What must happen | Where the rule lives | Verified by | | Case | What must happen | Where the rule lives | Verified by | Evidence class |
|---|---|---|---| |---|---|---|---|---|
| UIE-1 | Review screen: all fields editable; client-computed square feet/bdft/widths shown with manual override; typed override persists across poll and refresh | `docs/IMPL-PLAN.md` Phase 3, `docs/UX.md` §Review | Playwright E2E test: edit, poll, refresh, assert persistence | | UIE-1 | Review screen: all fields editable; client-computed square feet/bdft/widths shown with manual override; typed override persists across poll and refresh | `docs/IMPL-PLAN.md` Phase 3, `docs/UX.md` §Review | Playwright E2E test: edit, poll, refresh, assert persistence | `e2e` |
| UIE-2 | Call 1 prefill at or above 0.7 confidence: fields pre-filled and mutable; user override sticks; below 0.7 fields empty; inline nudges for every still-required control | `docs/AGENTS.md` §6, issue #8/#9, `docs/UX.md` §Review | Playwright E2E test with mocked Call 1 results | | UIE-2 | Call 1 prefill at or above 0.7 confidence: fields pre-filled and mutable; user override sticks; below 0.7 fields empty; inline nudges for every still-required control | `docs/AGENTS.md` §6, issue #8/#9, `docs/UX.md` §Review | Playwright E2E test with mocked Call 1 results | `e2e` |
| 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 | `docs/UX.md` §Publish, `docs/CONTENT-WOO.md` §2.5, `docs/AGENTS.md` §5, issue #3/#18 | Playwright E2E test with pre-existing SKU | | 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 | `docs/UX.md` §Publish, `docs/CONTENT-WOO.md` §2.5, `docs/AGENTS.md` §5, issue #3/#18 | Playwright E2E test with pre-existing SKU | `e2e` |
| UIE-4 | Stale field at publish: field shows "This value is no longer valid. Pick from the current list"; draft retained; no raw 422 | `docs/CONTENT-WOO.md` §2.6, issue #18 | Playwright E2E test with removed taxonomy item | | UIE-4 | Stale field at publish: field shows "This value is no longer valid. Pick from the current list"; draft retained; no raw 422 | `docs/CONTENT-WOO.md` §2.6, issue #18 | Playwright E2E test with removed taxonomy item | `e2e` |
| UIE-5 | Publish success: screen shows listing created, `woo_product_id` when available; local draft and photos gone; slab status `published` | `docs/CONTENT-WOO.md` §2.6, `docs/DATA-MODEL.md` slabs | Playwright E2E test: assert published state and cleanup | | UIE-5 | Publish success: screen shows listing created, `woo_product_id` when available; local draft and photos gone; slab status `published` | `docs/CONTENT-WOO.md` §2.6, `docs/DATA-MODEL.md` slabs | Playwright E2E test: assert published state and cleanup | `e2e` |
| UIE-6 | Publish failure: plain message, no status number, no stack; retry from same screen without re-photographing | `docs/CONTENT-WOO.md` §2.6, issue #18 | Playwright E2E test: assert retry path preserves state | | UIE-6 | Publish failure: plain message, no status number, no stack; retry from same screen without re-photographing | `docs/CONTENT-WOO.md` §2.6, issue #18 | Playwright E2E test: assert retry path preserves state | `e2e` |
| UIE-7 | Inference disabled: full manual taxonomy + content path works; no inference network calls; Generate text disabled; templates still work | `docs/IMPL-PLAN.md` Phase 3, `docs/UX.md` §Review | Playwright E2E test with inference flag off | | UIE-7 | Inference disabled: full manual taxonomy + content path works; no inference network calls; Generate text disabled; templates still work | `docs/IMPL-PLAN.md` Phase 3, `docs/UX.md` §Review | Playwright E2E test with inference flag off | `e2e` |
| UIE-8 | Settings UI: WordPress username + application password + test-woo; inference endpoint + test; pricing rules; brand voice/GEO; `woo_create_status`; store URL read-only | `docs/IMPL-PLAN.md` Phase 3, `docs/OPENAPI.md` §settings, issue #11 | Playwright E2E test: settings form and test buttons | | UIE-8 | Settings UI: WordPress username + application password + test-woo; inference endpoint + test; pricing rules; brand voice/GEO; `woo_create_status`; store URL read-only | `docs/IMPL-PLAN.md` Phase 3, `docs/OPENAPI.md` §settings, issue #11 | Playwright E2E test: settings form and test buttons | `e2e` |
| UIE-9 | Changing sensitivity slider immediately refreshes mask overlay without leaving screen or server round trip | `docs/TECH-SPEC-PIPELINE.md` §3.3, issue #15, `docs/UX.md` §Capture | Playwright E2E test: slider → overlay update, no HTTP call | | UIE-9 | Changing sensitivity slider immediately refreshes mask overlay without leaving screen or server round trip | `docs/TECH-SPEC-PIPELINE.md` §3.3, issue #15, `docs/UX.md` §Capture | Playwright E2E test: slider → overlay update, no HTTP call | `e2e` |
| 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 | `docs/TECH-SPEC-PIPELINE.md` §3.3, issue #15 | Playwright E2E test: knob changes, persist, reset | | 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 | `docs/TECH-SPEC-PIPELINE.md` §3.3, issue #15 | Playwright E2E test: knob changes, persist, reset | `e2e` |
--- ---
## Phase 5 — Inference (after Gate C is green) ## Phase 5 — Inference (after Gate C is green)
**Exit evidence**: Ty runs species detection on 3 real slabs with judgment on acceptance; automation covers Call 1/2 cases; full inference-disabled path green. **Exit evidence (Evidence class).** Ty runs species detection on 3 real slabs with judgment on acceptance (`human-walk` or `product-state`). Automation covers Call 1/2 cases (`proxy-synthetic`). Full inference-disabled path proven (`e2e` and/or `proxy-synthetic` as declared).
### Integration — Call 1/2, portable context, confidence gating ### Integration — Call 1/2, portable context, confidence gating
| Case | What must happen | Where the rule lives | Verified by | | Case | What must happen | Where the rule lives | Verified by | Evidence class |
|---|---|---|---| |---|---|---|---|---|
| C1-1 | Call 1 sends all inventory photos at 1024px + WooCommerce taxonomy only + SKU/length/thickness; model constrained to cached taxonomy from WooCommerce cache | `docs/IMPL-PLAN.md` Phase 5, issue #12 | Integration test: inspect Call 1 request payload | | C1-1 | Call 1 sends all inventory photos at 1024px + WooCommerce taxonomy only + SKU/length/thickness; model constrained to cached taxonomy from WooCommerce cache | `docs/IMPL-PLAN.md` Phase 5, issue #12 | Integration test: inspect Call 1 request payload | `proxy-synthetic` |
| C1-2 | Confidence at or above 0.7: species, wood categories, edge, figure, grade, feat-* pre-filled and mutable; user override wins | `docs/AGENTS.md` §6, issue #8/#9 | Integration test with high-confidence mock response | | C1-2 | Confidence at or above 0.7: species, wood categories, edge, figure, grade, feat-* pre-filled and mutable; user override wins | `docs/AGENTS.md` §6, issue #8/#9 | Integration test with high-confidence mock response | `proxy-synthetic` |
| C1-3 | Confidence below 0.7: those fields stay empty; ready blocked until full named set is filled; species plus one figure is not the gate | `docs/AGENTS.md` §6, issue #8/#9 | Integration test with low-confidence mock response | | C1-3 | Confidence below 0.7: those fields stay empty; ready blocked until full named set is filled; species plus one figure is not the gate | `docs/AGENTS.md` §6, issue #8/#9 | Integration test with low-confidence mock response | `proxy-synthetic` |
| C2-1 | Call 2 triggered only by user tap; portable Call 1 context: full resend default; confirmed versus inferred labels carried | `docs/ARCHITECTURE.md` §9, issue #2 | Integration test: assert no auto-trigger, full resend | | C2-1 | Call 2 triggered only by user tap; portable Call 1 context: full resend default; confirmed versus inferred labels carried | `docs/ARCHITECTURE.md` §9, issue #2 | Integration test: assert no auto-trigger, full resend | `proxy-synthetic` |
| C2-2 | Test-inference stateful mode: `previous_response_id` used when available; stateless mode: full resend; fail mode: Call 2 blocked | `docs/OPENAPI.md` §settings, issue #2 | Integration test with three probe modes | | C2-2 | Test-inference stateful mode: `previous_response_id` used when available; stateless mode: full resend; fail mode: Call 2 blocked | `docs/OPENAPI.md` §settings, issue #2 | Integration test with three probe modes | `proxy-synthetic` |
| C2-3 | LLM does not invent dimensions; templates inject deterministic numbers into title and description | `docs/PROMPTS.md` §Call 2, `docs/AGENTS.md` §6 | Integration test: inspect Call 2 response for invented numbers | | C2-3 | LLM does not invent dimensions; templates inject deterministic numbers into title and description | `docs/PROMPTS.md` §Call 2, `docs/AGENTS.md` §6 | Integration test: inspect Call 2 response for invented numbers | `proxy-synthetic` |
| C2-4 | Audit: inference_log stores request_payload and response_payload per call; prompt file edits apply without restart | `docs/DATA-MODEL.md` inference_log, `docs/PROMPTS.md` §Management | Integration test: verify log entry, edit prompt file | | C2-4 | Audit: inference_log stores request_payload and response_payload per call; prompt file edits apply without restart | `docs/DATA-MODEL.md` inference_log, `docs/PROMPTS.md` §Management | Integration test: verify log entry, edit prompt file | `proxy-synthetic` |
### UI end-to-end — inference disabled path ### UI end-to-end — inference disabled path
| Case | What must happen | Where the rule lives | Verified by | | Case | What must happen | Where the rule lives | Verified by | Evidence class |
|---|---|---|---| |---|---|---|---|---|
| OFF-1 | Full suite (calibrated to ready to publish to success) passes with `inference_enabled: false`; zero calls to inference endpoint asserted | `docs/AGENTS.md` §6, `docs/IMPL-PLAN.md` Phase 5 | Playwright E2E test with network assertion for zero inference calls | | OFF-1 | Full suite (calibrated to ready to publish to success) passes with `inference_enabled: false`; zero calls to inference endpoint asserted | `docs/AGENTS.md` §6, `docs/IMPL-PLAN.md` Phase 5 | Playwright E2E test with network assertion for zero inference calls | `e2e` |
| OFF-2 | `test-inference` fail mode blocks Call 2 enablement only; Gate C inference-disabled path unaffected | `docs/OPENAPI.md` §settings, issue #2 | Playwright E2E test: assert Call 2 disabled, full suite passes | | OFF-2 | `test-inference` fail mode blocks Call 2 enablement only; Gate C inference-disabled path unaffected | `docs/OPENAPI.md` §settings, issue #2 | Playwright E2E test: assert Call 2 disabled, full suite passes | `e2e` |
--- ---
## Phase 6 / Ops — Deployment, user acceptance, hardening ## Phase 6 / Ops — Deployment, user acceptance, hardening
**Exit evidence**: deployment smoke automated; Ty walks `docs/UAT-PRACTICE.md` and signs off on UAT results; v1 declared done. **Exit evidence (Evidence class).** Deployment smoke automated (`deploy`). Ty walks `docs/UAT-PRACTICE.md` and signs off (`human-walk`). v1 declared done only after Ty sign-off.
### Ops — compose health, deploy path, backup/restore ### Ops — compose health, deploy path, backup/restore
| Case | What must happen | Where the rule lives | Verified by | | Case | What must happen | Where the rule lives | Verified by | Evidence class |
|---|---|---|---| |---|---|---|---|---|
| OPS-2 | INVENTORY.md filled: APP_HOST, APP_HOSTNAME, ports verified free, Caddy location confirmed, DNS reachable from phone on LAN/Tailscale | `docs/DEPLOYMENT.md` §1, `docs/IMPL-PLAN.md` Phase 6 | Shell script: port scan, Caddy check, DNS resolve | | OPS-2 | INVENTORY.md filled: APP_HOST, APP_HOSTNAME, ports verified free, Caddy location confirmed, DNS reachable from phone on LAN/Tailscale | `docs/DEPLOYMENT.md` §1, `docs/IMPL-PLAN.md` Phase 6 | Shell script: port scan, Caddy check, DNS resolve | `deploy` |
| OPS-3 | Compose health: `GET https://${APP_HOSTNAME}/api/health` ok; `test-woo` ok with species populated; `woo_create_status=draft`; inference disabled for early gates | `docs/DEPLOYMENT.md` §8, `docs/UAT-PRACTICE.md` §1 | Shell script: curl health + test-woo endpoints | | OPS-3 | Compose health: `GET https://${APP_HOSTNAME}/api/health` ok; `test-woo` ok with species populated; `woo_create_status=draft`; inference disabled for early gates | `docs/DEPLOYMENT.md` §8, `docs/UAT-PRACTICE.md` §1 | Shell script: curl health + test-woo endpoints | `deploy` |
| OPS-4 | Backup: `deploy/backup.sh` creates WAL-safe `.backup` of slab.db and tar of images volume; retention 7 daily + 4 weekly | `docs/DEPLOYMENT.md` §6 | Shell script: run backup, verify files and retention | | OPS-4 | Backup: `deploy/backup.sh` creates WAL-safe `.backup` of slab.db and tar of images volume; retention 7 daily + 4 weekly | `docs/DEPLOYMENT.md` §6 | Shell script: run backup, verify files and retention | `deploy` |
| OPS-5 | Restore: stop compose → restore DB + untar images → start compose → health ok; tested at least once before declaring backup done | `docs/DEPLOYMENT.md` §6, `docs/IMPL-PLAN.md` Phase 6 | Shell script: full restore cycle with health assertion | | OPS-5 | Restore: stop compose → restore DB + untar images → start compose → health ok; tested at least once before declaring backup done | `docs/DEPLOYMENT.md` §6, `docs/IMPL-PLAN.md` Phase 6 | Shell script: full restore cycle with health assertion | `deploy` |
### Ops — instrumentation (issue #6 substance) ### Ops — instrumentation (issue #6 substance)
| Case | What must happen | Where the rule lives | Verified by | | Case | What must happen | Where the rule lives | Verified by | Evidence class |
|---|---|---|---| |---|---|---|---|---|
| INS-1 | Health non-200 on database or volume failure; `woo_reachable` false on WooCommerce unreachable | `docs/OPENAPI.md` §health, issue #6 | Unit test with simulated DB/volume failure | | INS-1 | Health non-200 on database or volume failure; `woo_reachable` false on WooCommerce unreachable | `docs/OPENAPI.md` §health, issue #6 | Unit test with simulated DB/volume failure | `proxy-synthetic` |
| INS-2 | Request ID logged on every server request | issue #6 | Unit test: assert request ID in log output | | INS-2 | Request ID logged on every server request | issue #6 | Unit test: assert request ID in log output | `proxy-synthetic` |
| INS-3 | Inference request_payload and response_payload recorded in inference_log; payloads contain model name and confidence data | `docs/DATA-MODEL.md` inference_log, issue #6 | Integration test: assert log entry after inference call | | INS-3 | Inference request_payload and response_payload recorded in inference_log; payloads contain model name and confidence data | `docs/DATA-MODEL.md` inference_log, issue #6 | Integration test: assert log entry after inference call | `proxy-synthetic` |
| INS-4 | Sync_log written with payload_hash on every publish attempt; retains success and failure | `docs/DATA-MODEL.md` sync_log, issue #6 | Integration test: assert sync_log entries for success and failure | | INS-4 | Sync_log written with payload_hash on every publish attempt; retains success and failure | `docs/DATA-MODEL.md` sync_log, issue #6 | Integration test: assert sync_log entries for success and failure | `proxy-synthetic` |
### Ops — end-to-end on all target browsers ### Ops — end-to-end on all target browsers
| Case | What must happen | Where the rule lives | Verified by | | Case | What must happen | Where the rule lives | Verified by | Evidence class |
|---|---|---|---| |---|---|---|---|---|
| E2E-1 | Capture to mask/axis confirm to calibrated upload to review to publish to WooCommerce draft `SLAB-UAT-*` works on iOS Safari (phone) | `docs/UX.md` §Browser compatibility, `docs/IMPL-PLAN.md` Phase 6 | Playwright E2E on iOS Safari | | E2E-1 | Capture to mask/axis confirm to calibrated upload to review to publish to WooCommerce draft `SLAB-UAT-*` works on iOS Safari (phone) | `docs/UX.md` §Browser compatibility, `docs/IMPL-PLAN.md` Phase 6 | Playwright E2E on iOS Safari | `e2e` |
| E2E-2 | Same flow works on Android Chrome (phone) | `docs/UX.md` §Browser compatibility, `docs/IMPL-PLAN.md` Phase 6 | Playwright E2E on Android Chrome | | E2E-2 | Same flow works on Android Chrome (phone) | `docs/UX.md` §Browser compatibility, `docs/IMPL-PLAN.md` Phase 6 | Playwright E2E on Android Chrome | `e2e` |
| E2E-3 | Same flow works on desktop Chrome, Edge, Firefox (PC) | `docs/UX.md` §Browser compatibility, `docs/IMPL-PLAN.md` Phase 6 | Playwright E2E on desktop browsers | | E2E-3 | Same flow works on desktop Chrome, Edge, Firefox (PC) | `docs/UX.md` §Browser compatibility, `docs/IMPL-PLAN.md` Phase 6 | Playwright E2E on desktop browsers | `e2e` |
| E2E-4 | Manual length override recalculates widths/square feet/bdft correctly on all three browser families | `docs/TECH-SPEC-PIPELINE.md` §4.3, `docs/IMPL-PLAN.md` Phase 6 | Playwright E2E: override length, assert recalculation | | E2E-4 | Manual length override recalculates widths/square feet/bdft correctly on all three browser families | `docs/TECH-SPEC-PIPELINE.md` §4.3, `docs/IMPL-PLAN.md` Phase 6 | Playwright E2E: override length, assert recalculation | `e2e` |
| E2E-5 | Client crop: slab approximately 80 percent frame; shorter side ≥ 1600 when source allows; undersized source warns and requests retake | `docs/TECH-SPEC-PIPELINE.md` §5, `docs/IMPL-PLAN.md` Phase 6 | Playwright E2E: inspect crop output dimensions | | E2E-5 | Client crop: slab approximately 80 percent frame; shorter side ≥ 1600 when source allows; undersized source warns and requests retake | `docs/TECH-SPEC-PIPELINE.md` §5, `docs/IMPL-PLAN.md` Phase 6 | Playwright E2E: inspect crop output dimensions | `e2e` |
--- ---
@@ -376,7 +495,7 @@ These cases are the white-box bar for every dependency that touches the product.
**Full suite must pass with inference disabled.** Call 1/Call 2 cases are gated after Gate C is green. **Full suite must pass with inference disabled.** Call 1/Call 2 cases are gated after Gate C is green.
bdft equals square feet times thickness in inches (no divide-by-12). TV-3 proves the identity. Gate B requires TV-3, TV-6, TV-7, and TV-10 to show output on real fixtures. bdft equals square feet times thickness in inches (no divide-by-12). TV-3 proves the identity. Gate B requires TV-3, TV-6, TV-7, and TV-10 as `product-state` or `human-walk` on real fixtures. A `proxy-synthetic` unit pass alone does not satisfy that exit.
--- ---
@@ -422,14 +541,14 @@ This spec folds affirmative case substance from issue #19 where the rule exists
## Gates and phase map ## Gates and phase map
| Phase / Gate | Cases | Exit evidence | | Phase / Gate | Cases | Exit evidence (Evidence class) |
|---|---|---| |---|---|---|
| Phase 0 / Gate B | TV-1 through TV-10, V-1 through V-3, AES-1 through AES-2, H-1, D-1, OPS-1, GC-5 | Ty confirms TV-3, TV-6, TV-7, TV-10 on real fixtures; health up; AES round-trip; draft store; unit/API automated | | Phase 0 / Gate B | TV-1 through TV-10, V-1 through V-3, AES-1 through AES-2, H-1, D-1, OPS-1, GC-5 | See Gate B exit checklist. TV-3/6/7/10 need `product-state` or `human-walk` on real fixtures. Health needs `deploy`. AES and draft store need `api-contract`. Proxy-only TV passes do not green the gate. Bots never declare Gate B green. |
| Gate C (early Phase 1) | GC-1 through GC-4, W-1 through W-5, INF-1 through INF-4, UI-1 through UI-5 | Ty opens one `SLAB-UAT-*` WooCommerce draft in real store admin; inference OFF; mocked-store not exit | | Gate C (early Phase 1) | GC-1 through GC-4, W-1 through W-5, INF-1 through INF-4, UI-1 through UI-5 | Ty opens one `SLAB-UAT-*` Woo draft in real store admin (`human-walk` or `product-state`). Inference OFF. Mocked-store is not exit. Bots never declare Gate C green. |
| Phase 1 remainder | API-1 through API-10, TAX-1, TAX-2, TAX-3 | Agent-written test automation drives full calibrated to ready cycle; OPENAPI matches; revision and taxonomy green | | Phase 1 remainder | API-1 through API-10, TAX-1, TAX-2, TAX-3 | Full calibrated to ready cycle via API (`api-contract`). OPENAPI matches. Revision and taxonomy verified. |
| Phase 3 | UIE-1 through UIE-10 | Ty publishes test slab as Woo draft `SLAB-UAT-*`; automation UI cases; Ty is exit | | Phase 3 | UIE-1 through UIE-10 | Ty publishes test slab as Woo draft `SLAB-UAT-*` (`human-walk` or `product-state`). UI automation is `e2e`. Ty is exit. |
| Phase 5 | C1-1 through C2-4, OFF-1, OFF-2 | Ty runs species detection on 3 real slabs with judgment on acceptance; automation Call 1/2 cases; inference disabled path green | | Phase 5 | C1-1 through C2-4, OFF-1, OFF-2 | Ty judgment on 3 real slabs (`human-walk` or `product-state`). Call 1/2 automation is `proxy-synthetic`. Inference-disabled path proven per case class. |
| Phase 6 / production | OPS-2 through OPS-5, INS-1 through INS-4, E2E-1 through E2E-5 | Deployment smoke automated; Ty walks `docs/UAT-PRACTICE.md` and signs off on UAT; v1 done | | Phase 6 / production | OPS-2 through OPS-5, INS-1 through INS-4, E2E-1 through E2E-5 | Deploy smoke (`deploy`). Ty walks `docs/UAT-PRACTICE.md` (`human-walk`). v1 done only after Ty sign-off. |
--- ---
+51 -29
View File
@@ -1,6 +1,8 @@
# Gate B Results — 2026-09-07 # Gate B Results, 2026-09-07
Evidence branch: `gate-b` · tip `b3e2faf` (evidence logs from this commit; report fixed in next) Evidence branch: `gate-b`. Runner logs tip `b3e2faf`. This report follows `docs/TEST-SPEC.md` Evidence & process contract.
Allowed results only: `pass` | `fail` | `blocked`. Soft words do not count. Bots never declare Gate B green.
--- ---
@@ -8,43 +10,63 @@ Evidence branch: `gate-b` · tip `b3e2faf` (evidence logs from this commit; repo
Every Gate B test case from `docs/TEST-SPEC.md`, stated in plain English for the mill owner. Every Gate B test case from `docs/TEST-SPEC.md`, stated in plain English for the mill owner.
| Case | What this proves for the mill product | Result | Evidence | Blocker | | Case | What this proves for the mill product | Evidence class | Result | Evidence | Blocker |
|---|---|---|---|---| |---|---|---|---|---|---|
| **TV-1** | Green-background photos: the slab interior stays visible after the green sheet is removed. | pass | `test-results/gate-b-2026-09-07/vitest.txt:33-34` | — | | **TV-1** | Green-background photos: the slab interior stays visible after the green sheet is removed. | `proxy-synthetic` | pass | `test-results/gate-b-2026-09-07/vitest.txt:33-34` | |
| **TV-2** | Black-background photos: the slab interior stays visible after the black sheet is removed; only border-connected dark pixels are cut out. | pass | `test-results/gate-b-2026-09-07/vitest.txt:35-36` | — | | **TV-2** | Black-background photos: the slab interior stays visible after the black sheet is removed; only border-connected dark pixels are cut out. | `proxy-synthetic` | pass | `test-results/gate-b-2026-09-07/vitest.txt:35-36` | |
| **TV-3** | Board feet math is correct: bdft = sqft × thickness (no divide-by-12). 1 sqft at 1 inch thick = 1 bdft. | pass | `test-results/gate-b-2026-09-07/vitest.txt:22-23` | — | | **TV-3** | Board feet math unit runner (synthetic). bdft = sqft × thickness. Does not satisfy Gate B product-state exit. | `proxy-synthetic` | pass | `test-results/gate-b-2026-09-07/vitest.txt:22-23` | |
| **TV-4** | Width samples measured at every 6 inches along the length axis report min, max, and average correctly. | pass | `test-results/gate-b-2026-09-07/vitest.txt:31` | — | | **TV-4** | Width samples measured at every 6 inches along the length axis report min, max, and average correctly. | `proxy-synthetic` | pass | `test-results/gate-b-2026-09-07/vitest.txt:31` | |
| **TV-5** | Slabs with one angled end: the length axis follows the full length, and widths stay perpendicular. | pass | `test-results/gate-b-2026-09-07/vitest.txt:32` | — | | **TV-5** | Slabs with one angled end: the length axis follows the full length, and widths stay perpendicular. | `proxy-synthetic` | pass | `test-results/gate-b-2026-09-07/vitest.txt:32` | |
| **TV-6** | Output photos are cropped to 3:4 ratio, slab fills 80% of the frame, shorter side ≥ 1600 px, saved as transparent PNG. | pass | `test-results/gate-b-2026-09-07/vitest.txt:142-145` | — | | **TV-6** | Crop/PNG unit runner (synthetic). 3:4, 80% fill, ≥1600, transparent PNG. Does not satisfy Gate B product-state exit. | `proxy-synthetic` | pass | `test-results/gate-b-2026-09-07/vitest.txt:142-145` | |
| **TV-7** | If the source photo is too small to make a good output, the system warns and refuses to upscale — requires a retake. | pass | `test-results/gate-b-2026-09-07/vitest.txt:146-148` | — | | **TV-7** | Undersized-source unit runner (synthetic). Warn and refuse upscale. Does not satisfy Gate B product-state exit. | `proxy-synthetic` | pass | `test-results/gate-b-2026-09-07/vitest.txt:146-148` | |
| **TV-8** | Pricing: recommended price = bdft × price-per-bdft; user overrides are stored and rounded correctly. | pass | `test-results/gate-b-2026-09-07/vitest.txt:4-9` | — | | **TV-8** | Pricing: recommended price = bdft × price-per-bdft; user overrides are stored and rounded correctly. | `proxy-synthetic` | pass | `test-results/gate-b-2026-09-07/vitest.txt:4-9` | |
| **TV-9** | Thickness in inches maps to the right WooCommerce range label (e.g. 1" rounds up to "1"–1 1/2""). | pass | `test-results/gate-b-2026-09-07/vitest.txt:10-15` | — | | **TV-9** | Thickness in inches maps to the right WooCommerce range label (for example 1" rounds up to 1"–1 1/2"). | `proxy-synthetic` | pass | `test-results/gate-b-2026-09-07/vitest.txt:10-15` | |
| **TV-10** | All numbers round half-up to 2 decimals (length, sqft, bdft, price). | pass | `test-results/gate-b-2026-09-07/vitest.txt:24` | — | | **TV-10** | Rounding unit runner (synthetic). Half-up to 2 decimals. Does not satisfy Gate B product-state exit. | `proxy-synthetic` | pass | `test-results/gate-b-2026-09-07/vitest.txt:24` | |
| **V-1** | SKU must be 3–24 characters, uppercase letters, digits, and hyphens only. Valid SKUs accepted; invalid ones rejected. | pass | `test-results/gate-b-2026-09-07/vitest.txt:96-104` | — | | **V-1** | SKU must be 3–24 characters, uppercase letters, digits, and hyphens only. Valid SKUs accepted; invalid ones rejected. | `proxy-synthetic` | pass | `test-results/gate-b-2026-09-07/vitest.txt:96-104` | |
| **V-2** | Title, short title, and description have WooCommerce character limits enforced. | pass | `test-results/gate-b-2026-09-07/vitest.txt:105-115` | — | | **V-2** | Title, short title, and description have WooCommerce character limits enforced. | `proxy-synthetic` | pass | `test-results/gate-b-2026-09-07/vitest.txt:105-115` | |
| **V-3** | The validation module version hash works: the server returns a fresh module when the client's cached version is stale. | pass | `test-results/gate-b-2026-09-07/vitest.txt:116-120` | — | | **V-3** | The validation module version hash works: the server returns a fresh module when the client's cached version is stale. | `proxy-synthetic` | pass | `test-results/gate-b-2026-09-07/vitest.txt:116-120` | |
| **AES-1** | WooCommerce credentials are encrypted at rest and decrypt back to the original values; ciphertext never leaks in settings responses. | pass | `test-results/gate-b-2026-09-07/pytest.txt:30-43` | — | | **AES-1** | WooCommerce credentials are encrypted at rest and decrypt back to the original values; ciphertext never leaks in settings responses. | `api-contract` | pass | `test-results/gate-b-2026-09-07/pytest.txt:30-43` | |
| **AES-2** | Without the AES key, the app refuses to start — no fallback to storing secrets in plain text. | pass | `test-results/gate-b-2026-09-07/pytest.txt:44-45` | — | | **AES-2** | Without the AES key, the app refuses to start. No fallback to storing secrets in plain text. | `proxy-synthetic` | pass | `test-results/gate-b-2026-09-07/pytest.txt:44-45` | |
| **H-1** | Health endpoint returns OK with version and `woo_reachable: false` when no store is configured. | pass | `test-results/gate-b-2026-09-07/pytest.txt:9-13` | — | | **H-1** | Health endpoint returns OK with version and `woo_reachable: false` when no store is configured. | `api-contract` | pass | `test-results/gate-b-2026-09-07/pytest.txt:9-13` | |
| **D-1** | Creating a slab draft via the API (multipart upload with data, files, metadata) returns 201, sets status to calibrated, and returns server revision. | fail | `test-results/gate-b-2026-09-07/pytest.txt:14-21` | [#29](https://gitea.vps1.afterthedemo.com/Ty_Tech/SlabUploader/issues/29) — `python-multipart` not installed | | **D-1** | Creating a slab draft via the API (multipart upload with data, files, metadata) returns 201, sets status to calibrated, and returns server revision. | `api-contract` | fail | `test-results/gate-b-2026-09-07/pytest.txt:14-21` | [#29](https://gitea.vps1.afterthedemo.com/Ty_Tech/SlabUploader/issues/29). `python-multipart` not installed. |
| **OPS-1** | After deploying Docker Compose, `GET https://APP_HOSTNAME/api/health` returns OK. | not run | `test-results/gate-b-2026-09-07/health.txt:18-26` | HTTPS path broken: `slab.local` unresolved; ports 443/18443 refused. HTTP `:18081` health returned 200 (interim only). | | **OPS-1** | After deploying Docker Compose, `GET https://APP_HOSTNAME/api/health` returns OK. | `deploy` | blocked | `test-results/gate-b-2026-09-07/health.txt:18-26` | HTTPS path broken: `slab.local` unresolved; ports 443/18443 refused. HTTP `:18081` health returned 200 (interim only, not the specified deploy path). |
| **GC-5** | The app refuses to start without `WOO_BASE_URL` or with an `http://` URL. The store URL is read-only in settings. | pass | `test-results/gate-b-2026-09-07/pytest.txt:22-29` | — | | **GC-5** | The app refuses to start without `WOO_BASE_URL` or with an `http://` URL. The store URL is read-only in settings. | `proxy-synthetic` | pass | `test-results/gate-b-2026-09-07/pytest.txt:22-29` | |
--- ---
## Summary ## Summary
17 pass · 1 fail · 1 not run 16 pass · 1 fail · 1 blocked · 0 product-state Gate B exit rows
Pass counts above include honest `proxy-synthetic` and `api-contract` rows. They do not mean Gate B is ready.
---
## Gate B exit checklist
Coding tip is not test complete. Test complete is not Gate ready. Bots never declare Gate B green.
| Exit condition | Required Evidence class | Result | Evidence / blocker |
|---|---|---|---|
| TV-3 bdft identity on real fixtures | `product-state` or `human-walk` | blocked | Only `proxy-synthetic` vitest pass recorded. No real-fixture product-state proof. |
| TV-6 crop/PNG rules on real fixtures | `product-state` or `human-walk` | blocked | Only `proxy-synthetic` vitest pass recorded. No real-fixture product-state proof. |
| TV-7 undersized-source warn/retake on real fixtures | `product-state` or `human-walk` | blocked | Only `proxy-synthetic` vitest pass recorded. No real-fixture product-state proof. |
| TV-10 rounding on real fixtures | `product-state` or `human-walk` | blocked | Only `proxy-synthetic` vitest pass recorded. No real-fixture product-state proof. |
| Health returns 200 on the specified HTTPS deploy path | `deploy` | blocked | OPS-1 blocked. HTTPS path broken. |
| AES credentials round-trip without ciphertext leak | `api-contract` | pass | AES-1 pytest evidence. |
| Draft store create/persist/retrieve | `api-contract` | fail | D-1 fail. Issue #29. `python-multipart` missing. |
| Unit and API automation recorded for Gate B cases | `proxy-synthetic` and/or `api-contract` as declared | pass | Vitest and pytest logs under `test-results/gate-b-2026-09-07/`. |
--- ---
## Unproven ## Unproven
The following are not covered by any passing test or evidence at this gate: The following are not covered by any passing evidence at the class Gate B exit requires:
- **Walkable UI / phone capture** — The frontend on the host is a stub. No phone or desktop browser flow has been exercised end to end (capture → mask → axis → numbers → review → publish). - **TV-3 / TV-6 / TV-7 / TV-10 product-state.** Unit runners passed as `proxy-synthetic` only. Gate B exit still needs real fixtures (`product-state` or `human-walk`).
- **HTTPS deployment** — OPS-1 was not run. The HTTPS path to the deployed app is not working. HTTP-only health on port 18081 returned OK but is not the specified deployment path. - **Walkable UI / phone capture.** The frontend on the host is a stub. No phone or desktop browser flow has been exercised end to end (capture to mask to axis to numbers to review to publish).
- **Real draft create on the deployed stack** — D-1 failed because `python-multipart` is missing. The API cannot accept multipart form data (the format used to upload slab photos and metadata). Issue #29 tracks this blocker. - **HTTPS deployment.** OPS-1 is `blocked`. The HTTPS path to the deployed app is not working. HTTP-only health on port 18081 returned OK but is not the specified deployment path.
- **End-to-end browser flows** — No Playwright or browser-based tests have been executed. All passing tests are isolated unit or API contract tests against synthetic fixtures, not real browser sessions. - **Real draft create on the deployed stack.** D-1 failed because `python-multipart` is missing. The API cannot accept multipart form data. Issue #29 tracks this blocker.
- **End-to-end browser flows.** No Playwright or browser-based tests have been executed. Passing rows here are unit or API contract tests against synthetic fixtures, not real browser sessions.
Gate B is not green. UAT is not ready. Gate B is not green. UAT is not ready.