Перейти к содержанию

Аудит SIM-MIS (40): полётное задание, исполнение, журнал, отчёт

Дата: 2026-09-16 · Ветка: feat/simulate-audit · Код: simulate/ (84 файла вне docs/) Требования: simulate/docs/50-mission/01-mission-execution.md (строки 567–606 — таблица SIM-MIS-01…40), головной реестр simulate/docs/90-final/02-requirements-registry.md (строки 265–304). Метод: чтение кода + прогоны .venv/bin/sim, .venv/bin/pytest, grep. Вердикт «реализовано» — только с местом в коде или выводом команды.


1. Сводка числами

Статус Кол-во Доля
РЕАЛИЗОВАНО 0 0 %
ЧАСТИЧНО 7 17,5 %
РАСХОЖДЕНИЕ 4 10 %
НЕ РЕАЛИЗОВАНО 28 70 %
НЕ ПРОВЕРЕНО 0 0 %
ТРЕБОВАНИЕ НЕГОДНОЕ 1 2,5 %
Всего проверено 40 100 %

По вехам (вехи — из головного реестра, строки 265–304):

Веха Всего РЕАЛИЗОВАНО ЧАСТИЧНО РАСХОЖДЕНИЕ НЕ РЕАЛИЗОВАНО НЕГОДНОЕ % полностью закрытых
M0 13 0 3 (04, 16, 23) 4 (03, 19, 22, 28) 5 (01, 06, 17, 20, 33) 1 (25) 0 %
M1 19 0 4 (10, 14, 21, 34) 0 15 0 0 %
M2 8 0 0 0 8 0 0 %

Ни одно требование домена «миссия» не закрыто полностью — включая все 13 требований M0, которые по BRD являются MUST и входят в критерий выхода M0. Затронуто кодом (частично или с расхождением) 8 из 13 M0.

Отдельно по трём пунктам, которые велено проверить особо:

Проверка Итог
Журнал против contracts/log_schema.json Манифест: нет 9 из 14 обязательных полей. Телеметрия: нет 4 из 4 обязательных (t_sim_s, uav_id, lon, lat), присутствует 3 поля из 31. Поток событий отсутствует целиком (0 из 34 типов). Схема в рантайме не проверяется.
Детерминизм (два прогона, один seed) Телеметрия — бит-в-бит (d348f1ae… дважды). Манифест — не бит-в-бит (generated_at = системные часы). PRNG в коде нет вовсе: --seed 42 и --seed 777 дают один и тот же telemetry_sha256.
Приём настоящего KML от планировщика Нет. grep -rniE "kml\|geojson\|kmz\|xml" src/ tests/ tools/ — 0 совпадений; файлов .kml/.geojson в репозитории нет. Принимается только внутренний JSON нашей схемы.

2. Чеклист по всем 40 требованиям

