Files
houseplan-card/docs/reviews/CODE-REVIEW-635-r2.md
T
2026-09-23 13:52:23 +00:00

250 lines
24 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# CODE-REVIEW-635-r2
- **Issue:** #635 «Индекс документов ревью, уроки, правило хранения раундов»
- **Этап:** code (PROCESS.md §2.7)
- **Заход:** r2 · блокирующих циклов израсходовано 1 из 4 (лимит 4)
- **Материал:** `3e96b37656b9ce49cd56e1aa2f65ebf0808f113b` (рабочая копия репозитория была на нём; `git fetch`/`checkout` не выполнялись). Диапазон `origin/dev..HEAD` — три коммита: `898642f8` (исходная реализация #635), `4d9754a1` (публикация документа r1), `3e96b376` (правка r2).
## Скоуп
r1 (`CODE-REVIEW-635-r1.md`, SHA `db0f5b9b1be025fca86736f70d6aedd71d057487`) закрыл вердикт жёлтым с одним High: `parseCounts` брал первое совпадение `High:`/`Medium:` по всему тексту документа (часто — цитата чужого раунда), а `parseFindings` не знал живых форматов заголовков находок (`### Medium (в скоупе…) — …`), из-за чего у 108/990 документов колонки H/M были ненулевые, а «Находки» — пустые, и `grep form-kit docs/reviews/INDEX.md` не находил ничего вопреки собственному примеру автора.
Дельта r2 — ровно один коммит `3e96b376` поверх `4d9754a1`, 4 файла: `scripts/reviews-index.mjs` (+182/-х строк), `test/reviews-index.test.mjs` (+63), `scripts/mutation-registry.mjs` (+11), `docs/reviews/INDEX.md` (пересборка). Это переписывает `parseVerdict`/`parseCounts`/`parseFindings` почти целиком и добавляет `parseFiles` — объём дельты (182 строки правки в файле, который в исходной реализации весил 159 строк) сопоставим с исходной задачей, поэтому разбор в этом раунде — полный по изменённому коду (`scripts/reviews-index.mjs`, тесты, мутант), а не только «проверка, что H1 закрыт». `PROCESS.md`, `docs/LESSONS.md`, `.github/workflows/process.yml`, `parseDocName` дельтой не задеты — унаследованы из r1 (раздел ниже).
## Как проверялось
1. Прочитан полный `git diff 4d9754a1..3e96b376` для всех четырёх файлов дельты.
2. Прочитан целиком текущий `scripts/reviews-index.mjs` (298 строк) и `test/reviews-index.test.mjs` (161 строка).
3. `node --test test/reviews-index.test.mjs` — 9/9 pass (включая тест живого каталога, который читает реальный `docs/reviews/`).
4. Мутант `reviews-index-counts-first-match` проверен вручную: патч `for (const scope of [text]) {` вместо цепочки приоритетов → целевой тест падает (`node --test --test-name-pattern="#635 r2: счётчик" test/reviews-index.test.mjs`), файл восстановлен. Тест умеет падать.
5. Дословно воспроизведены оба заявления автора против живого `docs/reviews/INDEX.md`: `grep -c form-kit` → 6 (совпадает); строка `#635 | CODE-REVIEW-635-r1.md` теперь показывает 🟡 1 0 с текстом находки (совпадает).
6. Прогнан `node scripts/reviews-index.mjs --dir=docs/reviews --check` на самом материале ревью (без модификаций рабочей копии, только чтение) — обнаружил H1 ниже.
7. Для H2 (см. ниже) — прямой вызов экспортированных `parseFindings`/`parseCounts` на реальных файлах `docs/reviews/CODE-REVIEW-485-r4.md` и `docs/reviews/CODE-REVIEW-258-r1.md` через `node -e`, плюс скрипт-сканер по всему `docs/reviews/`, воспроизводящий внутреннюю логику `severityBlocks`, чтобы оценить масштаб (11 документов пойманы узкой эвристикой, вероятно больше).
## Находки
### H1 — `docs/reviews/INDEX.md`, зафиксированный в материале ревью, устарел на собственном SHA — не проходит `--check`, которым сам же себя проверяет
**Файл:** `docs/reviews/INDEX.md` (весь материал ревью, `3e96b376`).
Воспроизведение — три команды, ничего в рабочей копии не менялось:
```
$ git rev-parse HEAD
3e96b37656b9ce49cd56e1aa2f65ebf0808f113b
$ git status --short
(пусто)
$ node scripts/reviews-index.mjs --dir=docs/reviews --check
docs/reviews/INDEX.md устарел — пересобрать: node scripts/reviews-index.mjs
```
Регенерация в `/tmp` и построчный `diff` с закоммиченным файлом показывают ровно
4 отсутствующие строки — весь issue `#625` (`SPEC-REVIEW-625-r1.md`,
`SPEC-REVIEW-625-r2.md`, `CODE-REVIEW-625-r1.md`, `CODE-REVIEW-625-r2.md`) — и
изменившуюся шапку: «Документов: 992, issue: 345» вместо фактических 996/346.
Эти четыре файла реально лежат в дереве материала (`git ls-files docs/reviews | grep 625`
их находит, `git log --oneline -1 -- docs/reviews/CODE-REVIEW-625-r1.md` → `f10dd1eb`,
предок `3e96b376`), т.е. не артефакт окружения ревьюера, а часть проверяемого коммита.
Причина, по всей видимости, в многократных ребейзах ветки (`Ревью не запускалось`
×3 в истории issue): `docs/reviews/INDEX.md` — статичный blob, который
перегенерируется только явным запуском скрипта; когда ветку перевозили на новый
`dev`, база успевала получить новые документы ревью (`#625`), а уже закоммиченный
снимок индекса — нет. `--check` — это именно тот гейт, который должен был поймать
расхождение, но он существует только как локальная CLI-опция; ни в одном workflow
(`grep -rn reviews-index .github/workflows/*.yml` — единственное вхождение
в `process.yml`, шаг публикации, не проверка) он не запускается на PR/ветке как
блокирующий шаг. `npm test` тоже не ловит: тест «живой каталог» (`test/reviews-index.test.mjs:87-93`)
проверяет свойства `docs/reviews/` напрямую (покрытие, доля распознанных
вердиктов), а не совпадение с уже закоммиченным `INDEX.md` — так что зелёный
Validate на этом SHA (о котором сказано в задании ревью) действительно не покрывает
этот дефект, это не пропуск в моей проверке гейтов.
Последствие: ровно то, ради чего заведён #635, — «агент, берущий задачу по
диалогам, не найдёт [решение или урок], кроме как перечитав документ» —
воспроизводится внутри собственного индекса: обе находки `#625` невидимы через
`INDEX.md`, хотя документы существуют. AC1 («индекс покрывает 100 % документов
каталога») не выполнено на материале ревью.
**Серьёзность:** High, в скоупе. Фикс: перед коммитом дельты (или в отдельном
шаге пайплайна) запускать `node scripts/reviews-index.mjs --dir=docs/reviews`
против **финального** дерева ветки (после последнего ребейза), не против
локального кеша разработчика; либо жёстко привязать регенерацию к
пред-мержевому гейту, а не только к шагу публикации документа ревью.
### M1 — `parseFindings`/`parseFiles`: фолбэк «первая строка тела блока» вырезает начало буллета и подставляет обрывок продолжения
**Файл:** `scripts/reviews-index.mjs:190-196` (ветка `severityBlocks` → `first`).
Когда у заголовка находки (`### Low`, `### Medium` и т. п.) нет собственного
текста после разделителя и нет пронумерованных пунктов (`**M1. …**`), код берёт
первую строку тела, которая не начинается с `|#<-`:
```js
const first = block.body.map((l) => l.trim()).find((l) => l && !/^[|#<-]/.test(l));
```
Символ `-` в этом исключении означает «пропустить маркер буллета», но
эффект — пропустить **весь буллет**, если он в один физический абзац не
уместился и перенесён на следующую строку (обычный стиль этой кодовой базы —
переносы строк внутри длинного предложения). Тогда «первой подходящей
строкой» становится не начало пункта, а его хвост — бессвязный вне контекста.
Воспроизведение (реальный файл материала, без изменений):
```
$ node -e "
import('./scripts/reviews-index.mjs').then(({parseFindings}) => {
console.log(parseFindings(require('fs').readFileSync('docs/reviews/CODE-REVIEW-485-r4.md','utf8')));
});"
[ 'пусто). Не эскалирую третий раунд подряд по той же логике r2/r3: защитные' ]
```
Настоящий текст пункта в документе (`docs/reviews/CODE-REVIEW-485-r4.md:110-113`):
«Нет golden-сцены для радара (`find demo/golden -iname "*radar*"` — по-прежнему
пусто). Не эскалирую третий раунд подряд по той же логике r2/r3…» — индекс
показывает обрывок с несогласованным «пусто)» в начале, будто предложение
начинается с закрывающей скобки.
Это не единичный случай. Точечный скан по `docs/reviews/` (репродуцирует
эвристику `severityBlocks` без изменения кода) находит минимум 11 документов с
этим паттерном, включая документы с реальным ненулевым Medium — не только
инертные «Low, унаследовано»:
- `CODE-REVIEW-258-r1.md` (🟡 M:1) — второй элемент «Находки» в индексе:
`'Записи толщины, которые не найдутся по ключу' теперь мёртв — wall_key`
(начинается с висячей одиночной кавычки — обрывок цитаты).
- `CODE-REVIEW-514-r3.md` (🟡 M:1) — второй элемент: `High не найдено. Medium
вне скоупа не найдено — единственный Medium (M1) целиком внутри…` — это не
находка, а мета-фраза «находок нет», которую `NOTHING_RE` должна была
отсеять, но отсеивает только когда это **всё** тело строки, а не когда это
«первая непустая строка» внутри более длинного абзаца.
- `CODE-REVIEW-476-r1.md`, `SPEC-REVIEW-199-r1.md`, `SPEC-REVIEW-309-r1.md`,
`CODE-REVIEW-304-r1.md`, `CODE-REVIEW-485-r1.md`, `CODE-REVIEW-485-r2.md`,
`SPEC-REVIEW-152-r1.md`, `SPEC-REVIEW-419-r1.md` — тот же паттерн (см. лог
проверки ниже).
Числа High/Medium (`parseCounts`) этим багом не затронуты — они не используют
эту ветку. Ломается именно то, ради чего добавлена колонка «Находки»/«Файлы» в
этом же раунде: агент, читающий строку индекса вместо документа, получает
синтаксически рваный, не соответствующий по смыслу текст вместо реальной сути
находки — то есть тот же класс дефекта, что и закрытый H1 r1 («индекс молчаливо
[искажает] находки»), только не пустотой, а произвольным обрывком.
**Серьёзность:** Medium, в скоупе (код и тесты дельты r2, `parseFindings` — то
самое, что в этом раунде переписывалось). Фикс в рамках задачи: при выборе
«первой строки тела» либо склеивать физически перенесённые строки одного
буллета перед поиском первой подходящей, либо не отфильтровывать строки,
начинающиеся с `-`, а прицельно вырезать только буллет-маркер (`^[-*]\s+`) и
использовать очищенную первую непустую строку целиком; плюс усилить `NOTHING_RE`
проверкой не только всей строки, но и вхождения фраз «не найдено»/«целиком
внутри» в начале извлечённого текста. Нужен тест-фикстура с многострочным
буллетом (аналогично уже добавленным фикстурам на реальных документах в этом же
раунде).
## Что проверено и корректно
- **AC2 (пример автора).** `grep form-kit docs/reviews/INDEX.md` — 6 строк
(было 0 в r1). Совпадает с заявлением.
- **Приоритет источника вердикта/счётчика.** Пересказ чужого раунда строчными
буквами в шапке документа (`CODE-REVIEW-152-r2.md:9` — «вердикт красный,
High: 1») больше не перебивает собственный зелёный вердикт документа
(строка 142: «**Вердикт: зелёный … High: 0 · Medium: 0**») — проверено и по
живому файлу, и по регэкспу (`VERDICT_OWN_LINE_RE` требует заглавную
«Вердикт» с начала строки).
- **Регресс-мутант.** `reviews-index-counts-first-match` действительно ловится
целевым тестом (проверено ручным патчем, не только доверием к отчёту автора).
- **Новые форматы заголовков находок.** `### Medium (в скоупе…) — …`,
`## Находка N (High…) — …`, `### [High] …`, `**M1. …**`-пункты внутри секции
— все разбираются тестами `test/reviews-index.test.mjs:104-150`, и я
подтвердил на реальных `CODE-REVIEW-639-r1.md`/`637-r1.md`, что
соответствующие строки `docs/reviews/INDEX.md` не пустые.
- **Собственный документ r1.** `CODE-REVIEW-635-r1.md` в живом индексе (строка
12) теперь 🟡 1 0 с корректным текстом находки — ретроспективная проверка,
что H1 закрыт не только на синтетических фикстурах.
- **9/9 юнит-тестов** `test/reviews-index.test.mjs`, включая тест живого
каталога (100 % покрытие имён, >90 % распознанных вердиктов) — прогнан лично,
не только со слов автора.
## Чего не проверял
- `npx tsc --noEmit`, полный `npm test`, `npm run build` — зачтено по зелёному
Validate на этом SHA (run `35859217407`, ссылка дана в задании ревью); диф не
трогает `.ts`/`src/**`, риск расхождения низкий.
- `node scripts/check-docs.mjs` — не запускал: diff не заходит в `src/**`
(только `scripts/`, `test/`, `docs/reviews/INDEX.md`), отпечаток скриншотов
документации не мог устареть от этой правки.
- `npm run invariants`, backend pytest, golden, performance-профили — не
относятся: геометрия, Python и рендер не затронуты.
- `demo/smoke_*.mjs` — фронтенд-дифф отсутствует, `smoke-select.mjs` не
запускал (аналогично r1, диф той же природы).
- Полный аудит остальных ~980 документов на предмет ещё не найденных
вариаций M1 — сканировал узкой эвристикой (одна конкретная форма фолбэка),
реальный масштаб может быть шире 11 найденных; не пытался перечислить все.
## Закрытие раунда r1
| Находка r1 | Чем закрыта | Где видно |
|---|---|---|
| H1 — `parseCounts` брал первое `High:`/`Medium:` по всему тексту (цитата чужого раунда, пример `CODE-REVIEW-594-r1.md`); `parseVerdict` так же терял приоритет собственной строки (пример `CODE-REVIEW-152-r2.md`); `parseFindings` не знал живых форматов заголовков (`### Medium (в скоупе…) — …`, `## Находка N (…)`, `**M1. …**`); колонки «Файлы» не было, `grep form-kit` находил 0 строк | Три правки в `scripts/reviews-index.mjs`: (1) `parseCounts`/`parseVerdict` — цепочка приоритета «своя строка `Вердикт` → секция `## Вердикт` → единственное значение по тексту → подсчёт по заголовкам» (`verdictLine`, `verdictSection`, `countsIn`); (2) `parseFindings` — `severityBlocks`/`numberedItems`/`FINDING_HEADING_RE` покрывают перечисленные живые форматы; (3) новая `parseFiles` + колонка «Файлы» | `scripts/reviews-index.mjs:50-220` (`git diff 4d9754a1..3e96b376`); тесты-регрессы на реальных документах `test/reviews-index.test.mjs:104-160` (594-r1, 152-r2, 639/637/162/141-фикстуры); в живом `docs/reviews/INDEX.md` — строка `#635 | CODE-REVIEW-635-r1.md` теперь 🟡 1 0 с текстом находки (было бы `0 0 —` до фикса), `grep form-kit` — 6 строк вместо 0; мутант `reviews-index-counts-first-match` (`scripts/mutation-registry.mjs`) — поймано вручную в этом раунде |
## Унаследовано из r1 (без повторного разбора — дельта r2 этого не касается)
Документ: `docs/reviews/CODE-REVIEW-635-r1.md`, SHA материала r1
`db0f5b9b1be025fca86736f70d6aedd71d057487`.
- **AC3 / PROCESS.md §2.10** — правило хранения раундов (все раунды текущей
линии в `docs/reviews/`, перенос в `legacy/` при стабильном релизе) записано
и принято r1; `PROCESS.md` не входит в diff `4d9754a1..3e96b376`.
- **`docs/LESSONS.md`** (12 датированных уроков) — не изменялся в дельте r2,
принят по содержанию r1.
- **`.github/workflows/process.yml`** — интеграция публикации (индекс
пересобирается и коммитится тем же коммитом, что документ ревью;
`review-doc-guard` пропускает `docs/reviews/`) — не изменялся в дельте r2,
принято по r1. (Отдельно от этого унаследованного факта — H1 этого раунда
показывает, что сама пересборка не гарантирует свежесть после последующих
ребейзов; это новый дефект процесса, не отменяющий корректность самой
интеграции шага.)
- **`parseDocName`** и разбор старых форматов заголовков находок (`### 1. …`,
строки таблиц `| H1 | … |`) — байт-в-байт не менялись в дельте r2 (только
обёрнуты в `forEach` без изменения самих регэкспов) — приняты по r1.
- **Мутанты `reviews-index-skips-self-check`, `reviews-index-verdict-substring`**
— не менялись в дельте, приняты по r1 (там же вручную проверено «поймано 1
из 1»).
## Гейты — что прогнал, что нет
| Гейт | Статус | Почему |
|---|---|---|
| `npx tsc --noEmit`, `npm test`, `npm run build` | не гонял | зелёный Validate на `3e96b376` (run 35859217407); diff не касается `.ts`/`src/**` |
| `node --test test/reviews-index.test.mjs` | прогнал, 9/9 | дешёвый, прямое покрытие дельты |
| `node scripts/reviews-index.mjs --dir=docs/reviews --check` | прогнал, **упал** | не покрыт ни Validate, ни `npm test`; это и есть H1 |
| Мутант `reviews-index-counts-first-match` | прогнал вручную (патч + тест) | ловится |
| `node scripts/check-docs.mjs` | не гонял | diff не в `src/**` |
| `npm run invariants` | не нужен | геометрия не затронута |
| `demo/smoke_*.mjs`, `smoke-select.mjs` | не гонял | фронтенд-диффа нет |
| `npm run golden:verify` | не нужен | рендер не затронут |
| `pytest tests_backend` | не нужен | Python не затронут |
## Вердикт
Жёлтый. High: 1 (`INDEX.md` материала ревью устарел, не проходит собственный
`--check`, AC1 не выполнено на этом SHA), Medium: 1 (в скоупе — `parseFindings`
подставляет обрывки многострочных буллетов вместо текста находки, минимум 11
документов). Оба — по коду, изменённому в этой же дельте r2, чинятся в этом же
issue.
---
<!-- material-anchors: сгенерировано конвейером (#414) -->
## Материал раунда
- Ветка: `issue/635-reviews-index`, коммит `3e96b37656b9` — ребейз его осиротит, и это нормально: ниже якоря, которые ребейз не меняет.
- Дерево материала: `1cca2716ecd7f5004a0a235b1ff47da5ffb43019`
```
git log --all --format='%H %T' | grep 1cca2716ecd7
```
- Тело issue: `6ae105e700c9b3bc91974f4e25052237ef671f31d5e59ee5a0841cbaa1bb26a6`
- Вердикт конвейера: `yellow` · High 1