# Ревью кода — 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` к вредоносному входу; поведение браузера с куками. - Альтернативной реализации, с которой можно сдиффить решения, у конвейера нет — проход независимой реализации упразднён решением о стоимости. «Не знаю, чего не знаю» здесь не достаёт никто, и на изменении, замораживающем публичную форму ответов, это дорого.