From c27d43f66a34fc77e429b10371b4ef6746326202 Mon Sep 17 00:00:00 2001 From: Leonid Pershin Date: Wed, 19 Aug 2026 23:35:30 +0300 Subject: [PATCH] Add foundational phase documentation for mod development --- docs/design/foundation.md | 180 +++++++++++++++++++++++++++++ docs/phases/22-mod-identity.md | 48 ++++++++ docs/phases/23-example-pack.md | 39 +++++++ docs/phases/24-behavior-numbers.md | 36 ++++++ docs/phases/25-golden-fixtures.md | 43 +++++++ docs/phases/26-school-seed.md | 41 +++++++ docs/phases/27-serviceability.md | 45 ++++++++ docs/phases/28-screen-tests.md | 41 +++++++ docs/phases/README.md | 22 ++++ 9 files changed, 495 insertions(+) create mode 100644 docs/design/foundation.md create mode 100644 docs/phases/22-mod-identity.md create mode 100644 docs/phases/23-example-pack.md create mode 100644 docs/phases/24-behavior-numbers.md create mode 100644 docs/phases/25-golden-fixtures.md create mode 100644 docs/phases/26-school-seed.md create mode 100644 docs/phases/27-serviceability.md create mode 100644 docs/phases/28-screen-tests.md diff --git a/docs/design/foundation.md b/docs/design/foundation.md new file mode 100644 index 0000000..f6fd84c --- /dev/null +++ b/docs/design/foundation.md @@ -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//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 без подписи | Предупреждение в лог при загрузке | +| Сид школы | Своё поле: случайный при создании, можно передать, видно игроку | +| Старые школы | Продолжают жить со своим прежним сидом из файла людей | +| Версия сейва | Читается: новее — не стартуем, старее — именованный шов для апгрейда | +| Дев-дамп | За переключателем, через снимки и мейлбокс, не в обход работника | +| Тесты экранов | Одна девзависимость; проверяется логика, не вёрстка | diff --git a/docs/phases/22-mod-identity.md b/docs/phases/22-mod-identity.md new file mode 100644 index 0000000..acc4bb6 --- /dev/null +++ b/docs/phases/22-mod-identity.md @@ -0,0 +1,48 @@ +# Фаза 22. Удостоверение пака + +## Зависимости + +- [Фаза 4](04-create-editor.md) — моды и каталог уже ездят в create + +## Зачем + +Мод перестаёт быть именем папки. У него появляется название, версия и список паков, без которых он +не работает, — а у сервера появляется право отказать во внятной форме вместо сломанного каталога. + +## Задачи + +- [ ] `pack.jsonc` в папке пака: `version` строкой, `requires` списком id. Файла нет — пак + по-прежнему валиден: id вместо названия, версия пустая, зависимостей нет +- [ ] Название пака — ключ по его id в его же `localizations/.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 перестановки паков. diff --git a/docs/phases/23-example-pack.md b/docs/phases/23-example-pack.md new file mode 100644 index 0000000..83dc268 --- /dev/null +++ b/docs/phases/23-example-pack.md @@ -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` проходит + +## Стоп + +Не превращать пример в контент-пак: чем он меньше, тем дольше проживёт. diff --git a/docs/phases/24-behavior-numbers.md b/docs/phases/24-behavior-numbers.md new file mode 100644 index 0000000..072d067 --- /dev/null +++ b/docs/phases/24-behavior-numbers.md @@ -0,0 +1,36 @@ +# Фаза 24. Числа поведения в данные + +## Зависимости + +- [Фаза 20](20-needs-actions.md) — `BehaviorDef` уже есть + +## Зачем + +[`../design/ai.md`](../design/ai.md) обещает: числа поведения — деф правил, как `StaffingDef` у +штата. Порог нужды и разброс на дорогу туда уехали, а веса целей остались константами в коде. +Пока они там, мод, меняющий одно число, вынужден быть форком. + +## Задачи + +- [ ] Веса целей переезжают в `BehaviorDef`: обязанность-урок, обязанность-переход, нужда на нуле, + обед +- [ ] Значения по умолчанию — сегодняшние; поведение не должно измениться ни на минуту +- [ ] Каталог без `BehaviorDef` работает на константах кода, а не падает +- [ ] Валидатор ловит отрицательные веса и порядок, который делает обед сильнее урока +- [ ] Комментарий у каждого числа объясняет, что оно перевешивает — иначе мод-автор крутит вслепую + +## Тесты, без которых фаза не закрыта + +- [ ] Ванильные числа дают ровно те же решения, что и до переезда (таблица входов и выходов) +- [ ] Пак, поднявший вес обеда выше урока, уводит класс с урока в столовую +- [ ] Каталог без `BehaviorDef` принимает решения на значениях по умолчанию +- [ ] Отрицательный вес роняет загрузку каталога + +## Критерий готовности + +- Отпечаток недели из фазы 25 не меняется от самого переезда +- `dotnet test` проходит + +## Стоп + +Не заводить формулы в данных. Число — да, выражение — нет. diff --git a/docs/phases/25-golden-fixtures.md b/docs/phases/25-golden-fixtures.md new file mode 100644 index 0000000..91f4ae0 --- /dev/null +++ b/docs/phases/25-golden-fixtures.md @@ -0,0 +1,43 @@ +# Фаза 25. Золотые файлы и мелкие долги + +## Зависимости + +Нет. Можно вести параллельно с 22–24. + +## Зачем + +Две регрессии сейчас ловятся только глазами: молчаливая смена генерации людей и сейв, который +перестал грузиться после нового поля. Обе дешёвы в защите и дороги в разборе постфактум. + +## Задачи + +- [ ] Отпечаток ростера файлом в фикстурах: фиксированные сид, карта, набор имён и родной язык +- [ ] Тест печатает **первую** несовпавшую строку, а не «строки различаются» +- [ ] Правило записано рядом с фикстурой: отпечаток обновляется в том же коммите, что и генерация, + и коммит объясняет, что поменялось +- [ ] Файл сейва текущего формата в фикстурах хоста; тест поднимает из него школу +- [ ] Рядом с ним — файл сейва **без** новых полей, чтобы «отсутствующее поле имеет разумное + значение» проверялось, а не подразумевалось +- [ ] Форма голодного дня закрепляется тестом: приходят сытыми, к своей смене около 0.4, уходят + голодными, к утру снова полны. Числа подобраны замером и сейчас ничем не защищены +- [ ] `uncovered` в staffing говорит, скольких учителей не хватает предмету +- [ ] Клиент показывает это число в списке непокрытых, через `t(...)` + +## Тесты, без которых фаза не закрыта + +- [ ] Отпечаток ростера совпадает с файлом +- [ ] Школа поднимается из сохранённого файла: те же люди, то же время, тот же штат +- [ ] Сейв без поля родного языка грузится и не перетасовывает набор +- [ ] Кривая голода за учебный день держится в заявленных границах +- [ ] `PrimarySchool` на ванильной карте требует трёх учителей, и ответ это говорит +- [ ] Назначение второго учителя уменьшает нехватку, но не закрывает её + +## Критерий готовности + +- Изменить порядок бросков в генераторе — золотой тест краснеет +- Добавить поле в сейв — старый файл по-прежнему грузится +- `dotnet test` и клиентские `npm test` / `run build` проходят + +## Стоп + +Не превращать золотой файл в снимок всего мира: отпечаток должен читаться человеком. diff --git a/docs/phases/26-school-seed.md b/docs/phases/26-school-seed.md new file mode 100644 index 0000000..6c8379a --- /dev/null +++ b/docs/phases/26-school-seed.md @@ -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` проходят + +## Стоп + +Не делать сид редактируемым у живой школы. Не показывать его там, где он мешает. diff --git a/docs/phases/27-serviceability.md b/docs/phases/27-serviceability.md new file mode 100644 index 0000000..3133f9c --- /dev/null +++ b/docs/phases/27-serviceability.md @@ -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` проходит; полный прогон решения стабилен + +## Стоп + +Не писать миграции, которых пока не нужно. Не открывать дев-ручки без переключателя. diff --git a/docs/phases/28-screen-tests.md b/docs/phases/28-screen-tests.md new file mode 100644 index 0000000..f26d423 --- /dev/null +++ b/docs/phases/28-screen-tests.md @@ -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(...)` в одном из экранов — тест краснеет + +## Стоп + +Не тащить фреймворк ради тестов. Не проверять вёрстку и стили — только поведение. diff --git a/docs/phases/README.md b/docs/phases/README.md index e52d3c4..b79179c 100644 --- a/docs/phases/README.md +++ b/docs/phases/README.md @@ -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-окружение и логика клиента под тестом |