Files
houseplan-card/docs/reviews/SPEC-REVIEW-517-r1.md
T
2026-09-10 06:54:51 +00:00

20 KiB
Raw Blame History

SPEC-REVIEW-517-r1

Скоуп

Issue #517 переносит хранение ТЗ полного трека из docs/specs/<NN>-*.md в тело issue и переносит доказуемость «вердикт ревью вынесен на этом тексте» с блоба файла на sha256 нормализованного тела issue, записываемый в блок якорей документов ревью. Пять поверхностей: (1) process.yml — новый шаг снятия хеша тела и сравнение с записью последнего зелёного SPEC-REVIEW; (2) scripts/review-doc-guard.mjs — anchorIssueBodyFrom/сравнение хешей; (3) scripts/process-gate.mjs — замена проверки 3 (файл ТЗ) на проверку заголовка ## ТЗ/AC1 в теле; (4) scripts/task-packet.mjs — смена приоритета источника AC; (5) PROCESS.md/AGENTS.md/docs/specs/README.md — переписывание правил. User-Visible: no, продукт и словари не затронуты.

Issue помечен small; ТЗ по правилу лёгкого трека действительно лежит в теле issue, файла docs/specs/517-*.md нет — соответствует ожиданию для этой метки. Ветки issue/517-* на origin нет: кода ещё не существует, ревью — чисто по тексту тела issue и его единственному комментарию (S2-аналитика).

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

  • Прочитаны docs/SCOPE.md, PROCESS.md (целиком), AGENTS.md.
  • Прочитано тело issue #517 и комментарий S2 (gh issue view 517 --json body,comments).
  • Каждое фактическое утверждение ТЗ и S2-комментария о текущем коде сверено с реальными файлами, а не принято на слово:
    • scripts/review-doc-guard.mjs: materialAnchorBlock (строка 256), anchorTreeFrom/anchorVerdictFrom (442, 451), reusableGreenVerdict (480) — сигнатуры и поведение совпадают с описанием в ТЗ (У2, У3).
    • scripts/process-gate.mjs: checkSpecs (305), NO_SPEC_FILE (76) — совпадает с описанием гейта в плане и S2.
    • scripts/task-packet.mjs: extractAcceptanceCriteria (49), acSource тернарник (строка ~112: specs.length ? specs... : issue.body) — поведение (AC из файла, тело — fallback) совпадает с тем, что план собирается инвертировать (AC5).
    • .github/workflows/process.yml: полностью прочитана структура job review — шаги Перейти на ветку задачи (безусловный), Привести ветку к dev/rebase (if: stage=='code'), Зафиксировать SHA материала ревью/ material (if: rebase.conflict != 'true', БЕЗ условия по stage), reuse (#499, if: stage=='code', сравнивает только дерево через reusableGreenVerdict), шаг вызова модели Review (if: gate.proceed && reuse.reuse != 'true'), шаг публикации документа (использует steps.material.outputs.* без разбора по stage).
  • Проверено отсутствие docs/specs/517-*.md (ls docs/specs/517* — нет файлов), что подтверждает корректность выбора лёгкого треке ТЗ-в-теле.
  • Проверены точные ссылки, которые ТЗ/S2 называют как подтверждённые фактом: docs/specs/README.md (раздел «Обязательные release-артефакты», на который ссылается PROCESS.md:158), список секций PROCESS.md (grep '^## \|^### ') — секции §2.3, §5, §7.1, §7.3 п.1 существуют и релевантны; секция «§10.5 п.3», которую S2-комментарий называет как место требования файла ТЗ в гейте, не существует (максимальный номер подраздела — §10.4, дальше идёт §11) — верная ссылка на этот гейт — §10.2 п.3.
  • Гейты (typecheck/test/build) не прогонялись и не нужны на этом этапе: кода нет, ветки нет, диффа нет — предмет ревью текстовый (тело issue), а не дерево репозитория.

Находки

Medium-1 (в скоупе). Гарантия AC2 не держится при повторном применении зелёного вердикта (#499)

Файл: тело issue #517, раздел «Уточнения S2», пункт У3, и AC2.

В чём дефект. AC2 обещает: правка тела issue после зелёного ревью ТЗ обязательно даёт код-ревью строку «ТЗ менялось после зелёного ревью ТЗ» и полный разбор. У3 при этом утверждает: «reuse (#499) и страховка #414 не затрагиваются» — то есть механизм повторного применения зелёного вердикта без вызова модели остаётся как есть. Но именно этот механизм и есть дыра: reusableGreenVerdict (scripts/review-doc-guard.mjs:480) признаёт вердикт переиспользуемым по ТРЁМ условиям — записанный вердикт green/High 0, наличие якоря дерева, и отсутствие отличий git-дерева от этого якоря (differs(tree)) — и не знает ничего о теле issue. Шаг reuse (process.yml:436-447) условен только на stage == 'code', а шаг вызова модели Review (~851) стоит под if: gate.proceed && reuse.reuse != 'true'. Значит, когда reuse=true, модель не вызывается вовсе — а именно туда, по плану ТЗ, должна попадать «находка, доставляемая ревьюеру в промпт».

Сценарий воспроизведения. Код-ревью раунда r_k проходит зелёным (High 0), дерево якорится. Слияние не удаётся (ребейз/страж #312) — задача возвращается в S6 и сразу в S7 (ровно сценарий #437 r4, на который ссылается сам reusableGreenVerdict). Между r_k и r_k+1 кто-то правит тело issue (уточняет AC, меняет формулировку риска) — дерево кода при этом не меняется. На r_k+1 reuse видит: дерево не отличается от якоря вне docs/reviews/** → вердикт переиспользуется, модель не вызывается, найденная-по-плану строка «ТЗ менялось» никогда не печатается. Раздел «Риски» ТЗ утверждает обратное: «правка после этого попадёт в следующий заход» — для этого конкретного пути это неверно: следующий заход — это переиспользование без чтения вообще.

Важно: на ПЕРВОМ раунде код-ревью (переход S5→S7, live-проверка, которую называет сам AC2) reuse в принципе не может сработать — предыдущего CODE-REVIEW-документа ещё нет, reusableGreenVerdict вернёт null, модель вызывается штатно. То есть заявленная в AC2 «живая проверка» пройдёт и подтвердит AC — но не покроет описанный выше многораундовый путь, и ни один названный в ТЗ тест/мутант его не ловит.

Почему это Medium, а не High. Основной путь (первое код-ревью после зелёного ревью ТЗ) действительно закрыт корректно — уязвим только повторный раунд после rebase-only возврата, что само по себе не рядовая, а уже особая ситуация (#437-класс). Дефект в скоупе задачи и правится в ней же: либо reusableGreenVerdict/вызывающий его CLI получает дополнительный параметр «хеш тела issue совпадает с якорем», либо в разделе «Риски» ТЗ явно фиксируется этот остаточный пробел как принятый (а не отрицается фразой «не затрагивается»).

Medium-2 (в скоупе). Метка small не соответствует собственному описанию задачи как полнотрековой

Файл: метки issue #517; комментарий S2 («Трек: полный (три поверхности — конвейер, гейт, документация процесса)... сегодня формально это трек small-стиля, поэтому дополнительно ставлю метку small»).

В чём дефект. §5 PROCESS.md требует ОДНОВРЕМЕННО: сложность/риск ≤3, ОДНУ поверхность, отсутствие миграции, отсутствие нового UX-контракта, отсутствие влияния на touch/perf. Автор сам называет три поверхности (пайплайн, гейт, процесс-документация) и явно пишет «трек: полный» прямым текстом — это прямое нарушение критерия «одна поверхность», названное самим автором, а не предположение ревьюера. Несмотря на это, к issue добавлена метка small, и заявленная причина — «чтобы process-gate --issues не требовал файла ТЗ на класс A» — не соответствует коду: checkSpecs (scripts/process-gate.mjs:311-312, if (!c.classes.has('A') || c.isRelease) continue;) проверяет только коммиты, несущие класс A. У этой задачи класса A нет вовсе (только B: process.yml, scripts/**; и C: docs/**) — проверка 3 не сработала бы независимо от метки. Причина, которой оправдан выбор трека, ложная.

Последствие. Метка small снижает бюджет циклов ревью ТЗ с 4 до 2 (§4: «Для лёгкого трека лимит ревью ТЗ — 2 цикла») для задачи, которую сам автор характеризует как трёхповерхностную и полнотрековую по существу — при том что именно эта задача переписывает структурные гарантии всего конвейера ревью. Обратное несоответствие (trivial/small — обоснование не выбора, а отказа от него, #338) здесь работает в неверную сторону: критерий нарушен и назван самим автором, но метка не снята.

Рекомендация. Либо снять small (раз задача и без него не требует файла ТЗ по факту отсутствия класса A — необходимости в метке нет вообще), либо получить явное решение владельца принять урезанный бюджет циклов для этой конкретной задачи, зафиксировав его в issue.

Low-1. У1 содержит неточное утверждение о текущем коде (не блокирует, для протокола)

Файл: тело issue #517, «Уточнения S2», пункт У1.

У1 утверждает: «на spec шага material нет (он под if: stage == 'code'-веткой ребейза), понадобится снимать хеш в отдельном шаге, общем для обоих этапов». Проверка process.yml показывает обратное: шаг material (id: material, строка 403) условен ТОЛЬКО на steps.rebase.outputs.conflict != 'true'; шаг rebase, который единственный несёт условие stage=='code', на этапе spec просто не выполняется, и его output пуст — а пустая строка != 'true' истинна. Значит, шаг material уже выполняется и на этапе spec (это же ревью — прямое доказательство: якорь ниже в этом документе получит дерево/SHA, снятые этим самым шагом на spec-прогоне). Задача не блокируется этим — решение, добавлять ли новый шаг issue_body или расширить уже существующий material дополнительным выводом, относится к «чего пользователь не наблюдает» (§7.1) и свободно решается разработчиком. Но фраза «всё, на что опирается решение, сверено с кодом» в S2 не вполне точна: там же назван несуществующий §10.5 п.3 (реальный номер — §10.2 п.3, «Что проверяет process-gate.mjs») и несуществующая функция acceptanceFrom в task-packet.mjs (реальный механизм — тернарник acSource плюс extractAcceptanceCriteria, поведение при этом описано верно). Снимается записью: исправить формулировку У1 и, если сохраняется решение о новом шаге issue_body, явно обосновать его отдельно от ошибочной посылки «шага нет».

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

  • AC1, AC3, AC4, AC5 — однозначны, доказательство названо для каждого (unit/тест на фикстурах/тест на текст), формулировки не оставляют места для трактовки.
  • Обязательные разделы §7.1 присутствуют; неприменимые (UX, модель данных/миграция, i18n, производительность) явно помечены неприменимыми с обоснованием («изменения только в конвейере... продукт и словари не затрагиваются»), а не молча пропущены.
  • Блок принятых предположений оформлен по правилу (У4 — «принято предположительно, менять свободно»), продуктовых открытых вопросов владельцу нет и не должно быть: персона задачи — автор ТЗ/ревьюер, не пользователь продукта (docs/SCOPE.md эту поверхность не описывает и не обязан); единственный «технический вопрос» (нормализация тела) автор корректно закрыл сам, не эскалируя владельцу — соответствует правилу «на этапе аналитики технические вопросы не задаются».
  • Обратная совместимость формата якорей (У3: новая строка Тело issue: независима от specs, старые документы читаются как раньше через anchorIssueBodyFrom → null) подтверждена по образцу уже существующих anchorTreeFrom/anchorVerdictFrom, которые именно так и устроены.
  • Откат описан конкретно (перечислены все файлы), риск лимита тела GitHub (65 536 знаков) сопоставлен с реальным максимумом ТЗ проекта (21 КБ, docs/specs/162-*.md) — не голословно.
  • «Не входит» и скоуп/не-скоуп разделены явно, смешанных вопросов владельцу не задано.
  • SCOPE.md намеренно не применяется как рамка приёмки: задача не продуктовая (User-Visible: no, персона — внутренний процесс), это соответствует собственному правилу PROCESS.md об инфраструктурных задачах, а не пробел.

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

  • Не прогонялись typecheck/test/build/process-gate — кода нет, ветки issue/517-* не существует на момент ревью; на этапе ТЗ это гейты следующего этапа.
  • Не проверялась синтаксическая корректность будущих правок YAML (process.yml) — их ещё не существует; проверялась только текущая, ДО правки, структура шагов, на которой основаны находки Medium-1/Low-1.
  • Не оценивалось качество мутационных гардов (review-anchor-drops-issue-body и т.д.) сверх того, что они названы и привязаны к конкретным чистым функциям — их реализация появится в коде, а не в ТЗ.

Вердикт

Жёлтый. High: 0, Medium: 2 (оба в скоупе задачи, чинятся в ней же, без повторного ревью после исправления по существу — только новый заход того же цикла ТЗ). Low: 1, снимается записью в этом документе, отдельного действия не требует.


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

  • Ветка: dev, коммит a824acc1b18a — ребейз его осиротит, и это нормально: ниже якоря, которые ребейз не меняет.
  • Дерево материала: 91c557112388667494babbaaa83f5f6a75195628
    git log --all --format='%H %T' | grep 91c557112388
    
  • Вердикт конвейера: yellow · High 0