# Фундамент: паки, числа, сид, версия и золотые файлы Договорённость на срез после жизни школы. Не текущий код — то, что решено сделать. Срез не добавляет игроку ни одной новой кнопки. Он берёт то, что ревью нашло дешёвым и заметным, и доводит до конца: мод становится настоящим объектом, числа поведения уезжают из кода в данные, сид перестаёт быть идентификатором школы, у сейва появляется читаемая версия, а регрессии, которые сейчас ловятся только глазами, начинают ловиться тестом. ## Почему сейчас **Моды существуют на бумаге.** В `mods/` лежит один `core`. Last-wins, патчи и порядок загрузки проверяются синтетическими документами в памяти, а путь «игрок выбрал мод» не проверяется вообще — подставить нечего. Пак не имеет удостоверения: `GET /api/mods` отдаёт `{id, required}`, и в диалоге создания игрок видит имя папки. Зависимостей между паками нет, поэтому мебельный набор, который патчит чужой def, может быть выбран без того, кого он патчит, — и школа не соберётся с невнятной ошибкой каталога. **Числа поведения живут в коде вопреки [`ai.md`](ai.md).** Там записано: «Числа поведения — отдельный деф правил, как `StaffingDef` у штата». В `BehaviorDef` уехали порог нужды, скорость обучения и разброс на дорогу, а веса целей остались константами в `Decision.cs`. Пока они там, мод не может перебалансировать поведение, не написав кода, — а это ровно то обещание, ради которого заводились дефы. **Две регрессии ловятся руками.** Генератор людей переписывали дважды за одну сессию ревью, а тест на детерминизм сравнивает два прогона *в одном процессе*: он не заметит, если поменяется сам порядок бросков. [`people.md`](people.md) называет отпечаток «единственным способом поймать регрессию в генераторе». Так же с сейвом: поле `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/` и ехать в вывод сборки теми же правилами. ## Числа поведения — в `BehaviorDef` Веса целей переезжают в деф правил: обязанность-урок, обязанность-переход, нужда на нуле, обед. Значения по умолчанию — сегодняшние, чтобы поведение не поменялось ни на минуту. Константы в коде остаются как запасной вариант на случай пака без `BehaviorDef` — каталог без правил поведения должен работать, а не падать. Это не рефакторинг ради красоты: после переезда мод, меняющий одно число, перестаёт быть форком. ## Золотые файлы **Ростер.** Отпечаток генерации для зафиксированного сида, карты, набора имён и родного языка — файлом в фикстурах, а не строкой в тесте. Тест сравнивает и, когда расходится, печатает первую несовпавшую строку. Правило, без которого это превратится в раздражение: **отпечаток обновляется в том же коммите, что и изменение генерации, и коммит объясняет, что именно поменялось**. Красный золотой тест — это вопрос «ты правда хотел?», а не приказ. **Сейв.** Файл школы текущего формата кладётся в фикстуры и живёт там навсегда. Тест грузит его и проверяет, что школа поднялась. Каждое следующее поле сейва обязано оставить этот файл рабочим — именно так проверяется обещание «лишние поля игнорируются, отсутствующие имеют разумное значение». Меняется формат по-настоящему — рядом кладётся новый файл, старый остаётся. ## Мелкие долги, которые дешевле закрыть здесь **Сколько учителей не хватает.** Сейчас `uncovered` говорит «предмет непокрыт». Замер показал, что `PrimarySchool` требует троих: восемьдесят часов в неделю против потолка в тридцать шесть. Игрок нанимает второго, ничего не меняется, и понять почему неоткуда. В строку непокрытого предмета добавляется, сколько человек его не вытягивают. **Def без подписи.** Загрузчик молча подставляет `defName`, когда ключа локали нет. Для `core` это ловит тест на полноту, для мода — ничего. Загрузчик начинает писать предупреждение в лог: мод-автор видит дыру сразу, а не по кривой подписи в дереве. ## Сид школы — свой, а не производный Сегодня сид генерации это `school.Id`. Отсюда три неприятности сразу: у двух игроков школа №1 населена одинаково; состав школы зависит от того, сколько школ создали до неё; воспроизвести чужой баг нельзя, не повторив последовательность идентификаторов. Ревью споткнулось об это дважды — тест, зелёный на машине разработчика и красный на чистом клоне, был ровно про это. Сид становится собственным полем: случайный при создании, необязательно передаваемый в запросе, видимый игроку. Файл людей его уже хранит, поэтому существующие школы не меняются — они просто продолжают жить со своим прежним числом. Побочная выгода важнее исходной причины: сидом можно поделиться. «Вот моя школа» становится одной строкой, а не архивом сейвов. ## Версия сейва читается `Format` штампуется при записи, копируется при чтении и **никогда не сравнивается**. Файл из будущей версии загрузится молча и будет понят неправильно, а места для миграции не предусмотрено. Правило то же, что с пропавшей папкой мода: формат новее своего — школу не стартовать, файл не трогать, в лог. Формат старее — явный шов для апгрейда, сегодня пустой, но названный. Пустой именованный шов дешевле, чем попытка вспомнить через полгода, куда его вставлять. ## Взгляд внутрь школы Всё ревью замеры делались временными тестами: узнать, кто где стоит и с какими нуждами, иначе нечем. Дев-ручка отдаёт ростер, присутствие, расписание и нужды одним JSON — за тем же переключателем, что и перезагрузка сейвов, и никогда включённой по умолчанию. Ручка обязана читать опубликованные снимки и ходить через мейлбокс, а не лезть в `World` мимо работника: инструмент диагностики, нарушающий инвариант, сам становится источником загадок. ## Экраны под тестом `AGENTS.md` записывал: у экранов тестов нет, потому что DOM-окружение стоило бы зависимости, которой у проекта нет. На первой фазе это было верно — экран состоял из часов. Сейчас в клиенте несколько тысяч строк, а vitest уже стоит: окружение — одна девзависимость. Проверяются не экраны целиком, а места, где на клиенте есть **логика**: сборка запроса из фильтров, превращение кода ошибки в текст, блокировки в диалоге создания. Вёрстка и стили остаются на глаза — их тест всё равно не удержит. ## Что этот срез не делает - Не грузит DLL-моды и не исполняет чужой код. Паки остаются данными. - Не даёт менять набор модов у живой школы. Каталог по-прежнему замерзает на работнике. - Не кладёт `defName` комнаты в снимок карты. Клиент смог бы стилизовать типы помещений от модов, но это версия протокола и правка в трёх местах — не «дёшево», значит не сюда. - Не заводит UI для порядка модов. Порядок — алфавит плюс зависимости; ручная перестановка ждёт того дня, когда паков станет больше трёх. ## Зафиксировано этим разговором | Тема | Решение | | --- | --- | | Удостоверение пака | `pack.jsonc`: версия и `requires`; название — в локалях по id пака | | Пак без файла | Валиден: id вместо названия, без зависимостей | | Зависимости | Проверяются на присутствие; порядок сервер выстраивает сам | | Порядок загрузки | Устойчивая топологическая сортировка поверх порядка игрока; результат виден в ответе | | Отсутствующая зависимость | Отказ создания с кодом ошибки, не сломанный каталог | | Цикл зависимостей | Отказ | | `core` | Такой же пак; особенность одна — нельзя снять | | Пример мода | Настоящая папка в репозитории, ради проверяемого пути, а не ради контента | | Веса целей | В `BehaviorDef`, значения по умолчанию — сегодняшние | | Каталог без `BehaviorDef` | Работает на константах кода | | Золотой ростер | Файл-отпечаток; обновляется в том же коммите, что и генерация | | Золотой сейв | Файл живёт вечно; новое поле обязано его не сломать | | Непокрытый предмет | Говорит, скольких учителей не хватает | | Def без подписи | Предупреждение в лог при загрузке | | Сид школы | Своё поле: случайный при создании, можно передать, видно игроку | | Старые школы | Продолжают жить со своим прежним сидом из файла людей | | Версия сейва | Читается: новее — не стартуем, старее — именованный шов для апгрейда | | Дев-дамп | За переключателем, через снимки и мейлбокс, не в обход работника | | Тесты экранов | Одна девзависимость; проверяется логика, не вёрстка |