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

03. Публичный контракт: REST гейтвея

Раздел комплекта системных требований сервиса планирования и распределения беспилотных авиационных работ. Описывает единственный публичный интерфейс продукта — HTTP-API гейтвея на /api: его маршруты, асинхронную модель расчёта, поток прогресса, контракт ошибок, идемпотентность, выгрузку файлов и справочники. Это развёртка принципов A-01, A-04…A-07, A-09 из ../50-stack/05-service-architecture.md, а не их пересмотр.

Что покрывает: форма запроса и ответа каждой ручки /api/..., коды состояния, заголовки, статусная машина задачи расчёта, формат событий SSE, тела ошибок, правила идемпотентности, лимиты входа, генерация OpenAPI.

Чего не покрывает:

  • содержимое тел сценария и плана — они целиком заданы в ../40-formats/03-our-mission-schema.md и здесь не дублируются, а цитируются ссылкой;
  • gRPC-контракты Planner.Plan, Planner.ExportPlan, Validator.Validate, метаданные вызовов и proto-пакеты — раздел про внутренние контракты;
  • ops-контракт (/healthz, /readyz, /status, /metrics), метрики и журналирование — разделы про сервисы и наблюдаемость; здесь ops-маршруты упоминаются только как исключение из правила «публичное — это /api»;
  • очередь расчётов, диспетчер и таблица plan_jobs как механизм — раздел про гейтвей и хранение; здесь фиксируется только то, что из очереди видно снаружи (статусы, 429);
  • содержимое файлов KML/GeoJSON/.plan/.waypoints../40-formats/.

Первоисточники: принципы — ../50-stack/05-service-architecture.md; чеклист R-xx — ../00-brief/04-requirements-checklist.md; бизнес-требования — ../90-business/05-fr-planning.md (FR-PLN, FR-VAL, FR-EXP, FR-MON), ../90-business/04-fr-inputs.md (FR-SCN, FR-TSK, FR-FLT), ../90-business/06-business-rules.md (BR), ../90-business/07-nonfunctional.md (NFR); схемы входа и выхода — ../40-formats/03-our-mission-schema.md; сценарии приёмки s01–s10 — ../70-plan/02-test-scenarios.md; ожидания фронтенда — ../../../src/frontend/README.md.

Решения, принятые как данность (обоснование — 11-decisions.md): D-01 — аутентификации пользователей нет, REST публичный, Authorization не требуется и не проверяется; D-02 — очередь расчётов в Postgres, не более 2 одновременных расчётов и не более 20 ожидающих; D-03 — сценарии и планы хранятся в Postgres, файлы экспорта генерируются на лету и не хранятся; D-04 — экспорт идёт через Planner.ExportPlan, гейтвей только транслирует файл; D-05 — PostGIS на старте выключен.

Расхождение с ../50-stack/03-architecture-draft.md (раздел «API») зафиксировано и разрешено в пользу A-07: пути — /api/plans, а не /plan; синхронный режим — только при time_limit_s ≤ 5, а не ≤ 30; перепланирование — POST /api/plans/{id}/replan, а не POST /plan/replan. Черновик в этой части считается устаревшим.


1. Реестр маршрутов и общая форма

Полный публичный перечень. Всего, чего нет в этой таблице (и в /docs), наружу не существует.

