Отлично — переходим к фазе 0: Scala 3 бэкенд в dicechess-analytics. План: каркас sbt-проекта (http4s + Tapir + Doobie + Flyway + Circe + MUnit + testcontainers), Flyway-baseline от текущей схемы PostgreSQL, перенос трёх GET-эндпоинтов с сохранением REST-контракта, правильное решение бага с рейтингом, CI. Движок подключаем как lv.id.jc:dicechess-engine-scala_3:1.2.4.
API движка ясен: FenParser.parse(fen): Either[String, GameState]. Создаю каркас проекта:
Теперь исходники. Конфигурация и подключение к БД:
Теперь API-модели (JSON-контракт в snake_case как у FastAPI) и репозитории:
Компиляция чистая (с -Werror). Перехожу к тестам:
CatsEffectSuite уже даёт IORuntime — импорт лишний, убираю:
9 из 10 прошли (testcontainers поднялся). Смотрю, что упало:
Версия API всё ещё 1.32 — docker-java не делает негоциацию, её надо задать явно. Пробую через системное свойство:
Фаза 0 выполнена — PR #59 (issue #58) открыт, CI отслеживается в фоне.
Что получилось — Scala 3 бэкенд в backend/ (~1200 строк, 21 файл), Python-приложение продолжает работать до полного паритета:
Архитектура. http4s + Tapir (типизированные эндпоинты, Swagger UI на /docs — замена FastAPI-доков) + Doobie + Flyway + Circe. Движок подключён как maven-зависимость и работает: smoke-тесты подтверждают резолвинг lv.id.jc:dicechess-engine-scala_3:1.2.4 из GitHub Packages и round-trip DFEN-парсинга — вся цепочка, которую мы строили сегодня, замкнулась.
Контракт сохранён. Все четыре GET-эндпоинта плюс корневой отвечают теми же JSON-формами (snake_case), той же сортировкой и теми же {"detail": "..."} на 404. UI ничего не заметит.
Баг rating_classic исправлен по-настоящему: рейтинг игрока вычисляется lateral join’ом из его последней партии, список игроков сортируется по нему — теперь поле, которое в Python всегда было null (а сортировка падала), несёт реальные данные.
Качество с первого коммита: 17 тестов — полный HTTP-цикл против PostgreSQL 18 в testcontainers с Flyway-миграцией с нуля и сидингом (partите/позиции/ходы/события), юнит-тесты конфига, smoke движка. Покрытие 88.4% при пороге 80 в CI, -Werror, scalafmt. Flyway с baseline-on-migrate — вашу базу на ASUS с 140k партий он не тронет, только поставит отметку.
Три заметки по ходу:
- Локальная среда: testcontainers не дружил с Rancher Desktop по трём пунктам (нестандартный сокет, Docker API 29 отверг древнюю версию 1.32 из docker-java, Ryuk-контейнер не стартует). Решено:
~/.testcontainers.propertiesс путём к сокету,-Dapi.version=1.43в build.sbt,TESTCONTAINERS_RYUK_DISABLED=trueв вашем.zshrc. Всё задокументировано в backend/README.md. - Возможный нюанс CI: воркфлоу резолвит пакет движка через repo-scoped
GITHUB_TOKEN— для публичных пакетов должно работать, но если GitHub ответит 401 на кросс-репозиторное чтение, понадобится PAT-секрет. Фоновая проверка покажет, разберу по логам. - От строгих флагов
-Yexplicit-nullsиstrictEqualityздесь осознанно отказался (в отличие от движка): модуль — обвязка над Java-библиотеками, с ними это превращается в борьбу. Аргументировано в PR.
Когда CI отработает и вы вольёте PR — фаза 0 закрыта, и дальше по плану: либо POST /api/games с валидацией движком (фаза 2 начинается естественно отсюда), либо параллельно UI-фаза с миграцией на SvelteKit.
Отлично — шаг 3: вынос реконструкции состояний с инвариант-тестами. Сначала прочитаю стор целиком, чтобы аккуратно отделить чистую логику от рантайм-состояния:
Теперь сам модуль — переношу логику без изменения поведения:
Теперь стор худеет до состояния и навигации:
Шаг 3 выполнен — PR #36 (issue #35) открыт, CI отслеживается в фоне.
Главная новость шага: инвариант-тест окупился в первые же минуты. Тест «каждое состояние = движок(предыдущее состояние, ход)» упал на расчёте использованных костей — и вскрыл настоящий продакшн-баг: база отдаёт кости цифрами ("113"), а код сравнивал их с буквами фигур. Из-за этого затемнение «использованных» костей не работало никогда, а иконки костей строили путь w1.svg — это и есть issue #30, которое мы отложили на шаге 1. Конвертация цифра→буква теперь живёт в одном месте (DIE_TO_PIECE из адаптера движка), и один фикс закрыл сразу три симптома: иконки в DiceBox, символы в истории и отметку использованных. PR закрывает и #35, и #30.
Что в PR:
- Стор похудел с 504 до 140 строк — осталось состояние, навигация, плейбек. Вся чистая логика — в
reconstruction.ts. - 24 теста (было 14): инвариант движка на согласованной партии (тестовые данные генерируются самим движком — гарантированно легальны), Roll-состояния и снап к
position_after_fen, деградация на битых данных (предупреждение + позиции из БД), интерливинг событий (оффер перед состоянием актора, accept у противоположного цвета, несовпавшие — в конец), группировка истории, использование костей. - Живая проверка: партия проигрывается чисто, иконки костей отрисовались впервые за жизнь проекта (
wP.svg,wB.svg,bQ.svg— ни одной битой), использованные кости тускнеют по мере прокрутки хода.
Когда CI и боты отработают — разберу. Останется шаг 4 (поиск/фильтры) — и фаза 1 целиком закрыта.