ID Требование (кратко) Веха Статус Доказательство Что расходится и чем грозит
SIM-MIS-01 Вход — комплект (scenario, plan, world_snapshot_id, run_config); план без сценария не исполняется, SCENARIO_MISSING M0 НЕ РЕАЛИЗОВАНО sim run --help → опции только --seed, --output, --world; src/geoscan_sim/cli.py:50-61. grep -rn SCENARIO_MISSING src/ tests/ tools/ → 0 (код есть только в contracts/error_codes.yaml:15). run_config в коде отсутствует как понятие. sim run fixtures/plans/s01.json без сценария → RC=0 Исполняется план без сценария: нет зон, нет ВП, нет ветра, нет площадок → все нарушения, ради которых существует симулятор, физически невычислимы. Прямое нарушение M0-MUST
SIM-MIS-02 Импорт: наша схема + GeoJSON RFC 7946 + KML OGC 2.2 обязательны M1 НЕ РЕАЛИЗОВАНО grep -rniE "kml\|geojson\|kmz\|xml" src/ tests/ tools/ → rc=1, 0 совпадений. find . -name "*.kml" -o -name "*.geojson" → пусто. src/geoscan_sim/mission/import_plan.py:16-22 — только json.load Планировщик по ТЗ выдаёт KML/GeoJSON (INH-01, R-OUT-2/3). Симулятор не может принять ни один реальный артефакт планировщика — стыковка двух половин продукта не существует
SIM-MIS-03 Фрейм высоты обязателен; clampToGround — фатальная ошибка, код ALT_FRAME_MISSING M0 РАСХОЖДЕНИЕ Отклонение работает: sim run /tmp/aud/no_frame.json → RC=1, PlanImportError: missions[0].sorties[0] missing altitude_frame; clampToGround → RC=1, invalid altitude_frame: 'clampToGround' (import_plan.py:41-47,58). Но grep -rn ALT_FRAME_MISSING src/ → 0; вместо кода — Python-traceback. import_plan.py:9 разрешает RELATIVE Нет машиночитаемого {code, severity, path, message, source_ref} (§1.5) — CI/веб не могут разобрать причину. RELATIVE не входит ни в SIM-GEO-09 {AGL,AMSL,ELLIPSOIDAL}, ни в §1.3 {AGL,AMSL} — принимается фрейм, семантика которого нигде не определена
SIM-MIS-04 ТТХ борта и нагрузки только из fleet.yaml/payloads.yaml; правка airspeed_cruise меняет makespan_s; UAV_MODEL_UNKNOWN M0 ЧАСТИЧНО ТТХ читаются из YAML: src/geoscan_sim/hardware/fleet.py:load_fleet_yaml/uav_spec, манифест содержит fleet_yaml_sha256/payloads_yaml_sha256. UAV_MODEL_UNKNOWN есть в mission/preflight.py:91. Но makespan_s факта не зависит от ТТХ: mission/runner.py:54 duration_s = min(_sortie_duration(sortie), 600.0) — берётся из плана. Проверено: план со speed_ms=500makespan_s.actual = 600.0 (тот же, что у s01) Приёмочный критерий требования («правка airspeed_cruise меняет makespan_s») не выполняется: факт makespan — это переписанный план, а не результат физики. Неизвестная модель роняет прогон traceback'ом KeyError: "Unknown UAV 'no_such_uav' in fleet.yaml" (hardware/fleet.py:30) уже после preflight, код UAV_MODEL_UNKNOWN до вывода не доходит
SIM-MIS-05 Три уровня валидации импорта; строгий режим по умолчанию; import_fixups[] всегда пуст M1 НЕ РЕАЛИЗОВАНО import_plan.validate_plan (import_plan.py:25-51) проверяет только altitude_frame и типы. Семантики §1.4 нет: стыковка фаз (PHASE_GAP grep→0), монотонность seq, согласование photos. Перекрёстной нет: план со speed_ms=500 принят, RC=0, preflight_codes: []. grep -rn "import_fixups\|tolerant" src/ → 0 Невалидный план исполняется молча. Строгий режим заявлен, но проверять нечем: 7 из 10 кодов §1.5 не существуют в коде
SIM-MIS-06 Фазы → путевые точки закрытого словаря из 10 типов; неизвестный тип — ошибка импорта M0 НЕ РЕАЛИЗОВАНО grep -rn "transect_start\|transect_end\|turn_entry\|turn_exit\|climb_to\|transit_wp\|approach_1\|rally\|acceptance_radius" src/ → все 0. Проверено: план с "phase": "teleport" → RC=0, preflight_codes: [], журнал записан Понятия путевой точки в симуляторе нет вовсе; интегратор летит одной фазой по прямой (engine/integrator.py:86-116). Любой мусор в поле phase принимается — импорт не защищает ни от чего
SIM-MIS-07 На survey ровно один триггер камеры; оба → CAMERA_TRIGGER_AMBIGUOUS M1 НЕ РЕАЛИЗОВАНО План с одновременными camera_trigger_interval_s: 2.0 и camera_trigger_distance_m: 50.0 → RC=0, preflight_codes: []. grep -rn CAMERA_TRIGGER_AMBIGUOUS src/ → 0 (код есть только в contracts/error_codes.yaml:57) Неоднозначный план триггера съёмки принимается; число кадров становится неопределённым, а photos_total — недоказуемым
SIM-MIS-08 Round-trip KML/GeoJSON → модель → экспорт M2 НЕ РЕАЛИЗОВАНО Экспорта нет: grep -rniE "kml\|geojson" src/ → 0; в journal/writer.py пишутся только run_manifest.json и telemetry.jsonl M2, к M0-коду претензий нет
SIM-MIS-09 acceptance_radius_m пер-класс; галсы ограничиваются створом; флаг ACCEPTANCE_SENSITIVE M1 НЕ РЕАЛИЗОВАНО grep -rn "acceptance_radius\|ACCEPTANCE_SENSITIVE" src/ → 0 Ни круга, ни створа; coverage_fraction факта не вычисляется вообще (копируется из плана — см. SIM-MIS-28)
SIM-MIS-10 Два типа разворота lzp/flyby, R = V²/(g·tan φ) M1 ЧАСТИЧНО Формула есть: src/geoscan_sim/uav/kinematics.py:turn_radius_m; эталон V-7 в sim validate --reference136.5 / 136.5 PASS; тест tests/test_uav.py::test_v7_turn_radius. Самих режимов нет: grep -rn "lzp\|flyby\|turn_mode" src/ → 0 Есть число, нет поведения: разворот не исполняется, разница покрытия lzp против flyby — один из главных ответов симулятора — не считается
SIM-MIS-11 Камера на развороте по умолчанию выключена M1 НЕ РЕАЛИЗОВАНО grep -rn "camera_trigger_in_turnaround\|photo" src/ → 0; src/geoscan_sim/camera/gsd.py (32 строки) считает только GSD Кадров как сущности нет; photos_out_of_transect невычислим
SIM-MIS-12 Недостижимая точка: спираль, таймаут 120 с, событие, contingency после 3 подряд M1 НЕ РЕАЛИЗОВАНО grep -rn "spiral\|unreachable\|WAYPOINT_UNREACHABLE" src/ → 0. Ближайшее — uav/kinematics.py:wind_triangle_ground_speed_ms бросает ValueError при W⊥ ≥ V_a, и preflight.py:155-158 даёт WIND_EXCEEDS_AIRSPEED до старта Точка «телепортируется»: интегратор не знает о точках. Нет unphotographed_area_km2
SIM-MIS-13 Перелёт по правилам Planner; при Δh > 30 м участок набора выделяется M1 НЕ РЕАЛИЗОВАНО engine/integrator.py:107height_m=config.flight_height_m, высота константна весь прогон; grep -rn "climb_rate\|climb" src/ --include=*.py → 0 Профиля высоты нет; время набора/снижения в makespan не входит
SIM-MIS-14 Посадка — 3 точки, заход против ветра; парашют 201 с 70 м, снос M1 ЧАСТИЧНО Снос считается: uav/kinematics.py:parachute_drift_m, эталон V-10 168 / 168 PASS в sim validate --reference, тест tests/test_uav.py::test_v10_parachute_drift; высота выброса 70 м — в допущении A-16 манифеста. В исполнении и отчёте нет: grep -rn "landing_offset_m" src/ → 0, маршрут посадки не моделируется (preflight.py:185-195 только ищет посадку под вышкой по литеральным координатам) Величина есть в справочной таблице, но не привязана к прогону: в fact_report.json её нет, «куда борт реально сел» не отвечается
SIM-MIS-15 Возврат на текущей высоте, но не ниже минимальной высоты возврата M1 НЕ РЕАЛИЗОВАНО grep -rn "return_altitude\|min_return_alt" src/ → 0 Режима возврата нет
SIM-MIS-16 Единые модельные часы и фиксированный dt для всех бортов и подсистем M0 ЧАСТИЧНО dt фиксирован: engine/integrator.py:22 dt_s: float = 0.1; телеметрия 10 Гц подтверждена (6000 строк на 600 с). Но борт ровно один: mission/runner.py:15-23 _first_sortie берёт первую миссию и первый вылет. Проверено на s04 (3 борта, 5 вылетов): журнал 6000 строк, единственный борт, v_air_ms: 27.8 «Единые часы для всех бортов» проверять не на чем — многобортового прогона не существует. run_config с полем dt отсутствует, задать шаг из CLI нельзя, значит и «шаг пер-борт отклоняется» нереализуемо
SIM-MIS-17 Автоматическая проверка сходимости по шагу (dt/2), флаг DT_NOT_CONVERGED M0 НЕ РЕАЛИЗОВАНО grep -rn DT_NOT_CONVERGED src/ tests/ → 0. sim run --help не имеет опции --dt; mission/runner.py:55-62 не передаёт dt_s в IntegratorConfig Приёмка A-03 BRD (dt→dt/2: \|Δmakespan\| ≤ 0,5 %, \|Δcoverage\| ≤ 0,2 п.п.) невыполнима технически — шаг нечем изменить. M0-MUST не закрыт
SIM-MIS-18 Три режима часов (1×, ускоренный, headless) дают идентичный журнал M2 НЕ РЕАЛИЗОВАНО grep -rn "clock_mode\|realtime\|accelerated\|time_scale" src/ → 0 (единственные совпадения на headless — имена функции run_plan_headless и docstring) M2. Попутно: clock_mode — обязательное поле log_schema.json, и его отсутствие ломает M0-манифест (см. SIM-MIS-23)
SIM-MIS-19 Детерминизм по seed: единый PRNG, подпотоки (подсистема, uav_id), фикс. порядок обновления M0 РАСХОЖДЕНИЕ Два прогона одного комплекта: sha256(telemetry.jsonl) = d348f1ae… оба раза (вывод ниже). Тесты tests/test_determinism.py::test_determinism_digest_stable_10x, tests/test_engine_smoke.py::test_run_deterministic_telemetry_hash_10x. Но PRNG нет: grep -rn "random\|Generator\|PCG64" src/ → ни одного использования; --seed 777 даёт тот же telemetry_sha256: a6706a37…, что и --seed 42 Детерминизм тривиален — воспроизводится нечего, потому что стохастики нет ни в ветре, ни в GNSS, ни в отказах. Ни подпотоков, ни второго борта, ни фиксированного порядка обхода бортов. Как только появится любая случайность, гарантии не будет: механизм, который требование предписывает, не построен
SIM-MIS-20 События внутри шага упорядочены по (t_event, uav_id, seq) с интерполяцией M0 НЕ РЕАЛИЗОВАНО grep -rn "event" src/ --include=*.py0 совпадений во всём пакете. Поток событий в журнал не пишется: journal/writer.py:30-57 создаёт только run_manifest.json и telemetry.jsonl Второй из трёх потоков журнала отсутствует. Ни одно требование, опирающееся на события (MIS-12/24/31, отчёт §5.2 по transect_start/turn_start/photo), не может быть выполнено
SIM-MIS-21 Физика не обращается к системным часам и не зависит от частоты рендера M1 ЧАСТИЧНО В физике часов нет: grep -rn "datetime.now\|time.time\|perf_counter\|monotonic" src/ → единственное совпадение journal/writer.py:34 (generated_at). Рендера не существует, вторую половину приёмки («замедление рендера вдвое») проверить нечем Единственное обращение к часам стоит ровно там, где ломает бит-в-бит журнала (см. SIM-MIS-22/23 и критерий выхода M0)
SIM-MIS-22 Журнал = манифест + поток событий + телеметрия; append-only; повторный run_id отклоняется M0 РАСХОЖДЕНИЕ Есть 2 части из 3 (journal/writer.py:48-57), потока событий нет (см. MIS-20). run_id не существует: grep -rn run_id src/ → 0. Файлы открываются на перезапись: writer.py:50 manifest_path.open("w"), writer.py:53 telemetry_path.open("w"). Проверено: повторный sim run другого плана в тот же каталог молча перезаписал журнал, md5 6d7f8d3d…895d118d… Журнал не append-only и не защищён: прогон можно бесследно затереть другим прогоном. Отсутствие run_id делает невозможными parent_run_id, переигровку, ссылки «находок» на диапазон журнала
SIM-MIS-23 Манифест: все хеши входа, seed, dt, версии образа и YAML парка, import_fixups[], assumptions_used[] M0 ЧАСТИЧНО Фактический манифест (вывод ниже) содержит seed, dt_s, fleet_yaml_sha256, payloads_yaml_sha256, assumptions_used, world_snapshot_id. Сверка с contracts/log_schema.json (прогон скрипта, вывод ниже): отсутствуют 9 из 14 обязательных — run_id, scenario_sha256, plan_sha256, run_config_sha256, telemetry_hz, clock_mode, import_fixups, started_at, exit_status; нет также sim_version, image_digest Приёмка требования («по манифесту прогон воспроизводится на другой машине») невыполнима: в манифесте нет ни хеша, ни имени плана — по нему нельзя установить, что исполнялось. assumptions_used берётся из src/geoscan_sim/hardware/assumptions.yaml (4 записи), а не из contracts/assumptions.yaml (27 записей) — два параллельных реестра допущений
SIM-MIS-24 Поток событий покрывает закрытый словарь §4.3 M1 НЕ РЕАЛИЗОВАНО grep -rn "event" src/ --include=*.py → 0. В contracts/log_schema.json описаны 34 типа событий, в коде — ни одного Ни mode_change, ни violation, ни contingency не фиксируются; INH-10 (violations против warnings) необеспечен
SIM-MIS-25 Телеметрия 10 Гц без децимации, поля §4.4 вкл. clearance_m и xtrack_error_m; 108 000 ± 20 записей, ≤ 20 МБ M0 ТРЕБОВАНИЕ НЕГОДНОЕ Дефект требования: реестр (строка 289) требует 10 Гц / 108 000 ± 20 / ≤ 20 МБ, а §4.4 и таблица того же документа (строка 591) требуют 5 Гц децимацией k = round(1/(5·dt)) = 2 / 54 000 ± 10 / ≤ 10 МБ — прямое противоречие внутри одного свода. Факт реализации: 10 Гц и объём измерены прогоном на 180 мин — записей=108000 (в допуске), размер=21.55 МБ (вне лимита ≤ 20 МБ) уже при 11 полях вместо 31. Из 31 поля схемы присутствуют 3 (heading_deg, v_air_ms, v_ground_ms); из 4 обязательных — 0; clearance_m и xtrack_error_m отсутствуют Требование нельзя принять как есть: две взаимоисключающие частоты и бюджет объёма, недостижимый с собственным набором полей (JSONL 31 поле × 108 000 записей заведомо > 20 МБ — измерено 21,55 МБ уже на 11 полях). Фактически же телеметрия непригодна ни для покрытия, ни для CFIT: нет ни координат, ни борта, ни клиренса, ни бокового уклонения
SIM-MIS-26 Паспорт кадра: позиция, углы, наклонная дальность, GSD, footprint, смаз M1 НЕ РЕАЛИЗОВАНО grep -rn "photo" src/ → 0; camera/gsd.py — только gsd_m/gsd_cm по номинальной высоте (gsd.py:18 altitude_m * payload.gsd_coefficient), наклонной дальности нет photos_total факта, gsd_actual_cm_p50/p95, motion_blur_px_p95 невычислимы
SIM-MIS-27 Keyframes каждые 60 с модельного времени M2 НЕ РЕАЛИЗОВАНО grep -rni keyframe src/ → 0 M2
SIM-MIS-28 Отчёт именами и единицами валидатора; строка = план / факт / дельта M0 РАСХОЖДЕНИЕ fact_report.json (вывод ниже) содержит 4 строки: makespan_s, total_flight_time_s, coverage_fraction, energy_used_frac, и в каждой только planned/actualколонки delta нет. Имён валидатора нет: grep -rn "photos_total\|transects_total\|turns_total\|turn_time_fraction\|sorties_total\|uavs_used" src/ → все 0, violations.* в отчёт не выводятся. Эталонный набор — docs/task5/70-plan/01-validator-first.md:45-62 Diff имён непуст → M0-MUST нарушен. Хуже: «факт» не является фактом — coverage_fraction.actual копируется из плана (runner.py:93 metrics.get("coverage_fraction")), energy_used_frac.actual = None (runner.py:97), makespan_s.actual = min(план, 600) (runner.py:54). На s04: план 5280 с, «факт» 600,0 с. Сравнение «план против факта» — главный выход проекта — сейчас сравнивает план сам с собой
SIM-MIS-29 Допуски дельт заданы явно и проверяются; превышение — флаг M1 НЕ РЕАЛИЗОВАНО В fact_report.json нет ни дельт, ни допусков, ни колонки «в допуске». tolerance встречается только в validator/references.py и validator/runner.py — это допуски эталонов V-1…V-14, не план/факт Расхождение план/факт никак не квалифицируется
SIM-MIS-30 Дельта makespan раскладывается на компоненты, сумма = полной дельте ± 1 с M1 НЕ РЕАЛИЗОВАНО grep -rniE "decompos\|delta_wind\|Δ_" src/ → 0 SIM-BIZ-06 (M1, MUST) опирается на это же разложение — падает вместе
SIM-MIS-31 Фактические нарушения при валидном плане — в раздел «находки» M1 НЕ РЕАЛИЗОВАНО grep -rniE "finding\|находк\|cfit" src/ → 0. fact_report.preflight — это результат предполётной проверки плана, а не находки прогона То, что показывается жюри («план прошёл валидацию, а вот что случилось при облёте»), не производится
SIM-MIS-32 Отчёт содержит 11 метрик, недоступных планировщику M1 НЕ РЕАЛИЗОВАНО Грепнуты все 14 имён §5.3 (min_clearance_m, clearance_p05_m, cfit_events, link_loss_events, link_loss_total_s, autonomous_return_km, photos_out_of_transect, gsd_actual_cm, motion_blur_px, unphotographed_area_km2, landing_offset_m, min_separation_m, queue_wait_total_s, energy_margin_frac_min) — в src/ 0 совпадений у всех, кроме parachute_drift_m (3 совпадения, все в кинематике и эталонах, не в отчёте) Блок, ради которого симулятор существует, в отчёте отсутствует полностью
SIM-MIS-33 Пакетный режим: N сценариев × M планов одной командой, без GUI/GPU/сети M0 НЕ РЕАЛИЗОВАНО cli.py:51@click.argument("plan", ...), ровно один план за вызов. grep -rn "batch\|csv" src/ → 0. find . -iname "docker*" -o -iname "*compose*" (вне .venv) → пусто Офлайн-исполнение есть, пакетного режима нет. Приёмка («docker compose up проходит s01–s10 × 2 плана, CSV/JSON») невыполнима: нет ни контейнера, ни батча, ни CSV. M0-MUST не закрыт
SIM-MIS-34 Бюджет производительности: 1 борт-час ≤ 60 с реального на ядро, число в отчёте M1 ЧАСТИЧНО Измерено (вывод ниже): 1 борт-час модельного времени = 0,81 с реального; sim run на s01 (600 с модели) = wall=0.35 s. Бюджет соблюдён с запасом ×74. Но: замер на s04 невозможен (моделируется 1 борт из 3 и 600 с из 5280), wall_time_s/sim_time_s в манифест не пишутся (journal/writer.py:32-44) Число получено на прямолинейном стабе без рельефа, камеры, событий и второго борта — оно ничего не говорит о бюджете настоящего движка. Требование формально «проходит», фактически не проверено на целевой нагрузке
SIM-MIS-35 Пакетный прогон возвращает RC ≠ 0 при фактических нарушениях M1 НЕ РЕАЛИЗОВАНО sim run fixtures/mutants/m01_nfz_intrusion.jsonpreflight_codes: ["NFZ_VIOLATION"], RC=0. sim run fixtures/mutants/m06_ir_on_gemini.jsonPAYLOAD_INCOMPATIBLE (по §1.5 — fatal, прогон не должен стартовать), RC=0, журнал записан. cli.py:55-62 не анализирует preflight_codes CI не падает на нарушениях: регрессия алгоритмов планировщика через симулятор невозможна. Прямое противоречие §1.5 «фатальная ошибка → прогон не стартует»
SIM-MIS-36 Интерактивный режим: ≤ 200 мс, ≥ 30 FPS, рендер не влияет на журнал M2 НЕ РЕАЛИЗОВАНО UI отсутствует целиком: в simulate/ нет ни одного .ts/.tsx/.js/.html/.css M2
SIM-MIS-37 Режим разбора записанного прогона без пересчёта физики M2 НЕ РЕАЛИЗОВАНО grep -rni "replay\|seek" src/ → 0; читателя журнала в коде нет (journal/ содержит только writer.py) M2
SIM-MIS-38 Команды оператора в журнал и interventions[]; повтор байт-в-байт M2 НЕ РЕАЛИЗОВАНО grep -rni "interventions\|operator_command" src/ → 0 M2
SIM-MIS-39 Команда доходит до борта только при связи; отклонённая даёт событие M2 НЕ РЕАЛИЗОВАНО grep -rni command_rejected src/ → 0. env/link.py:LinkModel моделирует только круг радиодальности и таймер автономности, команд не знает M2
SIM-MIS-40 Сравнительный прогон двух планов, K = 20 парных сидов M2 НЕ РЕАЛИЗОВАНО grep -rni compare src/ → 0 M2

