Files
houseplan-card/docs/reviews/SPEC-REVIEW-421-r1.md
2026-09-02 17:03:31 +00:00

20 KiB
Raw Permalink Blame History

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 и чинится добавлением нескольких предложений в тот же документ.


Материал раунда

  • Ветка: 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