ADR-0040: Visibility Policies

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

Контекст

P4 Discovery polish требовал замены поля DiscoveryPolicy.SupplierBlocklist []string полноценным aggregate-driven решением:

  • Существующее поле — слишком грубое: нет admin UI, audit trail, временных границ, group-scope, allow-исключений.
  • P2b/P2c/P3 закрепили pattern для rule aggregates: domain model + engine snapshot + outbox events + admin CRUD + health checker. Visibility policies должны использовать тот же pattern для operator/runbook ergonomics.
  • ADR-0026 (marketplace seller axis) обозначил необходимость seller blocklist; это отдельная P4b spec, но он использует ту же DiscoveryPolicy.SellerBlocklist форму. Visibility rules могут расти, чтобы закрыть seller axis в будущей итерации.

Решение

Создан новый aggregate VisibilityRule в discovery/visibility BC.

Aggregate model

  • VisibilityRule aggregate root: ID, Scope (customer | customer_pricing_group), Decision (deny | allow), Predicates, Effective period, Status, Description, Version.
  • Predicates VO: supplier_refs[], manufacturer_refs[], classification_tags[]. Combined по AND внутри одного rule; пустая axis = wildcard. At least one axis non-empty (invariant — operator safety).
  • Decision: deny | allow. Allow-rules — explicit overrides.

Pipeline integration

Layer 1.1.5 — новый VisibilityFilter step между SupplierIndexer (1.1) и CredentialRouter (1.2). Фильтрует briefings по эффективным правилам с per-briefing trace contribution (reason: visibility_blocked для dropped).

Eval semantics — specificity precedence

  • Rules matching customer-scope полностью overrider group-scope rules для того же briefing.
  • Within decisive scope: any matching allow rule beats всех matching deny rules.
  • No matching rules → default keep (visible).

Детерминированный, audit-friendly. Operator может объяснить любое решение через “applied_rules” в trace.

Time bounds

effective_from (required) + effective_until (nullable). Engine evaluator проверяет at time. Случай “временный block на 30 дней” — реален.

Engine + reload

In-memory snapshot через atomic.Pointer[]*VisibilityRule]. Reload trigger:

  • Startup hook (fx OnStart) — fail-fast на boot.
  • Periodic ticker (default 30s, configurable via VISIBILITY_RELOAD_INTERVAL) — safety net + eventual consistency.

Outbox events — discovery.visibility_rules.v1 topic, 4 lifecycle kinds: Created, Updated, Activated, Deactivated.

Admin HTTP

/api/v1/admin/discovery/visibility-rules/* — 6 endpoints (list / create / get / update / activate / deactivate). Soft delete only через deactivate.

Migration — code-only deprecation

DiscoveryPolicy.SupplierBlocklist []string field удалён из policy.go + PolicyMerger

  • trace reason. CustomerFixedPolicy в данный момент stub-loaded (proposal/fx.go::stubLoader), поэтому нет PG-данных для миграции. VisibilityRule полностью заменяет функционал (богаче — admin UI, audit, temporal, group-scope, allow rules).

SellerBlocklist []string остаётся — P4b spec будет с ним работать.

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

A. Embed visibility в supplier-network/credentials BC

Visibility — customer-driven concern; supplier-network — topology-driven. Lifecycle ортогональный. Embedding ломает BC isolation.

B. Inline в EffectivePolicy с расширением SupplierBlocklist (manufacturer / tag)

Не достаточно: нет admin UI, audit, time bounds, group-scope, allow override.

C. Decision-event audit trail (per-eval VisibilityDecision event)

Избыточно — trace applied_rules inline (TraceInlineLevel=raw) уже даёт audit. Cardinality взрывается; ClickHouse load для нулевого function gain.

D. Pure-deny semantics (без allow rules)

Customer exception в group ломает composition; вынуждает operator-restructure группы для one-off allows. Cost outweighs simplicity.

Последствия

Плюсы

  • Pattern uniform across pricing/stock/delivery/discovery (operator ergonomics, runbook reuse).
  • Future P5 transforms (price_to_range / hide_warehouse) — extend тот же engine. Natural growth.
  • Deterministic precedence + per-rule trace = clean audit story.

Минусы

  • +1 layer в pipeline cost (минимальный — snapshot RW lock).
  • Operator должен учить allow/deny precedence (документировано в runbook).

Нейтральные

  • SupplierBlocklist field полностью удалён, без backward-compat shim — оправдано отсутствием prod-данных (stub-loaded).

Ссылки

  • ADR-0035 (proposal pipeline layering) — VisibilityFilter добавлен additively (Layer 1.1.5).
  • ADR-0026 (marketplace seller axis) — P4b sibling spec будет работать с DiscoveryPolicy.SellerBlocklist.
  • ADR-0036 (pricing rule), ADR-0037 (stock rules), ADR-0038 (delivery rules), ADR-0039 (custom pricing handlers) — pattern siblings.
  • docs/superpowers/specs/2026-04-26-p4a-visibility-policies-design.md — full spec.
  • docs/superpowers/plans/2026-04-26-p4a-visibility-policies.md — implementation plan.