Add foundational phase documentation for mod development
This commit is contained in:
@@ -0,0 +1,180 @@
|
|||||||
|
# Фундамент: паки, числа, сид, версия и золотые файлы
|
||||||
|
|
||||||
|
Договорённость на срез после жизни школы. Не текущий код — то, что решено сделать.
|
||||||
|
|
||||||
|
Срез не добавляет игроку ни одной новой кнопки. Он берёт то, что ревью нашло дешёвым и заметным,
|
||||||
|
и доводит до конца: мод становится настоящим объектом, числа поведения уезжают из кода в данные,
|
||||||
|
сид перестаёт быть идентификатором школы, у сейва появляется читаемая версия, а регрессии, которые
|
||||||
|
сейчас ловятся только глазами, начинают ловиться тестом.
|
||||||
|
|
||||||
|
## Почему сейчас
|
||||||
|
|
||||||
|
**Моды существуют на бумаге.** В `mods/` лежит один `core`. Last-wins, патчи и порядок загрузки
|
||||||
|
проверяются синтетическими документами в памяти, а путь «игрок выбрал мод» не проверяется вообще —
|
||||||
|
подставить нечего. Пак не имеет удостоверения: `GET /api/mods` отдаёт `{id, required}`, и в диалоге
|
||||||
|
создания игрок видит имя папки. Зависимостей между паками нет, поэтому мебельный набор, который
|
||||||
|
патчит чужой def, может быть выбран без того, кого он патчит, — и школа не соберётся с невнятной
|
||||||
|
ошибкой каталога.
|
||||||
|
|
||||||
|
**Числа поведения живут в коде вопреки [`ai.md`](ai.md).** Там записано: «Числа поведения —
|
||||||
|
отдельный деф правил, как `StaffingDef` у штата». В `BehaviorDef` уехали порог нужды, скорость
|
||||||
|
обучения и разброс на дорогу, а веса целей остались константами в `Decision.cs`. Пока они там, мод
|
||||||
|
не может перебалансировать поведение, не написав кода, — а это ровно то обещание, ради которого
|
||||||
|
заводились дефы.
|
||||||
|
|
||||||
|
**Две регрессии ловятся руками.** Генератор людей переписывали дважды за одну сессию ревью, а
|
||||||
|
тест на детерминизм сравнивает два прогона *в одном процессе*: он не заметит, если поменяется сам
|
||||||
|
порядок бросков. [`people.md`](people.md) называет отпечаток «единственным способом поймать
|
||||||
|
регрессию в генераторе». Так же с сейвом: поле `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/` и ехать в вывод
|
||||||
|
сборки теми же правилами.
|
||||||
|
|
||||||
|
## Числа поведения — в `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 без подписи | Предупреждение в лог при загрузке |
|
||||||
|
| Сид школы | Своё поле: случайный при создании, можно передать, видно игроку |
|
||||||
|
| Старые школы | Продолжают жить со своим прежним сидом из файла людей |
|
||||||
|
| Версия сейва | Читается: новее — не стартуем, старее — именованный шов для апгрейда |
|
||||||
|
| Дев-дамп | За переключателем, через снимки и мейлбокс, не в обход работника |
|
||||||
|
| Тесты экранов | Одна девзависимость; проверяется логика, не вёрстка |
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
# Фаза 22. Удостоверение пака
|
||||||
|
|
||||||
|
## Зависимости
|
||||||
|
|
||||||
|
- [Фаза 4](04-create-editor.md) — моды и каталог уже ездят в create
|
||||||
|
|
||||||
|
## Зачем
|
||||||
|
|
||||||
|
Мод перестаёт быть именем папки. У него появляется название, версия и список паков, без которых он
|
||||||
|
не работает, — а у сервера появляется право отказать во внятной форме вместо сломанного каталога.
|
||||||
|
|
||||||
|
## Задачи
|
||||||
|
|
||||||
|
- [ ] `pack.jsonc` в папке пака: `version` строкой, `requires` списком id. Файла нет — пак
|
||||||
|
по-прежнему валиден: id вместо названия, версия пустая, зависимостей нет
|
||||||
|
- [ ] Название пака — ключ по его id в его же `localizations/<lang>.jsonc`; второго способа
|
||||||
|
называть вещи не заводить
|
||||||
|
- [ ] `GET /api/mods` принимает `?lang=ru|en` и отдаёт `label`, `version` и `requires` рядом с
|
||||||
|
`id` и `required`
|
||||||
|
- [ ] У `core` такой же `pack.jsonc` и такое же название в локалях
|
||||||
|
- [ ] Создание школы проверяет, что каждая зависимость выбрана; нет — `400` с кодом и id того,
|
||||||
|
кого не хватает
|
||||||
|
- [ ] Порядок загрузки выстраивает сервер: устойчивая топологическая сортировка поверх порядка
|
||||||
|
игрока, `core` всегда первый. Цикл зависимостей — отказ
|
||||||
|
- [ ] Разрешённый порядок виден: пишется в лог при старте школы и возвращается в ответе создания
|
||||||
|
- [ ] Сейв хранит **разрешённый** порядок паков, чтобы школа поднималась тем же каталогом
|
||||||
|
- [ ] Загрузчик пишет предупреждение, когда у конкретного def нет подписи в локали пака
|
||||||
|
- [ ] `docs/protocol.md` и [`../design/foundation.md`](../design/foundation.md) правятся тем же
|
||||||
|
коммитом, что и обработчики
|
||||||
|
|
||||||
|
## Тесты, без которых фаза не закрыта
|
||||||
|
|
||||||
|
- [ ] Пак без `pack.jsonc` виден в списке, id стоит вместо названия
|
||||||
|
- [ ] Название приходит на языке запроса, у `core` тоже
|
||||||
|
- [ ] Пак с невыбранной зависимостью не создаёт школу; в ответе видно, кого не хватает
|
||||||
|
- [ ] Зависимость, выбранная после зависимого, всё равно грузится раньше
|
||||||
|
- [ ] Цикл зависимостей — отказ, а не зависание
|
||||||
|
- [ ] Def без подписи даёт предупреждение, но не роняет каталог
|
||||||
|
|
||||||
|
## Критерий готовности
|
||||||
|
|
||||||
|
- В диалоге создания моды подписаны по-человечески, `core` заблокирован как раньше
|
||||||
|
- Школа с модом поднимается после перезапуска тем же набором и в том же порядке
|
||||||
|
- `dotnet test` и клиентские `npm test` / `run build` проходят
|
||||||
|
|
||||||
|
## Стоп
|
||||||
|
|
||||||
|
Не грузить DLL. Не давать менять набор модов у живой школы. Не делать UI перестановки паков.
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
# Фаза 23. Настоящий пример мода
|
||||||
|
|
||||||
|
## Зависимости
|
||||||
|
|
||||||
|
- [Фаза 22](22-mod-identity.md) — пример должен нести удостоверение, как все
|
||||||
|
|
||||||
|
## Зачем
|
||||||
|
|
||||||
|
Дорога от папки на диске до школы не проверена ни разу: в `mods/` лежит только `core`, а last-wins
|
||||||
|
и патчи живут в тестах на документах в памяти. Один маленький настоящий пак делает весь путь
|
||||||
|
проверяемым — и заодно служит образцом для мод-автора.
|
||||||
|
|
||||||
|
## Задачи
|
||||||
|
|
||||||
|
- [ ] Пак `mods/example` рядом с `core`: `pack.jsonc`, свои локали, пара черт, набор имён,
|
||||||
|
патч чужого def и одна комната
|
||||||
|
- [ ] Пак нарочно скучный: он образец и фикстура, а не контент. Ванильную игру он не меняет,
|
||||||
|
пока не выбран
|
||||||
|
- [ ] Пак едет в вывод сборки тестов теми же правилами, что и `core`
|
||||||
|
- [ ] Короткий `README.md` внутри пака: что где лежит и как добавить своё
|
||||||
|
- [ ] Ориентир в [`../design/defs.md`](../design/defs.md) показывает на него как на образец
|
||||||
|
|
||||||
|
## Тесты, без которых фаза не закрыта
|
||||||
|
|
||||||
|
- [ ] `GET /api/catalog?mods=example` отдаёт типы и подписи пака поверх `core`
|
||||||
|
- [ ] Школа создаётся с паком и поднимается с ним после перезапуска
|
||||||
|
- [ ] Патч пака виден в каталоге школы, а без пака его нет
|
||||||
|
- [ ] Одинаковый `defName` в `core` и в паке — побеждает пак, в логе предупреждение
|
||||||
|
- [ ] Карта пака проходит валидацию и годится для создания школы
|
||||||
|
|
||||||
|
## Критерий готовности
|
||||||
|
|
||||||
|
- Создать школу с включённым паком и увидеть его содержимое внутри школы
|
||||||
|
- Снять пак — школа создаётся прежней
|
||||||
|
- `dotnet test` проходит
|
||||||
|
|
||||||
|
## Стоп
|
||||||
|
|
||||||
|
Не превращать пример в контент-пак: чем он меньше, тем дольше проживёт.
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
# Фаза 24. Числа поведения в данные
|
||||||
|
|
||||||
|
## Зависимости
|
||||||
|
|
||||||
|
- [Фаза 20](20-needs-actions.md) — `BehaviorDef` уже есть
|
||||||
|
|
||||||
|
## Зачем
|
||||||
|
|
||||||
|
[`../design/ai.md`](../design/ai.md) обещает: числа поведения — деф правил, как `StaffingDef` у
|
||||||
|
штата. Порог нужды и разброс на дорогу туда уехали, а веса целей остались константами в коде.
|
||||||
|
Пока они там, мод, меняющий одно число, вынужден быть форком.
|
||||||
|
|
||||||
|
## Задачи
|
||||||
|
|
||||||
|
- [ ] Веса целей переезжают в `BehaviorDef`: обязанность-урок, обязанность-переход, нужда на нуле,
|
||||||
|
обед
|
||||||
|
- [ ] Значения по умолчанию — сегодняшние; поведение не должно измениться ни на минуту
|
||||||
|
- [ ] Каталог без `BehaviorDef` работает на константах кода, а не падает
|
||||||
|
- [ ] Валидатор ловит отрицательные веса и порядок, который делает обед сильнее урока
|
||||||
|
- [ ] Комментарий у каждого числа объясняет, что оно перевешивает — иначе мод-автор крутит вслепую
|
||||||
|
|
||||||
|
## Тесты, без которых фаза не закрыта
|
||||||
|
|
||||||
|
- [ ] Ванильные числа дают ровно те же решения, что и до переезда (таблица входов и выходов)
|
||||||
|
- [ ] Пак, поднявший вес обеда выше урока, уводит класс с урока в столовую
|
||||||
|
- [ ] Каталог без `BehaviorDef` принимает решения на значениях по умолчанию
|
||||||
|
- [ ] Отрицательный вес роняет загрузку каталога
|
||||||
|
|
||||||
|
## Критерий готовности
|
||||||
|
|
||||||
|
- Отпечаток недели из фазы 25 не меняется от самого переезда
|
||||||
|
- `dotnet test` проходит
|
||||||
|
|
||||||
|
## Стоп
|
||||||
|
|
||||||
|
Не заводить формулы в данных. Число — да, выражение — нет.
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
# Фаза 25. Золотые файлы и мелкие долги
|
||||||
|
|
||||||
|
## Зависимости
|
||||||
|
|
||||||
|
Нет. Можно вести параллельно с 22–24.
|
||||||
|
|
||||||
|
## Зачем
|
||||||
|
|
||||||
|
Две регрессии сейчас ловятся только глазами: молчаливая смена генерации людей и сейв, который
|
||||||
|
перестал грузиться после нового поля. Обе дешёвы в защите и дороги в разборе постфактум.
|
||||||
|
|
||||||
|
## Задачи
|
||||||
|
|
||||||
|
- [ ] Отпечаток ростера файлом в фикстурах: фиксированные сид, карта, набор имён и родной язык
|
||||||
|
- [ ] Тест печатает **первую** несовпавшую строку, а не «строки различаются»
|
||||||
|
- [ ] Правило записано рядом с фикстурой: отпечаток обновляется в том же коммите, что и генерация,
|
||||||
|
и коммит объясняет, что поменялось
|
||||||
|
- [ ] Файл сейва текущего формата в фикстурах хоста; тест поднимает из него школу
|
||||||
|
- [ ] Рядом с ним — файл сейва **без** новых полей, чтобы «отсутствующее поле имеет разумное
|
||||||
|
значение» проверялось, а не подразумевалось
|
||||||
|
- [ ] Форма голодного дня закрепляется тестом: приходят сытыми, к своей смене около 0.4, уходят
|
||||||
|
голодными, к утру снова полны. Числа подобраны замером и сейчас ничем не защищены
|
||||||
|
- [ ] `uncovered` в staffing говорит, скольких учителей не хватает предмету
|
||||||
|
- [ ] Клиент показывает это число в списке непокрытых, через `t(...)`
|
||||||
|
|
||||||
|
## Тесты, без которых фаза не закрыта
|
||||||
|
|
||||||
|
- [ ] Отпечаток ростера совпадает с файлом
|
||||||
|
- [ ] Школа поднимается из сохранённого файла: те же люди, то же время, тот же штат
|
||||||
|
- [ ] Сейв без поля родного языка грузится и не перетасовывает набор
|
||||||
|
- [ ] Кривая голода за учебный день держится в заявленных границах
|
||||||
|
- [ ] `PrimarySchool` на ванильной карте требует трёх учителей, и ответ это говорит
|
||||||
|
- [ ] Назначение второго учителя уменьшает нехватку, но не закрывает её
|
||||||
|
|
||||||
|
## Критерий готовности
|
||||||
|
|
||||||
|
- Изменить порядок бросков в генераторе — золотой тест краснеет
|
||||||
|
- Добавить поле в сейв — старый файл по-прежнему грузится
|
||||||
|
- `dotnet test` и клиентские `npm test` / `run build` проходят
|
||||||
|
|
||||||
|
## Стоп
|
||||||
|
|
||||||
|
Не превращать золотой файл в снимок всего мира: отпечаток должен читаться человеком.
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
# Фаза 26. Свой сид у школы
|
||||||
|
|
||||||
|
## Зависимости
|
||||||
|
|
||||||
|
- [Фаза 7](07-people-in-school.md) — сид уже лежит в файле людей
|
||||||
|
|
||||||
|
## Зачем
|
||||||
|
|
||||||
|
Сегодня сид генерации — это `school.Id`. Отсюда три неприятности сразу: у двух игроков школа №1
|
||||||
|
населена одинаково, состав школы зависит от того, сколько школ создали до неё, и воспроизвести
|
||||||
|
чужой баг нельзя, не повторив последовательность идентификаторов. Ревью споткнулось об это
|
||||||
|
дважды — тест, зелёный на машине разработчика и красный на чистом клоне, был ровно про это.
|
||||||
|
|
||||||
|
Сид уже хранится в файле людей. Не хватает одного: чтобы он был **свой**, а не производный.
|
||||||
|
|
||||||
|
## Задачи
|
||||||
|
|
||||||
|
- [ ] При создании школы сид берётся из генератора случайных чисел сервера, а не из id
|
||||||
|
- [ ] `POST /api/schools` принимает необязательный `seed`; передали — берётся он, нет — бросается
|
||||||
|
- [ ] Сид виден: в ответе `GET /api/schools` и на экране школы, чтобы его можно было переслать
|
||||||
|
- [ ] Существующие школы не меняются: сид читается из файла людей, как и сейчас
|
||||||
|
- [ ] Тесты хоста, которым нужен предсказуемый состав, передают сид явно, а не полагаются на
|
||||||
|
порядок создания
|
||||||
|
- [ ] `docs/protocol.md` и [`../design/people.md`](../design/people.md) правятся тем же коммитом
|
||||||
|
|
||||||
|
## Тесты, без которых фаза не закрыта
|
||||||
|
|
||||||
|
- [ ] Две школы, созданные с одним сидом, населены одинаково; с разными — по-разному
|
||||||
|
- [ ] Школа, созданная без сида, после перезапуска поднимает тот же состав
|
||||||
|
- [ ] Старый сейв, где сид совпадал с id, грузится и состав не меняется
|
||||||
|
- [ ] Переданный сид виден в списке школ
|
||||||
|
|
||||||
|
## Критерий готовности
|
||||||
|
|
||||||
|
- Создать две школы подряд без сида и увидеть разные фамилии в пятых классах
|
||||||
|
- Создать школу с чужим сидом и получить ту же школу
|
||||||
|
- `dotnet test` и клиентские `npm test` / `run build` проходят
|
||||||
|
|
||||||
|
## Стоп
|
||||||
|
|
||||||
|
Не делать сид редактируемым у живой школы. Не показывать его там, где он мешает.
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
# Фаза 27. Версия сейва и взгляд внутрь
|
||||||
|
|
||||||
|
## Зависимости
|
||||||
|
|
||||||
|
- [Фаза 2](02-school-worker.md) — сейв и дев-поверхность уже есть
|
||||||
|
|
||||||
|
## Зачем
|
||||||
|
|
||||||
|
Две дыры в обслуживании. Первая: `Format` в сейве штампуется при записи и копируется при чтении,
|
||||||
|
но никогда не сравнивается — файл из будущей версии загрузится молча и будет понят неправильно, а
|
||||||
|
места для миграции просто нет. Вторая: посмотреть внутрь живой школы нечем. Всё ревью замеры
|
||||||
|
делались временными тестами, потому что другого способа узнать, кто где и с какими нуждами, не
|
||||||
|
существует.
|
||||||
|
|
||||||
|
## Задачи
|
||||||
|
|
||||||
|
- [ ] Загрузка читает `Format`: новее своего — школу не стартовать, файл не трогать, в лог. Как с
|
||||||
|
пропавшей папкой мода
|
||||||
|
- [ ] Формат старее своего — явный шов для апгрейда: сегодня пустой, но названный и с комментарием
|
||||||
|
- [ ] `GET /api/dev/schools/{id}/dump` — ростер, присутствие, расписание и нужды одним JSON. За тем
|
||||||
|
же переключателем, что и `reload-schools`, и никогда в проде по умолчанию
|
||||||
|
- [ ] Дамп читает опубликованные снимки и мейлбокс, а не лезет в `World` мимо работника
|
||||||
|
- [ ] Раздел «Things that will bite you» в `AGENTS.md` пополняется тем, что нашло ревью: сид школы
|
||||||
|
был её id; тесты хоста делят один сервер и одну папку сейвов, поэтому начинают с очистки;
|
||||||
|
на паузе кадры присутствия не приходят вовсе
|
||||||
|
- [ ] Классы тестов вокруг Arch перестают идти параллельно: `HSchool.Simulation.Tests` получает
|
||||||
|
запрет параллельности, потому что нативная память Arch этого не любит
|
||||||
|
- [ ] `docs/protocol.md` описывает дамп в разделе дев-ручек
|
||||||
|
|
||||||
|
## Тесты, без которых фаза не закрыта
|
||||||
|
|
||||||
|
- [ ] Сейв с `format` больше текущего оставляет школу незапущенной и файл нетронутым
|
||||||
|
- [ ] Сейв текущего формата грузится как раньше
|
||||||
|
- [ ] Дамп отдаёт людей, их узлы и текущее расписание для живой школы
|
||||||
|
- [ ] Дамп неизвестной школы — `404`
|
||||||
|
|
||||||
|
## Критерий готовности
|
||||||
|
|
||||||
|
- Подсунуть сейв из будущего — сервер стартует, эта школа не поднимается, в логе понятно почему
|
||||||
|
- Снять дамп с идущей школы и увидеть, кто где стоит
|
||||||
|
- `dotnet test` проходит; полный прогон решения стабилен
|
||||||
|
|
||||||
|
## Стоп
|
||||||
|
|
||||||
|
Не писать миграции, которых пока не нужно. Не открывать дев-ручки без переключателя.
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
# Фаза 28. Тесты экранов
|
||||||
|
|
||||||
|
## Зависимости
|
||||||
|
|
||||||
|
- [Фаза 13](13-management-tab.md), [Фаза 17](17-timetable-screen.md) — самые крупные экраны уже есть
|
||||||
|
|
||||||
|
## Зачем
|
||||||
|
|
||||||
|
`AGENTS.md` говорит: у экранов тестов нет, потому что DOM-окружение стоило бы зависимости,
|
||||||
|
которой у проекта нет. На первой фазе это было верно — экран состоял из часов. Сейчас в клиенте
|
||||||
|
несколько тысяч строк с диалогами, сеткой расписания, редактором карты и панелью управления, а
|
||||||
|
vitest уже стоит: DOM-окружение — это одна девзависимость.
|
||||||
|
|
||||||
|
Цель не «покрыть клиент», а закрыть те места, где на клиенте есть **логика**: сборка запроса из
|
||||||
|
фильтров, разбор кода ошибки в текст, блокировки в диалоге создания.
|
||||||
|
|
||||||
|
## Задачи
|
||||||
|
|
||||||
|
- [ ] `happy-dom` как девзависимость и окружение vitest для тестов экранов
|
||||||
|
- [ ] Тесты панели людей: фильтры собирают правильный запрос, пейджер не уезжает за границы
|
||||||
|
- [ ] Тесты диалога создания: `core` нельзя снять, сброс карты возвращает дефолт, кнопка
|
||||||
|
блокируется на время запроса
|
||||||
|
- [ ] Тесты сетки расписания: код отказа планировщика превращается в текст, а не в молчание
|
||||||
|
- [ ] Тесты панели управления: отказ по пределу фонда показывается текстом
|
||||||
|
- [ ] Политика тестирования в `AGENTS.md` переписывается: что теперь проверяется тестом, а что
|
||||||
|
по-прежнему глазами
|
||||||
|
- [ ] Прогон экранов не должен заметно удлинять `npm test`
|
||||||
|
|
||||||
|
## Тесты, без которых фаза не закрыта
|
||||||
|
|
||||||
|
Сами тесты и есть содержание фазы; закрывают её четыре набора выше.
|
||||||
|
|
||||||
|
## Критерий готовности
|
||||||
|
|
||||||
|
- `npm --prefix src/HSchool.Client test` гоняет экраны и проходит
|
||||||
|
- CI не удлинился настолько, чтобы это раздражало
|
||||||
|
- Сломать `t(...)` в одном из экранов — тест краснеет
|
||||||
|
|
||||||
|
## Стоп
|
||||||
|
|
||||||
|
Не тащить фреймворк ради тестов. Не проверять вёрстку и стили — только поведение.
|
||||||
@@ -91,3 +91,25 @@
|
|||||||
| --- | --- | --- |
|
| --- | --- | --- |
|
||||||
| [20. Нужды и действия](20-needs-actions.md) | ✅ | `ActionDef` с полями, декей, восполнение |
|
| [20. Нужды и действия](20-needs-actions.md) | ✅ | `ActionDef` с полями, декей, восполнение |
|
||||||
| [21. Выбор действия](21-decisions.md) | ✅ | Цели и веса, «дойти → сделать», уход с урока, рост навыка |
|
| [21. Выбор действия](21-decisions.md) | ✅ | Цели и веса, «дойти → сделать», уход с урока, рост навыка |
|
||||||
|
|
||||||
|
## Срез 6. Фундамент
|
||||||
|
|
||||||
|
Дизайн: [`../design/foundation.md`](../design/foundation.md).
|
||||||
|
|
||||||
|
Срез без единой новой кнопки для игрока. Мод становится настоящим объектом, числа поведения
|
||||||
|
уезжают из кода в данные, сид перестаёт быть идентификатором, у сейва появляется версия, а то,
|
||||||
|
что ревью ловило глазами, начинает ловиться тестом. Всё здесь дёшево поодиночке и заметно
|
||||||
|
упрощает следующие срезы.
|
||||||
|
|
||||||
|
Порядок свободный: 25, 26 и 27 ни от чего не зависят, 23 стоит на 22. Начинать разумно с 25 —
|
||||||
|
она страхует всё остальное.
|
||||||
|
|
||||||
|
| Фаза | Статус | Зачем |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| [22. Удостоверение пака](22-mod-identity.md) | ⬜ | Название, версия, зависимости и порядок загрузки |
|
||||||
|
| [23. Пример мода](23-example-pack.md) | ⬜ | Настоящая папка вместо документов в памяти |
|
||||||
|
| [24. Числа поведения](24-behavior-numbers.md) | ⬜ | Веса целей в `BehaviorDef`, а не в коде |
|
||||||
|
| [25. Золотые файлы](25-golden-fixtures.md) | ⬜ | Отпечаток ростера, старый сейв, кривая голода, нехватка учителей |
|
||||||
|
| [26. Свой сид](26-school-seed.md) | ⬜ | Состав школы перестаёт зависеть от порядка создания |
|
||||||
|
| [27. Версия сейва и дамп](27-serviceability.md) | ⬜ | Формат читается, внутрь школы можно заглянуть |
|
||||||
|
| [28. Тесты экранов](28-screen-tests.md) | ⬜ | DOM-окружение и логика клиента под тестом |
|
||||||
|
|||||||
Reference in New Issue
Block a user