mirror of
https://github.com/Matysh/houseplan-card
synced 2026-10-04 05:41:34 +00:00
259 lines
21 KiB
Markdown
259 lines
21 KiB
Markdown
# 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 ещё не смёржен) страховка по комментариям:
|
||
|
||
```sh
|
||
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.
|
||
|
||
**Предлагаемое направление (не мандат, выбор за автором):**
|
||
1. Ужесточить страховку по комментариям: матчить не голый `CODE-REVIEW`, а
|
||
что-то вроде `CODE-REVIEW-<NUM>-r` — привязка к номеру issue резко снижает
|
||
шанс случайного совпадения в прозе, оставаясь той же «прозой», от которой
|
||
ТЗ явно отказывается полагаться как на основной источник, но убирает
|
||
наиболее грубый класс ложных срабатываний из резервного источника;
|
||
и/или
|
||
2. Добавить видимость расхождения: `::warning::`, когда
|
||
`attemptComments > attemptFiles + 1` без объясняющего файла — сегодня
|
||
расхождение тихое, лог `guard` печатает только итоговое число;
|
||
и/или
|
||
3. Смягчить формулировку «перерасчёт невозможен по построению» в
|
||
`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_INPUTS`
|
||
`scripts/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.yml` `main`↔`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 факт» насчёт номера).
|
||
|
||
---
|
||
|
||
<!-- material-anchors: сгенерировано конвейером (#414) -->
|
||
|
||
## Материал раунда
|
||
|
||
- Ветка: `issue/454-review-round-counter`, коммит `634478a51077` — ребейз его осиротит, и это нормально: ниже якоря, которые ребейз не меняет.
|
||
- Дерево материала: `f8467cb113cd3c09cf3a10235bb7034ee9121063`
|
||
```
|
||
git log --all --format='%H %T' | grep f8467cb113cd
|
||
```
|
||
- ТЗ `docs/specs/454-review-round-counter.md`, блоб `b5af292101a495e4699d839397e837a3a2467c85`
|
||
```
|
||
git log --all --find-object=b5af292101a495e4699d839397e837a3a2467c85 -- docs/specs/454-review-round-counter.md
|
||
```
|