Метод Путь Назначение Успех
POST /api/scenarios создать или импортировать сценарий 201
GET /api/scenarios список сценариев, включая демо s01–s10 200
GET /api/scenarios/{id} сценарий целиком 200
POST /api/plans поставить расчёт плана в очередь 202 (200 в sync-режиме)
GET /api/plans/{id} статус задачи и результат 200
GET /api/plans/{id}/events поток прогресса (SSE) 200
GET /api/plans/{id}/export/{fmt} выгрузка ПЗ, fmt = geojson | kml | plan | waypoints 200
POST /api/plans/{id}/replan пересчёт остатка работ при выбытии борта 202
POST /api/validate проверка пары «сценарий + план», в том числе чужого плана 200
GET /api/fleet справочник бортов из fleet.yaml 200
GET /api/payloads справочник нагрузок из payloads.yaml 200
GET /docs, /docs-json OpenAPI: интерфейс и спецификация 200
ID Требование Критерий приёмки Трассировка Сервис
SR-API-01 Гейтвей публикует ровно маршруты из таблицы выше плюс ops-контракт; ни один доменный процесс наружу не публикуется. Обращение к другому пути /api/... даёт 404 в форме контракта ошибок. Именование: конверт гейтвея (planId, scenarioId, status, errorCode, parentPlanId) — camelCase; тела сценария, плана и отчёта валидатора передаются как есть по схеме ../40-formats/03 в snake_case и гейтвеем не переименовываются; кодировка UTF-8, Content-Type: application/json на JSON-ручках Тест-барьер: снимок зарегистрированных маршрутов Fastify сравнивается с эталонным списком и падает при появлении незадекларированного маршрута. kubectl --context n2 -n geoscan get ingress показывает единственный backend gateway:3000. Тест: ответ GET /api/plans/{id} для s04 содержит одновременно planId и plan.metrics.makespan_s; поле plan побайтово равно тому, что вернул planner A-01; A-02; A-03 (keepCase); R-PLT-2; NFR-08; NFR-17 gateway
SR-API-02 Каждый ответ, включая ошибочный, несёт заголовок x-request-id: значение берётся из одноимённого заголовка запроса, если он есть, иначе генерируется гейтвеем; тот же идентификатор уходит в gRPC-metadata доменного вызова и в каждую строку журнала Ручной шаг: curl -i -H 'x-request-id: demo-1' -X POST https://geoscan.ff/api/plans … → в ответе тот же demo-1; kubectl --context n2 -n geoscan logs deploy/geoscan-planner \| grep demo-1 показывает записи этого расчёта; запрос без заголовка получает сгенерированный идентификатор A-04; A-05; NFR-24 gateway → planner/validator

Аутентификация пользователей отсутствует (D-01): заголовок Authorization не требуется и не проверяется, роли не вводятся. Форма A-04 сохраняется на участке «гейтвей → домен» (x-service-api-key, x-request-id), точка врезки guard'а остаётся в гейтвее — см. раздел про внутренние контракты, NFR-25, NFR-26.


2. Сценарии: приём, хранение, валидация входа

ID Требование Критерий приёмки Трассировка Сервис
SR-API-03 POST /api/scenarios принимает документ сценария по схеме ../40-formats/03 и возвращает 201 с телом {scenarioId, schema_version, name, createdAt} и заголовком Location: /api/scenarios/{scenarioId}. Сценарий сохраняется в Postgres (jsonb) и переживает перезапуск процесса Тест: POST /api/scenarios с содержимым scenarios/s04.json → 201; kubectl --context n2 -n geoscan rollout restart deploy/geoscanGET /api/scenarios/{id} отдаёт тот же документ побайтово D-03; FR-SCN-01; FR-SCN-02; NFR-19; R-IN-1…R-IN-8 gateway
SR-API-04 GET /api/scenarios отдаёт {items: [{scenarioId, name, source: "demo"\|"user", schema_version, createdAt, tasksCount, fleetSize}], total}; демо-сценарии s01–s10 присутствуют сразу после развёртывания, без ручного импорта. GET /api/scenarios/{id} отдаёт документ целиком; неизвестный идентификатор — 404 SCENARIO_NOT_FOUND Тест: на свежеразвёрнутом контуре GET /api/scenarios содержит десять записей с source: "demo" и идентификаторами s01s10; GET /api/scenarios/s08 возвращает сценарий с недостижимым GSD R-DEMO-1; FR-SCN-08; ../70-plan/02 (../70-plan/02-test-scenarios.md) gateway
SR-API-05 Тело со сценарием (в POST /api/scenarios, POST /api/plans, POST /api/validate) проверяется по JSON-схеме до постановки задачи в очередь и до вызова домена. Нарушение схемы — 400 SCHEMA_VALIDATION_FAILED с массивом errors[], у каждой записи path (JSON Pointer), message, rule; перечисляются все ошибки, а не первая. Неподдерживаемое значение schema_version — 400 SCHEMA_VERSION_UNSUPPORTED с перечнем поддерживаемых версий. Размер тела ограничен 25 МБ, превышение — 413 PAYLOAD_TOO_LARGE без вычитывания тела целиком Тест: сценарий без tasks[0].area → 400 с errors[], содержащим {"path":"/tasks/0/area","rule":"required"}; сценарий с двумя дефектами даёт две записи; schema_version: 99 → 400 SCHEMA_VERSION_UNSUPPORTED; тело 30 МБ → 413. Ни в одном из случаев расчёт не запускается: geoscan_plan_total не растёт A-06; D-02; FR-SCN-09; FR-VAL-01; NFR-20; NFR-28 gateway

