# Разрешение конфликтов

Когда два ингеста заявляют противоречащие значения для одной тройки `(сущность, предикат, окно валидности)`, Brain оценивает оба и выбирает один из четырёх исходов. Скоринг декларативный, не обученный — оператор крутит веса, а не модель.

## Формула скоринга

```
score = 0.30 · confidence
      + 0.40 · source_trust
      + 0.20 · recency
      + 0.10 · authority
```

Каждый член в `[0, 1]`:

- **confidence** — значение на факт, выставляет ингестер (NLU-экстрактор выдаёт 0.5–0.8; структурный ингест по умолчанию 0.95).
- **source_trust** — на каждую вертикаль и предикат. `rent` высоко доверен для `address`; `inbox` — меньше. Лежит в YAML словаря.
- **recency** — `0.5 ^ (age_days / decay_halflife)`, считается от `validFrom`.
- **authority** — явный override для фактов, выверенных человеком. По умолчанию 0.

Веса переопределяются на тенанта через env: `CONFLICT_W_CONFIDENCE=0.35` и т.д. Проверяются на старте.

## Четыре исхода

| Маржа = score(новый) − score(лучший противоречащий) | Исход | Что происходит |
| --- | --- | --- |
| `margin ≥ 0.15` | **INSERTED** (побеждает новый) | Старый факт замещается — его `validUntil` схлопывается к `validFrom` нового. Статус `superseded`. |
| `0 < margin < 0.15` | **COMPETING** | Оба остаются активными. Brain шлёт событие для разрешения человеком. |
| `−0.30 < margin ≤ 0` | **SUPERSEDED** (побеждает старый) | Новый факт сохраняется, но сразу `superseded`. Виден в timeline. |
| `margin ≤ −0.30` | **REJECTED** | Новый факт уходит в `dead_letter_fact` на разбор оператору. Аудит-след цел, но в поисковый граф факт не попадает. |

Пороги настраиваются через `CONFLICT_MARGIN_INSERT`, `CONFLICT_MARGIN_REJECT`. Дефолты совпадают со спекой.

## Разобранный пример

Тенант: `co_demo`. Предикат: `tier` (bitemporal, decay 30d, PII none).

Существующий факт:
```
predicate: tier, object: gold, validFrom: 2026-02-01, confidence: 0.92, source: rent (trust 0.9)
```

Новый ингест на 2026-04-01:
```
predicate: tier, object: platinum, validFrom: 2026-04-01, confidence: 0.88, source: inbox (trust 0.4)
```

Скоринг на момент ингеста:

```
new = 0.30·0.88 + 0.40·0.40 + 0.20·1.0  + 0.10·0  = 0.624
old = 0.30·0.92 + 0.40·0.90 + 0.20·0.21 + 0.10·0  = 0.678
margin = -0.054  → SUPERSEDED (побеждает старый, новый сохранён как superseded)
```

Итог: пользователь видит активным `tier=gold`. Платина из inbox лежит в timeline со `status: 'superseded'` — Brain не доверял шумному источнику настолько, чтобы перевернуть tier.

Будь у inbox `confidence: 0.95` и достаточно свежий, маржа ушла бы в плюс — исход `INSERTED`.

## Resolver в Dreams

В нерабочие часы `dreams_resolve` пересчитывает пары `COMPETING` старше 7 дней. LLM-судья выбирает победителя по контексту окружающих сущностей; проигравший получает `superseded` с `retractionReason='dreams_resolution'`. Консервативно — вердикт `unsure` оставляет обе пары человеку.

Включается через `DREAMS_RESOLVE_ENABLED=1`. Ограничено `DREAMS_RESOLVE_MAX_PAIRS` (по умолчанию 20).

## Dead-letter

Факты `REJECTED` попадают в `dead_letter_fact` со score, id противоречащего факта и причиной отклонения. Оператор смотрит их так:

```sql
SELECT * FROM dead_letter_fact WHERE companyId = 'co_demo' AND createdAt > '2026-05-01';
```

По нему ловят системные расхождения в source-trust — когда вертикаль шлёт плохие данные, dead-letter покажет это раньше, чем пожалуются пользователи.

## Связанное

- [Словарь предикатов](/ru/docs/concepts/predicates) — где живут source_trust и decay
- [POST /v1/ingest](/ru/docs/api/ingest) — каждый ингест идёт через этот resolver
- [Оператор-плейбук](/ru/docs/operator-playbook) — пересчёт dead-letter, настройка весов
