Сценарий: новый offer → каноническое сопоставление

NOTE

Статус: Target design. Документ описывает целевую доменную модель. Соответствующий код реализован частично (см. backend/internal/core/) или пока не начат. Правила маркировки — в 50-processes/documentation-standard.md.

Триггер

  • SupplierOfferSeen (Offers) — первое появление (supplier_ref, supplier_sku).
  • OfferCharacteristicsUpdated — single rematch.
  • IdentityProfileChanged или ManufacturerAliasAdded (Catalog) — массовый rematch.

Участники

BCРоль
MatchingOwner. Вычисление identity_signature, попытка match, выбор уровня confidence.
CatalogИсточник Identity Profiles, Manufacturers, Characteristics, существующих Canonical.
EnrichmentПоставщик embedding similarity как second signal.
OffersSubscriber MatchDecided — обновляет canonical_product_ref, match_confidence.
Moderation (AI-агент)Резолюция orphans + match_review через детерминированный LLM-pipeline. Подробно — ai-moderation-flow.md.
OperatorТолько escalation (low confidence / circuit broken / security-critical). По умолчанию не задействован.

Sequence diagram

sequenceDiagram
    autonumber
    participant OFR as Offers
    participant M as Matching
    participant CAT as Catalog
    participant EN as Enrichment
    participant MOD as Moderation (AI-агент)
    participant OP as Operator (только escalation)

    OFR->>M: 🟧 SupplierOfferSeen
    M->>M: 🟦 AttemptMatch
    Note over M: extract manufacturer / mpn / characteristics
    M->>CAT: read Manufacturer aliases
    M->>CAT: select Identity Profile by matcher
    alt профиль найден
        M->>M: compute identity_signature
        M->>CAT: lookup canonical by signature
        alt canonical exists
            M-->>OFR: 🟧 MatchDecided{exact|strong}
        else canonical NOT exists
            M-->>OFR: 🟧 MatchDecided{unmatched}<br/>но identity_signature валиден
            M-->>CAT: 🟧 MatchDecided{unmatched}
            CAT->>CAT: 🟪 Policy: создать canonical
            CAT->>CAT: 🟦 СоздатьКаноническийТовар
            CAT-->>CAT: 🟧 КаноническийТоварСоздан
            Note over CAT: relink offers с этим signature
        end
    else профиль НЕ найден / нет critical attrs
        M->>M: try by description+characteristics
        alt нашёл probable
            M-->>OFR: 🟧 MatchDecided{probable}
            M->>EN: запрос embedding similarity
            EN-->>M: similarity score
            M->>M: check second signals
            alt есть second signal
                M->>M: 🟧 SecondSignalFound
                M-->>OFR: 🟧 MatchUpgraded → strong
            else нет second signal
                M->>MOD: 🟧 ModerationItemAdded(match_review)
                MOD->>MOD: AI-агент: gather + LLM + validate
                alt AI confidence >= threshold
                    MOD->>M: 🟧 ProposedAction(MatchUpgrade)
                    M->>M: 🟦 ApplyModerationDecision
                    M-->>MOD: DecisionApplied{case_id}
                    M-->>OFR: 🟧 MatchUpgraded
                else low confidence / circuit
                    MOD->>OP: 🟧 CaseEscalated
                end
            end
        else weak
            M-->>OFR: 🟧 MatchDecided{weak}
            M->>MOD: 🟧 ModerationItemAdded
        else unmatched + no profile
            M-->>OFR: 🟧 MatchDecided{unmatched}
            M->>M: 🟦 OpenOrphan
            M-->>M: 🟧 OrphanItemAssigned
            M->>MOD: publish OrphanItemAssigned (orphan_resolution)
            MOD->>MOD: AI-агент: классификация / proposal
            MOD->>M: 🟧 ProposedAction(ResolveOrphan)
            M->>M: 🟦 ApplyModerationDecision
            M-->>MOD: DecisionApplied
        end
    end

