# Ревью кода — app-json-contract

Метка `large`, режим «по графу». Состав: `autotests`, `specs`, `code`,
`architecture`, `adversary`, `ops`, `triage`. `basics` не запускался — своих тем
проекта нет, все темы ядра разобраны именными проходами.

## План против исхода

| Тема | Дом | Глубина | Кто закрыл | Исход |
| --- | --- | --- | --- | --- |
| `requirements` | `openspec/specs/` + дельты | разбор | `specs` | 7 находок |
| `autotests` | `CLAUDE.md`, «Гейт» | доказательство | `autotests` | гейт зелёный, 4 находки об отсутствующей верификации |
| `conventions` | `docs/conventions/` | разбор | `code` | 11 находок, потолок конвенционной половины сработал (4 из 4, за срезом двое) |
| `architecture` | `docs/architecture.md` + `passport.md` | доказательство | `architecture` | 3 находки, потолок сработал |
| `security` | `docs/security.md` | доказательство | `adversary` | 3 построенных пути, 3 свойства |
| `operations` | `docs/architecture.md` «Эксплуатация» + `database.md` | доказательство | `ops` | 2 постмортема с замерами, 1 закрытая гипотеза |

**Проход `ops` едва не остался незапущенным** — он ждал освобождения машины
после враждебного прохода, и оркестратор его не позвал. Поймал это триаж
сверкой плана с исходом; проход запущен и вернул две находки уровня `major`,
обе с замерами. Это дефект прогона, а не темы, и он записан здесь.

## Что найдено и починено

Каждая правка закрыта оракулом; тесты названы поимённо.

| Находка | Проходы | Правка и оракул |
| --- | --- | --- |
| `413`, `429` и неизвестный путь под `/app` уходили телом библиотеки — мимо единой формы, ради которой заведена задача | architecture, code, specs, adversary | слой `OneErrorForm` плюс свой перехват неизвестного пути; `TestTooLargeOnTheRealPath`, `TestRateLimitRefusalGoesThroughOneErrorForm`, `TestUnknownAddressUnderAppRootUsesOneErrorForm` |
| объявленная частота опроса равнялась **всему** бюджету ограничителя | architecture, code, specs, adversary | частота выведена из доли бюджета; `TestPollIntervalLeavesBudgetHeadroom` |
| перечень доступных видов строился по ссылке, а не по содержимому: карточка обещала текст, которого адрес не отдал бы никогда | specs | перечень строится по содержимому; `TestEmptyTextIsNotAnAvailableView` |
| поле перечня пропадало из тела вместо пустого перечня | specs, code | поле стало указателем на срез и присутствует всегда; `TestRecordCard_NoTextYetGivesEmptyViews`, `TestCardAndPageItemShareOneShape` |
| ключ страницы с негодным временем принимался молча и отдавал пустой архив при непустом счётчике | specs, code | время разбирается и приводится к виду хранилища; `TestList_MalformedCursorIsRejected` (пять случаев) |
| выборка с непозитивным пределом роняла процесс обращением по индексу −1 | code | предел приводится к умолчанию в самом адаптере |
| неизвестное состояние отбора отдавало весь архив вместо отказа | code | ветвь отказа вместо молчаливого «без сужения» |
| разрешение тем шло без сужения владельцем | adversary | сужение добавлено; `TestForeignTopicDoesNotResolve` |
| длинное расширение из имени отправителя давало `500` и строку `ERROR` в журнале | adversary | потолок длины расширения; `TestIntake_AbsurdExtensionDoesNotBecomeInternalError` |
| `401` собирался руками мимо единой точки — на первой строке её собственной таблицы | code | заведён свой признак, ответ идёт через отображение |
| разрешение тем и перечень видов не исполнялись под тестом ни разу | autotests, triage | `TestTopicsResolveToNames`, `TestAvailableViewsCoverEveryKind` |
| ветвь замены правила ограничителя не исполнялась: правила копились бы с каждой выкладкой | autotests | повторный вызов в `TestRateLimitRuleCoversAppRoot` |
| **страница владельца сканировала весь архив сервиса** — замер: рост архива в 40 раз растил время страницы в 20–30 раз при неизменных сорока его записях | ops | два индекса в том же шаге схемы; `EXPLAIN QUERY PLAN` показывает покрывающий поиск вместо полного сканирования |
| **включение ограничителя схлопнуло все внутренние обмены OIDC-кода в один счётчик** — третий вход в пределах трёх секунд отвергался с текстом «Войти не удалось» | ops | внутреннему запросу задан адрес; регрессия внесена самим изменением и им же закрыта |

