mirror of
https://github.com/Matysh/houseplan-card
synced 2026-10-02 04:38:55 +00:00
test: prove AC10 — Optimize adds no junction violation (#329 M4)
AC10 was asserted, never shown. Optimize runs alignAllToGrid and repairNearAxisRoomWalls, which move nodes by fractions of a centimetre, and none of П1-П5 carries a margin wider than the grid step in general — so "obviously true by construction" was not available. Two units, both counting violations the way the write barrier does (each side through commitWallSegmentModel first): - the owner's fixture in legacy storage — the inherited apex is there before Optimize, and no rule's count grows after; - the П4 boundary — two rooms exactly 5 cm apart, where snapping could have pulled a node under the limit, stay clean. The first test asserts the baseline actually carries a violation, so it cannot pass by measuring an empty plan; violationsByRule fails loudly if the space or its catalogue goes missing, for the same reason. Spec revision 7 records the proof and the other three review answers. Issue: #329 User-Visible: no
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
# Issue #329 — ограничения стыков стен и честная острая вершина
|
||||
|
||||
Статус: ревизия 6 (r1: M1/M2/L1; r2: M1 — AC7; поправки владельца 2026-08-27: §4 — острая вершина без фаски и без зазубрин на гранях; П3 меряет СТЕНУ, а не атом — кейс компенсации перепада толщин). Решения владельца (чат, 2026-08-27, зафиксированы в issue):
|
||||
Статус: ревизия 7 (r1 код-ревью: H1 — обе стороны на бэкенде судятся после одной миграции, M1 — мёртвый код фаски удалён, M2/M3 — гайды, M4 — AC10 доказан тестами; r1: M1/M2/L1; r2: M1 — AC7; поправки владельца 2026-08-27: §4 — острая вершина без фаски и без зазубрин на гранях; П3 меряет СТЕНУ, а не атом — кейс компенсации перепада толщин). Решения владельца (чат, 2026-08-27, зафиксированы в issue):
|
||||
пять ограничений приняты; проверки действуют на записи, легаси читается как
|
||||
есть; для остаточных острых вершин легаси-планов рендер закрывает остриё
|
||||
честной фаской вместо «трезубца».
|
||||
@@ -104,6 +104,13 @@ margin), модуль `custom_components/houseplan/junction_limits.py`. Счит
|
||||
`junction_limit_<rule>`, отдаётся тем же `connection.send_error`, что и
|
||||
остальные семантические ошибки записи конфига.
|
||||
|
||||
Обе стороны сравнения (`previous` и `candidate`) проходят через
|
||||
`commit_wall_segment_model` до подсчёта. Без этого легаси-документ без
|
||||
каталога отвечал «нарушений нет» независимо от геометрии, и первая же
|
||||
структурная правка после обновления карточки читала унаследованное
|
||||
нарушение как новое — отказ несвязанной правке, прямое нарушение §3
|
||||
(находка H1 код-ревью r1).
|
||||
|
||||
Зеркалятся П1–П4. П5 (просвет комнаты) сознательно оставлен только на
|
||||
клиенте: это утверждение о ПОСТРОЕННЫХ телах стен, и повторение конвейера
|
||||
mitre/inset на Python было бы второй реализацией геометрии, расхождение
|
||||
@@ -211,7 +218,13 @@ mitre/inset на Python было бы второй реализацией гео
|
||||
фикстурах (тест паритета).
|
||||
- **AC10.** Идемпотентность: повторный commit валидного конфига байт-иден-
|
||||
тичен; Optimize на легаси-фикстуре не создаёт новых нарушений (AC-пост-
|
||||
условие §3).
|
||||
условие §3). Доказывается двумя юнитами: (1) фикстура владельца
|
||||
`329-sharp-apex.json` в легаси-хранении — унаследованное нарушение угла
|
||||
есть до Оптимизации и ни по одному правилу счёт не растёт после;
|
||||
(2) граничный кейс П4 — две комнаты ровно в 5 см, где привязка к решётке
|
||||
могла бы утащить узел под порог, после Оптимизации по-прежнему чисты.
|
||||
Оба считают нарушения ТЕМ ЖЕ способом, что барьер записи: обе стороны
|
||||
сперва через `commitWallSegmentModel`.
|
||||
|
||||
## 9. Принято предположительно (поменять свободно)
|
||||
|
||||
|
||||
@@ -216,3 +216,97 @@ test('П3 меряет стену, а не атом: компенсация пе
|
||||
assert.ok(checkSegmentLengths(mixed, CELL, PITCH)
|
||||
.some((item) => item.subject === 'thin'));
|
||||
});
|
||||
|
||||
// --- #329 AC10: Optimize — ремонтный путь, а не источник новых нарушений ---
|
||||
|
||||
/**
|
||||
* Нарушения одного пространства так, как их считает барьер записи: обе
|
||||
* стороны сперва проходят одну и ту же миграцию каталога.
|
||||
*/
|
||||
async function violationsByRule(config, spaceId) {
|
||||
const { commitWallSegmentModel } = await import('../test-build/wall-segment-model.js');
|
||||
const { checkNodes, checkSegmentLengths, checkNodeDistances } =
|
||||
await import('../test-build/junction-limits.js');
|
||||
const { config: migrated } = commitWallSegmentModel(
|
||||
JSON.parse(JSON.stringify(config)),
|
||||
);
|
||||
const space = (migrated.spaces || []).find(
|
||||
(item) => String(item.id) === String(spaceId),
|
||||
);
|
||||
// Молчаливое «пространства нет» превратило бы тест в проверку пустоты.
|
||||
assert.ok(space, `пространство ${spaceId} есть после миграции`);
|
||||
assert.ok((space.wall_segments || []).length > 0, 'каталог стен непуст');
|
||||
const segments = [
|
||||
...(space.wall_segments || []).map((item) => ({ id: item.id, a: item.a, b: item.b, cm: Number(item.cm) })),
|
||||
...(space.partitions || []).map((item) => ({ id: item.id, a: item.a, b: item.b, cm: Number(item.cm) })),
|
||||
];
|
||||
const cellCm = Number(space.cell_cm) || 1;
|
||||
const all = [
|
||||
...checkNodes(segments),
|
||||
...checkSegmentLengths(segments, cellCm, GRID_STEP_N),
|
||||
...checkNodeDistances(segments, cellCm, GRID_STEP_N),
|
||||
];
|
||||
const counts = {};
|
||||
for (const item of all) counts[item.rule] = (counts[item.rule] || 0) + 1;
|
||||
return counts;
|
||||
}
|
||||
|
||||
test('AC10: Optimize на легаси-плане с нарушением не добавляет новых', async () => {
|
||||
const { optimizePlans } = await import('../test-build/plan-optimizer.js');
|
||||
const { readFileSync } = await import('node:fs');
|
||||
const fixture = JSON.parse(readFileSync(
|
||||
new URL('./fixtures/329-sharp-apex.json', import.meta.url), 'utf8',
|
||||
));
|
||||
// Легаси-хранение: только контуры и `walls`, без каталога — то состояние,
|
||||
// в котором план приходит на Оптимизацию в первый раз.
|
||||
const space = {
|
||||
id: 'legacy', title: 'legacy', cell_cm: fixture.cell_cm,
|
||||
view_box: [0, 0, 1, 1],
|
||||
rooms: fixture.rooms.map(({ id, name, area, poly }) => ({ id, name, area, poly })),
|
||||
walls: fixture.walls.map(({ id, a, b, cm }) => ({ key: id, a, b, cm })),
|
||||
openings: [], room_drafts: [], partitions: [], wall_columns: [],
|
||||
};
|
||||
const config = { spaces: [space], markers: [], settings: {} };
|
||||
const before = await violationsByRule(config, 'legacy');
|
||||
// Шпиль владельца обязан читаться как унаследованное нарушение — иначе
|
||||
// тест доказывал бы «ничего не выросло» на пустом месте.
|
||||
assert.equal((before.angle || 0) > 0, true, 'фикстура несёт нарушение угла');
|
||||
|
||||
const result = optimizePlans(config, {});
|
||||
const after = await violationsByRule(result.config, 'legacy');
|
||||
for (const rule of new Set([...Object.keys(before), ...Object.keys(after)])) {
|
||||
assert.equal((after[rule] || 0) <= (before[rule] || 0), true,
|
||||
`Оптимизация добавила нарушений по правилу ${rule}: `
|
||||
+ `${before[rule] || 0} → ${after[rule] || 0}`);
|
||||
}
|
||||
});
|
||||
|
||||
test('AC10: привязка к решётке не утаскивает узел на пороге П4 под лимит', async () => {
|
||||
const { optimizePlans } = await import('../test-build/plan-optimizer.js');
|
||||
// Две комнаты, между гранями ровно 5 см — П4 выполняется впритык, и
|
||||
// сдвиг на доли сантиметра при выравнивании увёл бы его под порог.
|
||||
const CELL_LOCAL = 2;
|
||||
const u = (value) => (value / CELL_LOCAL) * GRID_STEP_N;
|
||||
const box = (x, y, w, h) => [[x, y], [x + w, y], [x + w, y + h], [x, y + h]];
|
||||
const left = box(u(100), u(100), u(200), u(200));
|
||||
const right = box(u(100) + u(205), u(100), u(200), u(200));
|
||||
const wallsOf = (poly, prefix) => poly.map((point, index) => ({
|
||||
key: `${prefix}${index}`, a: point, b: poly[(index + 1) % poly.length], cm: 15,
|
||||
}));
|
||||
const config = { spaces: [{
|
||||
id: 'edge', title: 'edge', cell_cm: CELL_LOCAL, view_box: [0, 0, 1, 1],
|
||||
rooms: [
|
||||
{ id: 'a', name: 'a', area: null, poly: left },
|
||||
{ id: 'b', name: 'b', area: null, poly: right },
|
||||
],
|
||||
walls: [...wallsOf(left, 'l'), ...wallsOf(right, 'r')],
|
||||
openings: [], room_drafts: [], partitions: [], wall_columns: [],
|
||||
}], markers: [], settings: {} };
|
||||
|
||||
const before = await violationsByRule(config, 'edge');
|
||||
assert.equal(before.distance || 0, 0, 'исходный план по П4 чист');
|
||||
const result = optimizePlans(config, {});
|
||||
const after = await violationsByRule(result.config, 'edge');
|
||||
assert.equal(after.distance || 0, 0,
|
||||
'после Оптимизации узлы не сблизились под 5 см');
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user