docs: review document for #369

Issue: #369
User-Visible: no
This commit is contained in:
claude[bot]
2026-08-29 07:48:25 +00:00
parent c50d9e4290
commit a2c189eaa3
+163 -174
View File
@@ -1,208 +1,197 @@
# SPEC-REVIEW-369-r1
# SPEC-REVIEW-369-r2
- Issue: https://github.com/Matysh/houseplan-card/issues/369
- Этап: ТЗ на ревью (PROCESS.md §2.4), полный трек (владелец назвал критерий
«одна поверхность» §5 как не выполненный — семь несвязанных поверхностей)
- Артефакт: `docs/specs/369-audit-lows.md`
- Материал ревью: SHA `cbf9318e` (`docs: specify #369 audit-lows batch`,
только этот файл, 117 строк, `User-Visible: no` — корректно для docs-коммита)
- Заход: r1 · блокирующих циклов израсходовано 0 из 4 (лимит для полного трека — 4)
- Материал ревью: дельта `cbf9318e..c50d9e42` («ревизия 2» документа,
`docs: #369 spec revision 2 per SPEC-REVIEW-369-r1`) — тот же единственный
файл, 3 хунка (+11/-7 строк), `User-Visible: no`, корректно для docs-коммита
- Заход: **r2** · блокирующих циклов израсходовано **1 из 4** (лимит для
полного трека — 4)
## Скоуп
## Несовпадение номера раунда в заголовке задачи — зафиксировано, не находка по ТЗ
Семь независимых Low-находок из аудита beta.2/beta.3 (а–ж): документирование
поведения purge пылесоса, консольный warn при отбраковке NaN-точки трейла,
нормализация `area === undefined` в климате легаси-маркеров (#317),
уточнение доступности контроллера при полностью отключённых entities (#318),
Shift-реактивность превью мебели без движения мыши, фильтр правой кнопки при
установке мебели/декора, унификация имени правообладателя furniture-pack.
Ни один пункт не расширяет продуктовый контракт — все семь чинят
рассогласование между заявленным и фактическим поведением уже принятых
функций (J1/J2/J4/J5/J6 по `docs/SCOPE.md`), нарушения `SCOPE.md` не найдено.
Заголовок этой задачи ревью объявлял «Заход: r1 · блокирующих циклов 0 из 4».
Это неверно относительно фактического состояния issue: в `docs/reviews/`
уже лежит `SPEC-REVIEW-369-r1.md` с вердиктом «жёлтый» (три Medium: M1, M2,
M3), и это подтверждено комментарием issue от `2026-08-29T07:41:01Z`
(«Вердикт: жёлтый · заход r1 · блокирующих циклов 0/4 · High: 0 · Medium: 3
→ в задаче»), а следующий комментарий владельца («Ревизия 2 ТЗ (dev
`c50d9e4`) по r1: **M1**...**M2**...**M3**... Возвращаю S4-spec-review»)
подтверждает, что это и есть возврат на правки по r1. Жёлтый вердикт тратит
бюджет §4 (не зелёный, #227) — значит фактический счёт: заход **r2**,
циклов **1/4**, а не r1/0. Использую r2 в имени этого документа: назвать
его r1 означало бы затереть уже существующий `SPEC-REVIEW-369-r1.md` —
ровно тот сценарий, от которого предостерегает сам процесс триггера. Не
продуктовая находка (не про ТЗ #369), но стоит сообщить о рассинхроне
источника номера раунда в автоматизации, которая формирует заголовок
задачи — иначе следующий заход рискует получить то же самое имя дважды.
## Скоуп раунда (правило дельты, PROCESS.md §2.9, #214)
Дельта — три точечных правки одного файла, отвечающие ровно на находки r1:
1. `- Ревизия: 1` → `- Ревизия: 2 ... — по SPEC-REVIEW-369-r1 (M1–M3)`
(метаданные, не содержательно).
2. Контракт (ж): список файлов расширен `docs/FURNITURE.md:41` (M1).
3. AC1: доказательство переформулировано с «check-docs зелёный» на «код-ревью
читает три файла» (M2).
4. AC8: добавлен мутант для (д) — «attach выброшен → смок AC5 красный по
free-флагу и счётчику слушателей» (M3).
Ни рекомбейза на ушедший вперёд `dev`, ни смены контракта поведения, ни
новой подсистемы — дельта локальна классически. Разбор этого раунда
ограничен M1–M3 и тем, не сломала ли точечная правка что-то из уже
подтверждённого в r1 (правки чисто аддитивны: добавлено имя файла, снята
формулировка доказательства AC1, добавлен один пункт списка мутантов —
ни один не пересекается текстуально с AC2–AC7 или разделами «Риски»,
«Release-артефакты», «Вне скоупа», «Откат»).
## Как проверялось
Ревью ТЗ проверяет заявимость и проверяемость контракта, а не код будущей
реализации — но каждый пункт (а)–(е) сверен построчно с текущим деревом
`dev` на SHA `cbf9318e`, чтобы отличить решение от догадки:
- `git diff cbf9318e..c50d9e42 -- docs/specs/369-audit-lows.md` — прочитан
целиком, три хунка перечислены выше.
- M1: контракт (ж) в текущем тексте (строка 66-68) действительно называет
четыре файла, включая `docs/FURNITURE.md:41`. `grep -rn Matyushin docs/
assets/` на HEAD (`c50d9e42`) по-прежнему находит все четыре целевых
файла (правка кода ещё не сделана — стадия ТЗ, ожидаемо) плюс историческую
прозу `docs/reviews/SPEC-REVIEW-369-r1.md` и сам текст спеки, цитирующие
старое имя как часть описания правки — см. находку L1 ниже.
- M2: AC1 в текущем тексте (строка 72-74) — «доказательство: код-ревью
читает три файла (check-docs содержимое не проверяет, он лишь не должен
покраснеть от ссылок/фингерпринта)» — снимает именно то расхождение
инструмент/утверждение, которое было найдено в r1 (`scripts/check-docs.mjs`
не читает содержимое абзацев).
- M3: AC8 (строка 92-96) теперь перечисляет мутант для (д) отдельно от
(б)/(в)/(г)/(е), с двумя названными детекторами (смена free-флага,
счётчик addEventListener/removeEventListener) — то же, что требовало M3.
- Трейлеры `c50d9e42`: `Issue: #369`, `User-Visible: no` — верно для
docs-only правки класса C (PROCESS.md §1).
- Диапазон изменений всей спек-стадии #369 (`cbf9318e` и `c50d9e42`) —
единственный файл `docs/specs/369-audit-lows.md` оба раза
(`git show --stat` на каждом коммите); `src/**`/`custom_components/**`
не тронуты ни разу.
- `src/devices.ts:1402-1410` (`markerClimateTarget`) — подтверждает (в):
условие сейчас `marker.area === null`, замена на `== null` описана точно.
- `src/device-presentation.ts:210-224` (`controllerAvailability`) — базовое
условие (г) подтверждено; уточнение с `ha_disabled`/`allEntityIds`
сверено с `src/ha-binding-status.ts:14-17,440-444` — поле `kind:
'ha_disabled'` с `reason: 'all_entities'` уже возвращается ровно в
описанном случае (allEntityIds непуст, enabledEntityIds пуст), так что
вторая половина условия «ИЛИ allEntityIds непуст при пустом
enabled-ростере» по инварианту резолвера избыточна (у `kind==='active'`
такой комбинации не бывает). Не блокирует: AC4 сформулирован через
наблюдаемый результат (available/not available), а не через внутреннее
ветвление, и мутант в AC8 целится в тот же результат — избыточность не
плодит двусмысленность. Отмечено ниже как Low.
- `src/vacuum.ts:250-256` (`smoothVacPath`) — подтверждает (б): нефинитный
сегмент отбрасывается сейчас молча (`continue`), без побочных эффектов.
- `src/houseplan-editor-runtime.ts:4756` (`_furnPointerMove`) — подтверждает
(д): `free: ev.shiftKey` читается только в pointermove, ни один
keydown/keyup-листенер на Shift не найден (`grep shiftKey` по `src/`).
- `src/houseplan-card.ts:6109` → `_decorPointerDown`
(`src/houseplan-editor-runtime.ts:4076-…`) — подтверждает (е): ни в одной
ветке (`line`/`rect`/`ellipse`/`text`/`furniture`) нет проверки
`ev.button`; правый клик проходит тем же путём, что и левый.
- `custom_components/houseplan/trails.py:252-274` — подтверждает (а):
`async_purge_orphans` действительно чистит трейлы orphan-маркеров на
каждый `config/set`, ссылка на #335 корректна.
- Репо-wide `grep -rn "Matyushin"` — см. находку H1 ниже: список файлов в
контракте (ж) неполон.
- `docs/USER-GUIDE.ru.md:867` уже фиксирует «Позиция, вложения и след
пылесоса при удалении маркера удаляются» — терминология «след» из (а)
согласована с уже принятой, не изобретена заново.
- `scripts/check-docs.mjs` прочитан целиком — см. находку M2.
- `assets/furniture/houseplan-0.3.0/README.md` — SHA-256 источника
архива не привязан к строке имени автора, утверждение AC7 о том, что
пересчитывать нечего, корректно.
Дешёвые гейты (`typecheck`/`test`/`build`/`check-docs`) на этом заходе не
запускал — тот же довод, что в r1: диапазон дельты не содержит ни одного
файла класса A/B (PROCESS.md §1), гейты нечего было бы доказать. Смоки,
golden, инварианты модели — не запускались по той же причине (ни
геометрия, ни `src/**`, ни визуальный результат в этой дельте не задеты).
Дешёвые гейты (`typecheck`/`test`/`build`) на этом заходе **не запускал**:
диапазон изменений — класса C, единственный файл
`docs/specs/369-audit-lows.md`, ни один файл `src/**` или
`custom_components/**/*.py` не тронут (`git show --stat cbf9318e`). Запуск
кодовых гейтов на неизменённом коде ничего не доказал бы и не входит в
§8 «объём соразмерен задаче» для этапа ТЗ. `check-docs.mjs`,
`model-invariants`, смоки и golden не запускались по той же причине — ни
геометрия, ни `src/**`, ни визуальный результат в этом коммите не заде́ты.
## Закрытие раунда r1
## Находки
| Находка (r1) | Чем закрыта | Где видно |
|---|---|---|
| **M1** — AC7 требовал пустой repo-wide grep, но контракт (ж) не покрывал все вхождения имени (`docs/FURNITURE.md:41` пропущен) | Контракт (ж) расширен до четырёх файлов, включая `docs/FURNITURE.md:41` | `docs/specs/369-audit-lows.md:66-68`, diff-хунк 2 (`cbf9318e..c50d9e42`) |
| **M2** — AC1 называл `check-docs` доказательством содержимого абзаца, хотя скрипт содержимое не проверяет | AC1 переформулирован: доказательство — прямое чтение трёх файлов на код-ревью, `check-docs` оставлен только структурным требованием | `docs/specs/369-audit-lows.md:72-74`, diff-хунк 3 |
| **M3** — AC8 не называл мутант для (д), смок AC5 нельзя было отличить «проверяет логику» от «проходит без неё» | AC8 получил явный мутант «attach выброшен → смок красный по free-флагу и счётчику слушателей» | `docs/specs/369-audit-lows.md:92-96`, diff-хунк 4 |
### Medium (в скоупе, чинится в этом же ТЗ)
Все три Medium из r1 закрыты предметно (текст ТЗ, не заявление автора).
**M1 — AC7 требует пустой repo-wide grep, но контракт (ж) не покрывает все вхождения имени.**
`docs/specs/369-audit-lows.md`, пункт (ж) и AC7.
## Унаследовано из r1
Контракт называет три файла для правки:
`assets/furniture/houseplan-0.3.0/{LICENSE.md,README.md,pack.json}`. Но
`docs/FURNITURE.md:41` тоже несёт старое имя: «The 77 drawings were created
by Sergey Matyushin (`Matysh`) and granted to the project under its MIT
License in issue #159» — тот же автор, тот же furniture-pack. AC7 требует
«grep «Matyushin» по репо пуст» — как написано, это repo-wide grep,
который останется красным после правки ровно трёх названных файлов.
Документ: `docs/reviews/SPEC-REVIEW-369-r1.md`, разобран на SHA `cbf9318e`.
Дельта r2 не касается ничего из перечисленного ниже — принимается без
повторной построчной проверки:
*Воспроизведение:* `grep -rn Matyushin docs/ assets/` на SHA `cbf9318e`
даёт 4 файла с вхождением (три названных в контракте + `docs/FURNITURE.md`).
- Сценарий («до/после», персона home admin/десктоп-редакторы) и соответствие
J1/J2/J4/J5/J6 из `docs/SCOPE.md` — нарушения не найдено.
- Все шесть кодовых утверждений контракта (а)–(е) сверены r1 построчно с
деревом `dev` на `cbf9318e` (`src/devices.ts:1402-1410`,
`src/device-presentation.ts:210-224` + `src/ha-binding-status.ts:14-17,440-444`,
`src/vacuum.ts:250-256`, `src/houseplan-editor-runtime.ts:4756`,
`src/houseplan-card.ts:6109`→`_decorPointerDown`,
`custom_components/houseplan/trails.py:252-274`) — ни одно не оказалось
догадкой, выданной за факт.
- AC2–AC6 — однозначны, способ доказательства (юнит/смок) назван, дельта их
текст не трогает.
- «Вне скоупа», «Откат», «Риски», «Release-артефакты» — не тронуты дельтой,
корректны по r1.
- **L1** (избыточное условие в контракте (г): вторая половина условия по
инварианту `resolveHaBindingStatus` не добавляет новых случаев) — снята
r1 с записью, не блокирует; дельта (г) не касалась.
- **L2** (не все разделы §7.1 вынесены отдельными заголовками) — снята r1
с записью как необязательная рекомендация «на следующую правку»;
ревизия 2 её не применила — это не нарушение, поскольку r1 явно
зафиксировал находку как dismissed (не «to-do»), см. PROCESS.md: Low
«либо правится, либо снимается с записью» — уже снята.
Правка: либо добавить `docs/FURNITURE.md` в список файлов контракта (ж) —
согласуется с духом решения владельца «автор — Sergey Matyunin», либо явно
сузить grep в AC7 до `assets/furniture/houseplan-0.3.0/`. Первое
предпочтительнее: расхождение имени в двух местах одного репозитория —
ровно тот дефект, который пункт (ж) должен закрыть целиком, а не частично.
## Находки (этот раунд)
**M2 — AC1 называет неверный инструмент доказательства.**
`docs/specs/369-audit-lows.md`, AC1: «(а) VACUUM.md + оба USER-GUIDE
содержат абзац; check-docs зелёный.»
### Low — AC7 не станет буквально «grep по репо пуст» даже после точечной правки; положительная проверка не покрывает добавленный файл
Прочитан `scripts/check-docs.mjs` целиком: скрипт проверяет структуру
public-документов — битые ссылки, `alt` у картинок, отпечаток скриншотов
(`sourceFingerprint`) и связность заголовков. Он не читает содержимое
абзаца и не упадёт, если требуемый текст отсутствует или отредактирован
мимо смысла — «content лежит, значит и абзац лежит» здесь неверно.
Зелёный `check-docs` докажет только то, что новый абзац не сломал ссылки
и не устарел фингерпринт скриншотов; он не докажет, что абзац вообще
добавлен и говорит то, что нужно.
`docs/specs/369-audit-lows.md`, AC7 (строка 88-91): «(ж) grep «Matyushin» по
репо пуст; pack.json/LICENSE.md/README.md несут «Sergey Matyunin»…».
Правка: заменить доказательство AC1 на явное «ревью читает три файла и
подтверждает наличие абзаца с описанным содержанием» (проверено чтением,
не исполнением — единственный законный способ для строкового факта,
которого никакой существующий автотест не проверяет), `check-docs`
оставить дополнительным (структурным) требованием, а не единственным.
Два самостоятельных нюанса одной находки:
**M3 — AC8 не называет мутант для пункта (д).**
`docs/specs/369-audit-lows.md`, AC8 перечисляет мутанты для (б), (в), (г),
(е), но не для (д) — хотя (д) тоже вносит новую логику (keydown/keyup на
`window`, обновление `free`-флага и его снятие при закрытии палитры).
Без названного способа сломать смок AC5 умышленно, ревьюер кода не сможет
отличить «смок проверяет добавленную логику» от «смок технически
проходит, потому что фикстура не создаёт условие, при котором листенер
вообще нужен» — тот самый класс дефекта, который уже стоил цикла на смоке
`visual_continuity` (PROCESS.md §13 п.5) и который правило «тест должен
уметь падать» (§2.7, §18) существует, чтобы ловить до код-ревью, а не во
время него.
1. Позитивная часть AC7 («несут «Sergey Matyunin»») по-прежнему называет
только три исходных файла и не упоминает добавленный `docs/FURNITURE.md`
— асимметрично контракту (ж), который теперь называет четыре. Негативная
часть («grep пуст») это транзитивно покрывает, так что асимметрия не
создаёт дыру в доказательстве, только небольшую редакционную
рассогласованность.
2. Буквально пустой repo-wide grep недостижим и после полной правки: сам
этот файл ТЗ (`docs/specs/369-audit-lows.md`) в прозе контракта (ж) и в
AC7 цитирует «Sergey Matyushin» как часть описания правки, `docs/reviews/
SPEC-REVIEW-369-r1.md` цитирует то же самое как историческую находку, и
этот документ (SPEC-REVIEW-369-r2) продолжит цитировать обе формы имени
после публикации в `docs/reviews/`. Ровно тот же класс дефекта AC уже
разбирался в прецеденте `docs/reviews/CODE-REVIEW-269-r1.md` (Low,
«буквальная формулировка AC2 не выполняется, хотя цель AC2 достигнута») —
там тоже historical-документ ревью цитировал старый текст, и находка была
снята без правки: архивный документ «описывает код, которого уже нет»
(§7.3), переписывать его задним числом бессмысленно и запрещено духом
канона.
Правка: добавить в AC8 явный мутант, например «keyup не сбрасывает
`free` → смок красный» либо «keydown-листенер не навешен → смок красный».
*Почему не блокирует:* цель AC7 — отсутствие живого расхождения имени в
активных ассетах/документации (`assets/furniture/houseplan-0.3.0/**`,
`docs/FURNITURE.md`), а не буквальный ноль совпадений во всём git-дереве
включая архив ревью. Это то же различие буквы и духа AC, что и в #269.
### Low (снимается с записью, не блокирует)
*Снимается без правки, с записью:* код-ревьюер этой задачи должен читать
AC7 как «`grep -rn Matyushin` пуст вне `docs/specs/**` и `docs/reviews/**`»
— не нужно заводить эту находку заново на код-ревью, ссылка на этот раздел
и на прецедент #269 достаточна.
**L1 — избыточное условие в контракте (г).**
См. раздел «Как проверялось»: вторая половина условия («ИЛИ `allEntityIds`
непуст при пустом enabled-ростере») по текущему инварианту
`resolveHaBindingStatus` (`src/ha-binding-status.ts`) не добавляет новых
случаев к `bindingStatus.kind === 'ha_disabled'` — `kind==='active'`
никогда не сочетается с непустым `allEntityIds` и пустым
`enabledEntityIds`. Снимаю без правки: AC4 сформулирован через наблюдаемый
результат, а не через внутреннее ветвление, двусмысленности для
реализации и для теста это не создаёт, а привязка к конкретному дереву
условий — техническое решение автора, которое ревьюер вправе не разделять,
но не обязан блокировать (PROCESS.md §7.1: техническое решается автором,
оспаривается — не запрещается).
**L2 — не все разделы §7.1 вынесены отдельными заголовками.**
Явных заголовков «Модель данных и миграция», «i18n» (есть только внутри
«Release-артефакты»), «План автотестов» (растворён по AC), «Скоуп» как
отдельного позитивного раздела (есть только «Вне скоупа») в документе нет.
По содержанию всё покрыто — компат-полей и миграции нет («конфиг/схема/
контракты не меняются» в «Откате»), i18n не требуется, план автотестов
проступает через доказательства при каждом AC — поэтому не блокирую и не
возвращаю по этой находке отдельно. Снимаю с записью: при следующей
правке документа (M1–M3 всё равно требуют редактирования файла) стоит
добавить три короткие явные строки-заголовка, чтобы трассируемость по
DoR-чеклисту (§2.5) не зависела от того, что читающий сам разложит текст
по разделам.
High: 0. Medium: 0 (у обеих Medium из r1 — M1, M2, M3 — закрытие
подтверждено выше построчно).
## Что проверено и корректно
- Сценарий и «что человек увидит до/после» — есть, продуктовый, без
терминов реализации, определяет персону (home admin, десктоп-редакторы)
корректно по `docs/SCOPE.md`.
- Все шесть кодовых утверждений (а)–(е) сверены с фактическим деревом на
SHA `cbf9318e` построчно (см. «Как проверялось») — ни одно не оказалось
догадкой, выданной за факт; расхождение с кодом не найдено, кроме
избыточности L1, которая не меняет наблюдаемый контракт.
Решение владельца по (ж) (Sergey Matyunin) зафиксировано в ТЗ дословно.
- AC2–AC6 однозначны и указывают способ доказательства (юнит/смок/grep),
кроме зафиксированных выше M1 (неполный grep) и M2 (неверный инструмент
для AC1).
- «Вне скоупа» отсекает ровно то, что нужно — семантику purge (а) кодом.
- Откат — один revert, конфиг/схема не меняются, соответствует
содержанию (все шесть кодовых пунктов point-fix, седьмой — метаданные).
- Риски названы по существу: (г) — видимое сужение недавнего #318,
(д) — утечка листенеров на `window`, обе снабжены проверкой
(регресс-юнит и счётчик addEventListener/removeEventListener
соответственно) — это ровно то, что нужно, чтобы риск не остался
голым наблюдением.
- Release-артефакты названы: одна пачка CHANGELOG en+ru (User-Visible: yes
для в/г/д/е — корректно, а/б/ж действительно не меняют видимое
пользователю поведение или являются мета/документацией), i18n верно
помечен как не требуемый.
- Трейлеры коммита `cbf9318e` корректны для docs-коммита класса C
(`Issue: #369`, `User-Visible: no`); AGENTS.md/PROCESS.md по классам
файлов не нарушены.
- Все три Medium из r1 (M1, M2, M3) закрыты текстом документа, не
заявлением автора — см. таблицу «Закрытие раунда r1».
- Дельта аддитивна и не пересекается текстуально ни с одним из унаследованных
разделов — риск регрессии на уже подтверждённых AC (класс дефекта #102)
отсутствует: правки не касаются формулировок AC2–AC6, контрактов (а)–(е),
разделов «Риски»/«Release-артефакты»/«Вне скоупа»/«Откат».
- Трейлеры коммита `c50d9e42` (`Issue: #369`, `User-Visible: no`) корректны
для docs-коммита класса C.
- Владелец вернул задачу в `S4-spec-review` после правки — процесс §2.4
соблюдён (комментарий `2026-08-29T07:41:51Z`).
## Чего не проверял
- Не проверял `docs/CONFIG-COMPATIBILITY.md` построчно на предмет скрытых
compatibility-полей — ни один из семи пунктов не трогает конфигурацию
или схему маркера, поэтому применимость документа сочтена нулевой без
построчной сверки.
- Не запускал `typecheck`/`test`/`build`/`check-docs`/смоки/golden —
причина в разделе «Как проверялось»: диапазон изменений этого SHA не
содержит кода, гейты нечего было бы проверять.
- Не проверял вживую логи покупателя фикстур для (б)/(г)/(д)/(е) —
тестов ещё нет (стадия ТЗ), сверка «умеет ли тест падать» относится к
код-ревью (§2.7) и будет сделана там по фактическим юнитам/смокам.
- Не проверял точную формулировку будущего абзаца в VACUUM.md/USER-GUIDE
— контракт (а) специально не фиксирует финальный текст, только смысл;
это осознанная свобода автора, а не пробел.
- `typecheck`/`test`/`build`/`check-docs`/смоки/golden/инварианты модели —
не запускал: за всю спек-стадию #369 (оба коммита, `cbf9318e` и
`c50d9e42`) единственный тронутый файл — `docs/specs/369-audit-lows.md`;
ни `src/**`, ни `custom_components/**/*.py` не задеты ни разу, гейтам
нечего было бы доказать.
- Не проверял живьём будущие юниты/смоки для (б)–(е) и (д) — их ещё нет,
сверка «умеет ли тест падать» относится к код-ревью (§2.7).
- Не проверял точную будущую формулировку абзацев VACUUM.md/USER-GUIDE —
контракт (а) намеренно не фиксирует финальный текст, только смысл; это
было явно принято в r1 и дельта этого не меняла.
- Не проверял `docs/CONFIG-COMPATIBILITY.md` — ни один из семи пунктов не
трогает конфигурацию/схему маркера (унаследовано из r1, дельта не
расширяет скоуп).
## Вердикт
Жёлтый: три находки Medium в скоупе задачи (M1, M2, M3), High нет. Все три
чинятся правкой того же файла `docs/specs/369-audit-lows.md` — не требуют
нового issue (#202). L1 и L2 сняты с записью выше.
Зелёный: обе стороны условия закрытия соблюдены — все три Medium из r1
(M1, M2, M3) закрыты предметно текстом документа, новых Medium/High не
найдено. Одна Low-находка этого раунда (AC7: буквально недостижимый
repo-wide grep, асимметрия позитивной проверки) снята без правки, с
записью и ссылкой на прямой прецедент #269. Расхождение номера раунда в
автоматике (задача звала себя «r1/0», фактически это «r2/1») зафиксировано
отдельно выше — не находка по ТЗ, но стоит сообщить в процесс.