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