session-12: event sourcing architecture v2

This commit is contained in:
“Naeel”
2026-06-20 09:55:11 +04:00
parent eadd22bdc1
commit e47e399f7b
@@ -0,0 +1,124 @@
# Сессия 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`