From d5268b93952b705bc4575834015dfe59919a8a79 Mon Sep 17 00:00:00 2001 From: "Zimin A.N." Date: Thu, 7 May 2026 23:19:02 +0300 Subject: [PATCH] =?UTF-8?q?docs:=20Diplodoc=20setup=20+=20=D0=BF=D0=BE?= =?UTF-8?q?=D0=BB=D0=BD=D0=BE=D0=B5=20=D1=80=D1=83=D0=BA=D0=BE=D0=B2=D0=BE?= =?UTF-8?q?=D0=B4=D1=81=D1=82=D0=B2=D0=BE=20=D0=B8=D0=BD=D1=82=D0=B5=D0=B3?= =?UTF-8?q?=D1=80=D0=B0=D1=86=D0=B8=D0=B8=20=D0=BF=D0=BE=20API?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Новая документация-as-code на платформе Diplodoc (Yandex open-source) с YFM (Yandex Flavored Markdown). Build → static HTML, deployable куда угодно. Что добавлено: - package.json + .yfm + toc.yaml — Diplodoc setup, навигация для sidebar - index.md — главная страница с tabs для разных audiences - README.md — инструкция для contributors Раздел integration/api/ — 11 страниц про интеграцию consumers: - index — обзор + глоссарий - auth — Keycloak service accounts, scope mapping - read-endpoints — GET /dictionaries, list/by-key, spatial filters - events — Kafka outbox топики, partition keys, idempotency, DLQ - webhooks — HMAC, SSRF guard, retry policy, test ping endpoint - bitemporal — validFrom/validTo + history, future-dated entries, bulk close/export - x-references — FK syntax, validation errors, scope hiding, orphan scanner, v2 cascade preview - caching — TTL / push / snapshot strategies, hybrid pattern - errors — full error code reference (validation/auth/conflict/server), retry strategy - best-practices — 33 numbered rules для consumers - bundle-cuod — все 40 справочников с FK графом и сценариями Обновлены: - ops/README.md — link на новую integration/api/, убран inline
- ops/slo.md — broken cross-repo links заменены на text references Build: cd docs && pnpm install && pnpm build → ./_dist (20 HTML files, 7.1M) pnpm serve # → http://localhost:8088 CI deploy job для GitLab Pages — в roadmap (см. docs/README.md). Существующий tech/api-integration.md оставлен в toc как legacy (содержимое мигрировано + расширено в integration/api/). --- docs/.gitignore | 4 + docs/.yfm | 12 + docs/README.md | 109 ++ docs/index.md | 62 + docs/integration/api/auth.md | 112 ++ docs/integration/api/best-practices.md | 156 +++ docs/integration/api/bitemporal.md | 185 +++ docs/integration/api/bundle-cuod.md | 138 +++ docs/integration/api/caching.md | 203 ++++ docs/integration/api/errors.md | 169 +++ docs/integration/api/events.md | 189 ++++ docs/integration/api/index.md | 54 + docs/integration/api/read-endpoints.md | 191 ++++ docs/integration/api/webhooks.md | 216 ++++ docs/integration/api/x-references.md | 142 +++ docs/ops/README.md | 6 +- docs/ops/slo.md | 4 +- docs/package.json | 18 + docs/pnpm-lock.yaml | 1429 ++++++++++++++++++++++++ docs/toc.yaml | 56 + 20 files changed, 3450 insertions(+), 5 deletions(-) create mode 100644 docs/.gitignore create mode 100644 docs/.yfm create mode 100644 docs/README.md create mode 100644 docs/index.md create mode 100644 docs/integration/api/auth.md create mode 100644 docs/integration/api/best-practices.md create mode 100644 docs/integration/api/bitemporal.md create mode 100644 docs/integration/api/bundle-cuod.md create mode 100644 docs/integration/api/caching.md create mode 100644 docs/integration/api/errors.md create mode 100644 docs/integration/api/events.md create mode 100644 docs/integration/api/index.md create mode 100644 docs/integration/api/read-endpoints.md create mode 100644 docs/integration/api/webhooks.md create mode 100644 docs/integration/api/x-references.md create mode 100644 docs/package.json create mode 100644 docs/pnpm-lock.yaml create mode 100644 docs/toc.yaml diff --git a/docs/.gitignore b/docs/.gitignore new file mode 100644 index 0000000..69b2344 --- /dev/null +++ b/docs/.gitignore @@ -0,0 +1,4 @@ +node_modules/ +_dist/ +.DS_Store +*.log diff --git a/docs/.yfm b/docs/.yfm new file mode 100644 index 0000000..49c48c8 --- /dev/null +++ b/docs/.yfm @@ -0,0 +1,12 @@ +langs: + - ru +lang: ru +varsPreset: external +allowHTML: false +needToSanitizeHtml: true +strict: false +ignore: + - node_modules + - _dist + - status/*-state.md +addSystemMeta: true diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 0000000..c50b1cd --- /dev/null +++ b/docs/README.md @@ -0,0 +1,109 @@ +# Ordinis docs (Diplodoc / YFM) + +Документация Ordinis НСИ ЦУОД ОДХ построена через **Diplodoc** (open-source +documentation platform от Yandex). Markdown source — YFM (Yandex Flavored +Markdown) с расширениями для tabs, notes, includes, mermaid диаграмм. + +## Структура + +``` +docs/ +├── package.json # @diplodoc/cli + scripts +├── .yfm # Diplodoc config (langs, plugins) +├── toc.yaml # Содержание / навигация (рендерится в sidebar) +├── index.md # Главная страница +├── integration/ # Интеграция по API +│ ├── api/ # Главы для consumers +│ │ ├── index.md # Обзор +│ │ ├── auth.md # Keycloak / JWT +│ │ ├── read-endpoints.md +│ │ ├── events.md # Kafka outbox +│ │ ├── webhooks.md +│ │ ├── bitemporal.md +│ │ ├── x-references.md +│ │ ├── caching.md +│ │ ├── errors.md +│ │ ├── best-practices.md +│ │ └── bundle-cuod.md +│ └── altum-imaging-order.md # Прикладной сценарий +├── ops/ # Operator runbook +│ ├── README.md +│ ├── slo.md +│ ├── outbox-dlq.md +│ ├── db-migrations.md +│ ├── vault-unseal.md +│ └── kafka-restart.md +├── status/ # State snapshots (исключено из build) +│ └── *-state.md +└── tech/ # Legacy технические страницы + └── api-integration.md +``` + +## Сборка + +Требуется Node 18+. + +```bash +cd docs +pnpm install +pnpm build # → ./_dist (HTML) +pnpm serve # → http://localhost:8088 +# либо одной командой: +pnpm dev +``` + +Build output (`_dist/`) — статический HTML, можно деплоить в: +- GitLab Pages +- S3 + CloudFront +- nginx/ingress на cluster.142 + +## Автор содержимого + +| Раздел | Owner | +|---|---| +| `/` (index, integration/api/*) | zimin.an@nstart.space | +| `ops/*` | on-call rotation team | +| `integration/altum-imaging-order.md` | Альтум team contact | + +## Что не committable + +- `_dist/` — build output (gitignore'нут) +- `node_modules/` — dependencies (gitignore'нут) + +## Deployment + +В roadmap — GitLab CI job `build-docs`: + +```yaml +build-docs: + stage: build + image: node:18 + script: + - cd docs + - pnpm install --frozen-lockfile + - pnpm build + artifacts: + paths: [docs/_dist] + expire_in: 1 week + rules: + - changes: ["docs/**/*"] +``` + +Затем deploy в GitLab Pages либо как отдельный k8s deployment с nginx. + +## YFM extensions используются + +- `{% list tabs %}` — табы для switching между audiences +- `{% note info|warning|tip %}` — выделенные блоки +- `mermaid` code blocks — диаграммы (graph LR, gantt) +- Cross-links в `toc.yaml` — sidebar navigation +- `varsPreset: external` — для shared includes (TBD when нужно) + +## Roadmap + +- [ ] CI job `build-docs` в `.gitlab-ci.yml` +- [ ] GitLab Pages deployment +- [ ] Search через Diplodoc search plugin +- [ ] EN translation (i18n via `langs:` в `.yfm`) +- [ ] Includes для shared snippets (auth examples, error tables) +- [ ] Versioning — separate doc trees per Ordinis major version diff --git a/docs/index.md b/docs/index.md new file mode 100644 index 0000000..b0d3d88 --- /dev/null +++ b/docs/index.md @@ -0,0 +1,62 @@ +# Ordinis НСИ ЦУОД ОДХ + +Документация для разработчиков, интеграторов и операторов. + +**Anchor v1 — production LIVE на cluster.142** (с 2026-05-07). +Альтум consumers подключены, blue/green cutover завершён. + +## Что внутри + +{% list tabs %} + +- Я интегрирую свой сервис + + Начни с [обзора API](integration/api/index.md) и [авторизации](integration/api/auth.md). + + Дальше: + - [Read-side endpoints](integration/api/read-endpoints.md) — как читать справочники + - [События (Kafka outbox)](integration/api/events.md) — топики `ordinis.cuod.events.*` + - [Webhooks](integration/api/webhooks.md) — push-уведомления вместо polling + - [Кэширование](integration/api/caching.md) — TTL / push / snapshot стратегии + +- Я строю UI / клиент + + - [Cross-references](integration/api/x-references.md) — FK между справочниками + - [Bitemporal](integration/api/bitemporal.md) — `validFrom` / `validTo` + история + - [Bundle cuod](integration/api/bundle-cuod.md) — список 40 справочников ЦУОД + - [Best practices](integration/api/best-practices.md) — частые ошибки + +- Я — оператор / on-call + + - [Runbook indexed by symptom](ops/README.md) + - [SLO + alert thresholds](ops/slo.md) + - [Outbox DLQ](ops/outbox-dlq.md) + - [Database migrations](ops/db-migrations.md) + - [Vault unseal](ops/vault-unseal.md) + - [Kafka restart](ops/kafka-restart.md) + +{% endlist %} + +## Среды + +| Среда | URL | Auth | Назначение | +|---|---|---|---| +| **prod** | `https://ordinis.k8s.264.nstart.cloud/` | JWT обязателен | Альтум real users | +| staging | `https://ordinis.k8s.265.nstart.cloud/` | optional (legacy mode) | Dev environment | +| local | `http://localhost:8080` | off | docker-compose dev | + +## Поддержка + +- Issues / bug reports: `git.nstart.cloud:2-6/2-6-4/terravault/ordinis` +- Эскалация: `zimin.an@nstart.space` +- Запросы новых ролей / scope в Keycloak: тот же email. + +## Версионирование + +API стабилизирован под v1 — breaking changes только через major bump (`v2`). См. +[Best practices](integration/api/best-practices.md) для рекомендаций по +обработке появления новых полей в v1 (forward compatibility). + +Bundle справочников ЦУОД versioned отдельно (`cuod-bundle:1.2.1`). Изменение +schemas — minor bump, добавление словарей — minor bump, удаление — major bump +(никогда без уведомления consumers через `nsi.dictionary.changed` event). diff --git a/docs/integration/api/auth.md b/docs/integration/api/auth.md new file mode 100644 index 0000000..5e932c4 --- /dev/null +++ b/docs/integration/api/auth.md @@ -0,0 +1,112 @@ +# Авторизация + +Ordinis принимает JWT от Keycloak realm `nstart`: + +- **Issuer:** `https://auth.nstart.space/realms/nstart` +- **Алгоритм:** RS256 +- **JWK Set discoverится** через `/.well-known/openid-configuration` + +## Service account (m2m) + +Для backend-to-backend интеграции (типичный сценарий): + +1. Запросить у Ordinis team создание клиента в realm `nstart`. +2. Назначить role: `ordinis-public` / `ordinis-internal` / `ordinis-restricted` + (по нужному scope, см. ниже). +3. Получить `client_id` + `client_secret`. Хранить в Vault либо secret manager, + **никогда** в коде. + +## Получение access token + +```bash +curl -X POST https://auth.nstart.space/realms/nstart/protocol/openid-connect/token \ + -H "Content-Type: application/x-www-form-urlencoded" \ + -d "grant_type=client_credentials" \ + -d "client_id=altum-backend" \ + -d "client_secret=" +``` + +Ответ: + +```json +{ + "access_token": "eyJhbGciOi...", + "expires_in": 300, + "token_type": "Bearer" +} +``` + +{% note warning %} + +Кэшируй токен с TTL = `expires_in - 30` (буфер на retries) в backend +memory. Не делай новый token request на каждый API call — Keycloak rate +limits и латентность. + +{% endnote %} + +## Использование + +Каждый запрос требует header: + +``` +Authorization: Bearer eyJhbGciOi... +``` + +Без header → `401 Unauthorized`. +С невалидным/expired токеном → `401`. +Token валиден но scope не позволяет доступ к словарю → `403 Forbidden`. + +## Маппинг ролей в DataScope + +Ordinis использует трёхуровневый доступ к словарям через `scope` field в +`DictionaryDefinition`: + +| Keycloak роль | Доступные scopes | +|---|---| +| `ordinis-public` | `PUBLIC` | +| `ordinis-internal` | `PUBLIC` + `INTERNAL` | +| `ordinis-restricted` | `PUBLIC` + `INTERNAL` + `RESTRICTED` | + +Альтернативный синтаксис ролей (для compat с другими сервисами в realm): +суффикс `-PUBLIC|INTERNAL|RESTRICTED` (например `altum-PUBLIC`, +`geoportal-INTERNAL`). Маппинг определяется в `ordinis-auth/.../ScopeContext.java`. + +{% note info %} + +Альтум consumer на проде использует role `ordinis-internal` — доступ к +PUBLIC + INTERNAL справочникам. RESTRICTED закрытый только для специально +authorized clients. + +{% endnote %} + +## Refresh tokens + +`client_credentials` flow **не поддерживает refresh tokens** — это +m2m flow без user session. Получай новый access token при истечении. + +Если используешь user-on-behalf flow (rare для backend) — refresh token +работает, см. Keycloak документацию. + +## Multiple environments + +| Среда | Issuer | Realm | +|---|---|---| +| prod | `https://auth.nstart.space/realms/nstart` | `nstart` | +| staging | same | same | +| local | same либо local Keycloak (опц) | same | + +Один Keycloak realm для всех сред. Service accounts shared (но `client_id` +обычно различается per env: `altum-backend-prod`, `altum-backend-staging`). + +## Troubleshooting + +| Симптом | Причина | Fix | +|---|---|---| +| 401 на каждый запрос | Token expired | Refresh с TTL buffer | +| 401 на новый token | Invalid client_secret | Re-fetch у Ordinis team | +| 403 на конкретные dictionaries | Scope mismatch | Запросить upgrade role у Ordinis team | +| 500 от Ordinis | Issuer unreachable / JWK fetch fail | Check `auth.nstart.space` health | + +## Дальше + +→ [Read-side endpoints](read-endpoints.md) — что можно читать с этим токеном. diff --git a/docs/integration/api/best-practices.md b/docs/integration/api/best-practices.md new file mode 100644 index 0000000..7e96bc6 --- /dev/null +++ b/docs/integration/api/best-practices.md @@ -0,0 +1,156 @@ +# Best practices + +Чек-лист для consumer'ов Ordinis API. + +## Идентификация записей + +1. **Используй `businessKey`, не UUID.** UUID — внутренний идентификатор + записи и **меняется** при создании новой версии (через update). + `businessKey` стабилен через всю жизнь записи. + +2. **Не делай FK на копию справочника.** Если копируешь данные к себе для + snapshot — храни `business_key VARCHAR` как key, не `id UUID` либо + `internal_id`. Это устойчиво к updates. + +## Авторизация + +3. **Кэшируй access token с TTL = `expires_in - 30s`.** Не делай новый + token request на каждый API call — Keycloak rate limits + латентность. + +4. **Refresh JWT перед TTL.** Не дожидайся 401 — большинство Keycloak SDK + рефрешат автоматически. + +5. **Не храни `client_secret` в коде / git.** Vault либо secret manager. + +## Чтение + +6. **Кэшируй per `(dictionary, lang)`.** Inline в коде — `staleTime` / + TTL. См. [Caching](caching.md). + +7. **Используй `?lang=` параметр** для flatten i18n полей если UI на одном + языке. Без `lang` ответ возвращает `{ru-RU: "...", en-US: "..."}` + объект — лишний размер payload + проще баг. + +8. **Не запрашивай history endpoint** на hot path. История нужна только + admin UI / audit. Список + by-key endpoints достаточны для типичного + consumer. + +## Запись (admin only) + +9. **Не пиши массовых update'ов** через одиночные API calls. Ordinis + оптимизирован под редкие записи и частые чтения. Для bulk import + используй `POST /admin/bundle/import` (через bundle Maven module). + +10. **Bulk close через `_bulk-close`** endpoint, не loop одиночных POST. + Per-record transactions, classification (skipped vs error vs closed), + меньше latency. + +11. **Bulk export CSV** через `_export?format=csv` для extracting filtered + данные в legacy системы. + +## Обработка событий + +12. **Idempotency** — события at-least-once, могут прийти дважды. Используй + `(dictionary, businessKey, txTime, version)` либо `deliveryId` (для webhooks) + как dedup key. + +13. **Async обработка** — отвечай webhook'у `202 Accepted` сразу, обрабатывай + в background. Жёсткий timeout 10s. + +14. **HMAC verify первым** — до парсинга JSON. Защита от spoofed events. + +15. **Stale cache на 5xx / timeout** — отдавай старые данные с warn'ом, не + падай каскадно. + +## Производительность + +16. **Parallel-fetch related dictionaries** при cascading select: + + ```ts + const [spacecrafts, satTypes, instruments] = await Promise.all([ + fetch('/api/v1/spacecraft/records?lang=ru'), + fetch('/api/v1/satellite_type/records?lang=ru'), + fetch('/api/v1/instrument/records?lang=ru'), + ]) + ``` + + Не последовательные `await`, не N+1 loops. + +17. **Cascading filters в memory** — после loading dictionaries фильтруй + клиент-side вместо новых API calls: + + ```ts + const allowedFormats = formats.filter(f => + instrument.data.supportedFormatCodes.includes(f.businessKey)) + ``` + +18. **Не делай polling в tight loop** — используй webhooks либо Kafka events + для realtime invalidation. Polling каждые 5 мин — OK, каждые 10 сек — нет. + +## Error handling + +19. **Don't retry 4xx (кроме 401 и 429).** Permanent errors требуют code + fix, не retry. + +20. **Exponential backoff на 5xx / timeout.** Base 0.5s, multiplier 2, + jitter 0-0.5s, max 5 attempts. См. [Errors](errors.md). + +21. **Сохраняй `traceId`** при эскалации в Ordinis team. Без traceId + investigation в Tempo занимает в 10x дольше. + +## Bundle versioning + +22. **Forward compatibility** — допускай новые поля в API responses которые + ты не знаешь. Не fail on unknown fields. Schema validation у consumer'а + должна быть `additionalProperties: true` (либо аналог). + +23. **Подписывайся на `nsi.dictionary.changed` events** для новых + справочников — они появляются с минор-bump bundle. + +24. **Monitor schema versions** в `GET /dictionaries` response — если + `schemaVersion` поменялся, проверь твой mapping не сломался. + +## Безопасность + +25. **HTTPS обязателен** — webhook URLs тоже только https. SSRF guard + блокирует http:// и private CIDR. + +26. **Не log access tokens** в plaintext. Большинство logging frameworks + имеют redaction patterns для `Authorization` header. + +27. **Rotate `client_secret`** регулярно. Раз в квартал — minimum, раз + в месяц — recommended для production. Coordinate с Ordinis team + при rotation. + +## Коммуникация с командой + +28. **Прежде чем bulk import** > 1000 records — talk to Ordinis team. У + нас есть staging environment + bulk-import endpoint без rate limits. + +29. **Запрашивай новый scope / role** через `zimin.an@nstart.space` + либо issue с label `keycloak-role-request`. + +30. **При проблеме**: воспроизведи на staging, скопируй traceId, открой + issue. Скриншоты UI ошибок помогают. + +## Тестирование + +31. **Mock Ordinis в unit tests** — не завись от availability staging + cluster. Нашa OpenAPI spec accurate, генерируй mock client. + +32. **Integration tests против staging** — не prod. Staging данные — тестовые, + можно create/close без последствий. + +33. **Load tests на staging** в low-traffic окно. Координируй с Ordinis + team — мы можем включить дополнительные replicas для вашего теста. + +## Reference: bundle cuod + +См. [Bundle cuod](bundle-cuod.md) — список 40 справочников, их связи, +типичные operations. + +## Дальше + +- [События](events.md) — push-обновления +- [Caching](caching.md) — стратегии кэша +- [Errors](errors.md) — обработка ошибок diff --git a/docs/integration/api/bitemporal.md b/docs/integration/api/bitemporal.md new file mode 100644 index 0000000..01c082f --- /dev/null +++ b/docs/integration/api/bitemporal.md @@ -0,0 +1,185 @@ +# Bitemporal модель + +Каждая запись Ordinis имеет двухмерное время: + +- **Бизнес-время** (`validFrom` / `validTo`) — когда запись действует в реальности. +- **Транзакционное время** (Hibernate Envers `revinfo.timestamp`) — когда + изменение зафиксировано в БД. + +Это две независимые оси: можно ввести запись задним числом +(business → past, txTime → now), либо запланировать на будущее +(business → future, txTime → now), либо retroactively изменить +прошлое (исправить ошибку — txTime → now, но business range уже past). + +## Поля + +```json +{ + "businessKey": "RESURS-P-3", + "validFrom": "2025-01-01T00:00:00+03:00", + "validTo": "9999-12-31T23:59:59+03:00", + "version": 2, + "data": { ... } +} +``` + +| Поле | Тип | Описание | +|---|---|---| +| `validFrom` | timestamp tz | Когда версия начинает действовать. | +| `validTo` | timestamp tz | До какого момента (или `9999-12-31` если бессрочно). | +| `version` | int | Optimistic lock + порядковый номер версии. Monotonic per businessKey. | + +## Update создаёт **новую версию** + +Update — это не overwrite. Это: + +1. Старая запись: `validTo = now()` (закрыта). +2. Новая запись: `validFrom = now()`, `validTo = 9999...`, `version + 1`. + +Обе версии остаются в БД. История доступна через +`GET /records/{key}/history` (см. [Read endpoints](read-endpoints.md)). + +```mermaid +gantt + dateFormat YYYY-MM-DD + title Версии записи RESURS-P-3 + section v1 + v1 (status=in-orbit) :2024-06-01, 2024-12-30 + section v2 + v2 (status=active) :2024-12-30, 2026-05-07 + section v3 + v3 (name updated) :2026-05-07, 9999-12-31 +``` + +## Запросы на момент + +`?at=2025-06-15` возвращает версию, которая была активна 15 июня 2025: + +```bash +curl "https://ordinis.k8s.264.nstart.cloud/api/v1/spacecraft/records/RESURS-P-3?at=2025-06-15T00:00:00Z" +``` + +→ v2 (status=active), потому что `2024-12-30 ≤ 2025-06-15 < 2026-05-07`. + +Без `?at` — default = `now()`. + +## Future-dated entries + +Можно ввести запись в будущее: + +```json +PUT /api/v1/dictionaries/spacecraft/records/MISSION-2027 +{ + "validFrom": "2027-01-01T00:00:00+03:00", + "data": { "name": "Запланированный КА", "status": "planned" } +} +``` + +Запись существует в БД, но `GET ?at=now` её не вернёт (потому что +`now < validFrom`). Появится автоматически 1 января 2027. + +## Закрытие записи (close) + +```bash +POST /api/v1/dictionaries/spacecraft/records/RESURS-P-3/close +{ + "at": "2026-05-07T12:00:00+03:00", + "reason": "Списан с орбиты" +} +``` + +Устанавливает `validTo = at`. После этого: +- `GET ?at=2026-05-07T11:00:00Z` → возвращает запись (ещё активна). +- `GET ?at=2026-05-07T13:00:00Z` → 404 (уже закрыта). +- `GET ?at=2026-05-07T11:30:00Z&include_closed=true` → для UI history. + +Outbox emit'ит event `action: closed`. + +## Bulk close + +Закрытие нескольких записей одной транзакцией: + +```bash +POST /api/v1/dictionaries/spacecraft/records/_bulk-close +{ + "businessKeys": ["RESURS-P-1", "RESURS-P-2", "RESURS-P-3"], + "at": "2026-05-07T00:00:00+03:00", + "reason": "Программа Ресурс-П завершена" +} +``` + +Response: + +```json +{ + "closed": 3, + "skipped": [], + "errors": [] +} +``` + +Каждая запись в собственной sub-transaction (Spring AOP per-record). +Если одна fail'нула с invalid_key — она в `errors`, остальные продолжают. + +## Bulk export CSV + +```bash +GET /api/v1/dictionaries/spacecraft/records/_export?format=csv&lang=ru-RU +``` + +Stream CSV (RFC 4180) с активными на now записями. Используется UI bulk +operations. + +Параметры — те же что list endpoint (`lang`, `at`, фильтры). + +## Использование для consumer'ов + +### Snapshot at order time + +Когда нужен «как было на момент заказа» — два варианта: + +**Вариант A: query с `?at=`** + +```bash +GET /api/v1/spacecraft/records/RESURS-P-3?at=2025-06-15T10:00:00Z +``` + +Pro: всегда свежие данные если возможна retroactive correction. +Con: depends on Ordinis availability + retention. + +**Вариант B: snapshot в свою БД** + +```sql +CREATE TABLE imaging_order ( + id UUID PRIMARY KEY, + spacecraft_business_key VARCHAR(256), + reference_snapshot JSONB, -- копии записей, не FK + created_at TIMESTAMPTZ DEFAULT NOW() +); +``` + +Pro: read-only к Ordinis, не зависит от availability/retention. +Con: stale если Ordinis retroactively исправил данные. + +Альтум использует **Вариант B** (snapshot) для legal compliance — заказы +attestation требуют не depending on external service. + +## Update с конкретным validFrom + +Не всегда нужно `validFrom = now()`. Можно указать: + +```bash +PUT /api/v1/dictionaries/spacecraft/records/RESURS-P-3 +{ + "validFrom": "2026-05-15T00:00:00+03:00", + "data": { "name": "Ресурс-П № 3 (новое имя с 15 мая)" } +} +``` + +Старая версия закрывается с `validTo = 2026-05-15`. Новая `validFrom = +2026-05-15`. До 15 мая `?at=now` возвращает старую, после — новую. + +## Дальше + +- [Cross-references (FK)](x-references.md) — связи между записями +- [Caching](caching.md) — учитываем bitemporal в инвалидации diff --git a/docs/integration/api/bundle-cuod.md b/docs/integration/api/bundle-cuod.md new file mode 100644 index 0000000..8d98360 --- /dev/null +++ b/docs/integration/api/bundle-cuod.md @@ -0,0 +1,138 @@ +# Bundle справочников cuod (anchor v1) + +ЦУОД (Центр обработки данных дистанционного зондирования Земли) — anchor +customer Ordinis v1. Bundle `cuod-bundle:1.2.1` содержит 40 справочников. + +## Полный список + +| Имя | Scope | Записей (typical) | Назначение | +|---|---|---|---| +| `spacecraft` | PUBLIC | 4-30 | Каталог КА (космические аппараты) | +| `satellite_type` | PUBLIC | 5-15 | Классификация типов КА | +| `spacecraft_status` | PUBLIC | 5-10 | Статусы КА (planned, in-orbit, active, decommissioned) | +| `mission` | PUBLIC | 9-20 | Программы (Ресурс-П, Канопус, Sentinel...) | +| `ground_station` | PUBLIC | 3-10 | Наземные пункты приёма | +| `antenna` | PUBLIC | 5-30 | Антенны на ground stations | +| `instrument` | PUBLIC | 10-50 | Бортовые сенсоры (Геотон, MODIS, C-SAR...) | +| `sensor_type` | PUBLIC | 5-15 | Типы сенсоров (optical, SAR, LIDAR, hyperspectral) | +| `data_format` | PUBLIC | 8-15 | GeoTIFF, NITF, SAFE, COG, JPEG2000, HDF5 | +| `processing_level` | PUBLIC | 8 | L0-L4 уровни обработки | +| `orbit_type` | PUBLIC | 5-10 | LEO, MEO, GEO, SSO, polar | +| `coverage_pattern` | PUBLIC | 5-15 | global, regional, targeted, swath | +| `revisit_period` | PUBLIC | 10-20 | Категории revisit time | +| `band_definition` | PUBLIC | 50-200 | Спектральные band definitions | +| `radiometric_resolution` | PUBLIC | 5-15 | bit depth (8, 12, 16-bit) | +| `swath_width` | PUBLIC | 10-20 | Категории ширины полосы | +| `temporal_resolution` | PUBLIC | 10-20 | Категории временного разрешения | +| `spatial_resolution` | PUBLIC | 15-30 | Категории пространственного разрешения | +| `aoi_type` | PUBLIC | 5-10 | Типы AOI (regions, footprint, custom polygon) | +| `acquisition_mode` | PUBLIC | 5-15 | Modes (push-broom, whisk-broom, frame, SAR strip) | +| `pointing_capability` | PUBLIC | 5-10 | Off-nadir capabilities (nadir-only, ±15°, ±45°) | +| `polarisation_mode` | PUBLIC | 5-10 | SAR polarisation (HH, HV, VH, VV, dual) | +| `antenna_type` | PUBLIC | 10-20 | Антенны: parabolic, phased-array, helical | +| `tracking_mode` | PUBLIC | 5-10 | Tracking algorithms | +| `data_product_type` | PUBLIC | 15-30 | Конечные продукты (orthorectified, mosaic, classified) | +| `coordinate_system` | PUBLIC | 20-50 | Проекции (WGS84, GK-Pulkovo, UTM-zones, MGRS) | +| `agency` | PUBLIC | 10-20 | Космические агентства (Roscosmos, NASA, ESA, JAXA) | +| `orbit_inclination` | PUBLIC | 10 | Категории наклонения | +| `quality_indicator` | INTERNAL | 10-20 | Индикаторы качества (cloud cover, snow cover, off-nadir) | +| `tasking_priority` | INTERNAL | 5-10 | Приоритеты заказов | +| `pricing_tier` | INTERNAL | 5-15 | Тарифы (по resolution / coverage) | +| `customer_segment` | INTERNAL | 10-20 | Сегменты заказчиков | +| `delivery_format` | INTERNAL | 10-20 | Формы поставки (FTP, S3, USB, physical media) | +| `processing_chain_type` | INTERNAL | 10-20 | Цепочки обработки | +| `archive_tier` | INTERNAL | 5 | Tiers хранения (hot/warm/cold) | +| `retention_policy` | RESTRICTED | 5-10 | Сроки хранения по типам данных | +| `access_classification` | RESTRICTED | 5-10 | Уровни секретности | +| `export_control` | RESTRICTED | 5-15 | Экспортные ограничения | +| `licensing_terms` | RESTRICTED | 10-20 | Лицензионные условия | +| `data_sharing_agreement` | RESTRICTED | 5-10 | Соглашения об обмене | + +(Точное число записей варьируется — bundle ships seed data, операторы +добавляют свои.) + +## Cross-references (FK графа) + +```mermaid +graph LR + SC[spacecraft] + ST[satellite_type] + SS[spacecraft_status] + M[mission] + I[instrument] + DF[data_format] + PL[processing_level] + SE[sensor_type] + OT[orbit_type] + AG[agency] + + SC --satelliteTypeCode--> ST + SC --statusCode--> SS + SC --orbitTypeCode--> OT + SC --agencyCode--> AG + M --spacecraftCodes--> SC + I --spacecraftCodes--> SC + I --sensorTypeCode--> SE + I --supportedFormatCodes--> DF + I --supportedLevelCodes--> PL +``` + +(Это subset, для full графа см. отдельные schemas в коде: +`ordinis-cuod-bundle/src/main/resources/cuod/schemas/`.) + +## Типичные сценарии + +### Сценарий: список всех КА с их типами + +1. `GET /api/v1/spacecraft/records?lang=ru` — все КА +2. `GET /api/v1/satellite_type/records?lang=ru` — все типы (parallel) +3. Memory join: `spacecraft.data.satelliteTypeCode === satellite_type.businessKey` + +### Сценарий: cascading select для UI + +```ts +// User выбрал "instrument: MSI-Sentinel-2" +const instrument = await get('/api/v1/instrument/records/MSI-S2?lang=ru') + +// Показать только supported formats +const allFormats = await get('/api/v1/data_format/records?lang=ru') +const allowed = allFormats.filter(f => + instrument.data.supportedFormatCodes.includes(f.businessKey)) + +// allowed: SAFE, GEOTIFF, COG, JPEG2000 +``` + +### Сценарий: snapshot для legal compliance + +При создании imaging order — snapshot всех использованных справочников +в свою БД. См. [Caching → Snapshot at order time](caching.md#snapshot-at-order-time). + +## Performance considerations + +- Полный bundle (все 40 справочников × средние 15 записей × ~1KB JSON = + ~600KB total). Cold load на startup — допустимо. +- После cold load — кэш на 5 мин TTL, рефреш по событиям через webhook / + Kafka. +- Spatial фильтры (`?bbox=`) — только на словари с geometry полями + (`spacecraft.aoi`, `mission.coverage_area`). Использует PostGIS GiST + index. + +## Bundle versioning policy + +| Изменение | Bump | +|---|---| +| Add field в schema (optional) | Patch (1.2.1 → 1.2.2) | +| Add справочник | Minor (1.2.x → 1.3.0) | +| Make required field из optional | Minor (consumers compatible если default sane) | +| Remove field / справочник | Major (1.x.x → 2.0.0) | +| Change field type | Major | +| Add x-references | Minor (но requires re-validation existing data) | + +Major changes coordinated с consumers через `nsi.dictionary.changed` events ++ admin announcement. + +## Дальше + +- [Read endpoints](read-endpoints.md) — как читать +- [Cross-references](x-references.md) — детали FK +- [Caching](caching.md) — стратегии кэша для bundle scale diff --git a/docs/integration/api/caching.md b/docs/integration/api/caching.md new file mode 100644 index 0000000..2a416e7 --- /dev/null +++ b/docs/integration/api/caching.md @@ -0,0 +1,203 @@ +# Стратегии кэширования + +Ordinis оптимизирован под редкие записи и частые чтения. Кэшируй у себя, +не делай round-trip на каждый GET. + +Три рекомендуемых стратегии: + +## A. Простой TTL (рекомендуется для v1) + +Самый простой подход — TTL 5 мин на per-`(dictionary, lang)` ключ. + +### TanStack Query (React) + +```ts +useQuery({ + queryKey: ['ordinis', 'spacecraft', lang], + queryFn: () => + fetch(`/api/reference/spacecraft?lang=${lang}`, { + headers: { Authorization: `Bearer ${token}` } + }).then(r => r.json()), + staleTime: 5 * 60_000, + gcTime: 30 * 60_000, + retry: 3, +}) +``` + +### Server-side кэш (Caffeine + Spring) + +```java +@Cacheable(value = "ordinis-records", key = "#dictionary + ':' + #lang") +public List fetchRecords(String dictionary, String lang) { + return ordinisClient.list(dictionary, lang); +} + +// application.yml +spring: + cache: + caffeine: + spec: maximumSize=10000,expireAfterWrite=5m +``` + +**Pro:** просто, работает out of the box. +**Con:** до 5 мин stale данных. При закрытии записи consumer покажет +её ещё 5 мин. + +## B. Webhook / Kafka push (рекомендуется для prod) + +Realtime invalidation через push-уведомления. + +### Kafka + +Подпишись на `ordinis.cuod.events.public` (см. [События](events.md)), +для каждого пришедшего event — invalidate соответствующий cache key: + +```python +def on_event(event): + dict_name = event['dictionary'] + business_key = event['businessKey'] + redis.delete(f"ordinis:{dict_name}:list") # invalidate list + redis.delete(f"ordinis:{dict_name}:byKey:{business_key}") +``` + +### Webhook + +Тот же pattern но на HTTP push (см. [Webhooks](webhooks.md)): + +```javascript +app.post('/webhooks/ordinis', (req, res) => { + if (!verifyHmac(req)) return res.status(403).send() + + for (const event of req.body.events) { + cache.delete(`ordinis:${event.dictionary}:list`) + cache.delete(`ordinis:${event.dictionary}:byKey:${event.businessKey}`) + } + res.status(202).send() +}) +``` + +**Pro:** < 1s invalidation latency. +**Con:** требует Kafka/HTTPS infrastructure + handle duplicates (at-least-once). + +## C. Snapshot at order time + +Для аудит-кейсов когда нужен «как было на момент заказа» (legal compliance). + +### Pattern + +```sql +CREATE TABLE imaging_order ( + id UUID PRIMARY KEY, + spacecraft_business_key VARCHAR(256), + instrument_business_key VARCHAR(256), + reference_snapshot JSONB, -- копии записей, не FK + created_at TIMESTAMPTZ DEFAULT NOW() +); +``` + +При создании заказа: + +```python +spacecraft_data = ordinis.get(f"/spacecraft/records/{key}") +instrument_data = ordinis.get(f"/instrument/records/{key}") + +db.execute( + "INSERT INTO imaging_order(id, spacecraft_business_key, instrument_business_key, reference_snapshot) " + "VALUES (%s, %s, %s, %s)", + (order_id, spacecraft_key, instrument_key, json.dumps({ + 'spacecraft': spacecraft_data, + 'instrument': instrument_data, + })) +) +``` + +**Pro:** гарантированная история, не зависит от Ordinis availability / +retention. Если Ordinis retroactively изменит запись — твоя копия не +сдвинется. + +**Con:** дубликат данных, увеличение твоей БД. Для Альтум typical scale +(~100k orders/year × ~1KB snapshot = 100MB/year) — не проблема. + +### Альтернатива: bitemporal query + +Можно не делать копию, а позже запрашивать `?at=`: + +```bash +GET /api/v1/spacecraft/records/RESURS-P-3?at=2025-06-15T10:00:00Z +``` + +См. [Bitemporal](bitemporal.md). Pro: меньше storage. Con: depends on +Ordinis availability + retention. Альтум выбрала **snapshot** для legal +compliance. + +## Hybrid: TTL + push invalidation + +Лучший из двух — TTL 1 час (long enough чтобы сократить request rate) + +event-driven invalidation (для realtime updates): + +```python +# Получаем event → invalidate +def on_event(event): + cache.delete(f"ordinis:{event['dictionary']}:list") + +# При cache miss — refetch с TTL 1h +def get_records(dictionary, lang): + cached = cache.get(f"ordinis:{dictionary}:list:{lang}") + if cached: + return cached + fresh = ordinis_client.list(dictionary, lang) + cache.setex(f"ordinis:{dictionary}:list:{lang}", 3600, fresh) + return fresh +``` + +## Cache key patterns + +| Что кэшируем | Ключ | +|---|---| +| List of dictionaries | `ordinis:dictionaries:{scope}` | +| List of records per dict | `ordinis:{dict}:list:{lang}` | +| Single record | `ordinis:{dict}:byKey:{businessKey}:{lang}` | +| History of record | `ordinis:{dict}:history:{businessKey}` (rare, TTL=24h) | + +Включай `lang` в key — иначе разные locales перетирают друг друга. + +## Invalidation на cascade close (v2 roadmap) + +В будущем при cascade close источника emit'ятся events для всех +referencing records. Consumer должен слушать events и invalidate их тоже: + +```python +def on_event(event): + cache.delete(f"ordinis:{event['dictionary']}:byKey:{event['businessKey']}") + + # NEW v2: cascade source — заодно invalidate dependents + if event.get('cascadeSource'): + # Источник тоже нужно invalidate (мы видим только закрытие dependent) + src = event['cascadeSource'] + cache.delete(f"ordinis:{src['dictionary']}:byKey:{src['businessKey']}") +``` + +См. design doc `dict-relationships-v2` (внутренний artifact). + +## Stale cache на 5xx / timeout + +Если Ordinis недоступен — отдавай старые данные с warn'ом, не падай каскадно: + +```python +def get_records_safe(dictionary, lang): + try: + return get_records(dictionary, lang) + except (TimeoutError, HTTPError) as e: + log.warning(f"Ordinis unavailable, using stale cache: {e}") + stale = cache.get(f"ordinis:{dictionary}:list:{lang}", ignore_ttl=True) + if stale: + return stale + raise +``` + +Метрика `_ordinis_stale_serves_total` для мониторинга. + +## Дальше + +- [Best practices](best-practices.md) — общие рекомендации +- [Bundle cuod](bundle-cuod.md) — что именно нужно кэшировать diff --git a/docs/integration/api/errors.md b/docs/integration/api/errors.md new file mode 100644 index 0000000..c0c7b0d --- /dev/null +++ b/docs/integration/api/errors.md @@ -0,0 +1,169 @@ +# Ошибки и rate limits + +## HTTP status coды + +Ordinis следует стандартам REST с расширениями: + +| Status | Используется для | Можно retry? | +|---|---|---| +| `200 OK` | GET / PUT успешен | — | +| `201 Created` | POST создан новый ресурс | — | +| `202 Accepted` | Async операция принята | — | +| `204 No Content` | DELETE / close успешен | — | +| `400 Bad Request` | Malformed JSON / missing required fields | Нет — fix payload | +| `401 Unauthorized` | Token expired / invalid / missing | Refresh token и retry | +| `403 Forbidden` | Token валиден, но scope не позволяет | Нет — request higher role | +| `404 Not Found` | Resource не существует / закрыт / out of scope | Зависит от context | +| `409 Conflict` | Optimistic lock fail / cascade block | Refetch + retry | +| `422 Unprocessable Entity` | Validation errors (schema / x-references) | Нет — fix data | +| `429 Too Many Requests` | Rate limit hit | Wait + retry с backoff | +| `500 Internal Server Error` | Server bug | Retry с exponential backoff | +| `502 Bad Gateway` | Upstream (config-server, Vault) недоступен | Retry | +| `503 Service Unavailable` | Postgres недоступен / restart | Retry с long backoff | +| `504 Gateway Timeout` | Запрос > timeout (typically 30s) | Retry | + +## Error response format + +Все 4xx/5xx с body имеют единый shape: + +```json +{ + "timestamp": "2026-05-07T15:23:45.123+03:00", + "status": 422, + "error": "Validation Error", + "errors": [ + { + "path": "/data/satelliteTypeCode", + "code": "x_references_target_not_found", + "message": "Referenced record not found in 'satellite_type' for code='UNKNOWN'" + }, + { + "path": "/data/launchDate", + "code": "schema_format_invalid", + "message": "Expected ISO 8601 date, got 'tomorrow'" + } + ], + "traceId": "abc123-def456-789" +} +``` + +| Поле | Описание | +|---|---| +| `timestamp` | Server time когда ошибка возникла. | +| `status` | HTTP status code. | +| `error` | Категория (`Validation Error`, `Authorization Error` и т.д.). | +| `errors` | Array of detail errors. Может быть один или несколько. | +| `errors[].path` | JSON Pointer к полю с ошибкой (например `/data/launchDate`). | +| `errors[].code` | Machine-readable код для programmatic handling. | +| `errors[].message` | Human-readable описание (русский для admin UI, английский для consumers). | +| `traceId` | Tempo trace ID — приложи при эскалации в Ordinis team. | + +## Известные error codes + +### Validation (422) + +| Code | Описание | Fix | +|---|---|---| +| `schema_required_missing` | Required field пропущен | Добавить поле | +| `schema_format_invalid` | Wrong format (date / email / etc) | Fix format | +| `schema_type_mismatch` | Wrong JSON type (string vs number) | Fix type | +| `x_references_dict_not_found` | Referenced dictionary не существует | Schema bug — escalate | +| `x_references_target_not_found` | Referenced record не активен / out of scope | Use existing key либо upgrade scope | +| `x_references_non_string` | v1 поддерживает только string refs | Fix schema | +| `x_references_malformed` | Bad x-references syntax | Fix schema | +| `x_localized_default_missing` | i18n field без `default_locale` | Add default locale | +| `geometry_invalid_wkt` | Невалидный WKT для geometry | Fix WKT | +| `geometry_invalid_lat_lon` | lat/lon вне диапазона | Use valid coords | + +### Authorization (401/403) + +| Code | Описание | +|---|---| +| `auth_token_missing` | No `Authorization` header | +| `auth_token_invalid` | Token не verified (signature, issuer) | +| `auth_token_expired` | Token expired | +| `auth_scope_insufficient` | Scope не покрывает запрашиваемый словарь | + +### Conflict (409) + +| Code | Описание | Fix | +|---|---|---| +| `optimistic_lock_failed` | Запись изменилась между read и write | Refetch + retry | +| `record_already_closed` | close() на уже закрытой записи | No-op либо ignore | +| `bulk_partial_failure` | Bulk операция: часть успешна, часть нет | Check `errors` array в response | + +### Server (5xx) + +| Code | Описание | +|---|---| +| `internal_unexpected` | Server bug — escalate с traceId | +| `db_unavailable` | Postgres недоступен | +| `vault_unavailable` | Vault sealed либо unreachable | +| `kafka_publish_failed` | Outbox не смог publish (но запись в БД OK — eventually consistent) | + +## Rate limits + +Сегодня (v1.2.1) Ordinis **не имеет explicit rate limits**. Но есть soft +limits через resource constraints: + +| Resource | Limit | +|---|---| +| Postgres connections | 200 (shared между read-api + writer + projection-writer) | +| Read-api app pods | 2× в проде, 1× CPU + 512MB memory | +| Writer pods | 2× в проде | +| Concurrent JWT verifications | bounded heap (~5k/sec практический максимум) | + +При превышении (типично DDoS-like patterns): +- HTTP `429 Too Many Requests` после ingress rate limit (ingress nginx default). +- HTTP `503 Service Unavailable` при exhausting connection pool. +- `5xx error rate spike` → alert `OrdinisReadApiErrorBudgetBurnFast`. + +### Recommended client-side limits + +Чтобы не triggers backend limits: + +| Сценарий | Recommended rate | +|---|---| +| Cold load на startup | `≤ 100 req/sec` parallel, then cache | +| Steady state read | `≤ 50 req/sec` per consumer | +| Bulk import / migration | Talk to Ordinis team первым | + +Внутренний `RateLimiter` (Caffeine `Bucket4j` — TBD) — в roadmap для v2 если +наблюдаются abuse patterns. + +## Retry strategy + +```python +import time, random +from typing import Callable + +def retry_with_backoff( + fn: Callable, max_attempts: int = 5, base: float = 0.5 +) -> any: + for attempt in range(max_attempts): + try: + return fn() + except (HTTPError, TimeoutError) as e: + if attempt == max_attempts - 1: + raise + if e.status_code in (400, 401, 403, 404, 422): + raise # don't retry permanent errors + sleep = base * (2 ** attempt) + random.uniform(0, 0.5) + time.sleep(sleep) +``` + +## Tracing + +Каждый response имеет `X-Trace-Id` header и `traceId` field в error body. +Ordinis эмитит OpenTelemetry traces в Tempo (`tempo.monitoring:4317`). + +При эскалации Ordinis team: +1. Скопируй `traceId` из response либо логов. +2. Создай issue с воспроизведением. +3. Ordinis team query'ит Tempo: `traceId="abc123-..."`. +4. Trace покажет full path: ingress → read-api → postgres → response. + +## Дальше + +- [Best practices](best-practices.md) — как писать robust integration code +- [Авторизация](auth.md) — как избежать 401/403 diff --git a/docs/integration/api/events.md b/docs/integration/api/events.md new file mode 100644 index 0000000..f5f0eef --- /dev/null +++ b/docs/integration/api/events.md @@ -0,0 +1,189 @@ +# События (Kafka outbox) + +Ordinis публикует все изменения справочников в Kafka через +**outbox pattern** (at-least-once гарантия). Consumers подписываются на +топики и инвалидируют свой кэш / обновляют projection. + +## Топики + +| Топик | Scope | Назначение | +|---|---|---| +| `ordinis.cuod.events.public` | PUBLIC | Изменения публичных справочников. | +| `ordinis.cuod.events.internal` | INTERNAL | INTERNAL словари — нужна role `ordinis-internal`+. | +| `ordinis.cuod.events.restricted` | RESTRICTED | RESTRICTED — role `ordinis-restricted`. | + +Topology: 1 broker (single-node на cluster.142), 3+ partitions per topic +(historical setup имел 12 partitions, при cluster recreate 2026-05-07 +spec задал 3; existing topics annotated `strimzi.io/managed=false`, +broker partitions сохранены), `cleanup.policy: delete`, +`retention.ms: 604800000` (7 дней). + +## Подключение + +### Адреса bootstrap + +| Среда | Bootstrap servers | Auth | +|---|---|---| +| prod | `ordinis-kafka-kafka-bootstrap.ordinis-prod:9092` (TLS+SASL: `:9093`) | SCRAM-SHA-512 | +| staging | `ordinis-kafka-kafka-bootstrap.ordinis-staging:9092` | SCRAM-SHA-512 | + +### Получение SASL креденшелов + +Запросить у Ordinis team создание Kafka user (Strimzi `KafkaUser` CR): + +```yaml +apiVersion: kafka.strimzi.io/v1beta2 +kind: KafkaUser +metadata: + name: altum-consumer + namespace: ordinis-prod + labels: + strimzi.io/cluster: ordinis-kafka +spec: + authentication: + type: scram-sha-512 + authorization: + type: simple + acls: + - resource: + type: topic + name: ordinis.cuod.events.public + patternType: literal + operations: [Read, Describe] + - resource: + type: group + name: altum-consumer- + patternType: prefix + operations: [Read] +``` + +Strimzi user-operator создаст Secret `altum-consumer` с полями `username`, +`password`. Используй их в Kafka client config. + +## Schema события + +### `dictionary.changed` + +Все события имеют общий envelope: + +```json +{ + "schemaVersion": "1.0.0", + "eventType": "dictionary.changed", + "occurredAt": "2026-05-07T15:23:45.123+03:00", + "txTime": "2026-05-07T15:23:45.100+03:00", + "changedBy": "admin@nstart.space", + "changeReason": "Обновление after launch", + "scope": "PUBLIC", + "dictionary": "spacecraft", + "businessKey": "RESURS-P-3", + "action": "updated", + "version": 3, + "validFrom": "2026-05-07T00:00:00+03:00", + "validTo": "9999-12-31T23:59:59+03:00", + "data": { + "name": { "ru-RU": "Ресурс-П № 3 (обновлён)" }, + "status": "active", + "satelliteTypeCode": "EARTH-OBSERVATION-OPTICAL" + }, + "previousData": { + "name": { "ru-RU": "Ресурс-П № 3" }, + "status": "in-orbit" + } +} +``` + +### Поля envelope + +| Поле | Описание | +|---|---| +| `schemaVersion` | Semver схемы события. Bump major = breaking change. | +| `eventType` | `dictionary.changed` для CRUD, `dictionary.bulk_imported` для bundle imports. | +| `occurredAt` | Wall-clock когда событие создано. | +| `txTime` | Transaction time изменения в БД (для ordering). | +| `action` | `created` / `updated` / `closed`. | +| `version` | Optimistic lock version. Monotonic per businessKey. | +| `data` | Payload новой версии. | +| `previousData` | Diff-like — поля, которые поменялись (только для `updated`). Полезен для consumer'ов которые хотят минимальный set re-fetches. | +| `validFrom` / `validTo` | Bitemporal границы. | + +### `action` semantics + +- **created** — новая запись с уникальным `businessKey`. `previousData = null`. +- **updated** — существующий businessKey, новая версия. `previousData` = + изменённые поля старой версии. +- **closed** — `validTo` обновлён в `now()` (раньше `9999-...`). Запись больше + не возвращается на `?at=now`. `previousData` = вся старая версия. + +### Cascade events (v2 roadmap) + +При cascade close (см. design doc `dict-relationships-v2` — внутренний +artifact в `~/.gstack/projects/claude/`) на каждую закрытую запись +отдельное событие emit'ится в outbox с дополнительным полем +`cascadeSource` = `{dictionary, businessKey}` источника. Consumer может +trace cascade chain. + +## Partition key + +Сообщения партиционируются по `dictionary + businessKey` (одна запись — +одна partition). Это даёт consumer'ам ordering guarantee per record. + +``` +key = "spacecraft:RESURS-P-3" +``` + +{% note info %} + +Если consumer обрабатывает события параллельно (multiple consumer threads / +pods), Kafka гарантирует только partition-level ordering. Ordering между +records в разных partitions не определён. Для consumer'ов это норма — +each businessKey изолирован. + +{% endnote %} + +## At-least-once семантика + +Outbox pattern гарантирует **at-least-once** delivery: + +1. Writer пишет запись в БД + outbox table в одной транзакции. +2. OutboxPoller (scheduled @1s) читает unpublished rows, отправляет в Kafka. +3. После Kafka ACK — отмечает row как published (`published_at` set). + +Что это значит для consumer'а: + +- Можешь получить **дубликат** события если poller crashed после ACK но до + `published_at` write. Idempotency у consumer'а — must. +- При network partition между Kafka и poller — сообщения накапливаются в + outbox (alert `OrdinisOutboxLagHigh` срабатывает > 100 events). + +## Idempotency + +Используй `(dictionary, businessKey, txTime, version)` как dedup key: + +```python +seen = redis.get(f"ordinis:event:{dict}:{key}:{tx_time}:{ver}") +if not seen: + process_event(event) + redis.setex(f"ordinis:event:...", 86400, "1") +``` + +## DLQ (Dead-letter queue) + +Если событие не публикуется > 1000 retry попыток (network issues, +Kafka unavailable) — переходит в DLQ table в Ordinis. Не теряется, но +требует ручного разбора. + +Симптом: alert `OrdinisOutboxDLQNonEmpty` → check `/api/v1/admin/audit?action=outbox.dlq`. + +## Топик для discovery + +В roadmap планируется `nsi.dictionary.changed` aggregator topic — single +firehose для consumers которые хотят видеть все scope'ы в одном потоке +(c фильтрацией consumer-side). Сейчас **нет**, подписывайся на 3 scope +топика отдельно. + +## Дальше + +- [Webhooks](webhooks.md) — alternative для consumer'ов без Kafka access +- [Bitemporal](bitemporal.md) — как интерпретировать `validFrom`/`validTo` +- [Caching](caching.md) — invalidation pattern с Kafka events diff --git a/docs/integration/api/index.md b/docs/integration/api/index.md new file mode 100644 index 0000000..421951e --- /dev/null +++ b/docs/integration/api/index.md @@ -0,0 +1,54 @@ +# Интеграция по API — обзор + +Этот раздел описывает как backend-сервисы (Альтум, геопортал, BI и т.д.) +читают и пишут справочники Ordinis НСИ ЦУОД. + +**Owner:** Ordinis team — `zimin.an@nstart.space`. + +## С чего начать + +1. [Получи Keycloak service account](auth.md) — нужен для всех API вызовов. +2. [Прочитай read-side endpoints](read-endpoints.md) — как получить данные. +3. [Выбери стратегию обновления](caching.md) — TTL, webhooks, либо Kafka events. + +## Что Ordinis даёт + +- **Bitemporal CRUD** для справочников — `validFrom` / `validTo` + revision + history через Hibernate Envers. +- **Multi-locale** — каждое поле может иметь `{ru-RU, en-US, ...}` варианты. + Flatten через `?lang=` параметр. +- **Cross-dictionary FK** через `x-references` schema extension. Server-side + валидация на CRUD + orphan scanner periodic. +- **Geometry support** — PostGIS для bbox/polygon фильтров (`spacecraft.aoi`, + `mission.coverage_area`). +- **Audit log** — все изменения через `/api/v1/admin/audit` (UI: `/audit`). +- **Outbox + Kafka** — at-least-once delivery всех изменений в 3 топика. +- **Webhooks** — push-уведомления HTTP к зарегистрированным URLs (для + consumers без Kafka access). +- **Bulk operations** — multi-select close + CSV export. + +## Что Ordinis НЕ делает + +- Не хранит "снимок на момент заказа" — для этого consumer должен сделать + snapshot rough copy в свою БД (см. [Caching → Snapshot strategy](caching.md#snapshot-at-order-time)). +- Не enforce'ит FK на уровне DB — это NoSQL-style references поверх JSONB. + Cascade rules в roadmap (см. design `dict-relationships-v2`). +- Не обеспечивает realtime sync — TTL = 5 мин минимум либо event-driven invalidation. + +## Версионирование + +- **API** — `/api/v1/...` стабилен. Breaking changes только через major (`v2`). +- **Bundle** (cuod-bundle) — semver. Minor bump при schema изменении, major + при удалении справочника. См. [Bundle cuod](bundle-cuod.md). + +## Глоссарий + +| Термин | Значение | +|---|---| +| Dictionary | Справочник (например `spacecraft`, `satellite_type`). | +| Record | Запись в справочнике с уникальным `businessKey`. | +| businessKey | Стабильный идентификатор (string). Не меняется при update. | +| Bundle | Maven module с schemas + seed data. ЦУОД bundle = 40 справочников. | +| Scope | Уровень доступа: `PUBLIC` / `INTERNAL` / `RESTRICTED`. | +| Bitemporal | Запись имеет `validFrom`/`validTo` + revision history (transaction time). | +| x-references | JSON Schema extension для cross-dictionary FK. | diff --git a/docs/integration/api/read-endpoints.md b/docs/integration/api/read-endpoints.md new file mode 100644 index 0000000..68bf4e8 --- /dev/null +++ b/docs/integration/api/read-endpoints.md @@ -0,0 +1,191 @@ +# Read-side endpoints + +Read-API в Ordinis — отдельный deployable (`ordinis-read-api`, scaled 2× в проде) +работающий на Postgres replica. Запросы из read-api **не блокируют** writer'а. + +Base URL: + +| Среда | URL | +|---|---| +| prod | `https://ordinis.k8s.264.nstart.cloud/api/v1` | +| staging | `https://ordinis.k8s.265.nstart.cloud/api/v1` | + +Все endpoints требуют JWT (см. [Авторизация](auth.md)). + +## OpenAPI / Swagger UI + +- **Swagger UI:** `https://ordinis.k8s.264.nstart.cloud/swagger-ui.html` +- **Spec JSON:** `https://ordinis.k8s.264.nstart.cloud/v3/api-docs` +- **Spec YAML:** `https://ordinis.k8s.264.nstart.cloud/v3/api-docs.yaml` + +Из spec'а можно generate'ить клиент: + +```bash +openapi-generator-cli generate \ + -i https://ordinis.k8s.264.nstart.cloud/v3/api-docs \ + -g python \ + -o ./altum-backend/clients/ordinis +``` + +Поддерживаются: Python, TypeScript, Go, Java, Kotlin, Ruby и др. + +## `GET /api/v1/dictionaries` + +Список доступных справочников (фильтрован по scope JWT'а). + +```bash +curl -H "Authorization: Bearer $TOKEN" \ + https://ordinis.k8s.264.nstart.cloud/api/v1/dictionaries +``` + +Response: + +```json +[ + { + "name": "spacecraft", + "displayName": "Космические аппараты", + "scope": "PUBLIC", + "schemaVersion": "1.0.0", + "recordsCount": 4 + }, + { + "name": "mission", + "displayName": "Миссии / программы", + "scope": "PUBLIC", + "schemaVersion": "1.0.0", + "recordsCount": 9 + } +] +``` + +## `GET /api/v1/{name}/records` + +{% note warning %} + +**БЕЗ префикса `/dictionaries/`!** Read-api сидит на `/api/v1/{name}/records`. +Префикс `/api/v1/dictionaries/...` — это admin write controller (CRUD), у него +нет bulk-list эндпоинта. + +{% endnote %} + +Активные записи на момент `at` (default: сейчас). + +### Параметры + +| Параметр | Тип | Пример | Назначение | +|---|---|---|---| +| `lang` | string | `ru-RU`, `en-US` | Flatten i18n полей в строку. Короткие `ru`/`en` тоже. | +| `at` | ISO 8601 | `2026-05-04T12:00:00+03:00` | Bitemporal query — активные на момент. | +| `as_scope` | string | `PUBLIC,INTERNAL` | Legacy для anonymous + `allowQueryScope=true`. | + +### Пример + +```bash +curl -H "Authorization: Bearer $TOKEN" \ + "https://ordinis.k8s.264.nstart.cloud/api/v1/spacecraft/records?lang=ru-RU" +``` + +Response: + +```json +[ + { + "businessKey": "RESURS-P-3", + "displayKey": "Ресурс-П № 3", + "scope": "PUBLIC", + "validFrom": "2025-01-01T00:00:00+03:00", + "validTo": "9999-12-31T23:59:59+03:00", + "data": { + "name": "Ресурс-П № 3", + "satelliteTypeCode": "EARTH-OBSERVATION-OPTICAL", + "launchDate": "2024-12-25", + "status": "active" + }, + "version": 2 + } +] +``` + +### Без `?lang` (raw i18n) + +```json +"name": { "ru-RU": "Ресурс-П № 3", "en-US": "Resurs-P No.3" } +``` + +С `?lang=ru-RU`: + +```json +"name": "Ресурс-П № 3" +``` + +Если запрошенного языка нет — fallback к `default_locale` (обычно `ru-RU`). + +## `GET /api/v1/{name}/records/{businessKey}` + +Одна запись. Параметры те же что у list. + +```bash +curl -H "Authorization: Bearer $TOKEN" \ + "https://ordinis.k8s.264.nstart.cloud/api/v1/spacecraft/records/RESURS-P-3?lang=ru" +``` + +`404 Not Found` — запись либо не существует, либо закрыта на момент `at`, +либо вне scope JWT'а (Ordinis скрывает существование). + +## `GET /api/v1/{name}/records/{businessKey}/history` + +Все версии записи (включая закрытые) для построения истории изменений в UI. + +```json +[ + { + "version": 1, + "validFrom": "2024-06-01T00:00:00+03:00", + "validTo": "2025-01-01T00:00:00+03:00", + "data": {...}, + "txTime": "2024-06-01T10:23:45+03:00", + "changedBy": "admin@nstart.space", + "changeReason": "initial" + }, + { + "version": 2, + "validFrom": "2025-01-01T00:00:00+03:00", + "validTo": "9999-12-31T23:59:59+03:00", + "data": {...}, + "txTime": "2024-12-30T14:11:22+03:00", + "changedBy": "admin@nstart.space", + "changeReason": "Обновление статуса после успешного запуска" + } +] +``` + +Для consumers history endpoint обычно не нужен — это UI-feature. + +## Spatial filters (geometry) + +Если справочник имеет geometry поля (`spacecraft.aoi`, `mission.coverage_area`), +поддерживаются bbox + polygon фильтры: + +### `?bbox=` (bounding box) + +``` +GET /api/v1/spacecraft/records?bbox=37.0,55.0,38.0,56.0 +``` + +Format: `min_lon,min_lat,max_lon,max_lat` (WGS84). Возвращает записи где +`geometry` пересекает bbox через PostGIS `ST_Intersects` + GiST index. + +### `?polygon=` (GeoJSON) + +``` +GET /api/v1/spacecraft/records?polygon={"type":"Polygon","coordinates":[[[37,55],[38,55],[38,56],[37,56],[37,55]]]} +``` + +Polygon — GeoJSON object (URL-encoded). Сложнее bbox, точнее. + +## Дальше + +- [События (Kafka outbox)](events.md) — push-обновления вместо polling +- [Bitemporal](bitemporal.md) — детали `validFrom`/`validTo` модели +- [Caching](caching.md) — стратегии актуализации diff --git a/docs/integration/api/webhooks.md b/docs/integration/api/webhooks.md new file mode 100644 index 0000000..6116059 --- /dev/null +++ b/docs/integration/api/webhooks.md @@ -0,0 +1,216 @@ +# Webhooks + +Альтернатива Kafka events для consumer'ов которые не хотят / не могут +поддерживать Kafka client. Ordinis отправляет HTTP POST на зарегистрированный +URL при изменениях справочников. + +{% note info %} + +Если у тебя есть Kafka access — используй [события](events.md). Webhooks +проще в развёртывании, но менее надёжны (HTTP retry vs Kafka durable log) +и медленнее (HTTP overhead vs Kafka batch). + +{% endnote %} + +## Регистрация подписки + +```bash +curl -X POST -H "Authorization: Bearer $ADMIN_TOKEN" \ + -H "Content-Type: application/json" \ + https://ordinis.k8s.264.nstart.cloud/api/v1/admin/webhooks/subscriptions \ + -d '{ + "name": "altum-cache-invalidation", + "url": "https://altum-backend.altum.local/webhooks/ordinis", + "secret": "", + "scopes": ["PUBLIC", "INTERNAL"], + "dictionaries": ["spacecraft", "satellite_type", "instrument"], + "actions": ["created", "updated", "closed"], + "active": true + }' +``` + +### Поля + +| Поле | Тип | Назначение | +|---|---|---| +| `name` | string | Человекочитаемое имя для admin UI. | +| `url` | string | HTTPS endpoint. HTTP блокируется SSRF guard'ом. | +| `secret` | string | HMAC-SHA256 секрет для подписи payload (см. ниже). | +| `scopes` | array | Фильтр по scope (subset из доступных consumer'у). | +| `dictionaries` | array | Фильтр по dictionary names. `null`/`[]` = все доступные. | +| `actions` | array | `created` / `updated` / `closed`. `null` = все. | +| `active` | bool | Можно временно отключить без удаления. | + +## SSRF guard + +Webhook URLs валидируются перед отправкой: + +- **Запрещено:** private CIDR (10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16), + localhost, link-local (169.254/16), metadata IPs. +- **Запрещено:** non-HTTPS scheme. Только `https://`. +- **Запрещено:** URL > 2048 chars. + +Метрика `nsi_webhook_ssrf_rejected_total` increments при попытках. См. +prometheus rule `OrdinisWebhookSsrfRejected`. + +## Payload + +POST на зарегистрированный URL с body: + +```json +{ + "deliveryId": "wd-abc123-xyz789", + "subscriptionName": "altum-cache-invalidation", + "occurredAt": "2026-05-07T15:23:45.123+03:00", + "events": [ + { + "schemaVersion": "1.0.0", + "eventType": "dictionary.changed", + "dictionary": "spacecraft", + "businessKey": "RESURS-P-3", + "action": "updated", + "version": 3, + "validFrom": "2026-05-07T00:00:00+03:00", + "data": { ... } + } + ] +} +``` + +{% note tip %} + +Payload может содержать **несколько events** (`events` array) если они +произошли в одной transaction (bulk close, bundle import). Не предполагай +single-event payload. + +{% endnote %} + +## HMAC подпись + +Каждый POST имеет header: + +``` +X-Ordinis-Signature: sha256= +X-Ordinis-Delivery: wd-abc123-xyz789 +X-Ordinis-Subscription: altum-cache-invalidation +``` + +Где `` = HMAC-SHA256 от raw body bytes с `secret` который ты задал +при регистрации. + +### Verify (Python) + +```python +import hmac, hashlib + +def verify(body: bytes, signature_header: str, secret: str) -> bool: + expected = "sha256=" + hmac.new( + secret.encode(), body, hashlib.sha256 + ).hexdigest() + return hmac.compare_digest(expected, signature_header) +``` + +### Verify (Node.js) + +```javascript +import crypto from 'crypto' + +function verify(body, signatureHeader, secret) { + const expected = 'sha256=' + crypto + .createHmac('sha256', secret) + .update(body) + .digest('hex') + return crypto.timingSafeEqual( + Buffer.from(expected), + Buffer.from(signatureHeader) + ) +} +``` + +{% note warning %} + +Verify подпись **до** парсинга JSON. Если invalid — `403 Forbidden`, +не трогай payload. Без verify ты уязвим к подделке events от любого, кто +знает твой URL. + +{% endnote %} + +## Response contract + +Consumer должен ответить `2xx` в течение 10 секунд: + +| Status | Семантика | +|---|---| +| `200` / `201` / `202` / `204` | Принято. Ordinis помечает delivered. | +| `4xx` | Permanent failure (твой bug). Ordinis НЕ retry. | +| `5xx` / timeout | Transient. Ordinis retry с exponential backoff. | + +## Retry policy + +| Попытка | Интервал | +|---|---| +| 1 | сразу | +| 2 | через 30s | +| 3 | через 2 мин | +| 4 | через 10 мин | +| 5 | через 1ч | +| 6-10 | каждые 6ч | +| 1000+ | DLQ → ручной разбор | + +После 1000 неудач delivery попадает в DLQ (`/api/v1/admin/webhooks/deliveries/dlq`). +Alert: `OrdinisWebhookDLQNonEmpty`. + +## Test ping + +Endpoint для проверки доступности URL **до** регистрации: + +```bash +curl -X POST -H "Authorization: Bearer $ADMIN_TOKEN" \ + -H "Content-Type: application/json" \ + https://ordinis.k8s.264.nstart.cloud/api/v1/admin/webhooks/test-ping \ + -d '{ + "url": "https://altum-backend.altum.local/webhooks/ordinis", + "secret": "" + }' +``` + +Response: + +```json +{ + "success": true, + "statusCode": 200, + "responseBody": "OK", + "elapsedMs": 142 +} +``` + +Либо details про failure (DNS issue, timeout, SSRF reject и т.д.). + +## Best practices + +1. **Idempotency** — webhook может прийти **дважды** (network retry, partial + failure). Используй `deliveryId` как dedup key. +2. **Async обработка** — отвечай `202 Accepted` сразу, обрабатывай в + background. 10s timeout жёсткий — если БД медленная, лучше принять и + сложить в очередь. +3. **HMAC verify первым** — до парсинга JSON. +4. **Metrics** — track received / processed / errors для своего side. + Сравнивай с Ordinis metrics при расследовании. +5. **Whitelist Ordinis IP** на firewall если строгие правила. IP cluster.142 + = `192.168.100.142`, но в production будет ingress IP. Спросить у Ordinis + team при registration. + +## Alerting (Ordinis side) + +| Alert | Trigger | +|---|---| +| `OrdinisWebhookHighFailureRate` | failure rate > 50% за 5 мин | +| `OrdinisWebhookDLQNonEmpty` | events в DLQ > 0 | +| `OrdinisWebhookDispatcherIdle` | 0 deliveries при > 0 active subscriptions | +| `OrdinisWebhookSsrfRejected` | SSRF guard сработал > 0 раз за 15 мин | + +## Дальше + +- [События (Kafka)](events.md) — primary delivery channel +- [Caching](caching.md) — webhook invalidation pattern diff --git a/docs/integration/api/x-references.md b/docs/integration/api/x-references.md new file mode 100644 index 0000000..c354929 --- /dev/null +++ b/docs/integration/api/x-references.md @@ -0,0 +1,142 @@ +# Cross-references (FK между справочниками) + +JSON Schema extension `x-references` объявляет, что значение поля должно +ссылаться на active record в указанном словаре. + +## Синтаксис + +```json +{ + "type": "object", + "properties": { + "satelliteTypeCode": { + "type": "string", + "x-references": "satellite_type.code" + } + } +} +``` + +Format: `"."`. Когда POST/PUT приходит, Ordinis +делает lookup в `satellite_type` где `data->>code == satelliteTypeCode`. +Если не найден / не активен / вне scope — `422 Validation Error`. + +## Текущие FK в bundle cuod + +| Source | Target | Поле | +|---|---|---| +| `spacecraft.satelliteTypeCode` | `satellite_type` | `code` | +| `spacecraft.statusCode` | `spacecraft_status` | `code` | +| `mission.spacecraftCodes` (array) | `spacecraft` | `businessKey` | +| `instrument.spacecraftCodes` (array) | `spacecraft` | `businessKey` | +| `instrument.supportedFormatCodes` (array) | `data_format` | `businessKey` | +| `instrument.supportedLevelCodes` (array) | `processing_level` | `businessKey` | + +(Точное число и список могут меняться с bundle versions — см. +[Bundle cuod](bundle-cuod.md).) + +## Server-side validation + +При POST/PUT ReferenceValidator проверяет каждое x-references поле: + +| Симптом | Error code | Описание | +|---|---|---| +| Referenced dictionary не существует | `x_references_dict_not_found` | Опечатка в schema. | +| Referenced record не найден / inactive | `x_references_target_not_found` | Bad value либо record закрыт. | +| Value не string (для v1) | `x_references_non_string` | v1 поддерживает только string refs. | +| Schema spec malformed | `x_references_malformed` | Bad schema. | + +Response: + +```json +{ + "errors": [ + { + "path": "/satelliteTypeCode", + "code": "x_references_target_not_found", + "message": "Referenced record not found in 'satellite_type' for code='UNKNOWN-TYPE'" + } + ] +} +``` + +## Scope hiding + +Если referenced dictionary — INTERNAL/RESTRICTED, а consumer имеет только +PUBLIC scope, ReferenceValidator возвращает тот же error +(`x_references_target_not_found`) что и на real not-found. Ordinis **не leak'ает** +existence of out-of-scope dictionary через differential errors. + +## Orphan reference scanner + +Возможна ситуация когда referenced record **закрывается**, а referencing +record остаётся: + +``` +1. spacecraft.RESURS-P-3 references satellite_type.EARTH-OPTICAL +2. Admin closes satellite_type.EARTH-OPTICAL (record.closed event emitted) +3. spacecraft.RESURS-P-3 теперь имеет dangling FK +``` + +Periodic scanner (`OrphanReferenceScanner`, runs каждые 5 мин) находит +такие orphan'ы и emit'ит metric: + +``` +nsi_orphan_references_total{source_dict="spacecraft",target_dict="satellite_type"} 1 +``` + +Alert `OrdinisOrphanReferences` срабатывает при > 0 за 10 мин. + +## Cascade rules — roadmap (v2) + +Сегодня закрытие источника НЕ closes referencing automatically. Это +дolжно явно делаться через bulk close либо individual updates. + +В v2 планируется schema extension `x-references-on-close`: + +```json +{ + "satelliteTypeCode": { + "type": "string", + "x-references": "satellite_type.code", + "x-references-on-close": "block" + } +} +``` + +Опции: +- **block** — закрытие источника невозможно пока есть active referencing (default). +- **warn** — закрытие проходит, в response warning + metric `nsi_cascade_warnings_total`. +- **cascade** — все referencing закрываются в той же транзакции. + +См. design doc `dict-relationships-v2` (внутренний artifact в +`~/.gstack/projects/claude/`) для полного scope. + +## Lineage discovery — roadmap (v2) + +Сегодня нет endpoint для "что depends on X". Admin должен вручную проходить +schemas чтобы найти ссылки. В v2 планируется: + +- `GET /api/v1/dictionaries/{name}/dependents` — словари которые ссылаются. +- `GET /api/v1/records/{dict}/{key}/dependents` — конкретные записи. + +См. design выше для подробностей. + +## Best practices + +1. **Используй businessKey как target field** для большинства FK. Это + стабильный идентификатор. Альтернатива (`code`, `name`) работает но + требует поддержки `x-unique` в target (иначе ambiguous matches). +2. **Не делай deeply nested refs** — v1 поддерживает только top-level + fields. Nested objects с x-references — пропустят validation. +3. **Array refs работают** — если поле `type: array, items: {type: string, + x-references: ...}`, каждый элемент валидируется. (Но в v1.2.1 это + делается на каждый элемент отдельно — for large arrays медленно. Будет + batch'нуто в v2.) +4. **Document FK в displayName** — `"description": "FK на satellite_type + через code"` помогает при ревью schemas. + +## Дальше + +- [Bundle cuod](bundle-cuod.md) — список текущих FK в проде +- [События](events.md) — учитывай cascade closure при инвалидации diff --git a/docs/ops/README.md b/docs/ops/README.md index 1e00ef1..e9e236e 100644 --- a/docs/ops/README.md +++ b/docs/ops/README.md @@ -71,7 +71,7 @@ kubectl get pods -n ordinis-staging | grep admin-ui | Status | Причина | |-----------------------|---------| -| `ImagePullBackOff` | Тег image не существует в registry. Проверь `kubectl describe`. Восстановить:
`kubectl set image deployment/ordinis-admin-ui -n ordinis-staging ui=repo.nstart.cloud/terravault/ordinis-admin-ui:main` | +| `ImagePullBackOff` | Тег image не существует в registry. Проверь `kubectl describe`. Восстановить: `kubectl set image deployment/ordinis-admin-ui ui=/ordinis-admin-ui:main -n ordinis-staging` | | `CrashLoopBackOff` | nginx config invalid либо resolver недоступен. См. logs. | | `Running 1/1`, но 502 | Upstream (writer/read-api) сам в дауне. Проверь те pod'ы. | @@ -91,8 +91,8 @@ kubectl get pods -n ordinis-staging | grep admin-ui ## 6. Связанные ресурсы -- API integration guide: [../tech/api-integration.md](../tech/api-integration.md) +- API integration guide: [../integration/api/index.md](../integration/api/index.md) (Diplodoc, latest) - Альтум integration: [../integration/altum-imaging-order.md](../integration/altum-imaging-order.md) -- Project state: [../status/2026-05-05-state.md](../status/2026-05-05-state.md) +- Project state snapshots: см. `docs/status/*-state.md` в repo (исключены из Diplodoc build) - Helm chart: `ordinis-infra/charts/ordinis/` - Vault setup: `ordinis-infra/k8s/vault/setup.sh` diff --git a/docs/ops/slo.md b/docs/ops/slo.md index e3fc6cb..89e8a4c 100644 --- a/docs/ops/slo.md +++ b/docs/ops/slo.md @@ -5,7 +5,7 @@ после 2-3 месяцев наблюдения. **Source of truth для алертов:** -[ordinis-infra/charts/ordinis/templates/prometheus-rules.yaml](../../code/ordinis-infra/charts/ordinis/templates/prometheus-rules.yaml). +`ordinis-infra/charts/ordinis/templates/prometheus-rules.yaml` (отдельный repo). ## 1. SLI (Service Level Indicators) @@ -63,7 +63,7 @@ Routing в Slack/Telegram/Email — отдельная задача: - **warning** → digest channel (раз в час summary) - [ ] Эскалация: critical → engineer → tech lead через 15 мин если не acked. -См. v1 closure backlog в [docs/status/2026-05-05-state.md](../status/2026-05-05-state.md). +См. `docs/status/2026-05-05-state.md` в repo (status snapshots исключены из Diplodoc build). ## 3. Error budget (v1 target) diff --git a/docs/package.json b/docs/package.json new file mode 100644 index 0000000..b00648a --- /dev/null +++ b/docs/package.json @@ -0,0 +1,18 @@ +{ + "name": "ordinis-docs", + "version": "1.0.0", + "private": true, + "description": "Ordinis НСИ ЦУОД ОДХ — техническая документация (Diplodoc / YFM)", + "scripts": { + "prebuild": "rm -rf ./_dist", + "build": "yfm build -i . -o ./_dist --output-format html", + "build:md": "yfm build -i . -o ./_dist --output-format md", + "serve": "npx serve ./_dist -p 8088", + "dev": "pnpm build && pnpm serve", + "clean": "rm -rf ./_dist" + }, + "devDependencies": { + "@diplodoc/cli": "^4.50.0" + }, + "packageManager": "pnpm@9.0.0" +} diff --git a/docs/pnpm-lock.yaml b/docs/pnpm-lock.yaml new file mode 100644 index 0000000..ead1c97 --- /dev/null +++ b/docs/pnpm-lock.yaml @@ -0,0 +1,1429 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + devDependencies: + '@diplodoc/cli': + specifier: ^4.50.0 + version: 4.60.0(@types/markdown-it@13.0.9)(markdown-it@13.0.2) + +packages: + + '@cospired/i18n-iso-languages@4.2.0': + resolution: {integrity: sha512-vy8cq1176MTxVwB1X9niQjcIYOH29F8Huxtx8hLmT5Uz3l1ztGDGri8KN/4zE7LV2mCT7JrcAoNV/I9yb+lNUw==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + + '@diplodoc/cli@4.60.0': + resolution: {integrity: sha512-1B5J04ohNV1saLkT2hIpG4YosCWVyJNBNuMlXl38RgOMqVYhJ7VdD+mtKnIW8pC6j7n/hliI2CPgNtEWbvlFGA==} + engines: {node: '>=18.*'} + hasBin: true + + '@diplodoc/client@3.12.0': + resolution: {integrity: sha512-LmYWT2jhZtndvfhtFcsO3CJh7IpZAXyMDo8pkQWdm92qX9AfUDfk4FcBZryueWGCaX0BMtFGogMHEstVbVCymg==} + engines: {node: '>=22', npm: '>=9.*'} + + '@diplodoc/cut-extension@1.1.1': + resolution: {integrity: sha512-+D7IPDrLg+ANhVusUA9t7n+sXpGzuoq9Lsr8BzgDBv8KWKD0DLGNfhspHKqi1prVVM8BZadmVR4BLsnTrV3zcA==} + + '@diplodoc/directive@0.3.3': + resolution: {integrity: sha512-QojMfwlyjg0uhPd7I/roIkBYDDBsNkYmHPr7nzkHEGYC/nszJy/4NuWshzB10YW3CbWSO9BUwCk6XoZXgTrKZg==} + + '@diplodoc/file-extension@0.2.2': + resolution: {integrity: sha512-BF8t27RGAn6C3pLvHcpOm5q1RQzgsqzSRsg5VNBlaCKMoZ9buWl5+FoqKXhHGg3zfBuYvPGBcrhmSBWnc0GP1Q==} + engines: {npm: '>=11.5.1'} + peerDependencies: + markdown-it: ^13.0.0 + + '@diplodoc/liquid@1.5.0': + resolution: {integrity: sha512-4AacHJ9bl65AuPjNHiPb/xWh6k1fba4AKTvy1C9ha8bNZJVwjdiObzMZSBujN/JTaKCH5xmqvj3pQEWBh+kxbw==} + engines: {npm: '>=11.5.1'} + + '@diplodoc/sentenizer@0.0.8': + resolution: {integrity: sha512-xiAi5gB49UxZZyVWOKw7sE/ixhCUpV4pmdtMnRtdtrljfNd7GZPghQoKutA/zFwEI2B5g+gyCJ59+rhrL19p7g==} + + '@diplodoc/tabs-extension@3.10.0': + resolution: {integrity: sha512-3Ky6FiGpoQgc5A/6R82y6kuTY0dMMImrFNW0ktPxAfs9CSWqpaPMWR2ZD2dy6Q0ks8mThLNJ4GjQn6vkwMuF9w==} + engines: {node: '>=22', npm: '>=11.5.1'} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + peerDependenciesMeta: + react: + optional: true + + '@diplodoc/transform@4.74.1': + resolution: {integrity: sha512-/q8ooMF/gmOzzsrVrD5BL33hXOsPeCVJZr238C0C9GzmmJJX2IPo0EiSBZkeQZY8AS3iS3SsvxtRE1GcWsUINQ==} + engines: {npm: '>=11.5.1'} + peerDependencies: + highlight.js: ^10.0.3 || ^11 + peerDependenciesMeta: + highlight.js: + optional: true + + '@diplodoc/translation@1.7.21': + resolution: {integrity: sha512-NiudDMlLXMWEKZn57f2L6P8lgueypU8JSOL82VRqPSWtrtkJw9DbTWlj7Tx/mRNLPdx9uvcDdyFIawAYgDVkVw==} + engines: {node: '>=22.*', npm: '>=11.5.1'} + peerDependencies: + markdown-it: ^13.0.2 + + '@diplodoc/utils@2.2.2': + resolution: {integrity: sha512-j6AgDFuxo7CVbs3EteJSqxGDwjCGuMfOGFSFbx+0rc90tj1qvgYMkmGQQdcUkL/aCFU/0JokCrD+4nSjIcAKKw==} + engines: {npm: '>=11.5.1'} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + peerDependenciesMeta: + react: + optional: true + + '@isaacs/cliui@8.0.2': + resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==} + engines: {node: '>=12'} + + '@pkgjs/parseargs@0.11.0': + resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} + engines: {node: '>=14'} + + '@sec-ant/readable-stream@0.4.1': + resolution: {integrity: sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg==} + + '@shellscape/i18n-iso-countries@7.5.0-shellscape.v1': + resolution: {integrity: sha512-FB7Grjo2ciFWcU6mNl3rf0xRnUnjzu3NlEOMOg9dhrsRm18WMOpWkHbwL21+HXnAVjsxIn8l3MnkiAz/LdmPiQ==} + engines: {node: '>= 16'} + + '@sindresorhus/merge-streams@4.0.0': + resolution: {integrity: sha512-tlqY9xq5ukxTUZBmoOp+m61cqwQD5pHJtFY3Mn8CA8ps6yghLH/Hw8UPdqg4OLmFW3IFlcXnQNmo/dh8HzXYIQ==} + engines: {node: '>=18'} + + '@types/linkify-it@3.0.5': + resolution: {integrity: sha512-yg6E+u0/+Zjva+buc3EIb+29XEg4wltq7cSmd4Uc2EE/1nUVmxyzpX6gUXD0V8jIrG0r7YeOGVIbYRkxeooCtw==} + + '@types/markdown-it@13.0.9': + resolution: {integrity: sha512-1XPwR0+MgXLWfTn9gCsZ55AHOKW1WN+P9vr0PaQh5aerR9LLQXUbjfEAFhjmEmyoYFWAyuN2Mqkn40MZ4ukjBw==} + + '@types/mdurl@1.0.5': + resolution: {integrity: sha512-6L6VymKTzYSrEf4Nev4Xa1LCHKrlTlYCBMTlQKFuddo1CvQcE52I0mwfOJayueUC7MJuXOeHTcIU683lzd0cUA==} + + '@types/ramda@0.28.25': + resolution: {integrity: sha512-HrQNqQAGcITpn9HAJFamDxm7iZeeXiP/95pN5OMbNniDjzCCeOHbBKNGmUy8NRi0fhYS+/cXeo91MFC+06gbow==} + + aggregate-error@3.1.0: + resolution: {integrity: sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==} + engines: {node: '>=8'} + + ajv-keywords@5.1.0: + resolution: {integrity: sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==} + peerDependencies: + ajv: ^8.8.2 + + ajv@8.20.0: + resolution: {integrity: sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==} + + ansi-regex@5.0.1: + resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} + engines: {node: '>=8'} + + ansi-regex@6.2.2: + resolution: {integrity: sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==} + engines: {node: '>=12'} + + ansi-styles@4.3.0: + resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} + engines: {node: '>=8'} + + ansi-styles@6.2.3: + resolution: {integrity: sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==} + engines: {node: '>=12'} + + argparse@1.0.10: + resolution: {integrity: sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==} + + argparse@2.0.1: + resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} + + atob@2.1.2: + resolution: {integrity: sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg==} + engines: {node: '>= 4.5.0'} + hasBin: true + + balanced-match@1.0.2: + resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + + boolbase@1.0.0: + resolution: {integrity: sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==} + + brace-expansion@2.1.0: + resolution: {integrity: sha512-TN1kCZAgdgweJhWWpgKYrQaMNHcDULHkWwQIspdtjV4Y5aurRdZpjAqn6yX3FPqTA9ngHCc4hJxMAMgGfve85w==} + + callsites@3.1.0: + resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} + engines: {node: '>=6'} + + chalk@4.1.2: + resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} + engines: {node: '>=10'} + + cheerio-select@2.1.0: + resolution: {integrity: sha512-9v9kG0LvzrlcungtnJtpGNxY+fzECQKhK4EGJX2vByejiMX84MFNQw4UxPJl3bFbTMw+Dfs37XaIkCwTZfLh4g==} + + cheerio@1.2.0: + resolution: {integrity: sha512-WDrybc/gKFpTYQutKIK6UvfcuxijIZfMfXaYm8NMsPQxSYvf+13fXUJ4rztGGbJcBQ/GF55gvrZ0Bc0bj/mqvg==} + engines: {node: '>=20.18.1'} + + clean-stack@2.2.0: + resolution: {integrity: sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==} + engines: {node: '>=6'} + + color-convert@2.0.1: + resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} + engines: {node: '>=7.0.0'} + + color-name@1.1.4: + resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + + commander@12.1.0: + resolution: {integrity: sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==} + engines: {node: '>=18'} + + commander@7.2.0: + resolution: {integrity: sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==} + engines: {node: '>= 10'} + + commander@8.3.0: + resolution: {integrity: sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==} + engines: {node: '>= 12'} + + cross-spawn@7.0.6: + resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} + engines: {node: '>= 8'} + + css-select@5.2.2: + resolution: {integrity: sha512-TizTzUddG/xYLA3NXodFM0fSbNizXjOKhqiQQwvhlspadZokn1KDy0NZFS0wuEubIYAV5/c1/lAr0TaaFXEXzw==} + + css-tree@2.2.1: + resolution: {integrity: sha512-OA0mILzGc1kCOCSJerOeqDxDQ4HOh+G8NbOJFOTgOCzpw7fCBubk0fEyxp8AgOL/jvLgYA/uV0cMbe43ElF1JA==} + engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0, npm: '>=7.0.0'} + + css-tree@2.3.1: + resolution: {integrity: sha512-6Fv1DV/TYw//QF5IzQdqsNDjx/wc8TrMBZsqjL9eW01tWb7R7k/mq+/VXfJCl7SoD5emsJop9cOByJZfs8hYIw==} + engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0} + + css-what@6.2.2: + resolution: {integrity: sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA==} + engines: {node: '>= 6'} + + css@3.0.0: + resolution: {integrity: sha512-DG9pFfwOrzc+hawpmqX/dHYHJG+Bsdb0klhyi1sDneOgGOXy9wQIC8hzyVp1e4NRYDBdxcylvywPkkXCHAzTyQ==} + + cssfilter@0.0.10: + resolution: {integrity: sha512-FAaLDaplstoRsDR8XGYH51znUN0UY7nMc6Z9/fvE8EXGwvJE9hu7W2vHwx1+bd6gCYnln9nLbzxFTrcO9YQDZw==} + + csso@5.0.5: + resolution: {integrity: sha512-0LrrStPOdJj+SPCCrGhzryycLjwcgUSHBtxNA8aIDxf0GLsRh1cKYhB00Gd1lDOS4yGH69+SNn13+TWbVHETFQ==} + engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0, npm: '>=7.0.0'} + + debug@4.4.3: + resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + decode-uri-component@0.2.2: + resolution: {integrity: sha512-FqUYQ+8o158GyGTrMFJms9qh3CqTKvAqgqsTnkLI8sKu0028orqBhxNMFkFen0zGyg6epACD32pjVk58ngIErQ==} + engines: {node: '>=0.10'} + + deepmerge@4.3.1: + resolution: {integrity: sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==} + engines: {node: '>=0.10.0'} + + diacritics@1.3.0: + resolution: {integrity: sha512-wlwEkqcsaxvPJML+rDh/2iS824jbREk6DUMUKkEaSlxdYHeS43cClJtsWglvw2RfeXGm6ohKDqsXteJ5sP5enA==} + + dom-serializer@2.0.0: + resolution: {integrity: sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==} + + domelementtype@2.3.0: + resolution: {integrity: sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==} + + domhandler@5.0.3: + resolution: {integrity: sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==} + engines: {node: '>= 4'} + + domutils@3.2.2: + resolution: {integrity: sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==} + + eastasianwidth@0.2.0: + resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} + + emoji-regex@8.0.0: + resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} + + emoji-regex@9.2.2: + resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==} + + encoding-sniffer@0.2.1: + resolution: {integrity: sha512-5gvq20T6vfpekVtqrYQsSCFZ1wEg5+wW0/QaZMWkFr6BqD3NfKs0rLCx4rrVlSWJeZb5NBJgVLswK/w2MWU+Gw==} + + entities@3.0.1: + resolution: {integrity: sha512-WiyBqoomrwMdFG1e0kqvASYfnlb0lp8M5o5Fw2OFq1hNZxxcNk8Ik0Xm7LxzBhuidnZB/UtBqVCgUz3kBOP51Q==} + engines: {node: '>=0.12'} + + entities@4.5.0: + resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==} + engines: {node: '>=0.12'} + + entities@6.0.1: + resolution: {integrity: sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==} + engines: {node: '>=0.12'} + + entities@7.0.1: + resolution: {integrity: sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==} + engines: {node: '>=0.12'} + + escape-string-regexp@4.0.0: + resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} + engines: {node: '>=10'} + + esm@3.2.25: + resolution: {integrity: sha512-U1suiZ2oDVWv4zPO56S0NcR5QriEahGtdN2OR6FiOG4WJvcjBVFB0qI4+eKoWFH483PKGuLuu6V8Z4T5g63UVA==} + engines: {node: '>=6'} + + esprima@4.0.1: + resolution: {integrity: sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==} + engines: {node: '>=4'} + hasBin: true + + execa@9.6.1: + resolution: {integrity: sha512-9Be3ZoN4LmYR90tUoVu2te2BsbzHfhJyfEiAVfz7N5/zv+jduIfLrV2xdQXOHbaD6KgpGdO9PRPM1Y4Q9QkPkA==} + engines: {node: ^18.19.0 || >=20.5.0} + + fast-deep-equal@3.1.3: + resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} + + fast-uri@3.1.2: + resolution: {integrity: sha512-rVjf7ArG3LTk+FS6Yw81V1DLuZl1bRbNrev6Tmd/9RaroeeRRJhAt7jg/6YFxbvAQXUCavSoZhPPj6oOx+5KjQ==} + + fast-xml-parser@4.5.6: + resolution: {integrity: sha512-Yd4vkROfJf8AuJrDIVMVmYfULKmIJszVsMv7Vo71aocsKgFxpdlpSHXSaInvyYfgw2PRuObQSW2GFpVMUjxu9A==} + hasBin: true + + figures@6.1.0: + resolution: {integrity: sha512-d+l3qxjSesT4V7v2fh+QnmFnUWv9lSpjarhShNTgBOfA0ttejbQUAlHLitbjkoRiDulW0OPoQPYIGhIC8ohejg==} + engines: {node: '>=18'} + + foreground-child@3.3.1: + resolution: {integrity: sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==} + engines: {node: '>=14'} + + get-root-node-polyfill@1.0.0: + resolution: {integrity: sha512-AzucsG1DdepagLF8tkxfjUqn3cCQ63MgH/tBWwPSy0BIDt8iLFZYDwnTxA08d+zdgL8l2dkPdZDe+Qkd+RMl9Q==} + + get-stream@9.0.1: + resolution: {integrity: sha512-kVCxPF3vQM/N0B1PmoqVUqgHP+EeVjmZSQn+1oCRPxd2P21P2F19lIgbR3HBosbB1PUhOAoctJnfEn2GbN2eZA==} + engines: {node: '>=18'} + + github-slugger@1.5.0: + resolution: {integrity: sha512-wIh+gKBI9Nshz2o46B0B3f5k/W+WI9ZAv6y5Dn5WJ5SK1t0TnDimB4WE5rmTD05ZAIn8HALCZVmCsvj0w0v0lw==} + + glob@10.5.0: + resolution: {integrity: sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==} + deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me + hasBin: true + + has-flag@4.0.0: + resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} + engines: {node: '>=8'} + + he@1.2.0: + resolution: {integrity: sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==} + hasBin: true + + highlight.js@11.11.1: + resolution: {integrity: sha512-Xwwo44whKBVCYoliBQwaPvtd/2tYFkRQtXDWj1nackaV2JPXx3L0+Jvd8/qCJ2p+ML0/XVkJ2q+Mr+UVdpJK5w==} + engines: {node: '>=12.0.0'} + + htmlparser2@10.1.0: + resolution: {integrity: sha512-VTZkM9GWRAtEpveh7MSF6SjjrpNVNNVJfFup7xTY3UpFtm67foy9HDVXneLtFVt4pMz5kZtgNcvCniNFb1hlEQ==} + + human-signals@8.0.1: + resolution: {integrity: sha512-eKCa6bwnJhvxj14kZk5NCPc6Hb6BdsU9DZcOnmQKSnO1VKrfV0zCvtttPZUsBvjmNDn8rpcJfpwSYnHBjc95MQ==} + engines: {node: '>=18.18.0'} + + iconv-lite@0.6.3: + resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==} + engines: {node: '>=0.10.0'} + + indent-string@4.0.0: + resolution: {integrity: sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==} + engines: {node: '>=8'} + + inherits@2.0.4: + resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} + + is-fullwidth-code-point@3.0.0: + resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} + engines: {node: '>=8'} + + is-observable@2.1.0: + resolution: {integrity: sha512-DailKdLb0WU+xX8K5w7VsJhapwHLZ9jjmazqCJq4X12CTgqq73TKnbRcnSLuXYPOoLQgV5IrD7ePiX/h1vnkBw==} + engines: {node: '>=8'} + + is-plain-obj@4.1.0: + resolution: {integrity: sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==} + engines: {node: '>=12'} + + is-plain-object@5.0.0: + resolution: {integrity: sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==} + engines: {node: '>=0.10.0'} + + is-stream@4.0.1: + resolution: {integrity: sha512-Dnz92NInDqYckGEUJv689RbRiTSEHCQ7wOVeALbkOz999YpqT46yMRIGtSNl2iCL1waAZSx40+h59NV/EwzV/A==} + engines: {node: '>=18'} + + is-unicode-supported@2.1.0: + resolution: {integrity: sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ==} + engines: {node: '>=18'} + + isexe@2.0.0: + resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + + jackspeak@3.4.3: + resolution: {integrity: sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==} + + js-yaml@3.14.2: + resolution: {integrity: sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==} + hasBin: true + + js-yaml@4.1.0: + resolution: {integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==} + hasBin: true + + json-schema-traverse@1.0.0: + resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==} + + json-schema@0.4.0: + resolution: {integrity: sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==} + + katex@0.16.45: + resolution: {integrity: sha512-pQpZbdBu7wCTmQUh7ufPmLr0pFoObnGUoL/yhtwJDgmmQpbkg/0HSVti25Fu4rmd1oCR6NGWe9vqTWuWv3GcNA==} + hasBin: true + + linkify-it@4.0.1: + resolution: {integrity: sha512-C7bfi1UZmoj8+PQx22XyeXCuBlokoyWQL5pWSP+EI6nzRylyThouddufc2c1NDIcP9k5agmN9fLpA7VNJfIiqw==} + + lodash@4.17.21: + resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==} + + lodash@4.18.1: + resolution: {integrity: sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==} + + lru-cache@10.4.3: + resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==} + + markdown-it-attrs@4.3.1: + resolution: {integrity: sha512-/ko6cba+H6gdZ0DOw7BbNMZtfuJTRp9g/IrGIuz8lYc/EfnmWRpaR3CFPnNbVz0LDvF8Gf1hFGPqrQqq7De0rg==} + engines: {node: '>=6'} + peerDependencies: + markdown-it: '>= 9.0.0' + + markdown-it-deflist@2.1.0: + resolution: {integrity: sha512-3OuqoRUlSxJiuQYu0cWTLHNhhq2xtoSFqsZK8plANg91+RJQU1ziQ6lA2LzmFAEes18uPBsHZpcX6We5l76Nzg==} + + markdown-it-deflist@3.0.0: + resolution: {integrity: sha512-OxPmQ/keJZwbubjiQWOvKLHwpV2wZ5I3Smc81OjhwbfJsjdRrvD5aLTQxmZzzePeO0kbGzAo3Krk4QLgA8PWLg==} + + markdown-it-directive@2.0.6: + resolution: {integrity: sha512-yTjIan0I3LRMooa9uPNTs1DlcNVx/NqRjrEd3eipe286I8t3z+xzSSstrQ3OGDmKJkDSjg0rShO1t3uMN04U3Q==} + peerDependencies: + '@types/markdown-it': ^12.0.0 || ^13.0.0 + markdown-it: ^12.0.0 || ^13.0.0 + + markdown-it-meta@0.0.1: + resolution: {integrity: sha512-sCQG7mHJM3i4l6MztgzxE8t3aTQB5CSCO0wq8k6CEaqud40eryWXqPesq5AyztbYgwnxJcNIsmFsKDRkrl6Zuw==} + + markdown-it-sup@1.0.0: + resolution: {integrity: sha512-E32m0nV9iyhRR7CrhnzL5msqic7rL1juWre6TQNxsnApg7Uf+F97JOKxUijg5YwXz86lZ0mqfOnutoryyNdntQ==} + + markdown-it@13.0.2: + resolution: {integrity: sha512-FtwnEuuK+2yVU7goGn/MJ0WBZMM9ZPgU9spqlFs7/A/pDIUNSOQZhUgOqYCficIuR2QaFnrt8LHqBWsbTAoI5w==} + hasBin: true + + markdownlint-micromark@0.1.7: + resolution: {integrity: sha512-BbRPTC72fl5vlSKv37v/xIENSRDYL/7X/XoFzZ740FGEbs9vZerLrIkFRY0rv7slQKxDczToYuMmqQFN61fi4Q==} + engines: {node: '>=16'} + + markdownlint-rule-helpers@0.17.2: + resolution: {integrity: sha512-XaeoW2NYSlWxMCZM2B3H7YTG6nlaLfkEZWMBhr4hSPlq9MuY2sy83+Xr89jXOqZMZYjvi5nBCGoFh7hHoPKZmA==} + engines: {node: '>=12'} + + markdownlint@0.32.1: + resolution: {integrity: sha512-3sx9xpi4xlHlokGyHO9k0g3gJbNY4DI6oNEeEYq5gQ4W7UkiJ90VDAnuDl2U+yyXOUa6BX+0gf69ZlTUGIBp6A==} + engines: {node: '>=18'} + + mdn-data@2.0.28: + resolution: {integrity: sha512-aylIc7Z9y4yzHYAJNuESG3hfhC+0Ibp/MAMiaOZgNv4pmEdFyfZhhhny4MNiAfWdBQ1RQ2mfDWmM1x8SvGyp8g==} + + mdn-data@2.0.30: + resolution: {integrity: sha512-GaqWWShW4kv/G9IEucWScBx9G1/vsFZZJUO+tD26M8J8z3Kw5RDQjaoZe03YAClgeS/SWPOcb4nkFBTEi5DUEA==} + + mdurl@1.0.1: + resolution: {integrity: sha512-/sKlQJCBYVY9Ers9hqzKou4H6V5UWc/M59TH2dvkt+84itfnq7uFOMLpOiOS4ujvHP4etln18fmIxA5R5fll0g==} + + minimatch@9.0.9: + resolution: {integrity: sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==} + engines: {node: '>=16 || 14 >=14.17'} + + minipass@7.1.3: + resolution: {integrity: sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==} + engines: {node: '>=16 || 14 >=14.17'} + + ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + + nanoid@3.3.12: + resolution: {integrity: sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + + node-html-parser@6.1.13: + resolution: {integrity: sha512-qIsTMOY4C/dAa5Q5vsobRpOOvPfC4pB61UVW2uSwZNUp0QU/jCekTal1vMmbO0DgdHeLUJpv/ARmDqErVxA3Sg==} + + normalize-path@3.0.0: + resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} + engines: {node: '>=0.10.0'} + + npm-run-path@6.0.0: + resolution: {integrity: sha512-9qny7Z9DsQU8Ou39ERsPU4OZQlSTP47ShQzuKZ6PRXpYLtIFgl/DEBYEXKlvcEa+9tHVcK8CF81Y2V72qaZhWA==} + engines: {node: '>=18'} + + nth-check@2.1.1: + resolution: {integrity: sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==} + + observable-fns@0.6.1: + resolution: {integrity: sha512-9gRK4+sRWzeN6AOewNBTLXir7Zl/i3GB6Yl26gK4flxz8BXVpD3kt8amREmWNb0mxYOGDotvE5a4N+PtGGKdkg==} + + openapi-types@12.1.3: + resolution: {integrity: sha512-N4YtSYJqghVu4iek2ZUvcN/0aqH1kRDuNqzcycDxhOUpg7GdvLa2F3DgS6yBNhInhv2r/6I0Flkn7CqL8+nIcw==} + + p-map@4.0.0: + resolution: {integrity: sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==} + engines: {node: '>=10'} + + package-json-from-dist@1.0.1: + resolution: {integrity: sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==} + + parse-ms@4.0.0: + resolution: {integrity: sha512-TXfryirbmq34y8QBwgqCVLi+8oA3oWx2eAnSn62ITyEhEYaWRlVZ2DvMM9eZbMs/RfxPu/PK/aBLyGj4IrqMHw==} + engines: {node: '>=18'} + + parse-srcset@1.0.2: + resolution: {integrity: sha512-/2qh0lav6CmI15FzA3i/2Bzk2zCgQhGMkvhOhKNcBVQ1ldgpbfiNTVslmooUmWJcADi1f1kIeynbDRVzNlfR6Q==} + + parse5-htmlparser2-tree-adapter@7.1.0: + resolution: {integrity: sha512-ruw5xyKs6lrpo9x9rCZqZZnIUntICjQAd0Wsmp396Ul9lN/h+ifgVV1x1gZHi8euej6wTfpqX8j+BFQxF0NS/g==} + + parse5-parser-stream@7.1.2: + resolution: {integrity: sha512-JyeQc9iwFLn5TbvvqACIF/VXG6abODeB3Fwmv/TGdLk2LfbWkaySGY72at4+Ty7EkPZj854u4CrICqNk2qIbow==} + + parse5@7.3.0: + resolution: {integrity: sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==} + + path-key@3.1.1: + resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} + engines: {node: '>=8'} + + path-key@4.0.0: + resolution: {integrity: sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==} + engines: {node: '>=12'} + + path-scurry@1.11.1: + resolution: {integrity: sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==} + engines: {node: '>=16 || 14 >=14.18'} + + picocolors@1.1.1: + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + + postcss@8.5.14: + resolution: {integrity: sha512-SoSL4+OSEtR99LHFZQiJLkT59C5B1amGO1NzTwj7TT1qCUgUO6hxOvzkOYxD+vMrXBM3XJIKzokoERdqQq/Zmg==} + engines: {node: ^10 || ^12 || >=14} + + pretty-ms@9.3.0: + resolution: {integrity: sha512-gjVS5hOP+M3wMm5nmNOucbIrqudzs9v/57bWRHQWLYklXqoXKrVfYW2W9+glfGsqtPgpiz5WwyEEB+ksXIx3gQ==} + engines: {node: '>=18'} + + punycode@2.3.1: + resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} + engines: {node: '>=6'} + + quick-lru@5.1.1: + resolution: {integrity: sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==} + engines: {node: '>=10'} + + ramda@0.28.0: + resolution: {integrity: sha512-9QnLuG/kPVgWvMQ4aODhsBUFKOUmnbUnsSXACv+NCQZcHbeb+v8Lodp8OVxtRULN1/xOyYLLaL6npE6dMq5QTA==} + + require-from-string@2.0.2: + resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} + engines: {node: '>=0.10.0'} + + safer-buffer@2.1.2: + resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} + + sanitize-html@2.17.3: + resolution: {integrity: sha512-Kn4srCAo2+wZyvCNKCSyB2g8RQ8IkX/gQs2uqoSRNu5t9I2qvUyAVvRDiFUVAiX3N3PNuwStY0eNr+ooBHVWEg==} + + sax@1.6.0: + resolution: {integrity: sha512-6R3J5M4AcbtLUdZmRv2SygeVaM7IhrLXu9BmnOGmmACak8fiUtOsYNWUS4uK7upbmHIBbLBeFeI//477BKLBzA==} + engines: {node: '>=11.0.0'} + + shebang-command@2.0.0: + resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} + engines: {node: '>=8'} + + shebang-regex@3.0.0: + resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} + engines: {node: '>=8'} + + signal-exit@4.1.0: + resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} + engines: {node: '>=14'} + + slugify@1.6.6: + resolution: {integrity: sha512-h+z7HKHYXj6wJU+AnS/+IH8Uh9fdcX1Lrhg1/VMdf9PwoBQXFcXiAdsy2tSK0P6gKwJLXp02r90ahUCqHk9rrw==} + engines: {node: '>=8.0.0'} + + source-map-js@1.2.1: + resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} + engines: {node: '>=0.10.0'} + + source-map-resolve@0.6.0: + resolution: {integrity: sha512-KXBr9d/fO/bWo97NXsPIAW1bFSBOuCnjbNTBMO7N59hsv5i9yzRDfcYwwt0l04+VqnKC+EwzvJZIP/qkuMgR/w==} + deprecated: See https://github.com/lydell/source-map-resolve#deprecated + + source-map@0.6.1: + resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} + engines: {node: '>=0.10.0'} + + sprintf-js@1.0.3: + resolution: {integrity: sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==} + + string-width@4.2.3: + resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} + engines: {node: '>=8'} + + string-width@5.1.2: + resolution: {integrity: sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==} + engines: {node: '>=12'} + + strip-ansi@6.0.1: + resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} + engines: {node: '>=8'} + + strip-ansi@7.2.0: + resolution: {integrity: sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==} + engines: {node: '>=12'} + + strip-final-newline@4.0.0: + resolution: {integrity: sha512-aulFJcD6YK8V1G7iRB5tigAP4TsHBZZrOV8pjV++zdUwmeV8uzbY7yn6h9MswN62adStNZFuCIx4haBnRuMDaw==} + engines: {node: '>=18'} + + strnum@1.1.2: + resolution: {integrity: sha512-vrN+B7DBIoTTZjnPNewwhx6cBA/H+IS7rfW68n7XxC1y7uoiGQBxaKzqucGUgavX15dJgiGztLJ8vxuEzwqBdA==} + + supports-color@7.2.0: + resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} + engines: {node: '>=8'} + + svgo@3.3.3: + resolution: {integrity: sha512-+wn7I4p7YgJhHs38k2TNjy1vCfPIfLIJWR5MnCStsN8WuuTcBnRKcMHQLMM2ijxGZmDoZwNv8ipl5aTTen62ng==} + engines: {node: '>=14.0.0'} + hasBin: true + + tapable@2.3.3: + resolution: {integrity: sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==} + engines: {node: '>=6'} + + threads@1.7.0: + resolution: {integrity: sha512-Mx5NBSHX3sQYR6iI9VYbgHKBLisyB+xROCBGjjWm1O9wb9vfLxdaGtmT/KCjUqMsSNW6nERzCW3T6H43LqjDZQ==} + + tiny-worker@2.3.0: + resolution: {integrity: sha512-pJ70wq5EAqTAEl9IkGzA+fN0836rycEuz2Cn6yeZ6FRzlVS5IDOkFHpIoEsksPRQV34GDqXm65+OlnZqUSyK2g==} + + ts-dedent@2.2.0: + resolution: {integrity: sha512-q5W7tVM71e2xjHZTlgfTDoPF/SmqKG5hddq9SzR49CH2hayqRKJtQ4mtRlSxKaJlR/+9rEM+mnBHf7I2/BQcpQ==} + engines: {node: '>=6.10'} + + ts-toolbelt@6.15.5: + resolution: {integrity: sha512-FZIXf1ksVyLcfr7M317jbB67XFJhOO1YqdTcuGaq9q5jLUoTikukZ+98TPjKiP2jC5CgmYdWWYs0s2nLSU0/1A==} + + uc.micro@1.0.6: + resolution: {integrity: sha512-8Y75pvTYkLJW2hWQHXxoqRgV7qb9B+9vFEtidML+7koHUFapnVJAZ6cKs+Qjz5Aw3aZWHMC6u0wJE3At+nSGwA==} + + undici@7.25.0: + resolution: {integrity: sha512-xXnp4kTyor2Zq+J1FfPI6Eq3ew5h6Vl0F/8d9XU5zZQf1tX9s2Su1/3PiMmUANFULpmksxkClamIZcaUqryHsQ==} + engines: {node: '>=20.18.1'} + + unicorn-magic@0.3.0: + resolution: {integrity: sha512-+QBBXBCvifc56fsbuxZQ6Sic3wqqc3WWaqxs58gvJrcOuN83HGTCwz3oS5phzU9LthRNE9VrJCFCLUgHeeFnfA==} + engines: {node: '>=18'} + + uri-js@4.4.1: + resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} + + whatwg-encoding@3.1.1: + resolution: {integrity: sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==} + engines: {node: '>=18'} + deprecated: Use @exodus/bytes instead for a more spec-conformant and faster implementation + + whatwg-mimetype@4.0.0: + resolution: {integrity: sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==} + engines: {node: '>=18'} + + which@2.0.2: + resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} + engines: {node: '>= 8'} + hasBin: true + + wrap-ansi@7.0.0: + resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} + engines: {node: '>=10'} + + wrap-ansi@8.1.0: + resolution: {integrity: sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==} + engines: {node: '>=12'} + + xml-formatter@3.7.0: + resolution: {integrity: sha512-+8qTc3zv2UcJ1v9IsSIce37Dl4MQG14Cp7tWrwmy202UaI1wqRukw5QMX1JHsV+DX64yw77EgGsj2s5wGvuMbQ==} + engines: {node: '>= 16'} + + xml-parser-xo@4.1.5: + resolution: {integrity: sha512-TxyRxk9sTOUg3glxSIY6f0nfuqRll2OEF8TspLgh5mZkLuBgheCn3zClcDSGJ58TvNmiwyCCuat4UajPud/5Og==} + engines: {node: '>= 16'} + + yoctocolors@2.1.2: + resolution: {integrity: sha512-CzhO+pFNo8ajLM2d2IW/R93ipy99LWjtwblvC1RsoSUMZgyLbYFr221TnSNT7GjGdYui6P459mw9JH/g/zW2ug==} + engines: {node: '>=18'} + +snapshots: + + '@cospired/i18n-iso-languages@4.2.0': {} + + '@diplodoc/cli@4.60.0(@types/markdown-it@13.0.9)(markdown-it@13.0.2)': + dependencies: + '@diplodoc/client': 3.12.0 + '@diplodoc/liquid': 1.5.0 + '@diplodoc/translation': 1.7.21(@types/markdown-it@13.0.9)(highlight.js@11.11.1)(markdown-it@13.0.2) + chalk: 4.1.2 + commander: 12.1.0 + execa: 9.6.1 + glob: 10.5.0 + highlight.js: 11.11.1 + js-yaml: 4.1.0 + katex: 0.16.45 + lodash: 4.17.21 + normalize-path: 3.0.0 + p-map: 4.0.0 + tapable: 2.3.3 + threads: 1.7.0 + ts-dedent: 2.2.0 + transitivePeerDependencies: + - '@types/markdown-it' + - markdown-it + - react + - supports-color + + '@diplodoc/client@3.12.0': {} + + '@diplodoc/cut-extension@1.1.1(@types/markdown-it@13.0.9)(markdown-it@13.0.2)': + dependencies: + '@diplodoc/directive': 0.3.3(@types/markdown-it@13.0.9)(markdown-it@13.0.2) + '@diplodoc/utils': 2.2.2 + transitivePeerDependencies: + - '@types/markdown-it' + - markdown-it + - react + + '@diplodoc/directive@0.3.3(@types/markdown-it@13.0.9)(markdown-it@13.0.2)': + dependencies: + markdown-it-directive: 2.0.6(@types/markdown-it@13.0.9)(markdown-it@13.0.2) + transitivePeerDependencies: + - '@types/markdown-it' + - markdown-it + + '@diplodoc/file-extension@0.2.2(@types/markdown-it@13.0.9)(markdown-it@13.0.2)': + dependencies: + '@diplodoc/directive': 0.3.3(@types/markdown-it@13.0.9)(markdown-it@13.0.2) + markdown-it: 13.0.2 + transitivePeerDependencies: + - '@types/markdown-it' + + '@diplodoc/liquid@1.5.0': + dependencies: + chalk: 4.1.2 + js-yaml: 4.1.0 + + '@diplodoc/sentenizer@0.0.8': + dependencies: + '@types/ramda': 0.28.25 + ramda: 0.28.0 + + '@diplodoc/tabs-extension@3.10.0': {} + + '@diplodoc/transform@4.74.1(@types/markdown-it@13.0.9)(highlight.js@11.11.1)': + dependencies: + '@diplodoc/cut-extension': 1.1.1(@types/markdown-it@13.0.9)(markdown-it@13.0.2) + '@diplodoc/file-extension': 0.2.2(@types/markdown-it@13.0.9)(markdown-it@13.0.2) + '@diplodoc/tabs-extension': 3.10.0 + '@diplodoc/utils': 2.2.2 + chalk: 4.1.2 + cheerio: 1.2.0 + css: 3.0.0 + cssfilter: 0.0.10 + get-root-node-polyfill: 1.0.0 + github-slugger: 1.5.0 + js-yaml: 4.1.0 + lodash: 4.18.1 + markdown-it: 13.0.2 + markdown-it-attrs: 4.3.1(markdown-it@13.0.2) + markdown-it-deflist: 2.1.0 + markdown-it-sup: 1.0.0 + markdownlint: 0.32.1 + markdownlint-rule-helpers: 0.17.2 + quick-lru: 5.1.1 + sanitize-html: 2.17.3 + slugify: 1.6.6 + svgo: 3.3.3 + ts-dedent: 2.2.0 + optionalDependencies: + highlight.js: 11.11.1 + transitivePeerDependencies: + - '@types/markdown-it' + - react + + '@diplodoc/translation@1.7.21(@types/markdown-it@13.0.9)(highlight.js@11.11.1)(markdown-it@13.0.2)': + dependencies: + '@cospired/i18n-iso-languages': 4.2.0 + '@diplodoc/directive': 0.3.3(@types/markdown-it@13.0.9)(markdown-it@13.0.2) + '@diplodoc/sentenizer': 0.0.8 + '@diplodoc/transform': 4.74.1(@types/markdown-it@13.0.9)(highlight.js@11.11.1) + '@shellscape/i18n-iso-countries': 7.5.0-shellscape.v1 + ajv: 8.20.0 + ajv-keywords: 5.1.0(ajv@8.20.0) + cheerio: 1.2.0 + domhandler: 5.0.3 + fast-xml-parser: 4.5.6 + js-yaml: 4.1.0 + json-schema: 0.4.0 + lodash: 4.17.21 + markdown-it: 13.0.2 + markdown-it-deflist: 3.0.0 + markdown-it-meta: 0.0.1 + markdown-it-sup: 1.0.0 + node-html-parser: 6.1.13 + openapi-types: 12.1.3 + ts-dedent: 2.2.0 + uri-js: 4.4.1 + xml-formatter: 3.7.0 + xml-parser-xo: 4.1.5 + transitivePeerDependencies: + - '@types/markdown-it' + - highlight.js + - react + + '@diplodoc/utils@2.2.2': {} + + '@isaacs/cliui@8.0.2': + dependencies: + string-width: 5.1.2 + string-width-cjs: string-width@4.2.3 + strip-ansi: 7.2.0 + strip-ansi-cjs: strip-ansi@6.0.1 + wrap-ansi: 8.1.0 + wrap-ansi-cjs: wrap-ansi@7.0.0 + + '@pkgjs/parseargs@0.11.0': + optional: true + + '@sec-ant/readable-stream@0.4.1': {} + + '@shellscape/i18n-iso-countries@7.5.0-shellscape.v1': + dependencies: + diacritics: 1.3.0 + + '@sindresorhus/merge-streams@4.0.0': {} + + '@types/linkify-it@3.0.5': {} + + '@types/markdown-it@13.0.9': + dependencies: + '@types/linkify-it': 3.0.5 + '@types/mdurl': 1.0.5 + + '@types/mdurl@1.0.5': {} + + '@types/ramda@0.28.25': + dependencies: + ts-toolbelt: 6.15.5 + + aggregate-error@3.1.0: + dependencies: + clean-stack: 2.2.0 + indent-string: 4.0.0 + + ajv-keywords@5.1.0(ajv@8.20.0): + dependencies: + ajv: 8.20.0 + fast-deep-equal: 3.1.3 + + ajv@8.20.0: + dependencies: + fast-deep-equal: 3.1.3 + fast-uri: 3.1.2 + json-schema-traverse: 1.0.0 + require-from-string: 2.0.2 + + ansi-regex@5.0.1: {} + + ansi-regex@6.2.2: {} + + ansi-styles@4.3.0: + dependencies: + color-convert: 2.0.1 + + ansi-styles@6.2.3: {} + + argparse@1.0.10: + dependencies: + sprintf-js: 1.0.3 + + argparse@2.0.1: {} + + atob@2.1.2: {} + + balanced-match@1.0.2: {} + + boolbase@1.0.0: {} + + brace-expansion@2.1.0: + dependencies: + balanced-match: 1.0.2 + + callsites@3.1.0: {} + + chalk@4.1.2: + dependencies: + ansi-styles: 4.3.0 + supports-color: 7.2.0 + + cheerio-select@2.1.0: + dependencies: + boolbase: 1.0.0 + css-select: 5.2.2 + css-what: 6.2.2 + domelementtype: 2.3.0 + domhandler: 5.0.3 + domutils: 3.2.2 + + cheerio@1.2.0: + dependencies: + cheerio-select: 2.1.0 + dom-serializer: 2.0.0 + domhandler: 5.0.3 + domutils: 3.2.2 + encoding-sniffer: 0.2.1 + htmlparser2: 10.1.0 + parse5: 7.3.0 + parse5-htmlparser2-tree-adapter: 7.1.0 + parse5-parser-stream: 7.1.2 + undici: 7.25.0 + whatwg-mimetype: 4.0.0 + + clean-stack@2.2.0: {} + + color-convert@2.0.1: + dependencies: + color-name: 1.1.4 + + color-name@1.1.4: {} + + commander@12.1.0: {} + + commander@7.2.0: {} + + commander@8.3.0: {} + + cross-spawn@7.0.6: + dependencies: + path-key: 3.1.1 + shebang-command: 2.0.0 + which: 2.0.2 + + css-select@5.2.2: + dependencies: + boolbase: 1.0.0 + css-what: 6.2.2 + domhandler: 5.0.3 + domutils: 3.2.2 + nth-check: 2.1.1 + + css-tree@2.2.1: + dependencies: + mdn-data: 2.0.28 + source-map-js: 1.2.1 + + css-tree@2.3.1: + dependencies: + mdn-data: 2.0.30 + source-map-js: 1.2.1 + + css-what@6.2.2: {} + + css@3.0.0: + dependencies: + inherits: 2.0.4 + source-map: 0.6.1 + source-map-resolve: 0.6.0 + + cssfilter@0.0.10: {} + + csso@5.0.5: + dependencies: + css-tree: 2.2.1 + + debug@4.4.3: + dependencies: + ms: 2.1.3 + + decode-uri-component@0.2.2: {} + + deepmerge@4.3.1: {} + + diacritics@1.3.0: {} + + dom-serializer@2.0.0: + dependencies: + domelementtype: 2.3.0 + domhandler: 5.0.3 + entities: 4.5.0 + + domelementtype@2.3.0: {} + + domhandler@5.0.3: + dependencies: + domelementtype: 2.3.0 + + domutils@3.2.2: + dependencies: + dom-serializer: 2.0.0 + domelementtype: 2.3.0 + domhandler: 5.0.3 + + eastasianwidth@0.2.0: {} + + emoji-regex@8.0.0: {} + + emoji-regex@9.2.2: {} + + encoding-sniffer@0.2.1: + dependencies: + iconv-lite: 0.6.3 + whatwg-encoding: 3.1.1 + + entities@3.0.1: {} + + entities@4.5.0: {} + + entities@6.0.1: {} + + entities@7.0.1: {} + + escape-string-regexp@4.0.0: {} + + esm@3.2.25: + optional: true + + esprima@4.0.1: {} + + execa@9.6.1: + dependencies: + '@sindresorhus/merge-streams': 4.0.0 + cross-spawn: 7.0.6 + figures: 6.1.0 + get-stream: 9.0.1 + human-signals: 8.0.1 + is-plain-obj: 4.1.0 + is-stream: 4.0.1 + npm-run-path: 6.0.0 + pretty-ms: 9.3.0 + signal-exit: 4.1.0 + strip-final-newline: 4.0.0 + yoctocolors: 2.1.2 + + fast-deep-equal@3.1.3: {} + + fast-uri@3.1.2: {} + + fast-xml-parser@4.5.6: + dependencies: + strnum: 1.1.2 + + figures@6.1.0: + dependencies: + is-unicode-supported: 2.1.0 + + foreground-child@3.3.1: + dependencies: + cross-spawn: 7.0.6 + signal-exit: 4.1.0 + + get-root-node-polyfill@1.0.0: {} + + get-stream@9.0.1: + dependencies: + '@sec-ant/readable-stream': 0.4.1 + is-stream: 4.0.1 + + github-slugger@1.5.0: {} + + glob@10.5.0: + dependencies: + foreground-child: 3.3.1 + jackspeak: 3.4.3 + minimatch: 9.0.9 + minipass: 7.1.3 + package-json-from-dist: 1.0.1 + path-scurry: 1.11.1 + + has-flag@4.0.0: {} + + he@1.2.0: {} + + highlight.js@11.11.1: {} + + htmlparser2@10.1.0: + dependencies: + domelementtype: 2.3.0 + domhandler: 5.0.3 + domutils: 3.2.2 + entities: 7.0.1 + + human-signals@8.0.1: {} + + iconv-lite@0.6.3: + dependencies: + safer-buffer: 2.1.2 + + indent-string@4.0.0: {} + + inherits@2.0.4: {} + + is-fullwidth-code-point@3.0.0: {} + + is-observable@2.1.0: {} + + is-plain-obj@4.1.0: {} + + is-plain-object@5.0.0: {} + + is-stream@4.0.1: {} + + is-unicode-supported@2.1.0: {} + + isexe@2.0.0: {} + + jackspeak@3.4.3: + dependencies: + '@isaacs/cliui': 8.0.2 + optionalDependencies: + '@pkgjs/parseargs': 0.11.0 + + js-yaml@3.14.2: + dependencies: + argparse: 1.0.10 + esprima: 4.0.1 + + js-yaml@4.1.0: + dependencies: + argparse: 2.0.1 + + json-schema-traverse@1.0.0: {} + + json-schema@0.4.0: {} + + katex@0.16.45: + dependencies: + commander: 8.3.0 + + linkify-it@4.0.1: + dependencies: + uc.micro: 1.0.6 + + lodash@4.17.21: {} + + lodash@4.18.1: {} + + lru-cache@10.4.3: {} + + markdown-it-attrs@4.3.1(markdown-it@13.0.2): + dependencies: + markdown-it: 13.0.2 + + markdown-it-deflist@2.1.0: {} + + markdown-it-deflist@3.0.0: {} + + markdown-it-directive@2.0.6(@types/markdown-it@13.0.9)(markdown-it@13.0.2): + dependencies: + '@types/markdown-it': 13.0.9 + markdown-it: 13.0.2 + + markdown-it-meta@0.0.1: + dependencies: + js-yaml: 3.14.2 + + markdown-it-sup@1.0.0: {} + + markdown-it@13.0.2: + dependencies: + argparse: 2.0.1 + entities: 3.0.1 + linkify-it: 4.0.1 + mdurl: 1.0.1 + uc.micro: 1.0.6 + + markdownlint-micromark@0.1.7: {} + + markdownlint-rule-helpers@0.17.2: {} + + markdownlint@0.32.1: + dependencies: + markdown-it: 13.0.2 + markdownlint-micromark: 0.1.7 + + mdn-data@2.0.28: {} + + mdn-data@2.0.30: {} + + mdurl@1.0.1: {} + + minimatch@9.0.9: + dependencies: + brace-expansion: 2.1.0 + + minipass@7.1.3: {} + + ms@2.1.3: {} + + nanoid@3.3.12: {} + + node-html-parser@6.1.13: + dependencies: + css-select: 5.2.2 + he: 1.2.0 + + normalize-path@3.0.0: {} + + npm-run-path@6.0.0: + dependencies: + path-key: 4.0.0 + unicorn-magic: 0.3.0 + + nth-check@2.1.1: + dependencies: + boolbase: 1.0.0 + + observable-fns@0.6.1: {} + + openapi-types@12.1.3: {} + + p-map@4.0.0: + dependencies: + aggregate-error: 3.1.0 + + package-json-from-dist@1.0.1: {} + + parse-ms@4.0.0: {} + + parse-srcset@1.0.2: {} + + parse5-htmlparser2-tree-adapter@7.1.0: + dependencies: + domhandler: 5.0.3 + parse5: 7.3.0 + + parse5-parser-stream@7.1.2: + dependencies: + parse5: 7.3.0 + + parse5@7.3.0: + dependencies: + entities: 6.0.1 + + path-key@3.1.1: {} + + path-key@4.0.0: {} + + path-scurry@1.11.1: + dependencies: + lru-cache: 10.4.3 + minipass: 7.1.3 + + picocolors@1.1.1: {} + + postcss@8.5.14: + dependencies: + nanoid: 3.3.12 + picocolors: 1.1.1 + source-map-js: 1.2.1 + + pretty-ms@9.3.0: + dependencies: + parse-ms: 4.0.0 + + punycode@2.3.1: {} + + quick-lru@5.1.1: {} + + ramda@0.28.0: {} + + require-from-string@2.0.2: {} + + safer-buffer@2.1.2: {} + + sanitize-html@2.17.3: + dependencies: + deepmerge: 4.3.1 + escape-string-regexp: 4.0.0 + htmlparser2: 10.1.0 + is-plain-object: 5.0.0 + parse-srcset: 1.0.2 + postcss: 8.5.14 + + sax@1.6.0: {} + + shebang-command@2.0.0: + dependencies: + shebang-regex: 3.0.0 + + shebang-regex@3.0.0: {} + + signal-exit@4.1.0: {} + + slugify@1.6.6: {} + + source-map-js@1.2.1: {} + + source-map-resolve@0.6.0: + dependencies: + atob: 2.1.2 + decode-uri-component: 0.2.2 + + source-map@0.6.1: {} + + sprintf-js@1.0.3: {} + + string-width@4.2.3: + dependencies: + emoji-regex: 8.0.0 + is-fullwidth-code-point: 3.0.0 + strip-ansi: 6.0.1 + + string-width@5.1.2: + dependencies: + eastasianwidth: 0.2.0 + emoji-regex: 9.2.2 + strip-ansi: 7.2.0 + + strip-ansi@6.0.1: + dependencies: + ansi-regex: 5.0.1 + + strip-ansi@7.2.0: + dependencies: + ansi-regex: 6.2.2 + + strip-final-newline@4.0.0: {} + + strnum@1.1.2: {} + + supports-color@7.2.0: + dependencies: + has-flag: 4.0.0 + + svgo@3.3.3: + dependencies: + commander: 7.2.0 + css-select: 5.2.2 + css-tree: 2.3.1 + css-what: 6.2.2 + csso: 5.0.5 + picocolors: 1.1.1 + sax: 1.6.0 + + tapable@2.3.3: {} + + threads@1.7.0: + dependencies: + callsites: 3.1.0 + debug: 4.4.3 + is-observable: 2.1.0 + observable-fns: 0.6.1 + optionalDependencies: + tiny-worker: 2.3.0 + transitivePeerDependencies: + - supports-color + + tiny-worker@2.3.0: + dependencies: + esm: 3.2.25 + optional: true + + ts-dedent@2.2.0: {} + + ts-toolbelt@6.15.5: {} + + uc.micro@1.0.6: {} + + undici@7.25.0: {} + + unicorn-magic@0.3.0: {} + + uri-js@4.4.1: + dependencies: + punycode: 2.3.1 + + whatwg-encoding@3.1.1: + dependencies: + iconv-lite: 0.6.3 + + whatwg-mimetype@4.0.0: {} + + which@2.0.2: + dependencies: + isexe: 2.0.0 + + wrap-ansi@7.0.0: + dependencies: + ansi-styles: 4.3.0 + string-width: 4.2.3 + strip-ansi: 6.0.1 + + wrap-ansi@8.1.0: + dependencies: + ansi-styles: 6.2.3 + string-width: 5.1.2 + strip-ansi: 7.2.0 + + xml-formatter@3.7.0: + dependencies: + xml-parser-xo: 4.1.5 + + xml-parser-xo@4.1.5: {} + + yoctocolors@2.1.2: {} diff --git a/docs/toc.yaml b/docs/toc.yaml new file mode 100644 index 0000000..15cd549 --- /dev/null +++ b/docs/toc.yaml @@ -0,0 +1,56 @@ +title: Ordinis НСИ ЦУОД ОДХ +href: index.md + +items: + - name: Введение + href: index.md + + - name: Интеграция по API + items: + - name: Обзор + href: integration/api/index.md + - name: Авторизация + href: integration/api/auth.md + - name: Read-side endpoints + href: integration/api/read-endpoints.md + - name: События (Kafka outbox) + href: integration/api/events.md + - name: Webhooks + href: integration/api/webhooks.md + - name: Bitemporal модель + href: integration/api/bitemporal.md + - name: Cross-references (FK) + href: integration/api/x-references.md + - name: Кэширование + href: integration/api/caching.md + - name: Ошибки и rate limits + href: integration/api/errors.md + - name: Best practices + href: integration/api/best-practices.md + - name: Bundle справочников cuod + href: integration/api/bundle-cuod.md + + - name: Прикладные сценарии + items: + - name: Альтум — заказ съёмки + href: integration/altum-imaging-order.md + + - name: Operations + items: + - name: Runbook (индекс) + href: ops/README.md + - name: SLO + href: ops/slo.md + - name: Outbox DLQ + href: ops/outbox-dlq.md + - name: Database migrations + href: ops/db-migrations.md + - name: Vault unseal + href: ops/vault-unseal.md + - name: Kafka restart + href: ops/kafka-restart.md + + - name: Архитектура (legacy) + items: + - name: API integration (старая страница, см. /integration/api/) + href: tech/api-integration.md