Files
houseplan-card/docs/reviews/SPEC-REVIEW-502-r1.md
T
2026-09-09 19:35:30 +00:00

16 KiB
Raw Blame History

SPEC-REVIEW-502-r1

Issue: #502 — «i18n-dead-keys: широкий паттерн r.+ маскирует мёртвые ключи» Этап: spec (PROCESS.md §2.4), лёгкий трек (small), ТЗ в теле issue. Заход: r1 · блокирующих циклов израсходовано 0 из 2 (лимит §4 для лёгкого трека). SHA рабочей копии на момент ревью: 72764cff906d8e27996e638ced8b708251e24657.

Скоуп

Класс B, один файл test/i18n-dead-keys.test.mjs (опционально новый test/helpers/i18n-consumers.mjs). Продуктового кода, i18n-словарей, UX, миграций задача не касается; User-Visible: no. Цель: сузить генератор «динамических потребителей» в гейте мёртвых i18n-ключей так, чтобы паттерн вида ^r.+$ (рождённый из 'r' + Date.now().toString(36)) не маскировал реальные мёртвые ключи семейства radar.*, room.*, run.* и т. п.

Как проверялось

  1. Прочитаны docs/SCOPE.md, AGENTS.md, PROCESS.md (§1–§10, включая §2.4, §2.10, §5, §7.1).

  2. Прочитано тело issue #502 целиком и оба комментария (S2-анализ, передача на ревью). Нашедшая задачу цепочка — код-ревью #485 r2 — тоже сверена.

  3. Прочитан текущий test/i18n-dead-keys.test.mjs на dev построчно — подтверждено, что expressionPattern для BinaryExpression с + действительно собирает регулярку без требования точки в статической части, и что 'r' + Date.now().toString(36) в src/houseplan-editor-runtime.ts (id черновика) даёт ровно ^r.+$, как написано в issue.

  4. Проверено текущее состояние radar.bad_references: grep показал, что ключ уже используется как строковый литерал в src/radar-setup.ts:334 (... : message === 'bad_references' ? 'radar.bad_references' : ...), т.е. дефект из #485 r2, который и породил эту задачу, на dev уже устранён (коммит e9fb255a fix: close radar review gaps), а #485 закрыт.

  5. Эмпирическая проверка выполнимости AC4 (гейт зелёный на реальном дереве после сужения) — не полагаясь на заявление автора. Собран одноразовый скрипт, реализующий текущий expressionPattern один в один плюс критерий контракта п.1 («статическая часть содержит .») как фильтр isKeyShapedPattern, прогнан на актуальном src/** и словарях src/i18n/en.json + src/i18n/support/en.json (полный листинг результата сохранён в сессии, не коммитился, репозиторий не менялся):

    OLD unused count: 0 []
    NEW unused count: 0 []
    

    Узкий фильтр отбрасывает несколько десятков динамических паттернов (CSS/HTML шаблоны в src/day-cycle-render.ts, src/backdrop-pick.ts, src/decor-image-editor.ts, src/color.ts, src/coincident-partitions.ts и др. — они никогда не были осмысленными i18n-потребителями, просто случайно совпадали под старым широким критерием), но ни один из них не превращает ни один ключ словаря в мёртвый: результат «0 unused» не меняется. Вывод: AC4 — не догадка, а проверяемо достижимое требование уже на сегодняшнем дереве, при разумной трактовке контракта п.1–2.

  6. Дешёвые гейты (диф не тронут — smoke/golden/backend не запускались, они не нужны для ревью ТЗ):

    • npx tsc --noEmit — зелёный (0 ошибок), выполнен на dev без изменений. Смысл: подтвердить, что рабочая копия сама по себе в порядке перед разбором ТЗ; продуктовый код в этом раунде не менялся, полный npm test/npm run build для ревью спецификации избыточны (диф — только тело issue).

Находки

Находок High или Medium (в скоупе или вне него) не обнаружено.

Low — снимается с записью

L1. Раздел «Зависимости» описывает уже не актуальную ситуацию. Файл: тело issue #502 (раздел «Зависимости»). Текст предполагает, что radar.bad_references может быть всё ещё мёртвым на момент реализации и тогда сужённый гейт «покраснеет на нём», требуя ждать слияния #485. Фактически #485 уже закрыт и слит, ключ уже используется в src/radar-setup.ts:334 — зависимость исполнена ещё до передачи ТЗ на ревью. Раздел не создаёт риска (он написан как условие «если … то», а не как факт) и не блокирует переход в S5-ready: реализатору достаточно перечитать текущее состояние dev перед стартом, что он обязан сделать в любом случае. Снимаю без правки текста — исправлять формулировку ради контроля версий issue не имеет смысла, реализатор увидит актуальное состояние сам.

L2. Место объявления DYNAMIC_KEY_FAMILIES не названо явно. Контракт п.3 вводит константу DYNAMIC_KEY_FAMILIES, «принятый предположительно» блок разносит чистые функции в test/helpers/i18n-consumers.mjs, а сам гейт остаётся в test/i18n-dead-keys.test.mjs — но не сказано, в каком из двух файлов будет жить сама константа-список семей. Это техническая, не продуктовая деталь (расположение файла, PROCESS §7.1: «место, где стоит гвард» — решает исполнитель), и она не единообразна ни с чем, что требовало бы одного конкретного ответа. Не блокирует; исполнитель решает свободно.

Проверка контракта на непротиворечивость и проверяемость

  • П.1 (точка в статической части) — проверяемо: AC1 и AC2 дают конкретные примеры входов и ожидаемых true/false, юнит на синтетическом AST. Убедился (см. «Как проверялось», п.6), что действующая реализация expressionPattern уже собирает source/dynamic в форме, из которой этот фильтр вычисляется механически (строка минус все .+-вставки содержит .). Однозначно.
  • П.2 (открытый хвост допустим, если после точки) — это уточнение к п.1, не отдельный независимый фильтр; пример .+\.title корректно иллюстрирует разрешённый случай, а ^r.+$ — прямое следствие того, что паттерн из п.1 уже отсеян (в нём нет точки вовсе), а не отдельно запрещённый случай «начала с хвоста». Формулировка чуть избыточна («паттерн … невозможен по построению п.1» — верно и прямо сказано в тексте, противоречия нет), Low не завожу.
  • П.3 (явный список семей + непустая причина + запись без покрытия — ошибка теста) — проверяемо AC4, оба условия (непустая причина, покрытие ≥1 ключа) разнесены по разным ветвям мутации в третьем столбце таблицы AC.
  • П.4 (реально мёртвые ключи не маскируются, уходят отдельным issue Codex) — граница со скоупом задачи прочерчена верно: класс A (словари) не затрагивается этим issue, что совпадает с заявленным классом B. Единственный известный на момент ТЗ случай (radar.bad_references) уже не актуален (см. L1); эмпирическая проверка (п.5 выше) показывает, что на сегодняшнем дереве других случаев нет — контракт не вступает в противоречие с AC4 «зелёный гейт на реальном дереве».
  • П.5 (форма сообщения об ошибке не меняется) — тривиально проверяемо визуальным сравнением текста в тесте, доказательство не требуется отдельным AC (описательное требование к диагностике, не к защите).

Утверждений, поданных как факт о продуктовом поведении без пометки «предположение», не найдено — блок «Принято предположительно» присутствует, покрывает единственное содержательное техническое решение (критерий «похоже на ключ» = наличие точки, и альтернатива, которую отвергли, названа явно). Продуктовых вопросов владельцу в тексте нет и не требуется: гейт не наблюдаем пользователем (персонами J1–J7 из docs/SCOPE.md этот тест не встречается вовсе), задача не расширяет и не сужает видимое поведение продукта.

Что проверено и корректно

  • Класс изменений (B), User-Visible: no, отсутствие миграций/i18n/UX/perf/ touch-влияния — заявлено верно и подтверждено чтением диффа не требуется, т.к. диффа кода ещё нет; сам факт, что единственный целевой файл лежит в test/**, подтверждён структурой репозитория.
  • Формат AC1–AC5: каждый несёт способ доказательства и (кроме AC5, честно помеченного «не защита») мутацию, которой он красне́ет — это ровно требование §2.7 на будущий код-ревью, заранее подготовленное автором, снижает риск возврата на код-ревью.
  • Откат — одним revert-коммитом, без затронутого продуктового кода; минимально достаточен для класса B.
  • Трек small: все пять критериев §5 действительно выполняются одновременно (одна поверхность — один тестовый файл; ни миграции, ни compatibility-полей; ни нового UX-контракта — потому что UX-контракта нет вовсе; ни perf/touch- влияния). Полный трек не требовался бы ни по одному критерию.
  • Эмпирическая проверка достижимости AC4 (раздел «Как проверялось», п.5) — сильнее, чем «ревьюер поверил автору»: воспроизведена логика контракта на реальном src-дереве, а не пересказана.

Чего не проверял

  • Полный npm test / npm run build / npm run bundle:sync — не нужны на этапе ревью спецификации: продуктового и тестового кода ещё нет, диф — только текст issue. Будут обязательны на код-ревью (§7.2), включая дисциплину «тест умеет падать» по мутациям, которые сам автор уже назвал в таблице AC.
  • Браузерные смоки, golden, backend pytest, performance-профили — не применимо: задача не трогает src/**, custom_components/**/*.py и не меняет видимый результат.
  • Точная реализация isKeyShapedPattern/unusedKeys как кода — не пишется на этом этапе (ревьюер ТЗ не правит продуктовый/тестовый код); эмпирическая симуляция в разделе «Как проверялось» использована только чтобы проверить ВЫПОЛНИМОСТЬ контракта, а не чтобы заменить будущий код-ревью.

Вердикт

Зелёный. Two Low-находки (L1, L2) сняты с записью выше, без правки текста issue — они не мешают переходу в S5-ready и не создают риска для реализации. High и Medium (в скоупе или вне него) не найдено.


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

  • Issue: #502, тело + 2 комментария (S2-анализ и передача на S4-spec-review), оба от Matysh, 2026-09-09.
  • ТЗ живёт в теле issue (лёгкий трек), файла в docs/specs/ нет и не должно быть.
  • SHA рабочей копии на момент вывода вердикта: 72764cff906d8e27996e638ced8b708251e24657 (сверено непосредственно перед подведением итогов).

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

  • Ветка: dev, коммит `` — ребейз его осиротит, и это нормально: ниже якоря, которые ребейз не меняет.
  • Якоря снять не удалось: ветки задачи нет, материал читался по dev.
  • Вердикт конвейера: green · High 0