diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 37b70562..062a8351 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -23,7 +23,9 @@ valid when it becomes a room or independent walls, while a rejected transaction stores no partial result and no longer reports false success. An invalid open passage also retains its specific actionable error instead - of being masked by the generic wall-model migration blocker. + of being masked by the generic wall-model migration blocker. The compact + space card keeps wall-hosted doors, windows, gates and passages visible after + the migration as well. Ambiguous legacy geometry is left unchanged with a recovery prompt. No Plan editor controls or zero-thickness-wall behaviour changed diff --git a/docs/CHANGELOG.ru.md b/docs/CHANGELOG.ru.md index d4fd6730..076877d7 100755 --- a/docs/CHANGELOG.ru.md +++ b/docs/CHANGELOG.ru.md @@ -28,7 +28,8 @@ в комнату или независимые стены, а отклонённая транзакция не записывает частичный результат и не показывает ложное сообщение об успехе. Невалидный открытый проход по-прежнему получает своё точное сообщение об ошибке, а не - общий блокер миграции стен. + общий блокер миграции стен. Компактная карточка пространства также не теряет + после миграции двери, окна, ворота и проходы, привязанные к стенам. Неоднозначная старая геометрия остаётся без изменений с подсказкой по восстановлению. Кнопки редактора плана и поведение стен нулевой толщины не менялись diff --git a/src/space-render.ts b/src/space-render.ts index e3fc6887..e5200234 100644 --- a/src/space-render.ts +++ b/src/space-render.ts @@ -215,7 +215,10 @@ export function renderSpaceStatic(o: StaticRenderOpts): TemplateResult | null { const walls: WallEntry[] = Array.isArray(spCfg.walls) ? spCfg.walls : []; const cellCm = Number(spCfg.cell_cm) > 0 ? Number(spCfg.cell_cm) : 5; const resolvedHosted = (spCfg.openings || []).flatMap((opening: OpeningCfg) => { - if (!opening.host) return []; + // Contour-wall hosts keep the same saved x/y/angle projection as legacy + // room openings. Only independent partitions need to be materialised from + // their host because that geometry is stored separately from the opening. + if (opening.host?.kind !== 'partition') return []; const resolved = resolvePartitionOpeningCompat( opening, space.partitions, NORM_W, cellCm, GRID_PITCH, ).resolved; @@ -273,7 +276,7 @@ export function renderSpaceStatic(o: StaticRenderOpts): TemplateResult | null { // Static intentionally keeps the historical door/window/gate wall output. // Only the new negative-space type participates in its masonry fingerprint. const resolvedRawOpenings = (spCfg.openings || []).flatMap((opening: OpeningCfg) => { - if (!opening.host) return [opening]; + if (!opening.host || opening.host.kind === 'wall') return [opening]; const resolved = resolvedHosted.find((item) => item.opening.id === opening.id); return resolved ? [materializePartitionOpening(opening, resolved, NORM_W)] : []; }); diff --git a/test/open-passage-contract.test.mjs b/test/open-passage-contract.test.mjs index 7b1374e6..fa9d7c50 100644 --- a/test/open-passage-contract.test.mjs +++ b/test/open-passage-contract.test.mjs @@ -47,6 +47,10 @@ test('passage bindings cannot reach subscriptions, locks or the info card', () = test('static passage cuts and tunnels are passage-only additions', () => { assert.match(staticRender, /staticPassageOpenings\(resolvedRawOpenings, NORM_W\)/); + assert.match(staticRender, /opening\.host\?\.kind !== 'partition'/, + 'only independent-wall hosts go through the partition resolver'); + assert.match(staticRender, /!opening\.host \|\| opening\.host\.kind === 'wall'/, + 'stable contour-wall hosts retain the legacy raw-opening projection'); assert.match(staticRender, /passages: staticPassages\.map/); assert.match(staticRender, /renderOpeningTunnelFills/); assert.match(staticRender, /passageDataTunnels/); @@ -69,7 +73,11 @@ test('placement preview adds passage-only cut geometry without changing saved sy }); test('all write/import paths invoke the semantic passage validator', () => { - assert.equal((backend.match(/validate_opening_passages\(/g) || []).length, 2); + assert.equal((backend.match(/validate_opening_passages\(/g) || []).length, 3); + assert.match(backend, /validate_opening_passages\(candidate_config, config_data\.get\("config"\)\)/, + 'Optimize validates the submitted v7 model before the identity barrier'); + assert.match(backend, /validate_opening_passages\(msg\["config"\], config_data\.get\("config"\)\)/, + 'Optimize validates the committed v8 model before persistence'); assert.match(importer, /validate_opening_passages\(merged_config, current_config\)/); assert.match(importer, /validate_opening_passages\(incoming_config, validate_all=True\)/); });