feat(gate-b): add product-state fixture harness for TV-3/6/7/10

Real-fixture path and harness for product-state evidence on TV-3 bdft identity,
TV-6 crop/PNG rules, TV-7 undersized warn/retake, and TV-10 rounding.

- fixtures/real/: acquisition README, placeholder metadata JSON templates
- fixtures/real-fixtures.ts: loader that decodes real PNGs, generates masks,
  runs the pipeline, fails-closed when absent or unmarked
- tv3-real.test.ts / tv6-real.test.ts / tv7-real.test.ts / tv10-real.test.ts:
  product-state tests that skip when real fixtures are absent
- Updated RESULTS.md with real-fixture harness note
- Added pngjs devDependency for PNG decoding in Node.js

Dual-proof preserved: proxy-synthetic unit rows remain separate and unchanged.
Gate B is not green; product-state rows are blocked pending Ty-supplied photos.
This commit is contained in:
2026-09-07 18:23:04 +00:00
parent d5d5cded62
commit fb892e4a32
13 changed files with 668 additions and 6 deletions
+10
View File
@@ -8,6 +8,7 @@
"name": "slabuploader-frontend",
"version": "0.0.0",
"devDependencies": {
"pngjs": "^7.0.0",
"typescript": "^5.6.3",
"vitest": "^2.1.4"
}
@@ -1044,6 +1045,15 @@
"integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
"dev": true
},
"node_modules/pngjs": {
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/pngjs/-/pngjs-7.0.0.tgz",
"integrity": "sha512-LKWqWJRhstyYo9pGvgor/ivk2w94eSjE3RGVuzLGlr3NmD8bf7RcYGze1mNdEHRP6TRP6rMuDHk5t44hnTRyow==",
"dev": true,
"engines": {
"node": ">=14.19.0"
}
},
"node_modules/postcss": {
"version": "8.5.28",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.28.tgz",
+1
View File
@@ -8,6 +8,7 @@
"test:watch": "vitest"
},
"devDependencies": {
"pngjs": "^7.0.0",
"typescript": "^5.6.3",
"vitest": "^2.1.4"
}
@@ -0,0 +1,225 @@
/**
* Real fixture harness — product-state evidence for TV-3, TV-6, TV-7, TV-10.
*
* Loads real PNG photos from `fixtures/real/`, decodes them, runs the
* pipeline to generate masks, and returns fixture data for product-state tests.
*
* When real PNG files are absent, every fixture returns `"blocked"` — the test
* harness fails-closed; it never substitutes synthetic data.
*/
import { readFileSync, existsSync } from "node:fs";
import { resolve, dirname, join } from "node:path";
import { fileURLToPath } from "node:url";
import { PNG } from "pngjs";
import type { RgbaImage, MaskBuffer, LengthAxis } from "../../lib/pipeline/types.js";
import { defaultKnobs, removeSheetBackground } from "../../lib/pipeline/mask.js";
import { findSlabBounds } from "../../lib/pipeline/crop.js";
// ---------------------------------------------------------------------------
// Paths
// ---------------------------------------------------------------------------
const __dirname = dirname(fileURLToPath(import.meta.url));
const REAL_DIR = join(__dirname, "real");
function realPath(filename: string): string {
return join(REAL_DIR, filename);
}
// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------
/** Decode a PNG file into an RgbaImage. Throws when file missing or corrupt. */
function decodePng(filePath: string): RgbaImage {
const data = readFileSync(filePath);
const png = PNG.sync.read(data);
return {
width: png.width,
height: png.height,
data: png.data, // Uint8ClampedArray-compatible (Uint8Array is fine for tests)
};
}
/** Generate a binary mask by running the pipeline's sheet-removal on the image. */
function generateMask(image: RgbaImage): MaskBuffer {
const result = removeSheetBackground(image, defaultKnobs());
return result.mask;
}
/** Build a simple right-pointing axis for the slab's bounding box. */
function buildAxis(mask: MaskBuffer, width: number, height: number): LengthAxis {
const bounds = findSlabBounds(mask, width, height);
const center = {
x: (bounds.left + bounds.right) / 2,
y: (bounds.top + bounds.bottom) / 2,
};
return {
center,
direction: { x: 1, y: 0 },
lengthPx: bounds.widthPx,
widthPx: bounds.heightPx,
corners: [
{ x: bounds.left, y: bounds.top },
{ x: bounds.right, y: bounds.top },
{ x: bounds.right, y: bounds.bottom },
{ x: bounds.left, y: bounds.bottom },
],
};
}
// ---------------------------------------------------------------------------
// Fixture loaders
// ---------------------------------------------------------------------------
export interface RealFixtureResult {
/** The decoded real image. */
image: RgbaImage;
/** Auto-generated binary mask from the real image. */
mask: MaskBuffer;
/** Length axis derived from the mask. */
axis: LengthAxis;
/** User-measured length in inches. */
lengthIn: number;
/** User-measured thickness in inches. */
thicknessIn: number;
/** Sqft computed from the real mask. */
computedSqft: number;
}
/** Attempt to load the TV-3 real slab fixture. Returns "blocked" when PNG absent or not marked real. */
export function loadTv3RealFixture(): "blocked" | RealFixtureResult {
const pngPath = realPath("tv3-slab.png");
const jsonPath = realPath("tv3-slab.json");
if (!existsSync(pngPath)) return "blocked";
if (!existsSync(jsonPath)) return "blocked";
const meta = JSON.parse(readFileSync(jsonPath, "utf-8"));
if (meta.real !== true) return "blocked";
const image = decodePng(pngPath);
const mask = generateMask(image);
const axis = buildAxis(mask, image.width, image.height);
const scale = axis.lengthPx / meta.lengthIn;
const maskPixelCount = countMaskPixels(mask, image.width, image.height);
const sqft = (maskPixelCount / (scale * scale)) / 144;
return {
image,
mask,
axis,
lengthIn: meta.lengthIn,
thicknessIn: meta.thicknessIn,
computedSqft: Math.round(sqft * 100) / 100,
};
}
/** Attempt to load the TV-6 real slab fixture. Returns "blocked" when PNG absent or not marked real. */
export function loadTv6RealFixture(): "blocked" | RealFixtureResult {
const pngPath = realPath("tv6-slab.png");
const jsonPath = realPath("tv6-slab.json");
if (!existsSync(pngPath)) return "blocked";
if (!existsSync(jsonPath)) return "blocked";
const meta = JSON.parse(readFileSync(jsonPath, "utf-8"));
if (meta.real !== true) return "blocked";
const image = decodePng(pngPath);
const mask = generateMask(image);
const axis = buildAxis(mask, image.width, image.height);
const scale = axis.lengthPx / meta.lengthIn;
const maskPixelCount = countMaskPixels(mask, image.width, image.height);
const sqft = (maskPixelCount / (scale * scale)) / 144;
return {
image,
mask,
axis,
lengthIn: meta.lengthIn,
thicknessIn: meta.thicknessIn,
computedSqft: Math.round(sqft * 100) / 100,
};
}
/** Attempt to load the TV-7 real slab fixture. Returns "blocked" when PNG absent or not marked real. */
export function loadTv7RealFixture(): "blocked" | RealFixtureResult {
const pngPath = realPath("tv7-slab.png");
const jsonPath = realPath("tv7-slab.json");
if (!existsSync(pngPath)) return "blocked";
if (!existsSync(jsonPath)) return "blocked";
const meta = JSON.parse(readFileSync(jsonPath, "utf-8"));
if (meta.real !== true) return "blocked";
const image = decodePng(pngPath);
const mask = generateMask(image);
const axis = buildAxis(mask, image.width, image.height);
const scale = axis.lengthPx / meta.lengthIn;
const maskPixelCount = countMaskPixels(mask, image.width, image.height);
const sqft = (maskPixelCount / (scale * scale)) / 144;
return {
image,
mask,
axis,
lengthIn: meta.lengthIn,
thicknessIn: meta.thicknessIn,
computedSqft: Math.round(sqft * 100) / 100,
};
}
/**
* TV-10 rounding fixture: real measurements from Ty, not synthetic cartoons.
* Returns "blocked" when metadata is absent or not marked real; does not need a PNG file.
*/
export interface RealRoundingFixture {
lengthRaw: number;
lengthExpected: number;
sqftRaw: number;
sqftExpected: number;
bdftRaw: number;
bdftExpected: number;
priceRaw: number;
priceExpected: number;
}
export function loadTv10RealFixture(): "blocked" | RealRoundingFixture {
const jsonPath = realPath("tv10-slab.json");
if (!existsSync(jsonPath)) return "blocked";
const meta = JSON.parse(readFileSync(jsonPath, "utf-8"));
if (meta.real !== true) return "blocked";
return {
lengthRaw: meta.measurements[0].raw,
lengthExpected: meta.measurements[0].expected,
sqftRaw: meta.measurements[1].raw,
sqftExpected: meta.measurements[1].expected,
bdftRaw: meta.measurements[2].raw,
bdftExpected: meta.measurements[2].expected,
priceRaw: meta.measurements[3].raw,
priceExpected: meta.measurements[3].expected,
};
}
// ---------------------------------------------------------------------------
// Internal helpers
// ---------------------------------------------------------------------------
function countMaskPixels(mask: MaskBuffer, width: number, height: number): number {
let count = 0;
for (let y = 0; y < height; y++) {
for (let x = 0; x < width; x++) {
if (mask[y * width + x]! >= 128) count++;
}
}
return count;
}
@@ -0,0 +1,57 @@
# Real Slab Fixtures
These are **real slab photo samples** used for `product-state` test evidence.
They are not synthetic; they are actual photographs of physical lumber slabs.
## Acquisition path
To populate this directory with real samples, Ty follows this procedure:
1. **Source** — A physical slab with a uniform green or black sheet backing
(the sheet must be uniform enough for `detectSheet` to auto-resolve).
2. **Photo** — Top-down shot, lens parallel to the broad face. Phone camera
is fine; higher resolution is better for TV-6 (≥ 1600 px shorter side after
3:4 crop at 80% fill).
3. **Measure** — Record the following for each sample:
- `lengthIn` — prevailing length in inches (to nearest 1/8")
- `thicknessIn` — thickness pick in inches (to nearest 1/8")
- `sqft` — projected top-down area from the mask (computed by the pipeline)
4. **File naming**:
- `tv3-slab.png` — A slab suitable for TV-3 (any size; known dimensions)
- `tv6-slab.png` — A slab large enough that the 3:4 crop ≥ 1600 px shorter side
- `tv7-slab.png` — A small slab that triggers the undersized warning after crop
5. **Metadata** — Create a matching `*-metadata.json` file alongside each PNG
with the measured values (see the JSON schema below).
6. **License** — Photos are TyTech production samples; no external copyright.
## Metadata schema
Each `*-metadata.json` follows this shape:
```json
{
"source": "ty-lab",
"lengthIn": 96,
"thicknessIn": 1.5,
"sqft": 0.64,
"expectedBdft": 0.96,
"photoNotes": "Walnut slab on green sheet, top-down, 2026-09"
}
```
- `lengthIn` / `thicknessIn` — measured with a tape, stored to 1/8" precision
- `sqft` — computed from the mask of this exact photo (pipeline `checkSourceSize` / `findSlabBounds`)
- `expectedBdft` — `sqft × thicknessIn` rounded to 2 decimals; the value the
`product-state` test asserts against
## What counts as product-state
- A real photograph of a real slab
- Measurements taken from that slab (tape measure, caliper)
- The pipeline running on the actual image file
## What does NOT count
- Procedurally drawn fake images (those are `proxy-synthetic`)
- Numbers invented without a physical measurement source
- "Real-looking" synthetic fixtures masquerading as product evidence
@@ -0,0 +1,11 @@
{
"source": "placeholder-template",
"real": false,
"measurements": [
{ "raw": 96.04, "expected": 96, "field": "lengthIn" },
{ "raw": 0.641, "expected": 0.64, "field": "sqft" },
{ "raw": 11.525, "expected": 11.53, "field": "bdft" },
{ "raw": 12.345, "expected": 12.35, "field": "price" }
],
"photoNotes": "PLACEHOLDER: replace with real Ty measurements and set real: true"
}
@@ -0,0 +1,9 @@
{
"source": "placeholder-template",
"real": false,
"lengthIn": 96,
"thicknessIn": 1.5,
"sqft": 0.64,
"expectedBdft": 0.96,
"photoNotes": "PLACEHOLDER: replace with real Ty measurements and set real: true"
}
@@ -0,0 +1,9 @@
{
"source": "placeholder-template",
"real": false,
"lengthIn": 96,
"thicknessIn": 1.5,
"sqft": 0.64,
"expectedBdft": 0.96,
"photoNotes": "PLACEHOLDER: replace with real Ty measurements and set real: true"
}
@@ -0,0 +1,9 @@
{
"source": "placeholder-template",
"real": false,
"lengthIn": 48,
"thicknessIn": 1.0,
"sqft": 0.15,
"expectedBdft": 0.15,
"photoNotes": "PLACEHOLDER: replace with real Ty measurements and set real: true"
}
+78
View File
@@ -0,0 +1,78 @@
/**
* TV-10 product-state: rounding on real measurements.
*
* Evidence class: `product-state`
*
* Real measurements from Ty, not synthetic cartoons.
* Fails-closed when metadata is absent.
*/
import { describe, expect, it } from "vitest";
import {
roundLengthIn,
roundBdft,
roundPrice,
roundSqft,
} from "../lib/pipeline/units.js";
import { loadTv10RealFixture } from "./fixtures/real-fixtures.js";
describe("TV-10 product-state: rounding on real measurements", () => {
const fixture = loadTv10RealFixture();
if (fixture === "blocked") {
it.skip("real fixture absent — blocked; no product-state evidence", () => {});
return;
}
it("rounds real length to nearest 1/8\"", () => {
const length = roundLengthIn(fixture.lengthRaw);
console.log(
JSON.stringify({
case: "TV-10-product-state",
length: { raw: fixture.lengthRaw, rounded: length, expected: fixture.lengthExpected },
}),
);
expect(length).toBe(fixture.lengthExpected);
});
it("rounds real sqft half-up to 2 decimals", () => {
const sqft = roundSqft(fixture.sqftRaw);
console.log(
JSON.stringify({
case: "TV-10-product-state",
sqft: { raw: fixture.sqftRaw, rounded: sqft, expected: fixture.sqftExpected },
}),
);
expect(sqft).toBe(fixture.sqftExpected);
});
it("rounds real bdft half-up to 2 decimals", () => {
const bdft = roundBdft(fixture.bdftRaw);
console.log(
JSON.stringify({
case: "TV-10-product-state",
bdft: { raw: fixture.bdftRaw, rounded: bdft, expected: fixture.bdftExpected },
}),
);
expect(bdft).toBe(fixture.bdftExpected);
});
it("rounds real price half-up to 2 decimals", () => {
const price = roundPrice(fixture.priceRaw);
console.log(
JSON.stringify({
case: "TV-10-product-state",
price: { raw: fixture.priceRaw, rounded: price, expected: fixture.priceExpected },
}),
);
expect(price).toBe(fixture.priceExpected);
});
});
+65
View File
@@ -0,0 +1,65 @@
/**
* TV-3 product-state: bdft identity on real slab fixtures.
*
* Evidence class: `product-state`
*
* Real slab photo + measurements from a physical slab.
* Fails-closed when real PNG/metadata is absent.
*/
import { describe, expect, it } from "vitest";
import {
computeBdft,
roundBdft,
} from "../lib/pipeline/units.js";
import { loadTv3RealFixture } from "./fixtures/real-fixtures.js";
describe("TV-3 product-state: bdft = sqft × thickness_in on real slab", () => {
const fixture = loadTv3RealFixture();
if (fixture === "blocked") {
it.skip("real fixture absent — blocked; no product-state evidence", () => {});
return;
}
it("bdft = sqft × thickness_in with no divide-by-12 (real slab measurements)", () => {
const { lengthIn, thicknessIn, computedSqft } = fixture;
console.log(
JSON.stringify({
case: "TV-3-product-state",
lengthIn,
thicknessIn,
computedSqft,
bdft: computeBdft(computedSqft, thicknessIn),
identity_1sqft_1in: computeBdft(1, 1),
}),
);
expect(computeBdft(computedSqft, thicknessIn)).toBe(roundBdft(computedSqft * thicknessIn));
expect(computeBdft(computedSqft, thicknessIn)).toBeCloseTo(
computedSqft * thicknessIn,
4,
);
// Identity: 1 sqft at 1 inch = 1 bdft
expect(computeBdft(1, 1)).toBe(1);
// No divide-by-12
expect(computeBdft(computedSqft, thicknessIn)).not.toBeCloseTo(
(computedSqft * thicknessIn) / 12,
5,
);
});
it("bdft is monotonic in thickness for the real slab", () => {
const { computedSqft, thicknessIn } = fixture;
const base = computeBdft(computedSqft, thicknessIn);
expect(computeBdft(computedSqft, thicknessIn + 0.125)).toBeGreaterThan(base);
expect(computeBdft(computedSqft, thicknessIn + 0.01)).toBeGreaterThanOrEqual(base);
});
it("bdft is monotonic in sqft for the real slab", () => {
const { computedSqft, thicknessIn } = fixture;
const base = computeBdft(computedSqft, thicknessIn);
expect(computeBdft(computedSqft + 0.01, thicknessIn)).toBeGreaterThan(base);
});
});
+100
View File
@@ -0,0 +1,100 @@
/**
* TV-6 product-state: 3:4 crop, 80% fill, transparent PNG, ≥1600px on real slab.
*
* Evidence class: `product-state`
*
* Real slab photo cropped through the pipeline.
* Fails-closed when real PNG is absent.
*/
import { describe, expect, it } from "vitest";
import {
ASPECT_H,
ASPECT_W,
FILL_TARGET,
OUTPUT_PX_MIN,
} from "../lib/pipeline/constants.js";
import {
computeCropTarget,
cropTo34,
findSlabBounds,
} from "../lib/pipeline/crop.js";
import { loadTv6RealFixture } from "./fixtures/real-fixtures.js";
describe("TV-6 product-state: 3:4 crop, 80% fill, ≥1600px on real slab", () => {
const fixture = loadTv6RealFixture();
if (fixture === "blocked") {
it.skip("real fixture absent — blocked; no product-state evidence", () => {});
return;
}
it("computes crop target preserving 3:4 aspect and 80% fill on real slab", () => {
const { mask, image, lengthIn } = fixture;
const bounds = findSlabBounds(mask, image.width, image.height);
const target = computeCropTarget(bounds.widthPx, bounds.heightPx);
expect(Math.abs(target.width / target.height - ASPECT_W / ASPECT_H)).toBeLessThan(0.01);
// Fill at limiting dimension should equal FILL_TARGET
const slabAspect = bounds.widthPx / bounds.heightPx;
const targetAspect = ASPECT_W / ASPECT_H;
const fillAtLimitingDim = slabAspect >= targetAspect
? bounds.heightPx / target.height
: bounds.widthPx / target.width;
expect(fillAtLimitingDim).toBeCloseTo(FILL_TARGET, 3);
console.log(
JSON.stringify({
case: "TV-6-product-state",
slabW: bounds.widthPx,
slabH: bounds.heightPx,
targetW: target.width,
targetH: target.height,
fillAtLimitingDim,
aspect: target.width / target.height,
}),
);
});
it("crops real slab to 3:4 with 80% fill, transparent PNG, shorter side ≥ 1600", () => {
const { image, mask, axis, lengthIn } = fixture;
const result = cropTo34(image, mask, axis, lengthIn);
expect(result.width).toBeGreaterThan(0);
expect(result.height).toBeGreaterThan(0);
// 3:4 aspect
expect(Math.abs(result.width / result.height - ASPECT_W / ASPECT_H)).toBeLessThan(0.01);
// Shorter side ≥ OUTPUT_PX_MIN
const shorterSide = Math.min(result.width, result.height);
expect(shorterSide).toBeGreaterThanOrEqual(OUTPUT_PX_MIN);
expect(result.meetsMinPx).toBe(true);
// Transparent margins
const marginAlpha = result.image.data[3];
expect(marginAlpha).toBe(0);
// Slab pixels opaque
const slabCenterX = Math.round(result.width / 2);
const slabCenterY = Math.round(result.height / 2);
const slabAlpha = result.image.data[(slabCenterY * result.width + slabCenterX) * 4 + 3]!;
expect(slabAlpha).toBeGreaterThan(0);
console.log(
JSON.stringify({
case: "TV-6-product-state",
frameW: result.width,
frameH: result.height,
shorterSide,
meetsMinPx: result.meetsMinPx,
marginAlpha,
slabAlpha,
}),
);
});
});
+78
View File
@@ -0,0 +1,78 @@
/**
* TV-7 product-state: undersized source warns, no upscale, not publishable.
*
* Evidence class: `product-state`
*
* Real small slab photo that triggers the undersized warning.
* Fails-closed when real PNG is absent.
*/
import { describe, expect, it } from "vitest";
import { OUTPUT_PX_MIN } from "../lib/pipeline/constants.js";
import {
computeCropTarget,
cropTo34,
findSlabBounds,
checkSourceSize,
} from "../lib/pipeline/crop.js";
import { loadTv7RealFixture } from "./fixtures/real-fixtures.js";
describe("TV-7 product-state: undersized-source warn/retake on real slab", () => {
const fixture = loadTv7RealFixture();
if (fixture === "blocked") {
it.skip("real fixture absent — blocked; no product-state evidence", () => {});
return;
}
it("detects undersized source on real slab and refuses upscale", () => {
const { mask, image } = fixture;
const bounds = findSlabBounds(mask, image.width, image.height);
const target = computeCropTarget(bounds.widthPx, bounds.heightPx);
expect(Math.min(target.width, target.height)).toBeLessThan(OUTPUT_PX_MIN);
console.log(
JSON.stringify({
case: "TV-7-product-state",
slabW: bounds.widthPx,
slabH: bounds.heightPx,
targetW: target.width,
targetH: target.height,
shorterSide: Math.min(target.width, target.height),
outputPxMin: OUTPUT_PX_MIN,
undersized: true,
}),
);
});
it("cropTo34 returns meetsMinPx=false and no upscale for undersized real slab", () => {
const { image, mask, axis, lengthIn } = fixture;
const result = cropTo34(image, mask, axis, lengthIn);
expect(result.meetsMinPx).toBe(false);
expect(Math.min(result.width, result.height)).toBeLessThan(OUTPUT_PX_MIN);
// No upscale: output ≤ source
expect(result.width).toBeLessThanOrEqual(image.width);
expect(result.height).toBeLessThanOrEqual(image.height);
console.log(
JSON.stringify({
case: "TV-7-product-state",
sourceW: image.width,
sourceH: image.height,
outputW: result.width,
outputH: result.height,
meetsMinPx: result.meetsMinPx,
}),
);
});
it("checkSourceSize returns 'undersized' for the real small slab", () => {
const { mask, image } = fixture;
const status = checkSourceSize(mask, image.width, image.height);
expect(status).toBe("undersized");
});
});
@@ -1,6 +1,10 @@
# Gate B pass2 results — 2026-09-07
Evidence collected on gate-b @ 302d694.
Evidence collected on gate-b @ 699b6a0.
Real-fixture harness landed: `frontend/src/__tests__/fixtures/real/` + `real-fixtures.ts` + four product-state test files.
Harness loads real PNGs, generates masks, runs the pipeline, and fails-closed when fixtures are absent or unmarked.
Dual-proof preserved: proxy-synthetic unit rows remain separate and unchanged.
---
@@ -10,17 +14,17 @@ Evidence collected on gate-b @ 302d694.
|---|---|---|---|---|---|
| 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 = sqft × thickness_in; 1 sqft @ 1" = 1 bdft; no divide-by-12 | `product-state` | blocked | vitest.txt: skipped (real fixture absent) | Real PNG + metadata needed; harness at `fixtures/real/` |
| 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 | 3:4 crop, slab centered, 80% fill, transparent PNG, shorter side ≥ 1600 when source allows | `product-state` | blocked | vitest.txt: skipped (real fixture absent) | Real PNG + metadata needed; harness at `fixtures/real/` |
| 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 warns, refuses upscale, photo not publishable until retake | `product-state` | blocked | vitest.txt: skipped (real fixture absent) | Real PNG + metadata needed; harness at `fixtures/real/` |
| 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: 96.04" → 96"; 0.641 sqft → 0.64; 11.525 bdft → 11.53; 12.345 price → 12.35 | `product-state` | blocked | vitest.txt: skipped (metadata placeholder, real: false) | Real measurements from Ty needed; harness at `fixtures/real/` |
| 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 | — |
@@ -39,11 +43,17 @@ Evidence collected on gate-b @ 302d694.
**18 pass / 1 fail / 5 blocked** (24 total rows)
Real-fixture harness (4 product-state test files + loader + acquisition README) is committed;
still blocked pending Ty-supplied PNG photos and real measurements.
---
## 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.
- **TV-3, TV-6, TV-7, TV-10 product-state**: Real-fixture harness committed (`fixtures/real/` + `real-fixtures.ts`
+ four product-state test files). Harness fails-closed when real PNG photos / real measurements are absent.
Still blocked pending Ty-supplied photos and measurements. 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.