Files
houseplan-card/docs/reviews/SPEC-REVIEW-454-r1.md
T
2026-09-04 16:58:31 +00:00

24 KiB
Raw Blame History

SPEC-REVIEW — issue #454 · заход r1

  • Issue: https://github.com/Matysh/houseplan-card/issues/454
  • Этап: ТЗ на ревью (PROCESS.md §2.4), заход r1, блокирующих циклов израсходовано 0 из 4 (лимит полный, small/trivial не стоят)
  • Материал: docs/specs/454-review-round-counter.md на коммите ebcf1223 (комментарий «ТЗ готово», ветка issue/454-review-round-counter)
  • Ревьюер: свежая сессия, без контекста написания ТЗ

Скоуп

Issue описывает дефект в guard-джобе .github/workflows/process.yml: счёт attempt/spent идёт по regex над телом комментария-вердикта, и вердикт без явной подстроки SPEC-REVIEW/CODE-REVIEW в тексте выпадает из подсчёта навсегда — что уже привело к перезаписи docs/reviews/SPEC-REVIEW-449-r1.md и к занижению бюджета циклов. ТЗ предлагает считать оба числа по опубликованным файлам docs/reviews/${marker}-${NUM}-r*.md на ветке задачи (attempt — от максимального номера файла плюс один, не от их количества), со страховкой «максимум из счёта по файлам и счёта по комментариям» на случай отказа шага публикации.

Класс изменения — B (инфраструктура): трогаются только .github/workflows/process.yml и scripts/review-doc-guard.mjs / test/review-doc-guard.test.mjs. Ни одного файла класса A нет, то есть по механическому признаку AGENTS.md/PROCESS.md §1 задача могла бы идти вне флоу вовсе (без ТЗ и его ревью). Автор в комментарии S2-analysis (2026-09-04) сознательно выбрал полный трек, назвав причину — правка меняет механизм, который управляет всеми остальными задачами, и ошибка в нём стоит дороже обычной инфраструктурной правки. Это решение владельца/автора по существу задачи, а не нарушение процесса, и не является предметом этого ревью.

docs/SCOPE.md к этой задаче неприменим по смыслу: правка не служит ни одному Core user job — это внутренний инструмент конвейера ревью, не продуктовая функция. Отмечаю это, а не пропускаю проверку молча.

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

  • Прочитаны docs/SCOPE.md, AGENTS.md, PROCESS.md (целиком, включая §2.10, §4, §7.1, §8, §10–12).
  • Прочитано тело issue #454 и оба комментария (S2-analysis владельца, «ТЗ готово» автора).
  • Прочитан docs/specs/454-review-round-counter.md целиком.
  • Сверены факты ТЗ с текущим кодом на dev (git show origin/dev:...):
    • .github/workflows/process.yml — блок guard/decide (строки ~60–164), построение имени документа doc="docs/reviews/${marker}-${NUM}-r${CYCLE}.md" (строки 661, 803), шаг резолва ветки задачи (строки ~211–225);
    • scripts/review-doc-guard.mjs — весь файл: существующие три режима CLI (--allow=/stdin, --anchor=, --doc=) и их назначение (allowlist пути публикации, дозапись якорей материала, проверка осиротевших SHA);
    • реальный формат строки вердикта в опубликованном документе (docs/reviews/CODE-REVIEW-450-r1.md на dev) — подтверждено, что строка Вердикт: встречается ровно один раз и в конце документа, заголовок ## Вердикт подстроку Вердикт: не даёт.
  • Прослежена арифметика контракта (§1–§3 ТЗ) вручную на реальной хронологии #449 из комментария владельца (таблица времени/вердиктов/наличия маркера) — см. находку M1 ниже.

Код не читался как продукт (ветки реализации ещё нет — этап S4-spec-review, scripts/review-doc-guard.mjs и process.yml на ветке задачи ещё не изменены относительно dev). Это ревью ТЗ, не код-ревью.

Находки

M1 (Medium, в скоупе) — AC2 не доказуем тем способом, который сам называет

