Files
transcriber/openspec/changes/archive/2026-08-12-go-1-26-upgrade/review/report.md
T
av 09228f23d8 Go обновлён до 1.26, а расхождение версий теперь роняет гейт
- шаг go-version в task gate сверяет объявленную версию в go.mod, Dockerfile,
  CLAUDE.md и README.md; судит по репозиторию, go не зовёт, docker и сети не
  требует
- заведена capability toolchain: до сих пор спеки нормировали только поведение
  сервиса, теперь и инструмент сборки. Причина и цена — в двух ADR
- закрыт дефект 2026-08-12: образ на golang:1.24-alpine разошёлся с go.mod и
  перестал собираться, а восемь шагов гейта и шесть проходов ревью были зелёными
2026-08-12 10:57:50 +03:00

621 lines
57 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.
# Ревью кода: go-1-26-upgrade — триаж
База диффа: `HEAD` (коммит `aa20b22`), изменение целиком в рабочем дереве. Дата
прогона: 2026-08-12.
## Сводка
**Размер, сложность, метка.** Размер — среднее: `tasks.md` 20 шагов в 4 разделах,
8 файлов; `internal/` не тронут ни строкой. Сложность — знакомое: все узлы
названы поимённо до работы, шагов формы «разобраться/выяснить» нет. **Метка
`medium`** (максимум по осям), **режим — по графу**. Триггеры `docs/review.md`
проверены все три группы, ни один пункт не совпал.
**Состояние гейта: ЗЕЛЁНЫЙ.** Подтверждено собственным прогоном триажа, а не
только отчётом прохода: `task gate BASE=HEAD` → exit 0. Все девять шагов зелёные,
включая новый `go-version`. Одно унаследованное замечание `tasks.py`
(`any-audio-source`: цель без задач и без тега `decomposed`) шаг не роняет и к
диффу отношения не имеет.
### План разметки задачи с исходом по каждой теме
| тема | дом | глубина | кто закрывает | исход |
|---|---|---|---|---|
| requirements | `openspec/specs/` + дельта `specs/toolchain/spec.md` | разбор | specs | **закрыта**, 4 находки (S1–S4) + 2 блока наблюдений |
| autotests | `CLAUDE.md`, раздел «Гейт» | — | autotests | **закрыта**, 3 находки (A1–A3) + отчёт гейта |
| conventions | `docs/conventions/` | разбор | code | **закрыта**, 3 находки (C1–C3) + 3 наблюдения ниже порога |
| architecture | `docs/architecture.md` + источник `docs/passport.md` | разбор | basics | **закрыта**, 1 находка (B1) + 4 пункта «дешевле переделать» |
| security | `docs/security.md` | разбор | basics | **закрыта**, находок нет: тема неприменима к диффу целиком |
| operations | `docs/architecture.md` §Эксплуатация + источник `docs/database.md` | разбор | basics | **закрыта**, 1 находка (B3); 4 вопроса из 6 неприменимы |
**Тем без отчёта нет.** Все шесть тем ядра вернули отчёты; своих тем сверх ядра у
проекта нет. Ни одна тема не осталась непроверенной по причине «проход не
запускался».
**Находок на входе:** 13 нумерованных (A1A3, S1S4, C1C3, B1B3) плюс 15
ненумерованных содержательных пунктов (3 «поведение вне спеки», 5 «границы
спеки», 3 наблюдения ниже порога, 4 «дешевле переделать до мерджа») = **28
позиций**. **Осталось в основных секциях: 6** — 2 блокирующие и 4 «стоит
исправить сейчас». Слито по причине 4 пары, понижено до гипотез 3, уехало в
promote 5, выброшено 6 (все названы поимённо).
### Сигнал о заниженной метке
**Сигнал подан двумя проходами независимо — `code` и `basics`.** Оба назвали одно
и то же: изменение вводит новое понятие (capability `toolchain` — первая, что
описывает не поведение сервиса, а инструмент разработки), новый каталог верхнего
уровня `scripts/` и новый шаг гейта; `design.md` сам записывает нерешённое
натяжение в размещении capability. Оба сказали, что метка `large` дала бы
отдельный проход `review-architecture`, и оба отказались решать за конвейер.
**Провенанс один, приоритет два.** Согласие двух проходов — это одна модель,
высказавшаяся дважды: `confidence` оно не повышает, приоритет повышает. Одна
строка с двумя провенансами, а не два пункта.
**Суждение триажа — факт для человека, не команда конвейеру:**
1. **По записанному правилу разметка верна.** `docs/review.md`, «Триггеры метки»:
в группе «Крупное здесь» ближайший пункт — «каркас приложения: сборка
фронтенда, раздача статики и шаг гейта разом» — требует трёх вещей сразу,
здесь только шаг гейта. В «Незнакомое здесь» не совпал ни один из шести: форма
решения (сравнение строк `sed`/`awk`, без `go` и без docker) была названа до
работы. Отрицательный тест пройден: миграции, формата файла, контракта API и
имени ключа конфига изменение не трогает. `review-scope` не ошибся против
правила, которое у него было.
2. **Ось, на которую указали проходы, в правиле отсутствует.** Ни один триггер не
говорит о заведении новой capability и о новом каталоге верхнего уровня. А
именно эта ось дала **обе блокирующие находки прогона** — обе про канон, а не
про код. Сигнал верен по существу: правило разметки не видит того, что в этом
изменении оказалось самым дорогим.
3. **Перезапуска это не требует, метку задним числом не пересматривают.** Тема
`architecture` дома не лишилась и без отчёта не осталась — её закрыл `basics`
на глубине «разбор». Честный остаток: тему смотрел проход широкого профиля, а
не специализированный, и вопрос «правильно ли выбрано имя и дом capability»
остался без независимого разбора (H-1).
4. **Что с этим делать — не здесь.** Кандидат в правило вынесен в promote (P-5).
---
## 1. Блокирует мердж (2 из 3)
### B-1. Требование «ровно одно вхождение» станет нормой в форме, которая про `Dockerfile` уже неверна, а на `CLAUDE.md` уронит гейт на правдивой строке
- Файл: `openspec/changes/go-1-26-upgrade/specs/toolchain/spec.md:16-18` против
`scripts/check-go-version.sh:127-131` и `:147-153`; правильные слова уже лежат в
`openspec/changes/go-1-26-upgrade/design.md:158-163`
- Severity: **major** | Confidence: **high**
- Действие: **развилка**
- Найдено проходами: `specs` (S1 и пункт «Границы спеки»), `basics` (пункт «Рамка
правила „ровно одно вхождение“»). **Слито триажем по причине:** причина одна —
требование написано пофайлово единым правилом, а четыре места устроены
по-разному. Правится одним абзацем.
- **Оракул — три прогона на копии дерева** (копии в scratchpad; рабочее дерево не
тронуто, `git status --porcelain` до и после совпадает):
1. второй сборочный слой `FROM docker.io/library/golang:1.26-alpine AS second`
**exit 0**. Норма гласит: «Каждое место MUST называть версию ровно один раз.
Второе вхождение числа в том же месте MUST считаться отказом», и перечень
мест включает `Dockerfile`. Код нормы не исполняет и исполнять не должен:
`collect Dockerfile "$(read_dockerfile)" 0` передаёт `strict=0` намеренно.
Контроль: тот же второй слой с `golang:1.25-alpine` → exit 1, «Dockerfile
называет несколько разных версий» — то есть совпадение слоёв проверяется,
единственность нет;
2. в `CLAUDE.md` дописана правдивая строка `- прежде собирались на Go 1.25; долг
закрыт задачей go-1-26-upgrade` → **exit 1**, «CLAUDE.md называет несколько
разных версий»;
3. в `README.md` дописан блок кода с `# нужен Go 1.26` → **exit 1**, «README.md
называет версию больше одного раза».
- Последствие. **Со стороны `Dockerfile`** — молчаливое расхождение нормы и кода:
после архивации нормой станет спека, а не `design.md`. Многослойная сборка с
двумя `FROM golang:` — законная форма. Ревьюер следующего изменения увидит
`strict=0`, прочтёт MUST и «починит» скрипт, уронив гейт на рабочем
`Dockerfile`; обратный исход не лучше — норма останется ложью, на которую
сошлются. **Со стороны документов** — гарантированный ложный красный:
`CLAUDE.md` по устройству ведёт историю (раздел «Гейт» прямо говорит «Два
прежних долга закрыты и здесь названы»), и первая же правдивая запись о прошлой
версии роняет шаг. По правилу проекта «Что считается сломанным — новый красный
шаг гейта… чинится прежде любой другой работы» это остановит работу, а
сообщение «называет несколько разных версий» толкает чинить не скрипт, а
исторический документ, то есть подделывать запись. `README.md` ловится тем же на
любом блоке кода с командой установки.
- Почему до мерджа: спека замерзает архивацией, после неё правка MUST — отдельное
изменение. Сегодня это один абзац.
- **Вопрос человеку:**
- **Вариант А (дешёвый, ожидаемый).** Развести правило по местам прямо в
требовании, дословно как уже написано в `design.md:158-163`: единственности
требовать от `go.mod`, `CLAUDE.md` и `README.md`, а от `Dockerfile` —
совпадения всех вхождений `FROM golang:`. Плюс сузить рамку для документов:
правило считает не файл целиком, а помеченную строку стека (или раздел
«Стек»/«Технологии»). Цена: абзац спеки + `read_doc` начинает читать раздел, а
не файл — несколько строк скрипта. Сценарий «Одно место называет два разных
числа» остаётся верным и правки не требует.
- **Вариант Б (дешевле сейчас, дороже потом).** Развести только `Dockerfile`
(правка чисто текстовая, кода не трогает), а цену «файл целиком» для
документов принять осознанно и записать остатком в спеке: «`CLAUDE.md` не
ведёт истории версий Go; запись о прошлой версии живёт в `docs/review.md`».
Тогда красный на истории — не сюрприз, а объявленный запрет.
### B-2. Канонический перечень capability назовёт три из четырёх ровно в момент архивации, и промолчит именно о новой
- Файл: `docs/architecture.md:11-26`; `openspec/changes/go-1-26-upgrade/tasks.md`,
раздел «3. Документы» (в нём `docs/architecture.md` нет)
- Severity: **minor** | Confidence: **high**
- Действие: **инлайн**
- Найдено проходами: `specs` (S3), `basics` (B1). **Дубль по причине, слит;**
предложение взято более широкое, от `basics`.
- Оракул — поимённые положения, все перепроверены триажем:
- `openspec/config.yaml:25-26` дословно: «Состояние спек и правило „первая
задача, трогающая поведение, заводит спеку своей capability“ —
docs/architecture.md, преамбула». Дом назначен, и он один;
- `docs/architecture.md:11` дословно: «Заведены три capability:»; `proposal.md`
заводит четвёртую;
- прецедент: прошлое изменение правило этот список **тем же коммитом**, что и
реализацию — `git show 01cc31d -- docs/architecture.md` даёт `-Заведены две
capability, и каждая описана частично:` / `+Заведены три capability:`;
- `tasks.md`, раздел 3, содержит ровно два пункта — `CLAUDE.md` и
`docs/review.md`; обзора архитектуры в нём нет;
- машинного оракула нет и быть не может: `CLAUDE.md`, «Гейт» — «согласованность
документов между собой и с кодом — её судят агенты, зовёт их скилл
`av-dev-docs:healthcheck`, и звать его надо руками».
- Последствие. После архивации в `openspec/specs/` появится четвёртая capability,
о которой единственный назначенный обзор молчит. Следующий, кто возьмётся за
версию инструмента сборки, пойдёт по указанному дому, четвёртой строки не найдёт
и либо заведёт вторую спеку на ту же тему, либо припишет требование в `pipeline`
— ровно то, от чего `design.md:79-82` отказался. Отказ молчаливый: гейт этого
класса не ловит по устройству.
- Предложение (инлайн, три правки):
1. четвёртая строка перечня в `docs/architecture.md` — про `toolchain`, со
ссылкой на спеку и задачу-источник;
2. оговорка к преамбуле: сегодня она читается «поведение системы здесь не
описывается — нормативно оно живёт в `openspec/specs/`», а `toolchain`
описывает **не** поведение сервиса; без оговорки преамбула становится
неверной в момент архивации;
3. пункт в `tasks.md`, раздел «3. Документы», чтобы правка не потерялась.
**Третий слот блокирующих не занят** — кандидатов нет: всё прочее либо не
замерзает мерджем, либо не имеет оракула.
---
## 2. Стоит исправить сейчас (4 из 4)
### N-1. Единственный новый страж проекта не покрыт ничем: следующая правка его регулярных выражений перестанет ловить случай молча
- Файл: `scripts/check-go-version.sh` (весь); `tasks.md:96-111`
- Severity: **major** | Confidence: **high**
- Действие: **развилка** (новая работа, за границей объявленного scope)
- Найдено проходами: `autotests` (A1), `specs` (S4). **Дубль по причине, слит.**
- Оракул (перепроверено триажем):
- `find . -iname '*check-go-version*'` → ровно один файл, сам скрипт; тестов нет;
- `grep -rln 'check-go-version' --include='*_test.go' --include='*.bats'
--include='*test*.sh' .` → пусто;
- `task gate` прогоняет скрипт ровно на согласованном дереве, то есть проверяет
один сценарий дельты из четырнадцати — «Версии совпадают»;
- раздел «4. Проверка» в `tasks.md` перечисляет 11 сценариев, все `[x]`, но ни
один не зафиксирован ничем, кроме прозы: это разовый ручной прогон, а не
оракул;
- положение проекта, которое здесь нарушено, записано: `docs/review.md»,
«Типовые узлы» → «Любой узел»: «изменённое место покрыто хоть одним
**проходящим** тестом».
- Последствие. POSIX-sh с разбором четырёх разных форм через `sed`/`awk` — класс
кода, где правка одного образца ломает смежный случай беззвучно. `go vet`,
`golangci-lint` и `gofmt` shell не видят; `shellcheck` в гейт сознательно не
введён. Правка третьего аргумента `collect` или образца `read_doc` снимает
проверку молча, и заметят это на следующем подъёме версии — примерно через год,
и ровно тем способом, каким был найден дефект 2026-08-12: образ перестал
собираться, и этого не увидел никто. Класс — «молчание»: страж перестаёт
стеречь, не сообщая об этом.
- **Вопрос человеку:**
- **А. Сейчас, в этом изменении.** Тест-скрипт рядом
(`scripts/check-go-version.test.sh`) и отдельный шаг гейта: десяток
мутационных прогонов на временной копии дерева — по одному на сценарий дельты.
Цена ~100 строк shell плюс шаг Taskfile. Плюс: страж проверен ровно тем
способом, каким `docs/review.md` велит проверять оракулы.
- **Б. Задачей урожая, вместе с `shellcheck` (P-1).** Один шаг гейта, гоняющий и
линтер оболочки, и мутационные прогоны. Плюс: не раздувает изменение, scope
остаётся заявленным. Минус: между мерджем и задачей страж не проверен ничем, и
правки в этот промежуток пройдут вслепую.
- **В. Принять остаток осознанно** и записать строкой в `docs/review.md`,
«Недоступно проверке» → «Перестали проверять сознательно», с ценой. Плюс:
честно и бесплатно. Минус: следующий промах этого класса будет уже вторым.
### N-2. Новое машинное правило не попало в единственный индекс механизированного, и следующий проход конвенций будет сверять версии руками
- Файл: `docs/conventions/README.md:50-65`
- Severity: **minor** | Confidence: **medium**
- Действие: **инлайн**
- Найдено проходом: `code` (C3)
- Оракул — поимённое положение конвенций: `docs/conventions/README.md:52-53`
(«Проверяется командами из CLAUDE.md; прозой не дублируется») и `:64-65` («Не
названное здесь место механизации означает, что проход по конвенциям будет
добросовестно проверять уже проверенное»). Строка про `docs.py check` в той же
таблице — прямой прецедент внесения шагов гейта.
- Последствие. Дифф заводит новое машинное правило и не вносит его в индекс,
объявленный исчерпывающим. Следующий проход по конвенциям и следующий человек
будут считать согласованность версий непроверенной и сверять её руками. Заодно
это единственное место в `docs/conventions/`, откуда новый скрипт вообще был бы
виден: сегодня из дома конвенций он не виден никак.
- Предложение: строка в таблицу — `| Одно число версии Go в go.mod, Dockerfile,
CLAUDE.md и README.md | Taskfile.yml → шаг go-version
(scripts/check-go-version.sh) |`.
### N-3. Сломанное окружение шаг объявит расхождением версий и назовёт невиновный файл
- Файл: `scripts/check-go-version.sh:105-119`, `:147-153`
- Severity: **minor** | Confidence: **high** (проход давал `medium`; поднято
прогоном)
- Действие: **инлайн**
- Найдено проходом: `code` (C1)
- **Оракул — два прогона на копии дерева:**
1. `chmod a-r README.md && sh scripts/check-go-version.sh` →
```
sed: can't read .../README.md: Permission denied
check-go-version: README.md не называет версию Go
Объявленная версия Go по местам:
go.mod 1.26
Dockerfile 1.26
CLAUDE.md 1.26
README.md версия не названа
exit=1
```
2. прогон с `PATH`, где нет `awk` → **exit 127**, код вне словаря вовсе.
Для сравнения, штатные пути проверены и корректны: нет файла места → 3, лишний
аргумент → 2, директива `toolchain` → 1, согласованное дерево → 0.
- Последствие. Значения добываются подстановкой команд в **аргументе** (`collect
README.md "$(read_doc "$readmemd")" 1`), а POSIX теряет код возврата подстановки,
стоящей в аргументе простой команды. Любой отказ чтения — файл есть, но
нечитаем; урезанный `PATH`; сломанный апплет busybox — даёт пустой вход,
`collect` видит `total -eq 0` и печатает утверждение **о содержимом файла** там,
где сломалось окружение. По словарю, который этот же дифф и расширил, 1 значит
«дрейф», 3 — «окружение», 4 — «внутренний сбой»; кода 4 скрипт не возвращает ни
на одном пути.
- **Честная оценка веса.** Ущерб невелик: `sed` печатает свою причину строкой
выше, так что человек у терминала подсказку видит, а `task gate` различает
только ноль и не-ноль. Вероятность низкая — CI у проекта нет, гейт гоняет
владелец на своей машине. Находка остаётся потому, что шаг гейта — источник, на
который смотрят как на истину, и ложное утверждение о конкретном файле из такого
источника дороже своей вероятности.
- Предложение: добывать значения через промежуточную переменную с проверкой кода,
а не в аргументе — четыре места, по одному на источник.
### N-4. Цена отказа от `shellcheck` названа в памятке вчетверо, и вопрос от этого отложится снова
- Файл: `CLAUDE.md:122-123`; то же в `design.md:37-38`
- Severity: **minor** | Confidence: **high**
- Действие: **инлайн**
- Найдено проходом: `code` (C2); сюда же ушла находка `autotests` A3 как замер
цены.
- Оракул — замер, снятый триажем на этом прогоне:
- `find . -path ./.git -prune -o -name '*.sh' -print` → ровно два файла:
`./scripts/check-go-version.sh` и `./docker/entrypoint.sh`; в гейте из них
один. Остальные три шага гейта — `docs.py`, `tasks.py`, `openspec.py` — Python
и лежат в плагинах вне репозитория;
- `shellcheck scripts/check-go-version.sh` → **одно** замечание, SC1007 на
`root=$(CDPATH= cd -- ... )`, и оно ложное: `CDPATH= ` — идиома очистки
переменной перед `cd`. Значит заведение линтера стоит одной строки шага плюс
одной директивы подавления.
- Последствие. Строка «`shellcheck` для скриптов гейта — их четыре, и заводить им
линтер надо разом» смешивает «скриптов в гейте четыре» с «`shellcheck` применим
к четырём». Применим он к одному. Следующий прочтёт в памятке цену «надо разом,
четыре штуки» и отложит вопрос снова — при том что нелинтуемым остаётся ровно
тот файл, который проход `code` был вынужден разбирать глазами построчно, а
триаж — прогонять руками.
- Предложение: переписать пункт по факту — «shell-скрипт в гейте один; три
соседних шага — Python в плагинах. Линтер не заведён; цена ему одна строка шага
и одно подавление SC1007». Ту же правку в `design.md`.
---
## 3. Гипотезы без доказательства
### H-1. Имя и дом capability `toolchain` замерзают мерджем (понижено: оракула нет)
Из прохода `basics`, «Дешевле переделать до мерджа». После архивации спека уезжает
в `openspec/specs/` насовсем; `design.md` сам признаёт, что при одном требовании
переименовать дешевле, чем расщепить. Вопрос дома шире: четыре однородных шага
набора проверок живут в двух разных домах — у трёх плагинных дома нет вовсе, у
четвёртого есть нормативная спека, и эта асимметрия становится постоянной.
**Почему понижено.** Оракула нет и построить его нечем: это суждение о
правильности имени, а не о поведении. Плюс два смягчающих факта: стадия ревью
дизайна (`specs` + rubric) прошла до кода и её замечания отработаны — вопрос уже
был на столе; `CLAUDE.md`, «Необратимое», имени capability не перечисляет.
Severity снята.
**Остаток честный:** независимого архитектурного разбора у этого вопроса не было —
на метке `medium` отдельный проход не запускается, тему закрывал `basics` широким
профилем. Это и есть содержание сигнала о заниженной метке.
### H-2. Пересборка того же коммита через месяц даст другой `ffmpeg` (понижено: замера нет, строка не из этого диффа)
Из прохода `basics` (B3). `Dockerfile:26` — рантайм-слой `alpine:latest`, а
`Taskfile.yml:93` собирает с `--pull`: два образа из одного коммита с разницей в
неделю несут разные `ffmpeg`. Регрессия конвертации после такой пересборки
выглядит как задачи в `failed` при пустом диффе репозитория, и откат на прежний
коммит её не чинит. Класс тот же, ради которого написан весь новый шаг:
объявленное и собранное расходятся, и сверять некому.
**Почему понижено.** Замера нет — ни одного числа о том, как часто и насколько
меняется `ffmpeg` в `alpine:latest`; снять на этом прогоне нечем. Строка внесена
не этим изменением, только активирована им. `Confidence: medium`.
### H-3. «Два дома у семантики шага» — проверено и не подтвердилось
Из прохода `basics`. Утверждение: семантика шага описана и в `CLAUDE.md` «Гейт», и
в спеке `toolchain`, а `openspec/config.yaml` предупреждает, что «второй дом факта
расходится с первым молча».
**Проверено триажем:** спека `spec.md:93-96` не пересказывает словарь, а
**ссылается** на него — «раздел „Гейт“ в `CLAUDE.md` объявляет словарь общим». Это
уже правильная форма: один дом факта, вторая точка — ссылка. Последствия не
построено, находкой не выводится.
---
## 4. Promote candidates
- **P-1. `shellcheck` шагом набора проверок.** Цена замерена на этом прогоне:
файлов `.sh` два, в гейте один, единственное сегодняшнее замечание — ложный
SC1007 на идиому `CDPATH= cd`. Шаг стоит одной строки плюс одной директивы
подавления.
- **P-2. Род узла «скрипт набора проверок» в `docs/review.md`, «Типовые узлы».**
Сегодня перечень родов покрывает только рантайм. Скрипт гейта — новый род с
собственными проверяемыми свойствами: отличает «расхождение» от «сломанного
окружения», исход есть функция коммита, покрыт мутационным прогоном. Без этого
рода свойство «изменённое место покрыто хоть одним проходящим тестом» к shell не
приложено ничем, и находка N-1 в следующий раз опять будет добываться с нуля.
- **P-3. Обёртки `Taskfile` отдают 1, когда скрипта нет, а словарь велит 3.** Из
S2/B2, слитых по причине. **Но так делают все четыре обёртки** — `docs`,
`tasks`, `openspec` и новая `go-version`: новый шаг лишь повторил существующий
рисунок, и дефектом **этого** диффа это не является. Сам скрипт при отсутствующем
месте выходит корректно — 3 (проверено). Правило, а не правка: привести все
четыре обёртки к 3 разом либо убрать «или файл не найден» из описания кода 3 и
объявить `exit 1` нормой для «скрипта шага нет».
- **P-4. Норму «версия внешнего инструмента объявлена числом и сверяется»
распространить на рантайм-базу образа.** Из H-2. Non-Goal этого изменения
записан; кандидат в отдельную задачу.
- **P-5. Триггер метки: изменение, заводящее новую capability или новый каталог
верхнего уровня.** Из сигнала о заниженной метке. Сегодня «Триггеры метки» видят
только объём и незнакомость формы решения; ось «изменение трогает канон» в них
отсутствует, а на этом прогоне именно она дала обе блокирующие находки.
---
## 5. Границы покрытия
Секция не сокращается. Без неё формулировка «критичных проблем не обнаружено»
запрещена — и здесь она не употребляется.
### План: темы, дома, глубины
Все шесть тем ядра размечены, у всех есть дом, все закрыты — таблица в сводке.
**Тем без дома нет. Тем без отчёта нет. Своих тем сверх ядра проект не
объявляет.** Глубина «разбор» у пяти тем, у `autotests` глубина не назначалась.
### Какие проходы запускались
На метке `medium`, в режиме «по графу», запускались четыре: `autotests`, `specs`,
`code`, `basics`. Стадия ревью дизайна (`specs` + `rubric`) прошла раньше, до
кода, и её замечания отработаны — на этом прогоне она не повторялась.
### Какие проходы не запускались и почему
- **`review-architecture`** — не запускается на метке `medium` по устройству
графа; тема `architecture` отдана `basics`. Именно об этом сигнал двух проходов.
- **Проход независимой реализации** — снят из конвейера по стоимости.
- **Проход про идиоматичность языка** — упразднён.
### Что каждый запущенный проход не мог проверить в принципе
Оговорка о происхождении: **сырые выводы, поданные триажу, несут границы прозой, а
не блоком `Coverage of this pass` из контракта.** Перечень ниже восстановлен по
тому, что проходы написали, а не по их charter'ам, — и может быть неполон. Это
отдельная строка деградации.
- `autotests` — не судит содержание кода; видит зелёное/красное и наличие тестов.
Прогон гейта не собирает образ (намеренно) и не считает покрытие изменённых строк.
- `specs` — судит соответствие кода дельта-спеке и обратно; не судит качество кода
вне нормы и не проверяет, нужна ли норма вообще.
- `code` — темы `conventions` плюс технический разбор; Go-кода дифф не содержит,
поэтому `logging.md`, `errors.md`, `config.md` неприменимы поимённо, и разбор
свёлся к shell, который ни один линтер проекта не видит.
- `basics` — темы `security`, `operations`, `architecture` широким профилем; на
этой метке заменяет специализированные проходы, а не дополняет их.
- **Триаж не находит ничего нового по определению**: работает с чужими выводами и
своими прогонами-оракулами. Пропуск любого прохода — его пропуск тоже.
### Неприменимые темы и вопросы — названы, а не пропущены
- **`security`: тема неприменима к диффу целиком.** `internal/` не тронут ни
строкой. Три вопроса темы из `docs/review.md` адресованы
`internal/service/transcribe.go` и `internal/metrics` — они не менялись, вопросы
остаются открытыми и после этого прогона. Периметр сборки дом объявляет вне
модели: `docs/security.md:233-235`.
- **`operations`: применимы 2 вопроса из 6.** Отказ соседа, повтор и
одновременность, остановка на середине, наблюдаемость, рост объёма —
неприменимы: рантайм не меняется. Три вопроса темы из `docs/review.md` к диффу
неприменимы и остаются открытыми.
- **`autotests`: вопрос темы из `docs/review.md:133-134`** адресован
job-конвейеру в `internal/`, которого дифф не трогает.
- **`conventions`: вопрос темы** («новая колонка правится во всех четырёх местах»)
— **колонок изменение не трогает вовсе**; `internal/adapter/repo/pocketbase` не
изменён ни строкой.
### Что осталось целиком на человеке
**Не проверит ни один проход:**
- `operations`: поведение внешних сервисов под нагрузкой и на границах;
- `operations`: реальный профиль нагрузки;
- `security`: стойкость `ffmpeg` к вредоносному входу.
**Перестали проверять сознательно:**
- `autotests`: разбор вывода настоящего `ffprobe` — решение и цена в
`docs/adr/ADR-2026-08-11-stub-adapters-in-tests.md`.
**Своё, для этого изменения:** **собираемость образа на объявленной версии** —
требование «Объявленное число — то, на котором проект собирается» прямо оставляет
проверку человеку и запрещает вводить её в набор проверок. Косвенные свидетельства
положительные (`go version` → `go1.26.5`; гейт зелёный; образ `transcriber:dev` в
наличии), но **самой сборки триаж не запускал** — она объявлена сделанной пунктом
`tasks.md` 4.2.
### Каких документов проекта не хватило
- **`docs/review.md`, «Типовые ложноположительные» — есть и непуст (4 пункта), но
все четыре про рантайм `internal/`.** Дифф его не трогает, поэтому **проектных
ложноположительных для него не существует вовсе**: отсев вкусовщины шёл по общим
критериям устава, без проектного входа.
- **`CLAUDE.md`, «Инварианты» — раздел есть, 9 пунктов, и ни один не применим к
диффу.** Следствие названо прямо: **ни одна находка этого прогона не поднята до
`critical` по основанию «нарушен инвариант проекта» — сослаться не на что.**
Ранжирование велось по обратимости, выведенной из механики openspec, и это
**предположение триажа**, а не записанное правило проекта.
- **`CLAUDE.md`, «Необратимое» — имени capability, состава `openspec/specs/` и
раскладки `scripts/` в нём нет.** Поэтому «спека замерзает мерджем» — вывод из
механики openspec, а не проектная норма.
- **`CLAUDE.md`, «Ориентир по размеру порции: не замерялся» — дословно.** Значит
суждение «объём right-size» опирается на оценку триажа, а не на проектное число.
### Сработавшие потолки
- `basics` — **сообщил сам**: 3 находки при потолке 4, за срезом ничего.
- `code` — **сообщил частично**: «Потолок конвенций 1/4 — срез не сработал». Про
потолок технической половины не сказал ничего.
- `specs` — **не сообщил потолок вовсе.** 4 находки. **Это находка о прогоне:**
проход обязан сообщать потолок сам.
- `autotests` — **не сообщил потолок вовсе.** 3 находки. То же.
- **Триаж:** 2 из 3 блокирующих, 4 из 4 «стоит исправить сейчас». Секция «сейчас»
заполнена под завязку; ничего не выброшено молча.
### Выброшено — поимённо
1. **A2**: вопрос темы про шаг конвейера — к диффу не относится.
2. **A3**: `shellcheck` SC1007 — ложное срабатывание, действующего последствия нет;
содержание уехало замером в N-4 и P-1.
3. **Порядок шагов в `gate`**: самый дешёвый шаг стоит пятым. Вкусовщина по всем
трём условиям: поведения не меняет, стоимости следующего изменения не меняет,
записанной конвенции о порядке шагов в проекте нет.
4. **`proposal.md`: «Внешних зависимостей изменение не трогает»** против переезда
`smithy-go` из `// indirect` в прямые требования. Версия `v1.27.7` не менялась,
пакет импортируется в `internal/adapter/recognizer/yandex/s3.go:15` — штатный
результат заказанного `go mod tidy`. Прозаическая неточность без последствий.
5. **`CLAUDE.md:114-115`** «краснеет с именем недостающего плагина» — новый шаг
плагином не является. Последствия не построено; чинится вместе с P-3.
6. **`docs/review.md:194-198`**: преамбула журнала не сходится с верхней записью.
Расхождение приехало предыдущим коммитом, вне диффа.
### Четыре строки, которые не принесёт ни один проход
1. **Решения проекта не сверялись.** `docs/adr/` — процессный документ, прогон его
не открывает. Для этого изменения это особенно весомо: `design.md` ссылается на
решения, но ни один проход не открывал `docs/adr/` и не проверял, не
противоречит ли новая норма уже принятому.
2. **Записанные наблюдения проекта не использовались.** `docs/research/` — тоже
процессный. Всякое число в этом отчёте снято на этом прогоне и сопровождается
командой замера.
3. **Поимённая сверка с руководствами по стилю языка не задавалась ни одним
проходом.** Для этого изменения дыра шире обычного: основной артефакт —
POSIX-shell, у которого в проекте нет ни конвенции, ни линтера, ни руководства.
4. **Альтернативной реализации, с которой можно сдиффить решения, у конвейера
нет.** Вопрос «а можно ли было решить это принципиально иначе — например, одним
`go.mod` как источником истины и генерацией остальных трёх мест» никто не
задавал.
### Среда прогона
- **Рабочее дерево не тронуто.** Все оракулы добыты на копиях в scratchpad. `git
status --porcelain` до и после прогона совпадает символ в символ.
- **Запреты `CLAUDE.md` соблюдены:** боевой каталог данных не трогался, боевой
токен не запускался, в Yandex Cloud не ходили, выкладка не запускалась,
`testdata` не заводилась, временное — только в scratchpad.
- **Отказов доступа не было.** `deny: Read(./build)` на путях этого изменения не
сработал (capability названа `toolchain` именно поэтому).
- **Ограничение среды:** подагентам запрещено писать файлы отчётов. Отчёт возвращён
триажем текстом и записан сюда оркестратором.
---
# Дополнение: перепроверка после отработки B-1
Записано оркестратором после того, как находки триажа были отработаны. Отчёт без
этого раздела сообщал бы о составе прогона неверно.
## Что было сделано по находкам
| Находка | Действие | Исход |
|---|---|---|
| B-1 | развилка → человеку | Выбран вариант А: правило множественности разведено по местам, рамка в документах сужена до раздела стека. Изменило требование и код |
| B-2 | инлайн | Четвёртая capability и оговорка о её природе — в преамбуле `docs/architecture.md`. Ссылка на спеку поставлена шагом синка: до архивации файла нет и `docs.py check` краснеет битой ссылкой |
| N-1 | развилка → человеку | Выбран вариант Б: задачей урожая, вместе с `shellcheck`. Между мерджем и той задачей страж не проверен ничем — названо остатком |
| N-2 | инлайн | Строка в таблице «Механизировано» `docs/conventions/README.md` |
| N-3 | инлайн | Проверка читаемости места: код 3 и сообщение о нечитаемости вместо «версия не названа» с кодом 1 |
| N-4 | инлайн | Цена отказа от `shellcheck` названа по замеру в `CLAUDE.md` и `design.md`; ложный `SC1007` подавлен в скрипте |
## Второй прогон: только проход `specs`
**Полный прогон ревью кода не повторялся.** Правка по B-1 изменила дельта-спеку и
код, поэтому перепрогнан **целенаправленно один проход** — `specs`, владеющий
темой `requirements`, чей дом и изменился. `autotests` заменён собственным
прогоном гейта оркестратором; `code` и `basics` не перезапускались.
**Чем это ограничено, прямо:** технический разбор новых функций `section`,
`has_section` и `collect_doc` независимым проходом **не выполнялся** — их читал
только `specs` в своей оптике (соответствие норме) и оркестратор. Проход `code`
видел прежнюю редакцию скрипта. Триаж второй раз не запускался, поэтому находки
ниже не проходили дедупликации и добычи оракула независимым агентом — оракулы у
них свои, прогонами.
**Разметка не повторялась,** хотя дельта-спека менялась. Причина названа: правка
сузила формулировку одного требования внутри уже размеченной capability, не
меняя ни набора capability, ни периметра узлов, ни списка тем — план тем остался
бы тем же. Это осознанное отступление от правила «дельта-спеки изменились —
повтори разметку», а не пропуск.
## Находки перепроверки — три, все minor, все отработаны
- **S1 закрыта по существу**, а не переформулировкой. Проверено обеими сторонами:
норма больше не требует единственности от сборочного образа, и код ровно это и
делает; правдивая историческая строка о прошлой версии в памятке даёт зелёное,
а второе число внутри раздела стека — красное.
- **F-1.** Таблица `design.md` продолжала велеть «ровно одно вхождение на файл» —
правило, обратное принятой норме, — и переживала бы мердж как единственное
описание того, как машина ищет число. Абзац-мотивировка вдобавок стал
фактически неверен. Переписаны оба.
- **F-2, дороже прочих.** Норма называла начало раздела и молчала о конце.
Раздел закрывался только заголовком того же уровня, а строка, похожая на
заголовок, внутри блока кода читалась как настоящий заголовок. Худший исход —
**ложное зелёное**: если строку версии из раздела убрать, а ниже по файлу
появится заголовок первого уровня и любое «Go 1.26», шаг добрал бы число из
чужого места и промолчал. Граница определена требованием и исполнена кодом;
контрольный прогон подтверждает, что ложное зелёное исчезло — шаг теперь
честно говорит «версия не названа».
- **F-3.** Отличие «сломанного окружения» от «пропавшей строки» держалось только
на коде: ни один сценарий его не требовал, автотеста нет, гейт гоняет скрипт
ровно на согласованном дереве. Записано требованием и сценарием.
## Оракулы перепроверки
Регрессионная батарея — **21 прогон**, все совпали с ожиданием: 4 мутации по
минору и 4 удаления строки версии (по одной на место), директива `toolchain`,
патч в теге, смена базы образа, два слоя одной версии, два слоя разных версий,
дубль внутри раздела, история вне раздела, пропавший раздел, `PATH` без `go`,
запуск из подкаталога, коды выхода 0/1/2/3. Отдельно проверено, что при
нечитаемом файле строка «не называет версию» не печатается ни разу.
Сверх того: `openspec validate --strict` — valid; `task gate` — exit 0;
`task image` пересобран на `golang:1.26-alpine` — exit 0; `shellcheck` на скрипте
чист.
## Что осталось открытым после отработки
- **N-1: страж не покрыт ничем.** Решением человека уехало задачей урожая. Из
девятнадцати сценариев дельты машина гоняет один — тот, где всё сошлось;
остальные восемнадцать подтверждены разовыми прогонами. До закрытия той задачи
правки скрипта идут вслепую.
- **H-2: рантайм-база образа берётся «последней доступной».** Два образа из
одного коммита с разницей в неделю несут разные `ffmpeg`. Вне границ задачи,
уезжает урожаем.
- **Сигнал о заниженной метке** остаётся фактом для человека: ось «изменение
трогает канон» в правиле выбора метки отсутствует, а на этом прогоне именно она
дала обе блокирующие находки. Кандидат в правило — P-5.