ADR-0046: canonical_assignments source field & manual-over-auto policy
Status: accepted Date: 2026-04-29 Deciders: agent-claude
Контекст
CanonicalProduct.Assignments в проде пуст: запись характеристик в агрегат
никогда не производилась автоматически. Исторически CanonicalProductRepo.Save()
переписывал assignments полностью через DELETE+INSERT — при любом вызове ручные
правки стирались без следа. Механизм защиты manual-правок отсутствовал.
ADR-0044 ввёл charnorm pipeline: char_name_mappings содержит нормализованные
имена характеристик, offer_characteristic_raw накапливает сырые значения от
поставщиков. ADR-0045 дал Tier 3 LLM matcher — подтверждённые offer↔canonical
links с confidence. Оба pipeline готовы; недостающее звено — worker, который
переносит нормализованные данные в canonical_assignments с правильным source
tracking.
Требования к циклу 1: (а) заполнять assignments автоматически на основе накопленных observations; (б) защищать ручные правки от перезаписи на уровне БД и домена; (в) не блокировать поток данных из-за неизвестных characteristic names.
Решение
-
Поле
sourceвcanonical_assignments: типtextс CHECK constraint, допустимые форматыauto:<strategy>илиmanual. Цикл 1 — стратегииauto:latestиmanual. Циклы 2-3 расширят список (auto:mode,auto:union,auto:trust_weighted,auto:llm). CHECK выбран вместо ENUM: расширение обычной миграцией, безALTER TYPE ADD VALUEс его ограничениями на откат. -
SQL-защита manual-строк: worker выполняет UPSERT с условием
WHERE canonical_assignments.source LIKE 'auto:%'. Строки сsource='manual'SQL игнорирует — защита работает на уровне БД без race-условий между процессами. -
Доменная политика: метод
CanonicalProduct.AssignCharacteristic(source=auto)приexisting.Source == "manual"возвращает(false, nil)— первая линия защиты в памяти, ещё до обращения к репозиторию. -
Auto-provision характеристик: новое поле
characteristics.statusс CHECK('pending', 'active'). Worker встречает неизвестное characteristic name — создаёт запись со статусомpending, assignment пишется немедленно. Pending-характеристики видны в proposal’ах. Модератор переводит вactiveчерезPATCH /admin/characteristics/{id}. -
Worker — отдельный процесс
cmd/canonical-assignment-workerпо шаблону charnorm-worker. Singleton через Postgres advisory lock (LeaseGuard.Acquire("canonical_assignment")). Loopback HTTP :9093. Изолирован от matcher-worker: упал — не уронил остальных; масштабируется независимо; собственный rate limit на ресурсы. -
CanonicalProductRepo.Save()больше не пишет assignments. Запись — через отдельный портCanonicalAssignmentWriter.BatchUpsert. Read path расширен полямиsource,decided_by,decided_at. Разделение ответственности устраняет риск случайного стирания через основной Save. -
Admin HTTP API в
catalog/canonical/api/http:GET /admin/canonical/{id}/assignments,PUT /admin/canonical/{id}/assignments/{char_id},DELETE /admin/canonical/{id}/assignments/{char_id},GET /admin/characteristics?status=pending,PATCH /admin/characteristics/{id}. Bearer-middleware через ENVCANONICAL_ASSIGNMENT_ADMIN_BEARER; пустой ключ → 503. Паттерн аналогичен ADR-0045 Admin API. -
loopbackhttp.Server.Extra []func(*http.ServeMux)— backward-compatible extension hook. Worker регистрирует admin handlers черезfx.Invokeдо Start. Основной сервер не требует изменений. -
Стратегия слияния цикла 1 —
latest-by-observed_at: для каждой пары(canonical_id, char_name)берётся observation с максимальнымobserved_at, tie-break поsupplier_code ASC. Стратегииmode,union,trust_weighted,llm— циклы 2 и 3.
Последствия
Плюсы:
- Ручные правки невозможно затереть автоматом: двойная защита SQL+domain.
- Незнакомые characteristic names не блокируют автоматику: pending-flow позволяет писать assignment немедленно.
- Worker изолирован: сбой не затрагивает matcher-worker или ingestion.
- CHECK constraint расширяется обычной миграцией —
ALTER TYPE ADD VALUEне нужен. - Разделение Save/BatchUpsert устраняет класс ошибок silent-overwrite.
Минусы / accepted trade-offs:
- Pending-характеристики могут накапливаться, если модератор не работает с очередью. Runbook описывает массовую очистку через SQL.
latest-by-observed_atигнорирует расхождения между поставщиками: supplier с поздней датой observation перезапишет supplier с лучшим качеством данных. Цикл 2 вводитtrust_weighted.- Ещё один контейнер в проде. Acceptable для cycle 1.
Альтернативы, отклонённые
- ENUM вместо CHECK для source. Отклонено: расширение ENUM в Postgres
усложняет откат миграций в future cycles;
ALTER TYPE ADD VALUEне транзакционен. - Очередь на ревью вместо auto-provision неизвестных chars. Отклонено: блокирует поток данных на старте; большинство pending-записей будут корректны.
- Worker внутри matcher-worker. Отклонено: один тяжёлый цикл может задержать другой; смешиваются метрики и логи; сбой одного убивает второго.
- Отдельная таблица
canonical_characteristic_valuesс deprecation старой. Отклонено: создаёт migration window с tech debt; модель агрегата не меняется. /metrics-snapshotJSON endpoint на worker. Отклонено: не согласуется с prevailing pattern (другие workers — slog-mirror only); метрики читаются через slog.
Реализация
Затронутые компоненты:
backend/internal/core/catalog/canonical/:
domain/assignment.go— полеSource, методAssignCharacteristicс manual guarddomain/characteristic.go— полеStatus(pending/active)domain/metrics.go(AtomicAssignmentMetrics)app/assignment_service.go(BatchAssign, стратегияlatest-by-observed_at)app/characteristic_service.go(auto-provision, promote)ports/assignment_writer.go(CanonicalAssignmentWriterinterface)infra/postgres/assignment_repo.go(BatchUpsert с WHERE source LIKE ‘auto:%’)infra/postgres/characteristic_repo.go(pending upsert)api/http/assignment_handler.go,characteristic_handler.go,admin_auth.godi.go
backend/cmd/canonical-assignment-worker/main.go
backend/internal/platform/loopbackhttp/server.go — поле Extra []func(*http.ServeMux)
backend/migrations/:
0040_canonical_assignments_source.sql0041_characteristics_status.sql
Deferred items
- Стратегии
auto:mode,auto:union,auto:trust_weighted— цикл 2. auto:llm— цикл 3 (после накопления обучающей выборки).- Общий cross-source Moderation BC (аналогично ADR-0045 п.4 — после второго consumer).
- UI для pending-характеристик и assignments review.
- Auto-cleanup stale pending-характеристик (TTL-based).
- Multi-reviewer claim для ручного редактирования.
- Per-supplier trust score (prerequisite для
trust_weighted).
Ссылки
- Spec:
docs/superpowers/specs/2026-04-29-canonical-assignments-foundation-design.md - Plan:
docs/superpowers/plans/2026-04-29-canonical-assignments-foundation.md - Roadmap:
docs/superpowers/specs/2026-04-29-canonical-assignments-roadmap.md - ADR-0044 — LLM gateway + charnorm pipeline (
char_name_mappings,offer_characteristic_raw) - ADR-0045 — Tier 3 LLM matcher (паттерн admin Bearer-middleware)
- ADR-0030 — Backend DI rule (main.go = lifecycle only)
- ADR-0043 — Ingestion orchestration (advisory lock pattern)