Шаги

  1. Trigger: SupplierOfferSeen или OfferCharacteristicsUpdated.
  2. Identity profile selection:
    • Берём characteristics_from_supplier + classification_tags + parsed manufacturer/mpn.
    • Применяем все matcher’ы Identity Profile (object_type == X и т.д.).
    • Выбираем профиль с max priority; tie-break — больше critical_attribute_keys.
  3. Identity signature:
    • Если найден профиль и есть mpn (или достаточный набор critical attrs):
      • Нормализуем (manufacturer, mpn?, profile, critical_attrs).
      • identity_signature = stable_hash(...).
  4. Canonical lookup:
    • SELECT canonical WHERE identity_signature = ?.
    • НайденMatchDecided{exact} (если совпадение по mpn) или {strong} (расхождение в некритичных).
    • Не найденMatchDecided{unmatched} + сигнал Catalog’у создать canonical (Policy).
  5. Если профиль НЕ найден (нет matcher’а):
    • Попытка через embedding / описание.
    • probable match: похожий canonical, но без identity. Требуется second signal:
      • Embedding similarity ≥ 0.85.
      • Совпадение classification_tag.
      • Packaging fingerprint совпал.
      • Multi-supplier consensus (≥ 2 поставщика прислали тот же набор critical → одинаковый identity_signature → автоматически exact/strong).
      • Manual подтверждение модератором за последние 30 дней похожего matching на этом profile.
    • weak — частичное совпадение, всегда moderation.
    • unmatched + no profile — orphan.
  6. Moderation queue / orphan pool (publish для AI-агента в Moderation BC):
    • probable без second signal → ModerationItemAdded(match_review).
    • weak всегда → ModerationItemAdded(match_review).
    • unmatched + no profileOpenOrphanOrphanItemAssigned(orphan_resolution) с категорией (hot/warm/cold) и SLA.
  7. AI-агент Moderation (по умолчанию автоматически):
    • Gather evidence: похожие resolved кейсы, embedding similarity, classification tags, multi-supplier consensus.
    • LLM в sandbox (temperature=0, timeout 30 сек, function calling с JSON schema).
    • Validate: confidence ≥ threshold, citations существуют, proposed_action валиден.
    • acceptProposedAction(MatchUpgrade|ResolveOrphan) → Matching применяет через ApplyModerationDecision → ack DecisionApplied{case_id}.
    • reject → решение остаётся probable / weak / unclassifiable (для orphan).
    • confidence < threshold или circuit brokenCaseEscalated → human queue (только при невозможности AI). Подробно — ai-moderation-flow.md.
  8. Catalog update (если match=exact/strong + новые characteristics из offer):
    • Conflict resolution → ОбновитьХарактеристикиТовара.
  9. Cascade rematch (при IdentityProfileChanged / ManufacturerAliasAdded):
    • RequestRematch массовый для всех затронутых offers.
    • RematchBatchCompleted event для метрик.

Decision points

flowchart TD
    A["SupplierOfferSeen"] --> B{"Identity Profile<br/>matcher срабатывает?"}
    B -->|да| C["compute identity_signature"]
    C --> D{"Canonical с этим<br/>signature существует?"}
    D -->|да| E["🟧 MatchDecided<br/>{exact|strong}"]
    D -->|нет| F["🟧 MatchDecided{unmatched}<br/>+ valid signature"]
    F --> G["Catalog: создать canonical"]
    B -->|нет| H["попытка через<br/>описание + embeddings"]
    H --> I{"score?"}
    I -->|exact-like| E
    I -->|probable| J{"есть second signal?"}
    J -->|да| K["🟧 MatchUpgraded → strong"]
    J -->|нет| L["🟪 ModerationItemAdded"]
    I -->|weak| L
    I -->|none + no profile| M["🟦 OpenOrphan"]
    M --> N["🟧 OrphanItemAssigned<br/>{hot|warm|cold}"]

Edge cases

СлучайПоведение
MPN отсутствует у offer и у профиля > 0 critical attrsIdentity signature использует sentinel для mpn + critical_attrs. Достаточно для дедупа, но требует более надёжной нормализации.
Два разных canonical с одинаковыми critical attrs (баг профиля)Identity collision — alert. Профиль требует расширения critical_attribute_keys через ADR.
Manufacturer alias добавлен после первого matchCascading RequestRematch для offers с этим alias parsed. Возможен MatchUpgraded.
Offer одновременно матчится к 2 canonical через разные эвристикиHard conflict — moderation. Не auto-разрешается.
Critical attribute обогащён AI, не verifiedНе используется в identity_signature (см. Catalog inv #5 + Enrichment inv). Может использоваться как сигнал (probable boost), но не для exact.
Поставщик переотправил тот же offerИдемпотентно — повторная попытка дает то же MatchDecision (если входные данные не изменились).

Инварианты сценария

  1. На один SupplierOffer — ровно одно текущее MatchDecision.
  2. match_confidence = unmatchedcanonical_product_ref is null.
  3. probable попадает в автомат только при наличии хотя бы одного из 5 second signals.
  4. weak всегда требует ручной верификации.
  5. Orphan SLA по категориям соблюдается (см. matching.md).
  6. AI-значения не входят в identity_signature.

Метрики и observability

  • match_decisions_total{confidence} — распределение confidence.
  • match_upgrades_total{from, to}, match_downgrades_total{from, to}.
  • orphan_pool_size{category}, orphan_resolution_time_seconds{category} (SLA).
  • moderation_queue_size{kind}.
  • identity_signature_collisions_total — alert при > 0.
  • rematch_batch_completed{trigger} — для understanding load.

Связанные файлы