Files
h-school/docs/design/off-queue.md
T
Leonid Pershin 12cc36b4dd
ci / server (push) Failing after 3m39s
ci / client (push) Successful in 20s
Merge branch 'phase/55-view-state'
# Conflicts:
#	docs/phases/README.md
2026-08-20 12:10:55 +03:00

13 KiB
Raw Blame History

Вне очереди

Задачи, которых нет в нумерованных срезах. Срез 10 — ремесло (craft.md), не сюда. Сюда — склейка уже живых систем и мелкий DX. После любой фазы школа целая.

Люди: people.md. Штат: staffing.md. Расписание: schedule.md. Присутствие: ai.md. Одежда и климат: inventory.md.

Связность урока

Урок уже растёт навык, если ученик стоит в кабинете и не голоден. Рядом лежат числа, которые игрок видит и которые на этот рост не влияют: учитель в туалете, навык учителя, тепло, учебник в сумке, снег на дороге.

Этот кусок отвечает на вопрос «почему нанимать сильного, чинить тепло и класть учебник в сумку». Оценок, посещаемости как журнала и приказов нет.

Что меняется в уже принятом

Было Стало Почему
Ученик в кабинете качает навык, учитель может быть где угодно Рост только если назначенный учитель стоит в том же узле и не идёт Зазор «обязанность / место» уже есть; урок его не смотрел
Зарплата зависит от навыков, урок — нет Навык учителя по предмету масштабирует Gain Иначе «математика 78» в пуле — только цена
Голод режет урок, тепло — нет Та же NeedFactor на тепло Полоска уже живая; inventory.md откладывал штраф
Учебники сегодняшних предметов едут в сумке Нет в сумке на этом уроке — половинный рост Переноска и шкафчик иначе остаются двумя числами
Погода одевает, на приход не влияет Дождь и снег добавляют минуты к запасу на дорогу Часы уже показывают осадки; ленивый уже опаздывает от черты

Формула

HSchool.Ai.LessonLearning.Gain по-прежнему чистое число. Множители:

  • нуждыNeedFactor(голод) × NeedFactor(тепло); нет нужды в каталоге — множитель 1;
  • черты ученика — как сейчас;
  • учительNeedFactor(навык / 100), навык — среднее по SubjectDef.skills у учителя, нет ключа — Range.Min этого навыка; учителя нет в комнате — множитель 0, рост не пишется;
  • учебник — 1, если в сумке есть экземпляр с subject этого урока; иначе BehaviorDef.lessonNoTextbookFactor (ваниль 0.5).

Учитель «стоит в комнате»: NodeId = кабинет урока, путь пуст, остаток минут 0. Действие учителя не отменяет урок (дежурство — не ActionDef). Нет человека с этим id — учителя нет.

Дорога: к нынешнему SlackMinutes (черты + бросок) прибавляются минуты из BehaviorDef.commuteRainMinutes / commuteSnowMinutes, если в момент сборки плана дня на улице дождь или снег. HSchool.Ai осадки не знает — симуляция передаёт уже посчитанные минуты.

Что видит игрок

Числа на карточке. Плюс строка в сегодняшней истории, один раз на ключ в сутки, не каждый тик:

Тип Когда thingDef
lesson-no-teacher Урок шёл, учителя не было в кабинете предмет
lesson-cold На уроке, который иначе учил, тепло ниже needThreshold предмет
lesson-no-textbook На уроке нет учебника этого предмета в сумке предмет

Опоздание из-за снега в лог не пишем: весь состав получил бы одну и ту же строку, а место человека и так видно.

Клиент по-прежнему рисует label с сервера. Сокет и сейв не меняются. Версию протокола не бампить.

Где живёт код

Нового проекта нет. Формула и план дня — HSchool.Ai. Присутствие учителя, тепло, сумка, погода на входе в план — HSchool.Simulation. Подписи лога — каталог. Список типов — docs/protocol.md (HTTP лога).

Заведомо не сейчас

  • Оценки, журнал посещаемости, выговор за прогул.
  • Перегруз замедляет ходьбу.
  • Насмешки над формой и мнения — срез 9.
  • Вызов родителей, деньги за одежду, болезнь от промокания.
  • Клиент сам считает «учится хуже».

Зафиксировано этим разговором

