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

Merge-лог: слияние принятых пакетов исправлений geoscan (Б-1…Б-9)

Ветка-приёмник — feat/geoscan-m1, база слияния — 5e046484 («docs(task5): merge-log приёмки волны 4 geoscan»). Область — продукт geoscan, всё вне simulate/. Дата слияния — 17.09.2026.

Блокеры и шаги воспроизведения взяты дословно из git show accept/geoscan-final:docs/task5/96-acceptance/01-blockers.md. Разборы ревью — /home/qovalenko/geoscan-queue/fix/reviews/review-geoscan-*.md.


1. Что влито

Пакет Блокер Вердикт ревью Влит Чем подтверждено
fix/gs-clean-clone Б-7 Из чистого клона проект не запускается ПРИНЯТО да, 84bdf0d7 Настоящий git clone в /tmp/gs-clean, без make: uv run pytest --collect-only275 tests collected; npm ci && npm run build в gateway/BUILD_EXIT=0 без ручной сборки shared
fix/gs-energy-model Б-1 Валидатор не допускает ни один план ПРИНЯТО да, d015be2a s01/s02/s06 × 2 критерия: G3-04 ok, ВЕРДИКТ: план допущен, EXIT=0. По всем 10 сценариям × 2 критерия допущено 18 из 20 (на базе — 0)
fix/gs-nfz-avoid Б-2 План заходит в НФЗ со статусом feasible ПРИНЯТО да, 78645f9b s02: violations.nfz 1610,07 → 0,0; валидатор G1-01…G1-04 ok, 0,000 m; числа планировщика и валидатора совпали
fix/gs-mixed-fleet Б-3 Смешанный парк: пустой план feasible ПРИНЯТО да, eff73b7a (конфликт) s04: 0 миссий/coverage 0.0 → 1 миссия fw-201, coverage 0.3398, наружу UAV_UNUSED×2 + PARTIAL_COVERAGE
fix/gs-objective-total Б-4 «Минимум налёта» проигрывает своей же функции ПРИНЯТО да, ba413af7 s03: total_flight_time_s в режиме total_flight_time 3452,84 → 2095,92 с, что теперь меньше 2300,92 с режима makespan (на базе было в 1,5 раза больше)
fix/gs-export-sorties Б-9 GeoJSON не различает вылеты ПРИНЯТО да, a9d564cb s06-wind: у 36 маршрутных фич появились sortie (1/2), task_id, transect_id, start_s, duration_s; уникальных (uav_id, sortie, seq) — 36 из 36 (на базе 24 из 39, sortie отсутствовал)
fix/gs-export-layers Б-8 REST-экспорт теряет слои сценария ПРИНЯТО да, 9936acd9 Живой прогон REST (gateway + planner gRPC, s02-nfz): <Placemark> REST=27 против CLI=27 (на базе 19 против 23), <Document>scenario в REST появился, слой nfz REST=3 (был 0)
fix/gs-deploy-contract Б-6 Деплой в k3s сломан by-construction ПРИНЯТО да, ce205174 Живой python -m validator serve: /healthzHTTP 200 {"status":"ok"}, /readyzHTTP 200 {"ready":true}, /metrics отдаёт geoscan_validator_violations_total. На базе HTTP-сервера нет вовсе (HTTP 000). 70-migrate-job.yaml в kustomization.yaml, geoscan-validator в scrape-конфиге

2. Что НЕ влито

Пакет Блокер Вердикт ревью Влит
fix/gs-test-quality Б-5 Тестовый контур маскирует дефекты НА ДОРАБОТКУ нет

Ветка fix/gs-test-quality @ 007527e9 остаётся в репозитории; в feat/geoscan-m1 не вливалась. Разбор — §6.


3. Конфликты и как они решены

Пакеты резались по блокерам, а не по файлам, поэтому пересечения были ожидаемы. Фактически пересеклись четыре файла.

3.1. src/backend/planner/planner/pipeline.py — единственный текстовый конфликт

