Files
contracts/History/session-12-event-sourcing-architecture.md
T

125 lines
6.6 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Сессия 12 — Архитектура Event Sourcing v2
Дата: 2026-06-20
## Контекст
Обсуждение новой архитектуры сравнения договоров: переход от pairwise diff по `row_num`
к Event Sourcing с LLM-интерпретацией допсоглашений (ДС).
Участники: DeepSeek (этот агент), Gemini, Sonnet.
## Ключевые решения
### 1. Разделение труда
- **LLM** переводит текст ДС → список операций (ADD/UPDATE/DELETE/UNRESOLVED)
- **Lucee** исполняет операции как лог, собирает кумулятивный статус
- LLM не участвует в математике сравнения
### 2. Хэш вместо row_num
- `md5(lower(trim(name)) || coalesce(date_start,''))` — только неизменяемые идентификаторы
- `qty`, `price`, `sum` НЕ входят в хэш — это изменяемые атрибуты
- Если `qty` в хэше: ДС меняет количество → новый хэш → ADD вместо UPDATE → дубликат
- Уникальность: имя + дата достаточно (две строки с одинаковым именем и датой в одном договоре — крайний случай, решается уточнением name)
### 3. Старый код
- `spec_rows` + старый differ остаются параллельно (prod не трогаем)
- Первичный договор: старый экстрактор строк
- Допники: новый движок с операциями
- После обкатки — старый отключаем
### 4. ВМ-прокси как async-буфер для LLM
- Lucee через cfhttp висит синхронно до 120с
- ВМ (Flask + httpx) забирает задачу, вызывает LLM, отдаёт готовый JSON
- Новый endpoint `/llm-ops` в том же `convert_server.py`
- Допники обрабатываются по одному (SSE прогресс)
### 5. LLM-промпт (новый, для ДС)
- Контекст: полная текущая спецификация (hash + все поля) + текст ДС
- 100 строк ~10KB — для 120B модели не проблема
- LLM сам определяет full_replace vs изменения построчно
- Возвращает частичные изменения (только изменённые поля), Lucee делает COALESCE
### 6. БД: Event Sourcing
- `spec_events`: лог операций (contract_id, supplement_id, seq INTEGER per-contract, action, target_hash, new_values JSONB, comment)
- `spec_current`: текущий статус (таблица, обновляется при apply)
- full_replace → явные DELETE на каждую строку (аудит)
- Весь ДС — одна транзакция (атомарность)
- Откат целиком по supplement_id
### 7. UNRESOLVED
- Тип операции уже определён LLM (ADD/UPDATE/DELETE)
- UNRESOLVED — LLM не смог найти хэш для привязки
- Оператор в `resolve.cfm`: выпадайка с существующими услугами → привязать хэш
- Применяется как UPDATE к выбранной услуге
### 8. UI
- `resolve.cfm` — отдельная страница для ручного разрешения
- Список UNRESOLVED строк, дропдаун с услугами, кнопка «Подтвердить»
- Текущий `view.cfm` — предпросмотр документа (первые 15 строк)
### 9. Триггер пайплайна
- `process.cfm?contract_id=X&v=2` — новая версия
- Старая кнопка = v1, новая кнопка = v2 (рядом)
- Никаких флагов в БД, никакого auto-detect
- Когда обкатаем → переключаем дефолтную кнопку
### 10. parsed_text для ДС
- `process.cfm` v2 переиспользует ту же логику textify (elements_json → текст), что и v1
- `parser.cfm` НЕ трогать
- Текст формируется в process.cfm и передаётся на ВМ
### 11. Первичное заполнение spec_current
- `process.cfm?v=2` обрабатывает все supplements с нуля, по порядку
- Initial: `current_spec = []` (пустой) → LLM возвращает всё как ADD → spec_current заполнен
- Каждый следующий ДС: `current_spec` из уже заполненного spec_current
- `spec_rows` не трогаем — старый пайплайн независим
- Для существующих контрактов: v2 пересчитывает с нуля через LLM, миграция не нужна
## Технические детали реализации
### Таблицы (новые)
```sql
spec_events (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
contract_id UUID REFERENCES contracts(id),
supplement_id UUID REFERENCES supplements(id),
seq INTEGER NOT NULL, -- per-contract счётчик
action TEXT NOT NULL, -- ADD, UPDATE, DELETE, UNRESOLVED
target_hash TEXT, -- хэш услуги
new_values JSONB, -- для ADD/UPDATE
comment TEXT,
resolved_by TEXT, -- кто разрешил UNRESOLVED
created_at TIMESTAMPTZ DEFAULT now(),
UNIQUE(contract_id, seq)
)
spec_current (
contract_id UUID REFERENCES contracts(id),
name_hash TEXT NOT NULL, -- md5(lower(trim(name)) || coalesce(date_start,''))
name TEXT,
price NUMERIC,
qty NUMERIC,
sum NUMERIC,
date_start TEXT,
updated_at TIMESTAMPTZ DEFAULT now(),
PRIMARY KEY (contract_id, position_hash)
)
```
### API ВМ `/llm-ops`
```
POST /llm-ops
body: {contract_id, supplement_id, current_spec: [{hash, name, price, qty, sum, date_start}], doc_text: "..."}
returns: {operations: [{action, target_hash, new_values, comment}], mode: "partial"|"full_replace"}
```
## Связанные файлы
- Запрос Sonnet: `contractor/Files/sonnet-v2-request.md`
- Старый код парсинга: `contractor/parser.cfm`
- Старый код сравнения: `contractor/differ.cfm`
- Старый код экстракции: `contractor/extractor.cfm`
- Текущий process: `contractor/process.cfm`
- ВМ-прокси: `contractor/deploy/convert_server.py`
- ВМ-nginx: `contractor/deploy/nginx-contracts.conf`