Разбор своего проекта · слепок 2026-09-14
Тема раскладывается на колонки и ступени сложности, карточки разбираются чек-листом с клавиатуры, а вопросы заводит не человек в файлах, а агент через MCP прямо в базу работающего сервиса. Ниже — что внутри, какие решения оказались важными и что из них едва не стоило контента.
source=user, и
синхронизация из файлов её не трогает.Единица работы — направление: тема со своей структурой. У направления есть колонки, у широкой колонки — под-колонки, и собственные ступени (от 2 до 8). Ступени — шкала сложности именно этой темы, а не универсальные junior/middle/senior: у Kafka это «Понятия → Конфигурация → Эксплуатация → Внутренности → Проектирование».
Карточка — вопрос и эталонный ответ на 400–1500 знаков: сначала суть, потом механика, потом
пример. У карточки есть короткая подтема внутри колонки и 1–3 тега из словаря сквозных концептов
(architecture, partitioning, consistency, streaming…):
названия технологий тегами не бывают, технология и так видна по колонке.
Состояние разбора — три статуса. Клавиши 1/2/3 ставят статус и переводят к следующей
карточке, n ведёт к ближайшей неразобранной; счётчики колонок и рядов показывают,
сколько закрыто.
Без входа открыты только демонстрационные направления и их переводы; остальное — Kafka, Spark на Kubernetes и S3, отраслевой набор и агентная разработка — за логином.
Бэкенд — FastAPI и SQLite, 2 550 строк. Фронт — React, Vite и React Flow, 5 580 строк. Ни
очередей, ни воркеров, ни внешних сервисов: один процесс uvicorn за nginx и один файл
базы. Поток данных раньше читался как «файлы → импорт → база → API → доска»; сегодня он короче —
база → API → доска, а файлы подключаются только на старте и только как сид.
| Часть | За что отвечает |
|---|---|
pools.py | таксономия направления: колонки, под-колонки, цвета, веса, ступени |
importer.py | карточки из Markdown и JSON с проверкой, что колонка и ступень существуют |
sync.py | два режима синхронизации: засев и явное обновление пресета |
db.py | SQLite: направления, банк вопросов, аккаунты, чек-лист |
auth.py, tenancy.py | пароли, серверные сессии, роли и изоляция тенантов |
layout.ts | раскладка swimlane: позиции колонок, под-колонок и ступеней |
Открытая карточка работает в двух режимах: плавающим окном по центру доски (по умолчанию) или панелью справа с изменяемой шириной. Окно тащится за шапку и запоминает место, тема одна на весь сайт и она же выбирает оформление доски.
Первые три месяца продукт был про интервью: кандидат, сессия, оценки, отчёт. Пивот убрал это целиком — не спрятал за флагом, а удалил: таблицы сессий, оценки 1–5, страницу отчёта и экспорт в HTML. Вместо «интервьюер оценивает кандидата» осталось «человек разбирает тему и отмечает, что уже знает».
Такое удаление дешевле, чем кажется, если продукт держится на данных, а не на экранах: ушли ровно те таблицы и ручки, которые обслуживали сессии, а доска, банк вопросов и импорт остались нетронутыми. Дороже всего оказалось переписать формулировки: вопрос «для интервью» и вопрос «для разбора» звучат по-разному — во втором не бывает мини-кейсов с выдуманными деталями, зато нужен прямой вопрос о сути, понятный без контекста.
Это решение, ради которого переписана синхронизация, и оно же — главный риск, который пришлось закрыть. Как было опасно: синхронизация шла на каждом старте сервиса, то есть на каждой выкладке, и не только создавала направления, но и перезаписывала их конфиг из файлов, а карточки, чьи файлы пропали, прятала. Правка колонок через интерфейс откатывалась ближайшим деплоем, удаление вопроса из репозитория убирало его с доски, и при этом сам репозиторий был единственной копией контента.
Старт сервера и обычный вызов синхронизации: создать направление, если его нет, добавить карточки с незанятыми id. Ничего не перезаписывает и не прячет — деплой физически не может затереть накопленное.
Отдельный флаг, лучше с указанием одного направления и сначала «вхолостую»: прогон идёт на копии базы и возвращает отчёт — что изменится в конфиге, сколько карточек будет переписано, сколько скрыто, где конфликт с правками пользователя.
В репозитории остался только демонстрационный слой — 208 файлов двух направлений и их переводов. Остальное живёт в базе; версионируемая копия — отдельный репозиторий с JSON-выгрузкой по направлению, которую забирает тот же скрипт, что снимает снимок базы.
Два сценария, в которых можно было потерять контент. Оба касались не «большой» логики, а границ — и именно поэтому их не видно на первый взгляд.
Чужая карточка. Защита правленых карточек считалась в пределах одного направления, хотя id уникален во всём тенанте. Карточка, которую поправили и перенесли в другое направление, при обновлении пресета перезаписывалась файлом и утаскивалась обратно — а в отчёте это выглядело как обычная строка «изменится N».
Фантомная колонка. Засев не трогает конфиг направления, поэтому карточка из файла могла попасть в колонку или ступень, которых в живой структуре уже нет (их убрали в интерфейсе вместе с вопросами). На доске появлялся столбец, которого нет в конфиге, счётчик карточек при этом рос, и проверка выкладки молчала.
Оба закрыты: защита стала тенантной, а засев пропускает карточки, не подходящие живой структуре, и показывает их в отчёте отдельным счётчиком.
Поверх API работает MCP-сервер — 25 инструментов, сгруппированных по тому, что правится:
Смысл не в том, что «агент умеет дёргать API», а в том, что правка контента перестала требовать файлов, коммита и выкладки. Агент собирает направление целиком: строит каркас, проверяет покрытие матрицы, заносит вопросы — и видит результат на живой доске сразу. Удаление колонки, ступени или направления с вопросами требует явного подтверждения, а вопросы можно предварительно перенести.
Рядом живёт скилл для случая «разложи тему целиком»: JSON с каркасом и карточками → проверка покрытия → заливка в живую базу, где недостающие колонки и ступени дописываются, а занятые id пропускаются или обновляются по явному флагу.
Выкладка — только ручной запуск: merge в основную ветку ничего не публикует. Раннер собирает фронт и отправляет один архив по SSH-ключу, прибитому к единственной команде: ключ не даёт ни шелла, ни другого доступа. Архив проверяется до любых изменений на диске, а юнит systemd скрипт выкладки не трогает — его ставит только провижининг, чтобы архив не мог подменить пользователя сервиса.
После выкладки проверяется и публичный адрес: health отвечает, список направлений и демо-граф открываются без cookie, закрытое направление без cookie отдаёт 401.
Smoke идёт по живому рантайму на свежей базе: начинает без входа в демо, проверяет темы и языковую пару, заходит по скрытой ссылке, создаёт направление мастером, правит структуру перетаскиванием, разбирает карточки с клавиатуры, заводит и удаляет аккаунт.
Один из шагов появился после случайного падения: фоновые узлы доски не имели размера, React Flow
держал их скрытыми до замера, и при потере замера колонки не появлялись вовсе. Теперь узлы несут
размер из раскладки, а шаг воспроизводит поломку заглушкой ResizeObserver — проверка,
которая до исправления падала, а после проходит.
AGENTS.md, CLAUDE.md, DEPLOY.md, backend/app/sync.py,
backend/app/main.py, deploy/ladder-backup.py, deploy/deploy-ladder.sh,
tools/ladder-mcp/, frontend/smoke.mjsdocs/superpowers/specs/2026-09-11-content-in-db-design.md,
docs/superpowers/specs/2026-09-11-demo-and-access-design.mdladder-backup --counts на сервере, 2026-09-14wc -l по backend/app и frontend/src,
git rev-list --count, на 2026-09-14