mirror of
https://github.com/Matysh/houseplan-card
synced 2026-09-29 03:09:36 +00:00
committed by
Sergey Matyunin
parent
56e01148f8
commit
edf532e217
@@ -0,0 +1,253 @@
|
||||
# CODE-REVIEW-205-r1 — продолжение следа пылесоса после короткой остановки
|
||||
|
||||
- Issue: [#205](https://github.com/Matysh/houseplan-card/issues/205)
|
||||
- Этап: `code` (PROCESS.md §2.7)
|
||||
- Диапазон: `origin/dev...HEAD`, `origin/dev` = `ad8e7a5`, `HEAD` = `11b0283`
|
||||
(ветка `issue/205-vacuum-trail-grace`, detached `HEAD`); merge-base — `f287bdd`
|
||||
- ТЗ: [`docs/specs/205-vacuum-trail-resume-grace.md`](../specs/205-vacuum-trail-resume-grace.md),
|
||||
ревью ТЗ зелёное — [`SPEC-REVIEW-205-r1.md`](SPEC-REVIEW-205-r1.md)
|
||||
- Цикл: **r1/4**
|
||||
- Ревьюер: Claude, свежая сессия, без переписки с автором реализации
|
||||
- Вердикт: **зелёный**
|
||||
|
||||
## Скоуп ревью
|
||||
|
||||
Три коммита диапазона:
|
||||
|
||||
1. `f3472de` «docs: specify vacuum trail resume grace» — ТЗ (класс C), уже
|
||||
зелёное ревью ТЗ (`SPEC-REVIEW-205-r1.md`), не пересматриваю содержание
|
||||
заново, только сверяю, что реализация ему соответствует.
|
||||
2. `c47a6bc` «docs: review document for #205» — сам документ ревью ТЗ (класс C).
|
||||
3. `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 доказывается численно через
|
||||
SVG `d`/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»).
|
||||
Reference in New Issue
Block a user