Приём геофайлов (KML/KMZ, GeoJSON, SHP) как multipart/form-data каноническим перечнем маршрутов не описан — см. «Открытые вопросы», пункт 3.


3. Асинхронный расчёт плана

Модель по A-07: расчёт — задача со статусом, а не длинный синхронный вызов.

POST /api/plans
  → 202 Accepted
    Location: /api/plans/p-8f3c1a
    {"planId":"p-8f3c1a","status":"queued","scenarioId":"s04","queuePosition":1}

Статусная машина задачи (plan_jobs.status, D-02):

queued ──► running ──► done
   │           └─────► error
   └──────────────────► error      (диспетчер не смог запустить расчёт)

done и errorтерминальные: после перехода статус не меняется. Статус решателя (optimal | feasible | infeasible | timeout) живёт внутри результата, в plan.solver.status, и со статусом задачи не смешивается.

ID Требование Критерий приёмки Трассировка Сервис
SR-API-06 POST /api/plans с телом {scenarioId} либо {scenario} (встроенный документ) ставит задачу в очередь и отвечает 202 с телом {planId, status: "queued", scenarioId, queuePosition} и заголовком Location на /api/plans/{planId}; время ответа не зависит от solver.time_limit_s. Синхронный ответ — единственное исключение и только при solver.time_limit_s ≤ 5: тогда ручка отвечает 200 с телом {planId, status: "done", plan}. При time_limit_s > 5 синхронный режим недоступен ни по какому параметру запроса Тест: сценарий sc-area 200 км² с time_limit_s: 120 → ответ быстрее 1 с, status: "queued", Location совпадает с planId из тела. Тест: s01 с time_limit_s: 3 → 200 и непустой plan.missions; тот же сценарий с time_limit_s: 30 → 202 без поля plan A-07; D-02; NFR-05; NFR-04; FR-PLN-19 gateway → planner
SR-API-07 GET /api/plans/{id} отдаёт 200 и тело {planId, status, scenarioId, createdAt, startedAt?, finishedAt?, progress?, plan?, error?}: plan присутствует только при status: "done", error (в форме раздела 5) — только при status: "error". Неизвестный id — 404 PLAN_NOT_FOUND. Терминальный статус неизменен: повторные запросы к завершённой задаче дают побайтово одинаковое тело Тест: опрос ручки по ходу расчёта s04 даёт последовательность queuedrunningdone без возвратов назад; после done десять повторных запросов возвращают идентичное тело; GET /api/plans/нет-такого → 404 PLAN_NOT_FOUND A-07; D-03; NFR-19; FR-PLN-23 gateway

4. Поток прогресса (SSE)

