diff --git a/frontend/package-lock.json b/frontend/package-lock.json index ad1a01a..527534a 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -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", diff --git a/frontend/package.json b/frontend/package.json index e204ddc..3f54905 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -8,6 +8,7 @@ "test:watch": "vitest" }, "devDependencies": { + "pngjs": "^7.0.0", "typescript": "^5.6.3", "vitest": "^2.1.4" } diff --git a/frontend/src/__tests__/fixtures/real-fixtures.ts b/frontend/src/__tests__/fixtures/real-fixtures.ts new file mode 100644 index 0000000..eb894c2 --- /dev/null +++ b/frontend/src/__tests__/fixtures/real-fixtures.ts @@ -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; +} diff --git a/frontend/src/__tests__/fixtures/real/README.md b/frontend/src/__tests__/fixtures/real/README.md new file mode 100644 index 0000000..5348e49 --- /dev/null +++ b/frontend/src/__tests__/fixtures/real/README.md @@ -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 diff --git a/frontend/src/__tests__/fixtures/real/tv10-slab.json b/frontend/src/__tests__/fixtures/real/tv10-slab.json new file mode 100644 index 0000000..0307c07 --- /dev/null +++ b/frontend/src/__tests__/fixtures/real/tv10-slab.json @@ -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" +} diff --git a/frontend/src/__tests__/fixtures/real/tv3-slab.json b/frontend/src/__tests__/fixtures/real/tv3-slab.json new file mode 100644 index 0000000..a59966f --- /dev/null +++ b/frontend/src/__tests__/fixtures/real/tv3-slab.json @@ -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" +} diff --git a/frontend/src/__tests__/fixtures/real/tv6-slab.json b/frontend/src/__tests__/fixtures/real/tv6-slab.json new file mode 100644 index 0000000..a59966f --- /dev/null +++ b/frontend/src/__tests__/fixtures/real/tv6-slab.json @@ -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" +} diff --git a/frontend/src/__tests__/fixtures/real/tv7-slab.json b/frontend/src/__tests__/fixtures/real/tv7-slab.json new file mode 100644 index 0000000..8e312a2 --- /dev/null +++ b/frontend/src/__tests__/fixtures/real/tv7-slab.json @@ -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" +} diff --git a/frontend/src/__tests__/tv10-real.test.ts b/frontend/src/__tests__/tv10-real.test.ts new file mode 100644 index 0000000..32a4f2e --- /dev/null +++ b/frontend/src/__tests__/tv10-real.test.ts @@ -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); + }); +}); diff --git a/frontend/src/__tests__/tv3-real.test.ts b/frontend/src/__tests__/tv3-real.test.ts new file mode 100644 index 0000000..3dca564 --- /dev/null +++ b/frontend/src/__tests__/tv3-real.test.ts @@ -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); + }); +}); diff --git a/frontend/src/__tests__/tv6-real.test.ts b/frontend/src/__tests__/tv6-real.test.ts new file mode 100644 index 0000000..9c28344 --- /dev/null +++ b/frontend/src/__tests__/tv6-real.test.ts @@ -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, + }), + ); + }); +}); diff --git a/frontend/src/__tests__/tv7-real.test.ts b/frontend/src/__tests__/tv7-real.test.ts new file mode 100644 index 0000000..ccb7f22 --- /dev/null +++ b/frontend/src/__tests__/tv7-real.test.ts @@ -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"); + }); +}); diff --git a/test-results/gate-b-2026-09-07-pass2/RESULTS.md b/test-results/gate-b-2026-09-07-pass2/RESULTS.md index fac4f55..7b36452 100644 --- a/test-results/gate-b-2026-09-07-pass2/RESULTS.md +++ b/test-results/gate-b-2026-09-07-pass2/RESULTS.md @@ -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.