feat(data-plane): add provider contracts and ontology delivery

This commit is contained in:
Codex
2026-07-16 02:23:34 +03:00
parent e527812826
commit 569b8762e6
84 changed files with 11170 additions and 70 deletions
+143
View File
@@ -0,0 +1,143 @@
# ADR: канон External Provider Data Plane
Статус: **superseded**.
Дата: 2026-07-13.
Владелец решения: NODE.DC Platform.
> Заменено 2026-07-14 документом
> [`ADR_L2_OWNED_EXTERNAL_CONNECTORS.md`](ADR_L2_OWNED_EXTERNAL_CONNECTORS.md).
> Этот черновик неверно помещал provider adapter, normalisation и collection
> policy в `services/<provider>-gateway`. Новое правило: adapter принадлежит
> изолированному L2 workflow; Platform Data Plane остаётся provider-neutral.
## Контекст
Клиент может подключить к NODE.DC любой внешний продукт: телеметрию, ERP,
роботов, энергетику, BIM-систему или иной источник данных. Gelios Pro для
Gelios — первый конкретный поставщик для проверки канона, а не исключительная
архитектурная ветка. Нельзя превращать Engine workflow, Ontology Core или общую БД Platform
в место, куда попадают токены, raw payloads и частная логика каждого API.
Нужна повторяемая форма, в которой новый provider добавляется как отдельный
adapter, но получает общие правила scope, секретов, collection, хранения,
read-model, аудита и безопасной публикации в NDC.
## Решение
1. В `platform/packages/external-provider-contract` живёт общий versioned
контракт интеграции. Это не runtime и не база данных. Он задаёт форму
`provider`, `connection`, `capability catalog`, `credential reference`,
`access scope`, `field policy`, `collection profile`, `retention policy`,
`read model` и красный command-domain.
2. Каждый provider получает самостоятельный app-owned adapter в
`platform/services/<provider>-gateway`. Первый экземпляр —
`platform/services/gelios-gateway`. Adapter владеет intake contract, rate
budget, normalisation, collection policy, storage и своим internal
read/realtime API. Сам provider secret остаётся в Engine Credentials и
доступен только назначенному защищённому execution workflow.
3. Один provider service может обслуживать много клиентов. Каждая строка,
cursor, audit-event и read-model обязана иметь `tenant_id` и
`connection_id`; видимость provider account сама по себе не является
продуктовым scope. Для клиента с отдельными требованиями изоляции допустим
отдельный deployment/database profile без изменения контракта.
4. База принадлежит adapter-сервису, а не Ontology Core, Engine, Tasker или
общему Platform Postgres. Для пространственно-временного Gelios-кейса
базой служит PostgreSQL 16 + TimescaleDB + PostGIS (`gelios-postgres`).
Другой provider может выбрать иной storage engine только через явный ADR,
сохранив внешний контракт.
5. Ontology Core хранит только provider-neutral и provider-specific смыслы,
связи, правила и контракты. Он не хранит credentials, runtime telemetry,
customer raw payloads или renderer objects. Enforcement остаётся в
gateway/adapters. Engine Credentials хранит provider secret в границе
специально назначенного execution workflow; значение не сериализуется в
граф, ontology, логи, read-model или UI.
6. Engine L2 Collector использует credential reference и получает разрешённые
данные поставщика. Он передаёт в adapter только аутентифицированный нормализованный
intake payload. Остальные L2 workflow получают исключительно scoped
internal API/event contract и не читают gateway DB напрямую.
## Каноническая форма нового подключения
```text
Client / tenant
-> Engine Credential + protected Collector workflow
-> provider connection instance
-> provider adapter (safe intake policy + normalizer)
-> provider-owned storage and projections
-> internal read/realtime contract
-> L2 workflow / approved interface binding
-> renderer adapter
```
Каждый новый provider добавляет только свой adapter package, capability
catalog, schema mappings, scrubbed fixtures и domain ontology package. Он не
добавляет отдельную схему доступа к Engine/Studio и не создаёт прямой путь из
browser в provider API.
## Полнота данных без неконтролируемого объёма
«Предусмотреть все данные» означает каталогизировать каждую provider
capability и поле, а не опрашивать весь account на максимальной частоте.
Collection profile явно решает, какие safe-read capabilities, поля, scope и
частота включены в конкретной connection instance.
| Слой | Что хранится | Режим |
| --- | --- | --- |
| Capability catalog | documented endpoint/read-field/command capability и его риск | versioned source + ontology |
| Inventory/configuration | units, devices, groups, sensors, custom definitions | медленный reconcile |
| Current projection | последняя разрешённая позиция, состояние и display fields | idempotent upsert |
| Event history | нормализованные события и approved measurements | append-only, partitioned |
| Raw envelope | полный safe-read ответ с provenance и hash | restricted cold layer, retention-bound |
| Aggregates/features | rollups и признаки для аналитики/предиктива | derived, replaceable |
Raw envelope не выдаётся UI и не становится таблицей «всё в JSON навсегда».
Вначале он может быть compressed/partitioned storage с метаданными в БД; при
реальном объёме переносится в object storage, а PostgreSQL хранит immutable
index, hash, policy и ссылку. Retention, raw depth и downsampling утверждаются
после замера сообщений/сек, размера payload, требуемой истории, RPO/RTO и
стоимости. Так инженер может запросить ранее не показанное поле из
каталога/архива, не раздувая горячую read-модель.
## Realtime и интерфейс
Частота provider collection, обновления `current projection` и выдачи в UI —
три разные настройки. Например, map consumer может получать выбранную
read-модель раз в 3 секунды, но это не даёт ему права опрашивать Gelios раз в
3 секунды или создавать отдельный polling loop на каждого зрителя.
Gateway сначала обновляет одну current projection и публикует change event.
L2/Map binding затем может sampling/throttle этот поток по утверждённой
настройке интерфейса. Источник истины для live state — gateway storage, не
долгоживущий workflow и не Cesium session.
## Commands: моделируются, но не подключаются
Command templates, параметры, delivery states и audit входят в capability
catalog и ontology полностью. Read adapter не содержит send route и не
использует command capability. В будущем command execution создаётся только
как отдельный `provider-command-gateway`/red-domain deployment с явным
человеческим подтверждением, role/scope check, idempotency, audit и отдельным
security review. До этого команда не может быть отправлена из collector, L2,
Map или AI Workspace.
## Обязательные артефакты каждого adapter
- `provider manifest`: provider id, adapter version, auth modes, rate limits;
- capability and field catalog: read/write classification, source evidence,
pagination and error semantics;
- connection profile: tenant, secret reference, approved scope, field policy,
collection and retention profile;
- normalised contract and migrations; scrubbed fixtures and contract tests;
- health/metrics/audit without secrets or raw personal data;
- ontology package with stable subjects, relations and guardrails;
- internal read/realtime API contract; no browser/provider bypass.
## Не решено этим ADR
- конкретный deployment topology и HA/PITR target для каждого volume;
- выбор object storage после real-volume measurement;
- L2 stream execution contract и Module Studio binding implementation;
- параметры first Gelios collection profile и owner-approved connection scope.
Gelios-specific применение этого решения описано в
`docs/ADR_GELIOS_DATA_PLANE.md`.
+107
View File
@@ -0,0 +1,107 @@
# ADR: Gelios adapter в External Provider Data Plane
Статус: **superseded**.
Дата: 2026-07-13.
Владелец решения: NODE.DC Platform.
> Заменено 2026-07-14 документом
> [`ADR_L2_OWNED_EXTERNAL_CONNECTORS.md`](ADR_L2_OWNED_EXTERNAL_CONNECTORS.md).
> Gelios остаётся domain/ontology примером, но не получает отдельный
> provider-owned gateway: fetch, mapping и collection policy живут в его L2
> connector instance; Platform предоставляет нейтральный Data Plane.
## Контекст
Gelios Pro поставляет непрерывную телеметрию, конфигурацию устройств и пространственные данные. В текущем доступе подтверждены 107 видимых units, из них 105 с `lastMsg`; окончательный connection scope должен быть зафиксирован allowlist-ом владельца. Это не данные Ontology Core и не данные Tasker. Они требуют отдельного контура для текущего состояния, истории, геозапросов, аналитики и будущих прогнозов.
Gelios является первым provider adapter, который подчиняется общему
`docs/ADR_EXTERNAL_PROVIDER_DATA_PLANE.md`: connection instance хранит
connection scope/policy, Engine Credentials владеет provider secret, gateway
владеет storage/read-моделью, а ontology описывает значения, но не runtime
data.
Физические trike-команды существуют в домене, но **не входят в ingestion или тестирование**. Для них позднее потребуется отдельный, ручной и аудируемый контур.
## Решение
1. Создать отдельный сервис `platform/services/gelios-gateway` как storage/read gateway:
- не хранит и не получает provider token;
- принимает только аутентифицированный safe-read intake от защищённого Engine L2 Collector workflow;
- применяет allowlist и field policy до записи;
- нормализует ответ в сущности пакета `gelios`;
- публикует read-модель для Engine L2 workflow и Map View.
2. В Engine появляется отдельный Gelios Credential и NDC Agent L2 collection profile:
- credential скрывает access/refresh pair и его lifecycle;
- credential привязан только к намеренно пошаренному Collector workflow;
- Collector содержит allowlist read-capabilities и не имеет command transport;
- ни секрет, ни provider response без нормализации не передаются в ontology, UI, Gateway или другие workflow.
3. Выделить сервису собственную БД `gelios-postgres`, не деля её с Tasker, Authentik, Notification Core или Ontology Core. В будущей multi-tenant форме эта БД обслуживает несколько Gelios connection instances, но все records разделены `tenant_id` и `connection_id`.
4. Базовый движок: PostgreSQL 16 с расширениями TimescaleDB и PostGIS.
- Timescale hypertable хранит временные ряды и автоматически делит их по времени.
- PostGIS хранит нормализованные точки и геометрии зон, а не renderer-объекты Cesium.
- Данный выбор покрывает транзакционную конфигурацию, realtime upsert, исторические запросы, SQL-аналитику и географию одним контуром.
5. **Не** использовать RabbitMQ Tasker как общую шину Gelios. Если измерения покажут, что прямой writer или число независимых потребителей не справляются, добавить в Gelios-контур NATS JetStream с durable pull consumers. Он даст replay, acknowledgement и контролируемое удержание сообщений.
6. Engine level-2 Collector — единственная точка provider access; остальные workflow являются потребителями read-модели. Ontology Core остаётся только словарём и контрактами.
## Целевой поток
```text
Gelios REST (safe read only)
-> NDC Agent L2 collection profile + protected credential
-> authenticated normalized intake
-> Gelios Gateway: scope -> field policy -> normalizer
-> gelios-postgres: current state + immutable telemetry history
-> [при необходимости] NATS JetStream
-> `fleet.positions.current.v1` read API / realtime subscription
-> NDC workflow level 2
-> Map View semantic binding
-> Cesium renderer adapter
```
Ни один шаг не получает права отправить команду устройству. Красный command-domain находится вне этого потока; metadata каталога команд сохраняется, но send transport не создаётся.
## Модель хранения v0
| Слой | Назначение | Минимальные записи |
| --- | --- | --- |
| Контроль | граница и повторяемость сбора | `access_scope`, `collection_run`, `ingestion_cursor`, endpoint/response metrics |
| Каталог | стабильные сущности и их конфигурация | `unit`, `unit_group`, `tracker_device`, sensor/maintenance/custom-field definitions |
| Current state | одна актуальная запись на unit для карты и интерфейса | `unit_current`, `position_fix`, approved operational status |
| History | неизменяемые события с временем наблюдения и получения | `telemetry_snapshot`, selective `sensor_reading`, restricted raw payload reference |
| Spatial | геометрии для запросов, не Cesium graphics | point/track/geozone with SRID 4326 |
| Analytics | роллапы и признаки, не запросы по всему raw | hourly/daily aggregates, feature sets, model runs |
| Audit | попытки, ошибки, политика, будущие команды | collection audit; separate command audit later |
`unit_current` обновляется idempotently по `unitSubjectId`. История записывается append-only с ключом дедупликации, включающим provider unit id, observed time и fingerprint сообщения. Все временные таблицы имеют `observed_at` и `received_at`: задержка поставщика не должна переписывать фактическое время на карте.
## Индексы и жизненный цикл
- Основной путь истории: `(unit_subject_id, observed_at DESC)`.
- Пространственный индекс только для нормализованной geography/geometry; рендер-кэши в БД не храним.
- Сырые `params`/raw messages — restricted, отдельно от публичной Studio read-model.
- Политики retention, downsampling и резервного копирования должны быть утверждены до запуска history backfill. Они зависят от фактических msg/s, размера payload, нужной глубины истории, RPO/RTO и стоимости хранения.
- Для предиктива держать recent/raw слой и отдельные часовые/дневные агрегаты. Timescale continuous aggregates позволяют сохранять длительную агрегированную историю после сокращения raw при корректно согласованных refresh и retention политиках.
## Нулевая итерация без лишней инфраструктуры
1. Зафиксировать owner-approved allowlist и видимые поля.
2. Сделать только safe-read Engine Collector с ограничением по scope, rate limit, paging и cursor.
3. В течение согласованного окна измерить: сообщений/сек, размер ответа, lag, дубликаты, задержку записи и нагрузку запросов карты.
4. На фактах включить Timescale hypertables, PostGIS и retention policy; после этого решить, нужен ли JetStream сразу.
5. Подать только `fleet.positions.current.v1`/approved current position в Map View. Историю и raw не отдавать в renderer напрямую.
## Что не решено этим ADR
- окончательное правило connection scope;
- частота polling/возможность provider push;
- сроки хранения raw, нормализованной истории и агрегатов;
- RPO/RTO, репликация и production backup plan;
- допуск к ручному command gateway. До отдельного решения отправка команд запрещена.
## Обоснование и источники
- [Timescale hypertables](https://docs.timescale.com/use-timescale/latest/hypertables/) — временные таблицы PostgreSQL автоматически партиционируются по времени.
- [Timescale self-hosted installation](https://docs.timescale.com/self-hosted/latest/install/) — расширение разворачивается как self-hosted PostgreSQL-контур; production требует backup/PITR и HA-плана.
- [Retention и continuous aggregates](https://docs.timescale.com/use-timescale/latest/data-retention/data-retention-with-continuous-aggregates/) — raw и агрегаты требуют согласованных lifecycle-политик.
- [PostGIS](https://postgis.net/docs/en/) — PostgreSQL-расширение для spatial types и GiST R-tree индексов.
- [NATS JetStream consumers](https://docs.nats.io/nats-concepts/jetstream/consumers) — durable consumers дают acknowledgement, повторную доставку и recovery; рекомендуются pull consumers для новых масштабируемых обработчиков.
+274
View File
@@ -0,0 +1,274 @@
# ADR: L2-owned external connectors and provider-neutral Data Plane
Статус: **accepted**.
Дата: 2026-07-14.
Владелец решения: NODE.DC Platform.
## Контекст
NODE.DC — платформа разработки. Подключение внешнего API не должно создавать
ещё один provider-specific сервис, в котором навсегда зашиты логика клиента,
нормализация и правила отображения. Иначе каждый новый account или новый тип
объекта превращается в изменение Platform runtime.
Нужна одна повторяемая форма: Platform даёт безопасные границы, контракт
хранения и выдачи данных; изолированный workflow NDC Agent L2 является
адаптером конкретного API и остаётся редактируемым через предоставленный
Codex/Engine MCP.
Это решение заменяет provider-adapter часть
`ADR_EXTERNAL_PROVIDER_DATA_PLANE.md` и все provider-specific runtime
предписания `ADR_GELIOS_DATA_PLANE.md`. Они сохраняются как исторические
черновики, но не являются каноном для новой реализации.
## Решение
### 1. Адаптер внешнего API принадлежит L2
Один изолированный L2 workflow представляет одно connection instance и
является единственным местом для:
- обращения к API поставщика через ссылку на credential из Engine;
- выбора read-capability, cursor/pagination, rate limit, retry и расписания;
- получения raw envelope, разбиения большого ответа на native batch-операции;
- семантического mapping к версии ontology и формирования canonical facts;
- idempotency key, watermark и lifecycle источника;
- deploy/run/execution/trace через Engine MCP.
L2 не хранит значение provider secret или writer token: он использует только
Engine-bound credential references. Plaintext writer token выдаётся trusted
provisioner ровно один раз при create/rotate binding и затем хранится только в
Engine credential store, привязанном к этому L2; External Data Plane хранит
лишь его hash. Provisioner читает отдельный runner-owned secret file, который
read-only монтируется только в EDP и позднее в dedicated Engine provisioner,
работающий под выделенным UID/GID `11006`; это не shared `.env`, не
`NODEDC_INTERNAL_ACCESS_TOKEN` и не provider credential. До deployment этого
provisioner `EXTERNAL_DATA_PLANE_PROVISIONING_ENABLED=false`, поэтому issuance
routes fail closed. Token не попадает в L2 graph/profile, contract artifact,
execution log/trace, raw envelope, Foundry или UI. Command/write-capabilities
не получают transport route в read workflow.
Для новой учётной записи создаётся новый instance/profile этого workflow с
другой credential reference и connection configuration. Это не требует
изменения Platform source. Если поставщик раскрывает новый объект или поле,
сначала расширяется ontology/capability contract, затем изменяется именно
соответствующий L2 workflow.
### 2. Platform Data Plane нейтрален к provider и домену
Platform предоставляет один versioned **External Data Plane**. Он принимает
batch по внутреннему аутентифицированному контракту, хранит raw envelope с
retention/provenance, canonical facts, current/history projections и отдаёт
scoped data products/realtime updates. В нём запрещены:
- ветвления по названию provider, customer, account, vehicle или renderer;
- provider field mapping, unit allowlist, business filtering и visual rules;
- provider token, прямой browser-to-provider доступ и command transport.
Его canonical write-форма, которую Data Plane валидирует и сохраняет после
авторизации, содержит только нейтральные элементы:
```text
source: providerId + tenantId + connectionId
contract: contractVersion + ontologyRevision + dataProductId
batch: runId + sequence + idempotencyKey + receivedAt
raw: optional restricted ref + contentType + hash + retention policy
facts: stable sourceId + semanticType + observedAt + attributes + geometry?
```
Для нового L2 writer-а это не wire-форма. Он вызывает
`POST /internal/data-plane/v1/data-products/:dataProductId/publish` с bearer
writer token и формой `nodedc.data-product.publish/v1`, в которой вообще нет
`source`, contract metadata, transport или persistence policy. Любой caller
scope и scope headers запрещены.
External Data Plane разрешает token в собственный immutable writer binding
`(tenantId, connectionId, providerId, allowedDataProductIds, expiresAt)`,
проверяет active/non-expired binding, совпадение `providerId` и разрешение
`dataProductId`, затем сам materializes canonical `source` с tenant/connection
и только после этого валидирует и сохраняет batch. Присланный caller scope
отклоняется, а не merge/override-ится. Изменение tenant, connection, provider,
allowed data products или срока требует нового binding; для существующего
binding допустимы только token rotation и revoke.
Выпуск, rotation и revoke binding принимаются только от dedicated provisioning
principal с runner-owned secret file; он не монтируется в shared `.env` или L2.
Shared legacy bearer не может создавать writer capability.
Идентификатор источника стабилен в пределах `(providerId, connectionId,
sourceId)`. Отсутствие объекта в очередном ответе помечается lifecycle-state
или временем последнего наблюдения; запись и её история не удаляются.
### 3. Граница данных и интерфейса
```text
Provider API (safe read)
-> Engine provider-credential reference
-> L2 connector instance (adapter + mapping + batches)
-> Engine writer-credential reference
-> External Data Plane Data Product publish (scope materialized server-side)
-> External Data Plane (generic persistence + projections)
-> scoped data product / realtime stream
-> Foundry binding / user interface
```
Ontology Core описывает semantic types, связи, capability catalog и mapping
versions, но не хранит runtime payloads или секреты. Foundry получает только
approved data product и решает presentation: pins, visibility, layers and
filters. Оно не получает provider endpoint или credential.
### 4. Полнота без entity allowlist
Collection profile выбирает разрешённые **read-capabilities**, а не список
конкретных объектов. Если выбранный read endpoint возвращает все доступные
источнику сущности, L2 передаёт все валидные элементы. Новые сущности
добавляются автоматически; скрытие/отображение — задача data product/UI, а не
сбора. Технические лимиты существуют только как размер batch, backpressure,
quota и защита от повреждённого ответа, но не как бизнес-фильтр по ID.
### 5. Масштабирование и native NDC Agent nodes
L2 не запускает отдельный execution на каждую сущность. Он использует native
nodes NDC Agent: HTTP Request, Split Out, Split In Batches/Loop Over Items,
Aggregate, Postgres (когда нужен private workflow state), Set/If/Merge,
Schedule Trigger and Respond to Webhook. Code node допустим только как малый
преобразователь boundary-shape, когда эквивалентной native node нет; он не
становится скрытым сервисом или provider database.
Shared telemetry/history не записывается L2 напрямую в физические таблицы
Platform Postgres: это создало бы coupling к schema. Direct Postgres допустим
для private state конкретного workflow или отдельной project-owned базы.
Общий контур использует только External Data Plane contract и bulk batches.
### 5.1. Каталог custom nodes NODE.DC
Встроенные узлы runtime сохраняют свои штатные названия (`HTTP Request`,
`Schedule Trigger`, `Loop Over Items` и т. п.). Любой узел, код которого
принадлежит NODE.DC, обязан одновременно выполнять три условия:
- видимое имя начинается с `NDC `;
- runtime type принадлежит package namespace `n8n-nodes-ndc.*`;
- package-level test отклоняет публикацию узла без этого префикса и namespace.
Первый канонический набор состоит из:
- `NDC Data Product Publish` — runtime write boundary L2 → Data Plane;
- `NDC Data Product Read` — scoped current snapshot read по reader grant;
- `NDC Foundry Binding` — deploy/control-plane связь Data Product с
`Application → Page → typed slot`, но не транспорт каждого realtime tick.
`NDC Foundry Output` не используется как техническое имя: оно ошибочно
подразумевает прямой transport L2 → renderer. Provider-specific adapter,
mapping и collection profile остаются логикой конкретного L2 workflow на
штатных nodes; они не оформляются как custom NODE.DC nodes и не добавляют
provider branch в Data Plane или Foundry.
`NDC Foundry Binding` отправляет только
`nodedc.foundry.binding-upsert/v1`. Её постоянный L2 credential — отдельный
revocable `ndc_fndbg_*` workload grant с allowlist точных
`Application → Page → Binding → Slot → Data Product` targets. Короткоживущая
`fnd1.*` capability интерактивного Foundry MCP, browser session, EDP
writer/reader grant и `NODEDC_INTERNAL_ACCESS_TOKEN` для этой ноды запрещены.
Runtime node не содержит URL, endpoint, provider ID, tenant ID, connection ID,
token, contract version или history cadence. Узел выбирает только разрешённый
Data Product; всё остальное materializes из opaque writer/reader binding и
immutable Data Product definition.
Package `n8n-nodes-ndc` устанавливается как штатный private community package в
`/home/node/.n8n/nodes/node_modules`, чтобы n8n `PackageDirectoryLoader`
сохранил namespace `n8n-nodes-ndc.*`. `~/.n8n/custom` и
`N8N_CUSTOM_EXTENSIONS` запрещены для этого пакета: они создают namespace
`CUSTOM.*`. Ручное копирование или `npm install` в живом container, patch
Engine core и подмена built-in node запрещены. Production использует
проверенный offline tarball, immutable release, atomic current/previous switch,
read-only mount и restart всех n8n execution processes; acceptance завершается
только когда Engine schema и MCP catalog возвращают точные package-qualified
types.
### 5.2. Realtime delivery contract
Новый writer использует:
```text
POST /internal/data-plane/v1/data-products/:dataProductId/publish
Authorization: Bearer <opaque writer binding>
```
Body имеет schema `nodedc.data-product.publish/v1` и содержит только batch
identity и canonical facts. Data Plane одной транзакцией:
1. проверяет idempotency;
2. обновляет current projection только более новым или изменившимся фактом;
3. применяет declarative history policy (`none`, `all`, `sampled`);
4. добавляет changed facts в durable patch outbox;
5. фиксирует монотонный cursor.
Reader grant получает snapshot `nodedc.data-product.snapshot/v1`, затем
подключается к SSE stream с `after=<snapshot.cursor>` или `Last-Event-ID`.
Каждый outbox event имеет schema `nodedc.data-product.patch/v1`. Если cursor
уже удалён retention policy, stream отвечает `409 resync_required`, и consumer
повторяет snapshot. Browser не получает Data Plane token или scope: Foundry BFF
разрешает persisted binding и читает server-only reader grant.
`snapshot+patch` в v1 является **bounded product contract**. Current projection
одного scoped Data Product не может содержать больше 5000 entity keys
`(sourceId, semanticType)`. Snapshot возвращает целую согласованную projection
и один barrier cursor из одной `REPEATABLE READ` transaction. Query-параметр
`limit` — только защитный потолок, а не размер страницы: если полная projection
не помещается, Data Plane отвечает `413 data_product_snapshot_limit_exceeded`
и consumer не начинает patch stream с неполной базой.
Наивная pagination current snapshot по `sourceId`, offset или независимо
полученным page cursors запрещена: изменения между страницами могут быть
пропущены или продублированы относительно patch cursor. Product с ожидаемой
cardinality выше 5000 обязан **до включения realtime** выбрать один из двух
отдельно версионируемых контрактов:
- stable partitioning в несколько Data Products, где partition key является
частью definition, а каждый partition имеет собственный полный snapshot и
независимый patch cursor;
- новый query delivery contract с явно определёнными snapshot barrier,
continuation cursor, query scope и правилами перехода к patch stream.
Такой query contract не входит в `nodedc.data-product.snapshot/v1` и не может
быть имитирован полем `nextPageCursor`. До его отдельного утверждения runtime и
`NDC Data Product Read` работают только с bounded products до 5000 сущностей.
### 6. Порядок первой реализации
1. Версионировать нейтральный intake/read contract и поднять External Data
Plane без provider-specific logic.
2. Пересобрать существующий L2 proof в native-node pipeline: safe read →
split/batch → semantic mapping → generic append → safe summary. Никаких
provider ID filters и provider gateway endpoint.
3. Выполнить manual run, проверить execution/trace, idempotency and current
projection; только затем включить Schedule Trigger.
4. Provision immutable writer binding, one-time place its token into an opaque
Engine credential, then switch the L2 to
`/data-products/:dataProductId/publish` and remove caller scope/headers and
the shared legacy credential.
5. Подключить Foundry к data product current positions. История, геозоны и
новые сущности добавляются отдельными L2 collection profiles and ontology
revisions.
## Последствия
- `services/<provider>-gateway` не является шаблоном для новых интеграций.
Существующий experimental code не расширяется и не деплоится как часть этого
решения.
- Data Plane может иметь собственную БД/Timescale/PostGIS implementation, но
физическая схема остаётся внутренней деталью neutral service, а не API для
L2 или Foundry.
- Inline raw запрещён в L2 → Data Plane v1: до отдельного raw-vault допускается
только restricted ref/hash либо отсутствие raw envelope. Retention reference
вычисляется по серверному acceptance time, а не по timestamp из batch; Data
Plane выполняет bounded retention sweeps после готовности сервиса и по таймеру.
- Обычный Codex Desktop получает ровно те L2 grants, которые выданы владельцем
connection/workflow, и может безопасно развивать adapter только в этой
границе.
- `POST /internal/data-plane/v1/intake` остаётся временным migration-only route:
он выключен по умолчанию (`EXTERNAL_DATA_PLANE_LEGACY_INTAKE_ENABLED=false`),
требует `NODEDC_INTERNAL_ACCESS_TOKEN`, canonical scoped batch и совпадающие
scope headers. Новый или migrated L2 не получает этот shared token и не
использует fallback в legacy route. После миграции всех writers он удаляется.