feat(#22): Gate B BDFT identity and rounding (TV-3, TV-10)

Pure units module: bdft = sqft × thickness_in (no /12), half-up
rounding for sqft/bdft/price, length to nearest 1/8". Property
checks for monotonicity in thickness and sqft.

TDD: red evidence in test-results/tv3-tv10-red.txt, then minimum impl.

Results:
TV-3 | pass | test-results/tv3-tv10-green.txt
TV-10 | pass | test-results/tv3-tv10-green.txt
This commit is contained in:
2026-09-07 06:02:08 +00:00
parent 3c2f8871a8
commit 55d66b9644
6 changed files with 181 additions and 0 deletions
+37
View File
@@ -0,0 +1,37 @@
/** TECH-SPEC §8 TV-3 numeric fixture (client-authoritative sqft input). */
export function tv3Fixture(): {
lengthIn: number;
thicknessIn: number;
sqft: number;
expectedBdft: number;
} {
return {
lengthIn: 96,
thicknessIn: 1.5,
sqft: 0.64,
expectedBdft: 0.96,
};
}
/** TECH-SPEC §8 TV-10 rounding vectors. */
export function tv10Fixture(): {
lengthRaw: number;
lengthExpected: number;
sqftRaw: number;
sqftExpected: number;
bdftRaw: number;
bdftExpected: number;
priceRaw: number;
priceExpected: number;
} {
return {
lengthRaw: 96.04,
lengthExpected: 96,
sqftRaw: 0.641,
sqftExpected: 0.64,
bdftRaw: 11.525,
bdftExpected: 11.53,
priceRaw: 12.345,
priceExpected: 12.35,
};
}
@@ -0,0 +1,70 @@
import { describe, expect, it } from "vitest";
import {
computeBdft,
roundBdft,
roundLengthIn,
roundPrice,
roundSqft,
} from "../lib/pipeline/units.js";
import { tv3Fixture, tv10Fixture } from "./fixtures/units.js";
describe("TV-3 Scale, sqft, bdft", () => {
it("bdft = sqft × thickness_in with no divide-by-12", () => {
const { lengthIn, thicknessIn, sqft, expectedBdft } = tv3Fixture();
const bdft = computeBdft(sqft, thicknessIn);
console.log(
JSON.stringify({
case: "TV-3",
lengthIn,
thicknessIn,
sqft,
bdft,
expectedBdft,
identity_1sqft_1in: computeBdft(1, 1),
}),
);
expect(bdft).toBe(expectedBdft);
expect(bdft).toBe(0.96);
expect(computeBdft(1, 1)).toBe(1);
expect(bdft).not.toBeCloseTo((sqft * thicknessIn) / 12, 5);
});
it("bdft is monotonic in thickness and sqft", () => {
const baseSqft = 0.64;
const baseThick = 1.5;
expect(computeBdft(baseSqft, baseThick + 0.125)).toBeGreaterThan(
computeBdft(baseSqft, baseThick),
);
expect(computeBdft(baseSqft + 0.01, baseThick)).toBeGreaterThan(
computeBdft(baseSqft, baseThick),
);
});
});
describe("TV-10 Rounding", () => {
it("half-up / nearest-step matches TECH-SPEC vectors", () => {
const fx = tv10Fixture();
const length = roundLengthIn(fx.lengthRaw);
const sqft = roundSqft(fx.sqftRaw);
const bdft = roundBdft(fx.bdftRaw);
const price = roundPrice(fx.priceRaw);
console.log(
JSON.stringify({
case: "TV-10",
length: { raw: fx.lengthRaw, rounded: length, expected: fx.lengthExpected },
sqft: { raw: fx.sqftRaw, rounded: sqft, expected: fx.sqftExpected },
bdft: { raw: fx.bdftRaw, rounded: bdft, expected: fx.bdftExpected },
price: { raw: fx.priceRaw, rounded: price, expected: fx.priceExpected },
}),
);
expect(length).toBe(fx.lengthExpected);
expect(sqft).toBe(fx.sqftExpected);
expect(bdft).toBe(fx.bdftExpected);
expect(price).toBe(fx.priceExpected);
});
});
+1
View File
@@ -1,3 +1,4 @@
export * from "./constants.js"; export * from "./constants.js";
export * from "./types.js"; export * from "./types.js";
export * from "./mask.js"; export * from "./mask.js";
export * from "./units.js";
+39
View File
@@ -0,0 +1,39 @@
import {
AREA_DECIMALS,
BDFT_DECIMALS,
INCH_STEP,
PRICE_DECIMALS,
} from "./constants.js";
/** Half-up at .5 (away from zero). */
export function roundHalfUp(value: number, decimals: number): number {
if (!Number.isFinite(value)) {
return value;
}
const factor = 10 ** decimals;
const scaled = Math.abs(value) * factor;
const rounded = Math.floor(scaled + 0.5) / factor;
return value < 0 ? -rounded : rounded;
}
/** Nearest INCH_STEP (1/8"). */
export function roundLengthIn(lengthIn: number): number {
return Math.round(lengthIn / INCH_STEP) * INCH_STEP;
}
export function roundSqft(sqft: number): number {
return roundHalfUp(sqft, AREA_DECIMALS);
}
export function roundBdft(bdft: number): number {
return roundHalfUp(bdft, BDFT_DECIMALS);
}
export function roundPrice(price: number): number {
return roundHalfUp(price, PRICE_DECIMALS);
}
/** bdft = sqft × thickness_in. No divide-by-12. */
export function computeBdft(sqft: number, thicknessIn: number): number {
return roundBdft(sqft * thicknessIn);
}
+16
View File
@@ -0,0 +1,16 @@
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__/tv3-tv10-units.test.ts (3 tests) 7ms
Test Files 1 passed (1)
Tests 3 passed (3)
Start at 06:02:02
Duration 377ms (transform 61ms, setup 0ms, collect 63ms, tests 7ms, environment 0ms, prepare 67ms)
+18
View File
@@ -0,0 +1,18 @@
RUN v2.1.9 /workspace/gitea-atd/Ty_Tech/SlabUploader/frontend
❯ src/__tests__/tv3-tv10-units.test.ts (0 test)
⎯⎯⎯⎯⎯⎯ Failed Suites 1 ⎯⎯⎯⎯⎯⎯⎯
FAIL src/__tests__/tv3-tv10-units.test.ts [ src/__tests__/tv3-tv10-units.test.ts ]
Error: Failed to load url ../lib/pipeline/units.js (resolved id: ../lib/pipeline/units.js) in /workspace/gitea-atd/Ty_Tech/SlabUploader/frontend/src/__tests__/tv3-tv10-units.test.ts. Does the file exist?
❯ loadAndTransform node_modules/vite/dist/node/chunks/dep-BK3b2jBa.js:51969:17
⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[1/1]⎯
Test Files 1 failed (1)
Tests no tests
Start at 06:01:33
Duration 344ms (transform 42ms, setup 0ms, collect 0ms, tests 0ms, environment 0ms, prepare 69ms)