fix: isolate wall union failures and guard geometry writes

Issue: #278
User-Visible: yes
This commit is contained in:
Sergey Matyunin
2026-08-24 08:48:37 +03:00
parent 930a911a36
commit 8156f80140
31 changed files with 2101 additions and 1119 deletions
+23 -3
View File
@@ -12,12 +12,13 @@
* #252 (37 забытых позиций в layout), #248, #126. Каждый раз это находил
* человек глазами. Здесь те же вопросы задаются машинно и одинаково.
*
* Модуль сознательно не импортирует `src/**`: он должен читать сырой JSON
* экспорта и живого хранилища, не требуя сборки и не завися от того, что
* продуктовый код считает «правильным» сегодня.
* Structural geometry is the exception: the CLI deliberately imports the
* compiled production preparation instead of maintaining a second boolean
* model which can drift from the renderer (#278).
*/
import { readFileSync } from 'node:fs';
import { fileURLToPath } from 'node:url';
import { checkOptimizeGeometry } from '../test-build/plan-geometry-preflight.js';
/** Доля шага сетки, в пределах которой запись считается лежащей на ребре. */
const EDGE_TOLERANCE = 0.004;
@@ -286,6 +287,22 @@ export function readModel(text) {
return { config, layout };
}
/** Production structural pass with bounded, anonymised diagnostics. */
export function checkPhysicalGeometry(config) {
let result;
try { result = checkOptimizeGeometry(config); } catch {
return [{
invariant: 'physical_geometry', kind: 'geometry_exception', owner: 'config',
reference: 'prepare-exception', detail: 'production geometry check failed',
}];
}
return result.failures.map((failure, index) => ({
invariant: 'physical_geometry', kind: 'physical_geometry',
owner: `space[${index + 1}]`, reference: failure.reason,
detail: 'canonical wall geometry is not safe for a write',
}));
}
/** Наблюдения перечисляются по смыслу: «их 39» читателю ничего не говорит. */
function noteSummary(notes) {
const counts = new Map();
@@ -318,6 +335,8 @@ function report(violations, notes = []) {
layout_owner: 'Позиции без владельца',
wall_carrier: 'Записи толщины вне рёбер и перегородок',
open_span_carrier: 'Виртуальные проёмы вне границ комнат',
physical_geometry: 'Небезопасная каноническая геометрия стен',
geometry_exception: 'Сбой проверки канонической геометрии стен',
lost: 'Потерянные записи толщины',
wall_key: 'Записи толщины, которые не найдутся по ключу',
};
@@ -348,6 +367,7 @@ function main(argv) {
const violations = [
...checkReferences(model, { notes }),
...checkWallKeys(model.config, { notes }),
...checkPhysicalGeometry(model.config),
];
if (argv.includes('--json')) console.log(JSON.stringify({ violations, notes }, null, 2));
else console.log(report(violations, notes));
+63
View File
@@ -752,6 +752,69 @@ export const MUTANTS = [
replace: ' config = _json_copy(payload.get("config"))',
}],
},
{
id: 'wall-component-failure-kills-primary',
guard: 'node demo/smoke_wall_union_isolation.mjs',
because: 'a local shell/extra merge failure must remain a render-safe component set instead '
+ 'of restoring the all-or-nothing blank wall layer from #278',
patches: [{
file: 'src/wall-thickness.ts',
find: " status: degradedExtraCount || degradedCoreCount ? 'degraded-extra' : 'ok',",
replace: " status: degradedExtraCount || degradedCoreCount ? 'failed-core' : 'ok',",
}],
},
{
id: 'wall-isolated-extra-discarded',
guard: 'npx tsc -p tsconfig.test.json && node scripts/fix-test-build.mjs '
+ '&& node --test --test-name-pattern="isolates one failed independent-body" '
+ 'test/wall-thickness.test.mjs',
because: 'keeping only the primary makes the floor look mostly repaired while silently '
+ 'dropping the exact independent body whose union failed (#278)',
patches: [{
file: 'src/wall-thickness.ts',
find: ' isolated.push({ id: `extra-${index}`, geom: standalone });',
replace: ' // mutant: discard the valid isolated body',
}],
},
{
id: 'strict-wall-barrier-accepts-degraded',
guard: 'npx tsc -p tsconfig.test.json && node scripts/fix-test-build.mjs '
+ '&& node --test --test-name-pattern="strict one-space barrier" '
+ 'test/plan-geometry-preflight.test.mjs',
because: 'render-safe degradation is read compatibility, never permission to persist another '
+ 'geometry mutation over the unsafe candidate (#278)',
patches: [{
file: 'src/plan-geometry-preflight.ts',
find: " if (united.status === 'degraded-extra') {",
replace: " if (false && united.status === 'degraded-extra') {",
}],
},
{
id: 'wall-thickness-writer-bypasses-common-barrier',
guard: 'npx tsc -p tsconfig.test.json && node scripts/fix-test-build.mjs '
+ '&& node --test --test-name-pattern="production source routes physical writers" '
+ 'test/wall-union-isolation.test.mjs',
because: 'one direct physical writer is enough to recreate the corrupt persisted geometry '
+ 'even when Resize and the other editors use the shared transaction boundary (#278)',
patches: [{
file: 'src/houseplan-card.ts',
find: " if (this._commitPhysicalGeometry(this._t('history.wall_thickness'), before))",
replace: " if ((this._recordGeometry(this._t('history.wall_thickness'), before), true))",
}],
},
{
id: 'model-invariants-bypasses-production-geometry',
guard: 'npx tsc -p tsconfig.test.json && node scripts/fix-test-build.mjs '
+ '&& node --test --test-name-pattern="Optimize and model-invariants" '
+ 'test/wall-union-isolation.test.mjs',
because: 'the CLI must fail on the same degraded result as Optimize instead of returning the '
+ 'pre-#278 false green for a visually broken export',
patches: [{
file: 'scripts/model-invariants.mjs',
find: ' return result.failures.map((failure, index) => ({',
replace: ' return [].map((failure, index) => ({',
}],
},
{
id: 'optimize-preflight-bypassed',
guard: 'node demo/smoke_optimize_geometry_preflight.mjs',
+10
View File
@@ -27,6 +27,16 @@
/** @type {SmokeLink[]} */
export const SMOKE_LINKS = [
{
symbols: [
'WallGeometryStatus', 'WallGeometryComponent', 'WallBodiesGeometryResult',
'checkSpacePhysicalGeometry', 'spacePhysicalGeometryFingerprint',
],
smokes: ['smoke_wall_union_isolation.mjs'],
because: 'the #278 production bundle must paint isolated canonical components in Plan, '
+ 'View, Static, hidden Iso and light barriers, reject a physical write with zero WS/Undo, '
+ 'and still allow a non-geometry edit on the same degraded legacy space',
},
{
symbols: [
'resolveSafeResize', 'applySafeResize', 'validateSafeResize', 'clampSafeResize',