ADR-0028: Federated identity taxonomies (ETIM как один из многих)

Status: accepted Date: 2026-04-18 Deciders: команда проекта

Контекст

При анализе поставщиков обнаружено: часть из них (Systeme Electric, DKC, КЭАЗ через отдельный каталог) отдаёт характеристики и классификацию в стандарте ETIM (International Electrotechnical Model) — отраслевой стандарт для электротехники.

Но:

  • ETIM покрывает не всё. В машиностроении его нет; используются другие стандарты: eCl@ss, UNSPSC, GS1 GTIN, OKPD2 (РФ), ISO 13584 PLIB, internal industry-specific.
  • Часть поставщиков даёт ETIM параллельно со своими внутренними кодами; часть даёт только внутренние коды (ETM, smart-shop.pro).
  • Производитель одновременно может иметь MPN (manufacturer part number), GTIN/EAN (barcode), ETIM feature set, OKPD2 code — все они указывают на один и тот же товар.

Задача: принять ETIM как полезный sign, но не завязываться на него архитектурно. Tracium должен уметь сопоставлять товары через любую из поддерживаемых taxonomies, и добавлять новые (eCl@ss, UNSPSC) без break-change.

Решение

Federated taxonomy layer — Tracium поддерживает произвольный набор identity standards, каждый из которых — опциональный сигнал для matching + источник характеристик. ETIM — первый поддерживаемый, но не привилегированный.

1. IdentityStandard aggregate (Catalog BC, seed registry)

IdentityStandard
├── id
├── code: string               // "etim", "eclass", "gs1_gtin", "unspsc", "okpd2", "mpn", ...
├── version: string?           // "v7" для ETIM, "10.0.1" для eCl@ss, null для версионно-нейтральных (MPN/GTIN)
├── domain: string             // "electrical" | "mechanical" | "pharma" | "universal" | "custom"
├── kind: classification | characteristic_schema | identifier | hybrid
├── description
├── official_source_url?
├── status: active | deprecated | experimental
└── lifecycle metadata

Event-sourced. Расширение — через admin UI (не ADR; низкий риск, семантика проверяется per-mapping).

Стартовый seed:

codeversiondomainkind
etimv7electricalhybrid
etimv6electricalhybrid (для legacy поставщиков)
gs1_gtinuniversalidentifier
mpnuniversalidentifier
okpd22014universal (RU)classification

Остальные (eclass, unspsc, iso13584) добавляются по мере появления поставщика, использующего их.

2. TaxonomyCharacteristicMapping aggregate

TaxonomyCharacteristicMapping
├── id
├── standard_ref: IdentityStandard
├── external_feature_code      // EF001234 для ETIM; AD-VEV для eCl@ss; etc.
├── tracium_characteristic_key
├── unit_override?
├── value_mapping_ref?         // для enum-значений (EV-codes у ETIM)
├── source, confidence
└── lifecycle: active | deprecated | superseded_by(code)

Уникальность: (standard_ref, external_feature_code). Event-sourced.

3. TaxonomyClassMapping aggregate

TaxonomyClassMapping
├── id
├── standard_ref
├── external_class_code        // EC001234 ETIM; 27-01-01-01 eCl@ss
├── tracium_tags: [ClassificationTag]      // multi-tag: одна external class → N наших tags
├── parent_external_class_code?           // сохраняем hierarchy у standard'а
├── source, confidence

4. Пересечение стандартов

Один и тот же товар может быть описан несколькими стандартами одновременно:

  • ETIM feature EF001234 → Tracium key nominal_current_A.
  • eCl@ss feature AD-VEV-00123 → Tracium key nominal_current_A.

Два TaxonomyCharacteristicMapping указывают на один и тот же tracium_characteristic_key. При матчинге это нормально: оба сигнала независимо подтверждают, что товар имеет nominal_current_A=100.

5. CanonicalProduct.external_identities[]

CanonicalProduct хранит набор external identities, которые о нём известны:

CanonicalProduct {
  ...
  external_identities: [
    { standard_ref: etim_v7, external_code: "EC001234" },  // ETIM class
    { standard_ref: mpn,     external_code: "NXM-125S" },  // Manufacturer Part Number
    { standard_ref: gs1_gtin, external_code: "4601234567890" }, // barcode
    { standard_ref: okpd2,   external_code: "27.33.13.120" },
  ]
}

Это не identity_signature (критические характеристики), а дополнительные identity markers. Используются Matching как сигналы high-confidence.

6. SupplierCharacteristicMapping — расширение target

SupplierCharacteristicMapping {
  supplier_ref
  supplier_char_code
  target: CharacteristicMappingTarget
}

CharacteristicMappingTarget = Direct | ViaTaxonomy

Direct {
  tracium_characteristic_key
  unit_override?
}

ViaTaxonomy {
  standard_ref
  external_feature_code
  // реальный tracium_characteristic_key резолвится через TaxonomyCharacteristicMapping
}

Lookup при парсе supplier payload:

1. supplier_char_code → SupplierCharacteristicMapping → target.
2a. Direct target: использовать tracium_characteristic_key.
2b. ViaTaxonomy target: lookup TaxonomyCharacteristicMapping[standard, external_feature_code] → key.
3. Ни одного mapping нет → Moderation proposal.

