первая встреча непокрытой секции стала наблюдаемым событием
- свёртка спрашивает журнал, встречалось ли имя строго раньше по паре (received_at, id), и пишет WARN с атрибутом uncovered_new; повторные молчат. Признак выводится, а не хранится — реестр был бы второй копией факта - добавлена подкоманда `healthlog uncovered`: перечень накопленного, чтение только на чтение, экранированные имена и названные границы носителя - синк документации: ADR о выводе новизны из журнала, две записи в журнал дефектов, два правила промоутом в конвенции, терминал оператора назван адресатом недоверенного входа
This commit is contained in:
@@ -0,0 +1,277 @@
|
||||
# Триаж ревью: aktivnaya-proverka-novyh-sekcij
|
||||
|
||||
## Сводка
|
||||
|
||||
- **Профиль:** deep, режим по графу. **Гейт:** зелёный (exit 0 дважды,
|
||||
покрытие диффа 96%, 2 непокрытые строки — унаследованный пробел `main.go`).
|
||||
- **Сверка состава с профилем:** расхождений нет. Deep — 7–8 проходов
|
||||
(`docs/review.md`, итог по конвейеру); запущено 7, восьмой (`reimpl`) —
|
||||
по триггеру, и триггер не сработал (см. границы покрытия).
|
||||
- **Проходы поимённо:**
|
||||
- `gate` — отработал, 2 находки, зелёный;
|
||||
- `specs` — отработал, 5 находок + наблюдения вне спеки;
|
||||
- `code` — отработал, 3 находки;
|
||||
- `adversary` — отработал, 4 находки с прогнанными оракулами + 3 свойства
|
||||
без построенного пути;
|
||||
- `ops` — отработал, 2 находки, обязательные вопросы отвечены;
|
||||
- `architecture` — отработал, 2 находки + 1 правка «дешевле до мерджа»;
|
||||
- `reimpl` — **не запускался**: триггер «новое правило слияния, идентичности
|
||||
или разбора» не сработал — изменение вводит наблюдение поверх существующей
|
||||
логики;
|
||||
- `triage` — этот отчёт.
|
||||
- **Счёт:** на входе 19 именованных находок, после дедупликации по причине —
|
||||
13. В отчёте: 2 блокируют, 4 сейчас, 6 в гипотезах, 2 promote, 1 подтверждённая
|
||||
не влезла в потолок (названа в границах покрытия).
|
||||
- Находка №1 найдена тремя проходами независимо — это подняло её приоритет;
|
||||
подтверждением служит прогнанный оракул, а не согласие проходов.
|
||||
- Совпадений с «Типовыми ложноположительными» из `docs/review.md` нет; отсев
|
||||
шёл по проектному списку.
|
||||
|
||||
## Блокирует мердж
|
||||
|
||||
### Отказ слияния навсегда съедает событие о новой секции — владелец о ней не узнает никогда
|
||||
|
||||
- Файл: `internal/fold/fold.go:214-222`
|
||||
- Severity: major
|
||||
- Confidence: high
|
||||
- Оракул: `tmp/adv/lost_event_test.go`, перепрогнан триажем — FAIL
|
||||
воспроизведён: запись об отказе (`delivery fold failed`) не несёт
|
||||
`uncovered_new`, при этом `fail` пишет `residue.uncovered` в колонку
|
||||
(`fold.go:587`), и следующая доставка считает `ecg` виденным. Путь достижим:
|
||||
`context.DeadlineExceeded` не транзиентен, тело ~63 МиБ держит блокировку
|
||||
5.019 с (замер adversary).
|
||||
- Последствие: единственная возможность события «поток начал приносить новую
|
||||
секцию» — первая доставка с этим именем. Если её свёртка упала на слиянии,
|
||||
событие теряется молча и не повторяется: имя уже записано как виденное.
|
||||
Прямое нарушение требования дельта-спеки «Отказ свёртки не глотает событие»
|
||||
и класс «молчание» — самый дорогой после порчи данных.
|
||||
- Предложение: в ветви отказа слияния передавать `novelty` в остаток:
|
||||
`s.fail(ctx, deliveryID, err, parseResidue{uncovered: parsed.Uncovered,
|
||||
skipped: skippedEntities(parsed), novelty: novelty})`. Тест из
|
||||
`tmp/adv/lost_event_test.go` перенести в постоянные.
|
||||
- Найдено проходом: specs, code, adversary (дедуплицировано; оракул один)
|
||||
- Действие: инлайн
|
||||
|
||||
### Вывод команды обещает «журнал не приносил», хотя носитель перечня обрезается и обнуляется — оператор поверит пустоте, которой нельзя верить
|
||||
|
||||
- Файл: `cmd/healthlog/uncovered.go:71-95`
|
||||
- Severity: major
|
||||
- Confidence: high
|
||||
- Оракул: дословный пункт дельта-спеки —
|
||||
`openspec/changes/aktivnaya-proverka-novyh-sekcij/specs/uncovered-sections/spec.md:198-203`:
|
||||
«Система SHALL называть границы перечня в спеке **и в выводе команды**, а не
|
||||
обещать „всё, что поток когда-либо приносил"». Вывод границ не называет, а
|
||||
пустая ветвь печатает ровно запрещённое обещание: «Непокрытых секций журнал
|
||||
не приносил.»
|
||||
- Последствие: перечень производен от колонки, которую обрезает разбор
|
||||
(32 имени на доставку) и обнуляет пересборка (тела, удалённые ретеншеном,
|
||||
список теряют). Пустой вывод после ретеншена или вытеснения читается как
|
||||
«поток ничего не приносил» — оператор примет решение по неполному ответу,
|
||||
считая его полным.
|
||||
- Предложение: переписать пустую ветвь («в учётных записях журнала непокрытых
|
||||
имён нет») и добавить в вывод строку границ носителя (обрезка разбором,
|
||||
обнуление пересборкой). Закрепить сквозным тестом `runUncovered` на непустой
|
||||
базе — он же закрывает критерий приёмки К3 и находку specs о его отсутствии.
|
||||
- Найдено проходом: specs
|
||||
- Действие: инлайн
|
||||
|
||||
## Стоит исправить сейчас
|
||||
|
||||
### Одна битая строка `uncovered_sections` роняет и сверку (вечный WARN-дребезг), и команду перечня целиком, не называя виновника
|
||||
|
||||
- Файл: `internal/store/uncovered.go:37,92-98,167-186`
|
||||
- Severity: major
|
||||
- Confidence: high
|
||||
- Оракул: тест триажа `tmp/triage/jsoneach_test.go` — воспроизведено:
|
||||
`SQL logic error: malformed JSON (1)` на весь запрос; с
|
||||
`AND json_valid(uncovered_sections)` битая строка пропускается, живая
|
||||
считается. Достижимо не штатным путём (`json.Marshal` битого не пишет):
|
||||
ручная правка, будущая миграция мимо кода, восстановление БД. Схемной защиты
|
||||
`CHECK(json_valid(...))` в миграции 00005 нет.
|
||||
- Последствие: `json_each` над невалидным JSON — ошибка всего запроса, а guard
|
||||
`notEmptyList` проверяет только `'[]'` и `''`. Итог: сверка новизны вечно
|
||||
`unknown=true` (постоянный WARN, неотличимый от занятости базы), а
|
||||
`healthlog uncovered` отказывает целиком и не говорит, какая строка битая.
|
||||
- Предложение: добавить `AND json_valid(uncovered_sections)` в `notEmptyList`
|
||||
(действует на оба запроса) — проверено, чинит.
|
||||
- Найдено проходом: ops
|
||||
- Действие: инлайн
|
||||
|
||||
### Обоснование «индекс не нужен» стоит на замере не того случая: в заявленном сценарии скан почти полный на каждой доставке, а `LIMIT` не ограничивает работу
|
||||
|
||||
- Файл: `internal/store/uncovered.go:49-64,177-186`; `docs/architecture.md:383-387`
|
||||
- Severity: minor (факт high, операционная угроза low)
|
||||
- Confidence: high
|
||||
- Оракул: `tmp/adv/seen_cost_test.go` — журнал 105 000: имя с первой встречей в
|
||||
начале — 31 мкс, в хвосте — 13.9 мс, новое — 14.1 мс. Ранний выход `LIMIT 1`
|
||||
окупается только для имени из **начала** журнала; в заявленном сценарии
|
||||
(секция начала приезжать недавно) первая встреча — в хвосте, и цена платится
|
||||
на каждой доставке, а не «один раз за жизнь имени». Плюс `EXPLAIN QUERY PLAN`
|
||||
для `UncoveredSections` (ops): `SCAN d`, `USE TEMP B-TREE FOR GROUP BY` —
|
||||
`--limit 5` стоит столько же, сколько `--limit 200`.
|
||||
- Последствие: сегодня терпимо (~14 мс × 288 доставок ≈ 4 с чтения в сутки),
|
||||
но записанное рассуждение, на котором держится решение «индекса нет»,
|
||||
неверно, и следующая задача обопрётся на него как на факт. Вдобавок числа
|
||||
замера разошлись между двумя местами: `uncovered.go:53-58` — 28 мкс / 45 мс;
|
||||
`architecture.md:383-386` — 18 мкс / 46 мс / 57 мс (находка gate и code).
|
||||
- Предложение (развилка, варианты):
|
||||
- (а) принять цену и переписать обоснование в `uncovered.go` и
|
||||
`architecture.md` честно (худший случай — почти полный скан на доставку),
|
||||
заодно свести разошедшиеся числа и оставить их в одном месте со ссылкой из
|
||||
другого — ноль кода, минуты работы;
|
||||
- (б) завести индекс или таблицу первых встреч имён — миграция плюс правка
|
||||
`docs/database.md`; окупится только если различных имён станет много;
|
||||
- (в) ничего не менять — отвергается: обоснование записано неверно.
|
||||
- Найдено проходом: adversary, ops (дедуплицировано: одна причина — оценка
|
||||
стоимости снята на нерепрезентативном случае); расхождение чисел — gate, code
|
||||
- Действие: развилка
|
||||
|
||||
### Несостоявшаяся сверка не называет причину: `uncovered_seen_unknown=true` без ошибки, разбираться не по чему
|
||||
|
||||
- Файл: `internal/fold/fold.go:514-522`; `internal/store/uncovered.go:111`
|
||||
- Severity: minor
|
||||
- Confidence: high
|
||||
- Оракул: положение конвенции `docs/conventions/logging.md` (проход code);
|
||||
чтение кода — `novelty()` отбрасывает `err` из `SectionsSeenBefore` молча,
|
||||
из-за чего `clipCoord` в тексте ошибки `sectionSeenOnce` — мёртвый
|
||||
предохранитель: ошибка никуда не доезжает.
|
||||
- Последствие: оператор видит «сверка не состоялась, новыми объявлены все
|
||||
имена», но не видит, почему — занятость, отмена, битая строка (см. находку
|
||||
про `json_valid`) неразличимы. Молчание причины при говорящем признаке.
|
||||
- Предложение: при `unknown` доводить причину до записи — либо логировать в
|
||||
`novelty()`, либо нести ошибку в `sectionNovelty` до атрибутов чекпоинта /
|
||||
записи об отказе.
|
||||
- Найдено проходом: specs, code, adversary (дедуплицировано)
|
||||
- Действие: инлайн
|
||||
|
||||
### Сообщение чекпоинта вводит третий термин мимо словаря — операторский контракт (grep, алерты) затвердеет с неверным именем
|
||||
|
||||
- Файл: `internal/fold/fold.go` (msg `"delivery folded, unseen section name"`)
|
||||
- Severity: minor
|
||||
- Confidence: high
|
||||
- Оракул: словарь изменения — `uncovered` (состояние) и `new` (событие);
|
||||
«unseen» не существует ни в спеке, ни в атрибутах (атрибут называется
|
||||
`uncovered_new` — виден в прогоне `tmp/adv/dupclip_test.go`).
|
||||
- Последствие: после мерджа сообщение — операторский контракт; grep и алерты
|
||||
завяжутся на «unseen», и переименование станет дороже с каждой неделей.
|
||||
Сейчас — одна строка и один тест.
|
||||
- Предложение: переименовать msg в термины словаря, например
|
||||
`"delivery folded, uncovered section new"`.
|
||||
- Найдено проходом: architecture
|
||||
- Действие: инлайн
|
||||
|
||||
## Гипотезы без доказательства
|
||||
|
||||
- **Вторая сериализация журнального ключа** (`received_at || ' ' || id` +
|
||||
`splitJournalKey`, `internal/store/uncovered.go:180-181,216-226`) —
|
||||
корректность лексикографического сравнения держится на незакреплённой
|
||||
фиксированной ширине `FormatTime`. Оракула (теста, ломающего ширину) не
|
||||
построено; понижено до гипотезы. Дешёвая страховка — тест-шпилька на ширину
|
||||
формата. (architecture, minor/medium)
|
||||
- **Два SQL-пути в `SectionsSeenBefore`** (одно имя / батч,
|
||||
`internal/store/uncovered.go:84-87`) — экономия быстрого пути ≈ 13 с чтения
|
||||
в сутки; вопрос «что опытный человек удалил бы». Последствие — только цена
|
||||
сопровождения двух запросов; ущерб не построен. Если владелец возьмёт
|
||||
вариант (б) развилки про индекс — вопрос снимется сам. (architecture,
|
||||
minor/low)
|
||||
- **Сценарий «Отложенная доставка события не порождает» не закреплён тестом** —
|
||||
держится на порядке двух блоков в `fail` (`fold.go:546-565`); перестановка
|
||||
при рефакторинге даст дребезг события на каждом проходе занятой базы.
|
||||
(specs, minor)
|
||||
- **Ветвь новизны при переменной части в ключе навсегда занимает `msg`** — если
|
||||
HAE начнёт класть в ключ переменную часть, каждая доставка будет «с новой
|
||||
секцией» и чекпоинт станет постоянным WARN. Путь не построен (наблюдённые
|
||||
ключи стабильны). (adversary, свойство без пути)
|
||||
- **Два снимка в `UncoveredSections` (count и перечень) могут разойтись на
|
||||
единицу** — два запроса без общей транзакции; окно — конкурентная запись
|
||||
между ними. Команда диагностическая, ущерб — косметика остатка. (adversary,
|
||||
свойство без пути)
|
||||
- **Поведение вне спеки, исход не меняющее:** флаг `-limit` не заказан спекой;
|
||||
`context.Background()` вместо `signal.NotifyContext` (у соседнего `reindex` —
|
||||
второе). Оставлено на решение оркестратора без severity. (specs)
|
||||
|
||||
## Promote candidates
|
||||
|
||||
- **`CHECK (json_valid(...))` для JSON-колонок в будущих миграциях** — схемная
|
||||
защита, которой не хватило в 00005; правило для `docs/conventions/` или
|
||||
чек миграций в гейте. (из находки ops про битую строку)
|
||||
- **«Число замера живёт в одном месте, остальные ссылаются»** — числа одного
|
||||
замера разошлись между `uncovered.go` и `architecture.md` уже до мерджа;
|
||||
класс тот же, что у записи 2026-08-04 в `docs/review.md` (числа, на которых
|
||||
стоит нормативный текст, читаются как факт и не перепроверяются). (gate, code)
|
||||
|
||||
## Границы покрытия
|
||||
|
||||
**Прогон:** профиль deep, режим по графу. База диффа — рабочее дерево
|
||||
(HEAD == master). Гейт зелёный, exit 0 дважды; diff-coverage 96%.
|
||||
|
||||
**Запускалось:** gate, specs, code, adversary, ops, architecture, triage.
|
||||
Оракулы adversary прогнаны в `./tmp/adv`, оракул триажа — `./tmp/triage`.
|
||||
`task verify:archive` прогнан оркестратором — зелёный, 90 с.
|
||||
|
||||
**Не запускалось:**
|
||||
|
||||
- `reimpl` — триггер `docs/review.md` («новое правило слияния, идентичности или
|
||||
разбора») не сработал: изменение наблюдает поверх существующей логики, правил
|
||||
не двигает. Если считать сверку новизны «правилом наблюдения» — это
|
||||
расширительное чтение триггера; решение не запускать принял оркестратор, и
|
||||
класс «независимая реализация нашла бы другую форму» не покрыт.
|
||||
- `task verify:busy` — не гонялся. Именно он проверил бы поведение новой сверки
|
||||
при удерживаемой блокировке (ветвь `uncovered_seen_unknown` — ровно про
|
||||
занятую базу). Изменение правил разбора/слияния не двигает, поэтому
|
||||
обязательный порог CLAUDE.md формально не задет, но ветвь unknown проверена
|
||||
только юнит-тестами, не прогоном.
|
||||
|
||||
**Не влезло в потолок (подтверждено, но ниже линии):**
|
||||
|
||||
- Два разных ключа, совпадающих в первых 64 байтах, дают дубль после
|
||||
`clipSection`: одна доставка показана в перечне как две
|
||||
(`internal/store/uncovered.go:177-186`, `count(*)` считает строки
|
||||
`json_each`, а не доставки). Оракул: `tmp/adv/dupclip_test.go`, перепрогнан —
|
||||
FAIL воспроизведён. Вероятность мизерная (нужны имена длиннее 64 байт с общим
|
||||
префиксом), последствие — неверное число в диагностической команде. Чинится
|
||||
`count(DISTINCT d.received_at || ' ' || d.id)` либо дедупликацией имён после
|
||||
обрезки на разборе.
|
||||
- Непокрытые строки диспетчера подкоманд `cmd/healthlog/main.go:34-35` —
|
||||
унаследованный пробел (весь `main()` 0% и до изменения). (gate)
|
||||
|
||||
**Что запущенные проходы не могли проверить по построению:**
|
||||
|
||||
- `gate` — только механизируемое; «в Go так не пишут» не проверяет никто (см.
|
||||
ниже).
|
||||
- `specs` — судит против записанной спеки; сами границы спеки: отказ слияния
|
||||
как исход в дельте не описан (находка №1 стоит на общем требовании «не
|
||||
глотает событие»); приоритет ветви новизны над событиями потери содержания
|
||||
(`EntitiesHeld`, `PointsErased`) — выбор кода, не спеки; `Deliveries` считает
|
||||
пары (доставка, элемент массива) — семантика зафиксирована кодом.
|
||||
- `adversary` — три свойства названы без построенного пути (перечислены в
|
||||
гипотезах); не построенный путь не означает недостижимый.
|
||||
- `ops` — истории инцидентов у нового кода нет; поведение под реальным потоком
|
||||
не наблюдалось. Сигналов о молчании потока нет — существующий пробел, не
|
||||
этого диффа.
|
||||
- `architecture` — судит форму, не поведение; шов `loggedFold`/`breakSeenQuery`
|
||||
рассмотрен и находкой не признан.
|
||||
- `triage` — ничего нового не находит по построению; пропуск любого прохода —
|
||||
пропуск триажа тоже.
|
||||
|
||||
**Целиком на человеке** (`docs/review.md`, «Недоступно проверке» — два списка,
|
||||
намеренно раздельных):
|
||||
|
||||
*Не проверит ни один проход:* реальный профиль нагрузки (телефон шлёт молча и
|
||||
непрерывно); поведение HAE за пределами наблюдённого; полнота словаря переводов
|
||||
после обновления iOS; секции, которых поток ещё не приносил (`symptoms`, `ecg`,
|
||||
`heartRateNotifications`, `cycleTracking`, `medications`) — для этого изменения
|
||||
это особенно прямо: команда `uncovered` написана ровно про них, и судить можно
|
||||
только форму кода, не встречу с реальностью. Плюс общее: история инцидентов,
|
||||
поведение внешних систем в их версиях, завязка потребителей на текущее
|
||||
поведение, вопрос «а нужна ли эта функциональность вообще».
|
||||
|
||||
*Перестали проверять сознательно:* `verify:archive`/`verify:busy` вне гейта
|
||||
(в этом прогоне archive прогнан, busy — нет, см. выше); класс «в Go так не
|
||||
пишут» — не покрыт вовсе после упразднения `idiom` (запись 2026-08-02); класс
|
||||
«чего нет в зрелой реализации такого узла» — вне профиля `design`.
|
||||
|
||||
**Документы проекта:** всё нужное было на месте — инварианты `CLAUDE.md`
|
||||
(severity брались из них, не выводились), `docs/review.md` с журналом, типовыми
|
||||
ложноположительными и обоими списками «Недоступно проверке». Ни один проход не
|
||||
заявил недостающего документа; отдельных строк деградации нет.
|
||||
Reference in New Issue
Block a user