Ревью #682 r1, Medium: перенос добавляет документу уровень вложенности (`docs/reviews/X.md` → `legacy/reviews/<тег>/X.md`, `docs/specs/` → `legacy/specs/`), а относительные ссылки внутри перенесённых документов и в соседях, ссылавшихся на них, никто не пересчитывал — на `97d19268` 53 битые ссылки в 46 файлах (заявление «все 26 резолвятся» в `7feb6177` было верно только до переноса документов ревью). Гейты архив не смотрят. `reviews-archive.mjs`: `repairLinks` пересчитывает ссылку, если она не резолвится от нового места, а цель находится от нового или старого места через карту переносов; битая и до переноса ссылка не трогается. `--apply` делает это само, `--repair-links=<rev>` — для всех переименований `<rev>..HEAD`, `--check-links` печатает битые. Этим коммитом `--repair-links=origin/dev` переписал ровно 53 ссылки в 46 файлах; остались две прежние «...»-заглушки в CODE-REVIEW-448-r2 (битые и на dev). Тесты: перенесённый документ, сосед со ссылкой в архив, ТЗ со ссылкой на позже перенесённое ревью, битая-до-переноса не трогается, в `legacy/` битых нет; мутант `reviews-archive-links-from-new-place-only`. PROCESS §2.10 и DEVELOPMENT › Release называют переписывание и `--check-links`. Issue: #682 User-Visible: no Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018qZfe7YS4rqEMKoVeS3GKd
24 KiB
CODE-REVIEW-205-r1 — продолжение следа пылесоса после короткой остановки
- Issue: #205
- Этап:
code(PROCESS.md §2.7) - Диапазон:
origin/dev...HEAD,origin/dev=ad8e7a5,HEAD=11b0283(веткаissue/205-vacuum-trail-grace, detachedHEAD); merge-base —f287bdd - ТЗ:
docs/specs/205-vacuum-trail-resume-grace.md, ревью ТЗ зелёное —SPEC-REVIEW-205-r1.md - Цикл: r1/4
- Ревьюер: Claude, свежая сессия, без переписки с автором реализации
- Вердикт: зелёный
Скоуп ревью
Три коммита диапазона:
f3472de«docs: specify vacuum trail resume grace» — ТЗ (класс C), уже зелёное ревью ТЗ (SPEC-REVIEW-205-r1.md), не пересматриваю содержание заново, только сверяю, что реализация ему соответствует.c47a6bc«docs: review document for #205» — сам документ ревью ТЗ (класс C).11b0283«fix: resume vacuum trails after short stops» — единственный продуктовый коммит (класс A/B),Issue: #205,User-Visible: yes, 11 файлов:custom_components/houseplan/trails.py,demo/smoke_vacuum.mjs,scripts/mutation-gate.mjs,scripts/trail-resume-test-guard.mjs(новый),tests_backend/test_trails.py,tests_backend/test_trail_recorder.py,docs/CHANGELOG.md,docs/CHANGELOG.ru.md,docs/STATUS.md,docs/TESTING.md,docs/USER-GUIDE.ru.md,docs/VACUUM.md.
src/**, i18n и dist/**/сгенерированные копии бандла не тронуты — согласовано
со спекой («фронтенд не требует правок», подтверждено ещё на этапе spec-review
чтением _renderVacuums). Прочитано целиком: docs/SCOPE.md, AGENTS.md,
PROCESS.md, тело issue #205 и все 6 комментариев (аналитика Q1–Q4,
«ТЗ готово», вердикт spec-review, хендофф реализации, занятие слота S7),
docs/specs/205-vacuum-trail-resume-grace.md, SPEC-REVIEW-205-r1.md,
docs/VACUUM.md целиком, весь diff custom_components/houseplan/trails.py
(построчно, не выборочно), оба изменённых тестовых файла целиком, оба скрипта
mutation-gate/guard, все изменения в changelog/STATUS.md/TESTING.md/
USER-GUIDE.ru.md/VACUUM.md.
Как проверялось
Зависимости и Chromium уже установлены средой ревью; npm ci не запускался.
Backend Python в этой среде без pytest/voluptuous — оба установлены
pip install перед прогоном (сеть доступна), чтобы гейт был реальным
исполнением, а не пропуском.
| Гейт | Команда | Результат |
|---|---|---|
| Типы | npx tsc --noEmit |
green |
| Unit | npm test |
912/912 green |
| Сборка + синхронность бандлов | npm run build && cmp dist/houseplan-card.js custom_components/houseplan/frontend/houseplan-card.js && cmp dist/houseplan-card.js demo/srv/assets/houseplan-card.js |
green, обе копии байт-в-байт совпадают (diff не трогал src/**, пересборка ничего не меняет) |
| Whitespace | git diff --check origin/dev...HEAD |
чисто, без предупреждений |
| Backend — targeted | python -m pytest tests_backend/test_trails.py tests_backend/test_trail_recorder.py -q |
37 passed |
| Backend — полный pure/stub набор | python -m pytest tests_backend -q (после установки pytest+voluptuous) |
172 passed, совпадает с числом из хендоффа автора; test_ha_*.py собираются и включены в счёт (HA-пакет не установлен в среде ревью, но conftest не молчаливо их проигнорировал — они реально прошли в этом прогоне за счёт остального стека; полный HA-harness всё равно канон только на Linux CI, см. «Чего не проверял») |
| Целевой смок (назван в AC8) | node demo/smoke_vacuum.mjs |
OK |
| Mutation guard (AC10), точечно | node scripts/mutation-gate.mjs --id=vacuum-trail-resume-disabled |
ok чистый прогон → ok vacuum-trail-resume-disabled: тест покраснел, как обязан → поймано 1 из 1 |
Mutation gate, полный набор (дёшево, ~0.07s, весь список уже мал) |
node scripts/mutation-gate.mjs --check |
все 36 мутантов пойманы, включая новый |
Не прогонялись (соразмерность гейта, PROCESS.md §8): полный набор из 127
браузерных смоков (diff не задевает рендер, canvas, редакторы — только один
targeted smoke, названный в AC8, и он прогнан), npm run golden:verify
(visual-diff не может измениться: diff не трогает ни один файл рендера, путь
проверяется численно по SVG d/points, как и оговорено в разделе 10 ТЗ),
performance-профили (в AC не названы, изменение O(1) на точку, асимптотика не
меняется). Полный HA-harness backend — канонически только Linux CI
(AGENTS.md: .venv-backend недоступен в среде ревью).
Проверка дисциплины «тест умеет падать»
Для AC10 — не поверил заявлению автора «mutant caught (1/1)», а повторил сам:
node scripts/mutation-gate.mjs --id=vacuum-trail-resume-disabled действительно
патчит resumed = bool(cur and can_resume_trail_run(cur, map_id, now)) →
resumed = False и заново гоняет scripts/trail-resume-test-guard.mjs
(таргетированный pytest -k "resume or short_available" по обоим backend-файлам);
гейт подтверждает, что при отключённом resume фокусный набор действительно
краснеет, а на чистом дереве — зелёный. Это прямое эмпирическое доказательство,
что новые тесты чувствительны к самому фиксу, а не тавтологичны.
Построчная проверка кода против контракта ТЗ (раздел 6)
Читал custom_components/houseplan/trails.py целиком, не только diff.
- Пункт 1 (resume при совпадении map_id и
0 ≤ now-ended ≤ 1800).can_resume_trail_runпроверяетrun.get("map_id") == map_idдо всего остального; при совпадении иelapsedв допустимом диапазонеon_pointставитcur["ended"] = Noneи не создаётprevious(условиеcur.get("ended") is not None or cur.get("map_id") != map_idпосле этого ложно) — точное соответствие. - Пункт 2 (граница 30:00 включительно, эпсилон выше — новый run).
elapsed <= TRAIL_RESUME_GRACE_S— нестрогое неравенство,1800.0резолвится вTrue;1800.000001— вFalse. Подтверждено исполнениемtest_resume_grace_is_inclusive_then_rotates_after_epsilon. - Пункт 3 (смена карты — всегда жёсткая граница).
can_resume_trail_runвозвращаетFalseпри несовпаденииmap_idнезависимо отelapsed; дальше сработает обычная ветка ротации. Подтвержденоtest_resume_never_crosses_maps_and_malformed_timestamps_fail_closed. - Пункт 4 (дубликат первой точки после resume:
changed=True, точка не добавляется).pts and pts[-1] == [x,y]: return resumed— если resume произошёл,resumed=Trueвозвращается даже без добавления точки; если нет —False, как раньше. Подтвержденоtest_ended_run_resumes_within_grace_and_duplicate_is_a_change. - Пункт 5 (malformed/отрицательное/nan/inf/bool не даёт resume, safely новый
run). Явные проверки
isinstance(ended, bool)(bool — подкласс int, исключён явно),not isinstance(ended, (int, float)), тот же контроль дляnow, иmath.isfinite(elapsed)после вычитания — отсекаетnan/infи переполнения. Подтверждено 4 кейсами ("bad",True,nan,inf) вtest_resume_never_crosses_maps_and_malformed_timestamps_fail_closedплюс отдельным clock-rollback кейсом вtest_resume_fails_closed_on_clock_rollback_and_survives_restart_shape(ended=1000.0,now=900.0→elapsed=-100→0 <= elapsedложно). - Пункт 6 (idempotent
end_run, timestamp первого stop не сдвигается).end_runизменился сif cur and not cur.get("ended")наif cur and cur.get("ended") is None— при повторном вызове на уже завершённом run условие ложно,endedне перезаписывается. Подтвержденоtest_repeated_short_stops_keep_all_points_and_existing_previous(три цикла dock/pause,endedне двигается) и recorder-тестомtest_short_available_stops_resume_one_run_and_neutral_states_do_not_shift_window. - Пункт 7 (
unavailable/unknown/missing нейтральны, не двигают окно). Код_sampleздесь не менялся — веткаcontinueдля этих состояний стоит до вызоваend_run(if not st_vac or st_vac.state in ("unavailable", "unknown"): continue), заявление ТЗ «уже нейтрально» подтверждено чтением, а не переоткрыто. Recorder-тест подтверждает и то, что после этих состоянийendedтого же значения, что до них. - Пункт 8 (единый контракт для любого available non-moving state).
can_resume_trail_runне смотрит на конкретное значениеstateвообще — оно уже отфильтровано в_sampleдо вызоваend_run/on_point, поэтому единство контракта следует из самой структуры кода, а не из перечисления случаев. Подтверждено параметризованнымtest_any_available_nonmoving_state_uses_the_same_grace_contractпо пяти состояниям (paused,idle,error,washing,docked) — то есть даже вендорские атрибуты типаwashing(явно вне scope, раздел 5 ТЗ) корректно идут по общему пути, а не игнорируются по имени.
Побочная проверка: изменение end_run/on_point с truthy-проверок (not cur.get("ended"), cur.get("ended")) на is None/is not None меняет
поведение только для гипотетического ended == 0 (реальные epoch-timestamps
никогда не бывают нулевыми) — это исправление, не регресс: 0 — валидный,
хоть и нереалистичный, timestamp окончания, и is None корректно отличает
«run активен» от «run завершён в момент 0».
Проверка AC1–AC11
| AC | Статус | Как доказано |
|---|---|---|
| AC1 | Доказан | test_ended_run_resumes_within_grace_and_duplicate_is_a_change, test_short_available_stops_resume_one_run_and_neutral_states_do_not_shift_window — исполнены. |
| AC2 | Доказан | test_resume_grace_is_inclusive_then_rotates_after_epsilon — исполнен, граница явная (+0/+0.001). |
| AC3 | Доказан | test_resume_never_crosses_maps_... (хвост про floor-a/floor-b) — исполнен. |
| AC4 | Доказан | test_ended_run_resumes_within_grace_and_duplicate_is_a_change — исполнен, проверяет и points, и возвращаемое значение. |
| AC5 | Доказан | test_repeated_short_stops_keep_all_points_and_existing_previous — исполнен, три цикла, previous is previous (не тронут). |
| AC6 | Доказан | test_short_available_stops_... (unknown/missing) + test_any_available_nonmoving_state_... — исполнены. |
| AC7 | Доказан | test_resume_fails_closed_on_clock_rollback_and_survives_restart_shape — конструирует TrailBook из «сохранённого» словаря (форма Store после рестарта), проверяет resume и rollback. |
| AC8 | Доказан | node demo/smoke_vacuum.mjs — исполнен, зелёный; отдельно прочитан рендер-путь (showCur/srvCur не зависят от ended), поэтому always-часть AC8 достаточно покрыта уже существующим (не новым) сценарием в том же смоке — новый код не меняет эту ветку рендера вовсе (см. ниже). |
| AC9 | Доказан | Полный прогон tests_backend/test_trails.py + test_trail_recorder.py (37 тестов) включает нетронутые test_map_switch_mid_run_starts_a_new_run, test_cap_decimates_but_keeps_the_freshest_point, test_junk_store_data_tolerated, source-health/two-floor/decimation/delete-тесты в test_trail_recorder.py — все зелёные, без изменений в самих тестах. |
| AC10 | Доказан | node scripts/mutation-gate.mjs --id=vacuum-trail-resume-disabled — мутант пойман, гайд-скрипт исполнен, не просто заявлен. |
| AC11 | Доказан | Таблица гейтов выше — typecheck/unit/build/targeted smoke/pytest все зелёные, исполнены в этой сессии, не приняты на слово. |
Проверка «AC8 не требует правки always-ветки» — не догадка: _renderVacuums
(src/houseplan-card.ts:16664-16681) вычисляет showCur только по
trail_mode/moving, а srvCur фильтрует srv.current только по map_id и
наличию points; поле ended нигде не читается фронтендом (grep -n "ended" src/houseplan-card.ts src/vacuum.ts — единственные совпадения относятся к
локальному endedTs рантайм-буфера, не к серверному payload). Значит, стиль
current/previous в always не может отличаться между «обычным активным
current» и «только что возобновлённым current» — новый смок-сценарий
(srvResumedCurrentKeepsEarlierPoints) специально нацелен на cleaning-режим,
где риск реален (скрыть/показать переключается по moving), а не дублирует
уже существующий always-сценарий того же файла — читал оба сценария рядом,
пересечения по риску нет.
Находки
Ни одной High. Ни одной Medium. Ни одной Low.
Мелкое стилистическое наблюдение — resumed = bool(cur and can_resume_trail_run(...)): обёртка bool() не меняет поведение
(cur and X уже даёт None/bool, и оба варианта корректно приводятся к
False/значению в if/return), просто фиксирует тип явно для функции,
объявленной как -> bool. Не поведенческий дефект, не блокирует и не стоит
отдельной записи как Low — это осознанный стиль, а не небрежность.
Что проверено и признано корректным
- Причина и контракт ТЗ реализованы буквально — каждый из 8 пунктов раздела 6 сверен построчно с кодом и подтверждён исполнением соответствующего теста (см. таблицу выше), не принят на слово ни разу.
- Scope/non-scope соблюдены. Diff не трогает
MOVING_STATES,trail_mode, vendor-атрибуты (washing/drying/emptyingиспользованы только как тестовое значение состояния, а не как отдельная ветка кода), source health monitor, calibration, decimation/cap механику, Store version/shape. Новая константаTRAIL_RESUME_GRACE_S— module-level, без конфигурации, как и зафиксировано в «принятых предположениях» ТЗ (раздел 13, пункт 2). - Тесты умеют падать, не только показывают зелёное. Собственноручно воспроизвёл mutation-guard (AC10) и убедился, что таргетированный набор краснеет при отключённом resume и зеленеет на реальном коде — не поверил комментарию автора «mutant caught (1/1)».
- Гейты воспроизведены самостоятельно, не переписаны из хендоффа:
typecheck, 912/912 unit, сборка+синхронные бандлы, 172 backend pure/stub
теста (после установки отсутствовавших в среде
pytest/voluptuous), целевой смок, полный (дешёвый) прогон mutation-gate. Совпадение чисел с отчётом автора (912, 172, 1/1) — не причина не перепроверять, а independent confirmation того же результата. - Документация и трейлеры.
docs/CHANGELOG.md/docs/CHANGELOG.ru.mdправлены в том же коммите11b0283, что и код (User-Visible: yes) — соответствует правилу №10/PROCESS.md §2.6.docs/VACUUM.mdиdocs/USER-GUIDE.ru.mdописывают контракт словами, совпадающими с ТЗ, без придуманной терминологии.docs/TESTING.mdназывает конкретные файлы доказательства. Веткаissue/205-vacuum-trail-graceсоответствует трейлерамIssue: #205.git diff --checkчист. - Не расширяет скоуп. Единственный продуктовый файл —
custom_components/houseplan/trails.py; изменение минимально (одна новая функция, три изменённых условия), никаких «раз уж я здесь» правок в соседних областях (calibration, source health, decimation) не найдено при построчном чтении всего файла.
Чего не проверял
- Полный набор из 127 браузерных смоков — не запускал; diff не касается
canvas/редакторов/other rendering surfaces, единственный релевантный смок
(
smoke_vacuum.mjs, назван в AC8) исполнен явно. Сужение гейта осознанное (PROCESS.md §8), не молчаливый пропуск. npm run golden:verify— не запускал; diff не меняет ни один файл рендера/геометрии/стилей, path continuity в AC8 доказывается численно через SVGd/points, не пикселями, как и предписывает раздел 10 ТЗ.- Полный HA-harness backend (
test_ha_*.pyс реальным Home Assistant) — в среде ревью нет.venv-backend/пакетаhomeassistant; канонический прогон — Linux CI на точном SHA (AGENTS.md, PROCESS.md §8). Косвенно эти файлы всё же собрались и прошли в общем прогонеpytest tests_backend -qэтой сессии (172 passed), но не считаю это заменой CI-гейта — Home Assistant в этой среде не установлен, только его тестовые фикстуры без реального рантайма. - Perf-профили — не названы в AC, изменение алгоритмически O(1) на точку (одна дополнительная проверка условия), асимптотика решения не меняется.
- Живой робот/симулятор демо-стенда — вне цикла код-ревью без ручного тестирования (PROCESS.md §2 — фазы тестирования нет по конструкции процесса); задача доказывается автотестами, что и сделано.
- Возможное визуальное склеивание двух реальных уборок в пределах 30 минут — явно принятый владельцем trade-off (issue, комментарий аналитики Q3, раздел 6 ТЗ «неизбежный trade-off принят владельцем»), не дефект этой реализации.
Вердикт
Зелёный · цикл r1/4 · High: 0 · Medium: 0 → нет новых issue.
Реализация — узкая, точная правка TrailBook.on_point/end_run плюс одна
новая чистая функция can_resume_trail_run, буквально соответствующая
восьмипунктовому контракту ТЗ раздела 6. Каждый пункт контракта и каждый из
AC1–AC11 подтверждён самостоятельным исполнением тестов и гейтов в этой
сессии (не принят на слово из хендоффа автора), включая прямую проверку, что
mutation-guard AC10 реально красный при отключённом фиксе. Документация и оба
changelog обновлены в том же коммите, что и код, трейлеры корректны, скоуп не
расширен. Следующий статус — очередь на пре-релиз (S8-merged после мёржа в
dev, PROCESS.md §2.7 / AGENTS.md «Do not merge into dev by hand»).