Поставщики, отдающие native ETIM коды (Systeme, DKC), создают 1 SupplierCharacteristicMapping с ViaTaxonomy{standard=etim_v7} для всех своих кодов — one-time setup. Supplier без общей taxonomy (ETM) использует Direct для каждого ConfigCharCode.

7. Matching через multiple identities

Matching обновляется: при попытке связать SupplierOffer с CanonicalProduct, кроме сравнения critical characteristics, проверяет overlap external_identities:

  • Если supplier payload несёт ETIM class EC001234, а canonical имеет external_identities: [..., {etim_v7, EC001234}, ...] — это сильный сигнал для match_confidence=strong даже при частичном совпадении прочих атрибутов.
  • Если barcode совпадает (GS1 GTIN) — практически гарантия exact.
  • MPN match + manufacturer match → strong (для некоторых товаров — exact).

MatchSignal в ADR-0029 event storming (существующий) расширяется: identity signal теперь может содержать {kind: "gs1_gtin" | "etim_class" | "mpn" | ...}.

8. Capabilities

Capabilities.Taxonomies {
  SupportedStandards   []TaxonomyStandardRef   // какие стандарты connector возвращает в payload
  NativeCoding         bool                    // true если payload содержит external_code напрямую (Systeme/DKC ETIM)
  MixedWithInternal    bool                    // true если connector отдаёт и свои, и стандартные коды
  ExportsDictionary    bool                    // есть ли endpoint для выгрузки справочника (DKC /etim/*)
}

9. Сосуществование с SupplierClassificationMapping / SupplierManufacturerMapping

Те же aggregates расширяются по аналогии:

  • SupplierClassificationMapping.target = Direct | ViaTaxonomy(standard, external_class_code)TaxonomyClassMapping.
  • SupplierManufacturerMapping остаётся direct-only (производители не имеют глобальной taxonomy).

10. ETIM как первый конкретный standard

ETIM используется первым, потому что 3 из 5 поставщиков его отдают. Но:

  • В config connector’а — taxonomy_standards: [etim_v7], не etim: enabled (общий формат).
  • TaxonomyCharacteristicMapping seed идёт из официального ETIM dump (публично доступен).
  • Когда появится machinery поставщик (eCl@ss), seed из eCl@ss dump, без правок модели.

Последствия

Плюсы

  • Гибкость: новая отрасль / стандарт — новый IdentityStandard + таблица mapping’ов. Без изменения модели.
  • Cross-supplier consistency: два поставщика, отдающие ETIM EF001234, мапятся в один Tracium key — one-time work.
  • Matching boost: overlap external_identities — сильные сигналы для match_confidence.
  • Identity, а не только classification: barcode (GS1), MPN, OKPD2, промышленные stadnarts — все унифицировано.
  • Не лок-ин на ETIM: если в машиностроении ETIM нет — работа без него.

Минусы

  • Промежуточный слой — ещё один lookup: supplier → taxonomy → tracium vs supplier → tracium. Performance impact незначителен (in-memory maps).
  • Matrix размеров: N supplier × M standards × K Tracium keys → потенциально большой справочник. Mitigation: lazy population, один mapping — один source of truth.
  • Ответственность за качество taxonomy mappings — catalog ops. Это новая операционная работа.

Нейтральные последствия

  • Поставщики без taxonomy support (ETM, smart-shop.pro): SupplierCharacteristicMapping.target=Direct — поведение как раньше.
  • CanonicalProduct.external_identities[] — опциональное поле; у legacy canonicals пустое.

Рассмотренные альтернативы

A. Принять ETIM как центральный, мапить всё через него

Было в ранней редакции ADR-0028. Отклонено: ETIM — отраслевой (электротехника), в машиностроении / фарме / food — другие стандарты. Принятие ETIM core ломает ADR-0016 extensibility принцип.

B. Оставить только SupplierCharacteristicMapping direct, без taxonomy layer

Работает, но дублирует mapping effort. Два ETIM-поставщика → 2× ту же работу.

C. External IDs как free-form map без schema

canonical.external_ids = {"etim": "EC01", "mpn": "X"} — удобно, но теряется semantic (нет таблицы characteristic-level, нет validation, нет units, нет version-aware dispatch).

Миграция

  1. Ввести IdentityStandard, TaxonomyCharacteristicMapping, TaxonomyClassMapping aggregates.
  2. Seed ETIM v7 из официального dump.
  3. Seed mpn, gs1_gtin, okpd2 (identifier kind; no feature map, только external_code в CanonicalProduct.external_identities[]).
  4. Existing SupplierCharacteristicMapping{target=Direct} остаются как есть.
  5. Для Systeme / DKC — при подключении connector’а сразу создаём mappings с ViaTaxonomy{etim_v7}.
  6. Matching обновляется для использования external_identities[] overlap как сигнала — separate PR, после заполнения registries.

Implementation status updates

  • 2026-04-26 (P4c-1). canonical_products.classification_tags TEXT[] column added (migration 0030). SupplierIndexer reads the column end-to-end. Writers (AI classifier and per-supplier mapping) deferred — see docs/superpowers/specs/2026-04-26-p4c-1-classification-tags-design.md and the deferred-work plans docs/plans/2026-04-26-14-21-future-ai-normalization-tags.md, docs/plans/2026-04-26-14-28-future-sticky-override-mechanism.md.

Ссылки