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` с полями, декей, восполнение |
|
||||
| [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