8 Тестирование, сборка и поставка¶
Главы 4–7 описывают, как платформа устроена и как её исследовательский контур применяется в sim-to-real-эксперименте. Настоящая глава смотрит на платформу с инженерно-операционной стороны: какие средства проверки фиксируют корректность реализации, как из исходников собирается готовый к эксплуатации артефакт и каким способом результат попадает к пользователю — оператору, исследователю или разработчику плагинов. Изложение опирается на актуальное состояние репозитория: workflow-файлы в .github/workflows/, Dockerfile и Makefile модуля src/ks0223-web-mac/, тестовые проекты в Assets/Tests/EditMode/, backend.Tests/ и python/tests/training/, конфигурация документации mkdocs.yml, артефакты релизов в dist/. Все цифры по объёму тестового покрытия приведены без округления — в рамках работы важно не переоценить степень верификационной готовности платформы.
Раздел сознательно выполнен в более компактном формате, чем главы 4–7. Тестовое покрытие, CI и поставка платформы находятся в инженерной фазе, а не в исследовательской: они описывают реальное состояние инструментов, а не результаты экспериментов. Существенным элементом главы является честная фиксация направлений расширения — раздел 8.1.4 выделяет пробелы тестового покрытия, а раздел 8.7.3 — текущее состояние CHANGELOG и релизной процедуры.
8.1 Тестовое покрытие¶
Тестовое покрытие платформы организовано тремя независимыми наборами, по одному на каждый язык реализации: NUnit-тесты для Unity-runtime в EditMode-режиме, xUnit-тесты для backend-сервисов на .NET 8 и pytest-тесты для Python-обвязки обучения. Соответствующие проекты живут в самостоятельных каталогах и собираются разными toolchain-ами; единственная связующая точка — workflow ci.yml, который запускает каждый набор отдельным job-ом (см. раздел 8.2.1). Сводный объём тестов на момент написания работы зафиксирован в таблице 8.1; цифры намеренно приведены без агрегирования по «количеству строк покрытия», поскольку coverage-инструментация в проекте не настроена и формальная оценка покрытия по dotCover или coverage.py остаётся направлением расширения (см. 8.1.4).
Таблица 8.1 — Тестовые наборы платформы
| Домен | Каталог | Файлов | Тестов | Стек |
|---|---|---|---|---|
| Unity-runtime (EditMode) | Assets/Tests/EditMode/ |
5 | 22 | NUnit + Unity Test Framework 1.5 |
| Backend (.NET 8) | src/ks0223-web-mac/backend.Tests/ |
1 | 13 | xUnit + Microsoft.Extensions.Time.Testing |
| Python-обвязка | python/tests/training/ |
6 | 28 | pytest + Gymnasium-стабы |
| Frontend | — | 0 | 0 | не реализовано |
8.1.1 Unity EditMode тесты¶
EditMode-режим Unity Test Framework выполняет тесты вне Play-цикла: сцена не инстанцируется, FixedUpdate не вызывается, тесты живут в обычном CLR-домене и не требуют активного рендера. Для рассматриваемой платформы такой режим оказался достаточным — все пять тестовых файлов покрывают компоненты, поведение которых может быть выражено через прямые вызовы публичных методов и JsonUtility-сериализацию, без необходимости в физическом моделировании или ECS-обновлении. Сборка UavSimulator.EditModeTests.asmdef объявляет зависимости от runtime-сборок симулятора (UavSimulator.Contracts, UavSimulator.Core, UavSimulator.Plugins, UavSimulator.CityDemo, UavSimulator.Vehicles, UavSimulator.Recording) и тестовых пакетов NUnit; запуск возможен как из Test Runner-окна Editor, так и из CI через game-ci/unity-test-runner@v4.
ContractSerializationTests проверяет round-trip-сериализацию транспортных контрактов через JsonUtility. Два теста гарантируют, что заполненные структуры SimulationConfig и ControlCommand корректно проходят ToJson/FromJson-цикл с сохранением вложенных массивов ConfigKeyValue и SimulationAgentConfig. Такая проверка важна потому, что Unity использует собственный сериализатор с ограничениями по полиморфизму и null-обработке; ошибка в раскладке полей не вылавливается компилятором и обнаруживается лишь на runtime при невалидном JSON со стороны backend.
SimulationConfigValidator тестируется тремя сценариями: ошибка при пустом реестре плагинов, fallback на первый зарегистрированный плагин при отсутствии явных идентификаторов в конфигурации, подстановка timeScale = 1.0 вместо нулевого значения. Эти три кейса покрывают основные ветви валидации, выполняемой при reset со стороны runtime API; назначение тестов — зафиксировать поведение по умолчанию для backwards-совместимости со старыми сценариями и Python-клиентами.
SceneCameraRecorderTests (семь тестов) обходит ограничение EditMode на отсутствие активного рендера через специальный hook SceneCameraRecorder.Tick(capture: false), который проводит state-machine рекордера через все ветви — старт, авто-стоп по достижении длительности, подсчёт кадров, повторный старт после завершения — без вызова реального Camera.Render и без необходимости в ffmpeg. Подход иллюстрирует общий приём, применённый в EditMode-наборе: вместо имитации Play-цикла в коде runtime выделяются точки наблюдения, доступные тесту напрямую.
TrafficLightFsmTests (семь тестов) проверяет конечный автомат светофоров для city-demo-сцены: переходы Green → Yellow → Red, синхронизацию пары NS/EW, корректную обработку повторного StartCycle. Для тестируемости в TrafficLightController введён метод AdvanceTime(seconds), заменяющий накопление Time.deltaTime; такая проектируемая под тест поверхность типична для всех Unity-компонентов, попавших в EditMode-набор.
CityWaypointGraphTests (три теста) фиксирует контракт CityWaypointGraph как ScriptableObject: поиск узла по идентификатору, обработка отсутствующего идентификатора, валидность узла с несколькими исходящими рёбрами. Этот набор небольшой, но он закрепляет инвариант графа waypoint-ов, на который опирается процедурный спавн NPC-машин в city-demo-сцене.
[Test]
public void SimulationConfig_RoundTrip_JsonUtility()
{
var config = new SimulationConfig
{
seed = 123,
timeScale = 1.0f,
selectedTrackId = "track-01",
selectedVehicleId = "vehicle-01",
agents = new[] {
new SimulationAgentConfig { agentId = "ego", isPrimary = true },
},
};
var json = JsonUtility.ToJson(config);
var parsed = JsonUtility.FromJson<SimulationConfig>(json);
Assert.That(parsed.agents[0].agentId, Is.EqualTo("ego"));
}
Полный объём — двадцать два теста — обеспечивает базовый защитный пояс для тех частей runtime, которые меняются при доработках плагинов и контрактов. PlayMode-тесты, требующие реальной физической симуляции и активной сцены, в наборе отсутствуют и относятся к направлениям расширения (см. 8.1.4).
8.1.2 Backend xUnit тесты¶
Тестовый проект backend.Tests собран на xUnit и сосредоточен на одном классе — AutopilotSafetyFilter. Это сознательный выбор: фильтр безопасности автопилота отвечает за прерывание управления реальным роботом при обнаружении препятствия по сонару, и любая регрессия в его логике приводит непосредственно к физическому риску — столкновению робота со стенкой картонного коридора. Тринадцать тестов покрывают семь поведений фильтра: срабатывание e-stop при подкритическом расстоянии, sticky-hold с разрешением заднего хода, автоматический релиз после истечения hold-периода, клиппирование throttle вперёд и неклиппирование назад, deadman-таймер на длительной паузе вызовов, агрегация счётчиков срабатываний, корректная инициализация при Reset.
В тестах использован FakeTimeProvider из Microsoft.Extensions.Time.Testing — реализация TimeProvider, позволяющая программно сдвигать «текущее время» вызовом Advance(TimeSpan). Такой подход исключает реальные Thread.Sleep-задержки в тестах: проверка временного поведения фильтра выполняется в миллисекундах wall-clock, при том что сценарий покрывает восстановление через 510 миллисекунд после срабатывания. AutopilotSafetyOptions инжектируется явно, что позволяет тестам варьировать пороги без модификации продуктивных значений по умолчанию.
[Fact]
public void EStop_Releases_After_HoldMs_Elapsed()
{
var fake = new FakeTimeProvider();
var filter = CreateFilter(timeProvider: fake);
filter.Apply(0.5f, 0f, 0.10f); // t=0: e-stop
fake.Advance(TimeSpan.FromMilliseconds(250));
filter.Apply(0.5f, 0f, 0.10f); // hold-mid keep-alive
fake.Advance(TimeSpan.FromMilliseconds(260));
var decision = filter.Apply(0.5f, 0f, 1.0f); // t=510: распущен
Assert.False(decision.EStopActive);
}
Покрытие остальных backend-сервисов — RuntimeSessionManager, AutopilotService, ModelLifecycleService, RealRobotRuntimeProvider, SimRuntimeProvider — на момент написания работы выполняется опосредованно: через ручную проверку через WebUI и через xUnit-тесты единственного критического по безопасности компонента. Это направление расширения зафиксировано в разделе 8.1.4.
8.1.3 Python pytest для wrappers и launch¶
Python-набор включает шесть файлов и двадцать восемь тестовых функций, сосредоточенных на тренировочных обёртках python/training/ и пуске multi-runtime-конфигурации. Все обёртки, описанные в разделе 6.2, имеют отдельный тестовый файл; запуск выполняется командой pytest python/tests/training/ из корня репозитория и не требует Unity-runtime — тесты используют синтетические gym.Env-стабы.
test_anti_spin_reward.py (шесть тестов) проверяет AntiSpinRewardWrapper: отсутствие штрафа на коротких сериях одинаковых действий, активацию штрафа при достижении порога повторов, сброс счётчика при смене действия, корректное применение к награде. test_discrete_action_wrapper.py (шесть тестов) валидирует таблицу из пяти дискретных действий KS0223: преобразование DirForward в (throttle, steer) = (1, 0), DirLeft в (0.5, -1) и так далее, обработку out-of-range индексов, сохранение Action-space-метаданных. test_image_aug_wrapper.py (шесть тестов) фиксирует, что ImageAugObservationWrapper сохраняет неизменными размерности и dtype Dict-наблюдения и применяет домен-рандомизацию только к ключу image. test_latency_wrapper.py (шесть тестов) проверяет очередь отложенных действий DelayedActionWrapper с задаваемой задержкой в шагах.
def test_penalty_kicks_in_at_threshold():
env = AntiSpinRewardWrapper(_PassThroughEnv(),
repeat_threshold=5, repeat_penalty=0.5)
env.reset()
rewards = [env.step(3)[1] for _ in range(7)]
assert rewards[:4] == [1.0, 1.0, 1.0, 1.0]
assert rewards[4:] == [0.5, 0.5, 0.5]
Два файла обслуживают launch-логику: test_resume_curriculum.py (два теста) проверяет, что флаг --start-timestep корректно сообщает MazeCurriculumCallback стадию обучения при возобновлении из чекпоинта на тридцати тысячах шагов, и test_multi_runtime_launch.py (два теста) выполняет интеграционную проверку пула из трёх Unity-runtime-инстансов на портах 8000–8002. Второй файл по умолчанию пропускается через pytest.skipif при отсутствии переменной окружения RUSIM_MULTI_RUNTIME_TEST=1, поскольку требует трёх живых runtime-процессов; тесты предназначены для ручного запуска оператором перед длительной тренировкой и в нагрузочном smoke-сценарии.
8.1.4 Текущие пробелы и направления расширения¶
Тестовое покрытие платформы остаётся слабым местом инженерного контура. Во-первых, frontend-часть на React и TypeScript не покрыта тестами вообще: каталог тестов отсутствует, в package.json нет ни Jest, ни Vitest. Это обусловлено тем, что весь UI-слой проверяется ручным e2e-сценарием через Web UI и shadow-mode-предпросмотр, и в условиях небольшого размера фронта (порядка пятнадцати компонентов) автоматизация unit-тестов уступает по эффективности структурной проверке через TypeScript-компилятор. Тем не менее интеграционная проверка поведения через @playwright/test или эквивалент осталась бы желательной для критичных сценариев — выбор сценария, активация автопилота, обработка ошибки подключения.
Во-вторых, backend-набор покрывает только AutopilotSafetyFilter. Сервисы маршрутизации команд, lifecycle-управления моделями и интеграции с runtime через IKs0223RuntimeProvider верифицируются вручную; для них желателен набор тестов на основе in-memory-фейков рантайма. В-третьих, Unity-runtime тесты ограничены EditMode-режимом — PlayMode-тесты, способные подтвердить корректность физического шага Ks0223Vehicle или последовательности reset → step → state для активной сцены, не реализованы. В-четвёртых, формальный coverage-инструментарий (coverage.py для Python, dotnet test --collect:"XPlat Code Coverage" для C#, unity-test-runner с coverage-отчётом для Unity) не подключён, и оценка фактического процента покрытия не проводится. Эти четыре пробела — frontend, backend-широта, PlayMode-тесты, coverage-инструментарий — оформляют практический backlog инженерного слоя платформы.
8.2 Непрерывная интеграция¶
Платформа использует GitHub Actions как единственное средство непрерывной интеграции. На момент написания работы в каталоге .github/workflows/ находятся четыре workflow-файла, разделённых по ответственности: ci.yml (сборка и тестирование при каждом push), pages.yml (деплой документации), release-rusim.yml (публикация Python-пакета rusim), release-manifest.yml (генерация JSON-манифеста релиза для plugin SDK). Логическая структура CI/CD-конвейера показана на рисунке 8.1.
flowchart LR
Dev[git push develop] --> Trig{Trigger router}
Trig -- "any push" --> CI[ci.yml]
Trig -- "docs/**" --> Pages[pages.yml]
Trig -- "tag v*" --> Rel1[release-rusim.yml]
Trig -- "manual dispatch" --> Rel2[release-manifest.yml]
CI --> Backend[Backend build]
CI --> Frontend[Frontend build]
CI --> Unity[Unity tests]
CI --> PyLint[Python import check]
CI --> Demo[Demo proof CI]
Pages --> Build[mkdocs build --strict]
Build --> Deploy[Deploy to GitHub Pages]
Rel1 --> Wheel[Build whl + sdist]
Wheel --> Upload1[Upload to GH Release]
Rel2 --> Manifest[generate_release_manifest.py]
Manifest --> Upload2[Attach manifest to Release]
Рисунок 8.1 — Логическая структура CI/CD-конвейера
8.2.1 Workflow ci.yml: триггеры и шаги¶
Workflow ci.yml запускается на каждое событие push и pull_request без фильтрации по веткам и путям. Это упрощает контракт: любая правка в любой ветке обязана пройти базовый набор проверок. Workflow декомпозирован на пять параллельных job-ов; явные зависимости установлены только между unity-license-gate и unity-tests.
Таблица 8.2 — Состав job-ов workflow ci.yml
| Job | Назначение | Стек | Условие выполнения |
|---|---|---|---|
unity-license-gate |
Проверка наличия Unity-лицензии в secrets | bash | всегда |
backend-build |
Сборка backend.csproj |
actions/setup-dotnet@v5, .NET 8 |
всегда |
frontend-build |
Сборка Vite-фронта | actions/setup-node@v6, Node 20 |
всегда |
unity-tests |
EditMode/PlayMode тесты через GameCI | game-ci/unity-test-runner@v4 |
при наличии лицензии |
python-lint |
Импорт-чек sim_client.http_client |
actions/setup-python@v6, Python 3.11 |
всегда |
demo-proof-ci |
Smoke-цель make demo-proof-ci |
bash + Makefile | всегда |
Job unity-license-gate решает практическую проблему: интеграционные тесты Unity требуют активированной professional-лицензии, передаваемой через secrets; в публичных pull-request-ах от внешних контрибьюторов secrets недоступны, и попытка запуска unity-test-runner без лицензии завершается ошибкой. Gate-job проверяет, заполнен ли секрет UNITY_LICENSE, и записывает признак в output has_license. Job unity-tests стартует условно — if: needs.unity-license-gate.outputs.has_license == 'true' — что делает CI зелёным на чужих PR без потери способности запускать Unity-тесты на push-ах в develop.
Job python-tests устанавливает Python-пакет с extras [test,dev] (через pip install -e ".[test,dev]"), запускает ruff check, выполняет pytest и публикует coverage.xml как артефакт. Полный training-стек (torch + stable-baselines3, общий объём свыше гигабайта) в CI намеренно не ставится: соответствующие тесты используют pytest.importorskip("stable_baselines3") и пропускаются, когда heavy-deps отсутствуют. Локально разработчик собирает полное окружение командой pip install -e "python[test,dev,training]". Job rusim-smoke дополнительно валидирует все YAML-сценарии из configs/scenarios/**/*.yaml через установленный console-script.
Job demo-proof-ci исполняет цель make demo-proof-ci, реализующую graceful-skip-сценарий: если в окружении CI отсутствует Docker-демон или Unity-проект, цель не падает, а печатает диагностическое сообщение и возвращает код нуля. Назначение цели — зафиксировать наличие Makefile-точки входа в demo-flow, без претензии на запуск полной end-to-end-проверки в headless-CI.
unity-tests:
name: Unity Tests (GameCI)
needs: unity-license-gate
if: ${{ needs.unity-license-gate.outputs.has_license == 'true' }}
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: game-ci/unity-test-runner@v4
env:
UNITY_LICENSE: ${{ secrets.UNITY_LICENSE }}
with:
projectPath: src/UnityProject/uav-simulator
unityVersion: 6000.1.8f1
testMode: all
artifactsPath: artifacts
8.2.2 Workflow pages.yml: деплой документации¶
Workflow pages.yml отвечает за публикацию проектной документации на GitHub Pages по адресу https://uav-simulator.github.io/uavsimulator/. Триггер сужен до случаев, когда правки касаются именно документации: события push на ветки main и develop, ограниченные путями docs/**, mkdocs.yml и сам pages.yml. Это исключает повторные деплои при правке кода, не влияющего на документационный сайт.
Workflow собран из двух job-ов: build и deploy. build ставит Python 3.11, устанавливает mkdocs, mkdocs-material и pymdown-extensions из docs/requirements-pages.txt, исполняет mkdocs build --strict (флаг --strict превращает любые предупреждения mkdocs в ошибки) и публикует получившийся .mkdocs-site как pages-artifact. deploy использует actions/deploy-pages@v4 и берёт собранный artifact, запуская публикацию в окружение github-pages. Concurrency-группа pages с cancel-in-progress: true гарантирует, что параллельные push-ы документации не приводят к конкурирующему деплою.
on:
push:
branches: [main, develop]
paths:
- 'docs/**'
- 'mkdocs.yml'
- '.github/workflows/pages.yml'
workflow_dispatch:
concurrency:
group: pages
cancel-in-progress: true
Опция workflow_dispatch позволяет вручную перевыпустить документацию из UI Actions без необходимости создавать новый коммит — это удобно для перезапуска после правки секретов или после исправления внешних ссылок на ассеты, кэшируемые Pages.
8.2.3 Release workflows: rusim CLI и plugin SDK manifests¶
Релизный конвейер разделён на два workflow по типу артефакта. release-rusim.yml срабатывает на push тэга вида v* (а также по ручному workflow_dispatch с явно заданным тэгом) и собирает Python-пакет rusim из python/pyproject.toml. Перед сборкой workflow синхронизирует поле version в pyproject.toml со значением тэга — это исключает рассинхронизацию версии в метаданных пакета и в git-тэге, которая в случае ручной публикации часто становится источником багов. Сборка выполняется командой python -m build python --outdir dist/rusim/<tag>, после чего python -m twine check валидирует README/long-description-метаданные, и softprops/action-gh-release@v2 прикладывает .whl и .tar.gz к существующему GitHub Release.
release-manifest.yml запускается только вручную через workflow_dispatch с входным параметром tag. Workflow исполняет утилиту scripts/generate_release_manifest.py github-release, которая по тэгу собирает машиночитаемый JSON с описанием состава релиза: список приложенных к релизу файлов, их размеров, sha-256-сумм, ссылок на скачивание и сопоставление с конвенциями имени (uav-simulator-macos-<tag>.zip, <plugin>.rusim-plugin.zip). Сгенерированный манифест прикладывается к тому же GitHub Release под именем rusim-release-manifest.json и одновременно сохраняется как workflow-artifact.
Манифест предназначен для двух потребителей. Первый — CLI rusim plugin install, который при отсутствии локального файла запрашивает манифест по конвенциональному URL и определяет, какие плагины вообще доступны для установки в данной версии. Второй — Web UI и инсталлеры, которые при первом запуске обращаются к манифесту последнего релиза для проверки актуальности установленной версии и предложения обновления. Разделение на два workflow связано с тем, что release-rusim.yml исполняется автоматически на каждый тэг, тогда как release-manifest.yml запускается после того, как все артефакты — Python-пакет, Unity-builds, plugin-архивы — уже опубликованы как assets в Release; манифест должен описывать финальный состав, а не промежуточный.
8.3 Сборка backend и Web UI¶
8.3.1 Многоэтапный Dockerfile¶
Backend и Web UI поставляются как единый Docker-образ ks0223-web-mac:latest, собираемый из src/ks0223-web-mac/Dockerfile. Сборка организована в три стадии — frontend-build, backend-build, runtime — и использует docker buildkit-синтаксис # syntax=docker/dockerfile:1.7. Build-context намеренно установлен на корень репозитория, а не на каталог src/ks0223-web-mac/: Dockerfile копирует в образ как backend и frontend, так и Python-исходники python/sim_client/ (CLI rusim) — все три модуля живут в разных каталогах, и единый context упрощает координацию между ними.
Стадия frontend-build использует образ node:22-alpine. Сначала копируются только package.json и package-lock.json, выполняется npm ci (детерминистическая установка из lock-файла). Лишь после этого копируется остальной фронт и запускается npm run build. Такой порядок обеспечивает кэширование npm-слоя при изменении исходников без правки зависимостей — типовой шаблон для Node-сборок.
Стадия backend-build использует образ mcr.microsoft.com/dotnet/sdk:8.0. Сначала копируется только backend.csproj и выполняется dotnet restore, что даёт кэширование NuGet-зависимостей. После этого копируется код backend и собранный dist/ фронта в wwwroot/, после чего исполняется dotnet publish backend.csproj -c Release -o /app/publish --no-restore. ASP.NET Core-сервер таким образом отдаёт статику фронта сам — без необходимости в отдельном nginx-контейнере, что упрощает развёртывание на ноутбуке оператора.
Стадия runtime использует более лёгкий образ mcr.microsoft.com/dotnet/aspnet:8.0 (без SDK). В неё копируется опубликованный backend и устанавливается дополнительный набор Python-зависимостей: python3-venv, после чего создаётся изолированный venv /opt/rusim-venv, в который через pip install -e /app/sim_client --no-deps ставится CLI rusim из локальных исходников. Symlink /usr/local/bin/rusim указывает на entry-point этого venv. Backend при выполнении endpoint-а /api/scenarios/load делает Process.Start(rusim, scenario reset <yaml>), и этот endpoint должен работать независимо от того, установлен ли rusim на хосте.
FROM node:22-alpine AS frontend-build
WORKDIR /src/frontend
COPY src/ks0223-web-mac/frontend/package*.json ./
RUN npm ci
COPY src/ks0223-web-mac/frontend/ ./
RUN npm run build
FROM mcr.microsoft.com/dotnet/sdk:8.0 AS backend-build
WORKDIR /src
COPY src/ks0223-web-mac/backend/backend.csproj backend/
RUN dotnet restore backend/backend.csproj
COPY src/ks0223-web-mac/backend/ backend/
COPY --from=frontend-build /src/frontend/dist/ backend/wwwroot/
RUN dotnet publish backend/backend.csproj -c Release -o /app/publish --no-restore
FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS runtime
WORKDIR /app
COPY --from=backend-build /app/publish/ ./
RUN python3 -m venv /opt/rusim-venv \
&& /opt/rusim-venv/bin/pip install -e /app/sim_client --no-deps \
&& ln -s /opt/rusim-venv/bin/rusim /usr/local/bin/rusim
EXPOSE 5058
EXPOSE 5051/udp
HEALTHCHECK --interval=10s --timeout=3s --start-period=15s --retries=6 \
CMD curl -fsS "http://localhost:5058/api/health?clientId=hc&runtimeMode=real-robot" >/dev/null || exit 1
CMD ["dotnet", "backend.dll"]
Открыты два порта: 5058/tcp для HTTP API и Web UI и 5051/udp для приёма телеметрии напрямую от платформы KS0223 в reverse-канале. Healthcheck с интервалом десять секунд опрашивает /api/health с фиктивными clientId=healthcheck и runtimeMode=real-robot; вид параметров здесь продиктован контрактом backend — он требует обязательной идентификации сессии и режима работы для всех endpoint-ов.
8.3.2 Makefile цели: docker-build, docker-run, docker-update¶
Файл src/ks0223-web-mac/Makefile оформляет операторский интерфейс над контейнером. Семь основных целей, перечисленных в таблице 8.3, покрывают сценарий полной перенастройки локального стенда оператора без необходимости запоминать длинные команды docker run с правильным набором bind-mount-ов.
Таблица 8.3 — Основные цели Makefile
| Цель | Назначение |
|---|---|
docker-pull-base |
Скачать базовые образы (node:22-alpine, dotnet/sdk:8.0, dotnet/aspnet:8.0) |
docker-build |
Собрать образ ks0223-web-mac:latest из repo-root context |
docker-rebuild |
То же с --pull --no-cache для гарантии чистого состояния |
docker-run |
Запустить контейнер с bind-mount-ами logs/ и configs/scenarios/ |
docker-stop |
Удалить запущенный контейнер (idempotent) |
docker-wait |
Опрос /api/health до получения положительного ответа |
docker-update |
Композитная цель: build + run + wait |
Цель docker-update представляет основной операторский путь. Она исполняется как make docker-update после правки backend или frontend и за одну команду пересобирает образ, перезапускает контейнер и дожидается готовности healthcheck. Это исключает класс ошибок «оператор перезапустил контейнер, но забыл пересобрать образ»: цель всегда исходит из текущего состояния исходников.
REPO_ROOT ?= $(CURDIR)/../..
SCENARIOS_DIR ?= $(REPO_ROOT)/configs/scenarios
docker-build:
docker build -t $(IMAGE) -f $(CURDIR)/Dockerfile $(REPO_ROOT)
docker-run: docker-stop
mkdir -p $(LOGS_DIR)
docker run -d --name $(CONTAINER) \
-p $(PORT):5058 -p $(UDP_PORT):5051/udp \
-v $(LOGS_DIR):/app/logs \
-v $(SCENARIOS_DIR):/app/configs/scenarios:ro \
--add-host host.docker.internal:host-gateway \
$(IMAGE)
docker-update: docker-build docker-run docker-wait
Параметризация через ?= позволяет переопределять имя образа, имя контейнера, порты и путь логов через переменные окружения без правки Makefile. Это необходимо для одновременного запуска нескольких контейнеров — например, для shadow-mode-сценария (раздел 7.5.3), когда параллельно работают экземпляры sim-mode и real-robot-mode на разных портах.
8.3.3 Bind-mount configs/scenarios для динамической перезагрузки¶
Каталог configs/scenarios/ в репозитории хранит YAML-описания сценариев — наборов параметров rusim scenario reset <yaml>, фиксирующих сцену, агента, режим управления, доменные параметры и параметры eval. Два архитектурных свойства этого каталога заметно влияют на поставку. Во-первых, сценарии — данные, а не код: правка YAML не требует пересборки. Во-вторых, оператор работает со сценариями из хост-системы (через любой текстовый редактор), а исполняется reset уже на стороне backend, который живёт в контейнере.
Чтобы соединить эти свойства, в docker-run задан bind-mount -v $(SCENARIOS_DIR):/app/configs/scenarios:ro, отображающий хостовый каталог configs/scenarios/ в read-only-режиме внутрь контейнера по пути /app/configs/scenarios. Backend читает YAML из контейнерного пути через WebUI scenario-picker (выпадающий список доступных сценариев), но физически файлы остаются на хосте — оператор редактирует их любым редактором, и следующий вызов rusim scenario reset подхватывает новую версию без перезапуска контейнера.
Read-only-флаг :ro исключает класс ошибок, при которых контейнерный backend случайно записал бы в каталог сценариев, нарушив их git-state. Каталог логов наоборот монтируется в read-write-режим: SessionLogs__Directory в backend настроена на /app/logs, и контейнер пишет туда журналы сессий, доступные оператору на хосте. Соответствующая переменная среды RUSIM_BASE_URL=http://host.docker.internal:8000 указывает контейнерному backend, как достучаться до Unity-runtime, работающего в Editor на хост-системе через bridge-алиас host.docker.internal. Альтернативный путь — запуск Unity в headless-режиме внутри отдельного контейнера — рассмотрен в разделе 8.4.2 как направление расширения.
8.4 Сборка Unity runtime¶
8.4.1 Build pipeline и target платформы¶
Сборка standalone-runtime реализована классом RuntimeBuildPipeline в Assets/Editor/RuntimeBuildPipeline.cs. Класс предоставляет три статических entry-point — BuildMacOsRuntime, BuildWindowsRuntime, BuildLinuxRuntime — соответствующих трём целевым платформам: BuildTarget.StandaloneOSX, BuildTarget.StandaloneWindows64, BuildTarget.StandaloneLinux64. Точки вызываются из CI или с командной строки через Unity -batchmode -nographics -executeMethod UavSimulator.EditorTools.RuntimeBuildPipeline.BuildMacOsRuntime.
Перед собственно BuildPipeline.BuildPlayer исполняются два preprocessor-шага. RuntimeShaderAssetSeeder.EnsureRuntimeShaderAssets фиксирует список шейдеров, которые должны попасть в build (включая шейдеры, используемые только в runtime-материалах через RuntimeMaterialCompatibility). Без явного включения Unity вырезает их по graph-анализу как неиспользуемые. PluginCatalogSeeder.SyncBuiltinPluginCatalog синхронизирует BuiltinPluginCatalog-asset со списком встроенных плагинов — раздел 5 настоящей работы описывает соответствующий механизм; для сборки важно, что ассет каталога должен быть актуальным на момент BuildPipeline.BuildPlayer.
private static void BuildRuntime(BuildTarget target, string defaultOutput)
{
RuntimeShaderAssetSeeder.EnsureRuntimeShaderAssets();
PluginCatalogSeeder.SyncBuiltinPluginCatalog();
var outputPath = Environment.GetEnvironmentVariable("RUSIM_BUILD_OUTPUT")
?? defaultOutput;
var scenePath = Environment.GetEnvironmentVariable("RUSIM_BUILD_SCENE")
?? "Assets/Scenes/TrackScence.unity";
var options = new BuildPlayerOptions
{
scenes = new[] { scenePath },
locationPathName = Path.GetFullPath(outputPath),
target = target,
options = BuildOptions.None,
};
var report = BuildPipeline.BuildPlayer(options);
if (report.summary.result != BuildResult.Succeeded)
throw new InvalidOperationException($"Runtime build failed for {target}");
}
Параметры RUSIM_BUILD_OUTPUT и RUSIM_BUILD_SCENE через переменные среды позволяют CI задавать нестандартный путь вывода и нестандартную стартовую сцену без правки исходников. По умолчанию сборка идёт в build/runtime/<platform>/ со сценой Assets/Scenes/TrackScence.unity. После завершения build summary попадает в Debug.Log, что облегчает анализ в консольных логах GameCI.
8.4.2 Headless mode для тренировок¶
Тренировочный сценарий запускает Unity-runtime в режиме без активного Game-View и аудио. На macOS и Linux это достигается флагами -batchmode -nographics при запуске исполняемого файла; для тренировок на Windows-машине автора используется тот же набор флагов через WSL2-подсистему — это соответствует основному паттерну запуска, описанному в разделе 6.1.2.
В headless-режиме Unity не создаёт окно и не выполняет рендер в primary-buffer, но Camera.targetTexture и off-screen render через RenderTexture продолжают работать корректно. Это критично для платформы: тренировочная среда не видит «играбельной» сцены, но получает RGB-кадры через CameraFrameProvider, который рендерит в RenderTexture 84×84, читает её через AsyncGPUReadback и отдаёт обвязке через HTTP API (раздел 6.1). Описанная схема позволяет одной и той же сборке runtime обслуживать как интерактивный сценарий оператора (с активным Game-View), так и тренировочный multi-runtime-пул из трёх–четырёх инстансов headless-режима.
Параллельный запуск нескольких runtime-инстансов на одной машине требует разнесения портов HTTP-сервера: переменная среды UAVSIM_API_PORT принимает значение 8000–8003 и определяет, на каком порту runtime будет слушать reset/step-команды. Multi-runtime-launch-helper python/training/multi_runtime_launch.py (раздел 6.3.4) запускает три headless-копии одной и той же сборки с разными портами и проверяет их healthcheck через тест test_multi_runtime_launch.py. На GPU GeForce RTX 3070 четыре одновременных headless-инстанса дают эффективную скорость порядка ста двадцати environment-шагов в секунду на инстанс, что соответствует профилю PPO-обучения с n_envs = 4.
8.4.3 Editor-only ограничения POLYGON DemoScene¶
Часть содержимого Unity-проекта остаётся доступной только в Editor-режиме. Наиболее заметный пример — встроенная демо-сцена POLYGON City Pack (Assets/POLYGON city pack/scene/DemoScene.unity), используемая трассой track.city.polygon.v1 через CityPolygonTrack. В режиме useDemoScene = true (по умолчанию) трасса аддитивно загружает сцену, переподписывает её корневые GameObject-ы под себя и заменяет встроенные камеры и источники света. Это даёт визуально полноценный city-blocks-стенд из примерно девятисот девяноста девяти GameObject-ов, но привязывает сцену к Editor-only-механизму EditorSceneManager.OpenScene.
// Editor-only — for standalone builds, the POLYGON pack would need
// Addressables / Resources/ migration.
public sealed class CityPolygonTrack : TrackBase
{
private const string DemoScenePath =
"Assets/POLYGON city pack/scene/DemoScene.unity";
...
}
Соответствующие методы в CityPolygonTrack, BuiltinPluginFactory и RoadSystemRealisticTrack обёрнуты #if UNITY_EDITOR — это исключает попадание Editor-only-API в standalone-build, но одновременно делает указанные сцены недоступными в собранном .app/.exe. Альтернативное решение — миграция POLYGON-пакета на Addressables или Resources/ — потребует пересборки prefab-структуры пакета и не соответствует условиям лицензии POLYGON City Pack, требующим распространения исходных prefab-ов через Asset Store. По состоянию на текущий релиз city-demo-сцена работает только в Unity Editor, и cardboard-corridor- и cardboard-maze-сцены, лишённые этой зависимости, остаются основным сценарием для тренировок и операторских демонстраций. Этот компромисс зафиксирован в разделе 4.6 настоящей работы как известное ограничение и не препятствует выполнению ключевого sim-to-real-сценария (главы 6 и 7).
8.5 Поставка плагинов¶
8.5.1 Формат .rusim-plugin.zip¶
Формат поставки плагинов формализован в разделе 5 настоящей работы (полное описание .rusim-plugin.zip-архива и утилиты PluginExporter); здесь раздел рассматривает его исключительно с инженерной стороны — как часть конвейера поставки. Готовый плагин представляет собой ZIP-архив с фиксированным именем <pluginId>.rusim-plugin.zip и четырёхфайловым составом: manifest.json (идентификатор, тип, версия, требуемая версия рантайма), descriptor.json (сериализованный VehiclePluginDescriptor или TrackPluginDescriptor), device-contract.json (только для vehicle, формальное описание сенсорного контракта), README.md (auto-generated инструкция по установке).
Эталонный архив vehicle.arcade.green.v1.rusim-plugin.zip имеет суммарный размер четыре килобайта четыреста два байта при четырёх файлах: 216 байт manifest, 453 байта descriptor, 3369 байт device-contract, 364 байта README. Малый размер — следствие того, что плагин не несёт мешей или текстур; графика собирается из ассетов рантайма по идентификаторам. Это упрощает версионирование: совместимость плагина с релизом рантайма проверяется по полю compatibleRuntime манифеста, а не по бинарному совпадению ассетов.
Сборка плагина выполняется в Unity Editor (Tools > UavSimulator > Export Plugin (.zip)), а не в CI: отделение сборки от рантайма исследовано в разделе 5.4 как часть архитектурного решения. На уровне поставки это означает, что плагины собираются разработчиком вручную и публикуются как отдельные assets к существующему GitHub Release — отдельного workflow для plugin-build в CI на момент написания работы нет. Это направление расширения: автоматизированный workflow plugin-build.yml, который по push-у в packages/<plugin>/ собирал бы и валидировал архив, отвечал бы стандартному паттерну поставки UPM-пакетов.
8.5.2 Распространение через UPM¶
Plugin SDK — набор базовых классов и редакторных инструментов для разработчика плагина — поставляется как Unity Package и подключается стандартным механизмом UPM. Манифест пакета packages/com.uav-simulator.plugin-sdk/package.json фиксирует его идентификатор и метаданные:
{
"name": "com.uav-simulator.plugin-sdk",
"version": "0.1.0",
"displayName": "UAV Simulator Plugin SDK",
"description": "SDK for developing plugins (vehicles, tracks)…",
"unity": "6000.1",
"author": { "name": "Nikita Gorovenko" }
}
Пакет содержит две корневые директории: Runtime/ (классы VehiclePluginBase, TrackPluginBase, descriptor-types, runtime-utility) и Editor/ (ассистенты валидации и PluginExporter — инструмент Tools-меню для упаковки .rusim-plugin.zip). Установка в Editor стороннего разработчика выполняется через диалог Package Manager в режиме «Add package from git URL» с адресом https://github.com/uav-simulator/uavsimulator.git?path=packages/com.uav-simulator.plugin-sdk. UPM сам клонирует репозиторий, читает package.json указанного path и подключает его как пакет — что освобождает разработчика от необходимости копировать SDK в свой проект.
Полный сценарий разработки плагина — от установки SDK через UPM до экспорта .rusim-plugin.zip — изложен в разделе 5; настоящий подраздел фиксирует только механику поставки. Привязка пакета к git-URL означает, что версионирование SDK совпадает с git-тэгами репозитория uav-simulator: разработчик плагина может закрепиться на конкретной ревизии SDK через ?path=…&ref=v0.1.2, что обеспечивает стабильность intra-project разработки. Публикация SDK в публичный UPM-registry (registry.npmjs.org/scopes/uav-simulator или openupm) на текущий момент не выполнена — такой шаг становится оправданным после стабилизации API SDK и зафиксирован как направление расширения.
8.5.3 Каталог dist/plugins/¶
Каталог dist/plugins/ в репозитории содержит готовые .rusim-plugin.zip-архивы для последующего прикрепления к GitHub Release. По состоянию на момент написания работы он содержит один архив — vehicle.arcade.green.v1.rusim-plugin.zip — служащий референс-образцом для проверки rusim plugin install-цепочки. Каталог сознательно расположен в dist/, а не в src/: содержащиеся в нём архивы являются артефактами сборки, а не исходниками, и их содержимое полностью воспроизводимо из исходного descriptor-asset-а через PluginExporter. Размер dist/plugins/ ограничен этим — добавление множества больших archive-ов в репозиторий нарушило бы границу между source-tree и build-output.
Упомянутая в разделе 8.2.3 утилита generate_release_manifest.py индексирует содержимое dist/plugins/ при формировании JSON-манифеста релиза: каждый архив добавляется в манифест с полями pluginId, version, sha256, downloadUrl (определяемый по конвенции имени release-asset). CLI rusim plugin install --from-release v0.1.2 vehicle.arcade.green.v1 пользуется именно этим манифестом — он позволяет инсталлеру не знать заранее, какие плагины опубликованы в данной версии релиза, и запросить полный каталог одним HTTP-запросом.
8.6 Документация и Pages¶
8.6.1 mkdocs material: структура nav¶
Документационный сайт собран на mkdocs c темой mkdocs-material. Конфигурация находится в mkdocs.yml (71 строка) и определяет четыре существенных свойства: ru-локализация интерфейса (theme.language: ru), тёмная палитра (scheme: slate, primary blue grey), включённое навигационное меню с разделами (navigation.sections, navigation.expand, navigation.top) и набор markdown-extensions, среди которых ключевыми являются pymdownx.superfences с custom-fence для Mermaid и pymdownx.highlight для syntax-highlighting.
Структура nav декларирует двадцать шесть пунктов верхнего и второго уровней, отражающих логическую организацию проекта: операторская документация (Старт, Установка, Использование, CLI), архитектурно-API-блок (Архитектура, API, Model Lifecycle, Глоссарий), отчётный блок преддипломной практики (Обзор, Спринт 1, Спринт 2, Спринт 3 с подпунктами по ML- и Sprint-анализу), магистерская диссертация (12 файлов из docs/master-thesis/). Иерархия nav используется не только для меню сайта, но и как seed для боковой колонки документации, и для структурного оглавления внутри каждой страницы.
Поле exclude_docs исключает из сборки крупные docx-файлы преддипломной практики (report/prediploma-practice/*.docx) и каталог reports/, содержащий технические PNG-скриншоты и видео; включение этих файлов в сайт привело бы к разрастанию артефакта Pages и нарушению лимита GitHub Pages в один гигабайт. Раздел validation.links отключён по большинству ветвей (not_found: ignore, absolute_links: ignore), поскольку проект содержит большое число cross-links между маркдауном и docx/pdf-evidence-материалами, и строгая валидация дала бы ложно-положительные срабатывания.
8.6.2 Деплой через GitHub Actions на Pages¶
Деплой документации полностью автоматизирован workflow-ом pages.yml, описанным в разделе 8.2.2. На уровне поведения сайта это означает следующий сценарий: автор редактирует маркдаун в docs/, выполняет git push develop, через одну–полторы минуты обновлённый сайт становится доступен по адресу https://uav-simulator.github.io/uavsimulator/. Никаких ручных операций — генерации сайта, копирования в gh-pages-ветку, загрузки артефакта — не требуется.
Локальный preview-сайта поддерживается через венв .venv-docs/, в котором установлен mkdocs из docs/requirements-pages.txt. Команда .venv-docs/bin/mkdocs serve запускает локальный сервер на 8000-м порту с auto-reload, что даёт обратную связь по правкам в реальном времени. Команда .venv-docs/bin/mkdocs build --strict исполняется автором перед push-ем для проверки, что workflow не упадёт на флаге --strict. Это превентивная защита от ошибок mkdocs (битые ссылки, неизвестные admonition-типы, проблемы с heading-структурой), которые иначе обнаруживаются только в CI.
8.6.3 Auto-rebuild при push на develop¶
Триггер pages.yml фильтрует push-события на ветки main и develop по путям docs/**, mkdocs.yml, .github/workflows/pages.yml. Это даёт два важных свойства. Во-первых, пересборка документации происходит автоматически на каждый push в эти ветки — без необходимости в дополнительных тэгах или ручных шагах. Во-вторых, push-и, не затрагивающие документацию, не запускают workflow, что экономит CI-минуты и снижает шум в Pages-environment.
Включение develop в список целевых веток (помимо main) — сознательное решение, отражающее workflow проекта. Магистерская диссертация и отчёты практики находятся в активной разработке, и rebuild по develop обеспечивает оператору и руководителю немедленный доступ к актуальной версии без необходимости в merge-ах в main. Pages по своей механике публикует только последнюю успешную сборку, что соответствует логике live-документации.
Concurrency-группа pages с cancel-in-progress: true обеспечивает корректное поведение при последовательных коммитах. Если в течение тридцати–шестидесяти секунд (типичное время сборки) приходит второй push в docs/, текущий workflow отменяется и стартует новый — это исключает race condition между двумя одновременными деплоями и публикует только финальную версию. Без этой настройки последний коммит мог бы быть «обогнан» предыдущим деплоем и не попасть на Pages до следующего push-а.
8.7 Релизный процесс¶
8.7.1 Существующие версии v0.1.0, v0.1.1, v0.1.2¶
На момент написания работы платформа имеет три выпущенные версии — v0.1.0, v0.1.1, v0.1.2, опубликованные в марте 2026 года. Каждой версии соответствует git-тэг и каталог dist/<tag>/ с macOS-сборкой и контрольной суммой. Состав релизов приведён в таблице 8.4; цифры размеров получены прямым stat-замером файлов в репозитории.
Таблица 8.4 — Текущие релизы платформы
| Тэг | Дата | Артефакт | Размер | Тематика |
|---|---|---|---|---|
v0.1.0 |
2026-03-12 | uav-simulator-macos-v0.1.0.zip |
45,2 МБ | Первый публичный релиз; добавление команды rusim upgrade |
v0.1.1 |
2026-03-12 | uav-simulator-macos-v0.1.1.zip |
45,1 МБ | Tag-driven release pipeline; стабилизация механизма обновления |
v0.1.2 |
2026-03-12 | uav-simulator-macos-v0.1.2.zip |
45,1 МБ | Синхронизация версии Python-пакета rusim с git-тэгом |
Каждый артефакт сопровождается отдельным .sha256-файлом с контрольной суммой; пример для v0.1.2: 050ca9caba5f364e5537a6618d5d376711659c74a9795694e9198308eb335a83. Пользователь, скачавший релиз, может проверить целостность архива командой shasum -a 256 -c uav-simulator-macos-v0.1.2.zip.sha256 без обращения к внешним репозиториям. Архив содержит macOS-bundle uav-simulator.app со всем содержимым, требуемым для standalone-запуска: подписной код-ресурс, исполняемый бинарь uav-simulator, burst-оптимизированные plug-in-bundles, default preferences plist, ассеты сцены TrackScence.unity. Установочный сценарий пользователя сводится к распаковке архива и переносу .app в Applications/; никакого инсталлятора не требуется.
Linux- и Windows-сборки соответствующих версий пока не опубликованы. RuntimeBuildPipeline поддерживает три target-платформы (раздел 8.4.1), но регулярная публикация Linux- и Windows-сборок упирается в отсутствие соответствующих CI-runner-ов с Unity-лицензией; на текущий момент сборка выполняется на основной dev-машине автора под macOS и публикуется как единственный артефакт. Это направление расширения CI: расширение release-rusim.yml или появление отдельного release-runtime.yml с матрицей runs-on: [macos-latest, ubuntu-latest, windows-latest] и Unity-license-подкачкой через Game CI.
8.7.2 Семантическое версионирование платформы¶
Версионирование платформы следует упрощённой semantic-versioning-схеме vMAJOR.MINOR.PATCH. На текущей фазе стабилизации API схема используется в свёрнутой форме: MAJOR = 0 фиксирует pre-1.0-статус (API не гарантирует совместимости между минорами), MINOR инкрементируется при существенных функциональных изменениях, PATCH — при чисто-багфиксных правках без изменения API. Переход в 1.0.0 запланирован после стабилизации plugin-SDK-API и завершения формального API-аудита; на текущий момент платформа находится в фазе 0.x.
Несколько объектов в репозитории должны иметь согласованную версию: git-тэг, поле version в python/pyproject.toml, поле version в packages/com.uav-simulator.plugin-sdk/package.json, имя файла release-asset (uav-simulator-macos-vX.Y.Z.zip), Application.version в Unity Player-settings. На момент написания работы синхронизация выполняется частично автоматически: workflow release-rusim.yml (раздел 8.2.3) перезаписывает pyproject.toml под значение тэга; остальные точки синхронизируются вручную через подготовительный коммит перед тэгом. Полная автоматизация — например, через bumpversion-подобный инструмент, обновляющий все четыре места одной командой — относится к направлениям расширения, особенно учитывая, что v0.1.2 явно появился из-за ошибки рассинхронизации pyproject.toml-версии с тэгом v0.1.1.
Plugin-API-совместимость отдельно регулируется полем compatibleRuntime в манифесте плагина: значение >=0.1.0 означает, что плагин совместим с платформой, начиная с указанной версии. До перехода в 1.0 совместимость плагинов на стороне MINOR-инкрементов не гарантируется; разработчик плагина обязан явно объявить минимальную версию рантайма и при необходимости пересобирать плагин под новые мажор-минор-выпуски.
8.7.3 Release notes и CHANGELOG.md¶
Release notes к каждому тэгу формируются вручную через GitHub-UI на основе соответствующего git-log-диапазона; автоматизированной генерации release-notes из коммитов на момент написания работы нет. Коммитное сообщение тэга v0.1.2 — «fix: sync rusim package version with release tag» — служит первой строкой release notes; полное описание изменений добавляется автором в текстовое поле GitHub Release при публикации.
Файл CHANGELOG.md репозитория содержит исторический материал — записи Sprint 1 преддипломной практики и более старую секцию Added/Changed/Fixed/Removed с правками root Makefile, ROS2-helper-ов и notebook-ов. Структурно файл соответствует формату Keep a Changelog, но он не синхронизирован с текущими git-тэгами v0.1.0–v0.1.2: разделы по этим версиям отсутствуют. Это отражает реальный статус: изменения между релизами фиксировались в commit-сообщениях и в GitHub Release-описаниях, но не дублировались в CHANGELOG.md. Восстановление полной consistency между release-tags и CHANGELOG.md — отдельная задача из направлений расширения; решается она либо ручным заполнением (на текущем масштабе четырёх–пяти тэгов это разумно), либо автоматической генерацией через git-cliff или эквивалент при публикации тэга.
Резюмируя, релизный процесс платформы сейчас находится в стабильно-минимальной форме: тэги фиксируют состояния, артефакты воспроизводимо собираются workflow-ами, манифест и хэш-суммы сохраняются для верификации, документация публикуется автоматически. Существенные пробелы — отсутствие Linux- и Windows-сборок, неполная автоматизация синхронизации версий, расхождение CHANGELOG.md с release-tags и слабое тестовое покрытие — задокументированы выше и формируют конкретный backlog инженерного контура. Эти пробелы не препятствуют выполнению ключевого sim-to-real-сценария, описанного в главе 7, но определяют направления, в которых платформу предстоит развивать после защиты магистерской работы.