mirror of
https://github.com/Matysh/houseplan-card
synced 2026-10-02 12:49:56 +00:00
230 lines
20 KiB
Markdown
230 lines
20 KiB
Markdown
# SPEC-REVIEW-421-r1
|
||
|
||
- Issue: https://github.com/Matysh/houseplan-card/issues/421
|
||
- Этап: `S4-spec-review` (ревью ТЗ, PROCESS.md §2.4)
|
||
- Материал: `docs/specs/421-negative-test-proofs.md` на коммите `c409ec6a`
|
||
(ветка `origin/issue/421-negative-test-proofs`), тело issue #421,
|
||
комментарий-аналитика и комментарий «ТЗ готово» того же автора.
|
||
- Заход: r1 (первый раунд, разбор полный по определению).
|
||
|
||
## Скоуп ревью
|
||
|
||
Задача — техдолг: три существующих защитных проверки объявляют доказательство,
|
||
которого фактически не дают (support-preview lifecycle, `reportPageErrors()`,
|
||
acceptance-trace приёмки скриншотов документации). ТЗ утверждает, что
|
||
production-поведение не меняется, меняются только тесты/гейты/CLI-инструменты
|
||
(классы B по PROCESS.md §1). Продуктовых вопросов автор не поднимал — их
|
||
действительно нет: наблюдаемого пользователем поведения задача не касается.
|
||
|
||
Проверка велась состязательно: не «звучит ли складно», а совпадает ли описание
|
||
текущего кода в ТЗ с самим кодом, и реализуем ли план технически на существующей
|
||
инфраструктуре гейтов.
|
||
|
||
## Как проверялось
|
||
|
||
Гейты кода не гонялись — на этом SHA существует только документ ТЗ (docs-коммит
|
||
`c409ec6a`), реализации ещё нет, поэтому `typecheck`/`test`/`build` к предмету
|
||
ревью (тексту ТЗ) неприменимы; они станут предметом код-ревью следующего этапа.
|
||
Вместо этого сверено содержание ТЗ с действующим кодом трёх подсистем:
|
||
|
||
1. **`docs/SCOPE.md`, `PROCESS.md` (§1, §2.4, §2.5, §5, §7.1), `AGENTS.md`** —
|
||
прочитаны полностью до разбора ТЗ.
|
||
2. **`custom_components/houseplan/websocket_api.py:2126-2296`** — прочитан
|
||
код `ws_support_preview`, `ws_support_preview_discard`, `ws_support_submit`,
|
||
`_prune_support_previews` (строки 237-241). Сверено с описанием в ТЗ и в
|
||
теле issue.
|
||
3. **`demo/serve.mjs:44-114`** — прочитан код `roundTripLivePages()`,
|
||
`reportPageErrors()`, `finish()`. Сверено с описанием отдельного
|
||
round-trip в `reportPageErrors()`.
|
||
4. **`demo/guard/verify-guard.mjs`, `demo/guard/README.md`** — прочитаны
|
||
правила размещения проб (не `smoke_*`, не `demo/fixtures/`, отдельный
|
||
каталог вне корпуса `sourceFingerprint`).
|
||
5. **`scripts/docs-accept.mjs` (целиком), `scripts/docs-acceptance.mjs`
|
||
(`docsAcceptancePlan`), `test/docs-accept.test.mjs`** — сверено, что
|
||
`main()` действительно строит `accepted.acceptance` инлайн и что
|
||
существующий тест бьёт только по `verifyDocsCandidate`/`docsAcceptancePlan`,
|
||
не по этой ветке.
|
||
6. **`scripts/mutation-gate.mjs`** — прочитан формат реестра (`id`/`guard`),
|
||
существующие мутанты `smoke-guard-blind-to-tail` и
|
||
`smoke-guard-forgets-to-register-pages`, а также использование
|
||
`scripts/backend-test-guard.mjs` для адресного запуска одного HA-теста
|
||
(подтверждает «Принятое предположение» №2 ТЗ).
|
||
7. **`.github/workflows/validate.yml:500-542`** — сверено, где именно вызывается
|
||
`verify-guard.mjs` (job с реальным Chromium, шард 1, на каждый push) и что
|
||
это отдельно от дорогого `mutation-gate.yml` (пре-релизный).
|
||
8. **`scripts/source-fingerprint.mjs`** — сверено, что корпус `sourceFingerprint`
|
||
ограничен `src/**` + `rollup.config.mjs`/`tsconfig.json`/`package-lock.json`;
|
||
`scripts/**` и `demo/guard/**` в него не входят, поэтому изменения ТЗ не
|
||
требуют пересъёмки документационных скриншотов, как и заявлено в ТЗ.
|
||
9. **`tests_backend/test_ha_websocket.py:1328-1353`** — прочитан существующий
|
||
тест `test_support_preview_replacement_and_discard_are_draft_local`
|
||
полностью: подтверждено, что assert-ов ровно два (уникальность трёх
|
||
токенов и три успешных `discard`), submit заменённым/удалённым токеном не
|
||
отправляется вовсе.
|
||
|
||
## Находки
|
||
|
||
### Medium (в скоупе задачи) — отсутствуют обязательные разделы §7.1
|
||
|
||
**Файл:** `docs/specs/421-negative-test-proofs.md`
|
||
|
||
PROCESS.md §7.1 перечисляет обязательные разделы ТЗ: *сценарий* · *что человек
|
||
увидит до и после* · проблема · скоуп/не-скоуп · контракт поведения · **UX** ·
|
||
**модель данных и миграция** · i18n · AC с доказательством · план автотестов ·
|
||
риски · откат · release-артефакты. В документе отсутствуют явные разделы
|
||
«Сценарий», «Что человек увидит до и после» и «UX»; «Модель данных и миграция»
|
||
не выделена отдельно (частично покрыта фразой в разделе «i18n, compatibility,
|
||
touch и security»: «persisted config, import/export и WebSocket schema не
|
||
меняются» — но это про конфиг пользователя, не про артефакт `screenshots.json`,
|
||
который сама задача трогает).
|
||
|
||
**Почему это не придирка.** Правило явно объясняет причину: «ТЗ, которое не
|
||
может ответить на эти два вопроса, описывает работу, а не изменение продукта»
|
||
(§7.1). Эта задача действительно не меняет продукт — и именно поэтому короткий
|
||
явный ответ («сценария нет: ни одна из трёх персон docs/SCOPE.md не наблюдает
|
||
эффект этой задачи, наблюдаемое поведение до и после идентично») должен стоять
|
||
на своём месте, а не подразумеваться разбросанными по разным разделам
|
||
фразами. Без него DoR-чеклист §2.5 («ТЗ существует, ревью зелёное») формально
|
||
сверяется по неполному документу, и прецедент — что фраза «не меняется»,
|
||
сказанная вскользь в разделе про touch/i18n, заменяет собой продуктовый раздел
|
||
— плохо масштабируется на следующие тест-инфраструктурные задачи, где эффект
|
||
на самом деле есть, но автор по инерции решит, что раздел необязателен.
|
||
|
||
**Как воспроизвести:** открыть `docs/specs/421-negative-test-proofs.md` и
|
||
поискать заголовки «Сценарий», «Что человек увидит», «UX» — их нет; есть
|
||
«Цель», «Проблема», «Скоуп», «Не-скоуп», «Контракт», «Затрагиваемые файлы»,
|
||
«i18n, compatibility, touch и security», «Производительность», «Критерии
|
||
приёмки», «План отрицательной проверки», «Риски», «Откат»,
|
||
«Release-артефакты», «Принятые предположения».
|
||
|
||
**Что чинит находку:** добавить в тот же документ короткий явный блок (2-4
|
||
предложения на каждый из трёх недостающих разделов достаточно): кто и когда
|
||
эту работу наблюдает (никто — задача не меняет наблюдаемое поведение,
|
||
подтверждается AC9), почему UX не описывается (нет UI-поверхности), и что
|
||
единственный затронутый «данные»-артефакт — недокументный
|
||
`docs/images/screenshots.json.acceptance`, который не подпадает под
|
||
`CONFIG-COMPATIBILITY.md` и не мигрирует, а расширяется совместимо (поле
|
||
`lastWriteWasFingerprintOnly` уже описано в разделе «Контракт», просто не под
|
||
этим заголовком). Правки не расширяют скоуп и не меняют ни один AC.
|
||
|
||
### Low — неточная формулировка каденции новой browser probe (снимаю с записью)
|
||
|
||
**Файл:** `docs/specs/421-negative-test-proofs.md`, раздел «Производительность»:
|
||
«Новая browser probe и адресные мутанты входят в дорогой mutation/release
|
||
gate и не запускаются на каждом обычном frontend unit прогоне сверх уже
|
||
существующего verify-guard...». Читается так, будто новая проба — часть
|
||
дорогого пре-релизного гейта. По факту `verify-guard.mjs` (куда AC4 и раздел
|
||
«Затрагиваемые файлы» explicitly кладут новую пробу) уже вызывается в
|
||
`.github/workflows/validate.yml:542`, в лёгкой job «Смоки в браузере», на
|
||
**каждый push**, а не только перед релизом — дорогой является лишь часть с
|
||
адресными мутантами `scripts/mutation-gate.mjs`.
|
||
|
||
**Почему не поднимаю до Medium:** AC4/AC5 и раздел «Затрагиваемые файлы»
|
||
однозначно фиксируют, что проба живёт в `verify-guard.mjs` и мутант — в
|
||
`mutation-gate.mjs`; реализатор пойдёт за точными разделами, а не за одной
|
||
обзорной фразой в «Производительности». Снимаю решением ревьюера с записью
|
||
здесь, автор может поправить формулировку по желанию, блокирующей силы не
|
||
имеет.
|
||
|
||
## Что проверено и корректно
|
||
|
||
- **Фактическая точность описания текущего кода.** Все три «разрыва
|
||
доказательности», на которых строится ТЗ, подтверждены чтением
|
||
production-кода, а не пересказом issue:
|
||
- `ws_support_preview_discard` действительно отвечает `{"ok": True}`
|
||
идемпотентно, включая отсутствующий токен (`websocket_api.py:2242-2247`);
|
||
- `test_support_preview_replacement_and_discard_are_draft_local`
|
||
действительно не отправляет submit ни одним из трёх токенов после
|
||
discard — только проверяет успешность самого discard;
|
||
- `reportPageErrors()` (`demo/serve.mjs:98-99`) и `finish()`
|
||
(`demo/serve.mjs:108-113`) вызывают `roundTripLivePages()` раздельно —
|
||
зарегистрированные мутанты (`scripts/mutation-gate.mjs:120-141`) ломают
|
||
только путь `finish()`/`_livePages`;
|
||
- `scripts/docs-accept.mjs:150-159` действительно строит `acceptance` инлайн
|
||
в `main()`, читая `previous` напрямую из файла на диске — вне любой
|
||
экспортируемой, тестируемой функции.
|
||
- **AC1–AC9 однозначны и у каждого назван способ доказательства** (`unit`/
|
||
backend test/адресный мутант/`review`), включая явное указание, что зелёный
|
||
прогон без зафиксированного отрицательного прогона не закрывает AC (раздел
|
||
«План отрицательной проверки» — прямое применение правила ревью «тест должен
|
||
уметь падать» уже на этапе ТЗ).
|
||
- **Технически план реализуем на существующей инфраструктуре**, без новых
|
||
зависимостей и без нового процессного механизма:
|
||
- `_prune_support_previews(rt, now)` уже принимает `now` параметром, а
|
||
`time.monotonic()` — module-level импорт в `websocket_api.py`, значит
|
||
monkeypatch TTL детерминированным способом (AC3) осуществим без `sleep`;
|
||
- существующий `demo/guard/README.md` уже объясняет, почему новая проба
|
||
должна жить именно в `demo/guard/`, а не в `demo/smoke_*` или
|
||
`demo/fixtures/` (в т.ч. чтобы не задеть `sourceFingerprint`) — план ТЗ
|
||
следует этому правилу, а не изобретает своё;
|
||
- `scripts/backend-test-guard.mjs` существует и уже используется в
|
||
`mutation-gate.mjs` для запуска одного именованного backend-теста —
|
||
«Принятое предположение» №2 подтверждено, не является голой догадкой;
|
||
- `docs/images/screenshots.json.acceptance` не читается никаким другим
|
||
гейтом/скриптом кроме `docs-accept.mjs` (единственное совпадение вне
|
||
самого ТЗ и `docs-accept.mjs` — несвязанные документы `SPEC/CODE-REVIEW-406`
|
||
по слову «acceptance»), поэтому расширение поля совместимо и не требует
|
||
миграции.
|
||
- **Не-скоуп сформулирован жёстко и корректно** отсекает изменение
|
||
WebSocket-контракта, idempotency-семантики `discard`, UI Help & feedback,
|
||
общего smoke harness и правил приёмки скриншотов — совпадает с тем, что
|
||
показало чтение кода: реального различия в поведении между «что есть» и
|
||
«что должно быть по контракту» найдено не было, только в доказательности.
|
||
- **«Принятые предположения» — действительно предположения**, а не факты,
|
||
выданные за решение: помечены явно, ревьюер вправе их оспорить (оспаривать
|
||
не стал — они подтверждены чтением кода выше).
|
||
- **Выбор полного трека вместо `small` обоснован названным критерием**
|
||
(«нарушен критерий одной поверхности: три независимых защитных контура»,
|
||
комментарий-аналитика) — соответствует требованию PROCESS.md §5 называть
|
||
нарушенный критерий явно, а не писать «обычный трек» без обоснования.
|
||
- **Риски секции адресуют именно те способы, которыми доказательство может
|
||
остаться слабым** (маскировка relay-мока, flaky TTL-тест, повторное
|
||
попадание пробы в `finish()`, коллизия anchor мутанта, расхождение
|
||
CLI/unit-хелпера) — это ровно те точки, которые я independently проверял
|
||
бы при код-ревью; их называние в ТЗ снижает будущий риск и на код-ревью.
|
||
|
||
## Чего не проверял
|
||
|
||
- Гейты `typecheck`/`npm test`/`npm run build` — неприменимо: на этом SHA
|
||
реализации ещё нет, гейты будут предметом код-ревью.
|
||
- Собственно исполнение отрицательных проб (мутантов) — их ещё нет; план
|
||
проверен на реализуемость чтением инфраструктуры (`mutation-gate.mjs`,
|
||
`verify-guard.mjs`, `backend-test-guard.mjs`), не запуском.
|
||
- Полный текст `custom_components/houseplan/websocket_api.py` и
|
||
`scripts/docs-accept.mjs` вне участков, относящихся к трём заявленным швам
|
||
— вне скоупа задачи, чтение сосредоточено на затронутых строках.
|
||
- `docs/TESTING.md` — не читал; ТЗ верно оставляет его правку условной
|
||
(«только если без короткого описания новых guard IDs невозможно
|
||
обнаружить из runbook»), решение по факту реализации.
|
||
|
||
## Материал раунда
|
||
|
||
- Ветка: `origin/issue/421-negative-test-proofs`
|
||
- SHA: `c409ec6a`
|
||
- ТЗ: `docs/specs/421-negative-test-proofs.md` на этом SHA
|
||
- Issue: #421, комментарии аналитика и автора ТЗ прочитаны полностью
|
||
|
||
## Вердикт
|
||
|
||
Жёлтый. AC полны, доказуемы и технически реализуемы; описание текущего кода
|
||
в ТЗ точное (проверено чтением production-кода по всем трём швам). Единственная
|
||
находка — формальный пробел в обязательных разделах §7.1 («Сценарий», «Что
|
||
человек увидит», «UX»), который не меняет ни один AC и чинится добавлением
|
||
нескольких предложений в тот же документ.
|
||
|
||
---
|
||
|
||
<!-- material-anchors: сгенерировано конвейером (#414) -->
|
||
|
||
## Материал раунда
|
||
|
||
- Ветка: `issue/421-negative-test-proofs`, коммит `c409ec6a61b5` — ребейз его осиротит, и это нормально: ниже якоря, которые ребейз не меняет.
|
||
- Дерево материала: `e7d26df9bdc0c4b77e33a398ff79ed5f0c3ee21b`
|
||
```
|
||
git log --all --format='%H %T' | grep e7d26df9bdc0
|
||
```
|
||
- ТЗ `docs/specs/421-negative-test-proofs.md`, блоб `1a6b112c576747e4bf99f02fc890926c3bbd0b1b`
|
||
```
|
||
git log --all --find-object=1a6b112c576747e4bf99f02fc890926c3bbd0b1b -- docs/specs/421-negative-test-proofs.md
|
||
```
|