3. Главные расхождения

3.1. Настоящий KML/GeoJSON от планировщика не принимается — только внутренний JSON

Требовалось (SIM-MIS-02, INH-01, ТЗ п. 2, R-OUT-2/3): три обязательных формата — наша схема, GeoJSON RFC 7946, KML OGC 2.2 — дают идентичный список путевых точек. Сделано: src/geoscan_sim/mission/import_plan.py — 59 строк, единственный вход json.load.

$ grep -rniE "kml|geojson|kmz|xml" src/ tests/ tools/
(rc=1)

Ни парсера, ни фикстуры, ни теста. find по *.kml/*.geojson/*.kmz в репозитории — пусто. Почему разошлось: реализация писалась от фикстур fixtures/plans/*.json, которые созданы вручную в нашей схеме; настоящего артефакта планировщика в контуре не было. Чинить: импортёр KML (<Placemark>/<LineString>/<altitudeMode>/<ExtendedData>) и GeoJSON (FeatureCollection на борт, sortie_seq в properties), плюс тест «три формата → один список ППМ» на s01.

3.2. Журнал не соответствует собственному контракту contracts/log_schema.json

Требовалось (SIM-MIS-22/23/25, FIN-08 «заморозка контракта»): журнал = манифест + события + телеметрия, поля по §4.2–4.4. Сделано: две части из трёх, состав полей — свой.

manifest: отсутствуют обязательные поля: ['run_id', 'scenario_sha256', 'plan_sha256', 'run_config_sha256',
          'telemetry_hz', 'clock_mode', 'import_fixups', 'started_at', 'exit_status']
manifest: поля вне схемы: ['generated_at', 'phase_durations_s', 'preflight_codes', 'preflight_violations',
          'scenario_id', 'schema_version', 'telemetry_sha256', 'uav_model']
telemetry: отсутствуют обязательные: ['t_sim_s', 'uav_id', 'lon', 'lat']
telemetry: поля вне схемы: ['east_m', 'energy_left_wh', 'height_m', 'link_up', 'north_m', 'phase',
          't_s', 'track_deg']
telemetry: из 31 поля схемы присутствует: ['heading_deg', 'v_air_ms', 'v_ground_ms'] = 3

Схема не проверяется нигде: grep -rn "log_schema\|jsonschema" src/ tests/ даёт единственную загрузку в contracts/loader.py:63-66, которую никто не импортирует; в .venv пакет jsonschema даже не установлен (jsonschema installed: False). Почему разошлось: контракт писался как документ, движок — отдельно; связи «код → схема» нет ни в рантайме, ни в CI. Чинить: валидировать манифест и каждую запись телеметрии против log_schema.json в RunJournalWriter.write и в CI; привести TelemetrySample к полям §4.4 (в первую очередь uav_id, lon, lat, clearance_m, xtrack_error_m).

3.3. Поток событий отсутствует целиком

grep -rn "event" src/ --include=*.py0 совпадений. В log_schema.json описаны 34 типа событий, в требованиях §4.3 — закрытый словарь из 14 групп. В коде нет ни одного. Чем грозит: без событий не считаются transects_total, turns_total, photos_total, cfit_events, link_loss_events, не существует «находок» (MIS-31), не работает ни одна ссылка отчёта на журнал. Это один корень примерно половины «НЕ РЕАЛИЗОВАНО» в чеклисте.

3.4. Детерминизм подтверждён для телеметрии, но манифест бит-в-бит не воспроизводится

Требовалось (SIM-MIS-19, BIZ-03, критерий выхода M0 BRD §5: «2 прогона с одним seed → бит-в-бит журнал (sha256)»). Прогон (дословно):

$ sim run fixtures/plans/s01.json --seed 42 --output /tmp/aud_r1
{ "telemetry_sha256": "a6706a3732dc5ce2ac9bfdab3d1a3eea9c38e8199a3ba81d5c42900a5a2ee107" }
$ sim run fixtures/plans/s01.json --seed 42 --output /tmp/aud_r2
{ "telemetry_sha256": "a6706a3732dc5ce2ac9bfdab3d1a3eea9c38e8199a3ba81d5c42900a5a2ee107" }

$ sha256sum /tmp/aud_r{1,2}/telemetry.jsonl /tmp/aud_r{1,2}/run_manifest.json
d348f1ae5b32ab1708cde2034752a728af3d7469b25d6ef718105c01733b3110  /tmp/aud_r1/telemetry.jsonl
d348f1ae5b32ab1708cde2034752a728af3d7469b25d6ef718105c01733b3110  /tmp/aud_r2/telemetry.jsonl
c4cdacfd64b759e895024715e76c92d7bde35282908a42170883b1becf67ddf8  /tmp/aud_r1/run_manifest.json
7ffae6e301073b2f0f23ba9fa9d46efda0d76e0760b0b3dcf4e85528ab0d74d6  /tmp/aud_r2/run_manifest.json

$ diff /tmp/aud_r1/run_manifest.json /tmp/aud_r2/run_manifest.json
38c38
<   "generated_at": "2026-09-16T05:05:09.945877+00:00"
---
>   "generated_at": "2026-09-16T05:05:10.320729+00:00"

Телеметрия — бит-в-бит между процессами (это больше, чем проверяют тесты test_determinism_digest_stable_10x / test_run_deterministic_telemetry_hash_10x, которые гоняют 10 повторов внутри одного процесса). Манифест — нет: journal/writer.py:34 пишет datetime.now(). По букве SIM-MIS-22 журнал — это все три части, значит критерий выхода M0 не выполнен. Чинить: либо вынести generated_at из хешируемой части (как сделано для world_snapshot_id в SIM-WORLD-36), либо считать journal_sha256 по нормализованному манифесту; поле journal_sha256 в схеме уже предусмотрено.

3.5. Seed ни на что не влияет — PRNG в движке нет

$ sim run fixtures/plans/s01.json --seed 42  → telemetry_sha256: a6706a37…
$ sim run fixtures/plans/s01.json --seed 777 → telemetry_sha256: a6706a37…

grep -rn "random|Generator|PCG64" src/ не даёт ни одного использования генератора. SIM-MIS-19 требует единый PRNG с подпотоками на (подсистема, uav_id) — механизма нет, детерминизм достигнут отсутствием случайности. Чем грозит: как только появятся порывы ветра, шум GNSS, отказы и связь (домены ENV/M1), детерминизм придётся строить с нуля, и все зелёные тесты детерминизма сегодня ничего об этом не говорят. Это ложноположительный сигнал в CI.

3.6. «Факт» в отчёте план/факт — это переписанный план

mission/runner.py:54duration_s = min(_sortie_duration(sortie), 600.0); runner.py:93coverage_fraction.actual = metrics.get("coverage_fraction"); runner.py:97energy_used_frac.actual = None.

$ cat /tmp/aud/s04/fact_report.json     # план s04: 3 борта, 5 вылетов, makespan 5280 с
"makespan_s":          { "actual": 600.0, "planned": 5280 }
"total_flight_time_s": { "actual": 600.0, "planned": 12960 }
"coverage_fraction":   { "actual": 0.998, "planned": 0.998 }
"energy_used_frac":    { "actual": null,  "planned": 0.65 }

Проверено также, что «факт» не зависит от физики: план со speed_ms = 500 м/с даёт тот же makespan_s.actual = 600.0. Чем грозит: SIM-MIS-28 (M0), SIM-BIZ-27 (M0), UAV-40, BR-21 опираются на то, что симулятор даёт независимый факт. Сейчас он даёт копию плана, обрезанную константой 600 с. Любая демонстрация «план против факта» на этих числах вводит в заблуждение.

3.7. Ограничение 600 с и «первый вылет первой миссии» не описано ни одним требованием

runner.py:15-23 _first_sortie + runner.py:54 min(..., 600.0). На s04 из трёх бортов и пяти вылетов исполняется один борт в течение 600 с. В журнале нет ни признака усечения, ни списка пропущенных бортов, ни соответствующего warning'а. Это молчаливое сужение задачи — ровно то, что §1.5 запрещает («никаких „догадаемся сами“»). Чинить: как минимум — код RUN_TRUNCATED/warning в манифест; как максимум — многобортовый цикл, который требует SIM-MIS-16/20.

3.8. Нарушения не влияют на код выхода; фатальные коды §1.5 не останавливают прогон

$ sim run fixtures/mutants/m01_nfz_intrusion.json → preflight_codes: ["NFZ_VIOLATION"], RC=0
$ sim run fixtures/mutants/m06_ir_on_gemini.json  → preflight_codes: ["PAYLOAD_INCOMPATIBLE"], RC=0

PAYLOAD_INCOMPATIBLE по §1.5 — класс fatal, прогон не должен стартовать; он стартует и пишет журнал. SIM-MIS-35 (RC ≠ 0 при нарушениях) не выполнен. Чинить: в cli.py:run_cmd — раздельные коды выхода: fatal preflight → RC=2 без журнала, фактические нарушения → RC=1 с журналом.

3.9. Импорт не проверяет ничего, кроме фрейма высоты

Проверено прогонами: неизвестный тип фазы ("phase": "teleport"), оба триггера камеры одновременно, speed_ms = 500 м/с — все три приняты, RC=0, preflight_codes: []. Из 10 кодов §1.5 в коде существует один (UAV_MODEL_UNKNOWN), и тот роняет прогон traceback'ом. Семантический и перекрёстный уровни §1.4 отсутствуют. Чем грозит: SIM-MIS-05/06/07 (и косвенно MIS-03) — вход симулятора не защищён; любой дефект плана проявится не кодом ошибки, а неверными числами в отчёте.

3.10. Ошибки импорта отдаются Python-трейсбеком, а не машиночитаемым списком

$ sim run /tmp/aud/no_frame.json
...
geoscan_sim.mission.import_plan.PlanImportError: missions[0].sorties[0] missing altitude_frame
RC=1

Требование §1.5 — {code, severity, path, message, source_ref}. path фактически есть внутри строки, всё остальное — нет; кода ALT_FRAME_MISSING не существует в коде, хотя contracts/error_codes.yaml:21 его определяет и в шапке файла записано «Коды в коде симулятора — только из этого файла». Чинить: ввести исключение с полем code и таблицей из contracts/error_codes.yaml (загрузчик contracts/loader.py уже написан и не используется).

3.11. Preflight подменяет исполнение и опирается на литеральные константы

preflight.py:163 — NFZ определяется как max(lon) > 37.63; preflight.py:193 — вышка как |lon − 37.6495| < 0.001 и |lat − 55.772| < 0.001; preflight.py:119 — клиренс как alt < 100.0 без обращения к рельефу; preflight.py:130,133 — пороги interval ≥ 7.0 и spacing ≥ 200.0. Геометрия зон, HeightField и препятствия в проверках не участвуют; run_plan_headless мир вообще не загружает (world_snapshot_id — просто строка в манифесте). Следствие, проверенное прогоном: на валидном базовом плане s04 preflight выдаёт 4 ложных срабатывания (PAYLOAD_INCOMPATIBLE, NFZ_VIOLATION, ENDURANCE_EXCEEDED, PARACHUTE_OBSTACLE_CONFLICT), а тест ложных срабатываний покрывает только s01 (validator/mutate.py:run_base_plan_check). Чем грозит: «нарушения», которые видит симулятор, — свойство координатных констант, а не мира; на любом другом полигоне детекция развалится.

3.12. Сходимость по шагу (M0-MUST) невозможно даже попытаться проверить

SIM-MIS-17 и приёмка A-03 BRD требуют прогона с dt/2. sim run не имеет опции --dt; mission/runner.py:55-62 не передаёт dt_s в IntegratorConfig, оставляя дефолт 0,1 с; grep -rn DT_NOT_CONVERGED → 0. Чинить: --dt в CLI и run_config, автоматический парный прогон dt/dt/2 с флагом в манифесте.


4. Замечания к самим требованиям (часть 1)

4.1. SIM-MIS-25 — противоречие внутри свода (ТРЕБОВАНИЕ НЕГОДНОЕ). Реестр (90-final/02-requirements-registry.md:289): «10 Гц, без децимации, = шагу dt; 108 000 ± 20 записей; ≤ 20 МБ». Исходный документ, §4.4 и таблица требований (50-mission/01-mission-execution.md:591 и раздел «4.4 Телеметрия»): «5 Гц (A-MIS-06), получается децимацией шага, k = round(1/(5·dt)) = 2; 54 000 ± 10; ≤ 10 МБ». Это не редакционная неточность, а два взаимоисключающих механизма записи (децимация k=2 против k=1). Реализовано 10 Гц, но §4.4 остался непереписанным — тот, кто будет писать код по домену, прочтёт 5 Гц. Вдобавок бюджет ≤ 20 МБ несовместим с собственным набором полей: измерено 21,55 МБ на 108 000 записей JSONL уже при 11 полях; при 31 поле §4.4 это будет 50–60 МБ. Требование надо либо переводить на бинарный/колоночный формат, либо менять число.

4.2. Рассогласование уровня обязательности между исходным документом и реестром.

ID Исходный документ (строки 574–605) Реестр (строки 272–305)
SIM-MIS-08 MUST M2, SHOULD
SIM-MIS-18 MUST M2, SHOULD
SIM-MIS-34 SHOULD M1, MUST («повышено до MUST и объявлено единственным числом»)
SIM-MIS-38 MUST M2, COULD
SIM-MIS-39 MUST M2, COULD

Реестр объявлен головным, но исходный документ не обновлён. Пока оба файла в своде, любой вывод об «обязательности» зависит от того, какой файл читали. Особенно вреден SIM-MIS-34: он ещё и дублирует SIM-PLT-21 — два ID на один бюджет производительности (отмечено в сквозных темах свода), то есть у одного числа три разных нормативных статуса.

4.3. SIM-MIS-19 — приёмочный критерий не различает «детерминизм» и «отсутствие случайности». Формулировка «10 повторов → один sha256» выполняется тривиально любой чисто детерминированной программой без PRNG, что и произошло. При этом вторая половина критерия («добавление четвёртого борта не меняет траекторию первых трёх») требует многобортового прогона, которого в M0 не требует ни одно требование (SIM-MIS-16 говорит о единых часах, но нигде не сказано «M0 исполняет N бортов»). Итог: проверяемая половина ничего не доказывает, а доказывающая половина вне объёма вехи. Нужен критерий вида «прогоны с seed A и seed B различаются» плюс явное требование многобортового прогона в M0/M1.

4.4. SIM-MIS-22 против SIM-MIS-23 — дыра вокруг run_id. MIS-22 (M0) требует «повторная запись существующего run_id отклоняется», но MIS-23 (M0), перечисляющий обязательный состав манифеста, run_id не называет («хеши входа, seed, dt, версии образа и YAML парка, import_fixups[], assumptions_used[]). run_id обязателен только в contracts/log_schema.json. Реализация закономерно его не завела. То же касается exit_status, clock_mode, telemetry_hz, started_at — они обязательны по схеме и не названы ни одним текстовым требованием M0.

4.5. SIM-MIS-01 — run_config объявлен обязательным элементом входа, но нигде не специфицирован. Ни состава полей, ни формата, ни схемы: §3 описывает dt, частоту телеметрии и режим часов прозой, run_config_sha256 фигурирует только в §4.2. При этом MIS-09 («радиус задаётся в run_config»), MIS-11 («включение — флаг прогона»), MIS-16 («в run_config один dt»), MIS-29 («пороги вынесены в конфигурацию») все на него ссылаются. Нужен отдельный контракт run_config наравне с log_schema.json.

4.6. SIM-MIS-03 против SIM-GEO-09 — множество допустимых фреймов высоты не зафиксировано в одном месте. SIM-GEO-09: {AGL, AMSL, ELLIPSOIDAL}; §1.3 mission-документа: altitude_frame ∈ AGL | AMSL. Третьего варианта в mission-домене нет, а KML/MAVLink-фрейм RELATIVE (который реализация и приняла, import_plan.py:9) не упомянут нигде. Расхождение реализации здесь предсказуемо прямо из требований.

4.7. SIM-MIS-33 (M0, MUST) требует артефакта из чужого домена. Критерий приёмки — «docker compose up в изолированной сети проходит s01–s10 × 2 плана». Ни Dockerfile, ни compose-файл не являются предметом ни одного требования MIS; упаковка — домен PLT, где она заводится позже. M0-требование, которое невозможно закрыть средствами своего домена, будет провалено при любой добросовестной реализации.

4.8. SIM-MIS-17 (M0) требует переменного dt, которого не вводит ни одно требование. MIS-16 фиксирует dt = 0,1 с как константу и требует отклонять «шаг пер-борт»; MIS-17 требует автоматического прогона с dt/2. Ни одно требование не обязывает сделать dt параметром прогона. Формально непротиворечиво, практически — MIS-17 недостижим без того, что не потребовано.

4.9. SIM-MIS-28 — «diff имён полей пуст» проверяется только вручную. Валидатор планировщика — отдельный продукт, в этом репозитории его нет. Эталонный список имён приходится брать из docs/task5/70-plan/01-validator-first.md. Требование не указывает машиночитаемый источник имён (например, общий YAML-словарь метрик), поэтому «пустой diff» нечем автоматизировать — и это ровно тот шов, где расхождение и произошло.

4.10. SIM-MIS-35 против §1.5 — не определён приоритет кодов выхода. §1.5 требует RC ≠ 0 при фатальной ошибке импорта (прогон не стартует), MIS-35 — RC ≠ 0 при фактических нарушениях по итогам прогона. Это разные ситуации, но реестра кодов выхода (какой код при какой ситуации) в своде нет. Сейчас в реализации sim validate без флага даёт RC=2, ошибка импорта — RC=1, нарушения — RC=0; интерпретировать это снаружи невозможно.

4.11. SIM-MIS-29 — допуски объявлены допущением с открытым вопросом. A-MIS-08 (10 % / 2 п.п. / 2 % / 5 п.п.) и Q-17: в источниках таких чисел нет. Требование годное, но по правилу BRD §7.2 (величина с fidelity_class: assumed не может быть основанием NO-GO) вердикт по этим допускам нельзя использовать для отказа — это стоит записать прямо в требование, иначе оно будет прочитано как приёмочное.


5. Что осталось НЕ ПРОВЕРЕНО и почему

Строк со статусом НЕ ПРОВЕРЕНО в чеклисте нет — по каждому из 40 требований удалось получить доказательство в одну из сторон. Тем не менее ряд клауз внутри требований проверить было невозможно; это не меняет вердиктов (все они и так отрицательные по другим основаниям), но честность требует перечислить:

  1. SIM-MIS-23, клауза «прогон воспроизводится на другой машине с тем же образом». Второй машины и контейнерного образа в контуре нет (Dockerfile отсутствует). Вердикт ЧАСТИЧНО поставлен по составу манифеста (9 отсутствующих обязательных полей), а не по кросс-машинной проверке.
  2. SIM-MIS-21, клауза «не зависит от частоты рендера». Рендера не существует; проверено только отсутствие обращений к системным часам в физике (grep).
  3. SIM-MIS-19, клауза «добавление четвёртого борта не меняет траекторию первых трёх». Многобортового прогона нет в принципе (_first_sortie берёт один вылет), сконструировать проверку не на чем.
  4. SIM-MIS-34, замер на s04. Измерен стаб (0,81 с на борт-час); замер на целевой нагрузке (3 разнотипных борта, 40 км², 5280 с) невозможен — s04 исполняется как один борт 600 с. Приведённое число к бюджету требования отношения не имеет.
  5. SIM-MIS-17, прогон с dt/2. Технически неосуществим: dt не выводится ни в CLI, ни в run_config.
  6. Кросс-платформенная бит-идентичность. Проверена только повторяемость между двумя процессами на одной машине (x86-64, один .venv) — что и соответствует объявленному в BRD §11.2 ограничению.
  7. Валидация журнала внешним валидатором JSON Schema выполнена собственным скриптом сверки состава полей: пакет jsonschema в .venv не установлен (jsonschema installed: False), поэтому проверены required/properties вручную, без проверки типов и форматов (uuid, date-time). Типы полей, следовательно, не верифицированы — но это не влияет на вывод, поскольку сами поля отсутствуют.

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

cd /home/qovalenko/geoscan-wt/simulate-audit/simulate
.venv/bin/pytest -q                      # 50 passed in 1.13s
.venv/bin/sim run fixtures/plans/s01.json --seed 42  --output /tmp/aud_r1
.venv/bin/sim run fixtures/plans/s01.json --seed 42  --output /tmp/aud_r2
.venv/bin/sim run fixtures/plans/s01.json --seed 777 --output /tmp/aud_r3
sha256sum /tmp/aud_r{1,2}/telemetry.jsonl /tmp/aud_r{1,2}/run_manifest.json
diff /tmp/aud_r1/run_manifest.json /tmp/aud_r2/run_manifest.json
.venv/bin/sim run fixtures/plans/s04.json --output /tmp/aud/s04
.venv/bin/sim run fixtures/mutants/m01_nfz_intrusion.json --output /tmp/aud/m01; echo $?
grep -rn "event" src/ --include=*.py
grep -rniE "kml|geojson|kmz|xml" src/ tests/ tools/