mirror of
https://github.com/Matysh/houseplan-card
synced 2026-10-04 05:41:34 +00:00
285 lines
30 KiB
Markdown
285 lines
30 KiB
Markdown
# CODE-REVIEW-291-r1
|
||
|
||
- **Issue:** #291 «Барьер записи координат: убрать шум как явление»
|
||
- **Этап:** код-ревью (PROCESS.md §2.7), заход **r1** (первый заход код-ревью;
|
||
ревью ТЗ прошло отдельно, 3 захода, зелёное на r3 — сюда не засчитывается,
|
||
§10.4 «цикл считается по этапу»)
|
||
- **Диапазон:** `10fa0dc7..81d2a801` (merge-base с `origin/dev` .. HEAD ветки
|
||
`issue/291-lattice-coordinate-barrier`), 10 коммитов, 37 файлов, +2374/-328
|
||
- **ТЗ:** `docs/specs/291-lattice-coordinate-write-barrier.md`, зелёное ревью
|
||
`SPEC-REVIEW-291-r3.md`
|
||
- **Ревьюер:** свежая сессия, без контекста реализации; артефакты читаны из
|
||
ветки, а не со слов автора
|
||
|
||
## Скоуп
|
||
|
||
Единый барьер записи, приводящий координаты, лежащие ближе `1e-4` шага сетки
|
||
к узлу `k/240`, к точному double этого узла — во frontend и backend, в layout
|
||
и config, с отдельным отчётом Optimize и защитой от обхода. Не входит: смена
|
||
схемы хранения, #288/#289/#290, union-алгоритм #278, снап авторской off-grid
|
||
геометрии. Это первый код-ревью раунд — предмет разбора полный, дельты по
|
||
предыдущему раунду нет.
|
||
|
||
## Как проверялось
|
||
|
||
Гейты по PROCESS.md §8, соразмерно задаче (диапазон трогает `src/**`,
|
||
`custom_components/**/*.py`, геометрию и ссылки на неё):
|
||
|
||
| Гейт | Команда | Результат |
|
||
|---|---|---|
|
||
| Typecheck | `npx tsc --noEmit` | зелёный |
|
||
| Unit | `npm test` | 1232 passed, 1 skipped, 0 failed |
|
||
| Build + bundle parity | `npm run build && npm run bundle:sync` + `cmp` трёх копий | `dist/`, `custom_components/houseplan/frontend/`, `demo/srv/assets/` побайтово идентичны |
|
||
| Docs fingerprint | `node scripts/check-docs.mjs` | «Documentation checks passed (7 files, 10 external links)» — обязателен, diff трогает `src/**` |
|
||
| Model invariants | `node scripts/model-invariants.mjs --config <real-plan-first-floor.json как config> --lattice` (сам, до и после `canonicalizeConfigGeometry`) | до барьера: шум 66.15% (127/192), совпадает по порядку с измерением issue (65.38%/78.77%); после барьера: **шум 0 (0.00%)**, законно вне сетки 2 (1.04%) — воспроизводит формат AC3 буквально; второй прогон канонизации байт-в-байт идентичен первому (AC1/AC4 idempotence на живых данных, исполнено, не только прочитано) |
|
||
| Model invariants (violations) | `node scripts/model-invariants.mjs --config <migrated>` | «Инварианты выполнены»; 2 наблюдения (exact-endpoint fallback) — не нарушения, не относится к #291 |
|
||
| Mutation gate (структурная проверка) | `node scripts/mutation-gate.mjs --check` | все 180 патчей (включая 6 новых lattice-мутантов) применяются к текущему коду ровно один раз |
|
||
| Mutation gate (полный прогон, выборочно) | `--id=lattice-round-truncates`, `--id=frontend-writes-raw-coords` | оба: мутант применён → именованный тест **покраснел**, гейт зафиксировал ловлю. `--id=optimize-config-storage-half-raw` (backend-мутант, переиспользованный из #248 для «direct backend Store bypass») упал на отсутствии `pytest`/`homeassistant` в этой среде — не показатель дефекта, показатель среды (см. «Не проверял») |
|
||
| Performance (AC11) | `node demo/benchmark_coordinate_write_barrier.mjs` | ratio 0.666 (лимит 1.2) — независимый локальный прогон, не только цифра автора (0.823, тоже под лимитом) |
|
||
| Smoke — выбор | `node scripts/smoke-select.mjs --base origin/dev --head HEAD` | 3 «прямых совпадения» (`cellCm`) + 1 «зарегистрированная связь» (`repairSpaceReferences`) — вывод приложен ниже |
|
||
| Smoke — по AC | `node demo/smoke_lattice_write_barrier.mjs`, `node demo/smoke_optimize_coordinate_canonicalization.mjs` | оба **OK**, все под-проверки true |
|
||
| Smoke — по выбору | `smoke_decor`, `smoke_grid_scale_invariance`, `smoke_space_scale_defaults`, `smoke_orphan_space_references` | все **OK** |
|
||
| Backend | `python -m pytest tests_backend/test_coordinate_canonicalization.py -q` | **не выполнено** — в среде ревью нет `homeassistant` (`ModuleNotFoundError`), см. «Не проверял» |
|
||
| Golden | не прогонялся | сознательное решение, см. «Не проверял» |
|
||
| Полный набор смоков (182 файла) | не прогонялся | вне объёма код-ревью (PROCESS.md §8: «полные наборы — предрелизный гейт») |
|
||
| Process gate | `node scripts/process-gate.mjs --github-range --issues` локально на HEAD | «гейт пройден, предупреждений 0» |
|
||
|
||
Вывод `smoke-select.mjs`:
|
||
|
||
```
|
||
Изменено файлов src/**: 3 · символов проекта на изменённых строках: 26
|
||
Прямое совпадение (3): smoke_decor.mjs ← cellCm; smoke_grid_scale_invariance.mjs ← cellCm;
|
||
smoke_space_scale_defaults.mjs ← cellCm
|
||
Зарегистрированная связь (1): smoke_orphan_space_references.mjs ← repairSpaceReferences
|
||
```
|
||
Все четыре прогнаны (таблица выше), все зелёные.
|
||
|
||
### CI на ветке — что нашёл и как учёл
|
||
|
||
Validate на коммите с реализацией (`9a42c856`, run 32732620838) завершился
|
||
**cancelled**, с `process-gate: failure` и `docs: failure`; `golden`/`smoke`/
|
||
`performance_smoke` не завершились (`cancelled`), потому что следующий пуш
|
||
(`81d2a801`) отменил незавершённый прогон (штатное поведение
|
||
`concurrency.cancel-in-progress`, AGENTS.md). Разобрал каждую причину:
|
||
|
||
- `docs: failure` — ожидаемо, отпечаток скриншотов был устаревшим; закрыто тем
|
||
же следующим коммитом (`docs: accept screenshots for lattice barrier`);
|
||
- `process-gate: failure` — `git cat-file -e 89f8b581...^{commit}` не находит
|
||
объект: `BEFORE_SHA` ссылался на коммит, переписанный до этого пуша
|
||
(амend/форс на личной ветке задачи, что процессом не запрещено). Транзиентная
|
||
инфраструктурная причина, не нарушение правил — на финальном HEAD
|
||
`process-gate` зелёный и в CI (run 32732834660), и в моём локальном прогоне;
|
||
- `golden`/`smoke`/`performance_smoke` для финального HEAD в CI **не
|
||
запускались вовсе** — на ветках задач `changes`-job фильтрует по diff
|
||
последнего пуша (а не всей ветки), и пуш `81d2a801` тронул только
|
||
`docs/images/**`. Это осознанный дизайн (`validate.yml`: «настоящую приёмку
|
||
там делает код-ревью, которое гоняет гейты само, #127») — то есть именно
|
||
код-ревью (эта сессия), а не CI, отвечает за targeted-гейты, что я и сделал
|
||
выше.
|
||
|
||
Ссылка автора в хендоффе («Linux backend … зелёный in Validate 32732620838»)
|
||
точна для job `backend`, но не упоминает, что сам прогон был `cancelled` и
|
||
два job упали — это не искажение факта, но неполная картина. Отмечаю как
|
||
наблюдение, не как находку: обе причины разобраны и не относятся к
|
||
поведению #291.
|
||
|
||
## AC — построчно
|
||
|
||
| AC | Доказательство | Вердикт |
|
||
|---|---|---|
|
||
| AC1 Идемпотентность/точные биты | `test/coordinate-canonicalization.test.mjs`: цикл по 4801 узлам, идемпотентность, `-0`→`+0`, `0.06`/`0.2875` не снапятся, граница `1e-4` в обе стороны — плюс мой прогон на живой фикстуре (см. таблицу) | ✅ доказано автотестом + исполнением |
|
||
| AC2 Shared allow-list parity | одна fixture (`test/fixtures/coordinate-canonicalization.json`), deep-equal TS/Python (`test_python_and_frontend_share_the_scalar_lattice_fixture_contract`), input immutable | ✅ доказано автотестом (TS выполнен; Python — проверено чтением, см. «Не проверял») |
|
||
| AC3 Живой профиль после migration = 0 | `test/model-invariants.test.mjs`: `real-plan-*.json` clone → `profile.noise===0`, `far===measured.far`, `checkWallKeys`/`checkMixedRoleRecords` = 0, raw file byte-equal до/после — плюс мой независимый прогон CLI на `real-plan-first-floor.json` (66.15%→0.00%, идемпотентность подтверждена вторым прогоном) | ✅ доказано автотестом + исполнением |
|
||
| AC4 Произвольная editing session не возвращает noise | **см. находку M1** — нет production-bundle smoke, который бы рисовал/продолжал Walls chain, делал Resize, ставил opening/partition/column/decor, двигал marker/room label и проверял `latticeProfile.noise===0` после каждого шага, как требует буква AC | ⚠️ не доказано ни тестом, ни явной записью «проверено чтением» — структурно вероятно верно (AC5+AC1), но не показано |
|
||
| AC5 Write barrier нельзя обойти | `test/coordinate-write-barrier-guard.test.mjs` (инвентарь всех 4 frontend-writer'ов + 2 backend Store boundary в `store.py`); мутанты `frontend-writes-raw-coords` (#224, переиспользован) и `optimize-config/layout-storage-half-raw` (#248, переиспользован) целятся именно в bypass; первый прогнан мной полностью и покраснел как положено | ✅ доказано автотестом (frontend выполнено; backend-мутант не выполнен, см. «Не проверял») |
|
||
| AC6 Source fixtures остаются шумными | `test('барьер очищает клоны реальных планов, не переписывая source fixtures (#291)')`: `readFileSync` до/после теста байт-в-байт равны; `profile.noise >= 100` пин остаётся | ✅ доказано автотестом, выполнено |
|
||
| AC7 Optimize report и transaction | `smoke_optimize_coordinate_canonicalization.mjs` (17/17 true, выполнено мной), `test/plan-optimizer.test.mjs` (идемпотентность, `latticeCoordinatesCanonicalized`/`Far` не путается с `coordsCanonicalized`, лёгко проверяемый порядок: барьер применяется **до** `alignAllToGrid`, поэтому align не видит уже исправленный шум и не задваивает счётчик — проверено чтением `plan-optimizer.ts:415-420` и подтверждено тем, что `coordsCanonicalized===0` рядом с `latticeCoordinatesCanonicalized>0` в тесте «issue 273»/«ULP source») | ✅ доказано автотестом + smoke, выполнено |
|
||
| AC8 Compatibility paths | `test_backend_schemas_apply_the_same_allowlist`, `optimize-storage-roundtrip.json`, `280-optimize-rehost-candidate.json` обновлены канонизацией; no schema/version bump подтверждён (модель/сторедж не тронуты) | ✅ проверено чтением + существующими тестами (backend часть не исполнена мной) |
|
||
| AC9 Polyclip/#278 regression | `npm test` включает все 5 тестов `#278 …` (1207-1211), все зелёные; барьер не трогает `physical-geometry.ts`/union | ✅ доказано автотестом, выполнено |
|
||
| AC10 Мутанты | 6 новых lattice-мутантов + 2 переиспользованных (#224/#248) закрывают все 6 категорий буквы AC10 (round→truncate TS+Python, threshold too small/large, layout allow-list omission, unknown-field recursion, frontend writer bypass, backend Store bypass). `--check` зелёный на всех 180; полностью прогнаны мной 2 из 8 релевантных (TS-side); backend-мутант не прогнан (среда) | ✅ доказано структурно + частично исполнением, backend — проверено чтением |
|
||
| AC11 Performance | `demo/benchmark_coordinate_write_barrier.mjs`, ratio 0.666 (я) / 0.823 (автор), лимит 1.2; линейный один проход, без второго deep-clone (проверено чтением `plan-optimizer.ts`: `canonicalizeConfigGeometryInPlace` работает по месту, autor явно закомментировал это решение) | ✅ доказано автотестом/бенчмарком, выполнено |
|
||
| AC12 Локальные гейты | typecheck/test/build/bundle-parity/check-docs — выполнены (таблица выше); backend — не выполнен (среда) | ✅/⚠️ частично, см. «Не проверял» |
|
||
|
||
## Находки
|
||
|
||
### M1 (Medium, в скоупе) — AC4 не доказан ни автотестом, ни явной записью
|
||
|
||
AC4 требует production-bundle smoke, который **последовательно**: рисует/
|
||
продолжает Walls chain → Resize → ставит/двигает opening/partition/column/
|
||
decor → двигает device marker и room label → делает config/layout write,
|
||
reload, export — и после **каждого** commit проверяет
|
||
`latticeProfile.noise === 0`. Ни один смок в диффе (новый
|
||
`smoke_lattice_write_barrier.mjs`, обновлённый
|
||
`smoke_optimize_coordinate_canonicalization.mjs`) этого не делает:
|
||
`smoke_lattice_write_barrier.mjs` подаёт шумные данные напрямую в
|
||
`card._serverCfg`/`card._layout` и проверяет 4 writer-call-site, минуя
|
||
контроллеры инструментов (wall chain, resize, opening/partition/column/decor,
|
||
drag маркера/подписи). Ни одного смока в репозитории, ссылающегося на
|
||
`latticeProfile`, я не нашёл (`grep -rl latticeProfile demo/*.mjs` — пусто).
|
||
|
||
Структурно AC5 (единый барьер, непроходимый) + AC1 (сама функция корректна)
|
||
делают результат весьма вероятным без отдельного прогона — но это не то же
|
||
самое, что показать его для реальных контроллеров, которые формируют
|
||
кандидат разными путями (snap при рисовании, resize-пересчёт, поворот
|
||
проёма). Именно ради этой уверенности AC4 сформулирован отдельно от AC5, и
|
||
именно эту рамку (§7.1: «либо автотест, либо явная запись ревьюера») сейчас
|
||
не закрывает ни тест, ни хендофф-комментарий автора (там нет фразы вида
|
||
«AC4 закрыт составом AC1+AC5, отдельный смок не написан»).
|
||
|
||
**Воспроизведение отсутствия:** `grep -rl "latticeProfile\|noise === 0" demo/*.mjs` → пусто;
|
||
diff не трогает ни один из смоков с реальными жестами (`smoke_wall_chain_*`,
|
||
`smoke_room_resize`, `smoke_opening_preview`, `smoke_free_walls` и т.п.).
|
||
|
||
**Правка:** либо добавить проверку `latticeProfile.noise===0` после
|
||
committed-пар в один из существующих session-смоков (например, расширить
|
||
`smoke_lattice_write_barrier.mjs` реальными жестами через инструменты, а не
|
||
прямой инъекцией в `_serverCfg`), либо явно задокументировать в ТЗ/хендоффе,
|
||
что AC4 закрывается композицией AC1+AC5, и получить с этим согласие
|
||
ревьюера. Второй путь дешевле и, на мой взгляд, содержательно корректен —
|
||
но должен быть **сказан**, а не подразумеваться.
|
||
|
||
### M2 (Medium, в скоупе) — `docs/USER-GUIDE.ru.md` не обновлён
|
||
|
||
Спецификация (§13 «Ожидаемые файлы») называет оба файла:
|
||
`docs/USER-GUIDE.md` **и** `docs/USER-GUIDE.ru.md`. В диффе изменён только
|
||
`docs/USER-GUIDE.md` (добавлен абзац о невидимом floating-point хвосте,
|
||
отчёте Optimize и автоматической защите обычных правок) —
|
||
`git diff origin/dev...HEAD --stat -- docs/USER-GUIDE.ru.md` пуст.
|
||
|
||
RU-версия при этом не «пусто молчит»: раздел 19 уже содержит
|
||
общее описание похожего механизма (устранение «невидимого вычислительного
|
||
шума» при обычном сохранении, разделение строк «обновлено» и «шум устранён»
|
||
в превью), — но это описание существовало **до** #291 (`git blame`:
|
||
2026-08-20/23, до ветки задачи) и не называет ни нового per-space разбиения
|
||
(`gs.optimize_lattice_space`), ни физического максимума с тремя значащими
|
||
цифрами (`formatLatticeShiftCm`) — то, что EN-версия получила явно.
|
||
|
||
Это нарушает не абстрактное «обе версии», а конкретное правило:
|
||
AGENTS.md — «interface wording comes from `docs/USER-GUIDE.ru.md`… or the UI
|
||
starts speaking developer»; PROCESS §2.6 — документация в том же коммите,
|
||
что поведение; коммит помечен `User-Visible: yes` и уже поправил оба
|
||
CHANGELOG (RU+EN) — тот же стандарт должен быть применён и к самому
|
||
руководству пользователя.
|
||
|
||
**Воспроизведение:** `git diff origin/dev...HEAD --stat -- docs/USER-GUIDE.ru.md`
|
||
→ пустой вывод; сравнить с непустым для `docs/USER-GUIDE.md`.
|
||
|
||
**Правка:** перенести/адаптировать на русский абзац, добавленный в
|
||
`docs/USER-GUIDE.md` (раздел 19), с упоминанием per-space строк и
|
||
физического максимума — по терминологии `gs.optimize_lattice_summary`/
|
||
`gs.optimize_lattice_space` из `src/i18n/ru.json`.
|
||
|
||
### Low — не блокируют, зафиксированы без правки
|
||
|
||
- **L1.** Хендофф-комментарий автора цитирует run 32732620838 как «зелёный»
|
||
для backend-job, не упоминая, что сам прогон `cancelled` и два job упали
|
||
(`docs`, `process-gate`) — разобрано выше в «CI на ветке», причины не
|
||
относятся к #291 и с тех пор устранены. Отмечаю как точность цитирования,
|
||
не как дефект реализации.
|
||
|
||
## Что проверено и корректно
|
||
|
||
- Формула `maxShiftCm = shift * LATTICE_GRID_N * cellCm` в
|
||
`coordinate-canonicalization.ts` **не баг**: она буквально повторяет
|
||
уже существующую конвенцию `align-grid.ts:185` (`d * GRID_N * cellCm`) —
|
||
нормализованные координаты записаны в долях полной ширины плана
|
||
(`NORM_W`/`GRID_N=240` шагов на ширину), поэтому перевод в см требует
|
||
именно этого множителя. Проверил перекрёстно чтением обоих файлов.
|
||
- «Одно число — один источник»: `formatLatticeShiftCm(r.latticeMaxShiftCm)`
|
||
в диалоге Optimize (`houseplan-card.ts:16405-16416`) взят из того же
|
||
`OptimizeReport`, что и `Confirm`, и **не** смешивается с
|
||
`d.cm`/`gs.align_count` (обычный grid-align) — разные ключи, разные
|
||
строки, разные источники данных (`lattice.maxShiftCm` vs
|
||
`alignReport.maxShiftCm`), явно разведены и в коде, и в документации
|
||
(`CANVAS.md`: «kept separate from visible `moved/maxShift*`»).
|
||
Единственное смешение — суммарный счётчик в toast `gs.align_done`
|
||
(`m: ... + d.report.latticeCoordinatesCanonicalized + ...`), но это
|
||
легитимная сумма разнородных счётчиков для одной итоговой фразы, а не
|
||
повторное отображение одной и той же величины.
|
||
- Порядок операций в `optimizePlans()`: `latticeCanonicalizationReport()`
|
||
считается **до** `canonicalizeConfigGeometryInPlace`/
|
||
`canonicalizeLayoutGeometryInPlace`, которые в свою очередь идут **до**
|
||
`alignAllToGrid` — отчёт не искажён последующей мутацией, а align не
|
||
задваивает то, что уже поправил барьер (числами подтверждено в тесте
|
||
«Optimize canonicalizes the six-room ULP source…»: `coordsCanonicalized===0`
|
||
рядом с `latticeCoordinatesCanonicalized>0`).
|
||
- `changed` (готовность к записи) считается по финальному
|
||
`persistedConfig` **после** lattice-канонизации — поэтому
|
||
`latticeCoordinatesCanonicalized>0` всегда подтверждает `changed=true`,
|
||
без риска репортить ненулевые счётчики при `changed=false` (читал код,
|
||
логической рассинхронизации не нашёл).
|
||
- Backend-зеркало (`coordinate_canonicalization.py`): `math.floor(scaled +
|
||
0.5)` корректно воспроизводит tie-breaking JS `Math.round` (к
|
||
+∞, включая отрицательные `.5`) — проверено на нескольких контрольных
|
||
значениях вручную (-0.5→0, -1.5→-1, -2.5→-2, совпадает с JS). `store.py`
|
||
не тронут диффом вообще: расширение поведения происходит через уже
|
||
существующий единый вызов `canonicalize_config_geometry`/
|
||
`canonicalize_layout_geometry` внутри `async_save_config_state`/
|
||
`async_save_layout_state` (оба — из #224), что и есть корректный способ
|
||
расширить барьер без создания второй точки входа.
|
||
- Гвард `scripts/coordinate-write-barrier-guard.mjs`: пересчитал руками все
|
||
4 frontend-паттерна (`config/set` ×1, `layout/update` ×2, `localStorage.
|
||
setItem(LS_KEY` ×1) прямым `grep` по `src/houseplan-card.ts` — счётчики
|
||
гварда совпадают с реальным числом occurrences.
|
||
- i18n: оба новых ключа (`gs.optimize_lattice_summary`,
|
||
`gs.optimize_lattice_space`) присутствуют в `en.json`/`ru.json` буквально
|
||
как в §7.1 ТЗ; `test/i18n.test.mjs` проверяет и текст, и точки вызова в
|
||
`houseplan-card.ts`.
|
||
- Трейлеры: все 10 коммитов несут `Issue: #291` и корректный
|
||
`User-Visible: yes|no`; единственный `User-Visible: yes` коммит
|
||
(`9a42c856`) правит оба `docs/CHANGELOG.md`/`docs/CHANGELOG.ru.md` в
|
||
одном коммите.
|
||
- `node scripts/process-gate.mjs --github-range --issues`, прогнан локально
|
||
на HEAD: «гейт пройден, предупреждений 0».
|
||
- Документация подсистемы (`CANVAS.md`, `CONFIG-COMPATIBILITY.md`,
|
||
`ARCHITECTURE.md`, `TESTING.md`) содержательно и точно описывает новую
|
||
границу, терминологию не изобретает, ссылается на реальные имена полей
|
||
отчёта.
|
||
|
||
## Чего не проверял и почему
|
||
|
||
- **Backend pytest** (`tests_backend/test_coordinate_canonicalization.py`,
|
||
включая мутанты `python-lattice-round-truncates`,
|
||
`optimize-*-storage-half-raw`): в среде ревью не установлен
|
||
`homeassistant` (`ModuleNotFoundError`, даже с `pytest` доустановленным) —
|
||
известное и задокументированное ограничение (AGENTS.md: «A full Home
|
||
Assistant harness cannot run on native Windows… locally only the pure
|
||
subset runs»; здесь и чистый subset не собрался, так как модуль
|
||
`virtual_lights` тянет `homeassistant.components.frontend` уже на
|
||
импорте). Заменил чтением: TS/Python реализации построчно
|
||
идентичны по структуре, тест-файл 1:1 зеркалит TS-тест по значениям
|
||
(те же 4801 узлов, те же граничные случаи), а автор сослался на зелёный
|
||
`backend` job в CI (run 32732620838, JSON подтверждён мной через `gh run
|
||
view`) — «проверено чтением, не исполнением» для backend-части AC1/AC2/
|
||
AC5/AC8/AC10.
|
||
- **`npm run golden:verify`**: не прогонял. Diff потенциально меняет
|
||
видимый Optimize-диалог (новые строки отчёта), но
|
||
`test('модели проекта не несут шума решётки (#282)')` (зелёный, в
|
||
`npm test`) доказывает, что все demo-фикстуры, включая
|
||
`golden-geometry`, не содержат lattice-шума — значит новые строки
|
||
(`${r.latticeCoordinatesCanonicalized ? html\`...\` : nothing}`) в
|
||
golden-сценариях `optimize-preflight-dialog-*` не отрenders, и визуального
|
||
расхождения не должно быть. Не стал тратить на это предрелизный гейт;
|
||
если ошибаюсь — `golden:verify` перед бетой это покажет.
|
||
- **Полный набор из 182 браузерных смоков**: не прогонял, кроме выбранных
|
||
6 (2 по AC + 4 по smoke-select) — по правилу §8 «полные наборы — гейт
|
||
предрелиза, не код-ревью».
|
||
- **Полный HA-харнесс / WSL**: недоступен в среде ревью.
|
||
- Не проверял детально совместимость с #290 (уступ 316×1) и #288/#289 —
|
||
их регрессионные тесты в `npm test` прошли без изменений в диффе, а сам
|
||
диф их не трогает; это не отдельная точка риска этой задачи.
|
||
|
||
## Итог
|
||
|
||
High-находок нет. Две находки Medium, обе в скоупе задачи (обе — про
|
||
собственные акцептанс-критерии/список файлов ТЗ, не сторонний код) и обе
|
||
чинятся в этой же задаче без нового issue: M1 (нет production-smoke
|
||
для буквы AC4 либо явного признания замены на структурное доказательство)
|
||
и M2 (`docs/USER-GUIDE.ru.md` не обновлён при `User-Visible: yes`).
|
||
Ядро реализации — сама канонизация, барьер записи, backend-зеркало,
|
||
идемпотентность, защита от обхода, производительность — проверено и
|
||
работает, включая независимое исполнение на реальных (privacy-minimized)
|
||
данных из issue.
|
||
|
||
**Вердикт: жёлтый · заход r1 · блокирующих циклов 1/4 · High: 0 · Medium: 2 → в задаче**
|