Files
houseplan-card/docs/specs/197-junction-patch-fail-dark.md
T
2026-08-19 13:02:12 +03:00

500 lines
35 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Issue #197 — один junction-патч не гасит кладку всего плана
- **Issue:** https://github.com/Matysh/houseplan-card/issues/197
- **Редакция:** r2 после `docs/reviews/SPEC-REVIEW-197-r1.md`; статус
определяется только метками issue
- **Тип / приоритет:** bug / P2
- **Оценка:** пользовательская ценность 9/10; ценность для разработки 9/10;
сложность 6/10; риск 8/10
- **Область:** каноническая геометрия стен, Plan, View/киоск, static card,
hidden Iso, clean floor/paper, Glow и солнце
- **Модель данных:** без изменений и миграции
- **Связано:** #141, #150, #198, #199, #201, `docs/WALL-THICKNESS.md`,
`docs/ARCHITECTURE.md`, `docs/TOUCH-SUPPORT.md`
## 0. Ответ на ревью r1
High-1 принят в части воспроизводимости: первая редакция не проговорила
критически важный контракт координат `WallEntry.a/b`, поэтому независимый
повтор оказался не тем production-вызовом, который выполнял автор.
В коде ревью комнаты, cuts **и `walls[].a/b`** были заранее умножены на 1000.
Это двойное масштабирование wall endpoints: `WallEntry.a/b` являются
persisted config coordinates, а `entrySpan(w, coordScale)` в
`src/wall-thickness.ts:124–129` сам умножает их на `coordScale`. Production
renderer масштабирует room polygons и open cuts, но передаёт `space.walls`
неизменными. При корректном вызове исходный fixture по-прежнему даёт один patch
и `wallBodiesGeometry() === null` на `19e92e0`.
R2 закрывает High не возражением на словах, а:
1. фиксирует координатный контракт явно;
2. добавляет ниже полный исполняемый reproducer, который сам извлекает fixture
из issue и не допускает ручного преобразования walls;
3. уточняет AC1/AC2 так, чтобы unit сначала доказывал правильную подготовку
fixture и наличие patch, а затем красный `null` исходного кода;
4. отделяет #201: прямой exact-key helper `thicknessCmAt()` действительно даёт
`0` на новом atomic child, но canonical `wallIntervals()`/`intervalCmAt()` на
production input дают `20 см`; поэтому #201 не подавляет patch и остаётся
отдельной задачей для собственной аналитики вызовов.
## 1. Сценарий и персона
**Персона:** домашний администратор, который уже построил подробный план и
периодически обслуживает его через «Общие настройки → Оптимизировать планы».
**Поверхность и момент:** после оптимизации или обычного открытия ранее
сохранённого много-комнатного плана человек переходит в View либо Plan. В одном
виртуальном T-стыке сходятся реальные стены соседних комнат с ненулевой
толщиной.
Задача поддерживает J1, J4 и J6 из `docs/SCOPE.md`: план должен правдиво
показывать пространственную структуру, штатно обслуживаться через GUI и не
терять физическую геометрию от одного локального численного отказа. View,
киоск и static card — блокирующие поверхности; Plan editor остаётся
desktop-first, но не может создавать или показывать иную физику.
## 2. Что человек увидит до и после
**До:** из-за одного стыка во всём пространстве исчезает штрихованная кладка;
между отступившими заливками комнат остаются белые полосы полной или половинной
толщины, как будто все стены одновременно стали прозрачными.
**После:** весь план продолжает показывать стены. Проблемный T-стык строится
обычно; даже если его дополнительный соединительный фрагмент численно
необрабатываем, отказ ограничивается этим фрагментом и не удаляет остальную
кладку, бумагу и физические препятствия.
## 3. Подтверждённая проблема и причина
Дефект воспроизведён на `origin/dev` `19e92e0` с анонимизированным fixture из
issue и production-параметрами:
- 8 комнат, 25 wall-записей, 3 `open_spans`;
- только `rooms[].poly` и `open_spans` переведены в render coordinates через
`NORM_W = 1000`; `walls[].a/b` оставлены в persisted config coordinates;
- `pitch = GRID_STEP_N`, `cell_cm = 5`, `gridPitch = GRID_PITCH`,
`coordScale = 1000`;
- `wallIntervals()` успешно разрешает значения `15/20/22/28/29/33 см`;
- `wallBodiesGeometry(...)` возвращает `null`.
`walls[].a/b` нельзя предварительно умножать на `NORM_W`: `entrySpan()` делает
это внутри по переданному `coordScale`. Следующий reproducer является
каноническим для проверки r2 после обычной сборки `test-build`:
```js
import { execFileSync } from 'node:child_process';
import { wallBodiesGeometry, wallIntervals } from './test-build/wall-thickness.js';
import { resolveOpenCuts } from './test-build/open-spans.js';
import { GRID_PITCH, GRID_STEP_N, NORM_W } from './test-build/space-geometry.js';
const issue = JSON.parse(execFileSync('gh', [
'issue', 'view', '197', '--repo', 'Matysh/houseplan-card', '--json', 'body',
], { encoding: 'utf8' }));
const raw = issue.body.match(/```json\s*([\s\S]*?)```/)[1];
const fixture = JSON.parse(raw);
const rooms = fixture.rooms.map((room) => ({
...room,
poly: room.poly.map(([x, y]) => [x * NORM_W, y * NORM_W]),
}));
const walls = structuredClone(fixture.walls); // config coords: НЕ умножать
const openCuts = resolveOpenCuts(
rooms, fixture.open_spans, NORM_W, GRID_PITCH * 0.02,
);
const intervals = wallIntervals(
rooms, walls, openCuts, GRID_STEP_N, fixture.cell_cm, GRID_PITCH, NORM_W,
);
const geometry = wallBodiesGeometry(
rooms, walls, openCuts, [], GRID_STEP_N, fixture.cell_cm, GRID_PITCH,
NORM_W, [],
);
console.log({
counts: [rooms.length, walls.length, openCuts.length], // [8, 25, 3]
nodeCm: intervals
.filter((iv) => Math.abs(iv.a[1] - 550) < 1e-6
&& Math.abs(iv.b[1] - 550) < 1e-6)
.map((iv) => iv.cm), // содержит 20
failed: geometry === null, // true на 19e92e0
});
```
Для прямого контроля patch-list автор исполнил ту же compiled module с временно
экспортированной без изменения тела `virtualJunctionPatches()`; результат — один
patch:
```json
[[[620.8333333333334,550],[612.5,550],
[612.5000000000001,541.6666666666665],
[620.8333333333334,541.6666666666666]]]
```
В реализации test seam должен сделать этот pre-union результат проверяемым без
source rewriting; точная публичность helper не является продуктовым контрактом.
Причинная цепочка:
1. `virtualJunctionPatches()` создаёт один прямоугольный patch около
`[620.8333…, 550]`.
2. Две математически совпадающие координаты приходят разными IEEE-754 числами:
`612.5` и `612.5000000000001`; аналогичный шум есть по Y.
3. `polyclip-ts` не может завершить output ring при `union(body, patch)` и
выбрасывает исключение.
4. Room rings и atomic wall-edge bodies имеют per-piece `try/catch`, а цикл
junction patches в `src/wall-thickness.ts:1760–1761` — нет.
5. Исключение достигает общего `catch`, `wallBodiesGeometry()` возвращает
`null`, и все потребители закономерно включают общий fail-dark.
Дополнительные исполняемые проверки отделяют причину от корреляций:
- удаление 15-см микро-интервала оставляет результат `null`;
- удаление open cuts, которое отключает virtual-junction pass, даёт валидную
geometry;
- округление только координат patch с технической точностью от `10⁻⁹` до
`10⁻¹²` render unit даёт валидную geometry; итоговая площадь после canonical
clipping совпадает с вариантом, где единственный отказавший patch локально
пропущен.
Проверка замечания r1 о толщине также выполнена на корректном production input:
`wallIntervals()` и `intervalCmAt()` возвращают `20 см` на обоих atomic children
горизонтальной стены узла. `thicknessCmAt()` — более узкий direct-key helper —
не наследует parent entry для child key и возвращает `0`, однако
`virtualJunctionPatches()` его не вызывает: она получает уже разрешённые
`wallIntervals()`. Поэтому Medium #201 не является причиной нулевого patch-list
в корректном воспроизведении #197.
Это не дефект #150: atomic thickness profile и exterior transition исправны.
Короткий вне-сеточный интервал — отдельная находка #198. Общая проверка результата
Optimize перед записью — отдельный защитный барьер #199.
## 4. Зафиксированное продуктовое решение
Открытых продуктовых вопросов нет. Действующий контракт уже задан
`docs/WALL-THICKNESS.md` и ТЗ #141:
1. локальный independent/virtual junction не может удалить валидную кладку
остальных комнат;
2. канонический результат един для рисунка, пола и световых препятствий;
3. при невозможности обработать дополнительный patch допустима локальная
консервативная деградация без записи config;
4. основной structural boolean failure по-прежнему отличим от успешной пустой
geometry и остаётся fail-dark; #197 не превращает любой сбой в оптимистичный
raw-ring fallback.
## 5. Скоуп
В задачу входят:
1. численная стабилизация координат, вычисленных для virtual junction patches,
перед передачей в boolean engine;
2. изоляция `union` каждого patch: один отказ не откатывает ранее построенные
room rings, edge bodies и успешные patches;
3. сохранение валидной основной geometry, `paperGeom`, `depthUnits` и
`openingIndex`, когда отказал только дополнительный patch;
4. одинаковый исправленный structural result для Plan, View/киоска,
`houseplan-space-card`, hidden Iso/floor footprint, clean floor/area, Glow,
source guard и солнца;
5. неизменность opening cuts, exterior shell, независимых partitions/columns и
порядка, в котором они входят в canonical body;
6. существующие сохранённые планы без миграции и фоновой записи;
7. unit regression на полном fixture, матрица численных вариантов, targeted
production-bundle browser smoke, visual regression и release-документация.
## 6. Не входит в задачу
- удаление, слияние или изменение минимальной длины wall interval — #198;
- geometry self-check в Optimize preview/apply/undo — #199;
- изменение алгоритма `normalizeWallIntervals()`, `degradeWalls()` или
`alignAllToGrid()`;
- общая замена `polyclip-ts` или настройка его глобальной точности;
- новый persisted junction/node, schema version или migration;
- привязка offset/mitre-вершин к пользовательской сетке;
- изменение толщины, `MITRE_LIMIT`, типов стыка или правил #141;
- исправление произвольного основного exterior/ring/opening boolean failure;
- новые controls, предупреждения, диагностический toast или настройка fallback;
- публикация скрытой изометрии.
## 7. Контракт поведения
### 7.1. Численная стабилизация patch
1. Координаты patch должны быть конечными; patch с не-конечным значением либо
площадью не больше действующего geometry tolerance не передаётся в union.
2. Математически совпадающие результаты операций с общей вершиной и offsets
приводятся к одной детерминированной координате с технической точностью,
многократно меньшей общего geometry epsilon.
3. Стабилизация не использует шаг пользовательской сетки: half-depth 15–33 см и
диагональные mitre вправе находиться между grid nodes.
4. Сдвиг любой вершины от исходного конечного значения не превышает выбранный
numeric tolerance и не меняет видимую толщину, bounded-mitre envelope или
association с исходным узлом.
5. Результат не зависит от представления `x` против `x ± ulp`, направления
segment и порядка эквивалентных wall records.
### 7.2. Изоляция локального отказа
1. Каждый junction patch объединяется независимо, как room-ring и edge-body
pieces в соседних canonical loops.
2. Перед попыткой сохраняется последняя успешная `body`. Если union patch
выбрасывает исключение, эта `body` остаётся результатом следующего шага.
3. Ошибка одного patch не пропускает последующие patches, exterior clipping,
shell union, opening cuts и independent extra-body union.
4. Если до patches существует валидная непустая structural body, функция не
возвращает `null` только из-за patch. `paperGeom` также не теряется.
5. Если основная exterior/ring/opening geometry не может быть построена либо
финальный обязательный pass падает, сохраняется действующий общий `null` и
fail-dark; raw per-room rings не воскрешаются.
6. Fallback ничего не записывает, не меняет входные массивы и не создаёт
различающуюся физику между render consumers.
### 7.3. Канонические потребители
Один возвращённый `wallBodiesGeometry()`/structural cache определяет:
- masonry path полного Plan/View и киоска;
- masonry path static card;
- hidden Iso footprint и wall faces;
- clean-floor subtraction, room fill и displayed area;
- paper footprint;
- Glow barriers, source-inside-body guard и spill;
- солнечные препятствия.
Ни один consumer не получает отдельный SVG-only обход либо собственную
нормализацию patch. Presentation-различия вроде hatch suppression при малой
экранной толщине остаются допустимыми; физическое множество совпадает.
### 7.4. Совместимость существующей геометрии
- Исправный virtual T продолжает получать прежний bounded patch.
- Room L/T/nested joins, corner Split #123, thickness transition #150 и
zero-divider #172 не меняют внешний outline.
- Openings по-прежнему режут room masonry после junction pass; совпавший
independent body не режется room opening.
- Partitions, drafts и columns не увеличивают Stage floor footprint.
- Перестановка комнат/стен может менять внутренний порядок boolean operations,
но не геометрическое множество и не способность функции завершиться.
## 8. Архитектурный контракт реализации
1. Исправление живёт в общей geometry-логике `src/wall-thickness.ts`, рядом с
построением/union virtual junction patches, а не в отдельном renderer.
2. Numeric normalization является чистым локальным helper либо эквивалентной
операцией и применяется только к вычисляемым patch vertices.
3. Tolerance масштабируется согласованно с `coordScale`, чтобы normalized и
render-space вызовы описывали одну физическую форму; жёсткое округление до
сантиметра, grid step или фиксированного числа видимых знаков запрещено.
4. Per-patch fallback сохраняет `body` транзакционно: присваивание происходит
только после успешного `union`.
5. Structural cache key не меняется: стабилизация является детерминированной
функцией уже входящих в fingerprint координат, cuts и толщин.
6. Логирование, если добавляется, не содержит config/названий комнат и не
спамит каждый HA state tick. Новый публичный diagnostic API не требуется.
Предполагаемые файлы:
- `src/wall-thickness.ts`;
- `test/wall-thickness.test.mjs`;
- `demo/smoke_junction_patch_resilience.mjs` либо узкое расширение существующего
wall-thickness smoke с однозначной связью с #197;
- visual fixture в golden matrix, если текущая wall-junction сцена не доказывает
сохранение полного много-комнатного плана;
- `docs/WALL-THICKNESS.md`, `docs/ARCHITECTURE.md`,
`docs/USER-GUIDE.ru.md`, `docs/TESTING.md`, `docs/STATUS.md`;
- `docs/CHANGELOG.md`, `docs/CHANGELOG.ru.md`;
- три синхронные поставляемые копии bundle.
## 9. Модель данных, compatibility и миграция
Форматы `RoomCfg`, `WallEntry`, `open_spans`, openings, partitions и columns не
меняются. Новых ключей, aliases и version markers нет.
- старые и уже оптимизированные планы исправляются вычисляемо при чтении;
- render, preview и cache warm-up не выполняют config/layout/storage write;
- Optimize не запускается автоматически и его отчёт не меняется;
- import/export и backend validation не меняются;
- откат возвращает прежнее поведение без преобразования данных;
- future/unknown fields не затрагиваются.
## 10. UX, i18n, accessibility и touch
Новых controls, текстов, focus/keyboard semantics, ARIA, animation и locale keys
нет. RU/EN i18n не меняется.
Plan editor остаётся desktop-first. Touch editor — best effort / intentionally
degraded, но один сохранённый plan не может получить другую masonry geometry из-за
pointer type. View, kiosk и static card полностью поддерживаются и обязаны
оставаться читаемыми.
Светлая/тёмная тема, forced colours, `prefers-reduced-motion`, hatch color и wall
opacity не меняют fallback. Цвет не используется как единственное доказательство:
unit проверяет геометрическое множество, browser smoke — реально отрисованный
path на двух темах.
## 11. Критерии приёмки
- **AC1 (`unit`):** полный анонимизированный fixture из issue готовится ровно по
reproducer §3: room/cut coordinates масштабированы, persisted `walls[].a/b`
остаются нормализованными; получаются 8 rooms, 25 walls, 3 cuts и 20-см
horizontal atomic intervals. На исходном `dev` этот вызов даёт `null`, после
исправления — ненулевой объект с непустыми `geom` и `paperGeom`.
- **AC2 (`unit`):** test seam подтверждает ровно один pre-union patch с четырьмя
вершинами из §3; варианты `x`, `x ± ulp` и стабилизированный эквивалент дают
одно geometric set в пределах numeric tolerance и не меняют bounded envelope.
- **AC3 (`unit`):** принудительный throw на первом из нескольких patch unions
сохраняет последнюю успешную body, позволяет обработать следующий patch и не
меняет `paperGeom`; основной обязательный boolean throw по-прежнему даёт
`null`.
- **AC4 (`unit`):** удаление и слияние микро-интервала не используются как
лечение: regression проходит с исходными 25 wall records; входные `rooms`,
`walls`, `openCuts`, `openings` и `extraBodies` после вызова побайтно
эквивалентны исходным.
- **AC5 (`unit`):** перестановка room/wall records, обратное направление
эквивалентных segments и повторный вызов дают одно множество без исключения;
geometry сравнивается symmetric difference/area, а не строковым порядком rings.
- **AC6 (`unit`):** существующие матрицы #123/#141/#150/#172, nested/courtyard,
openings, open spans и independent extras остаются зелёными; валидные обычные
junction fixtures геометрически не меняются за пределами numeric tolerance.
- **AC7 (`smoke`):** targeted production-bundle smoke загружает полный fixture и
подтверждает непустой masonry path в Plan, View, kiosk/static и hidden Iso,
одинаковый structural fingerprint и отсутствие write после render.
- **AC8 (`smoke`):** тот же smoke проверяет clean floor/paper и Glow/source/sun
consumers: кладка не исчезает при переключении HA state, светлой/тёмной темы и
режима отображения; topology не пересчитывается от обычного state tick.
- **AC9 (`golden`):** детерминированная сцена полного плана показывает кладку и
T-стык в Plan и View минимум в dark theme. Baseline принимается только из
просмотренного полного Linux artifact через `golden:accept -- --reviewed`.
- **AC10 (`ревью кода`):** отсутствуют renderer-specific fallback, grid snapping
физических offsets, schema/backend/i18n изменения и исправления #198/#199;
локальный catch не скрывает основной structural failure.
- **AC11 (`typecheck` + `unit` + `build` + `smoke`):** локальный gate зелёный,
целевой smoke выполнен до S7, три bundle-копии после build побайтно одинаковы.
## 12. План автотестов и гейтов
### Unit
В `test/wall-thickness.test.mjs` либо отдельном узком pure-geometry файле:
1. добавить полный issue fixture без ручного сокращения и вызвать production
signature `wallBodiesGeometry()`;
2. проверить patch count/finite vertices/bounds и numeric-equivalent variants;
3. внедрить контролируемый отказ одного patch union через узкий pure helper либо
другую тестируемую границу, не подменяя production algorithm;
4. проверить продолжение после отказа и отдельно общий structural fail-dark;
5. проверить input immutability, repeatability и permutation invariance;
6. прогнать существующие wall junction/thickness/opening/floor/light regressions.
Минимум один новый тест обязан уметь падать: возврат старого незащищённого цикла
должен снова дать `null` на полном fixture. Ревьюер проверяет эту мутацию или
эквивалентное доказательство.
### Targeted browser smoke
`node demo/smoke_junction_patch_resilience.mjs` либо эквивалентный явно названный
scenario:
1. загружает поставляемый production bundle и fixture issue;
2. снимает signatures masonry/paper/floor в Plan, View, static и hidden Iso;
3. проверяет dark/light theme и HA light-state update без исчезновения paths;
4. проверяет Glow/source guard и доступный sun-occluder contract;
5. подтверждает отсутствие config/layout write.
Этот targeted smoke выполняется локально перед `S7-code-review`. Полный набор
smoke не запускается в implementation loop.
### Golden и pre-release
Golden-сцена может расширить существующий wall-junction scenario, если полный
fixture и глобальное сохранение кладки читаются однозначно. Локальное принятие
baseline ради зелёной ветки запрещено. Full smoke, golden capture/verify,
performance и Linux Validate выполняются перед бетой на точном SHA по
`PROCESS.md`.
### Обязательный implementation loop
```text
npm run typecheck
npm test
npm run build
сравнение трёх bundle-копий
node demo/smoke_junction_patch_resilience.mjs
```
Python/HA harness не нужен для локального gate: backend не меняется; полный
Linux harness остаётся release gate.
## 13. Производительность и безопасность
Numeric normalization и один локальный `try/catch` не меняют асимптотику.
Количество patches, boolean passes и structural cache invalidation остаются
прежними. HA states, theme, hover и animation tick не входят в geometry key.
Отдельный budget не добавляется. Перед бетой существующие large-house и Full
Performance должны пройти без ослабления порогов; точный fixture #197 добавляется
в performance только если измерение покажет отдельный значимый профиль.
Security/privacy влияние отсутствует: нет HTML/CSS ввода, сетевых запросов, HA
services, новых permissions или пользовательских данных. Полный fixture уже
анонимизирован; diagnostic output не должен печатать исходный config.
## 14. Риски и снижение
| Риск | Вероятность / ущерб | Снижение |
|---|---|---|
| Слишком грубая нормализация изменит толщину или mitre | средняя / высокий | tolerance значительно меньше geometry epsilon; exact bounds и symmetric-difference tests |
| Grid rounding сломает диагонали и half-depth | средняя / высокий | явный запрет grid snapping; matrix разных `coordScale` и толщин |
| Catch скроет основной отказ и вернёт опасную geometry | средняя / высокий | catch только вокруг одного optional patch; отдельный core-failure test |
| Plan исправится, а light/Iso продолжат использовать другой body | низкая / высокий | один canonical result и consumer smoke AC7/AC8 |
| Перестановка records снова вызовет polyclip failure | средняя / высокий | permutation + ulp matrix на полном fixture |
| Golden примет массовый anti-aliasing diff | низкая / средний | узкая сцена, просмотр полного Linux artifact, без локального auto-accept |
| Попутно изменится optimizer | низкая / высокий | явный non-scope и отдельные #198/#199 |
## 15. Откат
Откат — revert одного user-visible implementation commit вместе с тестами,
документацией, changelog и bundle-копиями. Persisted schema и данные не меняются,
поэтому migration/cleanup не нужны. После отката возвращается риск полного
исчезновения кладки на исходном fixture.
Feature flag не добавляется: это восстановление обязательного fail-closed
контракта общей физической геометрии, а не экспериментальная функция.
## 16. Release-артефакты
Изменение пользовательское. Implementation-коммит имеет `User-Visible: yes` и
в том же коммите обновляет:
- `docs/CHANGELOG.md` и `docs/CHANGELOG.ru.md` со ссылкой на #197;
- `docs/WALL-THICKNESS.md` — per-piece junction failure и numeric normalization;
- `docs/ARCHITECTURE.md` — граница optional patch против core structural failure;
- `docs/USER-GUIDE.ru.md` — диагностика исчезнувшей кладки после Optimize;
- `docs/TESTING.md` — unit/smoke/golden coverage #197;
- `docs/STATUS.md` — фактическая реализованная release-линия;
- unit fixture, targeted smoke и golden scenario/candidate;
- `dist/houseplan-card.js`,
`custom_components/houseplan/frontend/houseplan-card.js`,
`demo/srv/assets/houseplan-card.js` после build.
Screenshots вне golden не требуются. Backend, migration, i18n, отдельный security
report и отдельный performance budget не требуются. Issue закрывается только
после включения в опубликованную бету.
## 17. Принятые технические предположения — можно менять без продуктового ревью
1. Рекомендуемый numeric step — величина порядка `coordScale × 10⁻¹²` либо
эквивалентная relative/ULP-нормализация, доказанная AC2; точное имя helper и
коэффициент не являются продуктовым контрактом.
2. Нормализуются только junction patch vertices непосредственно перед boolean
boundary; persisted rooms/walls/cuts и основные room profiles не округляются.
3. Patch с не-конечными координатами или ничтожной площадью считается локально
непригодным и проходит тот же isolated fallback.
4. Если нормализованный patch всё равно вызывает исключение, он пропускается;
последующие patches и обязательные passes продолжаются.
5. Для текущего полного fixture пропуск и успешный стабилизированный union после
final exterior clipping дают одинаковую площадь; тест всё равно проверяет
успешную нормализацию, чтобы catch не был единственным лечением.
6. Инъекция отказа в unit может быть реализована через небольшой pure helper,
test-only boolean adapter или эквивалент без публичного runtime API.
7. Имена smoke/golden scenarios и раскладка test fixture свободны, если связь с
AC и возможность красного прогона сохраняются.
8. Нет открытых продуктовых вопросов; смежные решения вынесены в #198 и #199.