Формулировка AC2: «Вердикт без маркера в теле больше не занижает ни attempt, ни spent: сценарий #449 (r1 без маркера, r2 с маркером) даёт attempt=3, spent=2», доказательство — «unit на фикстуре реальных данных #449». План тестирования уточняет: «фикстура «как на #449» строится из реальных заголовков документов и строк вердиктов».

Проблема. Реальная история #449 (таблица в комментарии S2-analysis владельца) даёт для spec-этапа ровно три вердикта:

Время Вердикт Маркер в теле
14:46 жёлтый, «заход r1» нет
14:56 жёлтый, «заход r2» да
15:17 зелёный, «заход r3» да

Из-за бага документ SPEC-REVIEW-449-r1.md физически перезаписан: под этим именем сейчас лежит содержимое второго раунда (жёлтый), а третий раунд лежит под именем -r2.md (зелёный). Оригинальное содержимое первого раунда (тоже жёлтое) сохранилось только в старом git-блобе 1ce62613, вне рабочего дерева — и его восстановление ТЗ прямо выносит в «не-скоуп» («восстановление затёртого документа… отдельная ручная операция владельца, к механизму не относится»).

Если строить фикстуру буквально из реальных, то есть сейчас существующих на ветке файлов (-r1.md = жёлтый, -r2.md = зелёный) и применить контракт §1–§3 ТЗ:

  • attemptFromFiles = max(1, 2) + 1 = 3 — совпадает с AC2;
  • spentFromFiles = число файлов с блокирующим вердиктом = 1 (-r1.md жёлтый; -r2.md зелёный не считается) — не совпадает с заявленным spent=2;
  • страховка spentFromComments (правило контракта §3, не изменяется) даёт то же самое: of_stage по маркеру = [комментарий 14:56, комментарий 15:17], blocking = только 14:56 (жёлтый) = 1. max(1, 1) = 1.

Иными словами: подлинность «жёлтый» первого раунда физически утрачена вместе с его файлом, и ни счёт по файлам, ни счёт по комментариям (комментарий 14:46 не несёт маркера — это и есть исходный баг) не могут её восстановить. Это не ошибка алгоритма — это прямое следствие того, что данные уже потеряны, и сам же документ признаёт это в «не-скоупе». Но тогда посчитанное по реальным данным #449 значение — spent=1, а не 2, как утверждает AC2.

Число spent=2 достижимо только на реконструированной фикстуре: два отдельных, никогда не перезаписывавшихся файла (-r1.md и -r2.md), оба с вердиктом «жёлтый» — то есть на данных, которые не являются «реальными данными #449» в буквальном смысле, а представляют, как выглядела бы история #449, если бы коллизии имён не произошло. План тестирования не говорит, какое из двух прочтений имеется в виду, а расхождение материально: разработчик, построивший фикстуру по первому (буквальному) прочтению, получит подтверждённый унит-тестом spent=1 и не будет знать, ошибка это в реализации или в самом AC — до следующего раунда ревью.

Как воспроизвести рассуждение: взять реальные три вердикта и два реальных файла #449 (перечислены выше), применить формулы контракта §1–§3 ТЗ вручную — результат attempt=3, spent=1, а не attempt=3, spent=2.

