Независимый прогон кода, тестов, сборок и безопасности двух репозиториев: продукт legal-ops и сервис-ядро platform-core.
Продуктовые цели вынесены за скобки: смотрели только инженерное состояние.
Срез на 21.08.2026. Позиция автора: новый разработчик, разбор технического состояния проекта.
Метод: живой прогон тестов, сборок, линта и проверки зависимостей на своей машине плюс чтение кода и документации.
Каждая цифра получена самостоятельно, а не взята из документации.
Проект в отличной инженерной форме: за два месяца силами одного разработчика собрана система с настоящими тестами, настоящим релизным пайплайном и продуманной безопасностью. На техническом уровне качественно реализованы все ключевые продуктовые механики.
Точек роста три, и все три структурные, а не косметические. Их видно заранее, они лечатся известными шагами,
и закрывать их дешевле сейчас, чем в момент, когда на прод придёт нагрузка или в команду придёт второй разработчик.
Ничего из найденного не требует переписывать систему.
Три точки роста
01
Система не видит себя
Логи с прода не доезжают с 28 апреля. Инцидент разбирать нечем.
02
Живёт только в одном экземпляре
Одна реплика зашита в четырёх местах сразу. Рост нагрузки упрётся во все четыре.
03
Библиотеки не обновлялись 12 дней
15 серьёзных уязвимостей, среди них обход входа во фреймворке фронта.
Тестов прогнано
7 549
зелёных из 7 700 · 1 упал, и то по документированной причине
Коммитов в продукте
1 531
первый 17.06.2026 · 1 469 от одного человека
Страниц решений
220
у каждого решения записаны альтернативы и причина отказа
Заглушек в коде
4
на 133 тысячи строк · это очень мало
Уязвимостей ждут
15
высокого уровня · висят с 09.08 без движения
Оценка по плоскостям
зелёный - сделано и проверено · жёлтый - работает, но есть долг · красный - чинить в первую очередь
●
Тесты
7 700 тестов, тестового кода почти столько же, сколько продуктового. Проверяют не для галочки: 121 файл специально сторожит изоляцию данных между компаниями.
Сильно
●
Релизы и пайплайн
Прод катится только по версии и только вручную нажатой кнопкой. Перед выкаткой автоматический дамп базы, после выкатки внешняя приёмка.
Сильно
●
Безопасность периметра
Браузер никогда не говорит с бэком напрямую, все внутренние запросы подписаны. Агент ходит в интернет только по разрешённому списку доменов. Секретов и персональных данных в истории git нет.
Сильно
●
Резервные копии
Ночной бэкап на две независимые площадки, восстановление реально проверено скриптом, а не «должно работать».
Сильно
●
Качество кода
Заглушек почти нет, обходов типизации нет. Но 64 файла длиннее 500 строк, самый крупный компонент 2 076 строк, а автоматическая проверка стиля стоит только у фронта.
Есть долг
◐
Документация
425 файлов, wiki свежая. Но два самых старых файла распухли (журнал изменений 773 КБ одним куском), а roadmap не обновлялся с 19.06 и до сих пор утверждает «бэкенда нет».
Есть долг
◐
Разделение данных компаний
Каждый запрос фильтрует по компании, и 121 файл тестов это проверяет. Но структурного барьера в самой базе нет: один забытый фильтр в новом коде откроет данные чужой компании.
Есть долг
●
Наблюдаемость
Ни логирования, ни метрик, ни трейсинга. 312 отладочных вывода вместо структурного лога. Логи с прода не доезжают с 28 апреля, при этом хост выглядит наблюдаемым, потому что метрики идут.
Чинить
●
Обновление библиотек
Цифры уязвимостей на 21.08 совпадают с отчётом двенадцатидневной давности: ничего не обновляли. Среди них обход защиты входа в самом фреймворке фронта.
Чинить
●
Масштабирование
Одна реплика зашита в четырёх местах: миграции при старте, семь фоновых воркеров внутри веб-процесса, сессии в памяти, генерация PDF в том же контейнере.
Чинить
Три направления развития: что это значит на практике
Зона роста 01
Система не видит себя
Если завтра на проде что-то сломается, восстановить картину будет нечем. Логи не доезжают в хранилище с 28 апреля, 154 тысячи ошибок доставки, слой логов пуст.
Риск
Инцидент разбирается наугад. Время простоя растёт в разы, причина может остаться неизвестной.
Деньги
Агент тратит деньги на каждом проходе, но стоимость нигде не сохраняется. Узнать расход за неделю сейчас невозможно.
Лечение
Нормальное логирование со сквозным номером запроса, починенная доставка логов, таблица прогонов агента с моделью, длительностью и стоимостью.
Окупается сразу. Без этого остальные решения принимаются вслепую.
Зона роста 02
Библиотеки не обновлялись
Собственный аудит безопасности 09.08 нашёл 15 серьёзных уязвимостей в зависимостях. Через 12 дней цифры те же: 7 во фронте, 5 в бэке, 3 в ядре.
Совпадение
Одна из уязвимостей это обход промежуточного слоя фреймворка, а вся проверка входа живёт именно в нём.
Смягчает
Второй рубеж есть: каждая страница и каждый запрос проверяют сессию сами. Обход первого слоя данных сам по себе не отдаёт, но полагаться на это не стоит.
Лечение
Обновить фреймворк, потом обработку картинок и сетевой слой, потом остальное. После обновления полный прогон пайплайна и внешняя приёмка.
Ближайшие дни. Работа механическая, риск в том, что обновление трогает вход и сборку.
Зона роста 03
Живёт только в одном экземпляре
Систему нельзя запустить во втором экземпляре: она этого не переживёт. Ограничение зашито сразу в четырёх местах, и это записано прямо в коде честным комментарием.
Риск
Любой рост нагрузки или требование отказоустойчивости упирается в четыре точки одновременно. Второй экземпляр продублирует платные проходы к моделям.
Сейчас
Генерация PDF поднимает отдельный браузер на каждый вызов без ограничения. Два-три одновременных экспорта убивают контейнер, вместе с ним падают чат, голос и все фоновые задачи.
Лечение
Вынести фоновые задачи в отдельный контейнер, сессии перевести в общее хранилище, миграции сделать отдельным шагом выкатки.
Один-два месяца. Не блокирует сегодня, блокирует любой рост.
Что проверяли живьём
каждая строка это выполненная команда, а не утверждение из документации
Проверка
Результат
Вердикт
Типы бэкенда
чисто, ни одной ошибки
Зелёно
Типы ядра
чисто, ни одной ошибки
Зелёно
Типы фронта
90 ошибок, все до одной в тестовых файлах, продуктовый код чист
Долг
Сборка фронта
собирается, 28 страниц
Зелёно
Проверка стиля
0 ошибок, 89 предупреждений, планка стоит ровно на текущем числе
Долг зафиксирован
Тесты бэкенда
3 844 прошло, 165 пропущено, ни одного падения
Зелёно
Тесты фронта
3 235 прошло, 5 пропущено
Зелёно
Тесты ядра
470 прошло, 1 упал: ему нужен запущенный сервис рядом, это описано в пайплайне
Ожидаемо
Уязвимости зависимостей
фронт 7, бэкенд 5, ядро 3 высокого уровня
Чинить
Два наблюдения из самого прогона, оба про опыт нового человека в проекте. Тесты бэкенда не запускаются «из коробки»:
двенадцать файлов падают, пока руками не задать адреса базы и кэша. У ядра эта проблема решена одним файлом настройки, у бэкенда такого файла нет.
Плюс локальные тесты ходят в ту же базу, что и рабочий запуск: первый прогон дал 337 падений просто потому, что база отставала на пять миграций.
В пайплайне это закрыто, локально ловушка живая. Правка на десять строк.
Объём в цифрах
продуктовый код против тестового
Продуктового кода 133 600 строк, тестового 115 100. Соотношение почти один к одному, а в бэкенде тестов даже больше, чем кода.
Для проекта, написанного одним человеком за два месяца, это редкость: обычно тесты и есть то, что отрезают первым.
продуктовый кодтестыширина полосы = общий объём пакета
Таблиц в базе
38
плюс 54 миграции в продукте и 8 в ядре
Файлов документации
425
11 МБ · 22 страницы wiki по подсистемам
Отчётов аудитов
12
включая собственный тест на проникновение 09.08
Обходов типизации
0
ни одного отключения проверки типов на весь файл
Уязвимости в зависимостях
высокого уровня, по состоянию на 21.08 · те же цифры, что 09.08
Фронтенд
7
Бэкенд
5
Ядро
3
Неприятное совпадение во фронте: уязвимость даёт обход промежуточного слоя фреймворка, а вся проверка входа живёт именно там.
Рядом две уязвимости на пути, по которому сервер ходит в почту и в файловое хранилище.
Работа механическая: обновить и прогнать полный пайплайн с внешней приёмкой.
Что сделано хорошо
то, что обычно не доводят до конца, а тут довели
●
Браузер не говорит с бэкендом напрямую
Личность и роль подписываются на сервере и проверяются до того, как хоть один запрос узнает, кто пришёл. Подмена в полёте ломает запрос, а не покупает права.
●
Агент огорожен по-настоящему
Два независимых слоя контроля, опасные команды заблокированы, файловый доступ запёрт в рабочую папку, выход в интернет только по списку разрешённых доменов с защитой от похожих имён.
●
Чужой текст помечен как чужой
Тело письма и текст документа заворачиваются в явную рамку «это недоверенные данные», закрывающая метка внутри текста обезвреживается. Это прямая защита от подмены инструкций через письмо.
●
Пайплайн это гейт, а не декорация
Поднимает чистую базу на каждый прогон, катает миграции, гоняет тесты. Отдельная проверка ловит расхождение миграций между параллельными ветками. Сборка падает, если забыли переменную, из-за которой вход сломался бы молча.
●
Тесты проверяют то, что важно
Неделимость записи в журнал, окно повторного использования подписи, гонки при подключении источников, изоляция данных между компаниями. Это не тесты ради процента покрытия.
●
Одинаковые ошибки лечатся структурно
Восемь мест копировали руками один рискованный приём и один раз ошиблись живьём. После этого приём собрали в одну функцию с двумя закрытыми ловушками. Так и надо.
●
Ключи разведены по назначениям
Токены доступа к почте шифруются отдельным ключом от ключа входа. Одна утечка не вскрывает второе, смена пароля входа не рвёт подключения. Своей криптографии не писали.
●
Секретов и персональных данных в git нет
Проверена вся история обоих репозиториев, чисто. В индексе только шаблоны настроек.
●
Документация как система
220 атомарных страниц решений со статусами и взаимными ссылками. У каждой подсистемы есть раздел «грабли» с конкретными классами багов и способом их поймать. Обычно это живёт в голове у автора и уходит вместе с ним.
●
Бэкапы доведены до конца
Ночная копия уезжает на две независимые площадки, восстановление реально проверено на обоих дампах, плюс автоматический дамп перед каждой выкаткой.
Риски по убыванию
порядок = что чинить первым
Риск
Почему это дорого
Что делать
1
Инцидент на проде разбирать нечем
Логов нет с апреля, структурного лога нет, стоимость агента нигде не хранится.
Починить доставку логов, ввести нормальное логирование со сквозным номером запроса, писать прогоны агента в таблицу.
2
Уязвимости висят 12 дней
Одна из них даёт обход того самого слоя, где живёт проверка входа.
Обновить фреймворк фронта и связанные библиотеки, повторить внешнюю приёмку.
3
Дев-инфраструктура ядра открыта в локальной сети без пароля
На машине разработчика лежит копия реальных юридических данных. Стенд и прод при этом чисты, проверено.
Прибить порты к локальному адресу, дать кэшу пароль. Правка на три строки, в продукте это уже сделали, до ядра не доехало.
4
Утечка данных через картинку в ответе агента
Агент читает чужие письма и документы каждый час, и подменённая инструкция может увести данные через адрес картинки.
Запретить внешние картинки в отображении сообщений и добавить второй рубеж на уровне политики браузера.
5
Потолок в одну реплику
Любой рост нагрузки или требование отказоустойчивости упирается сразу в четыре места.
Вынести фоновые задачи в отдельный процесс, сессии в общее хранилище, замок на миграции.
6
Разделение компаний держится на дисциплине
Один забытый фильтр в новом коде открывает данные чужой компании.
Включить проверку на уровне самой базы как второй рубеж, начиная с самых чувствительных таблиц.
7
Контракты между слоями синхронизируются руками
Второй разработчик или второй продукт разъедет их молча, без ошибки сборки.
Общий пакет типов и машинное описание API.
8
Старые прокси-маршруты не проверяют путь
Авторизованный пользователь может дотянуться до того, что узкий прокси не обещал.
Перенести новый образцовый шаблон на четыре старых маршрута.
9
Генерация PDF без очереди
Два-три одновременных экспорта убивают контейнер, вместе с ним падает всё остальное.
Ограничить параллелизм, переиспользовать браузер вместо запуска на каждый вызов.
10
Покрытие тестами не измеряется
7 700 тестов это много, но какая часть кода за ними стоит, неизвестно. Решение «протестировано ли» принимается на глаз.
Включить замер в пайплайн и поставить порог, который нельзя опускать.
План: куда двигать дальше
три горизонта, по порядку
Сейчас
Одна-две недели
Наблюдаемость. Единственный пункт, который окупается сразу. Логирование со сквозным номером запроса, починенная доставка логов, таблица прогонов агента с моделью, длительностью и стоимостью.
Обновить зависимости. Фреймворк фронта в первую очередь, дальше остальное во всех трёх пакетах. После обновления полный пайплайн и внешняя приёмка.
Дожать хвосты своего аудита. Картинки в сообщениях, политика браузера, проверка пути в четырёх маршрутах, закрытые порты в ядре. Все четыре правки маленькие и у всех известен сценарий атаки.
Тесты бэкенда должны запускаться сразу. Файл настроек по образцу ядра плюс отдельная тестовая база. Десять строк, которые снимут ложное красное поле у каждого нового человека.
Ближний горизонт
Один-два месяца
Разнести процессы. Фоновые задачи в отдельный контейнер. Развязывает три вещи сразу: агент перестаёт конкурировать с чатом за процессор, веб становится масштабируемым, генерация PDF уезжает туда, где ей можно дать свою память и очередь.
Второй рубеж изоляции данных. Не вместо фильтра по компании, а под ним. Начать с самых чувствительных таблиц. Тесты изоляции уже есть, они же покажут, что ничего не сломалось.
Общий пакет контрактов. Типы данных между фронтом и бэком сейчас синхронизируются руками. Пока автор один, это работает.
Проверка стиля и покрытие в остальных пакетах. Сейчас 52 тысячи строк бэкенда живут без статического анализа вообще.
Долг фронта. 46 точечных отключений одного правила это сигнал пересмотреть модель состояния, а не добавлять новые отключения. Заодно станет реалистично разбить самый крупный компонент.
Средний горизонт
До второго продукта
Достроить ядро до роли ядра. По убыванию срочности: журнал аудита, индекс общей памяти, модель тонких прав, шина событий между продуктами, страница согласия.
Машинное описание API бэкенда. Закрывает разнородность проверки входящих данных и даёт генерацию клиента для фронта.
Решить, где живёт документация. Журнал изменений и описание архитектуры пора резать по годам и подсистемам, а устаревший roadmap либо обновить, либо пометить архивом.
Быстрые победы
каждый пункт делается за час-два и заметно улучшает жизнь
Тесты бэкенда запускаются сразу, без ручной настройки адресов, и на отдельной базе.
10 строк
Закрыть порты дев-инфраструктуры ядра и дать кэшу пароль, симметрично тому, как это уже сделано в продукте.
3 строки
Проверка пути в четырёх старых прокси-маршрутах, одинаково с новым образцовым шаблоном.
час
Запретить внешние картинки в сообщениях плюс второй рубеж на уровне политики браузера.
час
Пометить устаревший roadmap архивом и поставить в шапку ссылку на актуальное состояние, чтобы новый человек не читал «бэкенда нет».
5 минут
Свести описание архитектуры ядра с фактом: в одном месте документ и код говорят разное.
5 минут
Завести проверку стиля в бэкенде хотя бы с базовым набором правил, дальше опускать планку по мере разбора.
час
Ядро платформы отдельно
граница проведена правильно, содержания пока на один сервис
Само ядро сделано аккуратно: страницы входа с защитой от подделки запросов, ограничение попыток входа (проверено живьём),
выдача коротких пропусков, безопасное подключение почтового аккаунта. 470 тестов из 471 зелёные.
Но как «ядро большой платформы» оно сейчас закрывает одну из нескольких нужных ролей: вход. Всё остальное живёт в продукте.
Что в ядре естьзакрывает роль входа
Вход и единый аккаунт на все продукты семейства
Люди, компании, рабочие пространства и грубая роль в компании
Реестр подключённых источников с шифрованием токенов отдельным ключом
Выдача короткоживущих пропусков с описанным контрактом для интеграторов
Чего нет и что попросит второй продукт6 пунктов
Журнал аудита. Кто выдал пропуск, кто обменял токен, кто сменил роль: нигде не записано. Для системы с юридическими данными это первый вопрос на любой проверке безопасности.
Индекс общей памяти. Решение о распределённой памяти семейства принято, индекса в ядре нет, вся память живёт в продукте.
Тонкие права. В ядре только грубая роль. Права на разделы живут в продукте, второй продукт либо изобретёт свою модель, либо утащит эту копией.
Страница согласия. Пока все потребители доверенные, это не мешает. Сторонний клиент подключить нельзя.
Общий пакет типов пропуска. Сейчас продукт и ядро описывают одно и то же в двух местах.
События между продуктами. Шина не спроектирована вовсе.
Продукт legal-ops
1 531 коммит
Ядро platform-core
106 коммитов
Ядро это 5% усилий, а не половина системы. Заявка «первый сервис большой платформы» подкреплена архитектурной границей:
она реально соблюдается, продуктовый код ходит в ядро только через тонкий интерфейс. Граница правильная и её надо беречь.
А вот ожидать, что второй продукт «просто подключится», не стоит: половина того, что он попросит, ещё не написана.
Фоновые задачи: где именно жгутся деньги
восемь очередей, семь воркеров, все внутри веб-процесса
Три из семи фоновых задач платные: они будят модель. Сами очереди сделаны неглупо, платные проходы сознательно
не перезапускаются при сбое, чтобы не переплачивать. Проблема не в очередях, а в том, что всё это живёт в одном
процессе с чатом, голосом и генерацией PDF на настройках по умолчанию.
Задача
Расписание
Платная
Что делает
Сбор почты
каждые 15 минут
Да
Читает почту по каждой компании и каждому ящику
Разбор сигналов
каждые 3 часа
Да
Будит модель по собранным событиям
Сбор памяти
каждые 2 часа
Да
Будит модель на каждый неразобранный документ и письмо
Подъём отложенных сигналов
раз в сутки
Нет
Одно обновление в базе
Дедлайны комплаенса
раз в сутки
Нет
Сигналы по срокам всем компаниям с профилем
Слежение за контрактами
раз в сутки
Нет
Перевод в активные по дате, эскалация продлений и отказов
Уборка осиротевших файлов
раз в сутки
Нет
Чистит объекты в хранилище без записи в базе
Дорого
Двойная оплата при сбое
Если процесс подвиснет дольше 15 секунд, очередь считает задачу потерянной и запускает её заново. Защита «не перезапускать» на этот путь не действует: платный проход может отработать и быть оплачен дважды.
Лечится курсором в базе: «этот ящик собран до такого-то письма». Повтор становится дешёвым, а не повторно оплаченным.
Роняет
Генерация PDF убивает контейнер
Каждый экспорт поднимает отдельный браузер на 150-300 МБ без ограничения количества. Два-три одновременных упираются в лимит памяти, и контейнер получает принудительное убийство. Вместе с ним падают чат, голос и все семь фоновых задач.
Лечится внутри одного модуля: переиспользуемый браузер, очередь, жёсткий таймаут. Разнесения процессов не требует.
Слепо
За фоновыми задачами никто не смотрит
Итог прохода уходит одной строкой в отладочный вывод. Ни таблицы прогонов, ни метрик, ни сигнала «сбор не прошёл». Провалившийся платный проход виден только в логах, которых на проде нет.
Дешевле любого разнесения процессов и окупается первым же инцидентом.
Итог
Инженерно проект в хорошей форме. Тесты настоящие, пайплайн настоящий, безопасность продумана осознанно
и по итогам собственного теста на проникновение, документация местами лучше, чем в командах на порядок больше.
Основной долг не в качестве написанного кода, а в двух вещах вокруг него: системе нечем наблюдать за собой
и она умеет жить только в одном экземпляре. Обе решаются известными шагами, и обе стоит закрыть до того,
как на прод придёт нагрузка или в команду придёт второй разработчик.
Ядро отдельно: граница проведена правильно и соблюдается, но по содержанию это пока сервис входа, а не платформа.
Разрыв между этими двумя ролями стоит закрывать заранее, а не в момент, когда второй продукт постучится.