Конфликт при слиянии fix/gs-mixed-fleet поверх уже влитого fix/gs-nfz-avoid, в хвосте plan(). Обе стороны переписали одну и ту же строку return draft.model_copy(update={"metrics": metrics}), каждая — своим сторожем статуса:

  • nfz-avoid (Б-2): violations.nfz/violations.airspace > 0,01 → INFEASIBLE;
  • mixed-fleet (Б-3): пустой план при нулевом покрытии → INFEASIBLE, плюс предварительное добавление предупреждения PARTIAL_COVERAGE.

Решение — оба, а не один. Это два независимых основания понизить статус, и они не конкурируют: ни один из сторожей статус не повышает, поэтому порядок между ними безразличен, а выбор «взять свою сторону» молча потерял бы один из двух блокеров. Собранная версия: сначала PARTIAL_COVERAGE (он должен попасть в warnings до проверки unassigned or warnings), затем оба сторожа, затем одно обновление модели metrics + warnings + solver. Форма обновления взята из mixed-fleet (updates: dict), потому что nfz-avoid не переносил изменившийся список warnings в итоговый план — при его форме PARTIAL_COVERAGE/UAV_UNUSED наружу бы не вышли.

Коммит eff73b7a, итоговый фрагмент — pipeline.py:823-846.

3.2. pipeline.py, ветка полос — пересечение без текстового конфликта

Git слил автоматически, но правки семантически пересекаются, поэтому проверено руками:

  • nfz-avoid заменил источник полос: split_into_bands(poly, …) → разбиение вырезанной области ctx.flyable_parts;
  • mixed-fleet расширил условие входа в эту ветку (use_area_bands = options.naive_equal_split or len(spacings) > 1) и добавил внутрь полосы вызов assign_transects.

После слияния живы обе правки: полосы строятся из flyable_parts, а вход в ветку открыт и для смешанного парка. Проверено прогоном s02 (НФЗ, одна ширина полосы) и s04 (без НФЗ, три ширины) — оба дают ожидаемые ревью числа.

3.3. planner/tests/test_pipeline_defects.py — противоречие двух тестов одного места

Слито автоматически, но результат разобран отдельно, потому что здесь два теста одного и того же утверждения противоречили друг другу:

  • test_d75_energy_frac_uses_reserve_budget (был на базе, сохранён в ветках nfz-avoid и mixed-fleet как неизменённый хвост) требует energy_used_frac = used / budget_s, то есть от бюджета с вычетом резерва;
  • test_b1_energy_used_frac_matches_validator_g3_04 (пакет energy-model) требует доли полного запаса хода и проверяет это через реальный вердикт валидатора G3-04.

Неверен был d75. Ту же семантику «доля полного запаса» независимо задают контракт (90-business/08-data-model.md:325), фикстуры валидатора (validator/tests/fixtures.py:260), фронтендовая модель (src/frontend/packages/domain/src/planner.ts:583) и эталонные планы (src/backend/plans/s01-simple.plan.json:47 = 0,7128, а не 0,89). Git разрешил это верной стороной сам (удаление из energy-model победило, так как соседние ветки строку не меняли); решение подтверждено и оставлено.

Сопутствующая правка при слиянии: три документа ссылались на удалённый тест и описывали отменённую семантику. Актуализированы (коммит с merge-логом): docs/task5/94-fixes/planner-core.md:25 (D-75 → «ОТМЕНЕНО (Б-1)»), docs/task5/00-brief/04-requirements-checklist.md:56,121 (ссылки на test_b1_…).

3.4. test_solver_optimize.py, uv.lock, proto/geoscan/planner.proto

  • test_solver_optimize.py: nfz-avoid убрал s02 из списка скрытых исключений гейта, objective-total добавил проверку R-OPT-2. Правки в разных местах файла, слиты без конфликта, обе сохранились.
  • uv.lock: clean-clone (build-зависимость geoscan-proto) и deploy-contract (prometheus-client у валидатора) — непересекающиеся записи, авто-слияние. После слияния uv sync пересобрал geoscan-proto и geoscan-validator.
  • planner.proto: export-layers добавил bytes scenario_json = 4 в ответ. После слияния стабы перегенерированы (uv sync --reinstall-package geoscan-proto), TS-типы — npm run proto:gen внутри npm run build.

3.5. Правка куратора вне пакетов: гонка в тесте очереди

