docs: review document for #637
Проверка (CI) / Классификация изменённых файлов (push) Successful in 38s
Проверка (CI) / HACS: валидация репозитория (push) Skipped
Проверка (CI) / Hassfest: манифест интеграции (push) Skipped
Проверка (CI) / Мутанты по диффу (1/6): затронутые свидетели краснеют (push) Skipped
Проверка (CI) / Мутанты по диффу (2/6): затронутые свидетели краснеют (push) Skipped
Проверка (CI) / Мутанты по диффу (3/6): затронутые свидетели краснеют (push) Skipped
Проверка (CI) / Мутанты по диффу (4/6): затронутые свидетели краснеют (push) Skipped
Проверка (CI) / Мутанты по диффу (5/6): затронутые свидетели краснеют (push) Skipped
Проверка (CI) / Мутанты по диффу (6/6): затронутые свидетели краснеют (push) Skipped
Проверка (CI) / Фронтенд: типы, юниты, мутанты, синхрон бандла (push) Skipped
Проверка (CI) / Предполёт: документация, провенанс, процесс (push) Failing after 57s
Проверка (CI) / Переиспользование: это дерево уже проверено (push) Successful in 2m0s
Проверка (CI) / Смоки в браузере (шард 1 из 3) (push) Skipped
Проверка (CI) / Смоки в браузере (шард 2 из 3) (push) Skipped
Проверка (CI) / Смоки в браузере (шард 3 из 3) (push) Skipped
Проверка (CI) / Смоки: все шарды зелёные (push) Skipped
Проверка (CI) / Golden-кадры против принятых эталонов (push) Skipped
Проверка (CI) / Перф-смок: бюджет времени кадра (push) Skipped
Проверка (CI) / Геометрия: TS/Python parity исполнена (push) Skipped
Проверка (CI) / Бэкенд: pytest в Home Assistant (push) Skipped
Проверка (CI) / Доказательство выполненных проверок (push) Failing after 28s

