mirror of
https://github.com/Matysh/houseplan-card
synced 2026-10-02 21:01:21 +00:00
fix: isolate wall union failures and guard geometry writes
Issue: #278 User-Visible: yes
This commit is contained in:
@@ -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));
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
Reference in New Issue
Block a user