tests/integration/test_gateway_data.py::test_queue_full_returns_429_with_retry_after падал 2 прогона из 5 в одиночку (assert 202 == 429). Причина в тесте, не в продукте: он сеял ровно PLAN_QUEUE_LIMIT = 2 задачи, пока gateway остановлен SIGSTOP, и после SIGCONT воркер (PLAN_CONCURRENCY=1) успевал забрать одну в running раньше, чем долетал POST /api/plans — очередь оказывалась короче предела.

Флейк предсуществующий: файл последний раз менялся волной 3 (70b7d7b3 gateway-data), ни один из влитых пакетов Б-1…Б-9 его не трогает. Починено минимально (коммит 3f9880a7): сеем limit + 20 задач. Проверяемый инвариант не ослаблен — тест по-прежнему требует 429, Retry-After: 30 и errorCode: QUEUE_FULL. После правки 6 прогонов из 6 зелёные.


4. Тесты продукта после всех слияний

Ничего не исключалось, набор гонялся целиком.

4.1. Python: uv run pytest -q (из src/backend)

########## ФИНАЛЬНЫЙ ПРОГОН: uv run pytest -q
..........ssss.......................................................... [ 26%]
........................................................................ [ 52%]
........................................................................ [ 78%]
...........................................................              [100%]
=============================== warnings summary ===============================
.venv/lib/python3.12/site-packages/_pytest/pathlib.py:95
  /home/qovalenko/geoscan-wt/fix-curator-gs/src/backend/.venv/lib/python3.12/site-packages/_pytest/pathlib.py:95: PytestWarning: (rm_rf) error removing /tmp/pytest-of-qovalenko/garbage-8e749f96-c83f-4b78-be33-bdca7507b710
  <class 'OSError'>: [Errno 39] Directory not empty: '/tmp/pytest-of-qovalenko/garbage-8e749f96-c83f-4b78-be33-bdca7507b710'
    warnings.warn(

-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
271 passed, 4 skipped, 1 warning in 127.55s (0:02:07)

Набор устойчив: три полных прогона подряд зелёные, в том числе один со случайным порядком (pytest-randomly включён по умолчанию) и два с -p no:randomly; последний — 271 passed, 4 skipped, 1 warning in 118.04s (0:01:58).

4 skipped — предсуществующий гейт tests/e2e по поднятому стенду (tests/e2e/conftest.py), к слиянию отношения не имеет.

4.2. gateway: npm test (vitest)

 ✓ src/common/fastify/request-id.plugin.spec.ts (2 tests) 6ms
 ✓ src/config/config.schema.spec.ts (2 tests) 8ms
 ✓ src/config/env-example.sync.spec.ts (1 test) 6ms
 ✓ src/fleet/fleet.catalog.spec.ts (2 tests) 7ms
 ✓ src/plans/plans.repository.spec.ts (2 tests) 5ms
 ✓ src/plans/plans-export.service.spec.ts (1 test) 7ms
 ✓ src/openapi.snapshot.spec.ts (1 test) 138ms

 Test Files  7 passed (7)
      Tests  11 passed (11)

4.3. frontend: npm test (vitest)

 ✓ packages/domain/src/format.spec.ts (4 tests) 28ms
 ✓ packages/api-client/src/endpoints/scenarios.spec.ts (1 test) 11ms
 ✓ packages/api-client/src/client.spec.ts (3 tests) 19ms
 ✓ packages/state/src/plan-poll.spec.ts (4 tests) 20ms
 ✓ packages/state/src/mappers.spec.ts (4 tests) 19ms
 ✓ packages/domain/src/validation-temperature.spec.ts (1 test) 155ms

 Test Files  11 passed (11)
      Tests  24 passed (24)

4.4. Архитектурные контракты: uv run lint-imports --config .importlinter

Analyzed 116 files, 493 dependencies.
-------------------------------------

Валидатор не видит планировщик KEPT
Ядро без транспорта KEPT (2 ignored imports)
Контракты без геометрии KEPT
Слои валидатора KEPT

Contracts: 4 kept, 0 broken.

5. Невоспроизводимость блокеров на feat/geoscan-m1

Каждый блокер прогнан по шагам из 01-blockers.md дважды: на базе слияния 5e046484 (отдельный worktree /tmp/gs-merge-base, туда скопированы proto-стабы и собраны node-зависимости) и на слитой ветке.

Б-1 — валидатор не допускает ни один план планировщика

Дословные команды блокера, s01/s02/s06 × 2 критерия:

база 5e046484 feat/geoscan-m1
s01/s02/s06 ВЕРДИКТ: план НЕ допущен, EXIT=4, FAIL G3-04 ok G3-04, ВЕРДИКТ: план допущен, EXIT=0

Расширенно, все 10 сценариев × 2 критерия (planner runvalidator check):

s01-simple         makespan/total     EXIT=0 FAIL=[]
s02-nfz            makespan/total     EXIT=0 FAIL=[]
s03-multi-sites    makespan/total     EXIT=0 FAIL=[]
s04-mixed-fleet    makespan/total     EXIT=0 FAIL=[]
s05-mixed-survey   makespan/total     EXIT=0 FAIL=[]
s06-wind           makespan/total     EXIT=0 FAIL=[]
s07-endurance      makespan/total     EXIT=4 FAIL=[G3-01]
s08-infeasible-gsd makespan/total     EXIT=0 FAIL=[]
s09-high-wind      makespan/total     EXIT=0 FAIL=[]
s10-replan         makespan/total     EXIT=0 FAIL=[]

18 планов из 20 допущены (на базе — 0 из 10). Блокер не воспроизводится. Остаток по s07 — §6.3, это не Б-1 и не регрессия слияния.

Б-2 — план заходит в НФЗ со статусом feasible

planner run --scenario scenarios/s02-nfz.json --objective makespan:

база ветка
solver.status feasible feasible
violations.nfz 1610,074 0,0
валидатор G1-01 FAIL 971,096 m ok 0,000 m
валидатор G1-04 (буфер) FAIL ok 0,000 m
расхождение планировщик/валидатор 1610,07 против 971,10 0,0 против 0,000 — сошлись
coverage_frac 0,9999 0,9846

Падение покрытия на 1,5 % — физически непокрываемый коридор буфера 25 м вдоль зоны, цена обхода; makespan вырос 1021,4 → 1107,9 с. Блокер не воспроизводится.

Б-3 — смешанный парк: пустой план со статусом feasible

planner run --scenario scenarios/s04-mixed-fleet.json --objective makespan:

база ветка
missions 0, [] 1, ['fw-201'] (Геоскан 201, как и требует expected.note)
coverage_frac 0,0 0,3398
unassigned 147 × endurance_exceeded 123 × endurance_exceeded
warnings [] — наружу ничего ['UAV_UNUSED', 'UAV_UNUSED', 'PARTIAL_COVERAGE']

Формулировка блокера («0 миссий, покрытие 0.000, ошибки наружу нет») не воспроизводится. Оговорка ревью сохраняется: статус остался feasible при 34 % покрытия, наружу это выходит предупреждением, а не статусом — §6.2.

Б-4 — «минимум суммарного налёта» проигрывает по своей же функции

planner run --scenario scenarios/s03-multi-sites.json с двумя критериями:

база: makespan база: total_flight_time ветка: makespan ветка: total_flight_time
total_flight_time_s 2300,98 3452,84 2300,92 2095,92
total_distance_m 20 309,8 32 728,4 20 309,2 19 159,2
миссий 3 2 3 2

На базе режим total_flight_time давал налёт в 1,5 раза больше, чем makespan. На ветке он даёт 2095,92 < 2300,92 с, то есть выигрывает по собственной целевой функции; дистанция тоже стала меньше. Блокер не воспроизводится.

Б-6 — деплой в k3s сломан by-construction

Прогон на живом кластере не выполнялся (как и в самой приёмке — вывод блокера статический). Проверено кодом и манифестами, плюс живой запуск сервиса локально:

база ветка
python -m validator serve, GET :OPS_HTTP_PORT/healthz HTTP 000 — HTTP-сервера нет HTTP 200 {"status": "ok"}
/readyz HTTP 200 {"ready": true}
/metricsgeoscan_validator_violations_total метрики нет в коде # HELP geoscan_validator_violations_total …
70-migrate-job.yaml в kustomization.yaml нет (0 вхождений) есть
geoscan-validator в scrape-конфиге Prometheus нет (0 вхождений) targets: ["validator.geoscan.svc.cluster.local:3002"]

Пробы k3s/geoscan/40-deployment-validator.yaml:52-62 (/readyz, /healthz на :3002) теперь имеют за собой реальный сервер. Блокер не воспроизводится. Живой apply -k на n2 остаётся невыполненным — §6.4.

Б-7 — из чистого клона проект не запускается

Настоящий git clone в /tmp/gs-clean, make на машине отсутствует (NO MAKE), стабы в клоне не лежат (в proto-gen/src/geoscan/ только __init__.py), shared/dist отсутствует.

Шаг блокера база ветка
uv run pytest -q 6 errors during collection, ImportError: cannot import name 'validator_pb2_grpc' 275 tests collected in 2.08s — стабы генерирует uv sync через proto-gen/setup.py
npm run build в gateway/ без ручной сборки shared 26 ошибок TS (в чистом клоне вовсе exit 127: нет node_modules) BUILD_EXIT=0; prebuild сам делает npm --prefix ../shared ci && run build

Блокер не воспроизводится. Побочно это же объясняет 13 ERROR в tests/integration, которые видели все ревьюеры: у них не был собран shared/dist, и модульные фикстуры gateway падали на npm run build. После clean-clone эти 13 ошибок исчезли.

Б-8 — REST-экспорт теряет слои сценария, включая НФЗ

Живой прогон REST-пути (planner gRPC + node dist/main.js, POST /api/plans на s02-nfz, затем GET /api/plans/:id/export/kml), сравнение с python -m planner export на том же плане:

база ветка
<Placemark> REST против CLI 19 против 23 27 против 27
<Document>scenario в REST-KML нет есть
вхождений слоя nfz в REST-KML 0 (в CLI — 3) 3 (в CLI — 3)
вхождений allowed (разрешённое ВП) 0 (в CLI — 1) 1 (в CLI — 1)
фич в REST-GeoJSON 19 27

Оператор, открывший KML из веб-интерфейса, снова видит бесполётные зоны. Блокер не воспроизводится.

Б-9 — GeoJSON-экспорт не различает вылеты

planner export --fmt geojson на плане s06-wind (2 вылета):

база ветка
фич всего 39 39
фич со свойством sortie 0 36 (номера вылетов 1, 2)
transect_id 0 36
task_id 1 37
start_s / duration_s (время, Q-08) 0 / 0 36 / 36
уникальных (uav_id, sortie, seq) 24 из 39 37 из 39

Оставшиеся 2 неуникальные тройки — это 3 слоя сценария (у них sortie: null); у всех 36 маршрутных фич тройка уникальна. R-OUT-8 в GeoJSON восстановим. Блокер не воспроизводится.


6. Что осталось незакрытым

6.1. Б-5 — пакет fix/gs-test-quality не принят, вход в следующую итерацию

Вердикт ревью (заход 3 из 3): НА ДОРАБОТКУ. Перечисленная в Б-5 маскировка снята по-настоящему (скрытый deselect s02/s07/s10, CI фронтенда, barriers.sh, спека-копия request-id, тавтологии, забор rel=0.5), но пакет внёс собственные дефекты. Задание следующего захода, дословно по разбору:

  1. docs/task5/10-hardware/fleet.yaml — убрать geoscan_gemini.altitude_agl_min: 100. Константа подогнана под ожидаемое число теста и противоречит первоисточнику (assets/pdf/geoscan-gemini-specs.pdf минимума не содержит) и четырём производным документам, где минимум 100 м AGL закреплён только за Геоскан 201. Справочник ТТХ под ожидаемый результат теста не правится.
  2. test_ensure_scenario_acceptable_rejects_unreachable_gsd_on_s08_fleet перестроить так, чтобы он не опирался на выдуманную константу.
  3. planner/rpc/convert.py — убрать фильтр по ветру из проверки достижимости GSD. Пакет сломал RPC-путь на s09: сценарий, дающий план на базе, начинал падать с Infeasible: GSD_UNREACHABLE, вопреки BR-23 и expected самого s09.
  4. Добавить тест на п. 3 (run_plan_stream на s09 не бросает Infeasible).
  5. Гейт report.admitted is True развести по expected.input_error — сейчас параметр s08 не может позеленеть никогда, это такой же тупик, как скрытый skip.
  6. scripts/barriers.sh — вернуть gateway-typecheck и npm test в исполняемую часть: при set -euo pipefail красный python-прогон обрывал скрипт и выбивал node-барьеры из job'а.
  7. docs/task5/98-fixes/test-quality.md:12 — убрать неверное утверждение про ветер на RPC-пути.

Следствие для продукта — дыры в тестовом контуре остаются открытыми:

  • planner/tests/test_solver_optimize.py:97-100 — скрытый deselect s02/s07/s10 внутри теста «validator zero violations» всё ещё на месте для s07 и s10 (s02 снял nfz-avoid). Гейт по-прежнему не краснеет от s07/s10;
  • CI фронтенда (.gitea/workflows/frontend-quality.yaml) гоняет только typecheck+build: 11 spec-файлов и tools/verify-numbers.ts в CI не исполняются (локально зелёные — §4.3, но CI их не защищает);
  • scripts/barriers.sh гоняет только tests/barriers (10 тестов из 275): planner/tests, validator/tests, tests/integration не запускаются ни одним job'ом. То есть зелёный CI на этой ветке не равен зелёному §4.1;
  • тавтологии и вакуумные циклы в test_pipeline_defects.py, забор rel=0.5 в validator/tests/test_checks.py:94, спека-копия gateway/src/common/fastify/request-id.plugin.spec.ts — в силе.

Это самый тяжёлый незакрытый долг: §4 доказывает работоспособность этим прогоном, а не автоматикой, которая поймает регрессию завтра.

6.2. Хвосты принятых пакетов (ревью приняли с оговорками)

  • Б-3, остаток. Полосы делятся поровну по площади, без учёта возможностей борта: 32-минутный мультиротор получает ту же треть 357-километрового полигона, что и Геоскан 201, не отлетает её, и полоса теряется. Отсюда покрытие 0,34 на s04 и s10. Смешанный парк формально перестал быть пустым, но смешанным ещё не стал.
  • Б-3, статус. Пустой план стал infeasible, но план с покрытием 0,34 остаётся feasible — наружу это выходит только предупреждением PARTIAL_COVERAGE.
  • Б-3, данные. s04-mixed-fleet в name/description заявляет «~40 км²», фактическая площадь ~357 км². Правка данных сценария вынесена за пакет.
  • Б-2, бюджет возврата. pipeline.py:231 всё ещё оценивает возврат домой по прямой, тогда как фактический возврат строится обходом. На текущих сценариях не стреляет; на сценарии, где обход длиннее хорды, даст G3-01 FAIL.
  • Б-2, полосы по нескольким частям. split_into_bands вызывается для каждой части клипа отдельно и результаты складываются в один список, а выбор идёт по bands[i] — при клипе из более чем одной части полосы со второй части молча потеряются. Проверено: на всех 10 сценариях клип даёт ровно 1 часть, поэтому сейчас не стреляет. Латентный дефект, унаследованный от nfz-avoid.
  • Б-2, покрытие на s02. G6-01 warn 0,985 < 0,99 — непокрытый коридор буфера 25 м. Физически ожидаемо, но BR-21 для s02 декларирует покрытие 1,0; формулировку BR-21 стоит согласовать с наличием буфера.
  • Б-4, слабая модель стоимости. mip_assign_transects остался с упрощённой стоимостью и теперь обслуживает только pareto/naive — то есть pareto унаследовал ровно ту модель, из-за которой ломался total_flight_time.
  • Б-6, фикстура. tests/integration/test_validator_grpc_server.py:69-73ops_http_port не запрашивает grpc_server_port; тест проходит благодаря порядку в файле, в одиночку упадёт. Под -k/xdist набор станет красным.
  • Б-6, дубль. validator/validator/rpc/ops.py — построчная копия planner/planner/rpc/ops.py; кандидат на вынос в shared-py.
  • Б-7, quickstart. docs/task5/95-manual/02-quickstart.md:52-66 по-прежнему предписывает ручной grpc_tools.protoc. Шаг стал избыточным, но для жюри выглядит как обязательная ручная работа. Обновлён только src/backend/README.md.
  • Б-7, мёртвый код. proto-gen/generate_stubs.py:36stubs_present() нигде не используется.
  • Б-7, побочные эффекты теста. tests/test_clean_clone_b7.py правит рабочее дерево (SHARED_DIST.unlink()) и делает npm ci при обычном uv run pytest — сетевой и разрушающий шаг внутри python-прогона.
  • Б-8, покрытие теста. Новый gRPC-тест гоняется на s01-simple, где НФЗ нет; tests/e2e/test_stand_acceptance.py не утверждает наличие <Document>scenario.
  • Б-9, накопление времени. export/geojson.py:60-61continue на len(ph.path) < 2 стоит до накопления phase_start_s; при пустой геометрии этапа время последующих этапов «уедет». Сейчас таких этапов нет ни на одном сценарии.

6.3. Дефекты вне области Б-1…Б-9, не закрытые никем

G3-01 на s07-endurance — единственный оставшийся недопуск. Перерасход бюджета вылета: gemini-01/s17: 1921 с при бюджете 1920 с, gemini-01/s21: 1931 с при бюджете 1920 с.

Это не регрессия слияния — проверено прямым прогоном на базе 5e046484:

# база 5e046484
FAIL  G3-01  energy     Бюджет вылета с резервом                      170.753 s
FAIL  G3-04  energy     Согласованность energy_used_frac              42.000 шт
ВЕРДИКТ: план НЕ допущен
EXIT=4

# feat/geoscan-m1 после слияния
FAIL  G3-01  energy     Бюджет вылета с резервом                      170.722 s
ВЕРДИКТ: план НЕ допущен
EXIT=4

Величина практически не изменилась (170,753 → 170,722 с), G3-04 ушёл. Ни один из пакетов Б-1…Б-9 эту проверку не закрывает: Б-1 назвал ровно одну причину — G3-04. Похоже на ошибку округления при дроблении галса (split_transect). Завести отдельной задачей.

Побочно слиянием закрылись G6-03 («нет ни одного этапа turn») на s04/s09/s10 и G3-01 на s10, которые ревью Б-1 фиксировало как ничейные.

6.4. Регрессии слияния

Не обнаружено. Все три класса проверок (§4 тесты, §5 воспроизведение блокеров, контракты import-linter) зелёные; ни один влитый пакет не отменил правку соседа. Единственное падение, встреченное при слиянии, — test_queue_full_returns_429_with_retry_after, — предсуществующий флейк самого теста, разобран и починен в §3.5.

Отдельно зафиксирован наблюдавшийся один раз флейк tests/integration/test_gateway_data.py::test_readyz_503_when_postgres_down (URLError на /readyz после docker stop контейнера Postgres; в одиночку и в последующих полных прогонах — зелёный). Тест того же семейства «останови инфраструктуру и успей опросить за 5 с»; к влитым пакетам отношения не имеет, но кандидат на ужесточение таймингов в пакете Б-5.

6.5. Организационное — до раскатки

  • Образы стенда протухли. docker-compose.yml и src/backend/docker-compose.yml пиннят BUILD_COMMIT: 7173c9af, k3s/geoscan/* — тот же тег. После этого слияния образы надо перевыпустить с новым sha, иначе и compose-стенд, и k3s поедут на сборке без Б-1…Б-9. Тег образа — 8-символьный sha этого merge-коммита.
  • apply -k на n2 не выполнялся. Проверка Б-6 статическая плюс локальный запуск сервиса; живой прогон kubectl --context n2 apply -k k3s/geoscan/ и выход пода validator в Ready — за раскаткой.
  • ttlSecondsAfterFinished: 86400 при фиксированном имени Job по sha: повторный apply -k того же образа спустя сутки пересоздаст Job и прогонит миграции заново — требует идемпотентности мигратора, нигде не зафиксированной.