test: prove wall-draw junction parity

Issue: #461
User-Visible: no
This commit is contained in:
Sergey Matyunin
2026-09-05 14:39:00 +03:00
parent 13ab4b12b7
commit 41e6eef941
+89
View File
@@ -5,6 +5,8 @@ import { readFileSync } from 'node:fs';
import {
draftLiveCandidateSpace, draftLiveSeed, isSingleDraftAppend,
} from '../test-build/draft-live-preflight.js';
import { increasedViolations, junctionLimitViolations } from '../test-build/junction-limits.js';
import { GRID_STEP_N } from '../test-build/space-geometry.js';
const room = (id, x, y, size = 0.1) => ({
id,
@@ -17,6 +19,45 @@ const wallSegments = (one) => one.poly.map((a, index) => ({
owners: [one.id],
}));
const cmUnits = (value) => value / 5 * GRID_STEP_N;
const limitSegments = (space) => [
...(space.wall_segments || []),
...(space.partitions || []),
...(space.room_drafts || []).flatMap((draft) =>
(draft.segments || []).map((segment, index) => ({
id: segment.id,
a: draft.points[index], b: draft.points[index + 1], cm: segment.cm,
}))),
];
const introducedRules = (previous, candidate) => increasedViolations(
junctionLimitViolations(
{ model_version: 9, spaces: [candidate] }, candidate.id,
limitSegments(candidate), { status: 'lightweight' },
),
junctionLimitViolations(
{ model_version: 9, spaces: [previous] }, previous.id,
limitSegments(previous), { status: 'lightweight' },
),
).map((item) => item.rule).sort();
const paritySpace = (partitions, seedLengthCm = 40) => ({
id: 'space', cell_cm: 5,
rooms: [], walls: [], wall_segments: [], openings: [], open_spans: [],
wall_columns: [],
// Valid remote work makes the full input genuinely larger than the local
// projection without adding an inherited limit violation.
partitions: [
...partitions,
{ id: 'remote', a: [2, 2], b: [2 + cmUnits(40), 2], cm: 0 },
],
room_drafts: [{
id: 'active', points: [[0, 0], [cmUnits(seedLengthCm), 0]],
segments: [{ id: 'seed', cm: 0 }],
}],
});
const activeDraft = {
id: 'active',
points: [[0.05, 0.05], [0.1, 0.05], [0.1, 0.1], [0.2, 0.1]],
@@ -140,6 +181,54 @@ test('#461 remote geometry growth does not grow the selected component', () => {
assert.deepEqual(selectedEighty.space.partitions.map((item) => item.id), ['near']);
});
test('#461 local and full junction verdicts agree at every limit boundary', () => {
const ray = (id, angleDeg, lengthCm = 40) => {
const angle = angleDeg * Math.PI / 180;
return {
id, a: [0, 0],
b: [cmUnits(lengthCm) * Math.cos(angle), cmUnits(lengthCm) * Math.sin(angle)],
cm: 0,
};
};
const valenceRays = (total) => Array.from({ length: total - 1 }, (_, index) =>
ray(`ray-${total}-${index}`, (index + 1) * 360 / total));
const cases = [
{ name: '14 degree junction is rejected', partitions: [ray('angle-14', 14)],
expected: ['angle'] },
{ name: '15 degree junction is accepted', partitions: [ray('angle-15', 15)],
expected: [] },
{ name: '19 cm segment is rejected', partitions: [], seedLengthCm: 19,
expected: ['length'] },
{ name: '20 cm segment is accepted', partitions: [], seedLengthCm: 20,
expected: [] },
{ name: '4 cm node clearance is rejected', partitions: [{
id: 'distance-4',
a: [cmUnits(4), cmUnits(4)], b: [cmUnits(4), cmUnits(44)], cm: 0,
}], expected: ['distance'] },
{ name: '5 cm node clearance is accepted', partitions: [{
id: 'distance-5',
a: [cmUnits(5), cmUnits(5)], b: [cmUnits(5), cmUnits(45)], cm: 0,
}], expected: [] },
{ name: 'valence 6 is accepted', partitions: valenceRays(6), expected: [] },
{ name: 'valence 7 is rejected', partitions: valenceRays(7), expected: ['valence'] },
];
for (const scenario of cases) {
const candidate = paritySpace(scenario.partitions, scenario.seedLengthCm);
const previous = { ...candidate, room_drafts: [] };
const seed = draftLiveSeed(candidate, 'active');
const localPrevious = draftLiveCandidateSpace(previous, seed)?.space;
const localCandidate = draftLiveCandidateSpace(candidate, seed)?.space;
assert.ok(seed && localPrevious && localCandidate, scenario.name);
const full = introducedRules(previous, candidate);
const local = introducedRules(localPrevious, localCandidate);
assert.deepEqual(local, full, `${scenario.name}: local verdict must equal full verdict`);
assert.deepEqual(full, scenario.expected, `${scenario.name}: boundary verdict`);
assert.equal(localCandidate.partitions.some((item) => item.id === 'remote'), false,
`${scenario.name}: parity must be proved on a bounded projection`);
}
});
test('#461 projector closes a collinear run and keeps every ray at its junctions', () => {
const space = {
id: 'space', cell_cm: 5, rooms: [], walls: [], wall_segments: [],