ADR-0047: canonical_assignments policy + strategies + trust scale

Status: accepted Date: 2026-04-30 Deciders: agent-claude

Контекст

Закрытый цикл 1 (ADR-0046) реализовал foundation авто-fill характеристик с единственной стратегией auto:latest. Продуктовое наблюдение: характеристики канонических товаров стабильны во времени; данные от разных поставщиков чаще комплементарны, чем противоречивы. Слепое перезаписывание свежим значением, которое случайно может оказаться менее точным или содержать опечатку, ухудшает качество каталога.

Требования к циклу 2:

  1. Поведение замены значения зависит от характеристики (одни блокируются — например MPN/артикул, другие могут уточняться, третьи накапливают множество значений).
  2. Стратегия первичного выбора учитывает доверие к поставщику.
  3. Шкала доверия настраивается админом, поддерживает per-(supplier, dimension) overrides.
  4. Заменять существующее автоматическое значение можно только если новое объективно «чётче».
  5. Multi-valued характеристики (цвета, разъёмы) не выбирают одно «победившее» значение, а накапливают множество с дедупликацией.
  6. Админ может точечно дополнять/удалять элементы множества, не перезаписывая всё назначение.

Решение

  1. Per-characteristic policy — новая колонка characteristics.policy text NOT NULL с CHECK constraint:

    • single_locked — заполнили один раз, дальше не трогаем (auto не перезаписывает).
    • single_sharpenable — авто может заменить, но только если новое значение объективно «чётче» по формальной метрике.
    • multi_union — массив значений с дедупликацией по нормализованному ключу. Дефолт при auto-provision выводится через DerivePolicy(data_type, code): эвристика по data_type (numeric → sharpenable; enum → multi_union; остальное → locked) плюс ручной override-список для известных кодов (mpn, model_number, manufacturer, sku, series, gtin, ean, barcode → принудительно locked).
  2. Захардкоженная цепочка стратегий первичного выбора trust_weighted → mode → latest. Применяется при первом заполнении и при tie-break sharper-replacement. Без per-characteristic merge_strategy конфигурации — поведение управляется политикой, не выбором стратегии.

  3. Двухуровневая trust scale поставщиков:

    • supplier_trust_defaults(supplier_code, score smallint CHECK 0..100, ...) — дефолтный score per-supplier (sparse — отсутствие записи = 50).
    • supplier_dimension_trust(supplier_code, dimension, score, ...) — override per-(supplier, dimension). Resolver fallback: dimension-override → supplier default → global 50.
  4. Источник dimension для категории: новая колонка units.dimension text (nullable). Сидится миграцией для 12 канонических размерностей (voltage, current, power, energy, length, mass, time, frequency, temperature, pressure, angle, area). Характеристика → unit → dimension → category для trust override.

  5. Sharpness chain (для single_sharpenable) — три regex-правила с приоритетом:

    • ① диапазон → точка внутри диапазона ("10-15A" vs "12A").
    • ② значение с единицей → значение без единицы ("220" vs "220V").
    • ③ больше значащих цифр после запятой при одинаковых единицах ("12" vs "12.5"). Если ни одно не сработало — keep existing.
  6. Multi-union с дедупликацией: значения хранятся как JSONB-массив [{display, key}], ключ = lowercase(trim(value)). Индекс по выражению ((value->>'key')) WHERE value ? 'key' для быстрой проверки существования. Display сохраняет оригинальное написание от первого поставщика.

  7. Расширение source CHECK constraint на 7 значений: auto:latest, auto:mode, auto:trust_weighted, auto:sharpened, auto:union, manual, manual:union. Расширяет foundation cycle 1 (auto:latest, manual).

  8. decision_meta jsonb — новая колонка для observability. Воркер пишет {deciding_supplier, trust_score, trust_source}; GET assignments отдаёт в response.

  9. Hard reset migration (миграция 0052): DELETE FROM canonical_assignments WHERE source LIKE 'auto:%'. БД пустая на момент деплоя цикла 2 — безопасно.

  10. Архитектурная декомпозиция β: новые подпакеты catalog/canonical/app/strategy (pure functions: ChooseInChain, AssessSharpness, MergePolicy, NormalizeUnionKey) и catalog/canonical/app/trust (Resolver interface). Постгрес-реализация TrustRepo в infra/postgres. Зеркалит паттерн P3/P5a/Tier 3.

  11. Sticky-override manual — наследуется из cycle 1: SQL WHERE source LIKE 'auto:%' guard + domain-policy. Расширяется manual:union source для административных операций над элементами множеств.

  12. Admin HTTP-API (11 новых endpoints): PATCH /admin/characteristics/{id}/policy, GET /admin/units + PATCH /admin/units/{id}/dimension, два уровня trust CRUD (/admin/trust/suppliers, /admin/trust/suppliers/{code}/dimensions), точечные операции POST/DELETE /admin/canonical/{id}/assignments/{cid}/values. Все за Bearer middleware, монтируются в loopback :9093 через loopbackhttp.Server.Extra hook.

Принятые ограничения

  • Сравнение значений только по форме: семантическая нормализация (Вт ↔ кВт, ,.) — отдельная программа, см. roadmap §7. Sharpness работает с raw JSON через regex.
  • LLM-разрешение спорных случаев отложено в цикл 3.
  • Per-characteristic merge_strategy не вводится — управление через policy достаточно.
  • ETIM/eCl@ss классификация остаётся в roadmap §7. dimension единицы — упрощённая категория для цикла 2.
  • Multi-dimensional trust matrix (ADR-0026 §5b) — упрощённая 2-уровневая модель в цикле 2.

Альтернативы

  • Per-characteristic merge_strategy колонка: отвергнута — добавляет точку конфигурации без подтверждённой потребности; цепочка стратегий покрывает случаи.
  • Trust-tier (gold/silver/bronze) вместо score: отвергнут — числовая шкала позволяет точную настройку и расширение до multi-dim позже.
  • LLM подбирает policy при auto-provision: отвергнут — лишний токен-бюджет, эвристика + ручной override-список покрывают 80% случаев.
  • Категория из ETIM-класса: отложен в roadmap §7 (ETIM-классификация ещё не реализована).

Последствия

  • Существующее значение в canonical_assignments не перетирается без явного основания. Поток данных от ингеста становится append-only по умолчанию.
  • Trust scale настраивается через API, изменения вступают в силу со следующего тика воркера.
  • Multi-union массивы естественно накапливают значения; админ-API позволяет точечную модерацию.
  • decision_meta обеспечивает observability — модератор видит, кто и почему принял решение.
  • Schema migration: 7 миграций (0048-0054), каждая откатываемая. БД на момент деплоя пустая — нет рисков для существующих данных.

Связанные документы

  • Spec: docs/superpowers/specs/2026-04-30-canonical-assignments-strategies-design.md
  • Plan: docs/superpowers/plans/2026-04-30-canonical-assignments-strategies.md
  • Roadmap: docs/superpowers/specs/2026-04-29-canonical-assignments-roadmap.md (§4)
  • Cycle 1: ADR-0046, foundation execution memory
  • Foundation pattern: P3 (handler registry), P5a (ranking), Tier 3 (matcher) — pure-functions подпакеты + I/O resolver