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 "./types.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);
|
||||
}
|
||||
Reference in New Issue
Block a user