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
VisibilityRuleaggregate root: ID, Scope (customer | customer_pricing_group), Decision (deny | allow), Predicates, Effective period, Status, Description, Version.PredicatesVO: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
allowrule beats всех matchingdenyrules. - 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).
Нейтральные
SupplierBlocklistfield полностью удалён, без 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.