feat: junction nodes — detector, fans over the approved chamfer, scene set (#302)

Вторая половина переработки узлов поверх ядра из прошлого коммита:

- `junctionContractHoles` — объективный инвариант «тело ⊇ полосы ∪ веера в
  фасадной границе» как экспортная чистая функция; самопроверка на заведомо
  дырявой фикстуре входит в юниты. Первая формулировка детектора из ТЗ
  («окружено кладкой с ≥5 из 8 сторон») уточнена в §8.4 по факту измерения:
  она ложно флагует легитимный пол комнаты в острых внутренних углах.
- `junctionNodeBound` — «гладкая» фасадная граница (конверт с обычными углами)
  экспортирована: ею клиппуются куски узла и ею же пользуются тесты.
- юниты #302: веера/рефлекс/короткий толстый саппорт/детектор/репро
  end-to-end; хелпер тестов старого контракта переведён на strip-safe
  семантику по саппортам и фасадной границе.
- смок `smoke_junction_holes`: контрактные пробы считаются в node из той же
  фикстуры и проверяются в браузере по реальному `d`-пути карточки.
- сет из 16 golden-сцен стыков крупным планом (звёзды лучей: T/X/Y/острые/
  почти коллинеарные/виртуальные/колонна/черновик) + сцена-репро владельца;
  билдер сцен и `zoomCenter` в harness. Контракт сцены
  `multiwall-junction-bevel-view-dark` инвертирован по решению владельца:
  проба в перекрытии полос обязана быть ЗАПОЛНЕНА (strip-safe), а не пустой.
- семь мутантов §14, включая слепоту детектора и невозврат саппортов.
- фикстура #197: площадь кладки выросла на 0.2 юнита² — слайвер вееров вдоль
  хорд фаски; константа обновлена с комментарием.

Эталоны новых сцен идут отдельным коммитом с положенными трейлерами.

Issue: #302
User-Visible: yes
This commit is contained in:
Codex
2026-08-25 17:34:19 +03:00
parent a1361974a3
commit 76d60b3e1b
24 changed files with 1160 additions and 598 deletions
+71
View File
@@ -2209,6 +2209,77 @@ export const MUTANTS = [
replace: ' .dev:not(.unavail):hover {',
}],
},
{
id: 'junction-fans-disabled',
guard: 'npx tsc -p tsconfig.test.json && node scripts/fix-test-build.mjs && node --test --test-name-pattern="issue 302" test/wall-thickness.test.mjs',
because: 'без вееров сектор между соседними полосами узла остаётся дырой — '
+ 'это и есть класс артефактов #302',
patches: [{
file: 'src/wall-thickness.ts',
find: ' if (mitre && mitreWithinStrips',
replace: ' if (false && mitre && mitreWithinStrips',
}, {
file: 'src/wall-thickness.ts',
find: ' push(out.fans, [[P[0], P[1]], EA, A2, B2, EB]);',
replace: ' void A2; void B2;',
}],
},
{
id: 'junction-supports-not-restored',
guard: 'npx tsc -p tsconfig.test.json && node scripts/fix-test-build.mjs && node --test --test-name-pattern="hole-free end to end" test/wall-thickness.test.mjs',
because: 'фаска старого слоя срезает материал полос на острых стыках; '
+ 'без возврата саппорт-квадов узла репро владельца снова дырявое',
patches: [{
file: 'src/wall-thickness.ts',
find: ' for (const piece of [...corners.supports, ...corners.fans]) {',
replace: ' for (const piece of corners.fans) {',
}],
},
{
id: 'junction-fan-ignores-thick-length',
guard: 'npx tsc -p tsconfig.test.json && node scripts/fix-test-build.mjs && node --test --test-name-pattern="issue 302" test/wall-thickness.test.mjs',
because: 'веер, шагающий за короткий толстый саппорт, рисует латеральный '
+ 'фантом рядом с тонким продолжением (#271)',
patches: [{
file: 'src/wall-thickness.ts',
find: ' mitreWithinStrips = tA <= A.thickLength && tB <= B.thickLength;',
replace: ' mitreWithinStrips = true;',
}],
},
{
id: 'junction-reflex-sector-fanned',
guard: 'npx tsc -p tsconfig.test.json && node scripts/fix-test-build.mjs && node --test --test-name-pattern="issue 302" test/wall-thickness.test.mjs',
because: 'рефлексный сектор — внешность выпуклого угла; веер там — лишний '
+ 'фасадный материал (контракт вогнутого Split)',
patches: [{
file: 'src/wall-thickness.ts',
find: ' if (sector > Math.PI + 1e-9) continue;',
replace: ' void sector;',
}],
},
{
id: 'junction-detector-blind',
guard: 'npx tsc -p tsconfig.test.json && node scripts/fix-test-build.mjs && node --test --test-name-pattern="issue 302" test/wall-thickness.test.mjs',
because: 'слепой детектор превращает инвариант «нет дыр» в декорацию — '
+ 'самопроверка на заведомо дырявой фикстуре обязана краснеть',
patches: [{
file: 'src/wall-thickness.ts',
find: ' if (!inGeometry(geometry, x, y)) holes.push([x, y]);',
replace: ' void x; void y;',
}],
},
{
id: 'junction-pieces-unbounded',
guard: 'npx tsc -p tsconfig.test.json && node scripts/fix-test-build.mjs '
+ '&& node --test --test-name-pattern="concave vertex" test/wall-thickness.test.mjs',
because: 'куски узла без клипа гладкой фасадной границей отращивают новый '
+ 'фасад на вогнутой вершине',
patches: [{
file: 'src/wall-thickness.ts',
find: ' if (bound) ring = intersection(ring, bound);',
replace: ' void bound;',
}],
},
{
id: 'hatch-step-ignores-cell-cm',
guard: 'npx tsc -p tsconfig.test.json && node scripts/fix-test-build.mjs '