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/geoscan → GET /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" и идентификаторами s01…s10; 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 даёт последовательность queued → running → done без возвратов назад; после done десять повторных запросов возвращают идентичное тело; GET /api/plans/нет-такого → 404 PLAN_NOT_FOUND |
A-07; D-03; NFR-19; FR-PLN-23 | gateway |
4. Поток прогресса (SSE)¶
GET /api/plans/{id}/events — Content-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=0 → POST /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_201 — endurance_max: 180, wind_max: 12, altitude_agl_min: 100, can_fly_below_launch_point: false; geoscan_801 и geoscan_gemini — wind_max: 10, endurance_max: 40; geoscan_gemini — altitude_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.
Открытые вопросы¶
- Отмена расчёта.
NFR-05требует доступной во время расчёта отмены, но в каноническом перечне маршрутов ручки отмены нет, а разрыв SSE-потока расчёт не останавливает (SR-API-08). Варианты: (а)DELETE /api/plans/{id}— перевод задачи вerrorсerrorCode: CANCELLEDи отмена gRPC-стрима; (б)POST /api/plans/{id}/cancelс тем же эффектом; (в) отмена только на клиенте — расчёт досчитывается в фоне, интерфейс перестаёт его показывать. До выбора вариантаNFR-05в части отмены не закрыт ни одним требованием этого раздела. - Список планов.
NFR-19требует, чтобы после перезапуска были доступны «ранее посчитанные планы», а в каноне есть толькоGET /api/plans/{id}— ручки списка нет. Варианты: (а) добавитьGET /api/plans?scenarioId=…с пагинацией; (б) отдавать планы отдельным полем вGET /api/scenarios/{id}; (в) считать реестр планов делом интерфейса — вариант ломаетNFR-19при смене браузера. Решение затрагивает раздел про гейтвей и хранение. - Импорт геофайлов.
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, «Известные ограничения»). - Форма отчёта
POST /api/validate. Схема в ../40-formats/03 описываетmetrics,violations,warnings,unassignedвнутри плана, но не отдельный отчёт с разделами и счётчиками, которого требуетFR-VAL-15; источники расходятся и в числе проверок (восемь жёстких в ../70-plan/01, 24 в шести группах во фронтенде на моках, 48 в макете_6). До решения тело ответа SR-API-18 описано составом полей схемы плана; окончательную форму задаёт раздел про валидатор. - Экспорт плана целиком без
?uav=. Канон описывает только «?uav=для одного борта», аFR-EXP-01требует отдельного файла на каждый борт. В SR-API-15 принят zip-архив; альтернативы — сделать параметр обязательным (400 без него) либо отдавать один файл со всеми бортами, что противоречитR-OUT-1. Решение затрагивает раздел про экспорт и форматы.