## Решение человека по ходу ревью

**Норма «непрочитанная длительность отличима от нулевой» снята.** Проверено
прогоном: числовая колонка хранилища пустого значения не держит, пустое кладётся
нулём, и ветвь кода была недостижима. Отличимость стоила бы четвёртой колонки
либо текстового типа у чисел; платить не за что — обе величины ставит приём и
ставит всегда, а запись с непрочитанными метаданными отвергается отказом и не
заводится вовсе. Правлены спека `storage`, `docs/database.md`, комментарии и
критерий приёмки; мёртвая ветвь убрана.

## Урожай — реальное, но не для этого мерджа

- **Ключ ограничителя частоты — адрес, а не учётная запись.** Свойство
  библиотеки: двое за одним адресом делят бюджет. Арифметика против высокой
  цены — объявленная частота даёт восьмерых одновременно опрашивающих на адрес.
  Оракула на ущерб нет и быть не может: реального профиля нагрузки проект не
  знает.
- **`TrustedProxy.Headers` не настроен нигде**, а сервис публикуется через
  обратный прокси. Значит бюджет ограничителя считается по адресу прокси, то
  есть общий на всех посетителей. Проверено чтением; на живом прокси не
  воспроизводилось — его конфигурация вне репозитория.
- **Отказ ограничителя не виден в журнале контейнера** — канале, который
  архитектура называет основным: встроенный слой стоит раньше нашего журнала
  запросов. Виден только во внутренней таблице хранилища.
- **Уборка имени файла снимает только категорию Cc.** Разворот направления и
  невидимые пробелы доезжают до колонки, до ответа и до имени файла на диске.
  Видит это владелец записи и владелец сервиса.
- **Точность длительности.** Колонка названа в миллисекундах, а источник даёт
  целые секунды: значение всегда кратно тысяче.
- **Род узла «читающий обработчик и список» в `docs/review.md` не заведён** —
  свойства вроде устойчивости окна и потолка страницы там не спрашивает никто.

## Границы покрытия

- Метка `large`, режим «по графу», проходов семь. Потолки сработали у `code`
  (конвенционная половина, 4 из 4) и у `architecture` (3 из 3) — оба объявили.
- **`ops` едва не остался незапущенным**, и поймал это только триаж. Строка
  оставлена намеренно: пропуск был неотличим от прохода без находок.
- **Триаж получил дайджест оркестратора, а не сырые выводы проходов** — часть
  их находок дошла до него уже починенной. Дедупликация выполнена над
  дайджестом; находка, которую проход показал, а оркестратор не перечислил, для
  триажа была невидима.
- Решения проекта (`docs/adr/`) и записанные наблюдения (`docs/research/`)
  прогоном не открывались: процессные документы. Расхождение изменения с
  записанным решением ловится сверкой документации, а не ревью.
- Все числа отчёта сняты на этом прогоне: время страницы на 5k/50k/200k строк,
  бюджет ограничителя, коды и тела ответов.
- Живой прогон сценария вошедшего локально невозможен по устройству проекта:
  сессию выдаёт только провайдер OIDC, а локальный запуск наружу не ходит.
  Этот путь закрыт машиной в тестах, через настоящий роутер и настоящее
  хранилище.
- Не проверено ничем: поведение настоящих SpeechKit, Object Storage, Authelia и
  обратного прокси; реальный профиль нагрузки и реальный размер архива;
  стойкость `ffmpeg` к вредоносному входу; поведение браузера с куками.
- Альтернативной реализации, с которой можно сдиффить решения, у конвейера нет —
  проход независимой реализации упразднён решением о стоимости. «Не знаю, чего
  не знаю» здесь не достаёт никто, и на изменении, замораживающем публичную
  форму ответов, это дорого.
