# Ревью изменения `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 ./...`. Код изменения не правился.
