# Фундамент: паки, числа, сид, версия и золотые файлы Договорённость на срез после жизни школы. Срез закрыт фазами 22–28; таблица внизу — решения. Абзацы «почему сейчас» ниже описывают дыры, которые срез закрыл, а не текущий код. ## Почему сейчас **Моды существуют на бумаге — остался проверяемый путь.** Удостоверение пака уже есть: `pack.jsonc` (версия и `requires`), название в локалях по id, `GET /api/mods?lang=` отдаёт подпись, создание отказывает во внятном коде, если зависимости нет или они замкнуты в цикл. Порядок загрузки сервер выстраивает сам и кладёт в сейв. Рядом с `core` лежит `mods/example` — маленький скучный пак на диске, чтобы last-wins, патчи и дорога «игрок выбрал мод» проверялись настоящей папкой, а не документами в памяти. **Числа поведения уехали в `BehaviorDef`.** Порог нужды, обучение, разброс на дорогу и веса целей (урок, переход, нужда на нуле, обед) — данные. Каталог без дефа падает на константы кода, а не ломается. Пак может поднять `lunchWeight` выше урока без форка. **Золотые файлы ловят то, что раньше ловили глазами.** Отпечаток ростера — файл в фикстурах. Сейв текущего формата и сейв без `nativeLanguage` грузятся тестом хоста. Формат новее своего школу не стартует и файл не трогает. ## Удостоверение пака `mods//pack.jsonc` — один объект, не список: ```jsonc { "version": "1.0", "requires": ["core"], } ``` **Название — не в этом файле.** Оно живёт там же, где все остальные подписи: в `localizations/.jsonc` того же пака, ключом по id пака. Заводить второй способ называть вещи ради одной строки не стоит, а так мод-автор пишет название рядом с названиями своих комнат. **Файла может не быть.** Папка без `pack.jsonc` — по-прежнему валидный пак: id вместо названия, версия пустая, зависимостей нет. Быстрый мод «две черты и набор имён» не должен требовать церемоний. **`requires` — присутствие и порядок.** Пак, который патчит чужие def, обязан грузиться после того, кого патчит. Поэтому сервер не просто проверяет, что зависимость выбрана, а **переставляет** выбранные паки так, чтобы зависимости шли раньше (устойчивая топологическая сортировка: порядок игрока сохраняется всюду, где он не противоречит зависимостям). Тихой перестановка не будет — разрешённый порядок возвращается в ответе создания и пишется в лог. Отсутствующая зависимость — отказ создания с внятным кодом, а не сломанный каталог. Цикл зависимостей — тоже отказ: сортировать нечего. **`core` не особенный.** У него такой же `pack.jsonc` и такое же название в локалях; особенность у него ровно одна и прежняя — его нельзя снять. ## Пример пака в репозитории Один настоящий пак рядом с `core`, маленький и скучный: пара черт, набор имён, патч чужого def и своя строка локализации. Он нужен не как контент, а как **проверяемый путь**: каталог с модом, создание школы с модом, патч поверх `core`, конфликт `defName` и last-wins, зависимость. Сегодня всё это живёт только в тестах с документами в памяти, а дорога от папки на диске до школы не проверена ни разу. Пак должен быть виден из тестов хоста, то есть лежать рядом с `core` в `mods/` и ехать в вывод сборки теми же правилами. Живая папка — `src/HSchool.Server/mods/example/`. ## Числа поведения — в `BehaviorDef` Веса целей переезжают в деф правил: обязанность-урок, обязанность-переход, нужда на нуле, обед. Значения по умолчанию — сегодняшние, чтобы поведение не поменялось ни на минуту. Константы в коде остаются как запасной вариант на случай пака без `BehaviorDef` — каталог без правил поведения должен работать, а не падать. Это не рефакторинг ради красоты: после переезда мод, меняющий одно число, перестаёт быть форком. ## Золотые файлы **Ростер.** Отпечаток генерации для зафиксированного сида, карты, набора имён и родного языка — файлом в фикстурах, а не строкой в тесте. Тест сравнивает и, когда расходится, печатает первую несовпавшую строку. Правило, без которого это превратится в раздражение: **отпечаток обновляется в том же коммите, что и изменение генерации, и коммит объясняет, что именно поменялось**. Красный золотой тест — это вопрос «ты правда хотел?», а не приказ. **Сейв.** Файл школы текущего формата кладётся в фикстуры и живёт там навсегда. Тест грузит его и проверяет, что школа поднялась. Каждое следующее поле сейва обязано оставить этот файл рабочим — именно так проверяется обещание «лишние поля игнорируются, отсутствующие имеют разумное значение». Меняется формат по-настоящему — рядом кладётся новый файл, старый остаётся. ## Мелкие долги, которые дешевле закрыть здесь **Сколько учителей не хватает.** Сейчас `uncovered` говорит «предмет непокрыт». Замер показал, что `PrimarySchool` требует троих: восемьдесят часов в неделю против потолка в тридцать шесть. Игрок нанимает второго, ничего не меняется, и понять почему неоткуда. В строку непокрытого предмета добавляется, сколько человек его не вытягивают. **Def без подписи.** Загрузчик пишет предупреждение в лог, когда у конкретного неабстрактного def нет ключа ни в `ru`, ни в `en`. Для `core` это ловит тест на полноту; для мода автор видит дыру сразу, а не по кривой подписи в дереве. Каталог при этом собирается — подставляется `defName`. ## Сид школы — свой, а не производный Сегодня сид генерации это `school.Id`. Отсюда три неприятности сразу: у двух игроков школа №1 населена одинаково; состав школы зависит от того, сколько школ создали до неё; воспроизвести чужой баг нельзя, не повторив последовательность идентификаторов. Ревью споткнулось об это дважды — тест, зелёный на машине разработчика и красный на чистом клоне, был ровно про это. Сид становится собственным полем: случайный при создании, необязательно передаваемый в запросе, видимый игроку. Файл людей его уже хранит, поэтому существующие школы не меняются — они просто продолжают жить со своим прежним числом. Побочная выгода важнее исходной причины: сидом можно поделиться. «Вот моя школа» становится одной строкой, а не архивом сейвов. ## Версия сейва читается `Format` штампуется при записи, копируется при чтении и **никогда не сравнивается**. Файл из будущей версии загрузится молча и будет понят неправильно, а места для миграции не предусмотрено. Правило то же, что с пропавшей папкой мода: формат новее своего — школу не стартовать, файл не трогать, в лог. Формат старее — явный шов для апгрейда, сегодня пустой, но названный. Пустой именованный шов дешевле, чем попытка вспомнить через полгода, куда его вставлять. ## Взгляд внутрь школы Всё ревью замеры делались временными тестами: узнать, кто где стоит и с какими нуждами, иначе нечем. Дев-ручка отдаёт ростер, присутствие, расписание и нужды одним JSON — за тем же переключателем, что и перезагрузка сейвов, и никогда включённой по умолчанию. Ручка обязана читать опубликованные снимки и ходить через мейлбокс, а не лезть в `World` мимо работника: инструмент диагностики, нарушающий инвариант, сам становится источником загадок. ## Экраны под тестом `AGENTS.md` записывал: у экранов тестов нет, потому что DOM-окружение стоило бы зависимости, которой у проекта нет. На первой фазе это было верно — экран состоял из часов. Сейчас в клиенте несколько тысяч строк, а vitest уже стоит: окружение — одна девзависимость. Проверяются не экраны целиком, а места, где на клиенте есть **логика**: сборка запроса из фильтров, превращение кода ошибки в текст, блокировки в диалоге создания. Вёрстка и стили остаются на глаза — их тест всё равно не удержит. ## Что этот срез не делает - Не грузит DLL-моды и не исполняет чужой код. Паки остаются данными. - Не даёт менять набор модов у живой школы. Каталог по-прежнему замерзает на работнике. - Не кладёт `defName` комнаты в снимок карты. Клиент смог бы стилизовать типы помещений от модов, но это версия протокола и правка в трёх местах — не «дёшево», значит не сюда. - Не заводит UI для порядка модов. Порядок — выбор игрока плюс устойчивая топологическая сортировка по `requires`; ручная перестановка ждёт того дня, когда паков станет больше трёх. ## Зафиксировано этим разговором | Тема | Решение | | --- | --- | | Удостоверение пака | `pack.jsonc`: версия и `requires`; название — в локалях по id пака | | Пак без файла | Валиден: id вместо названия, без зависимостей | | Зависимости | Проверяются на присутствие; порядок сервер выстраивает сам | | Порядок загрузки | Устойчивая топологическая сортировка поверх порядка игрока; результат виден в ответе | | Отсутствующая зависимость | Отказ создания с кодом ошибки, не сломанный каталог | | Цикл зависимостей | Отказ | | `core` | Такой же пак; особенность одна — нельзя снять | | Пример мода | Настоящая папка в репозитории, ради проверяемого пути, а не ради контента | | Веса целей | В `BehaviorDef`, значения по умолчанию — сегодняшние | | Каталог без `BehaviorDef` | Работает на константах кода | | Золотой ростер | Файл-отпечаток; обновляется в том же коммите, что и генерация | | Золотой сейв | Файл живёт вечно; новое поле обязано его не сломать | | Непокрытый предмет | Говорит, скольких учителей не хватает | | Def без подписи | Предупреждение в лог при загрузке | | Сид школы | Своё поле: случайный при создании, можно передать, видно игроку | | Старые школы | Продолжают жить со своим прежним сидом из файла людей | | Версия сейва | Читается: новее — не стартуем, старее — именованный шов для апгрейда | | Дев-дамп | За переключателем, через снимки и мейлбокс, не в обход работника | | Тесты экранов | Одна девзависимость; проверяется логика, не вёрстка |