21 KiB
CODE-REVIEW — issue #454 · заход r3 (см. «Заход vs факт» ниже)
- Issue: https://github.com/Matysh/houseplan-card/issues/454
- Материал:
git rev-parse HEAD=4b443d8c(веткаissue/454-review-round-counter, ранее реализация была на634478a5; конвейер перед ревью привёл её кdevчерез ребейз (+12 коммитовdev, включая05ef3181,aa51ed2b,8935a736,169fed01,4203a01b,2202b207и др.) — после ребейза это другой код (PROCESS.md §7.2), поэтому разбор ниже полный, не по дельте. git diff origin/dev...HEAD --stat: 8 файлов, +1226/−3 —.github/workflows/process.yml,scripts/review-doc-guard.mjs,scripts/mutation-gate.mjs,test/review-doc-guard.test.mjs,docs/specs/454-review-round-counter.md,docs/specs/README.md,docs/reviews/SPEC-REVIEW-454-r1.md,-r2.md.- Класс изменения: B (инфраструктура),
User-Visible: noна всех коммитах — провереноgit show -s --format=%Bна каждом из 7 коммитов диапазона.
Заход vs факт (важно для чтения этого документа)
Заголовок задачи называет это «заход r3». Проверено по таймлайну меток
(gh api repos/.../issues/454/timeline): метка S7-code-review навешена
на #454 ровно один раз, в 16:57:56. Это первый и единственный код-ревью
раунд #454 — фактически он r1. Номер r3 в имени документа — не ошибка
приложенного шаблона, а живое проявление ровно того дефекта, который #454
чинит: см. M1 ниже, где это воспроизведено вплоть до реального лога guard
этого самого прогона. Оставляю нумерацию файла как задаст шаг публикации
(CODE-REVIEW-454-r3.md) — переименовывать его вручную значит повторить
ту же ошибку «не доверять факту конвейера», от которой и уходит #454.
Раздела «Унаследовано из r<N−1>»/«Закрытие раунда r<N−1>» в этом документе
нет: предыдущего код-ревью раунда #454 не существовало.
Скоуп
process.yml: шаг guard/decide больше не считает attempt/spent
целиком по прозе комментариев issue — рядом встаёт независимый счёт по
опубликованным docs/reviews/${marker}-${NUM}-r*.md (через новый мелкий
checkout ref: dev + gh api contents). Логика вынесена в чистые функции
scripts/review-doc-guard.mjs (reviewRoundsFromFiles, attemptFromRounds,
verdictDeclaration, blockingFromDocs, reviewCounters, CLI --counters),
покрыта test/review-doc-guard.test.mjs, три новых мутанта в
scripts/mutation-gate.mjs. docs/specs/README.md — строка индекса.
Продуктовый код (src/**), i18n, бандл, touch — не задеты.
Как проверялось — гейты
| Гейт | Команда | Результат |
|---|---|---|
| Typecheck | npx tsc --noEmit |
чисто, без вывода |
| Unit-тесты | npm test |
1936 pass / 0 fail / 1 skip |
| Build | npm run build |
сборка прошла (rollup -c), см. отдельное наблюдение о фингерпринте ниже — не относится к диффу #454 |
| Mutation gate | node scripts/mutation-gate.mjs --check |
EXIT=0, все мутанты (включая три новых review-round-*) поймали своё |
| check-docs | node scripts/check-docs.mjs |
passed (7 files, 12 links) — не обязателен для этого диффа (src/** не тронут), прогнан для полноты |
Не гоняю: golden:verify, smoke_*.mjs, pytest tests_backend, performance —
диапазон не касается рендера, геометрии, бэкенда или perf-путей. Инварианты
модели (npm run invariants) не нужны — ни ребро, ни layout, ни
marker.space не затронуты.
Находки
M1 (Medium, в скоупе задачи) — страховка по комментариям может завысить и attempt, и spent через посторонний маркер в прозе; заявление «перерасчёт невозможен по построению» не выполняется на реальных данных
Воспроизведено исполнением на боевых данных #454, не на синтетике.
Guard этого самого прогона (job 101105199249, run 33898021033,
gh api repos/Matysh/houseplan-card/actions/jobs/101105199249/logs)
напечатал:
этап code, заход 3, блокирующих циклов 0 из 4
при том, что по таймлайну меток это первый код-ревью раунд #454 (должно быть
заход 1). Причина — старая (пока не заменённая, process.yml живёт на
main, куда #454 ещё не смёржен) страховка по комментариям:
of_stage="[.comments[] | select(.body | test(\"Вердикт:\")) | select(.body | test(\"CODE-REVIEW\"))]"
Прогон дословно этим выражением на реальных комментариях #454
(gh issue view 454 --json comments, jq) даёт of_stage.length = 2:
#issuecomment-5543499255(16:29:05) — зелёный вердикт SPEC-REVIEW r2. МатчитВердикт:(свой) иCODE-REVIEW— потому что текст вердикта разбирает историю #449 и содержит фразу «маркерCODE-REVIEWв нём есть, но в прозе». Это не отсылка к собственному файлу, а цитата чужого маркера внутри анализа самой этой задачи.#issuecomment-5543835259(16:57:54) — комментарий «Реализация готова», вообще не вердикт. МатчитВердикт:— потому что в тексте есть фраза «строкиВердикт:в нём нет» (описание AC2 в обратных кавычках), иCODE-REVIEW— потому что в тексте разбираются найденные баги наCODE-REVIEW-441-r1.md/CODE-REVIEW-230-r2.md/«CODE-REVIEW 439».
Оба совпадения — ложные: ни один из двух комментариев не является
код-ревью-вердиктом #454. attempt = 2 + 1 = 3, spent = 0 (ни один из двух
не «жёлтый/красный» в своей строке) — совпадает с логом guard buкв в букву.
Тот же результат получен НОВЫМ кодом, который и рецензируется, не только
старым shell (проверено исполнением на HEAD):
$ node -e 'import("./scripts/review-doc-guard.mjs").then(m => console.log(
m.reviewCounters({ rounds: [], docs: [], comments: { attempt: 3, spent: 0 } })))'
{
attempt: 3, spent: 0,
attemptFiles: 1, spentFiles: 0,
attemptComments: 3, spentComments: 0,
blocking: [], unread: []
}
rounds: [] — потому что ни одного CODE-REVIEW-454-r*.md ещё не
опубликовано (проверено git ls-tree -r HEAD --name-only | grep 454 — есть
только два SPEC-REVIEW-454-*). Файловая половина эту задачу считает
абсолютно верно (attemptFiles=1). Ломает её Math.max со второй, не
переписываемой этим ТЗ половиной.
Почему это не «синтетический край», а найденный вживую пример класса #89.
ТЗ и AC5 формулируют защиту так: «Вердикт чужого этапа не влияет на счёт
(#89 не сломан)» — без оговорки. По факту это верно только для
reviewRoundsFromFiles/attemptFromRounds (доказано юнитом
«чужой этап и чужая задача в счёт не идут», test/review-doc-guard.test.mjs:300)
и НЕ верно для итогового reviewCounters, потому что attemptComments
считается прежним, недоказанным способом и участвует в max() наравне с
надёжным источником. В параграфе §5 самого ТЗ это фактически признано
оговоркой «по файлам» («Правило… по файлам оно выполняется строго»), но
таблица AC5 эту оговорку не несёт, а раздел §3 идёт дальше и заявляет
«перерасчёт невозможен по построению» — это неверно в общем виде: невозможно
удвоение счёта (сумма против максимума), но не невозможно завышение одной из
компонент максимума, и именно это сейчас произошло.
Чем краснеет сильнее, чем в этом раунде. В этом прогоне spentComments
совпал с spentFiles (оба 0) только потому, что заражающий SPEC-REVIEW-вердикт
был зелёным. Формула blocking смотрит на цвет строки вердикта самого
комментария, а не на то, кому вердикт принадлежит: будь заражающий комментарий
жёлтым/красным вердиктом СВОЕГО (не CODE-REVIEW) этапа, spentComments вырос
бы, max(spentFiles, spentComments) унаследовал бы это, и код-ревью потерял
бы цикл бюджета §4 раньше, чем реально стартовал, — тот самый вред, ради
которого заведено #89 и который AC5 обещает не допустить.
Практический эффект сегодня — не потеря артефакта (docs/reviews/ пуст
для CODE-REVIEW-454-*, коллизии нет) и не потеря бюджета (spent верен),
а искажённое имя файла: шаг публикации назовёт этот документ
CODE-REVIEW-454-r3.md, оставив r1/r2 навсегда пропущенными для этой
задачи. Не деструктивно, но прямо противоречит зафиксированному в ТЗ «После:
каждый заход получает собственное имя файла» — здесь заход получает чужое.
Почему Medium, а не High. Первичный вред, ради которого заведено #454 —
уничтожение предыдущего артефакта ревью (#220/#365-класс) — этим случаем не
демонстрируется: коллизии имени нет, spent не искажён. Демонстрируется
недоказанность одного конкретного защитного заявления (AC5/§3) в комбинации
источников, а не поломка первичной гарантии. Это чинится в текущей задаче
(в скоупе — счёт раундов ровно то, чем #454 занимается), не отдельным issue.
Предлагаемое направление (не мандат, выбор за автором):
- Ужесточить страховку по комментариям: матчить не голый
CODE-REVIEW, а что-то вродеCODE-REVIEW-<NUM>-r— привязка к номеру issue резко снижает шанс случайного совпадения в прозе, оставаясь той же «прозой», от которой ТЗ явно отказывается полагаться как на основной источник, но убирает наиболее грубый класс ложных срабатываний из резервного источника; и/или - Добавить видимость расхождения:
::warning::, когдаattemptComments > attemptFiles + 1без объясняющего файла — сегодня расхождение тихое, логguardпечатает только итоговое число; и/или - Смягчить формулировку «перерасчёт невозможен по построению» в
docs/specs/454-review-round-counter.md§3 и в комментарииprocess.ymlдо того, что реально доказано («не даёт двойного счёта одного раунда», а не «не может завысить»), и закрепить тестом текущее (несовершенное) поведение вместо документирования отсутствующей гарантии.
Любое из трёх снимает находку; я не настаиваю на конкретной реализации.
Наблюдения — не находки, не блокируют
- Бандл
custom_components/houseplan/frontend/houseplan-card.jsуже устарел относительно фингерпринта исходниковdev(проверено: свежийnpm run buildдаёт другой__HOUSEPLAN_BUILD_FINGERPRINT__, чем закоммиченный). Причина найдена: коммит28a4cb5e(issue #455, уже вdev) поменялpackage.json— входит вBUILD_INPUTSscripts/source-fingerprint.mjs— без последующегоbundle:sync. К #454 отношения не имеет (диф #454 не трогаетsrc/**/package.json/dist/**), предсуществует наdevнезависимо от этой задачи, и штатный механизм (rollup.config.mjs: несовпавший фингерпринт обязан «fail closed» перед golden/perf-съёмкой) рассчитан ровно на такой разрыв. Не завожу отдельный issue: не деструктивно, самоисправится ближайшимbundle:syncперед любой задачей, трогающей рендер/perf. Владельцу стоит знать на случай, если ближайшая golden-задача упрётся в неожиданный отказ. docs/specs/README.md: строка#454вставлена перед#440/#447— таблица в остальном идёт по возрастанию номера issue. Косметика, не правлю находкой.
AC — таблица доказательств
| AC | Критерий | Доказано | Комментарий |
|---|---|---|---|
| AC1 | attempt = max(номер)+1, не count+1 |
unit, чтением | test/review-doc-guard.test.mjs:288 — дыра [1,3]→4 проверена |
| AC2 | «#449 как есть» → attempt=3, spent=1 |
unit на слепке реальных данных | перепроверено мной независимым gh снимком #449 — числа совпадают |
| AC2b | Реконструкция r1/r2/r3 → attempt=4, spent=2 |
unit | пройден |
| AC3 | Имя файла никогда не повторяется | unit + мутант review-round-counts-files-not-max |
мутант краснеет корректно (проверено --check) |
| AC4 | Зелёный вердикт цикл не тратит | unit | test/review-doc-guard.test.mjs «зелёный вердикт цикла не тратит» |
| AC5 | Вердикт чужого этапа не влияет на счёт (#89) | unit — только для reviewRoundsFromFiles |
см. M1: не доказано для reviewCounters в целом; комментарийная половина не покрыта тестом на это утверждение и на реальных данных нарушается |
| AC6 | Отказ публикации не занижает счёт | unit + мутант review-round-drops-comment-insurance |
пройден |
| AC7 | Нет ветки/API недоступен → поведение как раньше | unit | пройден, включая «мусор на входе» |
| AC8 | process.yml идентичен в main и dev |
существующий шаг Validate | сейчас идентичны (проверено diff); автор откладывает зеркалирование в main до после мержа — обоснованно, гейт сравнивает ветки между собой и покраснел бы раньше времени |
| AC9 | Логика в тестируемом модуле, не в inline-shell | unit + чтение | test/review-doc-guard.test.mjs проверяет сам текст шага decide на вызов node scripts/review-doc-guard.mjs --counters |
Что проверено и корректно
- Файловая половина счёта (
reviewRoundsFromFiles,attemptFromRounds,verdictDeclaration,blockingFromDocs) — корректна, включая защиту от цитирования чужого вердикта («описание чужого раунда не объявляет вердикт», коммит4b443d8c, добавлен по итогам находки самим автором на корпусе 629 документов) и от блока внутри тройных кавычек не по месту. - Три новых мутанта реально ловятся штатным раннером (
--check, exit 0). - Трейлеры всех 7 коммитов диапазона:
Issue: #454,User-Visible: no— корректно для класса B, changelog не требуется. process.ymlmain↔devсовпадают сегодня; план зеркалирования после мержа обоснован и не является дефектом.- Гейты (typecheck/test/build/mutation/check-docs) зелёные на
HEAD.
Чего не проверял
golden:verify,smoke_*.mjs,pytest tests_backend, performance-профили, инварианты модели — diff их не касается.- Реальный прогон
guardС НОВЫМ кодом на живом GitHub Actions (сам workflow ещё не вmain/dev) — логика проверена юнитами и прямым вызовомreviewCounters/--countersна данных, снятых с реального API, но не в контексте самогоjobцеликом (шаг разрешения ветки черезgit ls-remote/gh api matching-refs, постраничный лимит 1000 файлов) — эта часть осталась shell-кодом без юнитов по прямому требованию ТЗ («guard остаётся лёгким», §4 ТЗ) и проверена только чтением. - Не пересчитывал вручную мутацию
review-round-drops-file-sourceпострочно за пределами штатного раннера — доверился--check.
Вердикт
Один Medium в скоупе, без High. Возврат автору тем же issue (#454), не отдельный issue.
Документ: docs/reviews/CODE-REVIEW-454-r3.md (публикуется шагом конвейера;
см. раздел «Заход vs факт» насчёт номера).
Материал раунда
- Ветка:
issue/454-review-round-counter, коммит634478a51077— ребейз его осиротит, и это нормально: ниже якоря, которые ребейз не меняет. - Дерево материала:
f8467cb113cd3c09cf3a10235bb7034ee9121063git log --all --format='%H %T' | grep f8467cb113cd - ТЗ
docs/specs/454-review-round-counter.md, блобb5af292101a495e4699d839397e837a3a2467c85git log --all --find-object=b5af292101a495e4699d839397e837a3a2467c85 -- docs/specs/454-review-round-counter.md