← Назад

Разбор своего проекта · слепок 2026-09-14

ladder: доска самоподготовки, которую наполняет агент

Тема раскладывается на колонки и ступени сложности, карточки разбираются чек-листом с клавиатуры, а вопросы заводит не человек в файлах, а агент через MCP прямо в базу работающего сервиса. Ниже — что внутри, какие решения оказались важными и что из них едва не стоило контента.

репозиторий: github.com/DiorditsPV/ladder сервис: ladder.paveldiordits.site числа — по файлам репозитория и живой базе на 2026-09-14

Суть в пяти пунктах

  1. 1
    Что это. Тема разложена на доску: вертикальные колонки — области темы, горизонтальные ряды — ступени сложности. На пересечении лежат карточки «вопрос + развёрнутый ответ», внутри под-колонки они идут лесенкой: сверху азы, ниже — вопрос выбора и компромиссов. Разбор ведётся чек-листом «знаю / повторить / не знаю».
  2. 2
    Откуда взялось. Сервис начинался как инструмент интервьюера: сессии с кандидатом, оценки 1–5, HTML-отчёт. В сентябре режим интервью удалён целиком — вместе с таблицами, оценками и экспортом. Осталась одна роль: человек разбирает тему сам.
  3. 3
    Главное решение. Источник правды — не репозиторий, а база работающего сервиса. Файлы только засевают недостающее и ничего не перезаписывают, поэтому выкладка кода не может затереть накопленные вопросы.
  4. 4
    Как наполняется. Контент заводится через MCP-сервер из 25 инструментов поверх собственного API: направление, колонки, ступени, топики и вопросы пачкой — прямо из сессии агента. Правка через API помечается source=user, и синхронизация из файлов её не трогает.
  5. 5
    Чем держится. Перед каждой выкладкой снимается снимок базы, после старта сверяется число видимых карточек по направлениям: стало меньше хоть в одном — выкладка падает. Плюс ночной таймер снимков и гейт CI из 174 тестов бэкенда, 6 тестов MCP и браузерного smoke.

01Что видит человек

Единица работы — направление: тема со своей структурой. У направления есть колонки, у широкой колонки — под-колонки, и собственные ступени (от 2 до 8). Ступени — шкала сложности именно этой темы, а не универсальные junior/middle/senior: у Kafka это «Понятия → Конфигурация → Эксплуатация → Внутренности → Проектирование».

ступени ↓ · колонки → азы практика эксперт Фреймворки Базы данных AirflowPySpark SQLДвижки что такое DAGчто такое RDD как идёт запросархитектура идемпотентностьshuffle оконные функции выбор оркестратораплан и индексы выбор движка
Доска: колонки с под-колонками по горизонтали, ступени сложности по вертикали. В каждой под-колонке карточки поднимаются от азов к вопросу выбора — пустая ячейка сразу видна как пробел в разборе.

Карточка — вопрос и эталонный ответ на 400–1500 знаков: сначала суть, потом механика, потом пример. У карточки есть короткая подтема внутри колонки и 1–3 тега из словаря сквозных концептов (architecture, partitioning, consistency, streaming…): названия технологий тегами не бывают, технология и так видна по колонке.

Состояние разбора — три статуса. Клавиши 1/2/3 ставят статус и переводят к следующей карточке, n ведёт к ближайшей неразобранной; счётчики колонок и рядов показывают, сколько закрыто.

8направлений в живой базе
435видимых карточек
61 / 44демо: дата-инженер и аналитик
RU / ENу демо есть переводы-пары

Без входа открыты только демонстрационные направления и их переводы; остальное — Kafka, Spark на Kubernetes и S3, отраслевой набор и агентная разработка — за логином.

02Как устроено внутри

Бэкенд — FastAPI и SQLite, 2 550 строк. Фронт — React, Vite и React Flow, 5 580 строк. Ни очередей, ни воркеров, ни внешних сервисов: один процесс uvicorn за nginx и один файл базы. Поток данных раньше читался как «файлы → импорт → база → API → доска»; сегодня он короче — база → API → доска, а файлы подключаются только на старте и только как сид.

ЧастьЗа что отвечает
pools.pyтаксономия направления: колонки, под-колонки, цвета, веса, ступени
importer.pyкарточки из Markdown и JSON с проверкой, что колонка и ступень существуют
sync.pyдва режима синхронизации: засев и явное обновление пресета
db.pySQLite: направления, банк вопросов, аккаунты, чек-лист
auth.py, tenancy.pyпароли, серверные сессии, роли и изоляция тенантов
layout.tsраскладка swimlane: позиции колонок, под-колонок и ступеней

Открытая карточка работает в двух режимах: плавающим окном по центру доски (по умолчанию) или панелью справа с изменяемой шириной. Окно тащится за шапку и запоминает место, тема одна на весь сайт и она же выбирает оформление доски.

03Поворот: из интервью в самоподготовку

