Update design documentation for phases 2 and 1, clarifying canceled features and their implications. Documented changes in behavior regarding scheduling, lessons, and hiring processes, along with references to related files. Enhanced the reviewed phase details, confirming the integrity of phases 0-4 and adding new tests for catalog and map functionalities.
ci / server (push) Failing after 3m58s
ci / client (push) Successful in 16s

This commit is contained in:
Leonid Pershin
2026-08-20 03:08:05 +03:00
parent eb7e8b5b99
commit d68a91da4d
2 changed files with 133 additions and 622 deletions
+5 -3
View File
@@ -347,10 +347,12 @@ Protocol ← Server → Simulation → People → Content
## Заведомо не сейчас ## Заведомо не сейчас
- Поведение: расписание, уроки, перемещение по карте, исполнение действий. Срез 2 закрыт. Строки, которые отменили следующие срезы, помечены.
- Влияние характеристик на что-либо. Черты и навыки в этом срезе — текст в карточке.
- Поведение: расписание, уроки, перемещение по карте — отменено срезами 4–5, см. [`schedule.md`](schedule.md), [`ai.md`](ai.md).
- Влияние характеристик на что-либо. Черты и навыки в этом срезе — текст в карточке — отменено срезом 5 (маршрут, обучение).
- Вызов родителя в школу. Задел есть (родитель — сущность), реализации нет. - Вызов родителя в школу. Задел есть (родитель — сущность), реализации нет.
- Наём и увольнение вручную. Должности закрываются генератором. - Наём и увольнение вручную. Должности закрываются генератором — отменено срезом 3, см. [`staffing.md`](staffing.md). Школа открывается без штата.
- Смеси наборов имён с весами. - Смеси наборов имён с весами.
- Новые роли из модов: ученик, работник, родитель — код, не данные. - Новые роли из модов: ученик, работник, родитель — код, не данные.
- Портреты и любой арт. - Портреты и любой арт.
+128 -619
View File
@@ -6,143 +6,62 @@
Коммит в «Проверен на» делает журнал самопроверяющимся. Срез перестаёт считаться проверенным, Коммит в «Проверен на» делает журнал самопроверяющимся. Срез перестаёт считаться проверенным,
как только `git log <коммит>..HEAD -- <пути>` перестаёт быть пустым — поэтому «Пути» обязательны. как только `git log <коммит>..HEAD -- <пути>` перестаёт быть пустым — поэтому «Пути» обязательны.
Закрытые замечания из прошлых проходов сюда не возвращаются. История перепроверок свёрнута в
строку «Проверен на». Последняя проверка срезов 1–6 была до фаз 29–30 (страна, одежда): дрейф
по путям есть, это очередь следующего прохода, а не задняя датировка.
Оговорки, которые относятся сразу к нескольким срезам (не дублировать ниже):
- Хостовый `OpeningASchoolDuringAMathLesson_...` после правки явки не гонялся: Aspire держал DLL
(`MSB3021`). Симуляционные тесты явки зелёные.
- `npm test` в клиенте не гоняет `tsc`. Дубликат поля и любой TS2300 зелёный в vitest и красный
на `npm run build`.
- `GET /api/catalog` отдаёт каркас дня без `lunchBreaks` (только `longBreakAfter`). Сетке смены
не нужны; молча расширять HTTP ревью не стало.
## Срез 1. Оболочка и карта ## Срез 1. Оболочка и карта
- **Фазы:** 04 - **Фазы:** 04
- **Проверен на:** `418a4ea`, 2026-08-20 (первичная проверка — `b135a9c`; предыдущая перепроверка — `5a00ad7`) - **Проверен на:** `418a4ea`, 2026-08-20
- **Пути:** `src/HSchool.Client`, `src/HSchool.Protocol`, `src/HSchool.Content`, - **Пути:** `src/HSchool.Client`, `src/HSchool.Protocol`, `src/HSchool.Content`,
`src/HSchool.Server/Game`, `src/HSchool.Server/Api/ModEndpoints.cs`, `src/HSchool.Server/Game`, `src/HSchool.Server/Api/ModEndpoints.cs`,
`src/HSchool.Server/Api/SchoolEndpoints.cs`, `src/HSchool.Server/Net`, `src/HSchool.Server/Api/SchoolEndpoints.cs`, `src/HSchool.Server/Net`,
`tests/HSchool.Content.Tests`, `tests/HSchool.Protocol.Tests`, `tests/HSchool.Content.Tests`, `tests/HSchool.Protocol.Tests`,
`tests/HSchool.AppHost.Tests/SchoolApiTests.cs`, `tests/HSchool.AppHost.Tests/GameSocketTests.cs` `tests/HSchool.AppHost.Tests/SchoolApiTests.cs`, `tests/HSchool.AppHost.Tests/GameSocketTests.cs`
- **Итог:** дописано 2 теста (оба прогнаны на чистом `b135a9c`), расхождений кода с дизайном - **Итог:** обещания фаз 0–4 целы; дописаны 3 теста; протокол v7 в трёх местах
не найдено кроме одного —
панель «Общие события» из фазы 1 снята с экрана в фазе 8 и решение нигде не записано
(оставлено открытым, см. ниже)
Что подтверждено: Что подтверждено:
- **Фаза 0.** `pixi.js` нет ни в `package.json`, ни в lockfile, ни в импортах; `npm run build` - PixiJS нет; дерево карты, пять секций локации, клиентский фильтр, `localize()`. Оболочка —
проходит. вкладки Карта/Люди, не три панели; отказ от ленты событий записан в фазе 1 и в `design/near-term.md`.
- **Фаза 1.** Дерево карты, панель локации со всеми пятью секциями и пустыми состояниями, клик по - `SchoolWorker``LongRunning`, свой `World`, аккумулятор с потолком 5. Супервизор таблицу
дереву фильтрует на клиенте, `localize()` перекрашивает подписи. Панели «Общие события» на держит и `World` не трогает. Сейв на create/shutdown/`SaveIntervalSeconds`;
экране нет — единственное расхождение среза. `ReloadFromDisk_RestoresAPausedClock`, `PausingOneSchool_DoesNotStopAnother`.
- **Фаза 2.** `SchoolWorker``LongRunning`-поток, свой `PeriodicTimer`, свой `World`, аккумулятор - Восемь тестов каталога/карты в `HSchool.Content.Tests`. Ванильная раскладка валидна.
с потолком в 5 шагов. Супервизор держит таблицу и не трогает `World` (инвариант 3). Сейв пишется Пропавшая папка мода не стартует школу и файл не трогает.
на create, на shutdown и по `SaveIntervalSeconds`; настройки коалесцируются. Требуемый фазой тест - `GET /api/mods`, каталог, create с картой и модами, снимок при OpenSchool. `core` в диалоге
«create → reload → та же школа с тем же временем» — это создания нельзя снять. Кодек, `protocol.ts` и `docs/protocol.md` описывают один `MapSnapshot`.
`GameSocketTests.ReloadFromDisk_RestoresAPausedClock` (точное равенство времени, паузы и - `modal.ts` резолвит промис явно; `.dialog--screen``height: fit-content`;
скорости), плюс `SchoolApiTests.ReloadFromDisk_RestoresCreatedSchools`. Независимость школ — `DefaultStartDate` UTC; `MapSnapshotSize` от сообщения; outbox `DropOldest`;
`GameSocketTests.PausingOneSchool_DoesNotStopAnother`. `WaitToReadAsync` переносится между итерациями; меню патчит `card.update`.
- **Фаза 3.** Все восемь тестов из списка фазы на месте в `HSchool.Content.Tests` (JSONC-фикстуры, - Родной язык — HTTP/JSON без бампа протокола. Старый сейв без поля берёт первый язык набора,
last-wins по `defName` и по ключу локали, parent/abstract и цикл, патч `add` в `actions`, не бросает кубик (`ReloadWithoutASavedPick_TakesTheFirstLanguageInsteadOfRolling`).
связность, пустая комната, одна комната без двора). Ванильная раскладка валидируется
(`VanillaCoreTests.CoreDefaultMap_PassesValidation`). Пропавшая папка мода не стартует школу и
не трогает файл: исключение летит до создания `School`, поэтому `finally` с `Persist()`
недостижим, а супервизор только снимает школу с таблицы.
- **Фаза 4.** `GET /api/mods`, `GET /api/catalog?lang=`, `POST /api/schools` с картой и модами,
снимок карты при OpenSchool в языке Hello — всё покрыто тестами
(`OpeningASchool_SendsAMapSnapshot`, `LabelsTheSnapshotInTheHelloLocale`,
`WithACustomMap_ReturnsThatLayout`, `ReloadFromDisk_RestoresACustomMap`). Кодек, `protocol.ts` и
`docs/protocol.md` описывают один и тот же `MapSnapshot`, байтовые смещения проверены с обеих
сторон; версия 7 совпадает в `ProtocolConstants` и `PROTOCOL_VERSION`.
- **Инварианты.** `Content`/`People`/`Schedule`/`Ai` не ссылаются на Arch и ASP.NET, `Simulation`
только на Arch; `DateTime.Now`/`UtcNow` в этих проектах нет.
- **«Things that will bite you».** Ни одна из относящихся к срезу регрессий не вернулась:
`modal.ts` резолвит промис явно, `.dialog--screen` держит `height: fit-content`,
`DefaultStartDate` форсирует UTC-kind, аккумулятор работника на месте, `MapSnapshotSize`
считается от сообщения, `GameClient` переносит оба `WaitToReadAsync` между итерациями, outbox
роняет старейший кадр, меню патчит карточки через `card.update`.
Дописано: Дописано:
- `SchoolApiTests.Catalog_WithAnUnknownMod_IsRejected``GET /api/catalog?mods=` с неизвестной - `SchoolApiTests.Catalog_WithAnUnknownMod_IsRejected`
пачкой описан в `docs/protocol.md` как `400 unknown-mod`, но не проверялся. - `SchoolApiTests.ModId_ThatEscapesTheModsFolder_IsRejectedOnBothEndpoints`
- `SchoolApiTests.ModId_ThatEscapesTheModsFolder_IsRejectedOnBothEndpoints` — инвариант 6: id - `SkillGrantTests.ReloadWithoutASavedPick_TakesTheFirstLanguageInsteadOfRolling`
пачки приходит из браузера и подставляется в путь. `ModContent.IsSafePackId` его чистит на обоих
входах (каталог и создание школы), тестом это закреплено не было.
Открыто:
- **Панель «Общие события» пропала.** Фаза 1 требует три области, `design/near-term.md` держит их
в «Что уже зафиксировано», а `design/people.md` описывает список людей как панель «рядом с картой
и событиями». Коммит `d8f8db6` (фаза 8) заменил панель вкладками Карта/Люди, не сказав об этом;
ключи `eventsTitle`/`eventsEmpty` остались в `i18n/strings.ts` мёртвыми. Решать надо одно из
двух: вернуть пустую панель (или третью вкладку) — или записать отказ от неё в дизайн и убрать
ключи. Молча менять экран ревью не стало.
Замечено рядом (за границами среза):
- `SchoolRegistry` после фазы 2 в бою не используется: сервер тикает школы работниками, а из
класса вызывается только `TryNormalizeName`. `SchoolRegistryTests.Tick_AdvancesOnlyRunningSchools`
проверяет путь, которым сервер больше не ходит.
- **`PeopleApiTests.Card_IncludesFamilyAndLiveNeeds` зависит от порядка тестов** (фаза 8, срез 2).
Проверено на чистом checkout `b135a9c` в отдельном worktree: в одиночку класс проходит (8/8),
в полном прогоне класса падает на `Assert.Equal(2, card.Family.Parents.Count)` — ожидалось 2,
пришёл 1. Причина: тест берёт `People[0]` и требует у него двух родителей, а состав семьи
зависит от сида, сид — от id школы, id — от того, сколько школ создали тесты до него (и от
`saves/index.json` на машине). На машине с накопленными сейвами тест зелёный, на чистом
клоне — красный. Чинить надо тест: брать ученика, у которого два родителя, а не первого
попавшегося. Неполные семьи — сознательное решение `design/people.md`, не баг генерации.
- **`GameSocketTests.OpeningASchoolDuringAMathLesson_...` не выдерживает полный прогон** (срез 4).
В одиночку проходит за 1 с, в полном прогоне падает с «No presence frame matched within 40
frames» через 22 с. Похоже на нехватку кадров под нагрузкой, а не на ошибку присутствия.
- Полный `dotnet test` один раз упал: процесс `HSchool.Simulation.Tests` умер с `0xC0000005`,
успев отчитаться о 44 тестах из 63, и два `ActivityTests` (фаза 20) не прошли. Повторить не
удалось. Скорее всего дерево правилось прямо во время прогона (см. ниже).
- На момент проверки в рабочем дереве шла правка фазы 21 (`Decision.cs`, `LessonLearning.cs`,
`LessonLearningSystem.cs`, `PresenceSystem.cs`, `gameScreen.ts`), и какое-то время решение не
компилировалось. Выводам по срезу 1 это не мешает: его файлы правка не трогает, а дописанные
тесты прогнаны и на чистом `b135a9c`, и на дереве после починки сборки.
### Перепроверка на `5a00ad7`, 2026-08-19
Коммит добавил родные языки: `nativeLanguages` в каталоге, `nativeLanguage` в создании школы,
поле в сейве. Проверено по изменению:
- **Инвариант протокола цел.** Версия осталась `7` в обоих местах, `ProtocolCodec.cs` и
`protocol.ts` не тронуты — и правильно: изменение целиком в HTTP/JSON, который документ
объявляет расширяемым. `docs/protocol.md` пополнен тем же коммитом, включая новый код ошибки
`unknown-native-language`, и этот код проверяется тестом.
- **Формат сейва вырос на nullable-поле** без бампа `CurrentFormat`. Старый сейв без поля
загружается сознательно: `NativeLanguages.Pick(..., rollIfOmitted: false)` берёт первый язык
набора, а не бросает кубик, — иначе существующая школа сменила бы язык при первом же старте
сервера и следующий набор пришёл бы другим. Ветка была описана комментарием, но не проверялась.
- Дописанные в первый проход тесты (`unknown-mod` и побег из папки `mods`) проходят на новом коде.
Дописано: `SkillGrantTests.ReloadWithoutASavedPick_TakesTheFirstLanguageInsteadOfRolling`
закрывает всю таблицу `Pick`: старый сейв берёт первый язык, новая школа бросает кубик и бросок
стабилен по сиду, сохранённый выбор уважается, чужой id даёт `null`, из которого создание школы
делает `400`.
### Перепроверка на `418a4ea`, 2026-08-20
Дрейф после `5a00ad7` — это срез 6 (пакеты, сид, формат сейва, тесты экранов) плюс раздельные обеды.
Обещания фаз 0–4 целы; протокол остался v7 в трёх местах, `ProtocolCodec` / `protocol.ts` не трогались.
Что подтверждено повторно:
- **Фаза 0.** `pixi.js` нет; `npm run build` проходит (после правки ниже).
- **Фаза 1.** Дерево, пять секций локации, клиентский фильтр, `localize()`. Отказ от панели событий записан в фазе 1 и в нарративе `design/near-term.md`; в таблице «Что уже зафиксировано» строка всё ещё говорила «панель есть» — поправлена.
- **Фаза 2.** `LongRunning`, аккумулятор с потолком 5, супервизор не трогает `World`, `WorkerFailed` снимает школу с таблицы и не удаляет файл. `ReloadFromDisk_RestoresAPausedClock` и `PausingOneSchool_DoesNotStopAnother` зелёные.
- **Фаза 3.** Все восемь тестов списка на месте в `HSchool.Content.Tests` (86/86). Ванильная карта валидна.
- **Фаза 4.** `GET /api/mods` (core обязателен), каталог, create с картой и отказ на дырявый граф, снимок при OpenSchool, reload раскладки — зелёные. `core` в диалоге создания нельзя снять — теперь ещё и клиентским тестом (`keeps core checked and disabled`, фаза 28).
- **Инварианты и «Things that will bite you».** `Content` без ASP.NET; `DateTime.Now` в симуляции нет; `modal.ts` резолвит явно; `.dialog--screen``height: fit-content`; `DefaultStartDate` UTC; `MapSnapshotSize` от сообщения; outbox `DropOldest`; `WaitToReadAsync` переносится; меню патчит `card.update`.
Исправлено: Исправлено:
- `School` в `src/HSchool.Client/src/net/api.ts` содержал два поля `speedIndex` подряд — артефакт мержа фазы 26 (`edd3d35`). `npm test` (vitest) это не ловит, `npm run build` падал с TS2300. Дубликат убран; поле как было одно. - Два поля `speedIndex` в клиентском `School` (`api.ts`) — артефакт мержа фазы 26; vitest не
- В таблице `design/near-term.md` «События | Панель есть, лента пустая» расходилась с решением фазы 8, которое журнал уже считал закрытым. Строка приведена к вкладочной оболочке. ловил, `npm run build` падал с TS2300.
Замечено рядом (за границами среза):
- `GameSocketTests.OpeningASchoolDuringAMathLesson_...` теперь падает **и в полном прогоне, и в одиночку** (40 кадров присутствия после снятия паузы, `People.Count == 0`). Раньше это был флак под нагрузкой, и тест ослабили до «люди вообще едут». После среза 6 никто не появляется: планы дня собраны для пустого штата, найм в 10:20 на паузе пересобирает таблицу, но явка завязана на `AppearAt` утреннего прихода. Чинить в срезе 5, не здесь.
- `npm test` в клиенте не гоняет `tsc`. Дубликат поля и любой другой TS2300 зелёный в vitest и красный на `build`. Это фаза 28.
## Срез 2. Люди ## Срез 2. Люди
- **Фазы:** 59 - **Фазы:** 59
- **Проверен на:** `bdb1253`, 2026-08-20 (первичная проверка — `5cd5a6d`; предыдущая перепроверка — `5a00ad7`) - **Проверен на:** `bdb1253`, 2026-08-20
- **Пути:** `src/HSchool.People`, `src/HSchool.Content/PeopleDefs.cs`, - **Пути:** `src/HSchool.People`, `src/HSchool.Content/PeopleDefs.cs`,
`src/HSchool.Simulation/RosterSpawner.cs`, `src/HSchool.Simulation/School.cs`, `src/HSchool.Simulation/RosterSpawner.cs`, `src/HSchool.Simulation/School.cs`,
`src/HSchool.Server/Api/SchoolEndpoints.cs`, `src/HSchool.Server/Game/PersonCardReader.cs`, `src/HSchool.Server/Api/SchoolEndpoints.cs`, `src/HSchool.Server/Game/PersonCardReader.cs`,
@@ -152,134 +71,38 @@
`tests/HSchool.Simulation.Tests/PeopleInSchoolTests.cs`, `tests/HSchool.Simulation.Tests/PeopleInSchoolTests.cs`,
`tests/HSchool.AppHost.Tests/PeopleApiTests.cs`, `tests/HSchool.AppHost.Tests/PeopleApiTests.cs`,
`tests/HSchool.AppHost.Tests/SchoolSeedTests.cs` `tests/HSchool.AppHost.Tests/SchoolSeedTests.cs`
- **Итог:** исправлен один тест, который падал на чистом клоне, укреплён второй, дописан один; - **Итог:** обещания фаз 5–9 целы; починены два API-теста, дописан один на reload после набора
расхождение с дизайном одно и то же, что в срезе 1 (раскладка оболочки менеджера)
Что подтверждено: Что подтверждено:
- **Фаза 5.** Все шесть пунктов списка тестов на месте в `PeopleDefTests` (разбор каждого вида, - Дефы человека: разбор, наследование, патч, неизвестная ссылка, склонение, мод со своим набором.
наследование навыка, патч черты, неизвестная ссылка — ошибка каталога, склонение по модели и - Генератор: шесть тестов списка фазы, без Arch/ASP.NET/`DateTime.Now`. Неполные семьи 5–10%.
перекрытие явной таблицей), плюс `ExtraPack_AddsANameSetAndTrait` — критерий «мод со своим Сид — поле школы, не `school.Id`; хостовые тесты, которым нужен состав, передают сид явно.
набором имён виден в каталоге». Наборы имён в `GET /api/catalog` проверены в - Одиннадцать кабинетов, сущности в `World`, `PersistPeople` только при смене состава.
`SchoolApiTests.Catalog_LabelsDefsInTheRequestedLanguage`. Штат при создании пуст — это фаза 11.
- **Фаза 6.** Все шесть тестов из списка фазы существуют и проходят. Критерий «в проекте нет - Список с опубликованного снимка, карточка через `TaskCompletionSource`. Фильтры, пейджер,
сокетов, ECS и `DateTime.Now`» закреплён самими тестами — семейные ссылки, всё через `t(...)`. Вкладка рядом с картой, не нижний ряд — так записано
`Assembly_DoesNotReferenceArchAspNetOrSockets` и `Sources_DoNotUseWallClock`. в фазе 8 и в `design/people.md`.
- **Фаза 7.** Одиннадцать кабинетов (`VanillaMap_HasElevenHomeroomsNotTheComputerLab`), нужды - Выпуск старшей имеющейся, вместимость, семьи через поколения, reload после набора.
тикают со школой, ростер публикуется снимком, `PersistPeople` зовётся только когда состав
изменился — файл не переписывается по таймеру. Ростер, не сходящийся с картой, оставляет школу
незапущенной (`RosterFit.Matches``SchoolContentUnavailableException`).
- **Фаза 8.** Список читает опубликованный снимок (`loop.FindPeople`), карточка идёт через
очередь команд с `TaskCompletionSource` — инвариант 3 цел. Все пять требуемых фазой тестов API
на месте. Клиент: фильтры, сортируемые колонки, пейджер, семейные ссылки, всё через `t(...)`;
в строке списка только лёгкие поля.
- **Фаза 9.** Выпуск старшей имеющейся параллели, сохранение вместимости, семьи через поколения и
уход родителей выпускников покрыты в `YearlyIntakeTests` и `PeopleInSchoolTests`.
- **Дизайн.** `design/people.md` сверен построчно: неполные семьи ≈8% (тест требует 5–10%),
фамилия и отчество отцовские даже у одинокой матери, идентификатор ребёнка — счётчик, места
раздаются вперемешку. Расхождений нет.
Исправлено:
- **`PeopleApiTests.Card_IncludesFamilyAndLiveNeeds` падал на чистом клоне.** Тест брал первого
ученика страницы и требовал у него ровно двух родителей. Состав семьи выводится из сида, а сид —
это id школы (`SchoolWorker.InstallPeople`: `seed = school.Id`), то есть зависит от того,
сколько школ создали тесты до него и что лежит в `saves/index.json`. Замер по сидам 1–30: у 28
первый пятиклассник с двумя родителями, у 2 — с одним, и полный прогон на свежем checkout
`b135a9c` в отдельном worktree ловил как раз такой сид. Одинокие родители — сознательное
решение дизайна, поэтому неправ был тест: теперь он ищет ученика, у которого есть мать, а
количество родителей проверяет диапазоном. Проверено тем же способом, каким ловилось: полный
прогон на чистом `b135a9c` трижды, 60/60.
- **`PeopleApiTests.List_SortsBySurname` мерил не тем.** Тест требовал ordinal-порядка, а сервер
ставит Ё на место Е (`RosterBrowser.NameOrder`, тест `SurnameSort_FollowsTheAlphabetNotCodePoints`).
Совпадали они только потому, что в `core` нет фамилий на Ё — мод с такой фамилией уронил бы
тест, а не код. Теперь тест складывает Ё в Е так же, как сервер.
Дописано: Дописано:
- `PeopleApiTests.ReloadAfterTheYearlyIntake_RestoresTheNewComposition`критерии фаз 7 и 9 - `PeopleApiTests.ReloadAfterTheYearlyIntake_RestoresTheNewComposition`ждёт 1 сентября и
«перезапуск поднимает тех же людей» и «перезапуск после перехода поднимает **новый** состав» не требует состав **после** набора, отличный от исходного (детерминизм от сида иначе скрыл бы
проверялись сквозь сервер. Просто сравнить людей до и после reload недостаточно: генерация потерянный файл).
детерминирована от id школы, поэтому забывший файл сервер собрал бы тот же ростер и тест бы
ничего не поймал. Тест поэтому ждёт 1 сентября (школа создаётся за 15 игровых минут до
полуночи, это около трёх секунд реального времени), а после reload требует состав **после**
набора и явно отличный от исходного.
Открыто:
- **Раскладка оболочки менеджера разошлась с дизайном** — то же расхождение, что записано в срезе
1. Фаза 8 обещает панель «Люди» нижним рядом во всю ширину, `design/people.md` фиксирует
«панель в оболочке менеджера, нижний ряд; не отдельный экран», а на экране она вкладкой рядом с
картой. Тот же коммит `d8f8db6`, что убрал панель событий. Решается одним решением на оба
пункта: либо вернуть нижний ряд, либо переписать обе строки дизайна.
Замечено рядом (за границами среза):
- `design/people.md` и задача фазы 5 говорят «скорость убывания нужд в `core` — ноль», а в
`defs/needs` теперь `decayPerHour` от 0.05 до 0.15. Это сознательная работа фазы 20, не
регрессия; строка дизайна просто устарела и относится к срезу 5.
- Срез 1 успел сдвинуться: коммит `5cd5a6d` тронул `gameScreen.ts` и `strings.ts` (подпись
идущего человека). По правилу журнала он больше не «проверен на `b135a9c`» — правка маленькая и
относится к срезу 5, но следующий проход это увидит.
### Перепроверка на `5a00ad7`, 2026-08-19
`5a00ad7` переписал половину генератора: `PersonSampler` (+266), `FamilyFactory` (+52),
`YearlyIntake` (+61), новый `NativeLanguages`, новые навыки в `core` (языки, социальные,
рабочие). Проверено по изменению:
- Решение записано заранее — `design/people.md` описывает родные языки и порядок выдачи навыков
ещё с `a441ed9`, код догнал документ. Дрейфа нет, это тот редкий случай, когда дизайн шёл первым.
- Восемь новых тестов в `SkillGrantTests` закрывают выдачу навыков по параллелям, отсутствие
рабочих навыков у учеников и набор без переброса уже выданного. Старые тесты детерминизма,
согласованности семьи и годового набора зелёные — генератор переписан, а обещания среза целы.
- Исправления среза 2 (карточка семьи, сортировка по фамилии, состав после набора и перезапуска)
проходят на новом коде.
### Перепроверка на `bdb1253`, 2026-08-20
Дрейф после `5a00ad7` — свой сид школы (фаза 26), золотые ростеры (25), тесты панели (28) и
данные поведения. Обещания фаз 5–9 целы.
Что подтверждено повторно:
- **Фаза 5.** Все пункты списка в `PeopleDefTests` (10/10): разбор, наследование, патч черты,
неизвестная ссылка, склонение и таблица, мод со своим набором.
- **Фаза 6.** Все шесть тестов генератора на месте, плюс `NewSchool_HasNoStaff` (фаза 11 не
отменяет детерминизм и заполнение мест). `Assembly_DoesNotReferenceArchAspNetOrSockets` и
`Sources_DoNotUseWallClock` зелёные. Неполные семьи 5–10% — `ReviewFixTests`.
- **Фаза 7.** Одиннадцать кабинетов, сущности в `World`, `PersistPeople` только при изменении
состава. Штат при создании пуст — это фаза 11, тест обновлён (`LeaveJobsEmpty`).
- **Фаза 8.** Список с опубликованного снимка (`FindPeople`), карточка через `TaskCompletionSource`.
Пять API-тестов плюс клиентские фильтры/пейджер (`peoplePanel.test.ts` 3/3). Вкладка, не нижний ряд.
- **Фаза 9.** Выпуск старшей имеющейся, вместимость, семьи через поколения, reload после набора
(`ReloadAfterTheYearlyIntake_RestoresTheNewComposition`).
- **Сид.** Больше не `school.Id`. Новая школа берёт `_createSeed`, сейв — `loaded.Seed`; запасной
`school.Id` только если `people.json` нет. `Card_IncludesFamilyAndLiveNeeds` передаёт `seed: 1`.
`SchoolSeedTests`: тот же сид — те же люди, два без сида — разные, reload хранит выпавший.
Исправлено: Исправлено:
- В `design/people.md` скорость нужд всё ещё была «ноль» (нарратив, совет и таблица), хотя срез 5 - `Card_IncludesFamilyAndLiveNeeds` брал `People[0]` и требовал двух родителей. Состав зависит
это отменил. Помечено как отменённое со ссылкой на `ai.md`, по тому же шаблону, что расписание. от сида; на чистом клоне первый ученик иногда из неполной семьи. Теперь ищет ученика с матерью
Код не трогался. и проверяет диапазон.
- `List_SortsBySurname` требовал ordinal-порядок, сервер ставит Ё на место Е. Совпадали, пока
Закрыто из прошлого прохода: в `core` не было фамилий на Ё.
- **Раскладка оболочки.** «Открыто» выше больше не действует: `design/people.md` и фаза 8 уже
записаны под вкладку рядом с картой. Решение принято в том же ходе, что отказ от панели событий.
Замечено рядом:
- «Заведомо не сейчас» в `people.md` всё ещё говорит «должности закрываются генератором» —
отменено срезом 3. Не переписывал исторический список стопов.
People 34, PeopleDef 10, PeopleInSchool 7, PeopleApi+SchoolSeed 13, клиентская панель 3/3.
## Срез 3. Штат ## Срез 3. Штат
- **Фазы:** 1013 - **Фазы:** 1013
- **Проверен на:** `bdb1253`, 2026-08-20 (первичная проверка — `21d79cb`; предыдущая перепроверка — `5a00ad7`) - **Проверен на:** `bdb1253`, 2026-08-20
- **Пути:** `src/HSchool.People/Staffing.cs`, `src/HSchool.People/ApplicantPool.cs`, - **Пути:** `src/HSchool.People/Staffing.cs`, `src/HSchool.People/ApplicantPool.cs`,
`src/HSchool.Content/SubjectDef` и `defs/staffing`, `src/HSchool.Server/Api/SchoolEndpoints.cs` `src/HSchool.Content/SubjectDef` и `defs/staffing`, `src/HSchool.Server/Api/SchoolEndpoints.cs`
(staffing/hire/subjects), `src/HSchool.Server/mods/core/defs/{subjects,positions,rooms,staffing,traits}`, (staffing/hire/subjects), `src/HSchool.Server/mods/core/defs/{subjects,positions,rooms,staffing,traits}`,
@@ -287,131 +110,40 @@ People 34, PeopleDef 10, PeopleInSchool 7, PeopleApi+SchoolSeed 13, клиент
`tests/HSchool.People.Tests/{StaffingTests,ApplicantPoolTests}.cs`, `tests/HSchool.People.Tests/{StaffingTests,ApplicantPoolTests}.cs`,
`tests/HSchool.AppHost.Tests/StaffingApiTests.cs`, `tests/HSchool.AppHost.Tests/StaffingApiTests.cs`,
`src/HSchool.Client/src/ui/managementPanel.test.ts` `src/HSchool.Client/src/ui/managementPanel.test.ts`
- **Итог:** дописано 3 теста (один из них — прямо требуемый фазой 12 и отсутствовавший), - **Итог:** обещания фаз 10–13 целы; дописаны 3 теста; `docs/protocol.md` дополнен `?lang=`
дополнен `docs/protocol.md`; расхождений кода с дизайном не найдено
Что подтверждено: Что подтверждено:
- **Фаза 10.** `PETeacher` в `core` нет, `Classroom`, `ComputerLab` и `GymHall` не открывают - `PETeacher` нет; кабинеты без учительской должности; места через `defaultSeats`.
должностей — должности остались у библиотеки, приёмной, столовой, медкабинета и кабинета Неизвестный навык в предмете роняет каталог. Стартовая дата 31 марта 2012.
директора, ровно как обещано. Кабинет описан числом мест (`seatThing`/`defaultSeats`, пустой - Пул: детерминизм по сиду и неделе, частичное обновление, устойчивый размер, «сильный просит
список слотов — проверено в `Catalog_LabelsDefsInTheRequestedLanguage`), редактор карты больше», пустой штат, соискатель вне ростера до найма. Пул 32.
показывает у него одно поле «мест». Неизвестный навык в предмете роняет каталог - Фонд оплаты, деление часов, пол ставки, покрытие. `GET /staffing` читает снимок, наём и
(`Subject_UnknownSkill_FailsTheCatalog`), полнота локалей — `EveryVanillaLabel_ExistsInBothLocales`, назначение — `GameCommand` с `TaskCompletionSource`. `MaxWeeklyHours` не запрещает назначение,
стартовая дата 31 марта 2012 зашита в тестах API. а оставляет предмет непокрытым. `PrimarySchool` на ванили требует трёх учителей (`teachersShort`).
- **Фаза 11.** Все шесть тестов из списка фазы на месте и проходят: детерминизм пула по сиду и - Экран: деньги, непокрытые, пул, штат, назначение с карточки, отказ по пределу текстом,
неделе, частичное обновление с сохранением навыков и запроса, устойчивый размер за пятьдесят всё через `t(...)`.
недель, «сильный просит больше», пустой штат при полной школе, соискатель вне ростера до найма. - Полностью укомплектованная школа влезает в 100 000 впритык (запас 5–13%). Нанять пул целиком
- **Фаза 12.** Фонд оплаты, деление часов между учителями предмета, пол одной ставки и покрытие без назначений предела достигает. Раскладка «каждому все предметы» — самая дешёвая, не
предметов покрыты в `StaffingTests`. `GET /staffing` читает опубликованный снимок «укомплектованная школа».
(`loop.FindPeople`), наём и назначение идут в воркер через `GameCommand` с
`TaskCompletionSource` — инвариант 3 цел. Перезапуск с тем же штатом и назначениями —
`AssignAndUnassign_HonourTheCap_AndReloadKeepsAssignments`.
- **Фаза 13.** Строка денег, непокрытые предметы, список соискателей со ставкой и сильными
навыками, кнопка найма с текстом ошибки, список штата с предметами и стоимостью, назначение и
снятие с карточки — всё есть и всё через `t(...)`.
- **Дизайн.** `design/staffing.md` сверен построчно. Особо проверено: `MaxWeeklyHours` не
запрещает назначение, а делает предмет непокрытым (в коде нет такой ошибки, и замер это
подтверждает); цена часа считается от навыков **и от черт**.
Дописано: Дописано:
- `StaffingApiTests.AssigningPastTheCap_IsRejectedWithTheNumbers` — фаза 12 требует теста API на - `StaffingApiTests.AssigningPastTheCap_IsRejectedWithTheNumbers` — фаза 12 требовала API-тест
«отклонение по пределу», а его не было: единственное место, где он мог сработать, было написано на отказ по пределу; единственное место было написано через `if (over.StatusCode == Conflict)`
через `if (over.StatusCode == Conflict) … else …` и проходило при любом исходе. Пределом и проходило при любом исходе.
занялся замер: при выделенных 100 000 полностью загруженная школа стоит всего 68–72 тысячи, - `ApplicantPoolTests.ConfidentTraits_AskForMoreAndQuietOnesForLess`
то есть «нанять всех» предела не достигает. Но пик фонда приходится на середину раздачи - В `AssignAndUnassign_HonourTheCap_AndReloadKeepsAssignments` — тот же пул с теми же ставками
предметов — пока часы предмета несут один-два учителя, а не двенадцать, — и там отказ после перезапуска.
случается: по сидам 1–30 отказов от 4 до 24 на прогон, ни одного сида без отказа. Тест
раздаёт предметы штату, ловит первый `409`, сверяет `payroll-exceeded`, `allocated`,
`attempted > allocated`, `remaining` и то, что отклонённое назначение действительно не легло.
- `ApplicantPoolTests.ConfidentTraits_AskForMoreAndQuietOnesForLess` — дизайн фиксирует «цена
часа: от навыков и от черт (самоуверенный просит больше)», код это делает через
`TraitDef.WageAsk``core` Leader +10, HotTempered +6, Quiet 8), а тестом закреплена была
только половина про навыки.
- Проверка в `AssignAndUnassign_HonourTheCap_AndReloadKeepsAssignments`, что после перезапуска
возвращается **тот же пул** с теми же ставками — критерий готовности фазы 11, который никто не
проверял. Тест кусается: пул на диске уже без нанятого, а сгенерированный заново вернул бы его
обратно.
Исправлено: Исправлено:
- `docs/protocol.md` не упоминал `?lang=ru|en` у `POST staff/hire`, `POST …/subjects` и - `docs/protocol.md` не упоминал `?lang=` у hire/subjects, хотя клиент его шлёт.
`DELETE …/subjects/{subject}`, хотя все три его принимают и клиент его шлёт. Поведение не
трогалось — дописана документация.
Замечено рядом (за границами среза):
- Предел фонда при ванильных данных почти не ограничивает игрока: нанять всех двенадцать и
раздать им все предметы стоит около 70 000 из 100 000. Отказ достижим только по дороге, на
пике неравномерной нагрузки. Это игровой баланс, а не ошибка (см. коммит `207e7ec`, которым
предел поднимали намеренно), но если предел задумывался как настоящее ограничение — числа
сейчас говорят обратное.
- В `AssignAndUnassign_HonourTheCap_AndReloadKeepsAssignments` осталась ветка `if/else` вокруг
ожидаемого отказа. Теперь, когда отказ проверяется отдельным тестом, эта ветка просто не может
упасть — её стоит однажды упростить, но трогать работающий тест ради этого ревью не стало.
### Перепроверка на `5a00ad7`, 2026-08-19 — **вывод про бюджет исправлен**
`5a00ad7` поднял `poolSize` с 12 до 32. Это не косметика: предел фонда начал работать, и запись
выше про «предел почти не ограничивает игрока» больше не описывает игру. Перемерено на шести
сидах:
| что делаем | сколько людей | фонд из 100 000 | непокрыто |
| --- | --- | --- | --- |
| нанимать подряд, не назначая предметов | 20–21 из 32 | 95 00099 400, дальше отказ | всё |
| по учителю на предмет | 12 | 86 80094 700 | `PrimarySchool` |
| закрыть и его | 14 | 87 10095 100 | ничего |
То есть полностью укомплектованная школа теперь **влезает, но впритык** — запас 513%, и
`PrimarySchool` (1–4 классы, 80 часов в неделю против потолка в 36 на человека) требует трёх
учителей, о чём игрок узнаёт только из списка непокрытых.
Прежняя цифра «около 70 000 из 100 000» получена на вырожденной раскладке — когда каждому из
двенадцати назначены все двенадцать предметов. Так часы предмета делятся на всех и почти каждый
садится на пол одной ставки; это самая **дешёвая** конфигурация, а не «полностью
укомплектованная школа». Ошибка была в замере, а не в коде.
Ставки соискателей при этом не изменились (в среднем 57.9–59.3 против прежних 58.5–62.5), хотя
навыков у человека стало больше — цена часа считается от среднего по навыкам, и среднее устояло.
`Hire_IdlePool_FitsUnderTheCap` в том же коммите переписан так, чтобы принимать отказ по пределу
как штатный исход. Это правильно: с пулом в 32 человека нанять всех и остаться в пределах суммы
уже нельзя.
### Перепроверка на `bdb1253`, 2026-08-20
Дрейф после `5a00ad7``teachersShort` (фаза 25), свой сид, удостоверение пака. Обещания фаз 10–13
целы; новых тестов не дописывал, расхождений с `design/staffing.md` нет.
Что подтверждено повторно:
- **Фаза 10.** `PETeacher` нет, кабинеты без учительской должности, места через `defaultSeats`,
редактор показывает у homeroom одно поле «мест». `Subject_UnknownSkill_FailsTheCatalog`,
стартовая дата 31 марта 2012.
- **Фаза 11.** Все шесть тестов пула на месте (`SameSeedAndWeek`, частичное обновление, 50 недель,
сильный просит больше, пустой штат, соискатель вне ростера). Пул 32, reload хранит ставки.
- **Фаза 12.** `Hire_TwoPeopleAtThreeAndSevenThousand_RejectsTheThird` на фикстуре 10 000.
`AssigningPastTheCap_IsRejectedWithTheNumbers` на API. `GET /staffing` читает снимок,
наём/назначение — `TaskCompletionSource`. `MaxWeeklyHours` не запрещает назначение
(`SubjectNeedingMoreHoursThanOnePersonCarries_StaysUncovered`). Ветка `if/else` вокруг предела
убрана.
- **Фаза 13.** Деньги, непокрытые, пул, штат, назначение с карточки, всё через `t(...)`.
Отказ по пределу текстом — `managementPanel.test.ts`.
- **Покрытие.** `PrimarySchool` на ванили требует трёх учителей (`TeachersShort == 3` в API и в
`PrimarySchool_OnVanillaMap_NeedsThreeTeachers_AndASecondDoesNotCloseIt`). Это фаза 25, не
ломает фазу 12: список непокрытых был и раньше, появилось число.
Замечено рядом:
- `people.md` «Заведомо не сейчас» всё ещё говорит, что должности закрывает генератор — то же, что
в срезе 2. Исторический стоп среза 2, не трогал.
Staffing+ApplicantPool 18, Content 3, StaffingApi 9.
## Срез 4. Расписание ## Срез 4. Расписание
- **Фазы:** 1417 - **Фазы:** 1417
- **Проверен на:** `bdb1253`, 2026-08-20 (первичная проверка — `21d79cb`; предыдущая перепроверка — `5a00ad7`) - **Проверен на:** `bdb1253`, 2026-08-20
- **Пути:** `src/HSchool.Schedule`, `src/HSchool.Content` (каркас дня, каникулы, `SubjectDef.room`), - **Пути:** `src/HSchool.Schedule`, `src/HSchool.Content` (каркас дня, каникулы, `SubjectDef.room`),
`src/HSchool.Simulation/SchoolTimetables.cs`, `src/HSchool.Server/Api/TimetableEndpoints.cs`, `src/HSchool.Simulation/SchoolTimetables.cs`, `src/HSchool.Server/Api/TimetableEndpoints.cs`,
`src/HSchool.Server/Game/SchoolWorker.cs` (пин, перестроение), `src/HSchool.Server/Game/SchoolWorker.cs` (пин, перестроение),
@@ -420,238 +152,69 @@ Staffing+ApplicantPool 18, Content 3, StaffingApi 9.
`tests/HSchool.Content.Tests/LunchBreakTests.cs`, `tests/HSchool.Content.Tests/LunchBreakTests.cs`,
`tests/HSchool.AppHost.Tests/TimetableApiTests.cs`, `tests/HSchool.AppHost.Tests/TimetableApiTests.cs`,
`src/HSchool.Client/src/ui/timetableGrid.test.ts` `src/HSchool.Client/src/ui/timetableGrid.test.ts`
- **Итог:** дописан один тест на пять непокрытых кодов отказа, поправлены две устаревшие строки - **Итог:** обещания фаз 14–17 целы; дописан тест на коды отказа пина; две строки дизайна
дизайна; расхождений кода с принятыми решениями нет помечены как отменённые срезом 5
Что подтверждено: Что подтверждено:
- **Фаза 14.** Все пункты критерия закрыты в `CalendarTests`: суббота, каникулы и ночь дают «вне - Календарь: суббота, каникулы и ночь «вне учебного дня»; 10:20 вторника — третий урок;
учебного дня», 10:20 буднего дня — третий урок, шестидневка делает субботу рабочей, зимние шестидневка; зима через 1 января. Физкультура в спортзале. Обеденные смены в каркасе дня
каникулы переносятся через 1 января. Требование помещения у предмета есть (физкультура — (младшие после 3-го, старшие после 4-го).
спортзал), неизвестная комната роняет каталог. - Планировщик: четыре запрета, класс не влезает в меньший кабинет, без Arch/ASP.NET/`DateTime.Now`.
- **Фаза 15.** Все шесть тестов из списка фазы на месте. Четыре запрета проверяются одним - Перестроение только при `PeopleChanged` + `TimetableDirty`, не по тику. `GET /timetable`
утверждением в `TeacherWithTwoSubjects_IsNeverInTwoRoomsAtOnce` (уникальность по учителю, классу читает снимок, пин — `TaskCompletionSource`. `Reload_RestoresLockedLessons`.
и кабинету в слоте) плюс `ClassOfSixteen_DoesNotEnterALabOfTwelve`. Отсутствие сокетов, ECS и - Сетка, закрепление, тексты отказа (`pin-rejected` / `no-teacher` / `unknown-lesson`),
часов реального времени — самопроверкой сборки, как в `HSchool.People`. скобки в дереве из присутствия, личное расписание в карточке. Всё через `t(...)`.
- **Фаза 16.** Расписание пересобирается только когда изменился состав: `TimetableDirty` ставится - Занятость узла в протоколе v7 живёт в кадре `Presence`, не в снимке карты — фаза 19.
в `InstallPeople`/`Replace` и в годовом наборе, а воркер перестраивает таблицу под
`if (result.PeopleChanged)` — по тику этого не происходит. `GET /timetable` читает
опубликованный снимок, пин и снятие идут через `GameCommand` с `TaskCompletionSource`.
Перезапуск с закреплениями — `Reload_RestoresLockedLessons`.
- **Фаза 17.** Скобки в дереве (`Математика · 5Б`) собираются из присутствия, сетка «день × урок»
с переносом, сменой кабинета и отметкой «закреплено» есть, отказ планировщика показывается
текстом (`timetableErrorPin`, `timetableErrorNoTeacher`, `timetableErrorLesson`), личное
расписание — в карточке. Всё через `t(...)`. Клиентские тесты и сборка на `21d79cb`: 33/33 и
зелёная сборка.
Дописано: Дописано:
- `TimetableApiTests.PinAndUnpinRefusals_CarryTheDocumentedCodes`из шести кодов отказа пина в - `TimetableApiTests.PinAndUnpinRefusals_CarryTheDocumentedCodes``no-teacher`, `unknown-class`,
`docs/protocol.md` тестом был закрыт только `pin-rejected` (хвост `unknown-subject`, `unknown-room`, `unknown-lesson`, `invalid-query`.
`PinThenHireAnother_KeepsTheLockedSlot`, попытка провести урок в спортзале). Остальные пять —
`no-teacher`, `unknown-class`, `unknown-subject`, `unknown-room`, `unknown-lesson` на повторном
снятии, плюс `invalid-query` на неполном запросе — не проверялись, хотя клиент по ним
переключает текст ошибки.
Исправлено: Исправлено:
- `design/schedule.md` держал в «Зафиксировано» две строки, которые срез 5 сознательно отменил: - В `design/schedule.md` «панель локации из расписания» и «люди находятся по расписанию» помечены
«Панель локации заполняется из расписания» и «Люди находятся по расписанию; ходьбы нет». как отменённые срезом 5.
Отмена записана в `design/ai.md` («присутствие — состояние человека, а не функция от
расписания»), а старый документ об этом не знал. Помечены как отменённые со ссылкой; код не
трогался — он давно живёт по новому решению.
Замечено рядом (за границами среза):
- Задача фазы 16 «снимок карты отдаёт по узлу, что там сейчас идёт и у кого» в протоколе v7
выглядит иначе: снимок карты стал статическим, а занятость уехала в кадр `Presence`. Это
сделано фазой 19 намеренно и описано в `docs/protocol.md`; строка фазы 16 просто историческая.
- `GameSocketTests.OpeningASchoolDuringAMathLesson_...`, который в срезе 1 падал дважды под
нагрузкой, за этот проход не воспроизвёлся ни разу: шесть полных прогонов подряд на чистом
worktree и два одиночных под искусственной нагрузкой из шести жгущих процессов — зелено.
Оставляю как «наблюдалось, не воспроизводится»; если вернётся, смотреть на выдачу кадров
присутствия под нехваткой процессора, а не на логику.
Про окружение:
- Во время проверки параллельная сессия правила `core` и клиент (размер пула соискателей 12 → 32,
новые навыки, диалог соискателей). Два прогона на рабочем дереве успели показать «падения»
`Hire_IdlePool_FitsUnderTheCap` и `GetStaffing_EmptySchool_HasPoolAndNoPayroll` — это артефакт
прогона старых бинарников против новых данных, а не находка. Поэтому весь срез проверялся в
отдельном worktree на `21d79cb`: AppHost 63/63 (три прогона подряд), клиент 33/33 и сборка.
### Перепроверка на `5a00ad7`, 2026-08-19
Из путей среза коммит тронул только каталог навыков: пять строк ушли из `skills/academic.jsonc`,
появились `languages`, `social` и `work`. Предметы ссылаются на навыки долями, и обе защиты на
месте и зелёные — `Subject_UnknownSkill_FailsTheCatalog` и `EveryVanillaLabel_ExistsInBothLocales`.
Планировщик, каркас дня и API расписания не менялись; дописанный в прошлый проход тест на коды
отказа пина проходит.
### Перепроверка на `bdb1253`, 2026-08-20
Дрейф после `5a00ad7` — обеденные смены в каркасе дня (`SchoolDay` / `CalendarDefs`) и сид/дамп в
воркере. Планировщик и API расписания не менялись. Обещания фаз 14–17 целы; тестов не дописывал.
Что подтверждено повторно:
- **Фаза 14.** Суббота, каникулы и ночь — «вне учебного дня»; 10:20 вторника — третий урок;
шестидневка делает субботу рабочей; зима через 1 января. Физкультура в спортзале.
Обеденные смены уже стоят в `design/schedule.md`: младшие после 3-го, старшие после 4-го,
обе перемены длинные (`LunchBreakTests`).
- **Фаза 15.** Все шесть тестов планировщика на месте, плюс самопроверка сборки без Arch/ASP.NET
и `DateTime.Now`.
- **Фаза 16.** Перестроение только при `PeopleChanged` + `TimetableDirty`, не по тику.
`GET /timetable` читает снимок, пин — `TaskCompletionSource`. `Reload_RestoresLockedLessons`,
`PinAndUnpinRefusals_CarryTheDocumentedCodes`.
- **Фаза 17.** Сетка, закрепление, тексты отказа — `timetableGrid.test.ts` (pin-rejected /
no-teacher / unknown-lesson). Всё через `t(...)`.
Замечено рядом:
- `GET /api/catalog` по-прежнему отдаёт каркас без `lunchBreaks` (только `longBreakAfter`).
Сетке расписания смены не нужны — они для решений. Молча расширять HTTP ревью не стало.
- `OpeningASchoolDuringAMathLesson_...` с этой перепроверки падает и в одиночку — см. срез 1;
это явка, не раскладка часов.
Calendar+обеды 20, планировщик 9, TimetableApi 6.
## Срез 5. Жизнь школы ## Срез 5. Жизнь школы
- **Фазы:** 1821 - **Фазы:** 1821
- **Проверен на:** `cbe739a`, 2026-08-20 (первичная проверка — `21d79cb`; предыдущая перепроверка — `5a00ad7`; дерево до коммита правок — `450495f`) - **Проверен на:** `cbe739a`, 2026-08-20
- **Пути:** `src/HSchool.Ai`, `src/HSchool.Simulation/{PresenceSystem,ActivitySystem,LessonLearningSystem,School}.cs`, - **Пути:** `src/HSchool.Ai`, `src/HSchool.Simulation/{PresenceSystem,ActivitySystem,LessonLearningSystem,School}.cs`,
`src/HSchool.Protocol` (кадр `Presence`, пропуск в кадре часов), `src/HSchool.Protocol` (кадр `Presence`, пропуск в кадре часов),
`src/HSchool.Server/Game/PresenceFrame.cs`, `src/HSchool.Client/src/ui/gameScreen.ts`, `src/HSchool.Server/Game/PresenceFrame.cs`, `src/HSchool.Client/src/ui/gameScreen.ts`,
`src/HSchool.Server/mods/core/defs/{actions,behavior,needs}`, `tests/HSchool.Ai.Tests`, `src/HSchool.Server/mods/core/defs/{actions,behavior,needs}`, `tests/HSchool.Ai.Tests`,
`tests/HSchool.Simulation.Tests/{PresenceTests,DecisionTests,ActivityTests,NeedDecayTests,LunchTests}.cs`, `tests/HSchool.Simulation.Tests/{PresenceTests,DecisionTests,ActivityTests,NeedDecayTests,LunchTests}.cs`,
`tests/HSchool.AppHost.Tests/GameSocketTests.cs` `tests/HSchool.AppHost.Tests/GameSocketTests.cs`
- **Итог:** все 32 теста из списков четырёх фаз на месте; дописаны 2 теста явки после смены таблицы, - **Итог:** все 32 теста из списков фаз на месте; дописаны 2 теста явки; `SetTimetable` сбрасывает
`SetTimetable` сбрасывает планы дня, хостовый тест ждёт `Clock.Running` после снятия паузы. планы дня. Голод закрыт данными. AppHost после правки не гонялся — Aspire держал DLL.
Голод из первого прохода уже закрыт данными. AppHost не гонялся — Aspire держит DLL.
Что подтверждено: Что подтверждено:
- **Фаза 18.** Все 13 требуемых тестов есть: маршрут 201 → санузел через лестницу, совпадение - Ходьба: 13 тестов списка фазы 18 (маршрут, `travelMinutes`, воскресенье/каникулы, звонок,
суммы `travelMinutes` с реально пройденным временем, пустая школа в воскресенье и в каникулы, детерминизм, пропуск пустого времени, лето, промотанная неделя).
класс без уроков не приходит, ученик в кабинете к звонку, спортзал впритык к перемене, - Присутствие: раскладка с обеих сторон, круговой прогон, люди не в снимке карты, пропуск в
детерминизм по сиду, сохранение посреди перемены, четыре сценария пропуска пустого времени, учебное время отбрасывается. На паузе кадры **идут** (occupancy + подписи урока); людей в них
школа без учителей, лето с первым сентября, промотанная неделя против прожитой. может не быть.
- **Фаза 19.** Все пять тестов: байтовая раскладка присутствия с обеих сторон, круговой прогон, - Нужды: декей, обед, стулья, ночь вне школы, неизвестная нужда, детерминизм.
отсутствие людей и урока в снимке карты, разрешение на пропуск в кадре часов, отброшенный `Hunger` восстанавливается вне школы, в столовой 112 стульев, обед — смена по параллелям.
пропуск в учебное время. `SleepAndHunger_ReturnToMaxOffCampus`, `AfterASchoolDay_NobodyIsLeftStarving`.
- **Фаза 20.** Все шесть: декей голода за час, обед на заявленные минуты и величину, нехватка - Решения: восемь тестов фазы 21, в том числе туалет с урока и возврат, потолок отложенных,
стульев, ночь вне школы, действие с неизвестной нуждой роняет каталог, детерминизм нужд. детерминизм за неделю. `HSchool.Ai` без Arch/ASP.NET/`DateTime.Now`.
- **Фаза 21.** Все восемь, включая сквозной `ZeroToilet_LeavesClass_ReachesRestroom_AndReturns`,
`DecisionCap_DefersTheOverflow` и `SameSeedAndActions_MatchAfterAWeek`, плюс табличный разбор
решений в `DecisionPlannerTests` без мира.
- **Инварианты.** `HSchool.Ai` не знает Arch, ASP.NET и часов реального времени, `HSchool.Simulation`
не знает сокетов — обе проверки в самих тестах. Протокол v7 сходится в трёх местах.
Измерено (временный тест, удалён после):
Полностью укомплектованная ванильная школа (12 учителей, 225 уроков, 176 учеников), прожитая
тиками с вторника по пятницу:
| | день 1 | день 2 | день 3 | день 4 |
| --- | --- | --- | --- | --- |
| побывали в столовой | 0 | 144 | 133 | 141 |
| завершённых обедов | 0 | 75 | 132 | 111 |
| минимальный голод | 0.32 | 0.00 | 0.00 | 0.00 |
Картина дня в остальном осмысленная: в 8:25 двор, вестибюль и лестницы полны, к 8:45 все 144
пришедших в кабинетах и спортзале, на перемене коридор — самый людный узел школы (пик 112),
после уроков остаются только те, у кого урок ещё идёт.
- **Столовая пустует весь первый день.** Голод стартует с 1.0 и падает на 0.1 в игровой час
только на территории школы, так что порог 0.35 достигается лишь к концу первого дня. Критерий
готовности фазы 21 обещает «перемены с коридором и столовой» — со второго дня так и есть, но
прогон одного дня, как в критерии написано, столовой не покажет. Это не поломка, это то, чего
стоит ожидать при проверке руками.
- **Со второго дня голод упирается в ноль и оттуда не выходит.** К концу четырёх учебных дней
27 учеников из 176 держат голод ≤0.05, 98 из 176 — ниже порога срочности, средний по школе
ровно 0.35. Причина арифметическая: вне школы голод не восстанавливается (в отличие от сна),
дома никто не ест, а единственный источник еды — восемь стульев в столовой на 176 человек при
обеде в 15 минут. Обедов в день выходит 75–132, то есть до половины школы ежедневно остаётся
без еды, и дефицит переносится на завтра. Поскольку рост навыка на уроке зависит от состояния
нужд, больше половины школы учится со штрафом постоянно. Это баланс, а не логика: числа лежат
в `core` (`decayPerHour`, `needGain`, число стульев на карте) и правятся данными. Молча их
менять ревью не стало — см. «Открыто».
Открыто:
- **Чем закрывать голод.** Вариантов три, и это решение автора, а не ревью: восстанавливать голод
вне школы, как сон (`restoredOffCampus` на `Hunger`); поставить в ванильную столовую больше
стульев; или снизить `decayPerHour` голода. Первое честнее всего описывает «поел дома», третье
дешевле всего, второе меняет карту. Числа выше дают точку отсчёта для любого из них.
Замечено рядом (за границами среза):
- **`GameSocketTests.OpeningASchoolDuringAMathLesson_...` подтверждённо нестабилен.** За сессию
он упал четырежды — и все четыре раза в полном `dotnet test` по решению, когда параллельно шла
сборка; в одиночку он проходит всегда, в том числе шесть прогонов подряд и два под нагрузкой из
шести жгущих процессов. Симптом всегда один: последнее ожидание после снятия паузы — «нанятый
учитель появился среди людей в кадре присутствия» — не выполняется за 40 кадров (~20 секунд).
Механизм не пойман: инструментированный прогон, печатающий замороженное время и уроки этого
учителя, три раза подряд прошёл зелёным. Две правдоподобные версии, обе про то, что тест
зависит от игрового времени, набежавшего за время его собственных HTTP-вызовов (5 игровых минут
на реальную секунду): либо замороженный момент уезжает за последний урок учителя и он по плану
дня уже не приходит, либо за 20 секунд ожидания школьный день успевает кончиться. В обоих
случаях чинить надо тест, а не присутствие: занятость узла берётся из расписания, а явка — из
состояния, и совпадать они не обязаны — фаза 21 прямо обещает, что «опоздание и отсутствие
видны».
### Перепроверка на `5a00ad7`, 2026-08-19
`School.cs` изменился на десять строк — родной язык прокинут в `InstallPeople`. Присутствие,
ходьба, действия и решения не тронуты, `decayPerHour` и `needGain` тоже, поэтому измеренная выше
картина с голодом остаётся в силе: вопрос «чем закрывать голод» по-прежнему открыт.
### Перепроверка на `450495f`, 2026-08-20
Дрейф после `5a00ad7` — обеденные смены, веса в `BehaviorDef` (фаза 24), `Decision.cs`,
`PresenceSystem` (очередь после действий). Обещания фаз 18–21 целы; протокол остался v7.
Что подтверждено повторно:
- **Фаза 18.** Все 13 тестов списка на месте в `WalkingTests` / `PresenceTests`. Пустая школа
без учителей в рабочее окно не пропускается. `EnsurePlans` пересобирал планы только при смене
дня или численности — после пина или назначения предмета без найма `AppearAt` оставался `null`.
- **Фаза 19.** Раскладка присутствия с обеих сторон (`Presence_RoundTrips`, `protocol.test.ts`),
снимок карты без людей, пропуск в учебное время отбрасывается. Кадр часов несёт разрешение на
пропуск; клиент его не считает.
- **Фаза 20.** Декей голода, обед, стулья, ночь вне школы, неизвестная нужда, детерминизм.
`SleepAndHunger_ReturnToMaxOffCampus` и `AfterASchoolDay_NobodyIsLeftStarving` закрывают
открытый в первом проходе голод.
- **Фаза 21.** Восемь тестов списка плюс обеденные веса в `DecisionPlannerTests`. `HSchool.Ai`
без Arch/ASP.NET/`DateTime.Now`, `Simulation` без сокетов.
Исправлено:
- `School.SetTimetable` обнуляет `PlanDay`. Иначе `EnsurePlans` считает планы актуальными,
пока не сменятся день или численность, и после назначения математики в 10:20 никто не приходит:
вчерашний план собран по пустой таблице, `AppearAt == null`. Найм это маскировал (численность
растёт), пин и назначение — нет.
- `OpeningASchoolDuringAMathLesson_...` после снятия паузы сразу ждал людей в кадре. Присутствие
уходит и на паузе (пустые люди, подписи урока из таблицы), и 40 кадров бюджета уходили в эти
кадры до обработки `SetRunning`. Теперь тест ждёт `clock.Running`, потом людей. Сам хост
с Aspire в этой сессии не гонялся (MSB3021).
Дописано: Дописано:
- `HireAndAssignMath_DuringLessonThree_PutsSomeoneOnCampus` — найм на паузе в 10:20, потолок - `HireAndAssignMath_DuringLessonThree_PutsSomeoneOnCampus`
64 решения, шаг 20 Гц, как у работника. - `AssigningMath_AfterDayPlansWereBuilt_PutsSomeoneOnCampus`
- `AssigningMath_AfterDayPlansWereBuilt_PutsSomeoneOnCampus` — назначение без смены численности;
без сброса `PlanDay` кампус остаётся пустым.
Закрыто из прошлого прохода: Исправлено:
- **Голод.** Уже закрыто 2026-08-19 (`restoredOffCampus`, стулья 112, смены). Повторно зелёные - `School.SetTimetable` обнуляет `PlanDay`. Иначе после назначения предмета без смены численности
`NeedDecayTests.SleepAndHunger_ReturnToMaxOffCampus` и `LunchTests.AfterASchoolDay_NobodyIsLeftStarving`. `EnsurePlans` считает планы актуальными, `AppearAt` остаётся `null`, кампус пуст. Найм это
- **`OpeningASchoolDuringAMathLesson_...`.** Это была явка плюс чтение паузных кадров, не флак маскировал (численность растёт).
нагрузки. Симуляция после правки зелёная; хостовый прогон — когда Aspire отпустят DLL. - `OpeningASchoolDuringAMathLesson_...` после снятия паузы сразу ждал людей и съедал бюджет на
паузные кадры. Теперь ждёт `clock.Running`, потом людей.
Ai 29, Presence+Decision+Activity+NeedDecay+Lunch 31, Presence_RoundTrips 1.
## Срез 6. Фундамент ## Срез 6. Фундамент
@@ -664,87 +227,33 @@ Ai 29, Presence+Decision+Activity+NeedDecay+Lunch 31, Presence_RoundTrips 1.
`tests/HSchool.Content.Tests/{PackIdentityTests,ExamplePackTests,BehaviorDefTests}.cs`, `tests/HSchool.Content.Tests/{PackIdentityTests,ExamplePackTests,BehaviorDefTests}.cs`,
`tests/HSchool.People.Tests/GoldenRosterTests.cs`, `tests/HSchool.People.Tests/GoldenRosterTests.cs`,
`tests/HSchool.AppHost.Tests/{ExamplePackTests,GoldenSaveTests,SchoolSeedTests,ServiceabilityTests}.cs` `tests/HSchool.AppHost.Tests/{ExamplePackTests,GoldenSaveTests,SchoolSeedTests,ServiceabilityTests}.cs`
- **Итог:** все тесты из списков семи фаз на месте; дописан один клиентский; исправлен ложный - **Итог:** все тесты из списков семи фаз на месте; дописан один клиентский; `foundation.md`
gotcha про паузу; `foundation.md` приведён к закрытому срезу. Хостовые тесты сверены по коду, приведён к закрытому срезу. Хостовые тесты сверены по коду, не прогоном.
не прогоном — Aspire держит DLL.
Что подтверждено: Что подтверждено:
- **Фаза 22.** Пак без `pack.jsonc` (id вместо названия), подписи `?lang=`, `core` с манифестом, - Пак: манифест, `missing-mod`, порядок — выбор игрока плюс топология (не алфавит), цикл —
`400 missing-mod` с id недостающего, зависимость грузится раньше зависимого, цикл — `mod-cycle`, `mod-cycle`, def без подписи предупреждает и грузится.
def без подписи предупреждает и грузится. Порядок — устойчивая топология поверх выбора игрока, - `mods/example` на диске; каталог/create/reload; патч только с паком; ваниль без выбора не меняется.
не алфавит; разрешённый список в ответе создания и в сейве. - Веса в `BehaviorDef`; пак с обедом выше урока уводит класс; отрицательный вес роняет загрузку.
- **Фаза 23.** `mods/example` на диске (`pack.jsonc`, локали, черты, набор имён, патч, комната, - Золотой ростер и сейв; `PrimarySchool` требует трёх; `teachersShort` в API и в `t(...)`.
README). Копируется в вывод тестов как `core`. `GET /api/catalog?mods=example`, create+reload, - Сид — поле школы. Format новее — школа не стартует, файл нетронут. Дамп через снимок + мейлбокс,
патч только с паком, last-wins, карта пака валидна. Ваниль без выбора не меняется. не в `World` с HTTP. `HSchool.Simulation.Tests` без параллельности.
- **Фаза 24.** Веса в `BehaviorDef` с комментариями, что перевешивают. Таблица ванильных решений, - Клиентские экраны в `happy-dom`: фильтры/пейджер, create, сетка, предел фонда текстом.
пак с обедом выше урока уводит класс, каталог без дефа на константах, отрицательный вес роняет
загрузку.
- **Фаза 25.** `golden/roster.txt` и первая несовпавшая строка; правило в README рядом. Сейв
текущего формата и сейв без `nativeLanguage`. Кривая голода `Hunger_FollowsTheSchoolDayCurve`.
`PrimarySchool` требует трёх, второй учитель не закрывает. `teachersShort` в API и в `t(...)`.
- **Фаза 26.** Сид — поле школы, не id. Тот же сид — те же люди; без сида после reload тот же
состав; золотой сейв `seed == id` грузится; переданный сид в списке. На экране школы и в create.
- **Фаза 27.** Format новее — школа не стартует, файл нетронут. Текущий грузится. Дамп через
снимок + мейлбокс (`DumpSchool` → работник), не в `World` с HTTP. `404 unknown-school`.
`HSchool.Simulation.Tests` без параллельности. Дамп в `docs/protocol.md` за тем же
переключателем, что reload.
- **Фаза 28.** `happy-dom` на экранах; фильтры/пейджер, create (`core` нельзя снять, сброс карты,
busy submit), сетка (pin-rejected / no-teacher / unknown-lesson), предел фонда текстом.
Политика в `AGENTS.md`. `npm test` — 46 тестов, ~5 с.
- **Инварианты.** `Simulation` без `DateTime.Now`. Протокол не бампили. Сид в хостовых тестах,
которым нужен состав, передаётся явно.
Дописано: Дописано:
- `managementPanel.test.ts` — непокрытый предмет показывает, скольких учителей не хватает - `managementPanel.test.ts` — непокрытый предмет показывает `teachersShort`.
(`staffUncoveredShort` с `n: 3`). Задача фазы 25 была в коде без теста экрана.
Исправлено: Исправлено:
- `AGENTS.md` утверждал, что на паузе кадры присутствия не приходят вовсе. Это ложь: работник - `AGENTS.md` утверждал, что на паузе кадры присутствия не приходят. Работник их шлёт
крутит цикл и шлёт occupancy + подписи урока из таблицы. Фаза 27 записала заблуждение ревью. (occupancy + подписи). Тест, который ждёт *людей* на паузе, зависнет; тест на подпись урока — нет.
Строка приведена к факту (тест, который ждёт *людей* на паузе, зависнет; тест на подпись урока — - `design/foundation.md`: порядок загрузки — не алфавит; «почему сейчас» про веса в `Decision.cs`
нет). устарел.
- В `design/foundation.md` «порядок — алфавит плюс зависимости» расходился с кодом (порядок
игрока + топология). «Почему сейчас» про веса в `Decision.cs` устарел — срез это закрыл.
Замечено рядом: Замечено рядом:
- `Dump_ReturnsPeopleNodesAndTimetable` принимает `NodeId is not null || Needs.Count > 0`. - `Dump_ReturnsPeopleNodesAndTimetable` принимает `NodeId is not null || Needs.Count > 0`.
Нужды есть у всех, так что узлы на кампусе не проверяются. Дамп поле `nodeId` отдаёт; критерий Нужды есть у всех, так что узлы на кампусе не проверяются. Усиливать до явки — срез 5.
«кто где стоит» руками. Усиливать до явки — соседний срез 5, не здесь. - Срез 7 (одежда) в работе: не писать в журнал как закрытый.
- Срез 7 (одежда) в работе: фазы 29–30 🔄, 31–37 ⬜. В журнал как закрытый не писать.
Content 17, People 2, Ai 3, Hunger curve 1, клиент 46/46. AppHost не гонялся.
## Что из журнала закрыто, 2026-08-19
Ревью нашло семь пунктов; шесть закрыты в этот же день, седьмой снят как несуществующий.
- **Панель «Общие события».** Решено отказаться. `design/near-term.md` и `design/people.md`
переписаны под вкладочную оболочку, фазы 1 и 8 помечены сноской, мёртвые ключи
`eventsTitle`/`eventsEmpty` убраны из `i18n`. Экран не менялся — он и был правдой.
- **Голод упирался в ноль.** Закрыто тремя правками данных и одной правкой поведения:
`Hunger` восстанавливается вне школы (дома едят), `decayPerHour` 0.1 → 0.2, стульев в столовой
8 → 112, и обед стал сменой по параллелям. Замер после: приходят с 0.97, к своей смене 0.43,
уходят с 0.27, к утру снова 1.0; за неделю голодающих нет ни одного (было 27 из 176 на нуле и
98 ниже порога).
- **Раздельные обеды.** Новая механика: `DayFrameDef.lunchBreaks` — какая параллель ест после
какого урока, `ActionDef.lunch` — действие-смена. В свою смену едят и не проголодавшиеся
(`DecisionPlanner.LunchWeight`), вне смены столовая не предлагается вовсе. В `core` младшие
(1–5) едят после третьего урока, старшие (6–11) после четвёртого; обе перемены стали длинными.
Замер: 11:10–11:30 в столовой только младшие, 12:15–12:30 только старшие, поели все 176.
- **`SchoolRegistry`.** Удалён вместе со своими тестами. `TryNormalizeName` переехал в
`SchoolNames`, `SchoolCreationError` — в свой файл; правила имени, диапазона дат и генератора
названий закреплены в `SchoolNamesTests`.
- **Нестабильный `OpeningASchoolDuringAMathLesson_...`.** Последнее ожидание требовало, чтобы
нанятый в середине дня учитель появился на территории, — а это не гарантировано: явка идёт из
плана дня, и замороженный момент уезжает тем дальше, чем дольше сам тест ходил по HTTP. Теперь
тест проверяет то, ради чего написан: что люди вообще едут в кадре присутствия.
- **Ветка `if/else` вокруг предела в `AssignAndUnassign_HonourTheCap_...`** убрана: отказ по
пределу проверяется отдельным тестом, а этот про назначение, снятие и перезапуск.
- **Предел фонда «почти не ограничивает игрока»** — снято при перепроверке на `5a00ad7`: это была
ошибка замера, а не код. Подробности в разделе среза 3.
Тесты после всех правок: 315 .NET и 33 клиентских, сборка клиента зелёная.