# Проверенные срезы Журнал ревью. Фаза с ✅ в [`README.md`](README.md) — заявка автора; строка здесь — проверенный факт: каждое обещание фазы сверено с кодом и тестом, найденное дописано или исправлено. Коммит в «Проверен на» делает журнал самопроверяющимся. Срез перестаёт считаться проверенным, как только `git log <коммит>..HEAD -- <пути>` перестаёт быть пустым — поэтому «Пути» обязательны. ## Срез 1. Оболочка и карта - **Фазы:** 0–4 - **Проверен на:** `b135a9c`, 2026-08-19 - **Пути:** `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.** `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`. Дописано: - `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`, и на дереве после починки сборки. ## Срез 2. Люди - **Фазы:** 5–9 - **Проверен на:** `5cd5a6d`, 2026-08-19 - **Пути:** `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`, `src/HSchool.Client/src/ui/peoplePanel.ts`, `src/HSchool.Client/src/ui/personCard.ts`, `tests/HSchool.People.Tests`, `tests/HSchool.Content.Tests/PeopleDefTests.cs`, `tests/HSchool.Simulation.Tests/PeopleInSchoolTests.cs`, `tests/HSchool.AppHost.Tests/PeopleApiTests.cs` - **Итог:** исправлен один тест, который падал на чистом клоне, укреплён второй, дописан один; расхождение с дизайном одно и то же, что в срезе 1 (раскладка оболочки менеджера) Что подтверждено: - **Фаза 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` нет фамилий на Ё — мод с такой фамилией уронил бы тест, а не код. Теперь тест складывает Ё в Е так же, как сервер. Дописано: - `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, но следующий проход это увидит. ## Срез 3. Штат - **Фазы:** 10–13 - **Проверен на:** `21d79cb`, 2026-08-19 - **Пути:** `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}`, `src/HSchool.Client/src/ui/managementPanel.ts`, `src/HSchool.Client/src/net/api.ts`, `tests/HSchool.People.Tests/{StaffingTests,ApplicantPoolTests}.cs`, `tests/HSchool.AppHost.Tests/StaffingApiTests.cs` - **Итог:** дописано 3 теста (один из них — прямо требуемый фазой 12 и отсутствовавший), дополнен `docs/protocol.md`; расхождений кода с дизайном не найдено Что подтверждено: - **Фаза 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` не запрещает назначение, а делает предмет непокрытым (в коде нет такой ошибки, и замер это подтверждает); цена часа считается от навыков **и от черт**. Дописано: - `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, который никто не проверял. Тест кусается: пул на диске уже без нанятого, а сгенерированный заново вернул бы его обратно. Исправлено: - `docs/protocol.md` не упоминал `?lang=ru|en` у `POST staff/hire`, `POST …/subjects` и `DELETE …/subjects/{subject}`, хотя все три его принимают и клиент его шлёт. Поведение не трогалось — дописана документация. Замечено рядом (за границами среза): - Предел фонда при ванильных данных почти не ограничивает игрока: нанять всех двенадцать и раздать им все предметы стоит около 70 000 из 100 000. Отказ достижим только по дороге, на пике неравномерной нагрузки. Это игровой баланс, а не ошибка (см. коммит `207e7ec`, которым предел поднимали намеренно), но если предел задумывался как настоящее ограничение — числа сейчас говорят обратное. - В `AssignAndUnassign_HonourTheCap_AndReloadKeepsAssignments` осталась ветка `if/else` вокруг ожидаемого отказа. Теперь, когда отказ проверяется отдельным тестом, эта ветка просто не может упасть — её стоит однажды упростить, но трогать работающий тест ради этого ревью не стало. ## Срез 4. Расписание - **Фазы:** 14–17 - **Проверен на:** `21d79cb`, 2026-08-19 - **Пути:** `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` (пин, перестроение), `src/HSchool.Client/src/ui/timetableGrid.ts`, `src/HSchool.Client/src/ui/gameScreen.ts`, `tests/HSchool.Schedule.Tests`, `tests/HSchool.Content.Tests/CalendarTests.cs`, `tests/HSchool.AppHost.Tests/TimetableApiTests.cs` - **Итог:** дописан один тест на пять непокрытых кодов отказа, поправлены две устаревшие строки дизайна; расхождений кода с принятыми решениями нет Что подтверждено: - **Фаза 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 и зелёная сборка. Дописано: - `TimetableApiTests.PinAndUnpinRefusals_CarryTheDocumentedCodes` — из шести кодов отказа пина в `docs/protocol.md` тестом был закрыт только `pin-rejected` (хвост `PinThenHireAnother_KeepsTheLockedSlot`, попытка провести урок в спортзале). Остальные пять — `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 и сборка. ## Срез 5. Жизнь школы - **Фазы:** 18–21 - **Проверен на:** `21d79cb`, 2026-08-19. Фаза 21 закрыта за час до проверки, `core` правился параллельно — проверка велась в отдельном worktree на коммите, не на рабочем дереве - **Пути:** `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}.cs` - **Итог:** все 32 теста из списков четырёх фаз на месте и проходят; тестов не дописано, зато измерен прожитый учебный день — и он показал две вещи, которых списки не ловят Что подтверждено: - **Фаза 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 прямо обещает, что «опоздание и отсутствие видны».