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
5 changed files with 1522 additions and 0 deletions
Showing only changes of commit c591ff1629 - Show all commits
@@ -0,0 +1,50 @@
# Gate B pass2 results — 2026-09-07
Evidence collected on gate-b @ 302d694.
---
## Results
| Case | What this proves (mill-owner English) | Evidence class | Result | Evidence | Blocker |
|---|---|---|---|---|---|
| TV-1 | Green-sheet chroma-key keeps interior green patch, does not eat border wood | `proxy-synthetic` | pass | vitest.txt:13 | — |
| TV-2 | Black-sheet threshold keeps dark interior wood, flood-fill isolates border-only | `proxy-synthetic` | pass | vitest.txt:13 | — |
| TV-3 | bdft = sqft × thickness_in; 1 sqft @ 1" = 1 bdft; no divide-by-12 | `product-state` | blocked | — | No real slab fixtures or Ty walk on real wood |
| TV-3 | bdft identity holds on synthetic fixture (proxy proof only) | `proxy-synthetic` | pass | vitest.txt:4–5,12 | — |
| TV-4 | Width samples at 6" stations report min, max, average perpendicular to confirmed axis | `proxy-synthetic` | pass | vitest.txt:14–15,20 | — |
| TV-5 | Angled-end slab: length axis follows prevailing length; widths stay perpendicular after confirm | `proxy-synthetic` | pass | vitest.txt:17–18,20 | — |
| TV-6 | 3:4 crop, slab centered, 80% fill, transparent PNG, shorter side ≥ 1600 when source allows | `product-state` | blocked | — | No real slab photos or Ty walk on real wood |
| TV-6 | Crop dimensions and fill ratio hold on synthetic fixture (proxy proof only) | `proxy-synthetic` | pass | vitest.txt:80–90,101 | — |
| TV-7 | Undersized source warns, refuses upscale, photo not publishable until retake | `product-state` | blocked | — | No real slab photos or Ty walk on real wood |
| TV-7 | Undersized-source logic holds on synthetic fixture (proxy proof only) | `proxy-synthetic` | pass | vitest.txt:92–99,101 | — |
| TV-8 | Pricing: bdft 0.96 × $12.50 → $12.00 recommended; override stored; no rule leaves empty | `proxy-synthetic` | pass | vitest.txt:11 | — |
| TV-9 | Thickness 1" rounds up to Woo range using synced attribute term, not hardcoded label | `proxy-synthetic` | pass | vitest.txt:10 | — |
| TV-10 | Rounding: 96.04" → 96"; 0.641 sqft → 0.64; 11.525 bdft → 11.53; 12.345 price → 12.35 | `product-state` | blocked | — | No real slab measurements or Ty walk on real wood |
| TV-10 | Rounding rules hold on synthetic fixture (proxy proof only) | `proxy-synthetic` | pass | vitest.txt:7–8,12 | — |
| V-1 | SKU charset `[A-Z0-9-]{3,24}` accepted; invalid chars/rejected length rejected | `proxy-synthetic` | pass | vitest.txt:21–50,79 | — |
| V-2 | Title/short_title/description maxLength enforced at field exit and submit | `proxy-synthetic` | pass | vitest.txt:52–68,79 | — |
| V-3 | Validation module hash: client cache, submit carry, mismatch returns 412 with fresh module | `proxy-synthetic` | pass | vitest.txt:70–77,79 | — |
| AES-1 | Credentials round-trip without ciphertext leak; settings GET never returns app password | `api-contract` | pass | pytest.txt:46 | — |
| AES-1 | Encrypt-then-decrypt round-trips for username, password, empty, unicode, special chars | `proxy-synthetic` | pass | pytest.txt:9–17 | — |
| AES-2 | Missing SLAB_AES_KEY env var: app refuses to start; no plaintext fallback | `proxy-synthetic` | pass | pytest.txt:23–24 | — |
| H-1 | `GET /api/health` returns 200 `{ "status":"ok" }` with `woo_reachable` false when not configured | `api-contract` | pass | pytest.txt:27–31 | — |
| D-1 | `POST /api/v1/slabs` accepts multipart, returns 201 with `status: calibrated` and `server_rev` | `api-contract` | fail | pytest.txt:32–39 | [#29](https://gitea.vps1.afterthedemo.com/Ty_Tech/SlabUploader/issues/29) |
| OPS-1 | `GET https://${APP_HOSTNAME}/api/health` returns ok after compose up with filled inventory | `deploy` | blocked | — | health.txt: slab.local unresolved, 443/18443 refuse, HTTP :18081 timed out |
| GC-5 | FastAPI refuses to start when WOO_BASE_URL missing or set to `http://` | `proxy-synthetic` | pass | pytest.txt:40–41 | — |
---
## Counts
**17 pass / 1 fail / 5 blocked** (23 total rows)
---
## Unproven / Gate exit
- **TV-3, TV-6, TV-7, TV-10 product-state**: No real slab fixtures, no Ty walk. Proxy-synthetic rows pass but do not satisfy Gate B exit.
- **OPS-1 deploy**: No compose-up, no Caddy, no inventory filled. Health endpoints all unreachable (health.txt).
- **Walkable UI / E2E**: No Playwright E2E tests exist yet; no browser session captured.
- **Real draft create (D-1)**: All 8 TestCreateSlab tests failed with `python-multipart` missing; no slab was created.
- **Gate B is not green. UAT is not ready.** Bots do not declare Gate B green.
@@ -0,0 +1,16 @@
Mon Sep 7 18:02:48 UTC 2026
=== HTTP LAN :18081 ===
curl: (28) Connection timed out after 3002 milliseconds
HTTP_FAIL:28
=== HTTPS slab.local ===
curl: (6) Could not resolve host: slab.local
HTTPS_DNS_FAIL:6
=== HTTPS 100.64.0.9:443 ===
curl: (7) Failed to connect to 100.64.0.9 port 443 after 652 ms: Could not connect to server
HTTPS443_FAIL:7
=== HTTPS 100.64.0.9:18443 ===
curl: (7) Failed to connect to 100.64.0.9 port 18443 after 147 ms: Could not connect to server
HTTPS18443_FAIL:7
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,2 @@
Mon Sep 7 18:02:14 UTC 2026
302d694
@@ -0,0 +1,108 @@
RUN v2.1.9 /workspace/gitea-atd/Ty_Tech/SlabUploader/frontend
stdout | src/__tests__/tv3-tv10-units.test.ts > TV-3 Scale, sqft, bdft > bdft = sqft × thickness_in with no divide-by-12
{"case":"TV-3","lengthIn":96,"thicknessIn":1.5,"sqft":0.64,"bdft":0.96,"expectedBdft":0.96,"identity_1sqft_1in":1}
stdout | src/__tests__/tv3-tv10-units.test.ts > TV-10 Rounding > half-up / nearest-step matches TECH-SPEC vectors
{"case":"TV-10","length":{"raw":96.04,"rounded":96,"expected":96},"sqft":{"raw":0.641,"rounded":0.64,"expected":0.64},"bdft":{"raw":11.525,"rounded":11.53,"expected":11.53},"price":{"raw":12.345,"rounded":12.35,"expected":12.35}}
✓ src/__tests__/tv9-thickness-band.test.ts (6 tests) 5ms
✓ src/__tests__/tv8-pricing.test.ts (6 tests) 5ms
✓ src/__tests__/tv3-tv10-units.test.ts (3 tests) 5ms
✓ src/__tests__/tv1-tv2-mask.test.ts (5 tests) 38ms
stdout | src/__tests__/tv4-tv5-axis.test.ts > TV-4 Width samples at 6" stations > reports min, max, average perpendicular to confirmed axis on 96" length
{"case":"TV-4","lengthPx":96,"widthPx":24,"scale":1,"stations":[6,12,18,24,30,36,42,48,54,60,66,72,78,84,90],"widths":[23.999999046325684,23.999999046325684,23.999999046325684,23.999999046325684,23.999999046325684,23.999999046325684,23.999999046325684,23.999999046325684,23.999999046325684,23.999999046325684,23.999999046325684,23.999999046325684,23.999999046325684,23.999999046325684,23.999999046325684],"min":23.999999046325684,"max":23.999999046325684,"average":23.999999046325684,"expectedWidthIn":24}
stdout | src/__tests__/tv4-tv5-axis.test.ts > TV-5 Angled-end length axis > follows prevailing length; widths stay perpendicular after confirm
{"case":"TV-5","direction":{"x":1,"y":0},"lengthPx":96,"widthPx":24,"bodySampleCount":12,"bodyWidths":[23.999999046325684,23.999999046325684,23.999999046325684,23.999999046325684,23.999999046325684,23.999999046325684,23.999999046325684,23.999999046325684,23.999999046325684,23.999999046325684,23.999999046325684,23.999999046325684],"min":0,"max":23.999999046325684,"average":21.19999910990397,"rotatedDirection":{"x":6.123233995736766e-17,"y":1}}
✓ src/__tests__/tv4-tv5-axis.test.ts (2 tests) 43ms
stdout | src/__tests__/validation.test.ts > V-1 SKU validation > accepts all valid SKU boundary strings
V-1 valid: "ABC" → length=3, valid=true
V-1 valid: "012" → length=3, valid=true
V-1 valid: "A0A" → length=3, valid=true
V-1 valid: "A-B" → length=3, valid=true
V-1 valid: "0-1" → length=3, valid=true
V-1 valid: "BW-0042" → length=7, valid=true
V-1 valid: "SLAB-UAT-001" → length=12, valid=true
V-1 valid: "A9Z" → length=3, valid=true
V-1 valid: "AAAAAAAAAAAAAAAAAAAAAAAA" → length=24, valid=true
V-1 valid: "A1B2C3D4E5F6G7H8I9J0K-12" → length=24, valid=true
V-1 valid: "A-A-A-A-A-A" → length=11, valid=true
V-1 valid: "1-2-3-4-5-6" → length=11, valid=true
V-1 valid: "ABCDEF" → length=6, valid=true
stdout | src/__tests__/validation.test.ts > V-1 SKU validation > rejects all invalid SKU boundary strings
V-1 invalid: "A" → valid=false, errors=too_short, invalid_charset
V-1 invalid: "AB" → valid=false, errors=too_short, invalid_charset
V-1 invalid: "AAAAAAAAAAAAAAAAAAAAAAAAA" → valid=false, errors=too_long, invalid_charset
V-1 invalid: "abc" → valid=false, errors=invalid_charset
V-1 invalid: "bw-0042" → valid=false, errors=invalid_charset
V-1 invalid: "aB3" → valid=false, errors=invalid_charset
V-1 invalid: "ABC!" → valid=false, errors=invalid_charset
V-1 invalid: "A B" → valid=false, errors=invalid_charset
V-1 invalid: "A_B" → valid=false, errors=invalid_charset
V-1 invalid: "ABC.D" → valid=false, errors=invalid_charset
V-1 invalid: "ABC#1" → valid=false, errors=invalid_charset
V-1 invalid: "ABC/1" → valid=false, errors=invalid_charset
V-1 invalid: "ABC\1" → valid=false, errors=invalid_charset
V-1 invalid: "" → valid=false, errors=too_short, invalid_charset
stdout | src/__tests__/validation.test.ts > V-2 content field maxLength validation > accepts title at exactly maxLength
V-2 title: length=200, valid=true
stdout | src/__tests__/validation.test.ts > V-2 content field maxLength validation > rejects title that exceeds maxLength by 1 character
V-2 title invalid: length=201, valid=false, errors=exceeds_max_length
stdout | src/__tests__/validation.test.ts > V-2 content field maxLength validation > accepts short_title at exactly maxLength
V-2 short_title: length=200, valid=true
stdout | src/__tests__/validation.test.ts > V-2 content field maxLength validation > rejects short_title that exceeds maxLength by 1 character
V-2 short_title invalid: length=201, valid=false, errors=exceeds_max_length
stdout | src/__tests__/validation.test.ts > V-2 content field maxLength validation > accepts description at exactly maxLength
V-2 description: length=2500, valid=true
stdout | src/__tests__/validation.test.ts > V-2 content field maxLength validation > rejects description that exceeds maxLength by 1 character
V-2 description invalid: length=2501, valid=false, errors=exceeds_max_length
stdout | src/__tests__/validation.test.ts > V-3 validation module version hash > computes a stable hash from current validation rules
V-3 module hash: "v1"
stdout | src/__tests__/validation.test.ts > V-3 validation module version hash > validateModuleHash returns true when client hash matches current module
V-3 match: hash="v1", valid=true
stdout | src/__tests__/validation.test.ts > V-3 validation module version hash > validateModuleHash returns 412 with fresh module when client hash mismatches
V-3 mismatch: clientHash="v0", serverHash="v1", errors=validation_module_stale
✓ src/__tests__/validation.test.ts (25 tests) 29ms
stdout | src/__tests__/tv6-tv7-crop.test.ts > TV-6 3:4 crop, 80% fill, transparent PNG, ≥1600px > computes crop target dimensions preserving 3:4 aspect and 80% fill at extremes
{"case":"TV-6","target1":{"slabW":1281,"slabH":3000,"frame":{"width":1601,"height":2135},"aspect":0.7498829039812647},"target2":{"slabW":2000,"slabH":1600,"frame":{"width":1500,"height":2000},"aspect":0.75}}
stdout | src/__tests__/tv6-tv7-crop.test.ts > TV-6 3:4 crop, 80% fill, transparent PNG, ≥1600px > finds slab bounding box correctly
{"case":"TV-6","bounds":{"left":1360,"top":100,"right":2640,"bottom":3099,"widthPx":1281,"heightPx":3000}}
stdout | src/__tests__/tv6-tv7-crop.test.ts > TV-6 3:4 crop, 80% fill, transparent PNG, ≥1600px > crops large source to 3:4 with 80% fill, shorter side ≥ 1600, transparent PNG
{"case":"TV-6","frameW":1601,"frameH":2135,"shorterSide":1601,"meetsMinPx":true,"marginAlpha":0,"slabCenterAlpha":255,"aspect":0.7498829039812647}
stdout | src/__tests__/tv6-tv7-crop.test.ts > TV-6 3:4 crop, 80% fill, transparent PNG, ≥1600px > property: fill at extremes equals FILL_TARGET at limiting dimension
{"case":"TV-6","fillAtLimitingDim":0.8001249219237976,"expectedFill":0.8}
stdout | src/__tests__/tv6-tv7-crop.test.ts > TV-7 Too-small source — warn, no upscale, not publishable > detects undersized source and refuses upscale
{"case":"TV-7","slabW":400,"slabH":300,"frameW":281,"frameH":375,"shorterSide":281,"outputPxMin":1600,"undersized":true}
stdout | src/__tests__/tv6-tv7-crop.test.ts > TV-7 Too-small source — warn, no upscale, not publishable > cropTo34 returns meetsMinPx = false for undersized source
{"case":"TV-7","frameW":281,"frameH":375,"shorterSide":281,"meetsMinPx":false}
stdout | src/__tests__/tv6-tv7-crop.test.ts > TV-7 Too-small source — warn, no upscale, not publishable > property: no upscale path — output is never larger than source
{"case":"TV-7","noUpscaleCheck":{"small":{"source":"800x640","output":"281x375"},"large":{"source":"4000x3200","output":"1601x2135"}}}
✓ src/__tests__/tv6-tv7-crop.test.ts (7 tests) 789ms
Test Files 7 passed (7)
Tests 54 passed (54)
Start at 18:02:58
Duration 1.29s (transform 558ms, setup 0ms, collect 966ms, tests 914ms, environment 1ms, prepare 574ms)
EXIT:0