21 KiB
CODE-REVIEW-397-r2
- Issue: #397 — «Undo позиций #74: своё же эхо сбрасывает историю, а доказывающий это смок подстроен»
- ТЗ:
docs/specs/397-device-position-echo.md(ревизия 2, принята SPEC-REVIEW-397-r1/r2 → зелёным) - Этап: code, заход r2, блокирующих циклов израсходовано 1 из 4
- r1:
docs/reviews/CODE-REVIEW-397-r1.md, вердикт жёлтый, SHA08d56122(Medium-1: AC3 не имел теста; Low-1, Low-2 — не блокировали) - Диапазон этого раунда:
git diff 08d56122..HEAD(HEADf4c66bed). Единственный продуктовый коммит раунда —f4c66bed(«test: prove AC3 on the reconnect path, with a probe that can fail»);b17f4167— сам документ r1, артефактов не меняет.
Почему разбор сужен до дельты, а не полный
Дельта раунда — 26 добавленных / 3 удалённые строки в одном файле
(demo/smoke_device_position_history.mjs), только фикстура. Продуктовый код
(src/houseplan-card.ts) в этом раунде не менялся — фикс d87cc298 тот же,
что проверялся в r1. Ребейза на dev не было (git merge-base HEAD origin/dev
не менялся между раундами — диапазон origin/dev..HEAD идентичен по базе),
контракт поведения не менялся, новая подсистема не задета. Условия §2.9 для
полного разбора не выполняются → разбор по AC ограничен AC3 (предмет
Medium-1) и тем, до чего дотягивается сама дельта — общими для нескольких AC
частями фикстуры, которые дельта тоже трогает (см. ниже).
Скоуп проверки дельты
demo/smoke_device_position_history.mjs:
- Новый обработчик
houseplan/config/getв фейковомcallWS— эхоc._serverCfg/c._cfgRev, чтобы_adoptStructuralResponsesне отличилconfigChangedпо посторонней причине и проверка отвечала только за layout-вопрос AC3. echoProbe-позиция сменена с канонической{x:0.42, y:0.42}на заведомо неканоническую{x:0.024999999999999942, y:0.7/7}— та же пара, что уже использовалась в ТЗ как пример расхождения канонизации (39 из 115 видаpx/800*0.997+0.0013расходятся;0.024999999999999942 → 0.025).- Новая проверка
out.reconnectKeepsHistory: после перемещения маркера —await c._loadFromServer()(полный reconnect-путь, читает ОБА ответа) —canUndoдолжен остатьсяtrue.
Это ровно то, что просило Medium-1 из r1: доказательство для
_adoptStructuralResponses через _loadFromServer, а не только через
_reloadLayoutOnly.
Поскольку echoProbe-позиция теперь общая для трёх проверок в одном блоке
(AC3 reconnectKeepsHistory, AC5б deleteEchoKeepsHistory, AC7
inFlightPositionWinsTheMerge), дельта фактически затрагивает доказательства
всех троих — разбор по коду и прогон распространён на них тоже, а не только
на новую строку.
Как проверялось
Зелёного Validate на f4c66bed нет — все гейты прогнаны локально.
- Чтение диффа.
git diff 08d56122..HEAD -- demo/smoke_device_position_history.mjsпострочно сверен с текстом Medium-1 и с планом автотестов ТЗ (пункт 5). - Чтение вызываемого пути.
_loadFromServer(src/houseplan-card.ts:4283-4369) действительно шлётPromise.all([houseplan/config/get, houseplan/layout/get])и передаёт оба ответа в_adoptStructuralResponses(cfgResp, layResp)(:4320) — то есть новая проверка идёт через второй путь, который в r1 был не покрыт, а не через_reloadLayoutOnlyещё раз. Прочитан также_adoptStructuralResponses(:4203-4269):configChangedсчитается поcontentFingerprint(nextConfig)vs_cfgContentFingerprint— при эхеc._serverCfgбез изменений этоfalse, значит очистка истории поconfigChanged(реальная, но не относящаяся к AC3 причина) исключена, и единственная переменная в проверке — расхождение_layoutиз B3. - Дешёвые гейты, все на
HEAD(f4c66bed), послеnpm run bundle:sync:npx tsc --noEmit— чисто.npm test— 1657 pass / 0 fail / 1 skip (совпадает с r1; продуктовый код не менялся, расхождения не ожидалось).npm run build— зелёный;git status --shortпосле сборки пуст → три копии бандла (dist/,custom_components/houseplan/frontend/,demo/srv/assets) совпадают с закоммиченными.node scripts/check-docs.mjs— не прогонял: дельта не трогаетsrc/**(толькоdemo/**), условие обязательности гейта не выполняется.npm run invariants/python -m pytest tests_backend— не применимы: геометрия, рёбра,layout-ключи решётки иcustom_components/**/*.pyдельтой не затронуты.npm run golden:verify— не применим: диф не меняет рендер/геометрию/ стили/слои, только служебную фикстуру теста.
node scripts/smoke-select.mjs --base 08d56122 --head HEAD— вывод: «Исполняемого frontend-диффа нет (src/**/*.tsне тронут). Browser-smoke этим диффом не выбираются — это не "пропустить проверки", а "выбирать нечего": смоки проверяют собранную карточку. Тронуто файлов: 2.» Полную матрицу (209 файлов) не гонял: инструмент прямо говорит, что выбирать нечего, диф — только фикстура одного уже названного АС3 смока, продуктовый код не менялся.node demo/smoke_device_position_history.mjs— прогнан явно наHEAD: все 31 проверка зелёные, включая новуюreconnectKeepsHistory: true.- Дисциплина «тест умеет падать» — проверена вручную, не принята на
слово автора. В
_persistDevicePlacement(src/houseplan-card.ts:5246-5250) временно заменилнаconst pos = canonicalizePosition(this._layout[deviceId]); if (contentFingerprint(pos) !== contentFingerprint(this._layout[deviceId])) { this._layout = { ...this._layout, [deviceId]: pos }; } pending = pos;(тот же мутант, что описан вconst pos = canonicalizePosition(this._layout[deviceId]); void pos; pending = pos;device-echo-keeps-local-noncanonical), пересобрал (npm run bundle:sync) и перезапустил смок. Результат:Совпадает буквально с заявлением автора в тексте коммита («five checks red… including reconnectKeepsHistory and deleteEchoKeepsHistory»).FAILED (5): - localCopyEqualsTheWire: expected true, got false - sameContentReloadKeepsHistory: expected true, got false - ownEchoMatchesWhatWentOverTheWire: expected true, got false - reconnectKeepsHistory: expected true, got false - deleteEchoKeepsHistory: expected true, got falsedeleteEchoKeepsHistoryкраснеет не потому, что ветка удаления сама сломана — а потому, что предшествующий_loadFromServer()в этом же прогоне уже обнулил_devicePositionHistory(расхождение реальное,canUndoостаётсяfalseдо конца блока); при исправленном коде тот же вызов ничего не чистит, иdeleteEchoKeepsHistoryпроверяет свою собственную ветку независимо — это подтверждено зелёным прогоном на восстановленном коде (см. п.5). Каскад ожидаем и не обесценивает проверку. После теста откатил файл (git checkout -- src/houseplan-card.ts,git status --shortснова пуст) и пересобрал (npm run bundle:sync), вернув дерево в состояниеHEAD; финальный прогон смока — снова все 31 зелёные (см. «Что проверено и корректно»). - Трейлеры.
git show f4c66bed—Issue: #397,User-Visible: no. Корректно: изменение затрагивает только тестовую фикстуру, видимого поведения не меняет (сам видимый эффект уже описан в CHANGELOG коммитомd87cc298из r1). Отдельной правки CHANGELOG не требуется и не добавлено.
Закрытие раунда r1
| Находка r1 | Чем закрыта | Где это видно |
|---|---|---|
Medium-1: AC3 не имел теста — _adoptStructuralResponses/_loadFromServer не упоминались ни в одном изменённом файле |
demo/smoke_device_position_history.mjs теперь вызывает await c._loadFromServer() сразу после записи неканонической позиции и пишет out.reconnectKeepsHistory; серверный конфиг в фейковом WS зеркалит c._serverCfg, чтобы исключить configChanged как постороннюю причину очистки |
demo/smoke_device_position_history.mjs:296-304 (проверка), :37-42 (мок config/get); падение подтверждено вручную (раздел «Как проверялось», п.6) — 5 красных, включая reconnectKeepsHistory |
Low-1: AC1 доказан не тем способом, что назвало ТЗ (браузерный смок + текстовый юнит вместо юнита с перехватом callWS) |
Не тронуто в этом раунде | Оставлено на усмотрение автора, как и предлагал r1; не блокирует |
| Low-2: план ТЗ (риски, п.3) просил отдельный юнит на «каноническая позиция не создаёт лишней записи» — теста нет, только код-гвард | Не тронуто в этом раунде | То же; гвард в коде не менялся и остаётся корректным (см. «Унаследовано») |
Low-1 и Low-2 не были предметом Medium и не требовали правки в этом раунде; формально они остаются открытыми, но не блокируют — решение об их закрытии (комментарием или правкой) остаётся за автором, как и было сказано в r1.
Разбор по AC (только то, чего касается дельта)
- AC3 (тот же инвариант для
_adoptStructuralResponses, reconnect-путь) — выполнено. Доказано смокомreconnectKeepsHistoryчерез реальный_loadFromServer(); падает без фикса, зеленеет с фиксом (проверено вручную, не на слово). Пункт 5 плана автотестов ТЗ закрыт. - AC5б (эхо удаления не чистит историю) — уже был «Выполнено» в r1;
затронут дельтой (та же
echoProbe-позиция, тот же блок). Перепроверено: зелёный наHEAD, независимо подтверждает свою ветку (без искусственной зависимости от AC3 при исправленном коде — см. п.6 «Как проверялось»). Остаётся выполненным. - AC7 (запись в полёте побеждает ответ сервера) — уже был «Выполнено» в
r1; использует тот же
echoProbe, но собственные значения позиции (inFlightPos = {x:0.63,y:0.21}), не изменённые этой дельтой. Перепроверено прогоном — зелёный,inFlightPositionWinsTheMerge: true. Остаётся выполненным.
Унаследовано из r1
Документ: docs/reviews/CODE-REVIEW-397-r1.md, SHA 08d56122. Принято без
повторной проверки в этом раунде (продуктовый код не менялся):
- AC1 — порядок операций в
_persistDevicePlacement, локальная запись канонического значения раньше_sentPos.setи раньшеcallWS. Юнитtest/device-position-echo.test.mjsи смокlocalCopyEqualsTheWireне тронуты дельтой, продуктовый код (src/houseplan-card.ts:5226-5261) тоже — оснований перепроверять нет. - AC2 (
sameContentReloadKeepsHistory+ownEchoMatchesWhatWentOverTheWire) — файл-источник (_reloadLayoutOnly,:4904-4948) не менялся, фикстура в этой части (используетdeviceId, неechoProbe) дельтой не затронута. - AC4 (
remoteContentClearsHistory) — то же самое, вне блокаechoProbe, код и фикстура в этой части не менялись. - AC5а (
applyDevicePlacement(layout, id, null)удаляет ключ до фиксации отпечатка) — юнит-доказательство, продуктовый код не менялся. - AC6 (смок краснеет на коде до фикса) — доказательство r1 относилось к прежней версии смока (3 красных проверки); в этом раунде переисполнено заново на новой версии смока (см. «Как проверялось», п.6, 5 красных проверок) — это не наследование, а повторное независимое доказательство, что фиксирую отдельно, чтобы не создавать видимость слепого доверия там, где дельта как раз и меняет предмет AC6.
- Мутант
device-echo-keeps-local-noncanonical, регистрация вscripts/mutation-gate.mjs, согласованность сtest/mutation-gate.test.mjs— файлы не в дельте, не перепроверялись. - Трейлеры и CHANGELOG коммита
d87cc298— не в дельте, r1 подтвердил. - Бандл-артефакты, актуальность скриншотов на момент
08d56122— вне дельты по скриншотам; актуальность наHEADподтверждена заново в этом раунде (см. «Как проверялось», п.3 —git status --shortпуст после пересборки), а не унаследована слепо, так как сборка в любом случае перезапускалась для falsification-теста.
Что проверено и корректно
houseplan/config/get-мок зеркалитc._serverCfg/c._cfgRevбез изменений →configChangedв_adoptStructuralResponsesостаётсяfalseна протяжении всего сценария AC3, значитreconnectKeepsHistoryотвечает ровно на layout-вопрос, а не на посторонний config-вопрос.- Проба
{x:0.024999999999999942, y:0.7/7}— действительно неканоническая (снапается на0.025/0.1), то есть чек не мог бы проходить «по случаю», как это было с прежним{0.42,0.42}(M1-класс дефекта, но для AC3, а не только для AC2, — и он же теперь закрыт заодно). - Финальный прогон смока на восстановленном
HEAD— 31/31 зелёных, включаяreconnectKeepsHistory,deleteEchoKeepsHistory,inFlightPositionWinsTheMerge. - Дерево репозитория после ручного falsification-теста восстановлено:
git status --shortпуст,git diffпуст. - Трейлеры
f4c66bed:Issue: #397,User-Visible: no— корректно для теста-only коммита.
Чего не проверял и почему
- GitHub-комментарии к issue #397 — MCP-инструмент
get_issue_commentsотклонён средой (permission not granted) на трёх попытках подряд; вердикт и контекст r1 восстановлены по коммитам и по самому документуCODE-REVIEW-397-r1.md, чего достаточно для этого раунда — SHA и находки там названы явно. Если в комментариях есть более свежее решение по Low-1/ Low-2 (например, автор их уже закрыл словом), эта информация не попала в документ; на вердикт это не влияет, так как Low не блокирует независимо от того, закрыт он явно или нет. node scripts/check-docs.mjs— дельта не трогаетsrc/**, условие обязательности гейта не выполняется.npm run invariants,python -m pytest tests_backend— геометрия,layout-ключи решётки иcustom_components/**/*.pyне затронуты ни этой дельтой, ни диапазоном в целом (r1 это тоже отметил).npm run golden:verify— диф не меняет рендер/геометрию/стили/слои.- Полная матрица
demo/smoke_*.mjs(209 файлов) —smoke-select.mjsпрямо ответил «выбирать нечего» (диф внеsrc/**); диф — фикстура одного уже названного АС смока, поверхность не расширена. scripts/mutation-gate.mjs(полный/точечный) — не в дельте, не перезапускал; r1 уже подтвердил регистрацию и согласованность (test/mutation-gate.test.mjs, 10/10) на неизменном с тех пор файле.- HA-харнесс — в песочнице нет Home Assistant; предрелизный гейт, не гейт ревью.
Вывод
Medium-1 закрыт доказательно (не на слово автора — falsification
воспроизведена и совпадает буквально: 5 красных проверок, включая
reconnectKeepsHistory). AC3 выполнен. Побочно исправлена скрытая слабость
доказательства AC5б (прежняя каноническая проба 0.42 не могла бы поймать
регрессию своей же ветки — теперь может, за счёт общей неканонической
echoProbe-позиции). Low-1 и Low-2 остаются открытыми, но не блокирующими,
как и в r1. Продуктовый код не менялся, три копии бандла синхронны,
трейлеры корректны, User-Visible: no — CHANGELOG не требуется. Находок в
скоупе задачи, требующих новой правки, нет.
Вердикт: зелёный.