Первые три месяца продукт был про интервью: кандидат, сессия, оценки, отчёт. Пивот убрал это целиком — не спрятал за флагом, а удалил: таблицы сессий, оценки 1–5, страницу отчёта и экспорт в HTML. Вместо «интервьюер оценивает кандидата» осталось «человек разбирает тему и отмечает, что уже знает».

Такое удаление дешевле, чем кажется, если продукт держится на данных, а не на экранах: ушли ровно те таблицы и ручки, которые обслуживали сессии, а доска, банк вопросов и импорт остались нетронутыми. Дороже всего оказалось переписать формулировки: вопрос «для интервью» и вопрос «для разбора» звучат по-разному — во втором не бывает мини-кейсов с выдуманными деталями, зато нужен прямой вопрос о сути, понятный без контекста.

04Контент живёт в базе, а не в гите

Это решение, ради которого переписана синхронизация, и оно же — главный риск, который пришлось закрыть. Как было опасно: синхронизация шла на каждом старте сервиса, то есть на каждой выкладке, и не только создавала направления, но и перезаписывала их конфиг из файлов, а карточки, чьи файлы пропали, прятала. Правка колонок через интерфейс откатывалась ближайшим деплоем, удаление вопроса из репозитория убирало его с доски, и при этом сам репозиторий был единственной копией контента.

Засев — по умолчанию

Старт сервера и обычный вызов синхронизации: создать направление, если его нет, добавить карточки с незанятыми id. Ничего не перезаписывает и не прячет — деплой физически не может затереть накопленное.

Обновление пресета — явно

Отдельный флаг, лучше с указанием одного направления и сначала «вхолостую»: прогон идёт на копии базы и возвращает отчёт — что изменится в конфиге, сколько карточек будет переписано, сколько скрыто, где конфликт с правками пользователя.

В репозитории остался только демонстрационный слой — 208 файлов двух направлений и их переводов. Остальное живёт в базе; версионируемая копия — отдельный репозиторий с JSON-выгрузкой по направлению, которую забирает тот же скрипт, что снимает снимок базы.

Что нашло ревью после внедрения

Два сценария, в которых можно было потерять контент. Оба касались не «большой» логики, а границ — и именно поэтому их не видно на первый взгляд.

Чужая карточка. Защита правленых карточек считалась в пределах одного направления, хотя id уникален во всём тенанте. Карточка, которую поправили и перенесли в другое направление, при обновлении пресета перезаписывалась файлом и утаскивалась обратно — а в отчёте это выглядело как обычная строка «изменится N».

Фантомная колонка. Засев не трогает конфиг направления, поэтому карточка из файла могла попасть в колонку или ступень, которых в живой структуре уже нет (их убрали в интерфейсе вместе с вопросами). На доске появлялся столбец, которого нет в конфиге, счётчик карточек при этом рос, и проверка выкладки молчала.

Оба закрыты: защита стала тенантной, а засев пропускает карточки, не подходящие живой структуре, и показывает их в отчёте отдельным счётчиком.

05MCP как основной способ писать контент

Поверх API работает MCP-сервер — 25 инструментов, сгруппированных по тому, что правится:

Смысл не в том, что «агент умеет дёргать API», а в том, что правка контента перестала требовать файлов, коммита и выкладки. Агент собирает направление целиком: строит каркас, проверяет покрытие матрицы, заносит вопросы — и видит результат на живой доске сразу. Удаление колонки, ступени или направления с вопросами требует явного подтверждения, а вопросы можно предварительно перенести.

Рядом живёт скилл для случая «разложи тему целиком»: JSON с каркасом и карточками → проверка покрытия → заливка в живую базу, где недостающие колонки и ступени дописываются, а занятые id пропускаются или обновляются по явному флагу.

06Выкладка и страховка

Выкладка — только ручной запуск: merge в основную ветку ничего не публикует. Раннер собирает фронт и отправляет один архив по SSH-ключу, прибитому к единственной команде: ключ не даёт ни шелла, ни другого доступа. Архив проверяется до любых изменений на диске, а юнит systemd скрипт выкладки не трогает — его ставит только провижининг, чтобы архив не мог подменить пользователя сервиса.

После выкладки проверяется и публичный адрес: health отвечает, список направлений и демо-граф открываются без cookie, закрытое направление без cookie отдаёт 401.

07Что проверяется автоматически

174теста бэкенда
6тестов MCP-сервера
34шага браузерного smoke
209 / 54коммитов и слитых PR

Smoke идёт по живому рантайму на свежей базе: начинает без входа в демо, проверяет темы и языковую пару, заходит по скрытой ссылке, создаёт направление мастером, правит структуру перетаскиванием, разбирает карточки с клавиатуры, заводит и удаляет аккаунт.

Один из шагов появился после случайного падения: фоновые узлы доски не имели размера, React Flow держал их скрытыми до замера, и при потере замера колонки не появлялись вовсе. Теперь узлы несут размер из раскладки, а шаг воспроизводит поломку заглушкой ResizeObserver — проверка, которая до исправления падала, а после проходит.

08Чего здесь нет

Источники