Remove off-queue.md and add new design documentation files including README.md, defs.md, near-term.md, projects.md, runtime.md, people.md, staffing.md, and schedule.md to outline the structure and agreements for the game's design phases.

This commit is contained in:
Leonid Pershin
2026-08-20 12:26:13 +03:00
parent 12cc36b4dd
commit a37a5eb82d
101 changed files with 1015 additions and 921 deletions
+172
View File
@@ -0,0 +1,172 @@
# Фундамент: паки, числа, сид, версия и золотые файлы
Договорённость на срез после жизни школы. Срез закрыт фазами 22–28; таблица внизу — решения.
Абзацы «почему сейчас» ниже описывают дыры, которые срез закрыл, а не текущий код.
## Почему сейчас
**Моды существуют на бумаге — остался проверяемый путь.** Удостоверение пака уже есть:
`pack.jsonc` (версия и `requires`), название в локалях по id, `GET /api/mods?lang=` отдаёт
подпись, создание отказывает во внятном коде, если зависимости нет или они замкнуты в цикл.
Порядок загрузки сервер выстраивает сам и кладёт в сейв. Рядом с `core` лежит `mods/example`
маленький скучный пак на диске, чтобы last-wins, патчи и дорога «игрок выбрал мод» проверялись
настоящей папкой, а не документами в памяти.
**Числа поведения уехали в `BehaviorDef`.** Порог нужды, обучение, разброс на дорогу и веса целей
(урок, переход, нужда на нуле, обед) — данные. Каталог без дефа падает на константы кода, а не
ломается. Пак может поднять `lunchWeight` выше урока без форка.
**Золотые файлы ловят то, что раньше ловили глазами.** Отпечаток ростера — файл в фикстурах.
Сейв текущего формата и сейв без `nativeLanguage` грузятся тестом хоста. Формат новее своего
школу не стартует и файл не трогает.
## Удостоверение пака
`mods/<id>/pack.jsonc` — один объект, не список:
```jsonc
{
"version": "1.0",
"requires": ["core"],
}
```
**Название — не в этом файле.** Оно живёт там же, где все остальные подписи: в
`localizations/<lang>.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 без подписи | Предупреждение в лог при загрузке |
| Сид школы | Своё поле: случайный при создании, можно передать, видно игроку |
| Старые школы | Продолжают жить со своим прежним сидом из файла людей |
| Версия сейва | Читается: новее — не стартуем, старее — именованный шов для апгрейда |
| Дев-дамп | За переключателем, через снимки и мейлбокс, не в обход работника |
| Тесты экранов | Одна девзависимость; проверяется логика, не вёрстка |