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:
@@ -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,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";
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
|
|
||||||
@@ -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)
|
||||||
|
|
||||||
Reference in New Issue
Block a user