diff --git a/docs/design/people.md b/docs/design/people.md index bb6c334..c793416 100644 --- a/docs/design/people.md +++ b/docs/design/people.md @@ -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). Школа открывается без штата. - Смеси наборов имён с весами. - Новые роли из модов: ученик, работник, родитель — код, не данные. - Портреты и любой арт. diff --git a/docs/phases/reviewed.md b/docs/phases/reviewed.md index 783ad89..df8b1d0 100644 --- a/docs/phases/reviewed.md +++ b/docs/phases/reviewed.md @@ -6,143 +6,62 @@ Коммит в «Проверен на» делает журнал самопроверяющимся. Срез перестаёт считаться проверенным, как только `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. Оболочка и карта - **Фазы:** 0–4 -- **Проверен на:** `418a4ea`, 2026-08-20 (первичная проверка — `b135a9c`; предыдущая перепроверка — `5a00ad7`) +- **Проверен на:** `418a4ea`, 2026-08-20 - **Пути:** `src/HSchool.Client`, `src/HSchool.Protocol`, `src/HSchool.Content`, `src/HSchool.Server/Game`, `src/HSchool.Server/Api/ModEndpoints.cs`, `src/HSchool.Server/Api/SchoolEndpoints.cs`, `src/HSchool.Server/Net`, `tests/HSchool.Content.Tests`, `tests/HSchool.Protocol.Tests`, `tests/HSchool.AppHost.Tests/SchoolApiTests.cs`, `tests/HSchool.AppHost.Tests/GameSocketTests.cs` -- **Итог:** дописано 2 теста (оба прогнаны на чистом `b135a9c`), расхождений кода с дизайном - не найдено кроме одного — - панель «Общие события» из фазы 1 снята с экрана в фазе 8 и решение нигде не записано - (оставлено открытым, см. ниже) +- **Итог:** обещания фаз 0–4 целы; дописаны 3 теста; протокол v7 в трёх местах Что подтверждено: -- **Фаза 0.** `pixi.js` нет ни в `package.json`, ни в lockfile, ни в импортах; `npm run build` - проходит. -- **Фаза 1.** Дерево карты, панель локации со всеми пятью секциями и пустыми состояниями, клик по - дереву фильтрует на клиенте, `localize()` перекрашивает подписи. Панели «Общие события» на - экране нет — единственное расхождение среза. -- **Фаза 2.** `SchoolWorker` — `LongRunning`-поток, свой `PeriodicTimer`, свой `World`, аккумулятор - с потолком в 5 шагов. Супервизор держит таблицу и не трогает `World` (инвариант 3). Сейв пишется - на create, на shutdown и по `SaveIntervalSeconds`; настройки коалесцируются. Требуемый фазой тест - «create → reload → та же школа с тем же временем» — это - `GameSocketTests.ReloadFromDisk_RestoresAPausedClock` (точное равенство времени, паузы и - скорости), плюс `SchoolApiTests.ReloadFromDisk_RestoresCreatedSchools`. Независимость школ — - `GameSocketTests.PausingOneSchool_DoesNotStopAnother`. -- **Фаза 3.** Все восемь тестов из списка фазы на месте в `HSchool.Content.Tests` (JSONC-фикстуры, - last-wins по `defName` и по ключу локали, parent/abstract и цикл, патч `add` в `actions`, - связность, пустая комната, одна комната без двора). Ванильная раскладка валидируется - (`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`. +- PixiJS нет; дерево карты, пять секций локации, клиентский фильтр, `localize()`. Оболочка — + вкладки Карта/Люди, не три панели; отказ от ленты событий записан в фазе 1 и в `design/near-term.md`. +- `SchoolWorker` — `LongRunning`, свой `World`, аккумулятор с потолком 5. Супервизор таблицу + держит и `World` не трогает. Сейв на create/shutdown/`SaveIntervalSeconds`; + `ReloadFromDisk_RestoresAPausedClock`, `PausingOneSchool_DoesNotStopAnother`. +- Восемь тестов каталога/карты в `HSchool.Content.Tests`. Ванильная раскладка валидна. + Пропавшая папка мода не стартует школу и файл не трогает. +- `GET /api/mods`, каталог, create с картой и модами, снимок при OpenSchool. `core` в диалоге + создания нельзя снять. Кодек, `protocol.ts` и `docs/protocol.md` описывают один `MapSnapshot`. +- `modal.ts` резолвит промис явно; `.dialog--screen` — `height: fit-content`; + `DefaultStartDate` UTC; `MapSnapshotSize` от сообщения; outbox `DropOldest`; + `WaitToReadAsync` переносится между итерациями; меню патчит `card.update`. +- Родной язык — HTTP/JSON без бампа протокола. Старый сейв без поля берёт первый язык набора, + не бросает кубик (`ReloadWithoutASavedPick_TakesTheFirstLanguageInsteadOfRolling`). Дописано: -- `SchoolApiTests.Catalog_WithAnUnknownMod_IsRejected` — `GET /api/catalog?mods=` с неизвестной - пачкой описан в `docs/protocol.md` как `400 unknown-mod`, но не проверялся. -- `SchoolApiTests.ModId_ThatEscapesTheModsFolder_IsRejectedOnBothEndpoints` — инвариант 6: id - пачки приходит из браузера и подставляется в путь. `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`. +- `SchoolApiTests.Catalog_WithAnUnknownMod_IsRejected` +- `SchoolApiTests.ModId_ThatEscapesTheModsFolder_IsRejectedOnBothEndpoints` +- `SkillGrantTests.ReloadWithoutASavedPick_TakesTheFirstLanguageInsteadOfRolling` Исправлено: -- `School` в `src/HSchool.Client/src/net/api.ts` содержал два поля `speedIndex` подряд — артефакт мержа фазы 26 (`edd3d35`). `npm test` (vitest) это не ловит, `npm run build` падал с TS2300. Дубликат убран; поле как было одно. -- В таблице `design/near-term.md` «События | Панель есть, лента пустая» расходилась с решением фазы 8, которое журнал уже считал закрытым. Строка приведена к вкладочной оболочке. - -Замечено рядом (за границами среза): - -- `GameSocketTests.OpeningASchoolDuringAMathLesson_...` теперь падает **и в полном прогоне, и в одиночку** (40 кадров присутствия после снятия паузы, `People.Count == 0`). Раньше это был флак под нагрузкой, и тест ослабили до «люди вообще едут». После среза 6 никто не появляется: планы дня собраны для пустого штата, найм в 10:20 на паузе пересобирает таблицу, но явка завязана на `AppearAt` утреннего прихода. Чинить в срезе 5, не здесь. -- `npm test` в клиенте не гоняет `tsc`. Дубликат поля и любой другой TS2300 зелёный в vitest и красный на `build`. Это фаза 28. +- Два поля `speedIndex` в клиентском `School` (`api.ts`) — артефакт мержа фазы 26; vitest не + ловил, `npm run build` падал с TS2300. ## Срез 2. Люди - **Фазы:** 5–9 -- **Проверен на:** `bdb1253`, 2026-08-20 (первичная проверка — `5cd5a6d`; предыдущая перепроверка — `5a00ad7`) +- **Проверен на:** `bdb1253`, 2026-08-20 - **Пути:** `src/HSchool.People`, `src/HSchool.Content/PeopleDefs.cs`, `src/HSchool.Simulation/RosterSpawner.cs`, `src/HSchool.Simulation/School.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.AppHost.Tests/PeopleApiTests.cs`, `tests/HSchool.AppHost.Tests/SchoolSeedTests.cs` -- **Итог:** исправлен один тест, который падал на чистом клоне, укреплён второй, дописан один; - расхождение с дизайном одно и то же, что в срезе 1 (раскладка оболочки менеджера) +- **Итог:** обещания фаз 5–9 целы; починены два API-теста, дописан один на reload после набора Что подтверждено: -- **Фаза 5.** Все шесть пунктов списка тестов на месте в `PeopleDefTests` (разбор каждого вида, - наследование навыка, патч черты, неизвестная ссылка — ошибка каталога, склонение по модели и - перекрытие явной таблицей), плюс `ExtraPack_AddsANameSetAndTrait` — критерий «мод со своим - набором имён виден в каталоге». Наборы имён в `GET /api/catalog` проверены в - `SchoolApiTests.Catalog_LabelsDefsInTheRequestedLanguage`. -- **Фаза 6.** Все шесть тестов из списка фазы существуют и проходят. Критерий «в проекте нет - сокетов, ECS и `DateTime.Now`» закреплён самими тестами — - `Assembly_DoesNotReferenceArchAspNetOrSockets` и `Sources_DoNotUseWallClock`. -- **Фаза 7.** Одиннадцать кабинетов (`VanillaMap_HasElevenHomeroomsNotTheComputerLab`), нужды - тикают со школой, ростер публикуется снимком, `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` нет фамилий на Ё — мод с такой фамилией уронил бы - тест, а не код. Теперь тест складывает Ё в Е так же, как сервер. +- Дефы человека: разбор, наследование, патч, неизвестная ссылка, склонение, мод со своим набором. +- Генератор: шесть тестов списка фазы, без Arch/ASP.NET/`DateTime.Now`. Неполные семьи 5–10%. + Сид — поле школы, не `school.Id`; хостовые тесты, которым нужен состав, передают сид явно. +- Одиннадцать кабинетов, сущности в `World`, `PersistPeople` только при смене состава. + Штат при создании пуст — это фаза 11. +- Список с опубликованного снимка, карточка через `TaskCompletionSource`. Фильтры, пейджер, + семейные ссылки, всё через `t(...)`. Вкладка рядом с картой, не нижний ряд — так записано + в фазе 8 и в `design/people.md`. +- Выпуск старшей имеющейся, вместимость, семьи через поколения, reload после набора. Дописано: -- `PeopleApiTests.ReloadAfterTheYearlyIntake_RestoresTheNewComposition` — критерии фаз 7 и 9 - «перезапуск поднимает тех же людей» и «перезапуск после перехода поднимает **новый** состав» не - проверялись сквозь сервер. Просто сравнить людей до и после 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 хранит выпавший. +- `PeopleApiTests.ReloadAfterTheYearlyIntake_RestoresTheNewComposition` — ждёт 1 сентября и + требует состав **после** набора, отличный от исходного (детерминизм от сида иначе скрыл бы + потерянный файл). Исправлено: -- В `design/people.md` скорость нужд всё ещё была «ноль» (нарратив, совет и таблица), хотя срез 5 - это отменил. Помечено как отменённое со ссылкой на `ai.md`, по тому же шаблону, что расписание. - Код не трогался. - -Закрыто из прошлого прохода: - -- **Раскладка оболочки.** «Открыто» выше больше не действует: `design/people.md` и фаза 8 уже - записаны под вкладку рядом с картой. Решение принято в том же ходе, что отказ от панели событий. - -Замечено рядом: - -- «Заведомо не сейчас» в `people.md` всё ещё говорит «должности закрываются генератором» — - отменено срезом 3. Не переписывал исторический список стопов. - -People 34, PeopleDef 10, PeopleInSchool 7, PeopleApi+SchoolSeed 13, клиентская панель 3/3. +- `Card_IncludesFamilyAndLiveNeeds` брал `People[0]` и требовал двух родителей. Состав зависит + от сида; на чистом клоне первый ученик иногда из неполной семьи. Теперь ищет ученика с матерью + и проверяет диапазон. +- `List_SortsBySurname` требовал ordinal-порядок, сервер ставит Ё на место Е. Совпадали, пока + в `core` не было фамилий на Ё. ## Срез 3. Штат - **Фазы:** 10–13 -- **Проверен на:** `bdb1253`, 2026-08-20 (первичная проверка — `21d79cb`; предыдущая перепроверка — `5a00ad7`) +- **Проверен на:** `bdb1253`, 2026-08-20 - **Пути:** `src/HSchool.People/Staffing.cs`, `src/HSchool.People/ApplicantPool.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}`, @@ -287,131 +110,40 @@ People 34, PeopleDef 10, PeopleInSchool 7, PeopleApi+SchoolSeed 13, клиент `tests/HSchool.People.Tests/{StaffingTests,ApplicantPoolTests}.cs`, `tests/HSchool.AppHost.Tests/StaffingApiTests.cs`, `src/HSchool.Client/src/ui/managementPanel.test.ts` -- **Итог:** дописано 3 теста (один из них — прямо требуемый фазой 12 и отсутствовавший), - дополнен `docs/protocol.md`; расхождений кода с дизайном не найдено +- **Итог:** обещания фаз 10–13 целы; дописаны 3 теста; `docs/protocol.md` дополнен `?lang=` Что подтверждено: -- **Фаза 10.** `PETeacher` в `core` нет, `Classroom`, `ComputerLab` и `GymHall` не открывают - должностей — должности остались у библиотеки, приёмной, столовой, медкабинета и кабинета - директора, ровно как обещано. Кабинет описан числом мест (`seatThing`/`defaultSeats`, пустой - список слотов — проверено в `Catalog_LabelsDefsInTheRequestedLanguage`), редактор карты - показывает у него одно поле «мест». Неизвестный навык в предмете роняет каталог - (`Subject_UnknownSkill_FailsTheCatalog`), полнота локалей — `EveryVanillaLabel_ExistsInBothLocales`, - стартовая дата 31 марта 2012 зашита в тестах API. -- **Фаза 11.** Все шесть тестов из списка фазы на месте и проходят: детерминизм пула по сиду и - неделе, частичное обновление с сохранением навыков и запроса, устойчивый размер за пятьдесят - недель, «сильный просит больше», пустой штат при полной школе, соискатель вне ростера до найма. -- **Фаза 12.** Фонд оплаты, деление часов между учителями предмета, пол одной ставки и покрытие - предметов покрыты в `StaffingTests`. `GET /staffing` читает опубликованный снимок - (`loop.FindPeople`), наём и назначение идут в воркер через `GameCommand` с - `TaskCompletionSource` — инвариант 3 цел. Перезапуск с тем же штатом и назначениями — - `AssignAndUnassign_HonourTheCap_AndReloadKeepsAssignments`. -- **Фаза 13.** Строка денег, непокрытые предметы, список соискателей со ставкой и сильными - навыками, кнопка найма с текстом ошибки, список штата с предметами и стоимостью, назначение и - снятие с карточки — всё есть и всё через `t(...)`. -- **Дизайн.** `design/staffing.md` сверен построчно. Особо проверено: `MaxWeeklyHours` не - запрещает назначение, а делает предмет непокрытым (в коде нет такой ошибки, и замер это - подтверждает); цена часа считается от навыков **и от черт**. +- `PETeacher` нет; кабинеты без учительской должности; места через `defaultSeats`. + Неизвестный навык в предмете роняет каталог. Стартовая дата 31 марта 2012. +- Пул: детерминизм по сиду и неделе, частичное обновление, устойчивый размер, «сильный просит + больше», пустой штат, соискатель вне ростера до найма. Пул 32. +- Фонд оплаты, деление часов, пол ставки, покрытие. `GET /staffing` читает снимок, наём и + назначение — `GameCommand` с `TaskCompletionSource`. `MaxWeeklyHours` не запрещает назначение, + а оставляет предмет непокрытым. `PrimarySchool` на ванили требует трёх учителей (`teachersShort`). +- Экран: деньги, непокрытые, пул, штат, назначение с карточки, отказ по пределу текстом, + всё через `t(...)`. +- Полностью укомплектованная школа влезает в 100 000 впритык (запас 5–13%). Нанять пул целиком + без назначений предела достигает. Раскладка «каждому все предметы» — самая дешёвая, не + «укомплектованная школа». Дописано: -- `StaffingApiTests.AssigningPastTheCap_IsRejectedWithTheNumbers` — фаза 12 требует теста API на - «отклонение по пределу», а его не было: единственное место, где он мог сработать, было написано - через `if (over.StatusCode == Conflict) … else …` и проходило при любом исходе. Пределом - занялся замер: при выделенных 100 000 полностью загруженная школа стоит всего 68–72 тысячи, - то есть «нанять всех» предела не достигает. Но пик фонда приходится на середину раздачи - предметов — пока часы предмета несут один-два учителя, а не двенадцать, — и там отказ - случается: по сидам 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, который никто не - проверял. Тест кусается: пул на диске уже без нанятого, а сгенерированный заново вернул бы его - обратно. +- `StaffingApiTests.AssigningPastTheCap_IsRejectedWithTheNumbers` — фаза 12 требовала API-тест + на отказ по пределу; единственное место было написано через `if (over.StatusCode == Conflict)` + и проходило при любом исходе. +- `ApplicantPoolTests.ConfidentTraits_AskForMoreAndQuietOnesForLess` +- В `AssignAndUnassign_HonourTheCap_AndReloadKeepsAssignments` — тот же пул с теми же ставками + после перезапуска. Исправлено: -- `docs/protocol.md` не упоминал `?lang=ru|en` у `POST staff/hire`, `POST …/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 000–99 400, дальше отказ | всё | -| по учителю на предмет | 12 | 86 800–94 700 | `PrimarySchool` | -| закрыть и его | 14 | 87 100–95 100 | ничего | - -То есть полностью укомплектованная школа теперь **влезает, но впритык** — запас 5–13%, и -`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. +- `docs/protocol.md` не упоминал `?lang=` у hire/subjects, хотя клиент его шлёт. ## Срез 4. Расписание - **Фазы:** 14–17 -- **Проверен на:** `bdb1253`, 2026-08-20 (первичная проверка — `21d79cb`; предыдущая перепроверка — `5a00ad7`) +- **Проверен на:** `bdb1253`, 2026-08-20 - **Пути:** `src/HSchool.Schedule`, `src/HSchool.Content` (каркас дня, каникулы, `SubjectDef.room`), `src/HSchool.Simulation/SchoolTimetables.cs`, `src/HSchool.Server/Api/TimetableEndpoints.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.AppHost.Tests/TimetableApiTests.cs`, `src/HSchool.Client/src/ui/timetableGrid.test.ts` -- **Итог:** дописан один тест на пять непокрытых кодов отказа, поправлены две устаревшие строки - дизайна; расхождений кода с принятыми решениями нет +- **Итог:** обещания фаз 14–17 целы; дописан тест на коды отказа пина; две строки дизайна + помечены как отменённые срезом 5 Что подтверждено: -- **Фаза 14.** Все пункты критерия закрыты в `CalendarTests`: суббота, каникулы и ночь дают «вне - учебного дня», 10:20 буднего дня — третий урок, шестидневка делает субботу рабочей, зимние - каникулы переносятся через 1 января. Требование помещения у предмета есть (физкультура — - спортзал), неизвестная комната роняет каталог. -- **Фаза 15.** Все шесть тестов из списка фазы на месте. Четыре запрета проверяются одним - утверждением в `TeacherWithTwoSubjects_IsNeverInTwoRoomsAtOnce` (уникальность по учителю, классу - и кабинету в слоте) плюс `ClassOfSixteen_DoesNotEnterALabOfTwelve`. Отсутствие сокетов, ECS и - часов реального времени — самопроверкой сборки, как в `HSchool.People`. -- **Фаза 16.** Расписание пересобирается только когда изменился состав: `TimetableDirty` ставится - в `InstallPeople`/`Replace` и в годовом наборе, а воркер перестраивает таблицу под - `if (result.PeopleChanged)` — по тику этого не происходит. `GET /timetable` читает - опубликованный снимок, пин и снятие идут через `GameCommand` с `TaskCompletionSource`. - Перезапуск с закреплениями — `Reload_RestoresLockedLessons`. -- **Фаза 17.** Скобки в дереве (`Математика · 5Б`) собираются из присутствия, сетка «день × урок» - с переносом, сменой кабинета и отметкой «закреплено» есть, отказ планировщика показывается - текстом (`timetableErrorPin`, `timetableErrorNoTeacher`, `timetableErrorLesson`), личное - расписание — в карточке. Всё через `t(...)`. Клиентские тесты и сборка на `21d79cb`: 33/33 и - зелёная сборка. +- Календарь: суббота, каникулы и ночь — «вне учебного дня»; 10:20 вторника — третий урок; + шестидневка; зима через 1 января. Физкультура в спортзале. Обеденные смены в каркасе дня + (младшие после 3-го, старшие после 4-го). +- Планировщик: четыре запрета, класс не влезает в меньший кабинет, без Arch/ASP.NET/`DateTime.Now`. +- Перестроение только при `PeopleChanged` + `TimetableDirty`, не по тику. `GET /timetable` + читает снимок, пин — `TaskCompletionSource`. `Reload_RestoresLockedLessons`. +- Сетка, закрепление, тексты отказа (`pin-rejected` / `no-teacher` / `unknown-lesson`), + скобки в дереве из присутствия, личное расписание в карточке. Всё через `t(...)`. +- Занятость узла в протоколе v7 живёт в кадре `Presence`, не в снимке карты — фаза 19. Дописано: -- `TimetableApiTests.PinAndUnpinRefusals_CarryTheDocumentedCodes` — из шести кодов отказа пина в - `docs/protocol.md` тестом был закрыт только `pin-rejected` (хвост - `PinThenHireAnother_KeepsTheLockedSlot`, попытка провести урок в спортзале). Остальные пять — - `no-teacher`, `unknown-class`, `unknown-subject`, `unknown-room`, `unknown-lesson` на повторном - снятии, плюс `invalid-query` на неполном запросе — не проверялись, хотя клиент по ним - переключает текст ошибки. +- `TimetableApiTests.PinAndUnpinRefusals_CarryTheDocumentedCodes` — `no-teacher`, `unknown-class`, + `unknown-subject`, `unknown-room`, `unknown-lesson`, `invalid-query`. Исправлено: -- `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. +- В `design/schedule.md` «панель локации из расписания» и «люди находятся по расписанию» помечены + как отменённые срезом 5. ## Срез 5. Жизнь школы - **Фазы:** 18–21 -- **Проверен на:** `cbe739a`, 2026-08-20 (первичная проверка — `21d79cb`; предыдущая перепроверка — `5a00ad7`; дерево до коммита правок — `450495f`) +- **Проверен на:** `cbe739a`, 2026-08-20 - **Пути:** `src/HSchool.Ai`, `src/HSchool.Simulation/{PresenceSystem,ActivitySystem,LessonLearningSystem,School}.cs`, `src/HSchool.Protocol` (кадр `Presence`, пропуск в кадре часов), `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`, `tests/HSchool.Simulation.Tests/{PresenceTests,DecisionTests,ActivityTests,NeedDecayTests,LunchTests}.cs`, `tests/HSchool.AppHost.Tests/GameSocketTests.cs` -- **Итог:** все 32 теста из списков четырёх фаз на месте; дописаны 2 теста явки после смены таблицы, - `SetTimetable` сбрасывает планы дня, хостовый тест ждёт `Clock.Running` после снятия паузы. - Голод из первого прохода уже закрыт данными. AppHost не гонялся — Aspire держит DLL. +- **Итог:** все 32 теста из списков фаз на месте; дописаны 2 теста явки; `SetTimetable` сбрасывает + планы дня. Голод закрыт данными. AppHost после правки не гонялся — Aspire держал DLL. Что подтверждено: -- **Фаза 18.** Все 13 требуемых тестов есть: маршрут 201 → санузел через лестницу, совпадение - суммы `travelMinutes` с реально пройденным временем, пустая школа в воскресенье и в каникулы, - класс без уроков не приходит, ученик в кабинете к звонку, спортзал впритык к перемене, - детерминизм по сиду, сохранение посреди перемены, четыре сценария пропуска пустого времени, - школа без учителей, лето с первым сентября, промотанная неделя против прожитой. -- **Фаза 19.** Все пять тестов: байтовая раскладка присутствия с обеих сторон, круговой прогон, - отсутствие людей и урока в снимке карты, разрешение на пропуск в кадре часов, отброшенный - пропуск в учебное время. -- **Фаза 20.** Все шесть: декей голода за час, обед на заявленные минуты и величину, нехватка - стульев, ночь вне школы, действие с неизвестной нуждой роняет каталог, детерминизм нужд. -- **Фаза 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). +- Ходьба: 13 тестов списка фазы 18 (маршрут, `travelMinutes`, воскресенье/каникулы, звонок, + детерминизм, пропуск пустого времени, лето, промотанная неделя). +- Присутствие: раскладка с обеих сторон, круговой прогон, люди не в снимке карты, пропуск в + учебное время отбрасывается. На паузе кадры **идут** (occupancy + подписи урока); людей в них + может не быть. +- Нужды: декей, обед, стулья, ночь вне школы, неизвестная нужда, детерминизм. + `Hunger` восстанавливается вне школы, в столовой 112 стульев, обед — смена по параллелям. + `SleepAndHunger_ReturnToMaxOffCampus`, `AfterASchoolDay_NobodyIsLeftStarving`. +- Решения: восемь тестов фазы 21, в том числе туалет с урока и возврат, потолок отложенных, + детерминизм за неделю. `HSchool.Ai` без Arch/ASP.NET/`DateTime.Now`. Дописано: -- `HireAndAssignMath_DuringLessonThree_PutsSomeoneOnCampus` — найм на паузе в 10:20, потолок - 64 решения, шаг 20 Гц, как у работника. -- `AssigningMath_AfterDayPlansWereBuilt_PutsSomeoneOnCampus` — назначение без смены численности; - без сброса `PlanDay` кампус остаётся пустым. +- `HireAndAssignMath_DuringLessonThree_PutsSomeoneOnCampus` +- `AssigningMath_AfterDayPlansWereBuilt_PutsSomeoneOnCampus` -Закрыто из прошлого прохода: +Исправлено: -- **Голод.** Уже закрыто 2026-08-19 (`restoredOffCampus`, стулья 112, смены). Повторно зелёные - `NeedDecayTests.SleepAndHunger_ReturnToMaxOffCampus` и `LunchTests.AfterASchoolDay_NobodyIsLeftStarving`. -- **`OpeningASchoolDuringAMathLesson_...`.** Это была явка плюс чтение паузных кадров, не флак - нагрузки. Симуляция после правки зелёная; хостовый прогон — когда Aspire отпустят DLL. - -Ai 29, Presence+Decision+Activity+NeedDecay+Lunch 31, Presence_RoundTrips 1. +- `School.SetTimetable` обнуляет `PlanDay`. Иначе после назначения предмета без смены численности + `EnsurePlans` считает планы актуальными, `AppearAt` остаётся `null`, кампус пуст. Найм это + маскировал (численность растёт). +- `OpeningASchoolDuringAMathLesson_...` после снятия паузы сразу ждал людей и съедал бюджет на + паузные кадры. Теперь ждёт `clock.Running`, потом людей. ## Срез 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.People.Tests/GoldenRosterTests.cs`, `tests/HSchool.AppHost.Tests/{ExamplePackTests,GoldenSaveTests,SchoolSeedTests,ServiceabilityTests}.cs` -- **Итог:** все тесты из списков семи фаз на месте; дописан один клиентский; исправлен ложный - gotcha про паузу; `foundation.md` приведён к закрытому срезу. Хостовые тесты сверены по коду, - не прогоном — Aspire держит DLL. +- **Итог:** все тесты из списков семи фаз на месте; дописан один клиентский; `foundation.md` + приведён к закрытому срезу. Хостовые тесты сверены по коду, не прогоном. Что подтверждено: -- **Фаза 22.** Пак без `pack.jsonc` (id вместо названия), подписи `?lang=`, `core` с манифестом, - `400 missing-mod` с id недостающего, зависимость грузится раньше зависимого, цикл — `mod-cycle`, - def без подписи предупреждает и грузится. Порядок — устойчивая топология поверх выбора игрока, - не алфавит; разрешённый список в ответе создания и в сейве. -- **Фаза 23.** `mods/example` на диске (`pack.jsonc`, локали, черты, набор имён, патч, комната, - README). Копируется в вывод тестов как `core`. `GET /api/catalog?mods=example`, create+reload, - патч только с паком, last-wins, карта пака валидна. Ваниль без выбора не меняется. -- **Фаза 24.** Веса в `BehaviorDef` с комментариями, что перевешивают. Таблица ванильных решений, - пак с обедом выше урока уводит класс, каталог без дефа на константах, отрицательный вес роняет - загрузку. -- **Фаза 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`. Протокол не бампили. Сид в хостовых тестах, - которым нужен состав, передаётся явно. +- Пак: манифест, `missing-mod`, порядок — выбор игрока плюс топология (не алфавит), цикл — + `mod-cycle`, def без подписи предупреждает и грузится. +- `mods/example` на диске; каталог/create/reload; патч только с паком; ваниль без выбора не меняется. +- Веса в `BehaviorDef`; пак с обедом выше урока уводит класс; отрицательный вес роняет загрузку. +- Золотой ростер и сейв; `PrimarySchool` требует трёх; `teachersShort` в API и в `t(...)`. +- Сид — поле школы. Format новее — школа не стартует, файл нетронут. Дамп через снимок + мейлбокс, + не в `World` с HTTP. `HSchool.Simulation.Tests` без параллельности. +- Клиентские экраны в `happy-dom`: фильтры/пейджер, create, сетка, предел фонда текстом. Дописано: -- `managementPanel.test.ts` — непокрытый предмет показывает, скольких учителей не хватает - (`staffUncoveredShort` с `n: 3`). Задача фазы 25 была в коде без теста экрана. +- `managementPanel.test.ts` — непокрытый предмет показывает `teachersShort`. Исправлено: -- `AGENTS.md` утверждал, что на паузе кадры присутствия не приходят вовсе. Это ложь: работник - крутит цикл и шлёт occupancy + подписи урока из таблицы. Фаза 27 записала заблуждение ревью. - Строка приведена к факту (тест, который ждёт *людей* на паузе, зависнет; тест на подпись урока — - нет). -- В `design/foundation.md` «порядок — алфавит плюс зависимости» расходился с кодом (порядок - игрока + топология). «Почему сейчас» про веса в `Decision.cs` устарел — срез это закрыл. +- `AGENTS.md` утверждал, что на паузе кадры присутствия не приходят. Работник их шлёт + (occupancy + подписи). Тест, который ждёт *людей* на паузе, зависнет; тест на подпись урока — нет. +- `design/foundation.md`: порядок загрузки — не алфавит; «почему сейчас» про веса в `Decision.cs` + устарел. Замечено рядом: - `Dump_ReturnsPeopleNodesAndTimetable` принимает `NodeId is not null || Needs.Count > 0`. - Нужды есть у всех, так что узлы на кампусе не проверяются. Дамп поле `nodeId` отдаёт; критерий - «кто где стоит» руками. Усиливать до явки — соседний срез 5, не здесь. -- Срез 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 клиентских, сборка клиента зелёная. + Нужды есть у всех, так что узлы на кампусе не проверяются. Усиливать до явки — срез 5. +- Срез 7 (одежда) в работе: не писать в журнал как закрытый.