Тема Решение
Нарезка Пять фаз во «Вне очереди», не срез 10
Стоп После каждой фазы школа целая
Протокол Не бампить; новые type только в HTTP лога
Сейв Не трогать
Учитель на уроке Тот же узел, не идёт; нет id — нет роста
Навык учителя Среднее по скиллам предмета, та же кривая что у нужды
Тепло Рядом с голодом, произведение двух NeedFactor
Учебник Только сумка, не шкафчик и не дом; ваниль ×0.5
Погода на дорогу Минуты в BehaviorDef; лога нет

Пропуск сборки при запуске

Зачем

run-aspire.cmd каждый раз вызывает dotnet run, а тот — MSBuild графа Aspire, даже когда исходники не менялись. Повторный запуск после Ctrl+C стоит десятки секунд впустую: SDK пересчитывает хост, хотя dll на месте.

Было / Стало / Почему

Было. Скрипт всегда dotnet run --project src/HSchool.AppHost. Аргументы проходили насквозь.

Стало. Если HSchool.AppHost.dll и HSchool.Server.dll новее входов (C#, csproj, props, global.json), скрипт делает dotnet run --no-build. Иначе — dotnet build, потом --no-build. --rebuild собирает всегда. Конфигурация (-c / --configuration) выбирает, какие dll сравнивать; по умолчанию Debug, как у dotnet run.

Почему. Инкрементальный MSBuild всё равно оценивает Aspire SDK. --no-build это пропускает. Клиентский TypeScript и JSONC модов не входы: Vite и каталог читают диск (ContentRoot), а не bin. Тестовые проекты AppHost не ссылается — их правки запуск не пересобирают.

Правила «устарел ли билд» живут в tools/apphost-uptodate.ps1 (то, что вызывает run-aspire.ps1) и в LaunchBuildStamp (то, что проверяют тесты). Менять надо оба.

Что не входит

Голый dotnet run --project src/HSchool.AppHost по-прежнему собирает — так удобнее агентам с грязным деревом. Linux-обёртки нет: точка входа Windows — .cmd. Протокол, HTTP и сейв не трогаем.

Что нового после входа

Зачем

Альфа обновляется часто. Игрок не смотрит git: если с прошлого захода в этот браузер накатили другую сборку Server, один раз показать, что вошло в неё.

Было / Стало / Почему

Было. Нет версии сборки на экране. Сессионная кука HttpOnly и про вход.

Стало. Сборка Server вшивает HEAD и git log --first-parent. GET /api/changelog?since= отдаёт текущий SHA и коммиты после якоря. Клиент держит hschool.seen-rev (год, Path=/, SameSite=Lax, не HttpOnly). Первый заход без куки окно не показывает — только запоминает текущий SHA. Окно — после входа, не при возврате из школы.

Почему. «Накатанный» — то, что в dll, а не грязный HEAD при --no-build. First-parent после --no-ff — это слитые фазы, не внутренняя нарезка ветки. Mark phase N оставляем в цепочке якорем, в список не рисуем: иначе кука на таком HEAD потеряла бы следующее слияние.

Что не входит

Ручной CHANGELOG.md. Запись на пользователе в users.json. Вызов git из бегущего процесса. Бамп версии сокета. Окно внутри открытой школы.

Состояние экрана

Зачем

Менеджер — списки с фильтрами. Сейчас они живут только в RAM: F5 всегда меню, повторный вход сбрасывает вкладки и запросы. Игрок сравнивает людей и соискателей; потерять «ученики 5А» на обновлении — дырка в уже живых экранах, не новая фича.

Было / Стало / Почему

Было. SPA без адреса. GameScreen.show и PeoplePanel.show обнуляют вкладки, фильтры и выбор. Язык уже в localStorage; игровые часы — на сервере.

Стало. Query URL держит где я: школа, обзор/управление, карта/люди, выбранные комната и человек. sessionStorage по schoolId держит фильтры, поиски, сорт, страницу, вкладку карточки и класс сетки — и последний маршрут, чтобы вход с меню поднял то же место. F5 с ?school= шлёт тот же OpenSchool, что карточка. Вход/выход — pushState; смена вкладки и фильтра — replaceState, чтобы «назад» не листал каждый клик.

Почему. Сервер не знает про вкладки; класть их в сейв — лишний HTTP. localStorage как у языка держал бы «5А» через месяц. URL без фильтров остаётся коротким и копируемым.

Что видит игрок

F5 внутри школы — та же школа, те же вкладки и фильтры. Назад — меню. Снова открыть ту же школу — не сброс на карту. Гость не входит в управление по ссылке. Скорость часов, пауза, открытый пул соискателей и мастер создания не вспоминаются.

Что не входит

Поиск по имени в списках, где его нет. Роутер-библиотека. Состояние на сервере. Бамп протокола.