GET /api/plans/{id}/eventsContent-Type: text/event-stream, без буферизации промежуточными узлами (Cache-Control: no-cache, X-Accel-Buffering: no). Гейтвей ретранслирует server-streaming Planner.Plan в события SSE.

event: snapshot
id: 0
data: {"planId":"p-8f3c1a","status":"running","progress":0.31,"startedAt":"2026-09-20T09:00:01Z"}

event: progress
id: 7
data: {"stage":"routing","progress":0.42,"elapsed_s":5.1,"message":"распределение вылетов"}

event: objective
id: 8
data: {"makespan_s":5460,"total_flight_time_s":13120,"solver_status":"feasible","elapsed_s":5.1}

: heartbeat

event: done
id: 12
data: {"planId":"p-8f3c1a","status":"done","solver_status":"optimal"}
ID Требование Критерий приёмки Трассировка Сервис
SR-API-08 Поток отдаёт четыре типа событий: progress (стадия расчёта и доля выполнения 0…1), objective (текущие значения обеих целевых метрик и статус решателя), done и error — терминальные (error несёт тело контракта ошибок). У каждого события монотонно растущий id, data — одна строка JSON. При отсутствии событий дольше 15 с отправляется комментарий-heartbeat (: heartbeat). Переподключение не воспроизводит историю: Last-Event-ID принимается и игнорируется, первым событием любого соединения отправляется snapshot, тело которого совпадает с ответом GET /api/plans/{id}; состояние клиент восстанавливает из него. После терминального события сервер закрывает поток; подписка на уже завершённую задачу отдаёт snapshot + терминальное событие и закрывается Тест: подписка на расчёт s04 даёт не менее одного progress, не менее одного objective и ровно одно терминальное событие, идентификаторы строго возрастают. Ручной шаг: на sc-area 200 км² с time_limit_s: 60 интервал между любыми двумя строками потока не превышает 15 с. Тест: разрыв на середине и переподключение с Last-Event-ID: 5 → первое событие snapshot с актуальным progress, повторов событий 6–7 нет. Тест: подписка на завершённую задачу возвращает два события и закрывает соединение (клиент получает EOF, а не таймаут) A-07; NFR-05; NFR-22; FR-PLN-20 gateway → planner

Разрыв потока клиентом не отменяет расчёт: задача досчитывается и завершается в done/error (отмена — «Открытые вопросы», пункт 1).


5. Контракт ошибок

Единая форма тела: {errorCode, message, details?, errors?}. message — по-русски (NFR-29), errorCode — латиницей в SCREAMING_SNAKE_CASE. Перевод кодов gRPC в HTTP — одной таблицей A-06 в единственной точке кода (exception filter гейтвея).

gRPC HTTP Когда
INVALID_ARGUMENT 400 сценарий не проходит схему или семантическую проверку входа
NOT_FOUND 404 нет сценария, плана или борта с таким идентификатором
FAILED_PRECONDITION 422 нет борта под требуемый GSD, тип съёмки без совместимой нагрузки, площадка вне разрешённого ВП
DEADLINE_EXCEEDED 504 синхронный вызов домена не уложился в дедлайн гейтвея
RESOURCE_EXHAUSTED 429 очередь расчётов заполнена
UNAVAILABLE 503 доменный процесс не поднят или не готов

Примеры тел — 400, 422, 429:

// 400
{ "errorCode": "SCHEMA_VALIDATION_FAILED",
  "message": "Сценарий не соответствует схеме",
  "errors": [ { "path": "/tasks/0/area", "rule": "required", "message": "Область съёмки обязательна" },
              { "path": "/weather/wind_speed_ms", "rule": "type", "message": "Ожидается число" } ] }

// 422, s08
{ "errorCode": "GSD_UNREACHABLE",
  "message": "Требуемое качество съёмки недостижимо ни одним бортом парка",
  "details": { "task_id": "t1", "required_gsd_cm": 0.5,
    "uavs": [ { "uav_id": "201-01", "model": "geoscan_201", "payload": "…",
                "best_gsd_cm": 1.96, "at_altitude_m": 100,
                "limited_by": "altitude_agl_min" } ] } }