Что нужно поправить. Явно указать в AC2/плане тестирования одно из двух: (a) фикстура — два независимых, не перезаписанных документа с вердиктом «жёлтый» у обоих (тогда spent=2 доказуемо, но это не «реальные данные #449», а реконструкция «как должно было быть»); либо (b) фикстура — буквально текущее состояние ветки #449, и тогда ожидаемое значение — attempt=3, spent=1, а способность корректно посчитать «сколько удаётся восстановить» (не «сколько было на самом деле») — это и есть то, что стоит проверять и описывать. Второй вариант честнее: он проверяет ровно то, что новый механизм физически способен дать при уже случившейся потере данных, не обещая невозможного.

Из скоупа задачи (спецификация того же issue #454), без High-находок — жёлтый вердикт с возвратом автору.

L1 (Low, снимается с записью) — нет явных разделов «UX» и «i18n»

PROCESS.md §7.1 перечисляет обязательные разделы ТЗ, включая «UX» и «i18n» отдельными пунктами. В документе нет ни одного из них как самостоятельного раздела (headers): «UX» частично закрыт строкой в шапке `Touch editor: not exposed — задача не касается интерфейса вовсе», «i18n» не упомянут вовсе ни разу.

Не блокирует и не создаёт риска: задача не трогает ни одного файла с интерфейсом или строками (src/**, i18n/*.json), это прямо следует из раздела «Класс изменения» и списка задеваемых файлов, и вывод «i18n/UX не затронуты» очевиден по содержанию, а не додуман. Снимаю находку с записью, а не требую правки: добавление двух формальных строк-заглушек «UX: нет» / «i18n: нет» было бы ритуалом без содержания. Если следующий раунд правит документ по M1, было бы уместно добавить эти строки заодно (не отдельным циклом).

L2 (Low, к сведению, не в счёт вердикта) — cohesion review-doc-guard.mjs

scripts/review-doc-guard.mjs сейчас узко специализирован — заголовок модуля прямо говорит «публикация ревью-документа не имеет права трогать ничего, кроме него» (проверка путей пуша, дозапись и проверка якорей материала). ТЗ добавляет туда четвёртую, содержательно не связанную обязанность — счёт attempt/spent для guard-джоба, который отрабатывает до публикации и даже до самого ревью. Технический вопрос именования/расположения модуля — это то, что PROCESS.md §7.1 явно отдаёт автору («где стоит гвард… агенты решают сами»), поэтому не поднимаю его до статуса находки, требующей правки, а оставляю как наблюдение: отдельный файл (например scripts/review-round-counter.mjs) сделал бы границу ответственности яснее и не расширял бы область, которую уже покрывает существующее название и докстринг. Автор/ревьюер кода вправе оспорить это на следующем этапе без возврата сюда.

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

  • Контракт §1 (источник истины — файлы, max+1, а не количество). Проверено рассуждением на реальных данных #449 и на таблице крайних случаев «Ошибки и крайние случаи»: дыра в нумерации (r1, r3) корректно даёт attempt=4, поведение при пустом множестве файлов (attempt=1, spent=0) совпадает с сегодняшним умолчанием attempt=1 в process.yml:93.
  • Контракт §2 (строка вердикта, жёлт/красн без учёта регистра). Совпадает с существующим правилом process.yml:100 («blocking» через тот же regex (жёлт|красн), i), не меняет то, что уже работает верно для идентификации блокирующих вердиктов внутри одного документа — подтверждено на реальном файле CODE-REVIEW-450-r1.md (единственное вхождение строки Вердикт: — финальная, помеченная как искомая).
  • Контракт §3 (страховка максимумом). Само правило корректно устраняет риск «перерасчёта», которого боялся автор исходного механизма (process.yml:88-93): максимум из двух счётов не может дать значение выше фактического числа опубликованных документов/вердиктов ни при каком их сочетании — арифметически max(a,b) ≤ true, только если оба источника не завышают, что для обоих верно по построению (ни файлы, ни комментарии не порождают лишних записей).
  • AC1, AC3–AC7. Формулировки однозначны, способ доказательства (unit, частично + мутант) указан и достижим — не нашёл ни одного нереализуемого условия среди них при том же рассуждении, что для M1 (AC4/AC5/AC7 дополнительно совпадают с уже работающими сегодня инвариантами #227/#89, которые контракт §5 явно объявляет неизменными).
  • Не-скоуп. Явно и корректно исключает правку задним числом чужого комментария и восстановление утраченного SPEC-REVIEW-449-r1.md — соответствует «владелец решает вручную», не додумано автором.
  • Ограничение «process.yml идентичен в main и dev» учтено в разделе «Карта реализации» (п. 5) и в разделе рисков; соответствует существующему шагу Validate, который эту идентичность проверяет — не проверял сам шаг Validate заново, доверяю его существованию по цитате в ТЗ (совпадает с тем, что описывает PROCESS.md/комментарий автора при S2-analysis).
  • Откат. План (один коммит-ревёрт process.yml плюс зеркало в main, скрипт и тесты безвредны сами по себе) реалистичен и не требует миграции данных — подтверждаю чтением, без исполнения (откатывать нечего, кода ещё нет).
  • Release-артефакты. User-Visible: no, трейлеры корректны для класса B; условие обновления PROCESS.md («если он описывает нынешний механизм подсчёта по комментариям») сверено с текстом PROCESS.md — документ обсуждает разделение attempt/spent концептуально (§4, §2.10), но не описывает реализацию «regex по телу комментария», поэтому условие, скорее всего, не сработает; это корректно оставлено условным, а не заявлено фактом.

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

  • Само существование и содержимое scripts/review-doc-guard.mjs / test/review-doc-guard.test.mjs на ветке задачи — на момент этого ревью ветка issue/454-review-round-counter содержит только docs/specs/454-review-round-counter.md (по коммиту ebcf1223, «ТЗ готово»), реализации ещё нет — это ожидаемо для этапа S4-spec-review, не пропуск.
  • Гейты (typecheck/test/build) не гонял: класс изменения — документация ТЗ, кода нет вовсе, гонять нечего. check-docs/golden/смоки/invariants неприменимы — diff не касается src/**, геометрии, визуала или бэкенда.
  • Не проверял исполнением, действительно ли gh api/git ls-remote способны без actions/checkout перечислить файлы docs/reviews/ на произвольной ветке из guard-джоба (контракт §4 ТЗ) — формально это описано в «Карте реализации» как gh api, что технически осуществимо (REST contents эндпоинт не требует локального дерева), но сам вызов не выполнял; это техническая деталь реализации, а не продуктовое ограничение, и её вправе уточнить автор на этапе кода.
  • Не проверял, как именно единственный существующий на сегодня файл docs/reviews/CODE-REVIEW-449-r1.md (единственный опубликованный документ на код-этапе #449) поведёт себя под новым алгоритмом — не требовалось для находки M1, которая полностью доказывается на spec-этапе; арифметика code-этапа (attempt=2, spent=1, что совпадает с ожиданием автора в комментарии S2-analysis) проверена мысленно и расхождений не показала.

Итог

Один Medium в скоупе (M1: AC2 недоказуем буквальным прочтением собственной формулировки — конкретное числовое расхождение показано выше), High — ноль. Остальные девять AC однозначны и доказуемы описанным способом. Low L1 снимается с записью, L2 — наблюдение вне вердикта. Вердикт — жёлтый, возврат автору на уточнение AC2 (одна строка/абзац, не переписывание ТЗ).


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

  • Ветка: issue/454-review-round-counter, ТЗ прочитано на коммите ebcf1223 (тот же SHA, что указан автором в комментарии «ТЗ готово»).
  • Сверка с базовым состоянием — origin/dev на момент ревью (см. «Как проверялось»): .github/workflows/process.yml, scripts/review-doc-guard.mjs, docs/reviews/CODE-REVIEW-450-r1.md.
  • Воспроизвести факты о #449, использованные в M1:
    gh issue view 449 --repo Matysh/houseplan-card --json comments
    gh issue view 454 --repo Matysh/houseplan-card --json body,comments
    git show origin/dev:.github/workflows/process.yml
    git show origin/dev:scripts/review-doc-guard.mjs
    git show origin/issue/454-review-round-counter:docs/specs/454-review-round-counter.md
    

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

  • Ветка: issue/454-review-round-counter, коммит ebcf12233af7 — ребейз его осиротит, и это нормально: ниже якоря, которые ребейз не меняет.
  • Дерево материала: e7a2554603963e6da4f3aa44b30502dc852a43f1
    git log --all --format='%H %T' | grep e7a255460396
    
  • ТЗ docs/specs/454-review-round-counter.md, блоб 730f7f3964e2269e7f867bedfde50290afe9a1a9
    git log --all --find-object=730f7f3964e2269e7f867bedfde50290afe9a1a9 -- docs/specs/454-review-round-counter.md