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

6.6 KiB
Raw Blame History

Сессия 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, миграция не нужна

Технические детали реализации

Таблицы (новые)

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