// 429
{ "errorCode": "QUEUE_FULL",
  "message": "Очередь расчётов заполнена, повторите попытку позже",
  "details": { "running": 2, "queued": 20, "limitRunning": 2, "limitQueued": 20 } }
ID Требование Критерий приёмки Трассировка Сервис
SR-API-09 Любой ответ с кодом ≥ 400 имеет тело {errorCode, message, details?, errors?}; перевод кода gRPC в HTTP выполняется таблицей A-06 в одном месте кода. Доменная ситуация (нерешаемость, недостижимый GSD, несовместимая нагрузка, переполненная очередь, недоступный домен) никогда не отдаётся как 500: 500 остаётся только за непойманным дефектом Тест-барьер: поиск по репозиторию не находит второй таблицы соответствия кодов; параметризованный тест прогоняет шесть кодов gRPC и сверяет HTTP-статус и форму тела. Прогон s02, s05, s07, s08, s09 через API не даёт ни одного 500 A-06; NFR-20; NFR-22 gateway
SR-API-10 Семантическая невыполнимость входа даёт 422 с машиночитаемым details (без errors[], которое зарезервировано за 400): для недостижимого GSD — требуемое значение и по каждому борту парка модель, установленная нагрузка, достижимое значение и высота, на которой оно достигается; для типа съёмки без совместимой нагрузки — перечень поддерживаемых типов Тест s08: POST /api/plans со сценарием GSD 0,5 см → 422 GSD_UNREACHABLE, details.required_gsd_cm = 0.5, details.uavs[] содержит записи по geoscan_201, geoscan_801, geoscan_gemini. Тест: задание с survey_type: "lidar" → 422 PAYLOAD_INCOMPATIBLE с details.supported_survey_types, поля errors[] нет A-06; FR-VAL-01; FR-VAL-02; FR-TSK-07; BR-22; BR-23; s08; US-05 gateway → planner
SR-API-11 Переполнение очереди даёт 429 QUEUE_FULL с заголовком Retry-After в секундах и details: {running, queued, limitRunning: 2, limitQueued: 20}. Порог — не более 2 одновременно исполняемых расчётов и не более 20 ожидающих Тест: 2 «долгих» расчёта плюс 20 поставленных в очередь; 23-й POST /api/plans → 429 QUEUE_FULL, Retry-After присутствует, details.queued = 20; после завершения одного расчёта следующий POST снова принимается D-02; A-06; NFR-02 gateway
SR-API-12 504 UPSTREAM_TIMEOUT возникает только в синхронных режимах — POST /api/plans при time_limit_s ≤ 5, POST /api/validate, GET …/export/{fmt} — когда доменный вызов не уложился в дедлайн гейтвея. В асинхронном расчёте исчерпание time_limit_s не является ошибкой: задача завершается как status: "done" с plan.solver.status: "timeout" и метриками лучшего найденного решения; пустой план при status: "done" запрещён Тест: с искусственно замедленным planner'ом POST /api/plans с time_limit_s: 3 → 504 UPSTREAM_TIMEOUT. Тест: sc-area 200 км² с time_limit_s: 10 в асинхронном режиме → status: "done", plan.solver.status: "timeout", plan.solver.solve_time_s не превышает 10 с плюс накладные, missions непусты, geoscan_plan_total{status="timeout"} вырос на 1 A-06; A-07; NFR-04; FR-PLN-23; A-12 gateway → planner
SR-API-13 Недоступность доменного процесса даёт 503 UPSTREAM_UNAVAILABLE с details.service (planner | validator | airspace), чтобы интерфейс показал «сервис расчёта недоступен», а не пустой план. Асинхронная задача, которую диспетчер не смог передать planner'у за 3 попытки, переходит в терминальный error с тем же телом в поле error Ручной шаг: kubectl --context n2 -n geoscan scale deploy/geoscan-planner --replicas=0POST /api/validate отдаёт 503 UPSTREAM_UNAVAILABLE с details.service = "planner"; поставленный ранее расчёт после 3 попыток виден в GET /api/plans/{id} как status: "error" с тем же errorCode A-06; NFR-22; D-02 gateway

