ADR-0050: DKC адаптер + федеративная таксономия (cycle 4)
Status: accepted Date: 2026-05-02 Deciders: agent-claude
Контекст
До цикла 4 Tracium поддерживал трёх поставщиков (ETM, IEK, Systeme Electric), все
работающие в режиме полного каталога (full-pull). Ingestion pipeline проходил через
IngestOrchestrator.RunOnce → SessionManager → Enumerator для каждого поставщика.
Цикл 4 добавляет DKC (ДКС, крупный российский дистрибьютор промышленного оборудования) с тремя новыми требованиями, которые раньше не встречались:
-
Инкрементальная синхронизация через номер ревизии. DKC API предоставляет эндпоинт
GET /v1/revisions/materials?since={revision}, возвращающий только изменения с заданной ревизии. Полный каталог — 1 млн+ позиций; full-pull при каждом тике неприемлем по времени и нагрузке на API. -
Многовалютность. DKC публикует цены одновременно в RUB и KZT. Таблица
offer_observationsхранила одну цену без информации о валюте. Схема требовала расширения. -
Федеративная таксономия ETIM-7. DKC связывает каждую позицию с классом стандарта ETIM-7 (European Technical Information Model). Tracium не имел инфраструктуры для хранения внешних таксономических стандартов. В дальнейшем потребуются ECLASS-12, GTIN, UNSPSC — хранить их нужно единообразно без изменения схемы при добавлении нового стандарта.
Дополнительный контекст:
- Механизм аутентификации DKC (
master_key_token_exchange) отличается от ETM/IEK/Systeme: статический мастер-ключ обменивается на временный access-token с TTL 24 часа. - Цикл 4 первым вводит механизм атрибуции внешних идентификаторов (ETIM class ID на canonical
product) с защитой ручной правки: автоматическая запись блокируется, если запись помечена
источником
manual. - Инициатива бидирекционного курсора (
tracium_bidirectional_cursor.md): схема хранения позиции курсора должна поддерживать не только входящую синхронизацию от поставщиков, но и будущий исходящий API для B2B-клиентов (GET /api/v1/products/changes?since=<cursor>).
Решение
1. Инкрементальный курсор через таблицу incremental_sync_cursor
Новая таблица incremental_sync_cursor хранит позицию курсора для произвольного субъекта
(поставщик или клиент). Ключевые решения:
subject_kind ∈ {supplier, client}— единственная таблица для входящей и исходящей синхронизации. Выходящий API (будущий цикл) используетsubject_kind=client.cursor_kind ∈ {revision_number, iso_date, page_offset, opaque}— полиморфный тип. DKC используетrevision_number; Systeme (будущий retrofit) —iso_date; ETM (вырожденный full-pull случай) —opaque.cursor_value JSONB— нетипизированное хранилище для конкретного значения.- Upsert-семантика: тик записывает новый курсор только при успешном завершении, атомарно с записью наблюдений.
2. PipelineRouter — маршрутизация по capabilities
PipelineRouter читает матрицу CapabilitiesRegistry (список возможностей коннектора) и
диспатчит тик поставщика в одну из двух реализаций:
FullCatalogPipeline— ETM / IEK / Systeme: адаптер поверх существующегоIngestOrchestrator.RunOnce.CursorPipelineImpl— DKC: cursor read → revision poll → seed-or-incremental → per-material pull → cursor advance.
Это backward-совместимое изменение: ETM/IEK/Systeme не затрагиваются.
3. Федеративная таксономия — параллельные таблицы по стандарту
Таксономический словарь DKC (ETIM-7) хранится в пяти новых таблицах:
taxonomy_identity_standards — реестр стандартов (ETIM-7, ECLASS-12, GTIN, ...)
taxonomy_classes — классы стандарта (PK: standard_code + external_code)
taxonomy_features — характеристики (PK: standard_code + external_code)
taxonomy_values — допустимые значения (PK: standard_code + external_code)
taxonomy_units — единицы измерения (PK: standard_code + external_code)
taxonomy_class_features — связи класс↔характеристика
characteristic_mappings — маппинг (standard_code, external_feature_code) → canonical characteristic_id
Ключ (standard_code, external_code) полностью изолирует словари стандартов —
добавление ECLASS-12 не меняет схему. Это не слияние в единый canonical-словарь:
мержинг eClass + ETIM на уровне canonical требует 6+ месяцев моделирования; реализуется
в отдельном цикле при реальной необходимости.
4. Атрибуция идентификаторов через canonical_external_identities
Таблица canonical_external_identities (canonical_id, standard_code, external_code, source)
связывает canonical product с классом внешнего стандарта.
Защита ручной правки: ExternalIdentityWriter.Write проверяет существующую запись перед
вставкой. Если текущий source = 'manual', автоматическая запись (источники auto_supplier,
auto_llm, auto_inference) блокируется. Метрика canonical_external_identity_write_skipped_manual_total
сигнализирует о заблокированных перезаписях.
5. Аутентификация master_key с per-process кэшем
MasterKeyTokenExchanger хранит access-token в памяти процесса (24 часа TTL, не в БД).
При получении HTTP 401 от DKC API Session сбрасывает кэш и повторяет обмен ровно один раз.
Кэш regenerable — потеря при рестарте безвредна.
6. Фоновый импорт словаря ETIM через taxonomy_import_jobs
Импорт словаря DKC запускается через admin endpoint
POST /admin/taxonomy/imports/etim/dkc → 202 + job_id. Фоновый runner в supplier-sync
(гейт cfg.TaxonomyImport.RunnerEnabled, по умолчанию false) полить
taxonomy_import_jobs WHERE status='pending', захватывает строку через UPDATE ... FOR UPDATE SKIP LOCKED,
выполняет 5-фазный Importer, помечает completed/failed.
Единственный runner — serial queue. При старте runner сбрасывает все running записи
в failed с причиной process_restart (защита от зависших job’ов после падения процесса).
7. Многовалютные наблюдения через колонку currency
Колонка offer_observations.currency добавлена миграцией 0066 (DEFAULT 'RUB').
ETM/IEK/Systeme получают RUB без изменений. DKC пишет отдельную строку offer_observations
per-currency (RUB + KZT) через CursorObservationWriter.
Последствия
Плюсы
- Федеративная таксономия unlocks будущие стандарты (ECLASS-12, GTIN, UNSPSC) без изменения схемы.
- Cursor flow обрабатывает дельты каталога 1 млн+ позиций дёшево (только изменения за период тика).
- Ручная правка
canonical_external_identitiesзащищена от автоматической перезаписи. - Бидирекциональный курсор (inbound DKC + future outbound B2B API) из одной таблицы.
taxonomy.Module()composable в обоих бинарниках (supplier-sync с активным runner’ом, api-server с dormant runner’ом) — fx optional deps черезoptional:"true"struct params.
Минусы
- 17 новых таблиц/колонок (миграции 0058–0067) — schema surface значительно выросла.
- Два pipeline’а (full_catalog + cursor) сосуществуют — операторы должны понимать, какой поставщик использует какой путь. Маршрутизация автоматическая (capabilities-driven), но troubleshooting требует знания обоих путей.
Риски и принятые компромиссы
- TaxonomyImport runner однопоточный, глобальная очередь. Большой импорт может задержать меньший. Приемлемо для цикла 4 (только ETIM-7 от DKC); пересматривается при появлении второго стандарта.
- Token cache только в памяти — потеря при рестарте безвредна (regenerable), но добавляет latency первого тика после рестарта (один HTTP round-trip на token exchange).
Рассмотренные альтернативы
Альтернатива A — Push-based incremental (DKC публикует события в Kafka)
DKC мог бы публиковать события изменений, которые Tracium потребляет асинхронно. Отклонено: DKC не имеет event surface и не планирует его. Poll-based cursor — единственный реалистичный вариант.
Альтернатива B — Единый canonical-словарь с маппингами из внешних стандартов
Слияние eClass, ETIM, GTIN в единый canonical-уровень на этапе импорта. Отклонено: это 6-месячная задача моделирования (разные гранулярности классов, пересекающиеся определения характеристик). Федеративные параллельные таблицы по стандарту дают рабочую инфраструктуру сейчас; мерж откладывается до реальной необходимости.
Альтернатива C — Повторное использование IngestOrchestrator.RunOnce для cursor flow
RunOnce итерирует по SKU через Enumerator и записывает наблюдения. Cursor flow мог бы
эмулироваться через специальный Enumerator, читающий delta-эндпоинт.
Отклонено: семантика TickReport (общее число SKU, счётчик ошибок) не совпадает с cursor
mode (delta из N изменений поверх M-записей курсора); смешение создаёт ложные метрики.
Отдельный CursorPipelineImpl с собственными метриками (cursor_ticks, cursor_advanced)
семантически точнее.
Миграции
| № | Описание |
|---|---|
| 0058 | incremental_sync_cursor + индексы |
| 0059 | taxonomy_identity_standards + сид ETIM-7 |
| 0060 | taxonomy_classes |
| 0061 | taxonomy_features |
| 0062 | taxonomy_values |
| 0063 | taxonomy_units + taxonomy_class_features |
| 0064 | characteristic_mappings (standard_code + external_feature_code → canonical) |
| 0065 | canonical_external_identities |
| 0066 | offer_observations.currency (DEFAULT ‘RUB’) |
| 0067 | taxonomy_import_jobs |
Новые переменные окружения
| Переменная | Дефолт | Назначение |
|---|---|---|
DKC_BASE_URL | "" | URL DKC API. Пустая строка — модуль отключён |
DKC_MASTER_KEY | "" | master_key для token exchange |
DKC_TOKEN_TTL | 24h | TTL кэша access-token |
TAXONOMY_IMPORT_RUNNER_ENABLED | false | true только в supplier-sync env |
TAXONOMY_IMPORT_POLL_INTERVAL | 10s | Интервал проверки pending jobs |
Фазы реализации
Цикл 4 реализован за 37 коммитов в блоках:
- F (миграции, 10 коммитов): 0058–0067
- D (DKC domain + infra, 12 коммитов): TokenExchanger, IncrementalSyncCursor, DKC capabilities/session/connector/EnumeratorSeed, PipelineRouter, CursorPipelineImpl, IngestionMetrics extensions, supplier-sync DI
- T (Taxonomy BC, 9 коммитов): domain + 8 PG repos + ETIM parser + Importer + JobsRunner
- TaxonomyMetrics + admin HTTP
- I (интеграция, 7 + 1 chore коммитов): ExternalIdentityWriter, canonical-identities admin gap-fill, DKC productFeature в cursor pipeline, supplier-sync runtime swap to PipelineRouter, CursorObservationWriter per-currency, api-server wired with taxonomy.Module()
- C (closure, 4 коммита): ADR-0050, runbooks, memory snapshot
Deferred: C1 (DKC ingestion E2E), C2 (taxonomy import E2E) — отслеживаются как follow-up.
Связанные документы
- Spec:
docs/superpowers/specs/2026-04-30-dkc-adapter-and-taxonomy-design.md - Runbook (DKC cursor tick):
docs/operations/dkc-cursor-tick.md - Runbook (taxonomy import):
docs/operations/taxonomy-import.md - ADR-0024 — supplier credentials + auth schemas
- ADR-0026 — marketplace observations + seller axis
- ADR-0042 — ExchangeRate Layer 3a (unblocked DKC multi-currency)
- ADR-0043 — ingestion orchestration audit (per-supplier tickers, advisory lock)
- ADR-0046 / ADR-0047 / ADR-0049 — canonical assignments series (use canonical_external_identities)
- Memory:
tracium_bidirectional_cursor.md— cursor pattern rationale