Files
healthlog/docs/review.md
T
av 1b649ba3d5 добавлен словарь категориальных значений HAE → коды HealthKit
- фазы сна, контекст пульса и имена тренировок попадают в реестр
  `category_value` (миграция 00010): строка хранится дословно, выведенный код
  лежит рядом отдельной записью, а не полем внутри точки
- словарь и синонимы кодов живут в бинаре (`internal/healthkit`); локаль из
  `Accept-Language` сужает поиск, но в ключ реестра не входит — заголовков в
  сыром архиве нет
- наблюдение входит в отпечаток витрины, выведенный код — нет: он производная
  от словаря, а не от журнала
2026-08-04 07:32:19 +03:00

439 lines
42 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.
# Ревью: настройка и журнал
Конвейер — скилл `av-dev-pipeline:review-pipeline`, проходы — агенты
`av-dev-pipeline:review-*`. Здесь только проектная часть: чем этот проект
отличается от умолчаний конвейера и что в нём уже проскакивало.
## Как настроен конвейер
### Типовые узлы
Рода узлов проекта и свойства, по которым судится каждый. Рода, а не инвентарь
пакетов: род, который проект задумал, но ещё не написал, включён намеренно.
**Разбор пакета HAE** (`internal/hae`)
- Точка сохраняется дословно; ничего внутри неё не отбрасывается и не
переименовывается.
- Непонятое содержимое не роняет доставку: она принята, непокрытое названо.
- Слой выводится из выравнивания меток, а не из заголовка HAE — тот врёт.
- Текст ошибки не содержит значений из входа — род токена и смещение.
- Тест гоняется на реальном пакете из `testdata`, а не на выдуманном.
**HTTP-обработчик приёма** (`internal/httpapi`)
- Код ответа отражает доставку, а не разбор: битый JSON — 400, непонятое
содержимое — 200.
- Тело попадает в архив раньше, чем в разбор; потеря архива необратима.
- Тело и заголовки в лог выше `DEBUG` не уезжают, токены — никогда.
- Есть названный предел на размер тела и на заголовки.
**Свёртка и репозиторий часовых объектов** (`internal/store`, `internal/fold`)
- Результат — функция **префикса** журнала: узел не читает состояние, которое
сам же меняет, без границы по `received_at` разбираемой доставки.
- Правило выбора между версиями — функция множества версий либо явно функция
порядка журнала; третьего состояния нет.
- Столкновение разрешается полнотой, а не свежестью; изменение запечатанного
часа пишется `WARN`, но данные пишутся.
- Транзакция не держит блокировку дольше `busy_timeout`: канонизация и
сжатие — вне её.
**Файловый архив и ретеншен** (`internal/archive`)
- Путь строится из значений, которых отправитель не контролирует.
- Удаление тела опирается на колонку, отличающую ноль от «не измерялось».
- Место на диске и рост каталога названы числом.
**Проигрыватель журнала и CLI** (`internal/replay`, `cmd/`)
- Повторный прогон даёт то же состояние и тот же отпечаток.
- Новая единица хранения входит в отпечаток и в счётчики отчёта.
- Расход памяти не растёт вместе с длиной журнала.
- Подмена базы — решение человека при остановленном сервисе, не команды.
**Обработчик чтения и адаптер MCP** (Read API, MCP — ещё не написаны)
- Агрегат считается только там, где род свёртки измерен; нижний слой HAE не
суммируется никогда.
- Ответ имеет предел размера, и предел объявлен, а не подразумевается.
- Адаптер MCP собственной логики не несёт — те же обработчики.
### Типовые ложноположительные
Находки, которые здесь выглядят убедительно и всегда неверны.
- **«Ответ 200 на непонятое содержимое проглатывает ошибку.»** Не дефект:
инвариант «сохранили — значит приняли». Телефон шлёт молча и не
перешлёт — код ответа отражает доставку, а не разбор.
- **«`source` не входит в ключ — идентичность неполна.»** Не дефект: поле
измерено нестабильным (разведка, находка 36), включение его в ключ задваивает
точки.
- **«Точка хранится избыточно, поля дублируются.»** Не дефект: точки хранятся
дословно, инвариант прямой. Экономия здесь необратима.
- **«Часовой объект не считает агрегат при записи.»** Не дефект: своей
агрегации в хранении нет, род свёртки выводится сверкой слоёв в ответе.
- **«У одной метки три записи сна — дубликат.»** Не дефект: у точки-измерения
конец равен началу, под одной меткой лежит до трёх записей.
- **«Русские строки в значениях — незакрытая локализация.»** Наполовину: строки
приходят на языке телефона, и это факт источника; дефектом является только
отсутствие стабильного кода рядом с переводом.
### Вопросы к проходам
Форма: `<имя прохода>: <вопрос> (<провенанс>)`. Задаются дополнительно к
обязательным.
- `ops`: читает ли узел состояние, которое сам же меняет, и остаётся ли
результат функцией от **префикса** журнала (запись 2026-08-01, свёртка не
воспроизводилась при пересборке).
- `ops`: поведение библиотеки, драйвера и `PRAGMA` измерено или вычитано из
документации; что возвращается в **вырожденном** случае и отличим ли этот
ответ от штатного (запись 2026-08-02 про упразднение `idiom`; прецедент
`-1 >= -1` — 1492 тика из 5502).
- `ops`: хватит ли сигналов владельцу, когда поток оборвётся ночью (переселено
из упразднённого `negative`).
- `architecture`: не изобретаем ли то, что уже есть в стандартной библиотеке —
своя абстракция, повторяющая форму существующей (переселено из `idiom`).
- `architecture`: что опытный человек отсюда удалил бы (переселено из
`negative`).
- `rubric`: пришпилено ли утверждение теста к числу, производному от размера
корпуса — корпус растёт с каждой доставкой (запись 2026-08-02, прогон живого
архива был красным).
- `adversary`: доводится ли значение точки или тело доставки до лога выше
`DEBUG` хотя бы одним путём (запись 2026-08-02, тело 8 МиБ в тексте ошибки).
- `triage`: перечислены ли запущенные проходы поимённо и с исходом; непущенный
проход идёт в границы покрытия строкой «не запускался» (запись 2026-08-02,
чекпоинт кода прошёл без трёх проходов).
### Триггеры профиля
Уточняет умолчания конвейера, не отменяет их.
- **`deep`** — изменения в правиле разбора, идентичности, слияния или вывода
слоя; миграции схемы; всё, что трогает `internal/store`, `internal/fold`,
`internal/replay`.
- **«Поведение, видимое снаружи»** здесь — код ответа приёма, форма ответа
чтения, содержимое архива и **состояние, которое даёт пересборка**: витрина
наблюдаема через пересборку, поэтому расхождение с журналом — внешнее
поведение, а не внутренняя деталь.
- **`reimpl`** запускается по триггеру «новое правило слияния, идентичности или
разбора». Единственный раз, когда триаж назвал его отсутствие дырой
покрытия, — это была задача с новым правилом слияния сущностей.
Второй замер (2026-08-03, словарь категориальных значений): триггер сработал
на новом правиле разбора и ключе реестра, проход **окупился** — он независимо
подтвердил замером две находки, до того имевшие только одно измерение (пик
памяти накопителя: 1002 МиБ против 780 на базе; единицы счётчика отброшенных),
и отдельно назвал семь мест, где существующее решение оказалось **лучше** его
собственного. Второе ценно не меньше первого: оно показывает, где проход
соглашается, а не только где спорит.
- **`quick`** — правка документов, конфигурации, сообщений; ничего, что меняет
хранимое.
### Недоступно проверке
**Не проверит ни один проход.** Реальный профиль нагрузки: телефон шлёт молча и
непрерывно, объём и частота меряются только по факту. Поведение приложения HAE
за пределами наблюдённого — расписание автоматизаций пожелание, а не гарантия
(разведка, находка 28). Полнота словаря переводов после обновления iOS.
Секции, которых поток ещё не приносил: `symptoms`, `ecg`,
`heartRateNotifications`, `cycleTracking`, `medications` — разбор писался
вслепую, и проход может судить только форму кода, не соответствие реальности.
**Перестали проверять сознательно.**
- Прогон живого архива (`task verify:archive`) и свёртка под удерживаемой
блокировкой (`task verify:busy`) в гейт не входят: минута и 25 секунд
соответственно, плюс данные, которых нет ни на какой другой машине. Гоняет их
человек перед задачей, трогающей разбор или слияние (запись 2026-08-02,
прогон живого архива был красным и об этом никто не знал).
- Класс «в Go так не пишут» — поимённая сверка с Effective Go, Go Code Review
Comments, стайлгайдами Uber и Google — не покрыт вовсе после упразднения
`idiom`. Класс обратимый, портит форму кода, а не данные, но признавать это
надо в границах покрытия, а не считать проверенным (запись 2026-08-02).
- Класс «чего нет в зрелой реализации такого узла» — вне профиля `design`.
## Журнал дефектов
Запись на каждый воспроизведённый дефект, сразу, а не ретроспективно: со
временем теряется не факт, а причина непоймания.
Реализованные задачи, находки ревью и решения сюда не пишутся: у них есть
коммит, спека и задача. Здесь только промахи конвейера и решения о его составе.
Форма:
<!-- копия: журнал-дефектов-форма из av-dev-pipeline/skills/review-pipeline/references/review-journal.md -->
## ГГГГ-ММ-ДД — <краткое последствие> [проскочил|пойман]
- **Где:** путь:строка либо «конвейер, а не код»
- **Симптом:** как обнаружилось, кем и когда
- **Причина:** что на самом деле было не так
- **Чем воспроизведён:** тест, команда, замер — с числами
- **Почему не поймали:** только для проскочивших — какой проход обязан был найти
и что ему помешало
- **Что меняем:** правило прохода, шаг гейта, конвенция, факт в документе
проекта — либо «ничего, цена поимки выше цены дефекта»
<!-- /копия: журнал-дефектов-форма -->
Последний пункт важнее остальных. Вывод «ничего не меняем» — законный исход:
не всякий дефект стоит того, чтобы усложнять ради него ревью каждой задачи.
---
## 2026-08-01 — свёртка не воспроизводилась при пересборке журнала [проскочил]
- **Где:** `internal/store/delivery.go`, `LastDerivedLayer`
- **Симптом:** прогон живого архива (99 доставок) вторым проходом дал 1742
объекта вместо 1737, а координат сна 182 вместо 174. Нашёл тест сходимости
на шаге apply — не ревью.
- **Причина:** доставка без плотных метрик наследует слой автоматизации.
Запрос брал последний выведенный слой **вообще**, а не последний до этой
доставки, поэтому при пересборке доставка наследовала слой «из будущего».
Свёртка переставала быть функцией от префикса журнала.
- **Почему не поймали:** формулировка «наследует последний надёжно выведенный
слой той же автоматизации» звучит однозначно и в спеке, и в дизайне —
пропущенное слово «предшествующей» не выглядит пропуском. Проходы `specs` и
`architecture` сверяли код со спекой и понятиями, а инвариант
«`import + replay` даёт то же состояние» ни один из них не проверял на
конкретном правиле: он записан в архитектуре как свойство системы, а не как
критерий для каждого узла, читающего состояние.
- **Что меняем:** в проходы `rubric` и `ops` — вопрос
«читает ли узел состояние, которое сам же меняет, и остаётся ли он функцией
от префикса журнала». Дешевле правила: любой запрос к `delivery` из свёртки
обязан иметь границу по `received_at` разбираемой доставки. Тест сходимости
на живом архиве (`internal/fold/replay_test.go`) остаётся постоянным —
именно он это поймал.
## 2026-08-02 — прогон живого архива был красным и об этом никто не знал [проскочил]
- **Где:** `internal/fold/replay_test.go` (перенесён в `internal/replay/archive_test.go`)
- **Симптом:** первый же запуск `task verify:archive` в задаче про пересборку
дал `координат sleep_analysis 222, измерено 174`. Проверено прогоном прежней
редакции теста на том же архиве: она даёт ровно те же 222, 2049 объектов и тот
же отпечаток — значит тест покраснел не от изменений задачи, а сам, когда
архив дорос с 94 доставок до 116.
- **Причина:** утверждение было пришпилено к **числу, производному от корпуса**
(174 координаты сна). Корпус растёт с каждой доставкой, то есть константа
протухает по расписанию телефона. Проверяемое свойство при этом другое и от
размера корпуса не зависит: ключ по интервалу не схлопывает записи до ключа
по метке (222 координаты против 218 меток).
- **Почему не поймали:** прогон живого архива намеренно не входит в `task gate`
(минута работы, данные есть только на этой машине). У проверки, которую гейт
не гоняет, краснота никому не видна — она обнаруживается только следующей
задачей, которая до неё дотянется. Ни один проход ревью прогон не запускал:
проходы читают код, а не гоняют опциональные команды.
- **Что меняем:** утверждение переписано на само свойство (координат строго
больше, чем различных меток), измеренные числа остались в `t.Logf`. Правило
общее и годится в конвенции: **в проверке на живом корпусе нельзя утверждать
число, производное от размера корпуса** — утверждать надо инвариант, а число
печатать. Гейт при этом не трогаем: цена ежедневной минуты выше цены такой
протухшей константы, а после этой задачи прогон стал ещё и единственным, кто
проверяет настоящий проигрыватель журнала.
## 2026-08-02 — чекпоинт кода прошёл без трёх проходов, и ровно они нашли всё [проскочил]
- **Где:** конвейер, а не код: коммит `f8200f7` («тренировки и записи с
собственным `id`»), шаг 7 пайплайна задачи (тогда — проектная копия
`healthlog-task-pipeline`, ныне `av-dev-pipeline:task-pipeline`), профиль
`deep`.
- **Симптом:** изменение было закоммичено и заархивировано как прошедшее ревью.
Дозапуск трёх пропущенных проходов на **уже закоммиченном** коде дал девять
причин, семь из которых пошли в работу с прогнанными оракулами: скелет из
`null` затирает маршрут молча и необратимо; одно поле не той формы уносит
тренировку, а доставка при этом числится разобранной; откат бинаря поверх
новой схемы стартует без слова; победитель внутри доставки зависит от порядка
элементов на проводе; провенанс устаревает на каждой повторной присылке;
канонизация идёт внутри транзакции вопреки собственному комментарию (768 МиБ
пика, 5.019 с удержания блокировки); тело в 8 МиБ целиком уезжает в текст
ошибки и оттуда в `WARN`.
- **Причина:** сабагент, проводивший задачу, на чекпоинте кода запустил не все
проходы профиля `deep` — не отработали `adversary`, `ops` и архитектурный.
Отчёт триажа при этом был выпущен и выглядел полным: он агрегирует то, что
ему подали, и о непоступивших проходах не знает. Секция границ покрытия
обязана была это назвать, но она заполняется тем же триажем — то есть
единственный, кто мог заметить пропуск, узнаёт о нём из того же источника,
который его допустил.
- **Почему не поймали:** пропуск прохода **не отличим от прохода без находок**.
Гейт зелёный, спеки сошлись, applicative-проходы отработали — снаружи это
выглядит как чистое ревью. Все семь находок принадлежат ровно тем классам,
которые applicative-проходы не достают по построению: враждебно
сконструированный вход (`adversary`), поведение под откатом и конкуренцией
(`ops`), второй способ делать уже сделанное (архитектура). Recall чек-листа
равен длине чек-листа, а этих пунктов в чек-листах нет и быть не может.
- **Что меняем:** отчёт ревью обязан перечислять запущенные проходы **поимённо
и с исходом**, а оркестратор задачи — сверять этот перечень с составом
профиля до того, как коммитить; непущенный проход идёт в границы покрытия
строкой «не запускался», а не отсутствует. Правилом линтера это не
выражается, автоматической проверки нет — но пропуск, названный в отчёте,
стоит одной строки, а пропуск молчащий стоил семи находок и отдельной задачи
на их дозакрытие. Состав проходов и профилей при этом не трогаем: они
сработали ровно так, как задуманы, — их просто не позвали.
## 2026-08-02 — тест на утечку значений в лог краснел от хода часов [проскочил]
- **Где:** `internal/fold/log_test.go`, `TestFoldНесравнимыеНаборыДаютWarn`
- **Симптом:** гейт задачи про цену читающего маршрута покраснел на чужом
тесте: «в логе оказалось значение точки "5.1"». Значения в логе не было —
подстрока нашлась в метке времени записи (`…T20:23:35.193…` содержит `5.1`).
Повторный прогон зелёный.
- **Причина:** утверждение искало секрет в **сыром буфере** записи, а буфер
содержит служебное поле `time` с долями секунды. Вероятность совпадения для
двухсимвольного числа с точкой — около процента на прогон, то есть тест
флаки по построению, и краснеет он у того, кто мимо проходил.
- **Почему не поймали:** шаг `flaky` гейта гоняет набор дважды подряд —
вероятность поймать однопроцентную флаки за два прогона мала, а сам тест
выглядит образцовым: он проверяет ровно тот инвариант, который проекту
дороже всего («данные о здоровье чувствительнее токенов»). Ни один проход
ревью не смотрит на тесты чужих задач.
- **Что меняем:** правило в [conventions/testing.md](conventions/testing.md) — проверка «в логе
нет значения» разбирает запись и выбрасывает `time`, а не ищет в сыром
буфере. Гейт не трогаем: два прогона против однопроцентной флаки не помогут,
а десять стоили бы дороже самой находки.
## 2026-08-02 — состав конвейера сужен: 11 проходов до 6–9
Не промах, а решение по итогам пяти задач подряд. Записано здесь, потому что
именно здесь лежит цена непоймания: если что-то теперь проскочит, первый вопрос
будет «не тот ли это класс, который мы перестали проверять».
- **Повод:** профиль `deep` стоял на всех пяти задачах сессии и гонял 11
проходов на коде плюс 4 на дизайне — порядка полутора миллионов токенов на
задачу. Ревью, а не написание кода, стало основной статьёй расхода.
- **На чём основано:** поимённая атрибуция находок надёжна только для
дозапуска трёх проходов на `f8200f7` — там оркестратор запускал их сам.
В двух циклах, которые вели сабагенты, находки перечислены без указания
прохода, и это ограничение вывода названо здесь честно.
- **Что убрано и почему:**
- `negative`**удалён**. За сессию ни одной именной находки; блокер про
откат релиза он нашёл дублем с `ops`, то есть заплатил триажу
дедупликацией. Два его живых вопроса переселены: «хватит ли сигналов
владельцу, когда поток оборвётся ночью» — в `ops`, вопрос 7; «что опытный
человек отсюда удалил бы» — в `architecture`, вопрос 5.
- `rubric`**только в `design`**. Его же 14 свойств из design-прогона
ложатся приёмочными критериями в `tasks.md`; судить код по критерию, под
который он писался, — корреляция по построению.
- `reimpl`**по триггеру** «новое правило слияния, идентичности или
разбора». Самый дорогой проход конвейера; единственный раз, когда триаж
назвал его отсутствие дырой покрытия, — это была задача с новым правилом
слияния сущностей, то есть ровно триггерный случай.
- **Что переставлено, и это важнее сокращения:** `adversary` и `ops` были в
`deep`-только, а `standard` гонял четыре самых слабых generative-прохода.
То есть профиль, которым закрывается большинство задач, запускал ровно тех,
кто ничего не принёс, и не запускал тех, кто принёс почти всё. Оба переехали
в `standard`. Это одновременно дешевле и качественнее.
- **Что чуть не убрали по ошибке:** `idiom` был в списке на удаление как
«вкусовщина». Отменено фактом: в задаче про цену читающего маршрута он нашёл,
что `-1 >= -1` читается как «журнал разобран целиком», и **воспроизвёл**
1492 тика из 5502. Плюс три эксперимента на дизайне `razbor-metrik-v-obekty`.
Вывод, который стоит помнить: этот проход зарабатывает **экспериментами
против поведения stdlib и драйвера**, а не цитатами из гайдов, — и потому у
него есть внешний оракул. Оценка «не всплыл поимённо ни разу» была верна по
имевшимся данным и неверна по существу.
- **Что мы сознательно перестали проверять:** класс «чего нет в зрелой
реализации такого узла» вне профиля `design`, и «пять вопросов второго
инженера» как отдельная постановка. Обратимость этого класса высокая: он
портит форму кода и полноту наблюдаемости, а не данные. Если проскочит
дефект этого класса — запись сюда и пересмотр решения.
- **Побочная выгода, ради которой стоило резать отдельно:** реестр из 6–9
проходов сверяется взглядом. Промах 2026-08-02 (запись выше) был молчащим
пропуском трёх проходов из одиннадцати; на коротком списке требование
«перечисли запущенные проходы поимённо и с исходом» наконец выполнимо.
## 2026-08-02 — `idiom` тоже упразднён, класс переселён
Решение владельца, принятое после того, как оркестратор привёл доводы против
удаления (находка на чекпойнте WAL, воспроизведённая: 1492 тика из 5502) и они
были выслушаны. Записано отдельной строкой, потому что довод был, и если класс
проскочит — искать надо здесь.
- **Что переселено, а не выброшено.** Проход зарабатывал экспериментами против
поведения stdlib и драйвера, и именно эта способность перенесена поимённо:
- «поведение библиотеки, драйвера и `PRAGMA` измеряется, а не вычитывается из
документации; что возвращается в **вырожденном** случае и отличим ли этот
ответ от штатного» — в `ops`, обязательный вопрос 8, вместе с прецедентом
`-1 >= -1` и оговоркой про `data_version` как свойство соединения;
- «не изобретаем ли то, что уже есть в библиотеке» — в `architecture`,
вопрос 1, с перечнем конструкций stdlib: своя абстракция, повторяющая форму
существующей, — находка того же класса, что и второй способ делать одно и
то же.
- **Что действительно потеряно.** Поимённая сверка с положениями Effective Go,
Go Code Review Comments, Go Proverbs и стайлгайдов Uber и Google. Различение
«идиоматично» против «распространено» больше не задаётся никем: `architecture`
спрашивает про форму решения, `ops` — про поведение под нагрузкой, но ни один
не спросит «в Go так не пишут». Класс обратимый — портит форму кода, не
данные, — но он теперь не покрыт вовсе, и это надо признавать в границах
покрытия, а не считать проверенным.
- **Итог по конвейеру:** `quick` 4, `standard` 6, `deep` 78, `design` 3.
Было 11 на коде и 4 на дизайне.
## 2026-08-03 — метка от часов в отпечатке сделала тест функцией секунды прогона [пойман]
- **Где:** `internal/fold/categorical_test.go`, `TestFoldЛокальНеМеняетСостояния`
- **Симптом:** гейт покраснел на одном подтесте из четырёх: «заголовок
`{"Accept-Language":["de"]}` сдвинул отпечаток витрины». Три подтеста прошли.
- **Причина:** тест сравнивал отпечатки четырёх независимых витрин, а метку
приёма доставки брал из `store.Now()`. Провенанс первой встречи входит в
отпечаток реестра — значит отпечаток зависел от того, уложились ли подтесты в
одну секунду. Тест был флаки по построению и краснел бы у того, кто мимо
проходил.
- **Чем воспроизведён:** сам гейт; после замены `store.Now()` на фиксированную
метку — `go test ./internal/fold -count=2` зелёный.
- **Что меняем:** ничего в конвейере — гейт сработал ровно так, как задуман, и
поймал класс, который прошлый раз (2026-08-02, подстрока «5.1» в метке
времени) прожил незамеченным. Правило то же и уже записано в
[conventions/testing.md](conventions/testing.md): величина, зависящая от хода
часов, не участвует в утверждении. Запись здесь — потому что это второй случай
одного класса за два дня, и третий стоит считать сигналом, а не совпадением.
## 2026-08-03 — прогон живого архива красный на master, и это не заметили две задачи подряд [проскочил]
- **Где:** `internal/replay/archive_test.go`, `measureStyles`
- **Симптом:** `task verify:archive` в задаче про словарь категориальных
значений упал на `step_count: противоречащих часов 1 при 22 согласных`.
Проверено прогоном **базовой ревизии** `3df42af` из копии дерева на том же
архиве: те же 2875 объектов, те же 285 координат сна, тот же отказ. Краснота
унаследована, изменением не внесена.
- **Причина:** утверждение «противоречий ноль» — посылка «род измерим», верная
на корпусе, где её снимали. Корпус вырос до 145 доставок, и у `step_count`
появился час, где минутный и часовой слои разошлись. Сама система при этом
ведёт себя правильно: род объявляется только при единогласном свидетельстве,
и `step_count` числится `unknown`.
- **Почему не поймали:** ровно та же причина, что и в записи 2026-08-02, — у
проверки, которую гейт не гоняет, краснота никому не видна. Разница в том, что
тогда протухла константа, а теперь под вопросом сама посылка: противоречие —
это либо дефект правила, либо законное свойство корпуса, и решать это не
прогону.
- **Что меняем:** конвейер — ничего. Решение о том, чем стал `step_count`
(дефект измерения рода или законное противоречие, которое надо печатать, а не
утверждать), принадлежит владельцу и заведено задачей отдельно от этого
изменения. Названо здесь, чтобы третья задача подряд не открывала его заново.
## 2026-08-03 — ответ владельца не превращал задачу в берущуюся [проскочил]
- **Где:** конвейер, а не код — учёт задач, шаг «ответ на вопрос»
- **Симптом:** первая сессия по `av-dev-pm:session` показала четыре задачи с
тегом `question`. Три из них были решены владельцем **2026-08-02**, и решение
лежало первым абзацем тела: тай-брейк — вариант (б), порядок журнала —
вариант (в) после `/stats`, откат релиза — вариант (2). Но раздел «Вопросы»
остался непустым, тег остался на месте, и `sprint take` отказал бы взять эти
задачи в набор.
- **Причина:** ответ на вопрос — это **три правки** (опустошить раздел, снять
тег, переписать «зачем»), и делаются они в момент ответа. Была сделана только
запись решения. Судит при этом раздел, а не тег, поэтому решённая задача
выглядела нерешённой ровно так же, как настоящая нерешённая.
- **Чем воспроизведён:** `tasks.py list --questions` — 4 записи, из них 3 с
датированным решением в теле. После правок — 0.
- **Что изменено:** ничего в коде; три задачи приведены в берущийся вид,
четвёртая (`entity-without-parsed-label`) решена на этой сессии.
Два числа этой же сессии, названные, чтобы их было с чем сравнивать:
- **Ориентир «5–8 задач в спринте» ничем не замерян** — он взят из умолчания
скилла. Первый собственный замер даст этот спринт, и пересматривать ориентир
надо на следующей сессии, а не «когда-нибудь».
- **Отбор порции по залежалости (`list --stale`) в этом цикле слеп:** все 49
файлов каталога получили одну дату при переезде на канон (коммит `d79189b`),
и храповик на давно неподвижных задачах включится только с накоплением
собственной истории правок. Порция этой сессии отобрана по цели.