6. Идемпотентность мутаций

ID Требование Критерий приёмки Трассировка Сервис
SR-API-14 POST /api/plans и POST /api/plans/{id}/replan принимают заголовок Idempotency-Key. Первый запрос занимает пару {scope, key} (scope — путь ручки) и сохраняет полный ответ (код, тело, Location); повтор с тем же ключом и тем же телом отдаёт сохранённый ответ байт-в-байт плюс заголовок Idempotency-Replayed: true и не запускает второй расчёт. Тот же ключ с другим телом — 409 IDEMPOTENCY_KEY_CONFLICT. Повтор, пришедший, пока первый запрос ещё исполняется, отдаёт тот же 202 с тем же planId. Запрос без заголовка трактуется как новый расчёт Тест: два одинаковых POST /api/plans с Idempotency-Key: k1 → одинаковые planId и тела, geoscan_plan_total вырос на 1, а не на 2; третий запрос с k1 и изменённым objective.criterion → 409 IDEMPOTENCY_KEY_CONFLICT; два параллельных запроса с k2 дают один planId A-09; NFR-21 gateway

7. Экспорт полётных заданий

GET /api/plans/{id}/export/{fmt}. Гейтвей вызывает Planner.ExportPlan (D-04) и транслирует файл клиенту; файлы не хранятся (D-03).

fmt Content-Type Расширение
geojson application/geo+json .geojson
kml application/vnd.google-earth.kml+xml .kml
plan application/json .plan
waypoints text/plain; charset=utf-8 .waypoints
любой, без ?uav= application/zip .zip, внутри — файл на борт
ID Требование Критерий приёмки Трассировка Сервис
SR-API-15 Ручка поддерживает четыре формата и параметр ?uav={uav_id}. С параметром отдаётся один файл по борту с Content-Type из таблицы и Content-Disposition: attachment; filename="{planId}_{uav_id}.{ext}". Без параметра отдаётся application/zip с отдельным файлом на каждый борт плана — «индивидуальное ПЗ для каждого БВС». Неизвестный fmt — 400 EXPORT_FORMAT_UNSUPPORTED с перечнем поддерживаемых; борт, которого нет в плане, — 404 UAV_NOT_IN_PLAN Тест s04 (три борта): …/export/kml?uav=201-01 → 200, Content-Type: application/vnd.google-earth.kml+xml, имя файла содержит идентификатор борта, содержимое проходит проверку по XSD OGC KML 2.2; …/export/kml без параметра → zip из трёх файлов; …/export/geojson?uav=201-01 проходит проверку по RFC 7946; …/export/dxf → 400; …/export/kml?uav=нет-такого → 404 R-OUT-1; R-OUT-2; R-OUT-3; R-DEMO-5; FR-EXP-01; FR-EXP-02; FR-EXP-03; FR-EXP-14; D-04 gateway → planner
SR-API-16 Экспорт доступен только для задачи в терминальном статусе done: при queued/running — 409 PLAN_NOT_READY с details.status, при error — 409 PLAN_FAILED, при неизвестном id — 404 PLAN_NOT_FOUND. План, не допущенный валидатором, выгрузить можно, но ответ помечается заголовком X-Plan-Admitted: false, и признак недопущенного плана попадает в сам файл Тест: запрос экспорта сразу после POST /api/plans → 409 PLAN_NOT_READY; после перехода в done тот же запрос → 200. Тест: план с violations.nfz > 0 выгружается с X-Plan-Admitted: false, признак виден в содержимом файла FR-VAL-12; BR-01; A-07; D-03 gateway

