# Ревью изменения `trusted-header-login` ## Сводка - **Режим прогона:** по графу. **Метка:** `large`. *Размер и сложность в задании триажу не переданы* — обоснование разметки воспроизвести нечем, метка взята как объявленная. - **База диффа:** `origin/master`. Изменение читалось вместе с непрослеженными файлами (`cmd/devtools/`, `internal/adapter/repo/pocketbase/identity.go`, `migrations/202608220001_trusted_header_login.go`, `internal/controller/http/identity.go`, дельта-спеки change). - **Состояние гейта:** зелёный целиком, кроме шага `dockerfile` — `hadolint` даёт `DL3066` на `Dockerfile:94`. Отказ **унаследован** (воспроизведён на чистом `origin/master`), долг объявлен строкой в `CLAUDE.md`, раздел «Гейт». Новым красным шагом это изменение гейт не красит. - **На входе:** 31 пронумерованная находка проходов + 4 пункта «дешевле переделать до мерджа» от `architecture` + 1 замер без дефекта от `ops`. **На выходе:** 3 + 4 = 7 в основных секциях, остальное — ниже, ничего не выброшено молча. ### План с исходом по каждой теме | тема | дом | глубина | кто закрывает | исход | |---|---|---|---|---| | requirements | дельта-спеки change + `openspec/specs/` | разбор | `specs` | закрыта, 8 находок (5–12); в отчёт ушли 2, ещё 4 — ниже | | autotests | `CLAUDE.md`, «Гейт» | — | `autotests` | закрыта, 4 находки (1–4); в отчёт не ушла ни одна самостоятельно, одна закрывается правкой №3 | | conventions | `docs/conventions/` | разбор | `code` | закрыта, 8 находок (15–22); в отчёт ушли 2 | | architecture | `docs/architecture.md` + `docs/passport.md` | доказательство | `architecture` | закрыта, 2 находки + 4 пункта «до мерджа»; в отчёт ушла 1 | | security | `docs/security.md` | доказательство | `adversary` | закрыта, 6 находок (23–28); в отчёт ушли 3 | | operations | `docs/architecture.md` «Эксплуатация» + `docs/database.md` | доказательство | `ops` | закрыта, 3 находки + 1 замер без дефекта; в отчёт не ушла ни одна, старшая — в гипотезах | - **Тем без отчёта нет.** Тем без дома в плане нет. - **`basics` не запускался** — своих тем проекта нет, все шесть разобраны именными проходами. - **Сигнал о заниженной метке:** `review-code` отработал и возражений по метке **не подал**; `review-basics` не запускался, поднять метку было некому. Молчание здесь — молчание одного корректора из двух, а не двух из двух. --- ## Блокирует мердж ### Запрос, отвергнутый ограничителем частоты, всё равно заводит учётную запись - Файл: `internal/controller/http/identity.go:91`, `internal/controller/http/errors.go:219` - Severity: major (`critical` не ставлю: инвариант `CLAUDE.md` этим не нарушен, а путь ведёт к мусору в коллекции, а не к порче чужих данных) - Confidence: high - Оракул: **свой падающий тест, прогнан на этом прогоне.** Слой узнавания стоит на `DefaultLoadAuthTokenMiddlewarePriority + 1` = **−1019**, ограничитель библиотеки — на **−1000** (`apis/middlewares_rate_limit.go:16`), то есть узнавание отрабатывает **до** ограничителя. Прогон: 120 запросов выбирают бюджет, следующие 50 запросов новыми логинами получают `429` — **все 50**, — а учётных записей в коллекции становится `2 → 52`. - Последствие: ограничитель частоты не защищает **ничего** из того, что делает слой узнавания. Каждый отвергнутый запрос — это как минимум чтение из базы (поиск по логину идёт на каждом запросе по норме `access`), а с новым именем — ещё и запись. Учётная запись, заведённая мусором, из сервиса не убирается: спека `access` называет цену прямо — «слить их или убрать нечем». Барьер «заголовок ставит прокси» этот путь сужает, но не закрывает: `docs/review.md`, «Недоступно проверке», записывает поведение прокси как непроверяемое отсюда, и строить на нём защиту базы значит держать один барьер вместо двух. - Предложение: перенести слой узнавания на `apis.DefaultRateLimitMiddlewarePriority + 1` (−999), а `RequireUser` — на `+ 2` (−998). Порядок «токен (−1020) → ограничитель (−1000) → узнавание (−999) → требование записи (−998) → предел тела (−990)» сохраняется целиком. - Найдено проходом: `adversary` (25); подтверждено собственным прогоном триажа. - Действие: **инлайн** > **Ответ на первый вопрос задания — перестановка проверена запуском.** С > приоритетами −999/−998 прогнан **весь** `go test ./...`: единственным красным > остался мой временный тест на находку про общий бюджет (ниже, №4), все > существующие проверки пакета `internal/controller/http` — включая оба критерия > приёмки, победу предъявленного токена над заголовком и «проба здоровья > учётной записи не заводит» — прошли. Заведение записи после `429` при этом > пропало: `2 → 2`. Предел тела на −990 действительно остаётся после требования > учётной записи, «отказ неузнанному до чтения тела» сохраняется. > > **Одно последствие перестановки задание не называет, и оно настоящее.** Сейчас > `RequireUser` (−1018) отказывает неузнанному **до** ограничителя, и отказы > бюджета не тратят. После переноса — тратят: замер на этом прогоне, 125 > запросов без заголовка, и следующий запрос узнанного человека получает `429` > (до перестановки — `200`). Само по себе это правильнее (счёт отказов и есть > работа ограничителя), но вместе с общим бюджетом (№4) оно даёт анониму, > дотянувшемуся до контейнера, выключение сервиса для всех. Поэтому №1 и №4 > едут одной правкой, а не порознь. ### Ключ учётной записи правится рукой в панели, и архив уезжает следующему с этим именем - Файл: `internal/adapter/repo/pocketbase/migrations/202608220001_trusted_header_login.go:77-82`, `internal/adapter/repo/pocketbase/panel.go:33-34` - Severity: major - Confidence: high - Оракул: дословное требование дельта-спеки `openspec/changes/trusted-header-login/specs/access/spec.md`: «**Ключ учётной записи MUST не правиться ничем, кроме заведения самим сервисом.** Ни запросом снаружи, ни рукой в панели». Плюс `grep -rn "OnRecordUpdateRequest" --include=*.go internal/ cmd/` → единственная привязка стоит на `migrations.RecordsCollection`, на `users` хука нет. Шаг схемы закрывает правила API (`UpdateRule = nil`), а панель работает суперпользователем — это записано комментарием самого шага, строки 48–50. - Последствие: переписанный в панели `provider_login` отдаёт весь архив прежнего владельца тому, кто придёт с этим именем следующим. Владелец записи назначается один раз и не меняется — вернуть архив нечем. Правка своя, сознательная, но необратимая и **молчаливая**: журнала событий у коллекции пользователей нет. - Предложение: развилка ниже. - Найдено проходом: `specs` (6) - Действие: **развилка** > **Вопрос владельцу.** Требование спеки «ключ не правится рукой в панели» > сегодня не держит ничто: правила API закрыты, но панель ходит > суперпользователем. Три варианта: > **(А)** завести хук `OnRecordUpdateRequest(users)`, отвергающий смену > **непустого** `provider_login` — в проекте уже есть тот же приём для записей > (`pbrepo.BindPanelRules`), цена — десяток строк и проверка; > **(Б)** переписать требование, назвав панель доверенной и записав цену > («ключ правится владельцем сервиса, откатить правку нечем»); > **(В)** оставить как есть — тогда спека и код расходятся молча, и это > расхождение переживёт мердж. ### Заведение учётной записи не оставляет в журнале ни строки, а поломка контура пишется на уровне, которого в бою нет - Файл: `internal/controller/http/identity.go:101-108,128-151`, `internal/adapter/repo/pocketbase/identity.go:52-87` - Severity: major - Confidence: high - Оракул: **свой прогон.** Запрос новым логином с доверенного адреса → ответ `200`, учётная запись заведена, перехваченный журнал — **пустая строка** (`""`). Дословное требование дельта-спеки `access`: «Исход узнавания MUST оставлять строку журнала — и когда заголовок пришёл с недоверенного адреса, и **когда учётная запись заведена**. […] Строка несёт адрес пира и идентификатор учётной записи». Второй симптом: ветвь «более одного значения `Remote-User`» (строки 103–106) пишет `Debug`, боевой уровень — `Info` (`docs/conventions/logging.md`, «Расхождение: уровень зашит константой»). - Последствие: сервис заводит учётные записи молча — владелец не отличит «никто не заходил» от «завелось двадцать записей», а по спорной учётной записи не скажет, когда и с какого адреса она появилась. Хуже второе: `docs/security.md` называет прокси, добавляющий заголовок вместо замены, **главным** барьером контура, и половину этой беды сервис закрывает сам — но закрывает **невидимо**: строка о двух значениях в боевом журнале не появляется вовсе. Поломка контура, которую сервис поймал, владельцу неотличима от тишины. - Предложение: `logger.Info` на исходе «учётная запись заведена» — с адресом пира и идентификатором записи, **без** значения заголовка (инвариант «Содержимое записи остаётся приватным» и требование спеки «MUST не нести значения заголовка»); уровень ветви «два значения» поднять до `Warn` — ровно как у ветви недоверенного пира, по той же причине и с той же ценой. - Найдено проходами: `specs` (5) и `adversary` (26) — две находки об одной причине, слиты; **оракула у согласия проходов нет, оракул свой**. - Действие: **инлайн** --- ## Стоит исправить сейчас ### Бюджет ограничителя частоты общий на весь сервис: один человек выключает сервис остальным - Файл: `internal/controller/http/rate_limit.go:20-23`, `internal/controller/http/app.go:43-63`, `internal/config/config.go:91-96`, `cmd/transcriber/main.go:238-246` - Severity: major - Confidence: high - Оракул: **свой падающий тест.** 125 запросов первого человека выбирают бюджет, **первый** запрос второго человека получает `429`. Причина — в исходниках библиотеки: `checkRateLimit` берёт ключом `e.RealIP()` (`apis/middlewares_rate_limit.go`), а `RealIP()` при пустом `Settings.TrustedProxy.Headers` откатывается к адресу пира (`core/event_request.go:40-75`). Пир теперь **всегда** обратный прокси. - Последствие: объявленная сервисом частота опроса выводится из доли бюджета (`pollBudgetShare = 8`, `PollIntervalMs = 4000`) в расчёте на то, что бюджет делят немногие. Делят его **все**: восемь одновременных опросов выбирают его целиком, и девятый человек получает `429` на пустом месте. Отказ шумный, но причина невидима — по журналу он неотличим от собственной активности. - **Дефект старше этой задачи** (правило заведено `spa-skeleton` 2026-08-15, прокси перед сервисом стоял и тогда). Уточнение к формулировке прохода: `docs/database.md:344` и `app.go:43-49` **не** утверждают обратного — там прямо написано «бюджет считается по адресу спрашивающего, а не по учётной записи», и даже назван случай «двое за одним домашним адресом делят его пополам». Сломалось не правило, а **основание** правила: «адрес спрашивающего» выродился в один адрес на всех, и число 120/60 выбиралось не под это. - Найдено проходами: `architecture` (13) и `adversary` (23); подтверждено собственным прогоном. - Действие: **развилка** > **Ответ на второй вопрос задания: чинить здесь.** Не потому, что дефект > этой задачи — он не её, — а потому, что правка №1 делает его достижимым для > **анонима**: после переноса `RequireUser` за ограничитель отказы неузнанному > начинают тратить общий бюджет (замерено на этом прогоне: 125 анонимных > запросов → узнанный человек получает `429`). Мерджить №1 без ответа на №4 > значит завести новый путь к отказу сервиса. > > **Вопрос владельцу.** Чем считать бюджет ограничителя, когда весь трафик > приходит с одного адреса: > **(А)** заполнять `Settings.TrustedProxy.Headers` при подъёме (там же, где > `ApplyAppRateLimit`) — тогда ключом станет адрес человека из > `X-Forwarded-For`. Доверие к этому заголовку той же природы, что доверие к > `Remote-User`, и опирается на тот же перечень адресов; цена — ещё одно > требование к контуру, которое отсюда не проверить; > **(Б)** признать бюджет общим на сервис: поднять числа под ожидаемое число > людей, переписать `docs/database.md` и обоснование `pollBudgetShare`; > **(В)** отложить в урожай и мерджить №1 с известным ухудшением — тогда > анонимный поток через прокси выключает приложение всем. ### Негодное значение `Remote-Name` запирает человека в сервисе навсегда пятисоткой - Файл: `internal/adapter/repo/pocketbase/identity.go:71-87,147-159`, `internal/controller/http/identity.go:133-147` - Severity: major - Confidence: high - Оракул: **свой прогон.** Запрос с доверенного адреса, `Remote-User: namebearer`, `Remote-Name` из 5000 знаков → ответ `500` `{"error_code":"internal","message":"Внутренняя ошибка сервиса"}`, в журнале `level=ERROR msg="Failed to resolve account by login header" error="failed to create user account: name: Must be no more than 255 character(s).."`. Значение заголовка в ответ и в журнал при этом **не** уехало — инвариант приватности цел. - Последствие: приёму подвергается только логин (`AcceptProviderLogin`), а имя и почта уезжают в колонку как есть. Человек, чьё имя у провайдера длиннее 255 знаков, получает `500` на **каждом** запросе и в сервис не попадёт никогда; владелец получает `ERROR` на каждый такой запрос. Смежный симптом той же причины: разбор отказа судит по **наличию колонки** в `validation.Errors`, а не по причине отказа — негодный адрес почты неотличим от занятого, и запись молча заводится без почты вместо честного разбора. - Предложение: привести имя и почту к годным значениям в одном месте с логином — имя усечь до предела колонки, негодную почту не ставить вовсе. Этой же правкой закрывается непокрытая ветвь «отказ хранилища → отказ сервиса» (`identity.go:144-146`), которую `autotests` называет единственным намеренным исключением из «слой отказа не выдаёт»: у неё появляется достижимый и проверяемый вход. - Найдено проходами: `specs` (7, 10), `code` (15, 16), `adversary` (24) — пять формулировок, две причины, одно место правки; слиты. Согласие пяти проходов `Confidence` не повышает — оракул свой. - Действие: **инлайн** ### Образец конфига велит верить всей подсети docker: любой контейнер на хосте входит под любым именем - Файл: `config.example.toml:69-77` - Severity: major - Confidence: medium (путь не построен: второго контейнера в прогоне нет) - Оракул: положение `docs/security.md:109` дословно — источник заголовков есть «Обратный прокси — и **всякий, кто дотянулся до сервиса с доверенного адреса**». Образец задаёт этим адресом `172.16.0.0/12`, то есть весь умолчательный диапазон docker: под него попадает любой контейнер на хосте, а не только Caddy. - Последствие: барьер узнавания держится **только** на адресе пира (кук и токенов сервис не выдаёт). Любой сосед по хосту — а хост держит пет-проекты, а не один сервис — ставит `Remote-User: <чужой логин>` и получает чужой архив целиком, молча. Класс риска в `security.md` назван, ширина перечня — нет. - Предложение: развилка ниже. - Найдено проходом: `adversary` (27) - Действие: **развилка** > **Вопрос владельцу.** Чем сузить перечень доверенных адресов до самого > прокси: > **(А)** выделенная сеть compose с фиксированной подсетью под пару > «прокси ↔ сервис» — правка живёт в `pet-project-server`, здесь остаётся > значение образца и строка требования к контуру; > **(Б)** статический адрес прокси в сети docker — то же, но проще и хрупче; > **(В)** принять `/12` как цену и записать её строкой в `docs/security.md` > рядом с уже названным классом — тогда это осознанная цена, а не умолчание > образца. ### Три конвенции проекта описывают снятый механизм, и одна противоречит себе внутри файла - Файл: `docs/conventions/web-ui.md:107-109`, `docs/conventions/config.md:60-64` против `:99`, `docs/conventions/logging.md:34-37` - Severity: minor - Confidence: high - Оракул: чтение файлов на этом прогоне. `web-ui.md`: «**Сессия живёт кукой `transcriber_session`**» — кук сервис больше не ставит вовсе. `config.md` строка 60–64: «Секретов в этой секции больше нет», строка 99 в том же файле по-прежнему перечисляет `auth.client_secret` среди секретов. `logging.md` строки 34–37 описывают расхождение у пакета `cmd/oidcstub`, который этим же изменением удалён (`git status` → `D cmd/oidcstub/main.go`). - Последствие: конвенции — канон проекта, по ним работает следующая задача. Запись про куку заставит следующего агента искать куку и объяснять её отсутствие; запись про `client_secret` заставит охранять секрет, которого нет. Машина этого не ловит: согласованность документов между собой и с кодом судят агенты, и звать их надо руками (`CLAUDE.md`, «Гейт»). - Предложение: снять три записи, поправить перечень секретов в `config.md`. - Найдено проходом: `code` (20) - Действие: **инлайн** --- ## Гипотезы без доказательства Понижено и не влезло в потолок; каждая с причиной понижения. - **Откат бинаря поверх шага схемы `202608220001` обрывает вход** (`ops`, 29; было `major`). Оракул у прохода настоящий — падающий тест на восстановленном старом коде во временном worktree, `401` вместо `302`. Понижаю до `minor` **по стадии и по перекрытию**: (1) стадия — стройка, сервис на сервере остановлен, данных нет, откатываться не с чего; (2) `docs/architecture.md:142` уже объявляет откат неработающим начиная с шага `202608140002` — то есть любой бинарь старше 2026-08-14 и так не поднимается; новое окно отката — только версии между 14 и 22 августа, ни одна из которых не выкладывалась; (3) старый бинарь на новом конфиге и вовсе не стартует, если владелец убрал ключи OIDC по процедуре из «Эксплуатации» — тогда отказ громкий, а не молчаливый. **Ответ на третий вопрос задания: да, стадия меняет серьёзность.** Правка тем не менее дешёвая и уместная — строка в `docs/architecture.md`, «Эксплуатация», рядом с уже стоящей про `202608140002`: не потому что откат сегодня возможен, а потому что порог перехода там принято называть прямо. - **Уникальный индекс по `provider_login` не частичный** (`code`, 17). Оракул прохода — исходники библиотеки (`core/collection_model.go:648-671`, индекс собирается без `WHERE`, сосед по коллекции — `WHERE email != ''`). Понижаю: сервис пустого логина в колонку не кладёт никогда (`AcceptProviderLogin` отвергает), коллекция пользователей на выкладке пуста, и последствие сводится к «в панели рукой не завести вторую запись без логина». Промах будущего, не дефект сегодня. - **Предел логина считается в байтах, колонка — в знаках** (`specs`, 8; `code`, 18). Проверено чтением: `len(login) > 255` в `identity.go:174` против `Max: 255` у текстовой колонки. Понижаю: перекос только в сторону строгости, кириллический логин длиннее 127 знаков у Authelia не встречается, следа `Debug` хватает. Пути нет — гипотеза. - **Склейка значений заголовка запятой обходит защиту «два значения»** (`adversary`, 28). Пути нет и построить его отсюда нечем: `net/http` заголовки запятой не сливает, а прокси, который сливает, — ровно тот класс, который `docs/review.md` относит к «не проверит ни один проход». Последствие при этом было бы неприятным и тихим (человек попадает в **новую** пустую учётную запись вместо своей), поэтому строка оставлена, а не выброшена. - **Откат шага схемы не восстанавливает `OAuth2.Enabled` и минирует записи без почты** (`specs`, 9; `ops`, 30). Оракул у `ops` есть — прогон на временной базе, `email: cannot be blank`. Понижаю по тому же основанию, что и №29: `down` на сервере не зовётся, а применённый шаг не переписывается по инварианту. - **Отказ хранилища при узнавании не покрыт ничем** (`autotests`, 2). Не выведена отдельной строкой: правка про негодное имя даёт этой ветви достижимый вход, и тест на неё пишется той же правкой. Если владелец выберет не чинить негодное имя — эта строка возвращается самостоятельной находкой. - **Непокрытые ветви `retryAfterConflict` и `web/src/api.ts`** (`autotests`, 1, 3, 12). Понижаю по записанному решению проекта: `CLAUDE.md`, «Гейт» — «покрытие изменённых строк не считается ничем». Своего `api.test.ts` у нового `web/src/api.ts` действительно нет (проверено `ls web/src/`), и это стоит завести — но это задача, а не блокирующая находка. --- ## Promote candidates Претензии на правило, а не на этот код. - **Поле `peer` заведено мимо словаря имён журнала** (`code`, 21). `docs/conventions/logging.md:99` требует точечной иерархии для системных доменов (`http.*`, `ext.*`, `webapp.*`), а в файле уже стоит *Расхождение* с перечнем самозаведённых имён. Кандидат в конвенцию: внести `http.peer_addr` в таблицу полей — тогда следующий агент возьмёт имя из словаря, а не сочинит третье. - **Третий способ вывода в репозитории** (`code`, 22). `cmd/devtools` печатает через stdlib `log`, тогда как `logging.md` знает два. Записанной конвенции об оснастке у проекта нет — значит это кандидат в правило, а не нарушение. - **Единственный дом имён заголовков `Remote-*`** (`architecture`, 14). `cmd/devtools/main.go:92-97` пишет `"Remote-User"`, `"Remote-Name"`, `"Remote-Email"` своими литералами, тогда как нормативные константы лежат в `internal/controller/http/identity.go:23-27`. Правка на две строки, но правило общее — «нормативное имя объявляется один раз» — и его стоит записать, а не чинить точечно. Смежный вопрос `docs/review.md`, «Вопросы по темам»: не завёл ли инструмент разработчика второй дом тому, что уже есть в проверках. - **Второй словарь текстов на клиенте** (`code`, 19). Половина находки — ложноположительная: `docs/conventions/web-ui.md:118` **сам** разрешает клиентский текст «связи нет» состоянием, а не ошибкой. Настоящий остаток — расхождение текстов: сервер на `401` отвечает «Требуется вход» про вход, которого у сервиса больше нет, а экран показывает свой текст поверх. Кандидат в правило: конвенция сегодня не различает «текст под код ответа» и «текст под состояние клиента», и на этом различии находка и разошлась. --- ## Урожай Настоящее, но не для этого мерджа. - **Бюджет ограничителя частоты по человеку, а не по адресу** — если владелец выберет вариант (В) на развилке №4. - **Секция конфига называется `[auth]` при отсутствии всякого входа** (`architecture`). Имя ключа конфига — из перечня необратимого в `CLAUDE.md` («спрашивается у человека всегда»), но **сегодня стадия стройки**: на сервере ничего нет, и переименование стоит ноль. После первой выкладки — уже решение человека. Если переименовывать, то сейчас. - **Колонка почты без единого читателя** (`architecture`). Почта пишется при заведении и не читается ничем. Не дефект: спека объявляет её необязательной и берёт «при заведении». Но колонка без читателя — долг, который стоит либо оправдать спекой, либо снять. - **Корень `/auth` перестал быть зарезервированным** (`architecture`). Проверено: упоминаний `/auth` в `app.go` больше нет. Освободившееся адресное пространство стоит либо занять, либо назвать свободным в спеке `webapp`. - **Свой тест у `web/src/api.ts`** — файл новый, разбор отказов в нём есть, проверки нет. - **Строка про порог отката в `docs/architecture.md`, «Эксплуатация»** — см. гипотезу №29. --- ## Границы покрытия ### План: темы, дома, глубины Воспроизведён таблицей в сводке выше целиком. Тем без дома в плане нет, тем без отчёта нет. ### Что запускалось - Режим: по графу, метка `large`. Запущены `specs`, `autotests`, `code`, `architecture`, `adversary`, `ops`. - **`basics` не запускался** — своих тем проекта у него на этом прогоне нет, все шесть тем разобраны именными проходами. Следствие: сигнал о заниженной метке мог подать только `review-code`, и он его не подал. ### Чего не хватило триажу, и это находки о прогоне - **Блоков «Coverage of this pass» в сырых выводах нет ни у одного прохода.** Контракт находок требует их у каждого, и сводить мне было нечего: строку «что этот проход не мог проверить в принципе» я по каждому проходу воспроизвести не могу. Ниже — только то, что выводится из плана и из документов проекта. - **Сработавшие потолки ни один проход не назвал.** Сколько находок проход показал, каков был его потолок и что осталось за срезом — не сообщено никем. Значит утверждать «проход показал всё, что нашёл» нельзя ни про один из шести. - **Размер и сложность изменения триажу не переданы** — только метка. Обоснование разметки в отчёте воспроизвести нечем. ### Что осталось целиком на человеке Из `docs/review.md`, «Недоступно проверке». **Два списка, и они не сливаются.** **Не проверит ни один проход:** - `operations`: поведение внешних сервисов под нагрузкой и на границах — SpeechKit и Object Storage поднять в тесте нечем; - `operations`: реальный профиль нагрузки; утверждения о росте остаются условиями, а не замерами; - `security`: стойкость `ffmpeg` к вредоносному входу; - `security`: **поведение настоящей Authelia и правило обратного прокси на домен сервиса.** С 2026-08-22 от прокси зависит **весь** барьер: он обязан заголовки `Remote-*` перезаписывать, а не пропускать пришедшие. Правило живёт в `pet-project-server`, и отсюда его не проверить. Прямо на этом висят находки №1 (насколько узок путь к заведению мусорных записей), №6 (ширина перечня доверенных адресов) и гипотеза про склейку запятой; - `security`: поведение браузера с куками — своих кук сервис больше не ставит, класс сузился до кук панели хранилища. **Перестали проверять сознательно:** - `autotests`: разбор вывода настоящего `ffprobe` — решение и цена в `ADR-2026-08-11-stub-adapters-in-tests.md`; - **работа сервиса с настоящими внешними собеседниками** в остатке: за настоящие SpeechKit и Object Storage живой прогон не отвечает — ключи выдуманные, распознавание подменяется в коде. Вход при этом с 2026-08-22 живой прогон проверяет целиком (`cmd/devtools proxy`), и это сдвиг в другую сторону. Плюс общее, что не проверяет ни один прогон: история инцидентов, поведение под реальным потоком, поведение внешних систем в их версиях, завязка потребителей на текущее поведение и вопрос «а нужна ли эта функциональность вообще». ### Каких документов проекта не хватило - `docs/review.md`, «Типовые ложноположительные» — **есть и не пуст**, отсев по нему выполнен. Одна находка (`code`, 19) отсеяна наполовину по общим критериям, а не по нему: разрешение клиентского текста для состояния «связи нет» стоит в `docs/conventions/web-ui.md`, а не в разделе ложноположительных. - `CLAUDE.md`, «Инварианты» — есть; ни одна находка этого прогона до `critical` по этому основанию не поднята, потому что ни одна инвариант не нарушает: проверено поимённо для приватности содержимого (значение заголовка в журнал и в ответ не уходит — замерено), для секрета в конфиге (секретов в секции `[auth]` больше нет) и для запрета переписывать применённый шаг схемы. - **Прочих пробелов в документах проходы не назвали** — но назвать их было некому: блоков границ у проходов нет (см. выше). Отсутствие строки здесь не означает, что документов хватило. ### Четыре строки, которых не принёс ни один проход 1. **Решения проекта не сверялись.** `docs/adr.*` — процессный документ, прогон его не открывает. Изменение заводит `ADR-2026-08-22-login-by-trusted-header.md`, и сверку его с остальными записанными решениями — включая те, что это изменение помечает (`ADR-2026-08-12-oidc-exchange-via-own-route`, `ADR-2026-08-12-session-without-refresh`), — конвейер ревью не делал. Расхождение изменения с записанным решением ловит скилл `av-dev:doc-healthcheck`, и звать его надо руками. 2. **Записанные наблюдения проекта не использовались.** `docs/research.*` — тоже процессный. Всякое число в этом отчёте снято на этом прогоне: приоритеты слоёв взяты из исходников библиотеки в `GOMODCACHE`, счёт учётных записей и коды ответов — из прогонов, приложенных к находкам. 3. **Поимённая сверка с руководствами по стилю Go и Vue не задавалась ни одним проходом.** Различение «идиоматично против распространено» на этом прогоне не спрашивал никто. 4. **Альтернативной реализации, с которой можно сдиффить решения, у конвейера нет.** Проход независимой реализации снят по стоимости, а не по замеру. «Не знаю, чего не знаю» про схему узнавания по доверенному заголовку никто не доставал. Пятой строки — про метку `small` — на этом прогоне нет: метка `large`, дома тем `security`, `operations` и `architecture` открывались. ### Что делал сам триаж - Прочитаны: дифф изменения, `CLAUDE.md`, `docs/review.md` целиком, `docs/security.md`, `docs/architecture.md` «Эксплуатация», `docs/database.md` «Настройки с числовым значением», три конвенции, дельта-спека `access`, исходники `pocketbase@v0.39.10` (`apis/middlewares*.go`, `core/event_request.go`). - Запущено во временном файле проверок пакета `internal/controller/http` (удалён после прогона, дерево восстановлено, `go build ./...` и `go test ./internal/controller/http ./internal/adapter/repo/pocketbase` зелёные): - заведение учётных записей запросами, ответившими `429` (`2 → 52`); - общий бюджет ограничителя между двумя людьми; - `500` и `ERROR` на длинном `Remote-Name`; - пустой журнал на заведении учётной записи; - расход бюджета анонимными отказами до и после перестановки приоритетов (`200` → `429`). - Перестановка приоритетов на −999/−998 применена **временно** и откачена; на ней прогнан весь `go test ./...`. Код изменения не правился.