Issue: #637
User-Visible: no
This commit is contained in:
claude[bot]
2026-09-23 08:57:40 +00:00
parent 44aa358060
commit 4366a1c796
+197
View File
@@ -0,0 +1,197 @@
# CODE-REVIEW · issue #637 · заход r2
Материал: `44aa35806050e3f3c663b317aab6016b60f88888` (рабочая копия на нём,
`git status --porcelain` пуст). Ветка `issue/637-process-metrics`, 3 коммита
поверх текущего `origin/dev` (`0b55163d`): `46edcb1f` (ребейз-эквивалент
материала r1), `5df8cfc0` (документ ревью r1, публикация), `44aa3580` (фикс
находки r1). Класс B: `scripts/process-metrics.mjs`,
`test/process-metrics.test.mjs`. Трейлеры `Issue: #637`, `User-Visible: no` —
верно, видимого поведения продукта нет.
Трек — инфраструктурный (#562, §1): ни одного файла класса A, задача входит в
общий флоу сразу на `S7-code-review`.
## Раунд по дельте (§2.10)
1. **Вердикт и материал r1.** `docs/reviews/CODE-REVIEW-637-r1.md` (закоммичен
в `5df8cfc0`) — жёлтый, заход r1, блокирующих циклов 0/4, High 0, Medium 1
в скоупе. Материал раунда объявлен в самом документе:
SHA `966928135b6a40fd6a73c7631bd514a79015d48a`, дерево
`0687eddba719e669212123bf80bf8c6d683bbf50`, хеш тела issue
`187c1bf4bbdea65f4114112ea0ee62c313cb015d343fbb44d68507420f204e41`.
2. **SHA не резолвится** — `git cat-file -t 966928135b6a...` и поиск дерева
`0687eddba719` по `git log --all --format='%H %T'` дают пустой результат.
Это обычное дело (§2.10, п.2): автор явно пишет «Материал:
`44aa3580…` после rebase на актуальный `dev`». Проверил, что это чистый
ребейз, а не скрытая правка: родитель `46edcb1f` (эквивалент старого
материала на новой базе) — это ровно текущий `origin/dev` (`0b55163d`,
`git merge-base origin/dev 46edcb1f` совпадает), диапазон файлов и их
размер (`process-metrics.mjs` 291 строка, `test` 116 строк до фикса)
соответствуют тому, что описано и процитировано в документе r1
(`buildReport`, строки 162–165, тот же код). Значит дельта раунда — это
ровно коммит `44aa3580` (`scripts/process-metrics.mjs` +8/−4,
`test/process-metrics.test.mjs` +23), а не результат смены поведения из-за
рейбейза. Разбор по дельте оправдан: `dev` не «ушёл вперёд» относительно
материала — ребейз тривиальный, чужой скоуп не задет, новая подсистема не
затронута, объём дельты (31 строка) несопоставим с исходной задачей.
3. **Дельта объявлена:** `git diff 46edcb1f..44aa3580` = сам коммит
`44aa3580` (см. ниже «Закрытие раунда r1»).
## Закрытие раунда r1
| Находка r1 | Чем закрыта | Где это видно |
|---|---|---|
| Medium: `hours(median(...) ?? NaN) \|\| null` в `buildReport` схлопывает настоящую медиану 0 ч в `null`/«—» (`scripts/process-metrics.mjs:162-165` на материале r1) | Введена `medianHours(values)` (`scripts/process-metrics.mjs:35-38`): различает «нет данных» (`median` вернул `null` → `null`) и любой числовой результат, включая 0, через `value === null ? null : hours(value)` — без финального `\|\| null`, который путал `0` (falsy) с отсутствием данных. Все четыре медианы (`medianLeadToS7Hours`, `medianReviewToMergeHours`, `medianLeadToS8Hours`, `medianSpecLeadHours`, `scripts/process-metrics.mjs:166-169`) переведены на неё | `scripts/process-metrics.mjs:35-38,166-169`; новый тест `test/process-metrics.test.mjs:108-129` — воспроизводит ровно сценарий из репро r1 (переходы короче ≈3 минут) и проверяет `[0, 0, 0.1, 0]` вместо `[null, null, null, null]`, плюс отдельно проверяет, что пустая выборка по-прежнему даёт `null`/«—» (не путает две ветки местами) |
Мутационное доказательство сделал сам (мутация не входит в реестр — фикс в
чистом юните, п.8 §2.7 разрешает прогон со снятой защитой прямо в ревью, без
`scripts/mutation-gate.mjs`): вручную вернул старое выражение
`hours(median(...) ?? NaN) || null` во все четыре поля, прогнал
`node --test test/process-metrics.test.mjs` — новый тест красный
(`AssertionError [ERR_ASSERTION]` на `deepStrictEqual`, строка 118), остальные
7 тестов проходят. Откатил патч (`cp` из бэкапа), `git status --porcelain`
снова пуст, повторный прогон — 8/8 pass. Тест умеет падать.
## Унаследовано из r1
Без повторной проверки приняты (документ `docs/reviews/CODE-REVIEW-637-r1.md`,
материал которого эквивалентен текущему `46edcb1f`, см. п.2 выше) — дельта
`44aa3580` этих путей кода не касается:
- `issueMetrics` — вход по первой статусной метке, различение `entered`/S4/S5/
S7/S8, счётчики повторных постановок.
- `reviewRounds` — строгий регэксп имён файлов ревью, раунды по документам, а
не по событиям S7-метки (мутант `metrics-rounds-by-s7-events`, пойман).
- `runMetrics` — группировка прогонов конвейера `process #NN · …` в одну
строку; остальные workflow — по имени.
- `pipelineMetrics` — skipped-прогоны не считаются в wall-time (мутант
`metrics-count-skipped-pipeline-runs`, пойман).
- `jobMinutes` — доля «Мутанты» по имени job, устойчивость к сломанным полям
времени.
- `fetchSnapshot` — только чтение (`gh api` issues/timeline/actions/runs +
`git ls-tree`), пагинация, фильтр по `closed_at` внутри окна.
- `.github/workflows/process-metrics.yml` — `permissions` только на чтение,
расписание понедельник 05:00 UTC + `workflow_dispatch`, код и `ref: dev`.
- Трейлеры коммита и структура §2.10-блока материала.
Инвариант этого наследования: сама дельта (`buildReport`, только четыре поля
медиан плюс новый хелпер) не может задеть ни один из перечисленных путей —
они не вызывают и не вызываются из изменённых строк. Проверил чтением диффа
`44aa3580`, других правок в `scripts/process-metrics.mjs` нет.
## AC — что дельта задевает
- **AC1** (скрипт воспроизводит цифры аудита ±10 %): фикстура аудита
(`test/process-metrics.test.mjs:60-106`, unchanged) по-прежнему проходит —
значения там не нулевые, фикс их не касается численно, только путь для
нулевого случая. Дельта расширяет доказательство AC1 на нулевой случай,
который сама формулировка AC подразумевает («самый быстрый переход» —
ровно то, что баг маскировал). Живой прогон против реального `gh api` за
15–22.09 остаётся не выполненным — это унаследовано из r1 (см. ниже «Чего
не проверял»), делта на это не влияет.
- **AC2** (тест на парсинг фикстур): дельта добавляет восьмой тест, число
тестов выросло с 7 до 8, все проходят — доказательство AC2 усилилось, не
ослабло.
- **AC3** (первый отчёт опубликован автоматически): не задета дельтой,
наследуется из r1 как открытая до пост-мерж действия владельца
(зеркалирование workflow в `main`) — это явно вне контроля код-ревью и не
являлось находкой в r1.
## Гейты
- `node --test test/process-metrics.test.mjs` — прогнал сам: **8 pass, 0
fail** (7 из r1 + новый регрессионный).
- Мутация находки r1 (снятие `medianHours`, возврат старого выражения) —
прогнал сам: новый тест краснеет, остальные держатся; откатил, дерево чистое.
- Мутанты `metrics-count-skipped-pipeline-runs`,
`metrics-rounds-by-s7-events` — не перепрогонял: код, который они проверяют
(`pipelineMetrics`, `reviewRounds`), дельтой не тронут (см. «Унаследовано»),
а сам реестр `scripts/mutation-registry.mjs` не менялся между `46edcb1f` и
`44aa3580` (дифф двух файлов, реестра нет). Инвариант r1 переносится.
- `npx tsc --noEmit`, `npm test` (полный), `npm run build` со сверкой копий
бандла — не гонял: Validate на этом самом SHA (`44aa3580`) зелёный
(https://github.com/Matysh/houseplan-card/actions/runs/35839128726), дифф с
тех пор не менялся (рабочая копия чистая на этом SHA).
- `node scripts/check-docs.mjs` — не требуется: дифф не трогает `src/**`
(только `scripts/`, `test/`).
- `npm run invariants` — не требуется: геометрия, `layout`, толщина стен,
`marker.space`/`open_spans` не затронуты.
- `demo/smoke_*.mjs`, `npm run golden:verify` — не требуется: дифф не меняет
рендер, карточку, стили или слои; это чистые функции над снимком GitHub.
- `python -m pytest tests_backend -q` — не требуется: `custom_components/**`
не тронут.
- `test/single-source-numbers.test.mjs` — не относится: метрики процесса не
пользовательские величины карточки.
## Что проверено и корректно
- Фикс сохраняет форму данных: `medianHours` возвращает `null` только когда
`median()` сам вернул `null` (пустая выборка после `filter(Number.isFinite)`
в `median`, `scripts/process-metrics.mjs:29-34`), в остальных случаях —
всегда `hours(value)`, включая 0. Прочитано и подтверждено регрессионным
тестом на обеих ветках (пустая выборка и нулевая медиана в одном тесте, не
раздельно — так тест не может «повезти» и спутать ветки местами).
- `renderMarkdown`/`fmt` (`scripts/process-metrics.mjs:185`) корректно
отображает `0` как `0 ч`, а не как «—»: `value == null` ложно для `0`,
`Number.isNaN(0)` ложно — код `fmt` не менялся в этой дельте и уже был
правильным, баг был только в вычислении значения выше по цепочке, не в
форматировании. Подтверждено новым тестом
(`/Медиана вход → S7 \| 0 ч/`).
- Округление `hours()` (`Math.round((ms/3_600_000)*10)/10`) — проверил вручную
арифметику теста: 2 мин → 0.0(3) ч → округление до 0; 3 мин → 0.05 ч →
`Math.round(0.5)=1` (JS всегда вверх на `.5`) → 0.1 ч. Ожидаемый вектор
`[0, 0, 0.1, 0]` в тесте математически верен, не «подогнан» под реализацию.
- Трейлеры `44aa3580`: `Issue: #637`, `User-Visible: no` — соответствуют
`git show -s --format=full`.
- Хендофф-комментарий владельца/исполнителя точно называет материал раунда
(`44aa35806050e3f3c663b317aab6016b60f88888`) и он совпадает с
`git rev-parse HEAD` рабочей копии — соответствует §2.7 (вердикт привязан к
SHA, сверено непосредственно перед итогом).
## Чего не проверял
- Живой прогон `fetchSnapshot`/CLI против реального `gh api` — наследуется из
r1, не задет дельтой; автор по-прежнему планирует сверку с таблицей аудита
первым живым запуском после зеркалирования workflow в `main`.
- Мутанты `metrics-count-skipped-pipeline-runs`,
`metrics-rounds-by-s7-events` — не перепрогонял в этом раунде, обоснование
выше («Гейты»): код, который они проверяют, дельтой не задет.
- `npx tsc --noEmit`/`npm test` (полный)/`npm run build` — не гонял, опираюсь
на зелёный Validate на точном SHA материала (ссылка выше), как разрешает
сокращение гейтов ревью.
## Вывод
Единственная находка r1 (Medium, в скоупе) закрыта точечным фиксом:
`medianHours` корректно различает «данных нет» и «медиана равна 0»,
регрессионный тест воспроизводит исходное репро и умеет падать (проверено
мутацией вручную). Делта локальна, не задевает ни один из путей, признанных
корректными в r1, AC1/AC2 доказательство усилено, AC3 не в скоупе дельты.
High-находок нет, новых Medium/Low не нашёл.
**Вердикт: зелёный.**
## Материал раунда
- Ветка: `issue/637-process-metrics`, коммит `44aa35806050e3f3c663b317aab6016b60f88888`.
- Рабочая копия ревью — на этом SHA, `git status --porcelain` пуст.
- Предыдущий материал (r1) осиротел ребейзом (ожидаемо, §2.10 п.2): SHA
`966928135b6a40fd6a73c7631bd514a79015d48a` не резолвится; рабочий эквивалент
на текущей базе — `46edcb1f37a7bad93de97e04e2307f67d0d310d4`
(`git merge-base origin/dev 46edcb1f` = `origin/dev` = `0b55163d`).
---
<!-- material-anchors: сгенерировано конвейером (#414) -->
## Материал раунда
- Ветка: `issue/637-process-metrics`, коммит `44aa35806050` — ребейз его осиротит, и это нормально: ниже якоря, которые ребейз не меняет.
- Дерево материала: `c5dd9f105b40966232d871cb5b2a8dfbb5cf54fb`
```
git log --all --format='%H %T' | grep c5dd9f105b40
```
- Тело issue: `187c1bf4bbdea65f4114112ea0ee62c313cb015d343fbb44d68507420f204e41`
- Вердикт конвейера: `green` · High 0