8. Перепланирование при выбытии борта (s10)

ID Требование Критерий приёмки Трассировка Сервис
SR-API-17 POST /api/plans/{id}/replan принимает состояние выполнения как данные: {at_time, failed_uav_ids[], completed_transect_ids[], uav_state: [{uav_id, position, energy_left_frac}]} и ставит новую задачу расчёта: 202, тело {planId, parentPlanId, status: "queued"}, заголовок Location на новый план. Родительский план не изменяется и остаётся доступен по прежнему идентификатору. Родитель не в статусе done — 409 PLAN_NOT_READY; неизвестный борт в failed_uav_ids — 400 UAV_NOT_IN_PLAN Тест s10: прогнать s04, затем POST /api/plans/{id}/replan с выбытием одного борта в момент t → 202, parentPlanId равен исходному; по завершении новый план не содержит ни одного галса из completed_transect_ids, а GET /api/plans/{parent} отдаёт исходный план без изменений R-X-7; FR-MON-01; FR-MON-02; FR-MON-06; BR-27; s10; US-07 gateway → planner

9. Публичный валидатор и справочники

ID Требование Критерий приёмки Трассировка Сервис
SR-API-18 POST /api/validate принимает {scenario \| scenarioId, plan} и синхронно возвращает 200 с отчётом валидатора (метрики, violations, warnings, unassigned, признак допуска). Ручка работает над любым планом в схеме ../40-formats/03, в том числе посчитанным не нашим решателем; наличие плана в нашей базе не требуется Тест: отчёт по плану наивного бейзлайна, поданному файлом, содержит заполненные metrics и счётчик пройденных проверок. Тест: план с галсом внутри бесполётной зоны даёт violations.nfz > 0 и признак «не допущен», HTTP-статус при этом 200 — нарушение это результат, а не ошибка API FR-VAL-17; FR-VAL-12; US-09; A-01; A-15 gateway → validator
SR-API-19 GET /api/fleet и GET /api/payloads отдают справочники, собранные из ../10-hardware/fleet.yaml и ../10-hardware/payloads.yaml на этапе сборки образа: три планируемых борта (geoscan_201, geoscan_801, geoscan_gemini) и их нагрузки. Модели с признаком reference_only в ответ по умолчанию не попадают и отдаются только при ?include_reference=true с полем reference_only: true. Ответы кешируемые (ETag, Cache-Control: max-age) и не требуют обращений в сеть Тест: GET /api/fleet возвращает ровно три модели, имена полей совпадают с fleet.yaml: geoscan_201endurance_max: 180, wind_max: 12, altitude_agl_min: 100, can_fly_below_launch_point: false; geoscan_801 и geoscan_geminiwind_max: 10, endurance_max: 40; geoscan_geminialtitude_agl_max: 500, battery.energy_wh: 144.7, battery.charge_time_min: 105. ?include_reference=true добавляет модели с reference_only: true. Повторный запрос с If-None-Match → 304 R-IN-2; R-VEH-1; FR-FLT-01; FR-FLT-09; NFR-11 gateway

10. Документация API

