test: prove coincident partition acceptance

Issue: #276
User-Visible: no
This commit is contained in:
Sergey Matyunin
2026-08-24 05:57:57 +03:00
parent 57fdfdbd83
commit 82fe98bcd3
13 changed files with 290 additions and 24 deletions
File diff suppressed because one or more lines are too long
+84
View File
@@ -0,0 +1,84 @@
// #276: same-process incremental cost of coincident-partition reconciliation
// inside the complete Optimize candidate pass on the deterministic large house.
import { performance } from 'node:perf_hooks';
import { makeLargeHouseFixture, LARGE_HOUSE_COUNTS } from './fixtures/large-house.mjs';
import { optimizePlans } from '../test-build/plan-optimizer.js';
const WARMUPS = 5;
const SAMPLES = 40;
const RELATIVE_OVERHEAD = 0.15;
const ABSOLUTE_OVERHEAD_MS = 25;
const fixture = makeLargeHouseFixture();
const emptyReconciliation = (rawSpace, _model, walls) => ({
walls: walls || [],
partitions: Array.isArray(rawSpace?.partitions) ? rawSpace.partitions : [],
openings: Array.isArray(rawSpace?.openings) ? rawSpace.openings : [],
partitionsReconciled: 0,
openingsRehosted: 0,
});
const baseline = () => optimizePlans(fixture.config, {}, {}, {
reconcileCoincidentPartitions: emptyReconciliation,
});
const candidate = () => optimizePlans(fixture.config, {});
const timed = (operation) => {
const start = performance.now();
const result = operation();
if (!result || !result.report) throw new Error('Optimize candidate returned no report');
return performance.now() - start;
};
for (let index = 0; index < WARMUPS; index++) {
baseline();
candidate();
}
const baselineTimes = [];
const candidateTimes = [];
const overheadTimes = [];
for (let index = 0; index < SAMPLES; index++) {
const candidateFirst = index % 2 === 1;
const first = timed(candidateFirst ? candidate : baseline);
const second = timed(candidateFirst ? baseline : candidate);
const baselineMs = candidateFirst ? second : first;
const candidateMs = candidateFirst ? first : second;
baselineTimes.push(baselineMs);
candidateTimes.push(candidateMs);
overheadTimes.push(candidateMs - baselineMs);
}
const quantile = (values, ratio) => {
const sorted = [...values].sort((a, b) => a - b);
return sorted[Math.min(sorted.length - 1, Math.ceil(sorted.length * ratio) - 1)];
};
const summary = (values) => ({
min: Math.min(...values),
median: quantile(values, 0.5),
p95: quantile(values, 0.95),
max: Math.max(...values),
});
const baselineSummary = summary(baselineTimes);
const candidateSummary = summary(candidateTimes);
const overheadSummary = summary(overheadTimes);
const measuredOverheadP95 = Math.max(0, candidateSummary.p95 - baselineSummary.p95);
const relativeP95 = measuredOverheadP95
/ Math.max(baselineSummary.p95, Number.EPSILON);
const pass = measuredOverheadP95 <= ABSOLUTE_OVERHEAD_MS
&& relativeP95 <= RELATIVE_OVERHEAD;
const report = {
issue: 276,
fixture: LARGE_HOUSE_COUNTS,
warmups: WARMUPS,
samples: SAMPLES,
baseline: baselineSummary,
candidate: candidateSummary,
pairedOverhead: overheadSummary,
budgets: {
relativeOverhead: RELATIVE_OVERHEAD,
absoluteOverheadMs: ABSOLUTE_OVERHEAD_MS,
},
measured: { overheadP95Ms: measuredOverheadP95, relativeP95 },
pass,
};
console.log(JSON.stringify(report, null, 2));
if (!pass) process.exitCode = 1;
+30
View File
@@ -14,6 +14,9 @@ const orthogonalStripFixture = JSON.parse(readFileSync(
const wallKeyRoundtripFixture = JSON.parse(readFileSync(
new URL('../../test/fixtures/258-wall-key-roundtrip.json', import.meta.url), 'utf8',
));
const coincidentPartitionFixture = JSON.parse(readFileSync(
new URL('../../test/fixtures/276-coincident-partition.json', import.meta.url), 'utf8',
));
const fixtureFor = (scenario) => scenario.fixture === 'large'
? makeLargeHouseFixture()
@@ -67,6 +70,33 @@ async function stableEnvironment(page, scenario) {
/** Apply every data-only scenario override before the fixture crosses into the browser. */
export function prepareGoldenFixture(scenario) {
const fixture = fixtureFor(scenario);
if (scenario.coincidentPartition) {
const state = scenario.coincidentPartition;
if (!['before', 'thin', 'thick', 'virtual'].includes(state))
throw new Error(`unknown coincidentPartition state: ${state}`);
const space = structuredClone(coincidentPartitionFixture.spaces[0]);
space.id = scenario.space;
space.title = `Coincident partition ${state}`;
space.settings = {
fill_mode: 'none', show_borders: true, show_names: true,
};
if (state !== 'before') {
delete space.partitions;
space.walls[0].cm = state === 'thin' ? 10 : state === 'thick' ? 30 : 20;
const opening = space.openings[0];
delete opening.host;
opening.x = 0.504166667;
opening.y = 0.5;
opening.angle = -90;
if (state === 'virtual') {
space.open_spans = [{
a: [0.5041666666666667, 0.004166666666666667],
b: [0.5041666666666667, 0.9958333333333333],
}];
}
}
fixture.config.spaces.push(space);
}
if (scenario.cornerSplitWall) {
const stage = scenario.cornerSplitWall;
if (!['before', 'thin', 'thick', 'zero-taper'].includes(stage))
+13 -1
View File
@@ -1,7 +1,7 @@
import { fixtureWallKey } from '../fixtures/visual-matrix.mjs';
/** Data-only HP-QA-01 capture matrix. Bump when framing or scenarios change. */
export const GOLDEN_MATRIX_VERSION = 41;
export const GOLDEN_MATRIX_VERSION = 42;
const stage = { capture: 'stage', threshold: { maxChannelDelta: 10, maxDiffRatio: 0.0005 } };
const page = { capture: 'page', threshold: { maxChannelDelta: 10, maxDiffRatio: 0.0008 } };
@@ -165,6 +165,18 @@ const decorLayerProbes = {
};
export const GOLDEN_SCENARIOS = Object.freeze([
{ id: 'coincident-partition-before-dark', fixture: 'visual',
space: 'golden-coincident-partition', coincidentPartition: 'before',
mode: 'view', theme: 'dark', viewport: { width: 1000, height: 900 }, ...stage },
{ id: 'coincident-partition-thin-dark', fixture: 'visual',
space: 'golden-coincident-partition', coincidentPartition: 'thin',
mode: 'view', theme: 'dark', viewport: { width: 1000, height: 900 }, ...stage },
{ id: 'coincident-partition-thick-dark', fixture: 'visual',
space: 'golden-coincident-partition', coincidentPartition: 'thick',
mode: 'view', theme: 'dark', viewport: { width: 1000, height: 900 }, ...stage },
{ id: 'coincident-partition-virtual-dark', fixture: 'visual',
space: 'golden-coincident-partition', coincidentPartition: 'virtual',
mode: 'view', theme: 'dark', viewport: { width: 1000, height: 900 }, ...stage },
{ id: 'split-corner-wall-before-dark', fixture: 'visual', space: 'golden-corner-split',
cornerSplitWall: 'before', mode: 'view', theme: 'dark', viewport: { width: 1000, height: 900 }, ...stage },
{ id: 'split-corner-wall-thin-dark', fixture: 'visual', space: 'golden-corner-split',
+2 -2
View File
File diff suppressed because one or more lines are too long
+5
View File
@@ -545,6 +545,11 @@ partial/composite matches, overlapping openings and adjacent independent
bodies fail closed. The candidate then crosses the existing whole-plan
geometry preflight and one atomic Optimize write/Undo boundary. No render or
ordinary save path invokes this pass, so `PLAN_MODEL_VERSION` remains unchanged.
`OptimizeDependencies` is a narrow test/benchmark seam: production uses the
real helper, while the committed large-house benchmark substitutes a no-op to
measure only this pass and the unit contract instruments its exact per-space
call count. A source-ownership assertion fails if a render/pointer module ever
imports the helper.
`boundary` is one contextual UI tool over the existing `open_spans` model.
Before the first click, independent physical bodies block the room boundary
+12 -4
View File
@@ -2815,15 +2815,23 @@ require hands on real hardware — they remain for the human pass.
exact independent wall is removed, its hosted door becomes an ordinary
opening at the same centre/angle with all bindings and unknown fields,
and the wider centred thickness survives. Direction and room order do
not matter; partial/extra/unknown/draft/column/opening-conflict cases are
byte-preserved. Preview writes nothing, Apply uses one WS transaction,
not matter; three non-overlapping hosted door/window/gate records are
rehosted atomically, while a hosted-hosted overlap and all other
partial/extra/unknown/draft/column/opening conflicts fail closed. Preview
writes nothing, Apply uses one WS transaction,
reload is idempotent, Undo restores the hosted form, and Boundary plus
Thickness target the resulting shared wall [unit:
Thickness target the resulting shared wall. Four targeted golden scenes
retain the 5 cm offset and show before, 10 cm, 30 cm and virtual results;
the paired large-house benchmark enforces p95 overhead ≤15% and ≤25 ms,
while source ownership plus an injected counter keep the helper out of
render/pointer paths [unit:
test/coincident-partitions.test.mjs + test/plan-optimizer.test.mjs;
auto: smoke_optimize_coincident_partition; mutations:
`optimizer-coincident-opening-rehost-disabled`,
`optimizer-coincident-partial-accepted`, existing
`optimize-preflight-bypassed`].
`optimize-preflight-bypassed`; performance:
`npm run benchmark:coincident-partitions`; golden:
`coincident-partition-{before,thin,thick,virtual}-dark`].
- [ ] **Unit + backend**: inset/mitre/bevel, key from either end, degrade,
rekey, cm↔inches; `walls` schema bounds
[auto: test/wall-thickness.test.mjs + tests_backend/test_validation.py]
Binary file not shown.

Before

Width:  |  Height:  |  Size: 291 KiB

After

Width:  |  Height:  |  Size: 291 KiB

+12 -12
View File
@@ -2,7 +2,7 @@
"version": 1,
"fixture": "synthetic-only",
"chromium": "151.0.7922.34",
"sourceFingerprint": "ceb125adfd9b21b62821936055562a2864e7d57007694c16169f7bc7ffa383b4",
"sourceFingerprint": "06493179d9db0327626adb8a69fc7cdaf46e8e24f17dae066d93080e4ec0cedc",
"captureScriptSha256": "ce2e9542fed9dade3085be87d16f69adb2ac8262893ad78ad966b1b9673f2983",
"command": "npm run build && node demo/docs/capture.mjs",
"scenarios": {
@@ -14,7 +14,7 @@
},
"theme": "dark",
"language": "en",
"sourceSha256": "ceb125adfd9b21b62821936055562a2864e7d57007694c16169f7bc7ffa383b4",
"sourceSha256": "06493179d9db0327626adb8a69fc7cdaf46e8e24f17dae066d93080e4ec0cedc",
"imageSha256": "5ba555c1f46416ce17e40ef2b6ecd9eb6e1d0ce6b8d467acd0682af95bd36307"
},
"view-touch": {
@@ -25,7 +25,7 @@
},
"theme": "dark",
"language": "en",
"sourceSha256": "ceb125adfd9b21b62821936055562a2864e7d57007694c16169f7bc7ffa383b4",
"sourceSha256": "06493179d9db0327626adb8a69fc7cdaf46e8e24f17dae066d93080e4ec0cedc",
"imageSha256": "deaae14893e66337b42e45ccfe87cf16f571cfc10f98da4250a6a6589a092a43"
},
"space-create": {
@@ -36,7 +36,7 @@
},
"theme": "dark",
"language": "en",
"sourceSha256": "ceb125adfd9b21b62821936055562a2864e7d57007694c16169f7bc7ffa383b4",
"sourceSha256": "06493179d9db0327626adb8a69fc7cdaf46e8e24f17dae066d93080e4ec0cedc",
"imageSha256": "c33a7279165a4cec6fa6fadb6fd08cd967e082a17fe101ef442d27d36ae59b6b"
},
"room-contour-close": {
@@ -47,7 +47,7 @@
},
"theme": "dark",
"language": "en",
"sourceSha256": "ceb125adfd9b21b62821936055562a2864e7d57007694c16169f7bc7ffa383b4",
"sourceSha256": "06493179d9db0327626adb8a69fc7cdaf46e8e24f17dae066d93080e4ec0cedc",
"imageSha256": "37f7e4f1d7601bc5c0fdfd38c6482a6af85c845c883283b9aba16d6457383355"
},
"plan-context-tray": {
@@ -58,7 +58,7 @@
},
"theme": "dark",
"language": "en",
"sourceSha256": "ceb125adfd9b21b62821936055562a2864e7d57007694c16169f7bc7ffa383b4",
"sourceSha256": "06493179d9db0327626adb8a69fc7cdaf46e8e24f17dae066d93080e4ec0cedc",
"imageSha256": "b0398943d8a7c8215159d2d54b895c28feb1be8e303e65ca106ea5185c34644e"
},
"device-editor": {
@@ -69,8 +69,8 @@
},
"theme": "dark",
"language": "en",
"sourceSha256": "ceb125adfd9b21b62821936055562a2864e7d57007694c16169f7bc7ffa383b4",
"imageSha256": "c3704489b36fd4177e761124fac032d8d09d6a94e3110f9a77d40ec110c1590b"
"sourceSha256": "06493179d9db0327626adb8a69fc7cdaf46e8e24f17dae066d93080e4ec0cedc",
"imageSha256": "f24da9380b83b4e0087f81dadbf1ca45df0395558baa0441ebb8c4b14584c899"
},
"device-display-preview": {
"file": "06-device-display-preview.png",
@@ -80,7 +80,7 @@
},
"theme": "dark",
"language": "en",
"sourceSha256": "ceb125adfd9b21b62821936055562a2864e7d57007694c16169f7bc7ffa383b4",
"sourceSha256": "06493179d9db0327626adb8a69fc7cdaf46e8e24f17dae066d93080e4ec0cedc",
"imageSha256": "7241478b3fae2a5b1895f241bc50a65f1aaacce907d311cc87796a251c389a8b"
},
"background-editor": {
@@ -91,7 +91,7 @@
},
"theme": "dark",
"language": "en",
"sourceSha256": "ceb125adfd9b21b62821936055562a2864e7d57007694c16169f7bc7ffa383b4",
"sourceSha256": "06493179d9db0327626adb8a69fc7cdaf46e8e24f17dae066d93080e4ec0cedc",
"imageSha256": "8241b09b0d821247a86eaadd6043a37dde29623c870d8a142bcd381c638a306d"
},
"room-card": {
@@ -102,7 +102,7 @@
},
"theme": "dark",
"language": "en",
"sourceSha256": "ceb125adfd9b21b62821936055562a2864e7d57007694c16169f7bc7ffa383b4",
"sourceSha256": "06493179d9db0327626adb8a69fc7cdaf46e8e24f17dae066d93080e4ec0cedc",
"imageSha256": "029a3e69ec647a8a370d99e6bb7f9225833c526739076022f6b52ba54bff30ea"
},
"device-info": {
@@ -113,7 +113,7 @@
},
"theme": "dark",
"language": "en",
"sourceSha256": "ceb125adfd9b21b62821936055562a2864e7d57007694c16169f7bc7ffa383b4",
"sourceSha256": "06493179d9db0327626adb8a69fc7cdaf46e8e24f17dae066d93080e4ec0cedc",
"imageSha256": "8e65ff2191ad1f1172848418c8381967ecff2cc831653a8f9c3ae11352a0e84b"
}
}
+1
View File
@@ -21,6 +21,7 @@
"benchmark:glow": "node demo/benchmark_glow.mjs",
"benchmark:compare": "node demo/performance/compare.mjs",
"benchmark:optimize-geometry-preflight": "node demo/benchmark_optimize_geometry_preflight.mjs",
"benchmark:coincident-partitions": "node demo/benchmark_coincident_partitions.mjs",
"golden:capture": "node demo/golden/run.mjs --mode=capture",
"golden:verify": "node demo/golden/run.mjs --mode=verify",
"continuity:screencast": "node demo/screencast_visual_continuity.mjs",
+9 -1
View File
@@ -73,6 +73,11 @@ export interface OptimizeResult {
changed: boolean;
}
/** Test/benchmark seam for proving that structural maintenance stays inside Optimize. */
export interface OptimizeDependencies {
reconcileCoincidentPartitions?: typeof reconcileCoincidentPartitions;
}
const clone = <T>(value: T): T => JSON.parse(JSON.stringify(value));
const own = (o: any, key: string): boolean => Object.prototype.hasOwnProperty.call(o, key);
const clamp = (value: number, min: number, max: number): number => (
@@ -393,7 +398,10 @@ export function optimizePlans(
configIn: any,
layoutIn: Record<string, any>,
context: SpaceReferenceRepairContext = {},
dependencies: OptimizeDependencies = {},
): OptimizeResult {
const reconcilePartitions = dependencies.reconcileCoincidentPartitions
?? reconcileCoincidentPartitions;
const references = repairSpaceReferences(configIn, layoutIn, context);
const config = references.config;
const original = JSON.stringify(configIn || {});
@@ -545,7 +553,7 @@ export function optimizePlans(
// preflight checks the candidate (#276).
const reconciledModel = modelOf(space);
if (reconciledModel) {
const reconciled = reconcileCoincidentPartitions(
const reconciled = reconcilePartitions(
space, reconciledModel, space.walls || [], cuts,
{
pitch: GRID_STEP_N,
+89 -1
View File
@@ -1,9 +1,12 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import { readFileSync } from 'node:fs';
import { readFileSync, readdirSync } from 'node:fs';
import { join, relative } from 'node:path';
import { fileURLToPath } from 'node:url';
import { optimizePlans } from '../test-build/plan-optimizer.js';
import { checkOptimizeGeometry } from '../test-build/plan-geometry-preflight.js';
import { reconcileCoincidentPartitions } from '../test-build/coincident-partitions.js';
import { GRID_PITCH, GRID_STEP_N as S } from '../test-build/space-geometry.js';
import { wallIntervals } from '../test-build/wall-thickness.js';
@@ -68,6 +71,91 @@ test('issue 276 exact proof ignores endpoint direction and room order', () => {
}
});
test('issue 276 rehosts three non-overlapping door/window/gate openings atomically', () => {
const input = clone(fixture);
const space = input.spaces[0];
space.openings = [
{ id: 'hosted-door', type: 'door', x: 0, y: 0, angle: 0, length: 0.12,
contact: 'binary_sensor.door', host: { kind: 'partition', id: 'redundant', t: 59 / 238 } },
{ id: 'hosted-window', type: 'window', x: 0, y: 0, angle: 0, length: 0.12,
cover: 'cover.window', host: { kind: 'partition', id: 'redundant', t: 0.50 } },
{ id: 'hosted-gate', type: 'gate', x: 0, y: 0, angle: 0, length: 0.12,
future_field: { keep: true }, host: { kind: 'partition', id: 'redundant', t: 179 / 238 } },
];
const result = optimize(input);
assert.equal(result.report.partitionsReconciled, 1);
assert.equal(result.report.openingsRehosted, 3);
const openings = result.config.spaces[0].openings;
assert.deepEqual(openings.map((opening) => opening.id),
['hosted-door', 'hosted-window', 'hosted-gate']);
assert.deepEqual(openings.map((opening) => opening.type), ['door', 'window', 'gate']);
assert.ok(openings.every((opening) => opening.host === undefined));
assert.ok(openings.every((opening) => opening.x === 0.504166667));
assert.ok(openings[0].y < openings[1].y && openings[1].y < openings[2].y);
assert.equal(openings[0].contact, 'binary_sensor.door');
assert.equal(openings[1].cover, 'cover.window');
assert.deepEqual(openings[2].future_field, { keep: true });
assert.equal(checkOptimizeGeometry(result.config).ok, true);
const second = optimize(result.config);
assert.equal(second.changed, false);
assert.equal(second.report.openingsRehosted, 0);
assert.deepEqual(second.config, result.config);
});
test('issue 276 fails closed when two hosted openings would overlap after rehost', () => {
const input = clone(fixture);
const space = input.spaces[0];
space.openings = [
{ id: 'overlap-door', type: 'door', x: 0, y: 0, angle: 0, length: 0.2,
host: { kind: 'partition', id: 'redundant', t: 0.45 } },
{ id: 'overlap-window', type: 'window', x: 0, y: 0, angle: 0, length: 0.2,
host: { kind: 'partition', id: 'redundant', t: 0.55 } },
];
const result = optimize(input);
assert.equal(result.report.partitionsReconciled, 0);
assert.equal(result.report.openingsRehosted, 0);
assert.equal(result.config.spaces[0].partitions.length, 1);
assert.deepEqual(result.config.spaces[0].openings.map((opening) => opening.id),
['overlap-door', 'overlap-window']);
assert.ok(result.config.spaces[0].openings.every((opening) => opening.host?.id === 'redundant'));
});
test('issue 276 reconciliation is owned by explicit Optimize and called once per valid space', () => {
const input = clone(fixture);
const secondSpace = clone(input.spaces[0]);
secondSpace.id = 'offset-shared-wall-second';
input.spaces.push(secondSpace);
let calls = 0;
const instrumented = (...args) => {
calls++;
return reconcileCoincidentPartitions(...args);
};
const result = optimizePlans(input, {}, {}, {
reconcileCoincidentPartitions: instrumented,
});
assert.equal(calls, 2, 'one explicit pass per valid space in one Optimize candidate');
assert.equal(result.report.partitionsReconciled, 2);
const sourceRoot = fileURLToPath(new URL('../src/', import.meta.url));
const sourceFiles = [];
const walk = (directory) => {
for (const entry of readdirSync(directory, { withFileTypes: true })) {
const path = join(directory, entry.name);
if (entry.isDirectory()) walk(path);
else if (entry.isFile() && entry.name.endsWith('.ts')) sourceFiles.push(path);
}
};
walk(sourceRoot);
const owners = sourceFiles
.filter((path) => readFileSync(path, 'utf8').includes('reconcileCoincidentPartitions'))
.map((path) => relative(sourceRoot, path).replaceAll('\\', '/'))
.sort();
assert.deepEqual(owners, ['coincident-partitions.ts', 'plan-optimizer.ts'],
'render/pointer modules must not import or invoke the Optimize-only pass');
});
test('issue 276 fails closed for an orphan host, overlap, draft, column and unknown partition data', () => {
const variants = [];
+31 -1
View File
@@ -255,6 +255,36 @@ test('corner Split golden captures before, thin and thick facade states', () =>
}
});
test('issue 276 golden captures 5 cm offsets and hosted door before/after 10/30/virtual', () => {
const scenarios = GOLDEN_SCENARIOS.filter((scenario) => scenario.coincidentPartition);
assert.deepEqual(
scenarios.map((scenario) => scenario.coincidentPartition),
['before', 'thin', 'thick', 'virtual'],
);
for (const scenario of scenarios) {
const fixture = prepareGoldenFixture(scenario);
const space = fixture.config.spaces.find((item) => item.id === scenario.space);
assert.ok(space);
assert.equal(space.cell_cm, 5);
assert.ok(space.rooms[0].poly.some((point) => point[0] === 0.5
&& point[1] === 0.004166666666666667), 'short 5 cm offset must be visible');
assert.equal(space.openings.length, 1);
if (scenario.coincidentPartition === 'before') {
assert.equal(space.partitions.length, 1);
assert.equal(space.openings[0].host.id, 'redundant');
continue;
}
assert.equal(space.partitions, undefined);
assert.equal(space.openings[0].host, undefined);
assert.equal(space.openings[0].x, 0.504166667);
if (scenario.coincidentPartition === 'virtual') {
assert.equal(space.open_spans.length, 1);
} else {
assert.equal(space.walls[0].cm, scenario.coincidentPartition === 'thin' ? 10 : 30);
}
}
});
test('filled opening golden has a pixel-level seam detector', () => {
const scenario = GOLDEN_SCENARIOS.find((item) => item.id === 'openings-filled-tunnel-dark');
assert.ok(scenario);
@@ -322,7 +352,7 @@ test('sun-ray golden requires browser-painted light from a state-only sun entity
assert.ok(scenario);
const fixture = prepareGoldenFixture(scenario);
const space = fixture.config.spaces.find((item) => item.id === scenario.space);
assert.equal(GOLDEN_MATRIX_VERSION, 41);
assert.equal(GOLDEN_MATRIX_VERSION, 42);
assert.equal(space.settings.sun_rays, true);
assert.equal(scenario.northDeg, 90,
'the sign-sensitive golden must keep a non-zero north direction');