Files
airdef/docs/CODEX-HANDOFF.md
T
chexit e7934a1ebb
build / mod (push) Failing after 22s
release / windows (push) Failing after 2s
Give the enemy a structured intent and a durable route queue (#127)
Stage 2 of #127. A smart drone now carries why it is aimed where it is
(Intent: plan, sead, react, replace) apart from the aim bookkeeping
(unit, since, switches, confidence). Assignments are throttled per
adaptive tick and per contact, a fresh disclosure replaces an aim only
after a hold, within a switch cap and when the change is worth a new
route, and a killed aim is replaced as a separate, lower-priority
decision. The queue is bounded and taken by priority (plan > sead >
react > replace); unknown land is no longer free in a replanned route.

The autosave now carries the air state: the drones in flight, the launch
queue, every pending job with its id, aim, unit, version, priority,
intent and relief version, the contacts the jobs were chosen from and
the relief snapshots they plan over. The format is a versioned `air 1`
section; older saves read as before, scenarios do not carry the sky,
stale, ghost and duplicate jobs are cancelled on load, and the section is
bounded (512 drones, 512 route points, 4096 contacts, 8 reliefs, 900 KB).
A periodic save waits while a raid is only being planned.

enemy_picture() now explains targets and pending jobs (aims, aimed_by,
pending_jobs, reason, work_per_tick) in English, and intel_bench records
the queue and intent evidence next to completion, losses and failures.
2026-11-06 18:36:20 +03:00

72 KiB
Raw Permalink Blame History

Передача разработки AirDef

Это журнал фактически сделанных изменений и ограничений проверки, а не список обещанных функций. Рабочая ветка kimi-work; issues не закрывать, публиковать комментарии с коммитом и проверками. Движок — отдельный chexit/horizon-airdef, ветка airdef, подключён точным gitlink horizon/.

Текущее состояние: 4 ноября 2026

Последнее продолжение пяти задач: CONTINUATION-2026-11-04.md. Движок 57f8b2853c4443f7e3ff5701600cbb262b53118f, API29. В этом файле ниже сохранён исторический журнал предыдущих серий; старые SHA и числа тестов не заменяют новую контрольную точку.

След #91 продолжен: catalog/specs.json — единственная копия числовых строк дронов, ракет и комплексов; python3 tools/gen_catalog.py генерирует specs_generated.rs и unit_specs_generated.rs, --check (CI) валит расхождение. Числа не менялись; сверка с литералами подтвердила 1.6 как фактический голос P1-SUN (недостижимый P1sun => 1.7 в старом match не в счёт); на это добавлена Rust-регрессия. Остаток: варианты комплексов и игровая приёмка.

Продолжение #129: стадии холодного пуска и разгон (6 ноября 2026)

Ветка agent/issue-129-cold-launch, кодовый коммит 4b013fb7f30dbaa87a266b27a31911937fc523b4 (документация, измерения и кадры — следующим коммитом этой же ветки). Игра/движок не менялись: pin 278f356aa604fa21c2d15d0081823bbe0f891df2, API30. Push не выполнялся, issues не закрывались.

Контракт стадий. У MissileSpec появились eject и eject_drag; cold — тики физического выброса, boost — тики departure. guided(age) = age >= cold + boost; основная ветка наведения, самонаведение и облёт рельефа больше не могут включиться до зажигания (раньше было age >= boost, что при большом cold включало наведение до мотора). Зажигание — по-прежнему age > cold; событие MotorIgnition, звук, факел и голос используют текущую позицию ракеты. departure_ramp растёт 0→1 за boost; темп поворота в departure — turn·(0.2 + 0.8·ramp). Тяга ракетных профилей — accel·(0.45 + 0.55·ramp), на guidance — полная. P1-SUN (flame=0) сохраняет полный разгон без ракетной струи.

Профили. M48n6: cold=20, eject=2.4, eject_drag=0.07 — 1,00 с; M77n6: cold=24, eject=2.6, eject_drag=0.07 — 1,20 с; IrisT/M9m338: cold=8, eject=1.8/1.6, eject_drag=0.1 — 0,40 с. Рельсовые профили (cold=0) получили только spool тяги; ранний поворот на рельсовом пуске был проверен и отменён: он уводил Pac3 в землю на холмах и валил an_interceptor_over_rolling_land_does_not_hit_the_ground. speed, accel, turn, pk, fuse и правила столкновений не менялись.

RM48U. EJECT_TICKS 5 → 20. Выброс неосвещённый и замедляется; зажигание на EJECT_TICKS, факел/звук/голос с этого момента; насчитанное время полёта включает выброс; дым в записи начинается от точки зажигания, а не от горловины. Live, repeat и export читают одно и то же состояние lit/записанное MotorIgnition.

Проверки. core release 206 + 8 сценариев, 8 ignored; адаптер 67 + 1 boundary + 5 сценариев; настоящий .hmod — 30 шагов wasm_contract.toml; gen_catalog.py --check и gen_models.py --check без расхождений; test_gen_assets.py — 10 тестов; git diff --check чист. Новые регрессии: стадии по порядку и отсутствие powered-эффектов до зажигания; guided не раньше cold для всех профилей; непрерывность скорости/поворота и throttled первый powered тик; P1-SUN без факела; записанное зажигание через cold тиков и missile_is_lit по нему; RM48U без голоса/факела до зажигания и ровно один звук зажигания.

Баланс. Сетка #120 (6 вариантов × 8 seed, 2208 целей на сторону, baseline dc36d67): сбито 1988 → 1939, прорывов 216 → 258, ракет 3461 → 3467; near200 318 → 311, far1500 338 → 334, hills20 334 → 319; energy_lost 0 → 0, expired 1 → 0, scrub 2 → 4. SAMS=20 SIZE=12, 12 seed: 203 → 201 сбитых, 1 → 3 прорыва, 355 → 360 ракет. Отдельные CSV: сетка, рейды. Это намеренное следствие более длинного старта; скрытого повышения pk или максимальной скорости нет.

Парная съёмка. Один scratch-мир ColdLaunch129, одно сохранение, сценарий pair129, seed 965460218386186365, один клиент/движок API30, один режиссёр (/airdef replay export … best fps=30 size=1280x720 crf=16 sec=24), Metal/offscreen. Сценарий: С-400 1.5 72 8.5, роли 31, один Х-22 с 900; мод до — dc36d67, после — 4b013fb. Оба MP4: h264 1280×720 30 fps + AAC 48 кГц; до — 403 кадра/13,433 с, после — 412/13,733 с. План «Пуск» стартует на кадре 94 в обоих; первое освещение центра (окно 100×120, порог 100) до — кадр 101 (0,233 с после старта, cold=5 = 0,25 с), после — кадр 121 (0,900 с, cold=20 = 1,00 с); к 124 кадру устойчиво ≈198. Это подтверждает сдвиг холодной паузы на ≈0,67 с между сборками. MP4 в docs/media/cold-launch перекодированы в CRF26 для хранения; исходные файлы, их SHA-256, ffprobe и логика измерения — в issue-129-pair.json. Ограничение: у агента нет ввода изображений; проверены параметры, старт плана, яркость центра и парные кадры, но композицию и красоту кадров человеком ещё нужно оценить. Предпусковые кадры не пиксельно идентичны (живой прогон, движение камеры, анимация листвы); совпадают мир/сейв/сценарий/seed/метод камеры. Отдельный very-close статичный план и live-MP4 не сняты (--offscreen без окна). Это не полная визуальная приёмка.

Продолжение #122 (второй налёт из UI)

Точный лог Windows: stopped in UI event (fuel budget 1000000000, remaining Some(0)), backtrace known_units ← order_raid ← launch_wave ← ui_select — другой класс, чем тик 1f28f83. Причина: order_raid пересчитывал снимок известных комплексов на каждом дроне, а сам снимок был квадратичен по журналу и группировке. Исправление: один снимок на приказ (+ O(журнал+группировка) через множества, price_grid_on переиспользует свой снимок); семантика целеуказания и выбора носителей не менялась, сверка с прежним предикатом в нативном тесте, /stats в WASM совпал побайтово. Лимиты не трогали, HD/запись не отключали, размер волны не ограничивали.

Воспроизведение в настоящем WASM (измеренный fuel): до — второй приказ 111 дронов исчерпывает ровно 1e9 в событии (failures=1), тот же приказ 11 дронов — 247805865; после — тот же сценарий целиком: max command 32946177/1e9, tick 236581288/300M, ошибок 0. Диагностический гость (trigger.patch вызывает обработчик ui_select(BTN_LAUNCH)) и точный сценарий: issue-122-ui-fuel. Воспроизвести удалось только с разнесённой группировкой: стек в одной точке гибнет от одной боеголовки (known=0, приказ дёшев) — у пользователя силы дожили до второго приказа.

Закреплённые регрессии: нативные «эквивалентность снимка» и «второй умный приказ с толстым журналом принят и запущен»; тяжёлый WASM fixture расширен двумя умными приказами через airdef preset + airdef raid (функция кнопки ПУСК) — 4 принятых приказа, SHA fixture обновлён; лёгкий контракт 30 шагов с preset и отказом пустой волны. Новая команда airdef preset <n> — script-эквивалент кнопки готового состава.

Последний общий прогон: core release 195 + 8 сценариев / 7 ignored; адаптер 65 + 1 boundary + 5 сценариев; лёгкий WASM 30 шагов; тяжёлый WASM 283 шага (tick max 182413686/300M, command max 6939810/1e9, failures=0), отчёт flat-wasm-raid-budget; 6 тестов отчёта budget и build-тест прошли. Ни одно issue не закрыто; пуш не выполнялся.

Контрольная точка 3 ноября

Код предыдущих четырёх задач интегрирован до 6e2d661. Продолжение #102/обратного вывоза опубликовано до aab3e78; последнее продолжение расследования налётов — 2932364. Текущий движок — f95f03c08ec2916a0e5bd3b2d4a2a790e7937b47, API 27. README описывает доступное поведение; IMPLEMENTATION-STATUS.md отделяет реализацию от приёмки. Ни одно issue не закрыто. Участники: Codex (интеграция, #119, ревью, документация) и три GPT-6.1 Terra (энергетика, типизированная доставка, излучение). Новые числа — только игровые.

Issue Опубликованные контрольные коммиты
#119 c96676f: укрытия, read-only geometry, четыре проверки
#118 c775f31, 952a10f: доставка ТЗМ, возврат снятой ракеты ПВО, реальное движение и save/restore
#121 26b258f, 0df225a, 88f9405, 9e1e0f5, b377a5d, ce76f20: излучение, UI/save/replay, наблюдения, last-known SEAD
#120 143d9b2 → e38ca0d, затем a7b8cb9: конечное горение, coast, swept impact, диагностика, звук, исправленное ветвление фаз
WASM 6e2d661: проверка On/Off и запрета управления отсутствующим радаром, 24 шага

Для обновления существующей установки сначала git switch kimi-work, затем git pull --ff-only, python3 tools/airdef.py all, python3 tools/airdef.py play. all обновляет точный сабмодуль, собирает игру/мод и устанавливает пакет. На Windows обычно используется python вместо python3.

Продолжение #102 и обратного вывоза RM48U

  • d3378b3: fork() синхронизирует URL из .gitmodules даже на fast path совпавшего pin. Реальная Git-регрессия воспроизводит старый upstream URL и новый коммит, отсутствующий в локальном clone. Это исправляет наблюдавшееся на Windows upload-pack: not our ref 86b1014…; пользователь подтвердил ручное исправление через git submodule sync --recursive до публикации автоматизации.
  • bb931ab, e9fbaba, b0085cb: ground-unload вдали от склада резервирует одну RM48U и вызывает ТЗМ. ground-cancel отменяет ещё ожидающий вывоз/доставку; после забора груз продолжает путь на склад. Проверены физическое движение и полный save/restore до забора и после забора, без телепортации запаса.
  • 910f02c: потеря зарезервированной ракеты вместе с ячейками сбрасывает заявку на вывоз. Иначе можно было создать сохранение, которое loader сам отвергал как несовместимое состояние.
  • Двигатель: f4c1131d3, затем c89c78c47 опубликованы в horizon-airdef:airdef до обновления gitlink. API27 добавляет acoustics-status и capture-owner-status, scoped к запуску мода и поколению мира. Остановка capture проверяет владельца; смена мира снимает незапущенный запрос или посылает cancel.
  • 910f02c: команда /airdef capabilities; дальний звуковой fallback сохраняется до подготовки акустического пути, экспорт принимает только своё подтверждённое состояние; Arming ждёт до 600 тиков. prepared — milestone хоста, не GPU fence, не результат каждой акустической трассы и не готовый MP4.
  • 4dddea2, 8d5d0d1: opt-in test-budgets, отдельный ручной workflow и JSON provenance; тесты ошибок сборки, stale identity и ненулевого runtime exit. Native success не заменяет actual WASM.
  • aab3e78: repo-local packer использует target-cache закреплённого движка; CARGO_TARGET_DIR не переопределяется для сборки самого мода и не уводит .hmod в чужое дерево.

Проверки объединённого состояния: core release 190 + 4 сценария, 7 ignored; адаптер 60 + 1 boundary + 4 сценария; лёгкий WASM 26 шагов, тяжёлый 69 шагов; Git-регрессия и три теста failure report прошли. Исходный профиль запрашивал 64 ПВО + 160 целей, 602 tick / 300 frame; JSON фиксирует clean aab3e78, pin API27 и совпадающий build identity. Полные данные: budget report. Исправление оценки покрытия: второй запрос исходного 69-шагового сценария отклонялся во время подготовки первого. Его успешный exit не подтверждает 160 запущенных или одновременно летящих целей. В архивный JSON добавлена поправка; исправленный сценарий проверяет ответы. Для настоящего stress также нужны ответы render-world: прежний sandbox их не давал и часто проверял лишь ожидание земли.

Дополнительно два свежих профиля игры на Metal/no-audio: smoke off не ready, HD smoke ready; при HORIZON_ACOUSTICS=1 + HORIZON_NO_AUDIO=1 акустика enabled/request=true, prepared=false, awaiting-backend. Выход через quit подтверждён. Это не прослушивание, не benchmark и не новый MP4. Правила API, пределы проверок и команды: ENGINE.md.

Двойные налёты и плотность следов

Перед продолжением читать RAID-STABILITY.md: два соответствующих класса отключения теперь воспроизведены и исправлены — тик (1f28f83) и событие UI на втором приказе (#122, order_raid/known_units); приёмка конкретного Windows-сеанса остаётся. Текущий исправленный WASM stress проверяет две принятые волны, обслуживаемую плоскую геометрию и 184 воздушные цели в конкретный момент; новый JSON заменяет прежнее недостаточное доказательство покрытия. Лимиты fuel/memory не увеличивались. 86e1d272e в движке улучшает callback/fuel/error-chain diagnostics; ed7615d36 добавляет только необязательную flat-world среду тестов и отказывает неожиданным ошибкам команд в Script. Основная клиентская логика сцены этим test helper не заменяется.

692ae77: HD-треки ракет оптически плотнее на 12% через общий rocket() и cruise style. SAM/RM48U, крылатые/сверхзвуковые/баллистические треки покрыты; сегментов, радиусов и времени не добавлено. Пожары и дымка винтовых дронов не менялись. Обычные ванильные fallback-частицы этим множителем не меняются. Пять существующих smoke-тестов прошли; прирост FPS и приёмка новой плотности на Windows не заявлены.

Последний общий прогон: core release190 + 6 сценариев, 7 ignored; адаптер60 + 1 boundary + 4 сценария; лёгкий WASM26; тяжёлый WASM271; 5 тестов отчёта. Нативные overlap-тесты — airdef-core/tests/scenarios.rs: два стандартных массовых пресета и два налёта с разделением «Орешников», оба со встроенной записью.

Продолжение по пользовательскому HD-отключению: 1f28f83 заменяет полный known.retain при каждом взрыве на удаление прежнего квадрата; семантическая эквивалентность проверена. В диагностическом WASM max-menu ×2 раньше был trap tick1835 на 300M; после фикса полный 3602-tick прогон прошёл, max135273690. Гость отличается от baseline только временным trigger hook состава меню и исправлением. Это измерение fuel, не прирост FPS. Артефакты/ограничения — в RAID-STABILITY. cc48302 добавляет публичный native max-menu regression; 3b470dd закрепляет опциональный engine test --fuel-profile (не новый SDK API и не увеличение лимитов).

cbd6067 исправляет сообщение о пустом соседнем складе, показывает типизированные запасы на M, уточняет подсказку разведки и убирает устаревшую строку README о якобы отсутствующей доставке. GROUND-STRIKES.md — инструкция с различием номеров своего С-400/склада и вражеской ПУ. Default RM48U=0; ground-stock задаёт остаток, ground-load грузит одну; поздний трек не даёт fix. ab737e7: actual WASM теперь 27 шагов с проверкой ошибки пустого соседнего склада.

После фикса полный core: 192 passed + 6 сценариев / 7 ignored; новый max-menu сценарий отдельно прошёл седьмым. Адаптер60 + 1 boundary + 4 сценария; light WASM27; engine /spin fuel-profile test прошёл.

Исходная контрольная точка этой серии

Исходное состояние этой серии: 2689d93; движок закреплён на 86b1014a9218e68041444cbf52db02a85107c815, уровень API 26. Рабочее дерево было чистым, коммиты опубликованы. Текущий точный pin всегда смотреть в git ls-files --stage horizon, а происхождение собранного пакета — в airdef-mod/target/horizon-mod/build-identity.json.

Четыре задачи были заказаны владельцем отдельно:

Issue Заказанный остаток
#120 Конечная работа двигателя перехватчика, coast, энергия манёвра, согласованные эффекты
#118 Типизированная доставка RM48U через ТЗМ; не создавать неподтверждённые носители
#121 Переключение излучения, сохранение разрешённого внешнего целеуказания, разведка излучателей
#119 Ослабление непрямого урона известным укрытием, ограниченная политика Unknown

Исполнители работали в отдельных worktree; их проверенные коммиты перенесены в kimi-work. В ветках codex/issue-* сохранены промежуточные версии; продолжать нужно от kimi-work, где разрешены конфликты общего формата сохранений и сигнатур боевых функций.

Что уже сделано в предыдущей серии Codex

  • airdef-core компилирует исходники airdef-mod/src/sim/mod.rs через #[path]. Это единый источник симуляции, но разделение домена и представления ещё не завершено (#87).
  • Исправлены остаточные терминальные коллизии ответных ударов и скачок после world-query (#106/#117). Восстановление ответов мира не даёт права телепортироваться к плановой точке.
  • Доработаны запись ответных ударов, launch one-shot и live-след (#109), lifecycle показа и часы отложенного звука (#88/#89). Полная унификация всех потребителей сцены ещё не закончена.
  • Движок вынесен в отдельную репу; сборщик берётся из закреплённого форка, а не случайного PATH. Есть build identity, настоящий WASM contract и handshake доступности дыма API26 (#102/#104).
  • Обновлён upstream Horizon 0.2.97, исправления очереди/инвалидации акустики сохранены. Измерительные fixtures, notices и лицензии добавлены; прослушивание и массовый CPU-тест ещё нужны (#96).
  • Pipeline поддерживает authored overrides, manifest и экспорт итоговых габаритов в sim/generated_bounds.rs. Навигация учитывает footprint. Generated output вручную не редактировать.
  • Улучшены модели наземной техники; степень сходства и материалы ещё промежуточные (#115). Галерея: MODEL-PILOTS.md, VEHICLE-MODELS.md; это не приёмка всех летательных аппаратов.
  • 966b52f: раздельные ПУ С-400/С-500, роли Radar Search/Engagement/Height/Multi. Чужая подсказка разрешает огневому радару уточнение, но не подменяет собственное измерение.
  • 966b52f: резерв конкретной ячейки, отдельные крышки ТПК, выброс до включения двигателя. RM48U — условный игровой профиль С-400; AA и ground rounds делят четыре физические ячейки. Боеприпас списывается при выбросе, не при выдаче отложенного приказа; отмена не расходует его.
  • Конфигурация, маска использованных ячеек и положение крышек включены в save/replay. Smoke начинается в точке зажигания. С-500 ограничен двумя ракетами по числу ТПК; старое сохранение с четырьмя загруженными ракетами при загрузке ограничивается двумя.
  • 2689d93: документированы источники архитектуры батареи, команды, скриншоты и границы визуальной проверки.

Проверки и воспроизведение

На 966b52f: 169 тестов ядра + 4 сценария прошли, 7 ignored; адаптер — 53 теста, 1 boundary и 4 сценария прошли; WASM contract — 21 шаг. Это исходная контрольная точка, не результат новых четырёх задач. На 2689d93 был собран чистый пакет (airdef_dirty=false).

На итоговом объединённом коде 6e2d661 прошли:

  • airdef-core --release: 187 passed, 7 ignored, плюс 4 сценария.
  • airdef-mod: 56 passed, 1 boundary, 4 сценария.
  • Настоящий WASM-пакет: 24 шага, включая команды излучения и typed ammunition.
  • gen_models.py --check: различий нет; в этой серии геометрия ассетов не менялась.

Полный core здесь запускался с --release; это не прогон всех семи ignored-тестов. Отдельно выполнен сравнительный ignored-тест баланса (см. ниже).

cargo test --release --manifest-path airdef-core/Cargo.toml
cargo test --manifest-path airdef-mod/Cargo.toml
python3 tools/airdef.py test-wasm
python3 tools/gen_models.py --check
python3 tools/test_gen_assets.py
git diff --check

На этой macOS cargo находится в ~/.cargo/bin/cargo. GUI-проверки запускать с отдельным HORIZON_HOME и рабочим каталогом тестового мира; не использовать реальные сохранения владельца. Metal-проверки не подтверждают Windows/Linux, HDR всех платформ или прослушивание звука. Скриншоты крышек подтверждают только их позу: полного видео выброса/зажигания пока нет.

Технические детали новой серии

#120: энергетика и единственный исход перехвата

MissileKind::burn_ticks задаёт конечное время горения: простой профиль равен 2/3 игрового spec.life; для электрического P1-SUN используется весь срок жизни. Это не отдельная модель топлива и не реальные кривые тяги. Missile::engine_lit — общий источник состояния мотора.

Три ветки скорости разделены явно: cold (age <= cold, множитель 0.97 с прежним минимумом 0.4), powered (+accel, ограничение spec.speed), coast (множитель 0.995 без холодного минимума). После выгорания доступный поворот масштабируется текущей скоростью; при падении ниже 25% игрового максимума перехват заканчивается диагностикой MotorEnergyLost. Срок жизни остаётся отдельным ограничением. Полноценной аэродинамики, гравитационной баллистики и расхода топлива по тяге нет.

Swept fuse учитывает движение обоих объектов за тик; Impact получает точку closest approach и относительную скорость того же проверенного отрезка, включая удержание на Unknown. Геометрию не заменяет отдельный бонус «встречная/догонная». Один остаточный pk зависит от фактической скорости перехватчика и существующего состояния носителя/дальности. Повторный штраф по stealth и старый штраф по абсолютной скорости цели удалены из этого броска; обнаружение/сопровождение по-прежнему учитывают stealth в своих местах.

MotorBurnout и MotorEnergyLost записываются в scene journal; tally отличает потерю энергии. Live и replay прекращают факел и новый дым по одному состоянию. Out::engine_stops немедленно снимает голос мотора на burnout, не ждёт обычной трёхтиковой grace пропавшего источника; при переходе на replay/export live-команды остановки отбрасываются вместе с live-выводом. Перемотка восстанавливает моторное состояние из записи, включая возврат до выгорания.

При ревью был найден и исправлен недостижимый coast: ранний прототип проверял !lit, что перехватывало и cold, и coast. Не переносить один 143d9b2 без последующих исправлений. a7b8cb9 содержит отдельный тест точных правил скорости всех трёх фаз.

Сравнительная выборка только изменения #120, отдельно от #119/#118/#121: baseline 2689d93, агентская итоговая ветка 508f9a0 (её исправление перенесено как a7b8cb9). 12 одинаковых seed, SAMS=20 SIZE=12: 204 цели в каждой версии; baseline — 202 сбитых, 2 прорыва, 367 ракет; новое — 203, 1, 355. Это небольшой регрессионный ориентир, не доказательство баланса всех типов/дальностей. Данные каждого seed: CSV.

Расширенная сетка остатка #120 (ветка agent/issue-120-balance, коммит см. в git): baseline 2689d93 против текущего e6a7000 (то есть с #118/#119/#121 и сменами движка, не только #120), 6 вариантов × 8 одинаковых seed, рейд SIZE=36 (46 целей: Gerbera 10, Shahed 13, Geran3 5, Х-101 5, «Калибр» 4, Х-22 2, «Циркон» 1, «Искандер» 4, «Кинжаль» 2; «Орешник» harness исключает, как в one_raid): batt10/20/50 (10/20/50 ЗРК, дистанция 500), near200 (20 ЗРК, пуск в 200), far1500 (20 ЗРК, пуск в 1500), hills20 (20 ЗРК, холмы, 500). Кластер тот же MIX из 10 систем: SAMS=10 — по одному комплексу каждого типа. Итого 2208 целей на сторону, 96 строк: CSV. Команда (игнорируемый тест sim::balance::issue_120_grid, только harness-поля в Result, поведение симуляции не менялось):

SEEDS=1-8 cargo test --release --manifest-path airdef-core/Cargo.toml issue_120_grid -- --ignored --nocapture

Факты сетки (суммы по 8 seed каждого варианта, baseline → текущий): сбитые 297→301 (batt10), 331→334 (batt20), 357→363 (batt50), 313→318 (near200), 328→338 (far1500), 328→334 (hills20); прорывы 70→66, 37→34, 11→5, 53→48, 40→30, 39→33. Промахов по взрывателю заметно меньше (например far1500: 70→38). Расход ракет близок (516→504 … 597→583). На холмах потери у рельефа: разбито 14 и 14, самоуничтожений у хребта 6→2. Прорывы во всех вариантах — преимущественно баллистические и быстрые классы («Искандер», «Кинжаль», Х-22, «Циркон»); у медленных Shahed/Gerbera прорывов почти нет (0–2 на вариант), у ложных Gerbera — ноль ни разу. MotorEnergyLost не сработал ни в одном из 48 сценариев текущего кода, expired ≤ 1 на вариант: coast существует, но бои на этих дистанциях заканчиваются раньше, чем сопротивление 0.995/тик опустит скорость ниже порога 25% (и раньше выработки ресурса). Это измеренное свойство профиля, не подтверждённый дефект, поэтому параметры не менялись. Путь потери энергии покрыт юнит-тестом регресса (regress.rs), а не этой сеткой.

SAMS=20 SIZE=12 cargo test --release --manifest-path airdef-core/Cargo.toml sim::balance::one_raid -- --ignored --nocapture

#118: один физический груз и восстанавливаемая заявка

ground_request у С-400 — заявка на одну RM48U. Рядом со складом остаётся прямая передача; вне радиуса склада та же команда ground-load создаёт заявку, если есть запас и живая ТЗМ. ТЗМ забирает одну ракету из конечного запаса, едет к исправному развёрнутому носителю и передаёт её в общие ячейки. Если вытеснена ракета ПВО, ТЗМ везёт её обратно на склад. Generic reload не создаёт RM48U. Ударная заявка имеет приоритет перед generic reload.

Поля ground_stock / air_48n6_stock на складе — остатки, на ТЗМ — физический груз двух типов. Заявка, груз и марш сохраняются; оперативный job реконструируется после загрузки. Передача запрещена во время движения, подготовки пуска, перезарядки, отказа ПУ и при отсутствии вместимости. Разрушение очищает заявку/груз; repair не восстанавливает уничтоженный typed stock. Проверены настоящее движение Sim.step по плоскому WorldSource, передача/возврат, истощение, прерывание, несовместимость, заполненные ячейки, unload и полный state_text → apply_state с продолжением маршрута. GUI-доставка на сложном рельефе ещё не проверена.

Новых носителей вроде Бука не добавляли: один публичный игровой профиль не даёт основания приписывать его всем ЗРК. Обратный вывоз из удалённого носителя автоматизирован в продолжении выше. Новая команда ground-cancel отменяет ожидающую заявку, но не возвращает уже забранный груз в ПУ. ТЗМ вмещает два типизированных места RM48U/48Н6; прежние абстрактные generic reload-пакеты учитываются отдельно. Вывезенная ракета возвращается в конечный запас; ПВО восстанавливается при заборе только из реально имеющейся у ТЗМ ракеты 48Н6 и свободной ячейки. Резерв одной RM48U учитывается при выборе носителя удара.

#121: излучение отдельно от сети

radar_on — сохраняемая настройка, default=true для старых миров. Команда /airdef radar <номер> on|off и кнопка карты меняют её; у launcher-only нет своего активного радара. radar_up учитывает этот флаг. Off запрещает новые own measurements, но старые треки стареют обычно и разрешённый сетевой приём сохраняется. Это не выключение корпуса/связи/ГСН ракеты. Огневой радар, обязательный для отдельной ПУ, в Off не авторизует новые пуски.

Условный наблюдатель-противник раз в 20 тиков обнаруживает работающие радары в 1600 блоках. Это упрощённый игровой датчик по расстоянию, без LOS/радиопеленгации и ошибки оценки позиции. Контакт хранится как последняя наблюдённая точка и стареет за 6000 тиков. Off не стирает контакт, не телепортирует его вслед за носителем и не предотвращает раскрытие пуском/РЭБ.

В raid::radar_strikes удалён отдельный обход: раньше SEAD выбирал любую Radar и её настоящий pos, даже если общий planner уже работал по наблюдениям. Теперь SEAD берёт те же last-known контакты и их возраст. Явный sandbox-режим Aim::ObjectsAndSams по-прежнему выбирает расстановку напрямую; не путать его с умным противником. Проверки покрывают необнаруженную молчащую РЛС, смену позиции без наблюдения, повторное раскрытие, старение, relay freshness и seek.

Совместимость текстового состояния

Сохраняем строки sam расширением справа; индексы ниже включают слово sam как поле 0.

Индекс Значение
22–24 ground_rounds, ground_stock, air_48n6_stock
25–27 mode, radar_role, spent_cells
28 ground_request: 0/1, только С-400
29 radar_on: true/false, отсутствие означает true
30 ground_return: true/false, резерв вывоза одной RM48U из С-400
31 return_cargo: true/false, возвращаемая RM48U на ТЗМ

Две агентские ветки сначала заняли поле 28 одновременно; при интеграции излучение перенесено в 29, опубликованная совместимость заявки сохранена. Проверка полного roundtrip одновременно содержит ожидающую доставку и выключенный радар. Новые значения входят в durable mark; настройка излучения также в SamKey и ghost replay. Не переиспользовать эти позиции для нового поля.

#119: непрямой взрыв и укрытия

WorldCache::blast_cover читает текущую occupancy без запросов мира и без загрузки чанков. Три луча к репрезентативным точкам корпуса дают общий множитель для корпуса и узлов; для отмеченного объекта считается своя тройка. Известное укрытие оставляет 15% игрового воздействия по перекрытому лучу, открытый — 100%; края/проёмы дают промежуточное значение. Все лучи этого вызова читаются до отправки разрушения в движок. Исходный воксель пропускается, чтобы контакт на поверхности стены не закрывал взрыв сразу во все стороны.

Unknown, stale и слишком старые секции считаются неразрешёнными. Ожидание равно нулю: для них сохраняется прежний неослабленный урон как консервативная игровая политика, а не утверждение, что там воздух. После ответа мира этот взрыв не пересчитывается. Нет очереди отложенного урона и повторного применения на world reply. Частичные блоки на этом этапе считаются занятым кубом; тонкая collision shape для blast отдельно не запрашивается. Прямой вооружённый контакт не ослабляется этим множителем.

Проверки sim::damage::cover_tests: стена/проём для корпуса, узлов и объекта; Unknown и поздний ответ без повторного урона/запросов; прямое попадание; устаревшая геометрия; пропуск исходного блока. Все четыре прошли в объединённом наборе.

#127: устойчивая очередь маршрутов после массового перенацеливания

Реализована только узкая часть issue: sim::ew::RouteJob — задание с ID дрона, новой целью, ID цели-юнита, версией знаний и общим для тика снимком known_units (Arc). adapt по-прежнему выбирает новые цели раз в 40 тиков, но маршруты теперь достраивает replan_retargets каждый тик: одно задание или несколько, пока не исчерпан WORK_PER_TICK. Незавершённое задание остаётся в Sim::retargets (FIFO) и не теряется; повторный выбор того же дрона заменяет задание на месте, поэтому очередь не растёт.

Старый маршрут не очищается при перенацеливании: дрон следует готовому безопасному участку, пока новое задание не получит маршрут. Если задание отменяется (дрон погиб, цель изменена снова, юнит уничтожен), маршрут очищается как fallback — дрон летит прямо до следующего адаптивного тика. Lancet, баллистика и decoy, как и раньше, летят прямо: для них задание не создаётся. В полёте следующая известная цель берётся только если до неё есть разворот; цель сзади внутри радиуса разворота пропускается, и дрон завершает текущий заход, а не получает невыполнимый маршрут. Повторный план использует снимок разведки исходного выбора цели и известный рельеф той же версии задания: KnownRelief (сетка и высоты) лежит в Arc внутри RouteJob, Sim::known_relief хранит тот же Arc только для новых заданий. Завершение другой умной волны заменяет глобальный указатель, но не снимок ожидающего job. Выборка рельефа, цены и кэш маршрутов в replan_retargets ключуются версией знаний целиком — adapt tick плюс указатели на снимки known units и рельефа, — а не только рельефом: два job с одинаковым рельефом, но разными known units, не делят price/route grid. За пределами известной сетки — Unknown, новая карта не раскрывается.

Очередь этапа 1 была рантайм-состоянием планировщика: дроны и очередь запуска в state_text не входили, поэтому durable-очередью она не была. Этап 2 (ниже) сохраняет её вместе с дронами, очередью запуска, картиной и версиями рельефа. Регрессии этапа 1 остаются: mass_retarget_finishes_its_route_jobs_over_the_budget (40+ ретаргетов, прогресс каждый бюджетный тик, старый маршрут до нового, нет пустых маршрутов) и mass_retarget_is_deterministic_and_never_duplicates_a_drone (тот же seed — те же цели и маршруты; повторный adapt не создаёт дублей).

#127, этап 1: честная EnemyPicture

Журнал intel.revealed: Vec<(id, место, тик)> заменён картой контактов: Intel.contacts: BTreeMap<u32, Contact>, одна запись на юнита, последнее наблюдение побеждает. Контакт пишется только в момент раскрытия и читает истину только тогда: пуск (mod.rs/strike.rs) → Seen::Launch; фиксация излучения радаром рядом с чужим аппаратом (sight.rs, каждые 20 тиков, 1600 блоков) → Seen::Emission (отдельно помечается emission — вера в собственный сенсор опирается только на неё); работающая РЭБ (ew.rs) → Seen::Jamming; четвёртый канал — BSA: падение собственной боеголовки рядом с известным юнитом (Sim::observe_outcomes, радиус 16) фиксирует destroyed либо обновляет «жив» и место. Пока юнит незамечен, его перемещения, выключение радара, смена ролей/режима, расход БК, урон и гибель картину не меняют — это метаморфные тесты sim::ew::picture_tests.

Из контактов решается всё: снимок picture_of_defences() на приказ и на адаптивный тик (newest first, при равном тике — больший id, как у старого reversed-журнала), цели choose_target и SEAD (radar_strikes — только Probable и свежее), ретаргет в полёте (цель «мертва» лишь по контакту с destroyed; забытого контакта дрон не замечает), живость задания очереди (job_live — по картине, не по истине). Уверенность — функция одного возраста: Confirmed ≤ 600, Probable ≤ 3000, Doubtful до TTL 6000 тиков (Confidence, диагностика и SEAD её используют, цены — нет: консервативно считаются все живые контакты).

Модель возможностей в price_grid_known читает только картину: роли — published kind.spec().roles (смена /airdef roles невидима), дальности — ew::published_reach (без урона, фактора роли и мачт истины), донор — любой живой контакт вида Radar в link range от наблюдённого места (роль донора не наблюдаема: рабочий контакт РЛС предполагается кормящим сеть — это допущение, а не знание; разрыв связи/гибель донора незамеченными картину не меняют). Собственный сенсор верится, пока жива фиксация эмиссии; пуск при нуля эмиссионных фиксаций у С-400/С-500 читается как launcher-only: без донорского контакта такая ПУ в ценовой сетке «слепа», с ним бьёт на дальность донора. Горизонт РЛС игрока (self.horizons) из чужого планировщика убран: маскировка рельефа — тайна обороны, цены по ней консервативнее и объяснимее.

Диагностика без GUI: Sim::enemy_picture() -> EnemyPictureReport (англ.): по каждому контакту — наблюдение (канал, место, возраст), уверенность, живость, наблюдённое уничтожение, вера в сенсор, наличие донора и текстовое «что планировщик из этого делает». Адаптация по-прежнему ограничена статистикой секторов (best_bearing по kills/hits): новых всеведущих данных и усиления характеристик нет.

Форматы этапа 1 не менялись: intel оставался рантаймом (save хранил Layout, replay — траектории); durable-очередь появилась на этапе 2, ниже. Измерения до/после на одних seed: docs/measurements/issue-127-enemy-picture-baseline.json (0b645d77e) и issue-127-enemy-picture-result.json (эта ветка), bench — cargo run --release --example intel_bench. big-smart и logistics побайтово совпали с базой; ew-adaptive изменился (kills 62→59/61→59/61→56, hits 3→7/5→7/7→8, потери ЗРК 1→3/4→3/4→4) — фиксируется как факт перехода на честную картину, балансные параметры не трогали. Регрессии: метаморфные hidden_changes_do_not_move_the_picture_or_its_decisions, a_new_observation_is_what_updates_the_picture, launcher_only_is_blind_in_the_picture_without_a_donor (донор любой роли; разрыв связи наблюдённый/нет), destruction_moves_the_picture_only_when_watched, confidence_is_a_function_of_age_alone, enemy_picture_report_describes_observation_and_belief, an_impact_watches_the_kill_and_updates_only_the_picture (BSA end-to-end), обновлённые SEAD/#4/#21/#122 и обе регрессии очереди. Остаток #127 после этапа 1: durable save queue, structured intent, GUI.

#127, этап 2: structured intent, durable queue и объяснимость

Намерение отделено от цели. У дрона теперь два поля: ew::Intent — почему он летит туда — и ew::Aim — юнит, когда цель выбрана, сколько раз менялась и уверенность контакта. Intent: Plan (собственная цель плана: объект или юнит, выбранный при заказе), Sead (плановый удар по РЛС из radar_strikes), React (реакция на раскрытие: станция показала себя уже в воздухе) и Replace (картинка увидела гибель/забвение прежней цели). Случайный выбор цели (choose_target) больше не смешивается с реакцией: при заказе налёта intent фиксируется, а adapt помечает свои решения явно. Приоритет очереди — из намерения: Plan 3, Sead 2, React 1, Replace 0.

Throttle. SWITCH_HOLD = 160 тиков (8 с): свежее раскрытие может сменить цель только после выдержки; SWITCH_CAP = 3 смены на дрон за жизнь; ASSIGN_PER_TICK = 16 новых целей за адаптивный тик и ASSIGN_PER_CONTACT = 6 на один контакт за тик — массовый ретаргет растягивается на следующие адаптивные тики, а не переписывает все маршруты сразу. Смена возможна, только если контакт показался позже текущей цели (tick > aim.since), он не Doubtful, и он либо достовернее текущего, либо ближе на SWITCH_GAIN = 120 блоков: смена выбрасывает готовый маршрут и стоит планировщику нового, поэтому маргинальная не делается. Замена убитой цели выдержку не ждёт (цели уже нет), но упирается в тот же cap. В полёте, как и раньше, цель меняется только когда картинка её убила или забыла.

Явные игровые реакции без скрытых данных. Реакции читают только поля контакта, записанные каналом наблюдения: выключенный радар остаётся в картине таким, каким его видели (эмиссия не фиксировалась — собственный сенсор не верится), разделённая батарея читается как пуск без эмиссии (launcher-only, без донора «слепа»), конечный БК наблюдается только как пуски (Contact.launches: счётчик увиденных пусков; остаток магазина в картину не входит никогда), энергия/нагрев РЭБ — своё состояние станции, а не раскрытие. Метаморфный тест hidden_changes_do_not_move_the_picture_or_its_decisions расширен: скрытые перемещение, off, роли, режим, БК, урон, гибель и нагрев/замок/выключение РЭБ не двигают ни картинку, ни цены, ни намерения/aims/очередь заданий. Повторное наблюдение двигает всё — как и должно.

Неизвестная земля не бесплатна. raid::Relief получил поле unknown; у плана умной волны оно 0 (поведение этапа 1 сохранено), у повторного планирования задания — UNKNOWN_LAND = 6 сверх базовой цены клетки 1. Задание, чья версия рельефа ничего не знает, планируется по полностью неизвестной земле с той же ценой, а не «как по свободной»: маршрут держится размеченной части, если она есть. Тест unknown_land_is_not_free_in_a_replanned_route сравнивает маршрут с ценой 0 и с ценой 6 на одной сетке: бесплатный срезает через неразмеченное, платный идёт по размеченному.

Durable queue: формат save. state_text (автосейв) дописывает версионированную секцию (sim::durable, формат air 1):

air 1 <такт> <номер следующего задания> <дронов> <в очереди> <заданий> <контактов> <версий рельефа>
airintel <12 kills> <12 hits>
airglobal <версия рельефа|->
airrelief <номер> <minX> <minZ> <клетка> <сторона> <RLE: u<n> | <высота>x<n> ...>
aircontact <id> <тип> <x> <y> <z> <такт наблюдения> <канал> <эмиссия|-> <гибель|-> <пусков>
airdrone <id> <тип> ... <полёт> <намерение> <юнит> <since> <смены> <уверенность> ... <маршрут: n x y z ...|->
airqueue <такт запуска> <то же тело дрона>
airjob <id> <дрон> <x> <y> <z> <юнит> <версия> <приоритет> <намерение> <рельеф|->

Секция пишется только автосейвом: сценарий — это стартовое состояние, его тело её не несёт (scenario_text → state_text_with(..., air = false)). Старые saves без секции читаются как прежде (пустое небо); старые сборки новые saves читают как Layout, игнорируя air* (Layout пропускает неизвестные строки). Всё проверяется до применения: битая строка отказывает save целиком с номером строки и полем, счётчики в заголовке сверяются с числом строк (обрезанный файл отвергается), чужая строка внутри секции — тоже. Границы: 512 дронов, 512 точек маршрута, 4096 контактов, 8 версий рельефа, 40 000 клеток; координаты пишутся до 0.01 блока (полёт этого не замечает), рельеф — RLE целых высот. Секция дополнительно ограничена 900 КБ (хост разрешает 1 МиБ на одну запись): дрон, который не влез, не пишется, и его задание тоже (восстановить его было бы нельзя). Измеренный размер секции в bench: 4.0–8.8 КБ для logistics/ew-adaptive и 61–62 КБ для big-smart (38 дронов в воздухе).

Что именно восстанавливается. Дроны в воздухе (живые; падающие обломки — эффект, их нет), очередь запуска с тактами, задания (id, дрон, цель, юнит, версия, приоритет, намерение, версия рельефа), контакты картины (счётчики секторов, пуски), глобальный снимок рельефа и job_next. Картина задания восстанавливается «на свою версию»: контакты, наблюдённые не позже версии, плюс собственная цель задания (её последнее сохранённое наблюдение заменяет более позднее — иначе цель исчезла бы из своей же картины). Устаревшие задания отменяются при загрузке (prune_air): дрон исчез/погиб, цель уже другая, юнит наблюдённо уничтожен или забыт, дубль по дрону, версия старше JOB_TTL = 1200 тиков (60 с). Маршрут отменённого «по устареванию/дублю» задания сохраняется (он ведёт к той же цели), отменённого из-за смены цели — очищается.

apply_state сначала проверяет Layout и секцию, затем применяет обе; секция заменяет воздушное состояние, а не сливается с ним. durable_mark включает air_mark (число дронов/очереди/заданий/ контактов и job_next): автосейв берётся на запуске, потере, новом/завершённом задании и новом/забытом контакте, а не каждый тик полёта. Пока налёт планируется, периодический save откладывается (план не имеет durable-формы; поиск площадок ограничен sites::SEARCH_TICKS = 400); при смене мира пишется только Layout. Replay по-прежнему хранит траектории: секция пишется и читается только для живого Sim, записи не смешиваются с ней.

Остаток и ограничения: ракеты-перехватчики в воздухе, пусковые площадки (sites), промежуточный план, статистика волны и состояние RNG не сохраняются — после загрузки перехваты начнутся заново, очередь стартует без площадки (свободный слот), отчёт волны не придёт, а случайные броски идут от нового генератора мира; сама воздушная обстановка (дроны, цели, задания, картина) восстанавливается точно. GUI поверх отчёта не делался.

Объяснимость. EnemyPictureReport расширен без русского текста в core: у контакта — launches, aimed_by (сколько дронов на него нацелено) и pending_jobs; добавлены aims (дрон, юнит, intent, since, switches, confidence) и pending (id задания, дрон, юнит, точка, версия, приоритет, intent, relief_known, reason словами: ждёт бюджета / ждёт бюджета, земля неизвестна и стоит дороже / устарело), плюс work_per_tick. Тест the_report_explains_aims_and_pending_jobs_in_english проверяет и ASCII-only диагностику.

Измерения (baseline 8fc10c07a → эта ветка, cargo run --release --example intel_bench, seeds 1/7/42): docs/measurements/issue-127-intent-queue-baseline.json и issue-127-intent-queue-result.json. big-smart: завершение у всех, launched 38, kills 38, hits 0, потери 0 — как в базе; ticks 1716→1676 (seed 1) и 1542/1433 без изменений, fired 49→60, 48→68, 49→71 (больше пусков ПВО). logistics: все поля совпали (27 запусков, 11/12/12 сбито, 8/7/8 прорвалось, 18 ракет, 0 потерь). ew-adaptive: kills 59→58, 59→58, 56→63; hits 7→7, 7→9, 8→3; потери ЗРК 3→3, 3→4, 4→2; ticks 8084→7146, 8193→7650, 7916→7243; fired 69→76, 81→98, 69→82. Это следствие throttle назначений и приоритетной очереди, а не подкрутки баланса: параметры ПВО/дронов не менялись, числа фиксируются как факт. В result-файле есть новые поля (retargets_max, air_bytes, switches_max, intents), у baseline их нет.

Регрессии этапа 2. Core: the_plan_records_sead_separately_from_its_objective, assignments_are_throttled_per_tick_and_per_contact, a_fresh_disclosure_switches_an_aim_only_after_the_hold, a_watched_kill_replaces_the_aim_and_queues_a_lower_priority_route, launched_rounds_are_observed_but_the_magazine_is_not, unknown_land_is_not_free_in_a_replanned_route, the_route_queue_is_bounded_and_refuses_a_worthless_job, the_same_seed_chose_the_same_intents_and_jobs, the_report_explains_aims_and_pending_jobs_in_english, the_air_state_round_trips_every_job_field, a_pending_route_continues_after_a_save_and_a_load (save/load посреди незавершённой очереди: та же очередь, те же маршруты, обе кампании доводят задания, пустых маршрутов нет), stale_ghost_and_duplicate_jobs_are_cancelled, a_job_for_a_unit_the_picture_lost_is_cancelled_on_load, a_damaged_air_section_is_refused_with_its_line_and_field, the_section_stays_inside_its_byte_bound (600 дронов с длинными маршрутами: секция ≤ 900 КБ, записанные задания принадлежат записанным дронам), обновлённые массовые ретаргеты (40+ дронов: прогресс на бюджетном тике, throttle назначений, у дрона, стартовавшего без маршрута, маршрут появляется). Adapter: the_air_section_of_a_save_is_written_and_read_by_the_adapter (запись/чтение секции, замена не слияние, старый save без секции, сценарий без воздуха).

Что следующему агенту проверить в первую очередь

  1. На Windows: обновление сабмодуля через all, сохранение/перезаход, рабочий цикл игры.
  2. В GUI: полный холодный пуск крупным планом; карточка On/Off; маршрут ТЗМ по сложной местности; разрушение стены одним взрывом и защита от того же взрыва по исходной геометрии.
  3. Прослушать прекращение голоса двигателя при burnout и seek/экспорт вокруг этой границы.
  4. Расширенная балансировка #120 (батареи/дальности/классы) записана в measurements/issue-120-grid.csv; остаются прослушивание голоса мотора и платформенная приёмка.
  5. Продолжить оставшиеся архитектурные #87/#88/#89/#91/#102 и художественный #115 по статусной таблице.

Это незавершённая платформенная/визуальная приёмка, а не отсутствие перечисленного выше кода. Прогон четырёх задач не запускал GUI/MP4. В продолжении API27 есть два коротких игровых smoke, но новый MP4, прослушивание и платформенная приёмка Windows/Linux по-прежнему не выполнены.

Правила продолжения

  • Не закрывать issue даже после исправления; сообщать проверенный объём и остаток в комментарии.
  • Не переписывать gitlink на плавающую ветку. Изменения движка сначала публикуются отдельно.
  • Не путать наблюдённый трек с настоящим положением цели и own_tick с relay.
  • Не менять live-мир при seek; replay воспроизводит последствия, не применяет урон повторно.
  • Не считать Unknown свободным пространством или подтверждённой стеной без явной игровой политики.
  • Новые числа — игровой масштаб, не реальные характеристики вооружения.
  • Токен Gitea читать из локального файла во время запроса; не коммитить и не печатать.