ID Требование Критерий приёмки Трассировка Сервис
SR-API-20 OpenAPI-спецификация генерируется из кода гейтвея (декораторы DTO и контроллеров), публикуется на /docs (интерфейс) и /docs-json (спецификация) и покрывает все маршруты раздела 1: методы, коды ответов, схемы тел, заголовки Idempotency-Key и x-request-id, параметр ?uav=. Руками спецификация не правится Тест-барьер: число операций в /docs-json совпадает с числом зарегистрированных маршрутов /api/* (SR-API-01) и падает при появлении незадокументированного маршрута; у каждой операции описан хотя бы один ответ с кодом ≥ 400. Ручной шаг: https://geoscan.ff/docs открывается в офлайн-контуре и позволяет выполнить GET /api/fleet из браузера R-DOC-4; A-01; NFR-11; NFR-34 gateway

11. Покрытие

Источник Закрыто требованиями
A-01, A-02 (единая точка входа) SR-API-01, SR-API-20
A-04, A-05 (метаданные, сквозной контекст) SR-API-02
A-06 (контракт ошибок) SR-API-09…SR-API-13
A-07 (асинхронный расчёт) SR-API-06, SR-API-07, SR-API-08, SR-API-12
A-09 (идемпотентность) SR-API-14
R-OUT-1…R-OUT-3, R-DEMO-5 SR-API-15, SR-API-16
R-DEMO-1 (демо-сценарии) SR-API-04
R-DOC-4 (описание API) SR-API-20
R-X-7 (отказ борта) SR-API-17
R-IN-2, R-VEH-1 (парк на входе) SR-API-19
D-01…D-04 SR-API-02 (D-01), SR-API-11 (D-02), SR-API-03, SR-API-16 (D-03), SR-API-15 (D-04)

12. Соответствие реализации

Живой снимок расхождений gateway с этой спецификацией (без подмены нормы фактом) — 03-public-api-implementation.md. Блокеры сдачи, влияющие на API, — ../96-acceptance/01-blockers.md.


Открытые вопросы

  1. Отмена расчёта. NFR-05 требует доступной во время расчёта отмены, но в каноническом перечне маршрутов ручки отмены нет, а разрыв SSE-потока расчёт не останавливает (SR-API-08). Варианты: (а) DELETE /api/plans/{id} — перевод задачи в error с errorCode: CANCELLED и отмена gRPC-стрима; (б) POST /api/plans/{id}/cancel с тем же эффектом; (в) отмена только на клиенте — расчёт досчитывается в фоне, интерфейс перестаёт его показывать. До выбора варианта NFR-05 в части отмены не закрыт ни одним требованием этого раздела.
  2. Список планов. NFR-19 требует, чтобы после перезапуска были доступны «ранее посчитанные планы», а в каноне есть только GET /api/plans/{id} — ручки списка нет. Варианты: (а) добавить GET /api/plans?scenarioId=… с пагинацией; (б) отдавать планы отдельным полем в GET /api/scenarios/{id}; (в) считать реестр планов делом интерфейса — вариант ломает NFR-19 при смене браузера. Решение затрагивает раздел про гейтвей и хранение.
  3. Импорт геофайлов. FR-SCN-04 и FR-EXP-17 требуют приёма KML/KMZ, GeoJSON и SHP файлом с пересчётом CRS, а канон описывает POST /api/scenarios как JSON-ручку. Варианты: (а) отдельная ручка POST /api/scenarios/import с multipart/form-data (расширяет канонический перечень); (б) POST /api/scenarios принимает и application/json, и multipart/form-data; (в) разбор файла целиком на клиенте, на бэкенд приходит готовый JSON-сценарий — сегодня фронтенд файл принимает и игнорирует (../../../src/frontend/README.md, «Известные ограничения»).
  4. Форма отчёта POST /api/validate. Схема в ../40-formats/03 описывает metrics, violations, warnings, unassigned внутри плана, но не отдельный отчёт с разделами и счётчиками, которого требует FR-VAL-15; источники расходятся и в числе проверок (восемь жёстких в ../70-plan/01, 24 в шести группах во фронтенде на моках, 48 в макете _6). До решения тело ответа SR-API-18 описано составом полей схемы плана; окончательную форму задаёт раздел про валидатор.
  5. Экспорт плана целиком без ?uav=. Канон описывает только «?uav= для одного борта», а FR-EXP-01 требует отдельного файла на каждый борт. В SR-API-15 принят zip-архив; альтернативы — сделать параметр обязательным (400 без него) либо отдавать один файл со всеми бортами, что противоречит R-OUT-1. Решение затрагивает раздел про экспорт и форматы.