diff --git a/.gitignore b/.gitignore
index 7b62bb7..3fb7494 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,3 +1,6 @@
FILES
contracts-app/
-dogovora/
\ No newline at end of file
+dogovora/
+testgen/out/
+testgen/out_100files/
+contracts-flask/
\ No newline at end of file
diff --git a/Files/opus-request-zip-plan.md b/Files/opus-request-zip-plan.md
new file mode 100644
index 0000000..91bd4c2
--- /dev/null
+++ b/Files/opus-request-zip-plan.md
@@ -0,0 +1,200 @@
+# Запрос к Опусу — анализ и план развития Contracts App (v1.0.178)
+
+Дата: 26.06.2025. **Только инструкции, код не менять.**
+
+---
+
+## 1. Контекст: что за сервис
+
+Сервис **«Сверка договоров»** — обработка договоров облачного провайдера НУБЕС с контрагентами.
+Пользователь загружает ZIP-архивы с документами (договоры, допсоглашения, спецификации),
+система классифицирует, группирует по номерам договоров и сравнивает
+спецификации услуг — показывает diff (ADD/UPDATE/DELETE/UNRESOLVED).
+
+**Архитектура:**
+- **Lucee (CFML)** — фронтенд, домен contractor.luceek8s.dev.nubes.ru
+- **Python (VM)** — бэкенд, домен contracts.kube5s.ru, порт 8766
+- **PostgreSQL** — хранение
+- **nginx** — прокси
+- **LLM** — api.aillm.ru, модель gpt-oss-120b, HTTP/2 через httpx
+
+**Пайплайн:**
+1. Upload (.docx/.pdf/.doc/.zip) → парсинг (python-docx/pdfplumber) → elements_json
+2. Classify — LLM определяет: тип (contract/supplement/specification/other),
+ own_number, parent_number, counterparty, doc_date
+3. Group — Python normalize_number() + matching по номеру договора
+4. Compare — SSE, LLM сравнивает спецификации: ADD/UPDATE/DELETE/UNRESOLVED
+
+---
+
+## 2. Текущий код — ключевые файлы
+
+### 2.1. Classify
+
+**Файл:** `contractor/deploy/services/classify.py`
+
+- `MAX_WORKERS = 4` — параллельные запросы к LLM
+- `_smart_extract()` — выжимка текста: заголовок ~1500 симв + regex-хиты по маркерам
+- `_call_llm_classify()` — вызов LLM, парсинг JSON из ответа
+- `classify_batch()` — ThreadPoolExecutor, классифицирует все pending документы
+- `_safe_json_parse()` — чинит битый JSON из LLM (markdown, trailing commas)
+
+**Промпт classify:** `llm_prompt.py` → `build_classify_prompt(header_text)`.
+Запрашивает у LLM:
+```json
+{"doc_type": "contract|supplement|specification|other",
+ "own_number": "XXX001-03700",
+ "parent_number": "XXX001-03700", // для допников/спек
+ "doc_date": "2025-01-01",
+ "counterparty": "ООО \"Ромашка\""}
+```
+
+**Проблема classify:** при 50+ файлах синхронный вызов таймаутился.
+Сейчас: `subprocess.Popen` → classify_worker.py (async, 202 Accepted).
+Но фронтенд (`app.js` `runClassify()`) не полностью синхронизирован с async-ответом.
+
+### 2.2. Group
+
+**Файл:** `contractor/deploy/services/grouping.py`
+
+- `normalize_number()` — uppercase + только буквы/цифры.
+ «МЭС-123/2024» == «МЭС 123/2024» после нормализации.
+- `group_documents()` — иерархический алгоритм:
+ 1. contract → якорь группы
+ 2. supplement/spec → matching по parent_number (нормализованный)
+ 3. Оставшиеся → виртуальные группы по own_number
+ 4. Без номеров → `__unresolved__`
+- `apply_groups()` — создаёт записи в DB (contracts + supplements)
+
+**Известные баги normalize_number:**
+- Кириллическая `О` vs цифра `0` — не различаются
+- Латинская `C` vs кириллическая `С` — не различаются
+- `Ё` → выпадает
+
+### 2.3. Compare
+
+**Файлы:** `contractor/deploy/convert_server.py` (SSE endpoint `/process-v2`),
+`contractor/deploy/compare.js` (фронтенд SSE-клиент).
+
+**Промпты compare:** `llm_prompt.py`
+- `FALLBACK_EXTRACT` — для первого документа (базовый договор/спецификация):
+ ADD всех строк.
+- `FALLBACK_DIFF` — для допсоглашений: UPDATE/DELETE/ADD/UNRESOLVED.
+ Поддерживает два режима:
+ - `"mode": "partial"` — точечные изменения
+ - `"mode": "full_replace"` — полная замена спецификации
+
+**Проблема compare:** оба режима — «ADD всех строк». LLM не всегда корректно
+сопоставляет строки между v1 и v2 спецификации.
+
+### 2.4. ZIP handling
+
+**Файлы:** `contractor/deploy/files.js` (`addZipFile()`),
+`contractor/deploy/convert_server.py` (`_handle_unzip_upload()`).
+
+Текущая логика:
+1. ZIP загружается через FormData на `/unzip-upload`
+2. Бэкенд распаковывает, возвращает base64 каждого файла
+3. Фронтенд для каждого: base64 → Blob → File → upload (как addRegularFile)
+4. Дубликаты по имени: confirm-диалог, перезапись
+
+**Чего нет:**
+- `zip_source` — связь файла с родительским ZIP
+- Группировка в таблице по ZIP-источнику
+- ID файла = `zip_source + "/" + filename`
+
+### 2.5. Фронтенд
+
+**Ключевые JS-модули:** `contractor/deploy/` — state.js, files.js, groups.js, compare.js, app.js, app_utils.js
+
+**Таблица файлов:** `files.js` `renderFiles()` — плоский список, без группировки по ZIP.
+Уже есть `max-height: 50vh; overflow-y: auto` (скролл).
+
+**Степпер:** `app.js` `renderStepper()` — 4 шага: Загрузка → Классификация → Группировка → Сравнение.
+
+---
+
+## 3. Что сказал заказчик (26.06.2025)
+
+> «А что касается вводных — файлы могут быть в зипах. Как правило, по 1 контрагенту,
+> но я считаю, что по контрагенту может быть несколько зипов, и теоретически
+> могу представить ситуацию, когда будет в одном зипе по нескольким как-то
+> связанным контрагентам (какие-то агентские схемы). Наверно, вероятность того,
+> что в одном зипе один контрагент — весьма высокая, но не 100%.»
+
+Также обсуждалось:
+- Привязка файлов к родительскому ZIP
+- В таблице — ZIP как группирующий заголовок, ниже со сдвигом — вложенные файлы
+- ID файла = имя зипа + "/" + имя файла
+
+---
+
+## 4. Что обсуждали мы (ключевые выводы)
+
+1. **НУБЕС — всегда Исполнитель.** Контрагенты — Заказчики.
+2. **Один batch = один контрагент (почти всегда).**
+ Хоть 1 зип, хоть 5 зипов — всё по одному контрагенту.
+3. **Агентские схемы — исключение**, не основная логика.
+4. **`zip_source`** — для визуальной группировки в таблице.
+ Не влияет на логику classify/group/compare.
+5. **Два сценария UI:**
+ - «Точный» (≤100 файлов) — текущий UI с таблицей и ручным контролем
+ - «Поток» (100+) — упрощённый UI: прогресс-бар, авто-пайплайн, сводный отчёт
+ (без таблицы — 1000 DOM-строк вешают браузер)
+6. **Коллизия одинаковых имён из разных ZIP:**
+ - ID = `zip_source + "/" + filename` → разные сущности
+ - Compare покажет diff между версиями
+
+---
+
+## 5. Что нужно от Опуса
+
+### 5.1. ПЛАН по zip_source
+
+Как именно внедрить привязку файлов к родительскому ZIP на ВСЕХ слоях:
+- `unzip.py` → возвращать `zip_source`
+- `db/documents.py` → поле `zip_source`
+- `files.js` `renderFiles()` → группировка + отступ
+- `state.js` → поле `zip_source`
+- `index.cfm` / Lucee → надо ли менять?
+
+**Варианты отображения:**
+- Вариант А: ZIP как секция-заголовок, под ним файлы с отступом
+- Вариант Б: древовидная структура (раскрывающиеся ZIP'ы)
+- Вариант В: цветовая маркировка по ZIP
+
+Какой лучше для пользователя и почему?
+
+### 5.2. ПЛАН по двум сценариям UI
+
+Как архитектурно разделить «Точный» и «Поток» режимы:
+- Общий код (classify/group/compare не меняются)
+- Разный UI: что показывать, что скрывать
+- Как переключаться между режимами (авто по кол-ву файлов? ручной выбор?)
+- «Поток» — сводный отчёт: структура, что в нём
+
+### 5.3. АНАЛИЗ текущих промптов
+
+Внимательно изучи `llm_prompt.py`:
+- Есть ли проблемы в классификации (counterparty не определяется)
+- Достаточно ли хорош diff-промпт (compare)
+- Нужны ли разные варианты промптов для разных сценариев:
+ - Только НУБЕС-один контрагент (стандартный)
+ - Два контрагента (агентские схемы)
+ - Мусорные документы (акты сверки, счета)
+- Нужна ли корректировка глоссария, примеров
+
+### 5.4. РЕКОМЕНДАЦИИ по улучшению
+
+Что ещё можно улучшить, исходя из анализа кода и требований заказчика:
+- Приоритеты: что делать в первую очередь
+- Риски: что может сломаться
+- Оценка трудозатрат (грубо: маленькая/средняя/большая задача)
+
+---
+
+## 6. Ограничения
+
+- **Код не менять.** Только инструкции и план.
+- Ответ — в чат, подробно, с обоснованием каждого решения.
+- Если есть несколько вариантов — перечислить с плюсами/минусами.
diff --git a/Files/questions-to-customer.md b/Files/questions-to-customer.md
new file mode 100644
index 0000000..2369d47
--- /dev/null
+++ b/Files/questions-to-customer.md
@@ -0,0 +1,52 @@
+# Несколько вопросов по сервису «Сверка договоров»
+
+Чтобы настроить сервис точнее — пара коротких вопросов. Отвечать подробно не нужно,
+достаточно отметить вариант или написать пару слов.
+
+Это не финальный список: когда вы поработаете с сервисом, наверняка появятся
+свои пожелания и вопросы — тогда обсудим остальное.
+
+---
+
+## 1. Объём
+Сколько файлов обычно загружаете за один раз?
+(нужно, чтобы решить — оставить подробную таблицу или сделать упрощённый режим для больших пачек)
+
+- [ ] A. до 10–20
+- [ ] B. 50–100
+- [ ] C. 100+ (сотни/тысячи)
+
+## 2. Как называется НУБЕС в договорах
+В каждом договоре две стороны: вы (Исполнитель) и контрагент (Заказчик).
+Сейчас система иногда путает, кто из них контрагент.
+
+**Под какими названиями НУБЕС встречается в документах?**
+(например: «ООО НУБЕС», «Облачные технологии», ИНН …)
+
+> _ваш ответ:_
+
+## 3. «Мусорные» документы
+В архивах иногда попадаются не-договорные бумаги. Какие можно игнорировать при сверке?
+
+- [ ] акты сверки
+- [ ] счета / счета-фактуры / УПД
+- [ ] акты оказанных услуг
+- [ ] платёжные поручения
+- [ ] другое: _______________
+
+## 4. ZIP-архивы
+Мы поняли так: обычно **один архив = один контрагент**, но по одному контрагенту
+может быть несколько архивов, а изредка в одном архиве — несколько связанных
+контрагентов (агентские схемы). **Всё верно?** Если есть нюансы — допишите.
+
+> _ваш ответ:_
+
+## 5. Что важнее всего в результате
+На что смотрите в первую очередь, когда сверка готова?
+(например: что изменилось в ценах, какие позиции не сопоставились, итоговая сумма…)
+
+> _ваш ответ:_
+
+---
+
+Спасибо! Этого пока достаточно — остальное уточним по ходу.
diff --git a/History/architecture-research-v2-2026-06-27.md b/History/architecture-research-v2-2026-06-27.md
new file mode 100644
index 0000000..39f088a
--- /dev/null
+++ b/History/architecture-research-v2-2026-06-27.md
@@ -0,0 +1,795 @@
+# Архитектурное исследование: Сверка договоров v2
+
+**Дата:** 27.06.2026 | **Для:** DeepSeek V4 Pro | **По заказу:** Владимир Крупский
+
+---
+
+## Блок 1: Общая архитектура
+
+### 1.1 Архитектура «с нуля»
+
+Вот как бы я построил систему, зная все требования сейчас:
+
+```mermaid
+graph TB
+ subgraph "Ввод"
+ A[Файловая шара
/облачный диск]
+ end
+
+ subgraph "Pre-processing Pipeline"
+ B["① Фильтр мусора
━━━━━━━━━━━━━
Детерминированная
(ключевые слова + regex
по первым 2KB текста)"]
+ C["② Парсинг документов
━━━━━━━━━━━━━
Python (pdfplumber + python-docx)
→ elements_json"]
+ D["③ Классификация
━━━━━━━━━━━━━
LLM (лёгкая модель)
+ _smart_extract
→ тип/номер/дата/контрагент"]
+ E["④ Группировка
━━━━━━━━━━━━━
Детерминированная Python
нормализация номеров + matching"]
+ end
+
+ subgraph "Core Processing"
+ F["⑤ Извлечение спецификации
━━━━━━━━━━━━━
LLM (основная модель)
контекст: полный текст
договора/спецификации
→ ADD ops"]
+ G["⑥ Сравнение ДС
━━━━━━━━━━━━━
LLM + Event Sourcing
контекст: текущая spec
+ текст ДС
→ ADD/UPDATE/DELETE/UNRESOLVED"]
+ end
+
+ subgraph "Сверка"
+ H["⑦ Matching CRM ↔ Фискальная
━━━━━━━━━━━━━
Гибрид: хеш-матчинг по
нормализованному имени
+ LLM для несовпадений"]
+ I["⑧ Отчёт о расхождениях
━━━━━━━━━━━━━
diff-представление
подсветка: даты, цены, суммы"]
+ end
+
+ subgraph "Хранилище"
+ J[(PostgreSQL
Документы, Спецификации,
События, Промпты)]
+ K[(Доп. хранилище
CRM-выгрузки
agnostic schema)]
+ end
+
+ subgraph "Обратная связь"
+ L[Ручная коррекция
экспертом]
+ M[Версионирование
исправленных промптов]
+ end
+
+ A --> B --> C --> D --> E --> F --> G
+ G --> H --> I
+ J --- F
+ J --- G
+ K --- H
+ I --> L --> M
+ M -.-> F
+ M -.-> G
+
+ style B fill:#e8f5e9
+ style E fill:#e8f5e9
+ style D fill:#fff3e0
+ style F fill:#fff3e0
+ style G fill:#fff3e0
+ style H fill:#e3f2fd
+```
+
+**Зоны ответственности:**
+
+| Компонент | Где LLM | Где детерминированная логика |
+|---|---|---|
+| ① Фильтр мусора | ❌ НЕТ | Ключевые слова + regex по заголовкам (быстро, 0 токенов) |
+| ② Парсинг | ❌ НЕТ | pdfplumber / python-docx → `elements_json` |
+| ③ Классификация | ✅ ЛЁГКАЯ LLM | `_smart_extract()` выжимка, `_safe_json_parse()` |
+| ④ Группировка | ❌ НЕТ | `normalize_number()` + matching по parent_number |
+| ⑤ Извлечение | ✅ ОСНОВНАЯ LLM | Сборка промпта (`build_prompt`), `_elements_to_text()` |
+| ⑥ Сравнение | ✅ ОСНОВНАЯ LLM | Event Sourcing (apply ops), `_upsert_spec_current()` |
+| ⑦ Matching | ✅ LLM для несовпадений | Хеш-матчинг по нормализованному имени для очевидных |
+| ⑧ Отчёт | ❌ НЕТ | Чистый diff, группировка расхождений по типам |
+
+**Ключевой принцип:** LLM — только там, где нужна семантика. Всё остальное — быстрый детерминированный код. Это даёт:
+- Предсказуемость (детерминированное не ломается при смене модели)
+- Экономию токенов (LLM — дорого и медленно)
+- Отлаживаемость (можно тестировать unit-тестами без LLM)
+
+---
+
+### 1.2 Agent-based vs Pipeline
+
+```mermaid
+graph LR
+ subgraph "Pipeline (текущий)"
+ P1[Upload] --> P2[Parse] --> P3[Classify] --> P4[Group] --> P5[Compare]
+ end
+
+ subgraph "Agent-based (предлагаемый гибрид)"
+ O[Orchestrator Agent]
+ O --> W1[Parse Worker]
+ O --> W2[Classify Worker]
+ O --> W3[Compare Worker]
+ O --> W4[Match Worker]
+ O --> T[Tools: DB, LLM, FileSystem]
+ end
+```
+
+| Критерий | Pipeline | Agent-based |
+|---|---|---|
+| **Плюсы** | Предсказуемый порядок, легче отлаживать, меньше токенов, детерминированные шаги не требуют LLM | Гибкость: оркестратор решает **что делать** на основе промежуточных результатов. Может перепланировать при ошибках |
+| **Минусы** | Жёсткая последовательность. Если шаг упал — либо пропускаем, либо всё стоп. Трудно адаптировать под неожиданные форматы документов | Дороже (каждый шаг оркестратора — LLM-вызов). Сложнее отлаживать. Риск «галлюцинаций» оркестратора |
+| **Когда** | Когда формат входа **известен**, pipeline стабилен | Когда формат входа **неизвестен**, нужно адаптивное поведение |
+
+**Мой вердикт: ГИБРИДНЫЙ подход.**
+
+```
+Orchestrator (лёгкая LLM, ~500 токенов/вызов)
+ │
+ ├── «Это договор?» → Фильтр мусора (детерминирован)
+ ├── «Какой тип?» → Classify worker (LLM, уже есть)
+ ├── «С чем группировать?» → Grouping (детерминирован)
+ ├── «Извлечь спецификацию?» → Extract worker (LLM)
+ └── «Сравнить с CRM?» → Match worker (LLM + хеши)
+```
+
+**Почему не pure agents:**
+- 100+ файлов × 500 токенов оркестратора = 50K+ токенов только на планирование
+- Для **стандартных** договоров ЦОД pipeline предсказуем — хватит 95% случаев
+- Оркестратор нужен только для **краевых случаев**: нестандартный формат, ошибка парсинга, конфликт при группировке
+
+**Инструменты (tools) для агента:**
+- `parse_document(file_id)` → elements_json
+- `classify_document(file_id)` → {type, number, date, counterparty}
+- `extract_spec(contract_id)` → [spec_rows]
+- `compare_supplement(supp_id, current_spec)` → [ops]
+- `match_crm_row(spec_row)` → {crm_match, confidence}
+- `query_db(sql)` → rows (read-only)
+
+---
+
+### 1.3 RAG — нужен ли?
+
+**Кратко: для текущей задачи RAG НЕ НУЖЕН. Хватит контекстного окна.**
+
+Обоснование:
+
+| Что | Почему не RAG |
+|---|---|
+| **Текст одного допника** | 5-50 KB → влезает в контекстное окно gpt-oss-120b (8K токенов ≈ ~24KB текста) |
+| **Текущая спецификация** | 10-50 строк × ~200 симв = 10KB → тоже влезает |
+| **Сравнение договоров** | Не semantic search. Нужно **точное** сопоставление строк, а не «похожие документы» |
+
+**Когда RAG стал бы нужен (v3+):**
+- Если бы нужно было искать **похожие прецеденты** в истории (как раньше решали похожие расхождения)
+- Если бы был корпус из 10 000+ договоров и нужно было искать «как обычно формулируют услугу X»
+- Для чата: «покажи все договоры где цена стойко-места > 50 000»
+
+**Что где хранить:**
+
+| Хранилище | Что |
+|---|---|
+| **PostgreSQL (реляционная)** | Документы, `elements_json`, `spec_current`, `spec_events`, контракты, промпты, CRM-выгрузки |
+| **Файловая система** | Исходные .docx/.pdf (для перепарсивания при смене парсера) |
+| **Векторная БД** (пока НЕ нужно) | Эмбеддинги названий услуг для семантического matching (альтернатива LLM-matching) |
+
+**Но:** если модель сменится на что-то с окном 128K+ токенов (Claude, GPT-4o, Gemini), можно будет отправлять **весь договор целиком** + текущую спецификацию. Тогда `_smart_extract` станет не нужен — LLM сама найдёт нужные строки.
+
+---
+
+## Блок 2: Обработка 100+ файлов
+
+### 2.1 Узкие места и масштабирование
+
+```mermaid
+gantt
+ title Время обработки 100 файлов (текущий pipeline)
+ dateFormat X
+ axisFormat %s
+
+ section Фильтр мусора
+ 100 файлов × 10ms :0, 1
+
+ section Парсинг
+ 100 файлов × 500ms :1, 50
+
+ section Классификация
+ 100 файлов × 2-10s :50, 300
+
+ section Группировка
+ 1 вызов × 100ms :300, 300
+
+ section Сравнение (LLM)
+ 30 допников × 30s :300, 900
+```
+
+**Главное узкое место — КЛАССИФИКАЦИЯ (50-300s для 100 файлов при 4 воркерах).**
+
+Текущий `ThreadPoolExecutor(max_workers=4)` + `classify_worker.py` subprocess — уже правильное решение. Но для 100+ файлов:
+
+**Что ещё станет узким местом:**
+
+| Узкое место | Почему | Решение |
+|---|---|---|
+| **Классификация** | 100 файлов × 5s / 4 воркера = 125s | Увеличить `MAX_WORKERS` до 8-10 (но риск троттлинга api.aillm.ru) |
+| **Сравнение ДС** | 30 допников × 30s последовательно = 900s (15 мин!) | Параллельное сравнение **независимых** групп (разные contract_id — нет гонки) |
+| **Парсинг docx/pdf** | Java POI через Lucee — медленно для 100 файлов | Перенести парсинг на ВМ (pdfplumber/python-docx, см. ниже) |
+| **Память процесса** | 100 `elements_json` в памяти БД | Ок — в БД, не в памяти питона |
+| **api.aillm.ru rate limit** | Бесплатный эндпоинт, неизвестный лимит | Семафор + exponential backoff |
+
+**Предлагаемые улучшения:**
+
+1. **Перенос парсинга на ВМ** (убрать зависимость от Lucee/Java):
+ ```
+ СЕЙЧАС: JS → convert_server → Lucee parser.cfm (Java POI/PDFBox) → обратно на ВМ
+ ПРЕДЛОЖЕНИЕ: JS → convert_server → services/parse.py (pdfplumber + python-docx)
+ ```
+ - python-docx для .docx (чистый Python, без Java)
+ - pdfplumber для .pdf (лучше PDFBox для таблиц)
+ - Убирает latency сетевого вызова Lucee → ВМ
+
+2. **Асинхронная очередь классификации:**
+ ```
+ Upload → Parse → [положить в очередь] → сразу вернуть «файлы загружены»
+ → background: classify → group
+ ```
+ Заказчик не ждёт 125 секунд. Видит прогресс-бар через `/api/batch-progress`.
+
+3. **Параллельное сравнение групп:**
+ ```python
+ # Сейчас: последовательно по всем supps
+ for s in supps: # 30 допников × 30s = 900s
+ compare(s)
+
+ # Предложение: параллельно по НЕЗАВИСИМЫМ группам
+ with ThreadPoolExecutor(max_workers=3) as pool:
+ futures = {pool.submit(compare_group, g): g for g in independent_groups}
+ ```
+ Группы с разными `contract_id` независимы — можно сравнивать параллельно.
+
+---
+
+### 2.2 Очередь (RabbitMQ/Redis/Kafka) или PostgreSQL?
+
+| Критерий | PostgreSQL (текущий) | Redis | RabbitMQ |
+|---|---|---|---|
+| **Простота** | ✅ Уже есть, не надо ставить | Средне | Средне |
+| **Надёжность** | ✅ ACID, не теряем задачи | ❌ Может потерять при перезапуске | ✅ Persistence |
+| **Мониторинг** | ✅ SELECT для просмотра очереди | Нужен redis-cli | Нужен management plugin |
+| **Производительность** | Средне (polling) | ✅ Высокая (pub/sub) | ✅ Высокая |
+| **Подходит для** | **До 1000 файлов/день** | До 10K/день | До 100K/день |
+
+**Вердикт: PostgreSQL ДОСТАТОЧНО для текущего масштаба.**
+
+Для 100+ файлов за раз, несколько раз в неделю — PostgreSQL-очередь через `documents.classify_status = 'pending'` + `classify_worker.py` subprocess — адекватное решение. Не надо усложнять.
+
+**Когда переходить на RabbitMQ/Redis:**
+- Если заказчик начнёт загружать 1000+ файлов **ежедневно**
+- Если появятся **несколько воркеров** на разных машинах
+- Если нужен **приоритет** (срочные договоры вне очереди)
+
+**Предлагаемая схема на PostgreSQL (минимальные изменения):**
+
+```sql
+-- Добавляем поле для очереди
+ALTER TABLE documents ADD COLUMN IF NOT EXISTS classify_priority INT DEFAULT 0;
+ALTER TABLE documents ADD COLUMN IF NOT EXISTS classify_attempts INT DEFAULT 0;
+ALTER TABLE documents ADD COLUMN IF NOT EXISTS classify_next_attempt TIMESTAMPTZ;
+
+-- Воркер забирает задачи с ORDER BY priority, attempt, next_attempt
+SELECT * FROM documents
+WHERE classify_status = 'pending'
+ AND (classify_next_attempt IS NULL OR classify_next_attempt <= NOW())
+ORDER BY classify_priority DESC, classify_attempts ASC
+LIMIT 10
+FOR UPDATE SKIP LOCKED; -- конкурентное потребление
+```
+
+---
+
+### 2.3 «Мусорная» фильтрация на раннем этапе
+
+**Это КРИТИЧЕСКИ важно для 100+ файлов.** Если 50% файлов — счета/акты/платёжки, а мы их парсим и классифицируем — тратим 50% ресурсов впустую.
+
+**Предлагаемый трёхэтапный фильтр:**
+
+```
+Этап 1: Фильтр по имени файла (0ms, детерминирован)
+ ├── regex: (сч[её]т|акт|плат[её]ж|УПД|сверк|инвойс|invoice|act|payment)
+ ├── сразу помечать doc_type='garbage', НЕ парсить
+ └── точность: ~40% мусора
+
+Этап 2: Фильтр по первым 2KB текста (после парсинга, 10ms, детерминирован)
+ ├── Ключевые слова в заголовке:
+ │ «СЧЕТ-ФАКТУРА», «АКТ СВЕРКИ», «АКТ оказанных услуг»,
+ │ «ПЛАТЁЖНОЕ ПОРУЧЕНИЕ», «УПД», «СЧЕТ НА ОПЛАТУ»
+ ├── Если нашли → помечать doc_type='garbage', НЕ классифицировать LLM
+ └── точность: ~55% мусора (суммарно)
+
+Этап 3: LLM-классификация (оставшиеся, 2-10s)
+ ├── Только для файлов, прошедших этапы 1-2
+ └── LLM определяет точный тип: contract/supplement/specification/other
+```
+
+**Реализация этапа 2 (в `services/classify.py` перед `_call_llm_classify`):**
+
+```python
+GARBAGE_MARKERS = [
+ 'СЧЕТ-ФАКТУРА', 'СЧЕТ НА ОПЛАТУ', 'АКТ СВЕРКИ', 'АКТ ОКАЗАННЫХ УСЛУГ',
+ 'АКТ ВЫПОЛНЕННЫХ РАБОТ', 'ПЛАТЁЖНОЕ ПОРУЧЕНИЕ', 'УНИВЕРСАЛЬНЫЙ ПЕРЕДАТОЧНЫЙ',
+ 'УПД', 'ПЛАТЕЖНОЕ ПОРУЧЕНИЕ'
+]
+
+def is_garbage_by_header(text):
+ """Быстрая проверка — не гонять LLM на мусор."""
+ header = text[:2000].upper()
+ for marker in GARBAGE_MARKERS:
+ if marker in header:
+ return True
+ return False
+```
+
+**Экономия:** при 50% мусора в 100 файлах: вместо 100 LLM-вызовов (500s) → 50 LLM-вызовов (250s). **Экономия 50% времени и токенов.**
+
+---
+
+## Блок 3: Сравнение CRM ↔ фискальная система
+
+### 3.1 Agnostic к источнику — проектирование модуля сравнения
+
+**Проблема:** мы не знаем формат CRM. Сегодня — одна CRM, завтра — другая система.
+
+**Решение: Абстрактный интерфейс + адаптеры.**
+
+```mermaid
+graph TB
+ subgraph "Источники данных"
+ CRM1[CRM
(текущая)]
+ CRM2[Другая система
(будущая)]
+ FISC[Фискальная система
(из договоров)]
+ end
+
+ subgraph "Адаптеры (по одному на источник)"
+ A1[CRM Adapter
нормализует поля
в канонический формат]
+ A2[Future Adapter]
+ end
+
+ subgraph "Каноническая модель строки"
+ CAN[CanonicalRow
────────────
service_name: str
article_code: str
price: Decimal
qty: Decimal
sum: Decimal
date_start: Date
date_end: Date
unit: str
source: 'crm' | 'fiscal'
source_id: str]
+ end
+
+ subgraph "Matcher (agnostic)"
+ MATCH[RowMatcher
────────────
match_by_hash
match_by_name
match_by_llm
→ MatchResult]
+ end
+
+ CRM1 --> A1 --> CAN
+ CRM2 --> A2 --> CAN
+ FISC --> CAN
+ CAN --> MATCH
+```
+
+**Каноническая модель `CanonicalRow`:**
+
+```python
+@dataclass
+class CanonicalRow:
+ """Строка спецификации в каноническом формате (source-agnostic)."""
+ service_name: str # нормализованное название услуги
+ article_code: str | None # артикул (если есть)
+ price: Decimal | None
+ qty: Decimal | None
+ sum: Decimal | None
+ date_start: date | None # КРИТИЧНОЕ ПОЛЕ
+ date_end: date | None
+ unit: str | None # кВт, шт., U, Мбит/с, ...
+ source: str # 'crm' | 'fiscal'
+ source_id: str # ссылка на оригинал (для аудита)
+
+ @property
+ def name_hash(self) -> str:
+ """Нормализованный хеш названия для быстрого matching."""
+ return _hash(normalize_service_name(self.service_name))
+```
+
+**Адаптер для CRM (пример):**
+
+```python
+class CRMSourceAdapter:
+ """Адаптер для конкретной CRM. Меняется только этот класс."""
+
+ def extract_rows(self, crm_export_path: str) -> list[CanonicalRow]:
+ """Читает CSV/JSON/API CRM → список CanonicalRow."""
+ # Специфично для CRM заказчика
+ df = pd.read_csv(crm_export_path, sep=';')
+ rows = []
+ for _, r in df.iterrows():
+ rows.append(CanonicalRow(
+ service_name=r['Наименование'],
+ article_code=r.get('Артикул'),
+ price=Decimal(str(r['Цена'])),
+ qty=Decimal(str(r['Кол-во'])),
+ sum=Decimal(str(r['Сумма'])),
+ date_start=parse_date(r['Дата начала']),
+ date_end=parse_date(r.get('Дата окончания')),
+ unit=r.get('Ед. изм.'),
+ source='crm',
+ source_id=r['ID'],
+ ))
+ return rows
+```
+
+**Ключевое:** когда завтра появится другая система — пишем **только новый адаптер**. Matcher не меняется.
+
+---
+
+### 3.2 Matching строк: CRM ↔ фискальная система
+
+**Трёхуровневый matching (от быстрого к точному):**
+
+```mermaid
+graph LR
+ A[CRM строка] --> B{① Хеш-матчинг
по name_hash}
+ B -->|Совпал| D[✓ MATCH (100% confidence)]
+ B -->|Не совпал| C{② Семантический
по имени}
+ C -->|Высокая confidence| E[✓ MATCH (80-95% confidence)]
+ C -->|Низкая| F{③ LLM-матчинг}
+ F --> G[✓ MATCH / ✗ NO MATCH
+ объяснение]
+```
+
+**① Хеш-матчинг (0ms, 0 токенов):**
+```python
+def match_by_hash(crm_rows, fiscal_rows):
+ """Точное совпадение по нормализованному имени."""
+ fiscal_by_hash = {r.name_hash: r for r in fiscal_rows}
+ matched = []
+ unmatched = []
+ for crm_row in crm_rows:
+ if crm_row.name_hash in fiscal_by_hash:
+ matched.append((crm_row, fiscal_by_hash[crm_row.name_hash], 1.0))
+ else:
+ unmatched.append(crm_row)
+ return matched, unmatched
+```
+
+**② Семантический matching (Python, без LLM):**
+```python
+def match_by_name_similarity(crm_row, fiscal_rows, threshold=0.8):
+ """Fuzzy matching по названиям услуг."""
+ from difflib import SequenceMatcher
+
+ best_score = 0
+ best_match = None
+ for f_row in fiscal_rows:
+ score = SequenceMatcher(None,
+ crm_row.service_name.lower(),
+ f_row.service_name.lower()
+ ).ratio()
+ if score > best_score:
+ best_score = score
+ best_match = f_row
+
+ if best_score >= threshold:
+ return best_match, best_score
+ return None, 0
+```
+
+**③ LLM-матчинг (для оставшихся ~10-20% сложных случаев):**
+```
+Промпт:
+«Вот строка из CRM: {crm_row}
+Вот строки из фискальной системы: {fiscal_rows}
+Найди соответствие или скажи что соответствия нет.
+Учитывай: синонимы ("аренда стойки" = "colocation"),
+ объединение/разделение строк,
+ PAYG-услуги без артикулов.»
+```
+
+**Почему не только LLM:** 100 строк × 500 токенов = 50K токенов только на matching. А ①+② обрабатывают 80% за 0 токенов.
+
+---
+
+### 3.3 Даты — критичный фокус
+
+**Почему даты — главный источник расхождений (со слов заказчика):**
+
+- CRM может иметь `date_start = 01.01.2025`
+- Фискальная система (договор) может иметь `date_start = 15.01.2025` (дата подписания акта приёмки, а не договора)
+- Разница в 14 дней → недоплата/переплата за 14 дней × стоимость услуги
+
+**Стратегия сравнения с акцентом на `date_start`:**
+
+```python
+def compare_dates(crm_row, fiscal_row):
+ """Сравнение дат — основной фокус."""
+ result = {
+ 'matched': True,
+ 'date_start_match': True,
+ 'date_start_diff_days': 0,
+ 'date_start_warning': None,
+ 'price_match': True,
+ 'sum_match': True,
+ }
+
+ # Сравнение дат
+ if crm_row.date_start and fiscal_row.date_start:
+ diff = (crm_row.date_start - fiscal_row.date_start).days
+ result['date_start_diff_days'] = diff
+ if diff != 0:
+ result['date_start_match'] = False
+ if abs(diff) <= 5:
+ result['date_start_warning'] = 'minor' # возможно округление до месяца
+ elif abs(diff) <= 31:
+ result['date_start_warning'] = 'significant' # расхождение на месяц
+ else:
+ result['date_start_warning'] = 'critical' # серьёзное расхождение
+
+ # Если даты не совпадают, но всё остальное совпадает (имя, цена, количество)
+ if not result['date_start_match'] and result['price_match'] and result['sum_match']:
+ result['likely_cause'] = 'date_input_error' # вероятно ошибка ввода даты
+
+ return result
+```
+
+**Визуализация расхождений (диаграмма Ганта):**
+
+```
+Услуга | Янв | Фев | Март | Апр |
+────────────────────┼───────┼───────┼───────┼───────|
+CRM: Стойка 10kW |████████████████|
+Фискал: Стойка 10kW | ████████████████|
+ ^^^^— расхождение 15 дней
+```
+
+Это можно отрендерить как HTML/CSS бары — наглядно видны сдвиги дат.
+
+---
+
+### 3.4 PAYG (суффикс `-m`) — стратегия сопоставления
+
+**Проблема PAYG:**
+- PAYG-услуги (pay-as-you-go) — переменное потребление, нет фиксированной цены
+- В счетах нет кодов артикулов
+- Например: «IP-адрес IPv4-m» в CRM vs «IP-адрес IPv4» в договоре
+
+**Стратегия:**
+
+```python
+def normalize_payg_name(name):
+ """Убирает суффикс -m для сопоставления PAYG-услуг."""
+ import re
+ # Убираем суффикс -m (с границей слова или концом строки)
+ normalized = re.sub(r'-m(\s|$)', r'\1', name)
+ # Примеры:
+ # «IP-адрес IPv4-m» → «IP-адрес IPv4»
+ # «Канал связи 100Мбит/с-m» → «Канал связи 100Мбит/с»
+ return normalized
+```
+
+**Алгоритм для PAYG:**
+1. При matching по имени — нормализовать **оба** названия (убрать `-m`)
+2. Если match нашёлся → отметить флагом `payg: true`
+3. Для PAYG-услуг **не сравнивать суммы** (они переменные), сравнивать только **факт наличия услуги** и **единицу измерения**
+4. Для PAYG-услуг `date_start` **особенно важен** — PAYG тарифицируется с даты начала
+
+```python
+def match_payg(crm_row, fiscal_rows):
+ """Особая логика для PAYG."""
+ crm_name_normalized = normalize_payg_name(crm_row.service_name)
+
+ for f_row in fiscal_rows:
+ f_name_normalized = normalize_payg_name(f_row.service_name)
+
+ if crm_name_normalized == f_name_normalized:
+ return MatchResult(
+ matched=True,
+ payg=True,
+ compare_sum=False, # суммы не сравниваем для PAYG
+ compare_date_start=True, # даты КРИТИЧНЫ
+ note=f'PAYG: {crm_row.service_name} ↔ {f_row.service_name}',
+ )
+
+ return MatchResult(matched=False)
+```
+
+---
+
+## Блок 4: Итеративность и неопределённость
+
+### 4.1 Менять промпты/модели/подходы без переписывания кода
+
+**Что уже есть (✅ хорошо):**
+- Промпты в БД с версионированием (`prompts` таблица + `prompt.cfm`/`db/prompts.py`)
+- Редактор промптов с историей версий
+- Активный промпт выбирается из БД, не хардкод
+
+**Что предлагаю добавить:**
+
+```python
+# config.py — ЕДИНСТВЕННОЕ место для конфигурации LLM
+@dataclass
+class LLMConfig:
+ """Меняется без правки кода — через БД или env."""
+ model: str = os.environ.get("LLM_MODEL", "gpt-oss-120b")
+ url: str = os.environ.get("LLM_URL", "https://api.aillm.ru/v1/chat/completions")
+ max_tokens: int = int(os.environ.get("LLM_MAX_TOKENS", "8000"))
+ temperature: float = float(os.environ.get("LLM_TEMPERATURE", "0.1"))
+ timeout: int = int(os.environ.get("LLM_TIMEOUT", "120"))
+
+ # Разные модели для разных задач
+ classify_model: str = os.environ.get("LLM_CLASSIFY_MODEL", model) # полегче
+ extract_model: str = os.environ.get("LLM_EXTRACT_MODEL", model) # основная
+ match_model: str = os.environ.get("LLM_MATCH_MODEL", model) # для сверки
+```
+
+**Принцип: всё что может поменяться — в БД или env. Код — только движок.**
+
+| Что меняется | Где менять | Без правки кода? |
+|---|---|---|
+| Промпт | БД `prompts` → activate | ✅ Да |
+| Модель LLM | env `LLM_MODEL` | ✅ Да |
+| Температура | env `LLM_TEMPERATURE` | ✅ Да |
+| URL API | env `LLM_URL` | ✅ Да |
+| Garbage-маркеры | `garbage_markers.json` в БД или файле | ✅ Да |
+| Стратегия matching | `matching_rules` в БД | ✅ Да (если сделать rules engine) |
+| Порядок pipeline | ❌ Пока хардкод | 🔧 Можно сделать DAG в БД |
+
+**Предложение: Pipeline as DAG в БД:**
+
+```sql
+CREATE TABLE pipeline_steps (
+ id SERIAL PRIMARY KEY,
+ name TEXT NOT NULL, -- 'filter_garbage', 'parse', 'classify', 'extract', ...
+ handler TEXT NOT NULL, -- 'services.classify:classify_batch'
+ depends_on INT[] DEFAULT '{}', -- какие шаги должны быть завершены
+ config JSONB DEFAULT '{}', -- параметры шага
+ enabled BOOLEAN DEFAULT true,
+ created_at TIMESTAMPTZ DEFAULT NOW()
+);
+```
+
+Меняя записи в этой таблице, можно переставлять шаги или добавлять новые **без правки кода**.
+
+---
+
+### 4.2 MVP-границы: v1, v2, v3
+
+```
+v1 (MVP) — БАЗОВАЯ ФУНКЦИЯ
+├── ✅ Загрузка 100+ файлов (из локали/файловой шары)
+├── ✅ Фильтр мусора (этапы 1-2, детерминированные)
+├── ✅ Парсинг docx/pdf на ВМ (убрать зависимость от Lucee)
+├── ✅ Классификация (LLM, параллельно)
+├── ✅ Группировка по контрагентам
+├── ✅ Извлечение спецификации (LLM)
+├── ✅ Сравнение ДС (LLM + Event Sourcing)
+├── ✅ Выгрузка результата (CSV/JSON)
+├── ❌ БЕЗ сверки с CRM (только извлечение из договоров)
+├── ❌ БЕЗ красивого UI (минимальный интерфейс для отладки)
+└── Домен: contracts.kube5s.ru
+
+v2 — СВЕРКА
+├── ✅ Адаптер CRM (первый источник)
+├── ✅ Matching CRM ↔ фискальная (трёхуровневый)
+├── ✅ Отчёт о расхождениях (diff)
+├── ✅ Подсветка дат
+├── ✅ PAYG-обработка
+├── ✅ Ручная коррекция экспертом
+└── Домен: check.kube5s.ru
+
+v3 — ЮЗАБЕЛЬНОСТЬ
+├── ✅ Красивый UI (две панели, отчёт)
+├── ✅ Векторная БД для семантического поиска
+├── ✅ Чат (Q&A по всем договорам)
+├── ✅ Автообучение на коррекциях (few-shot из истории)
+├── ✅ Экспорт в Excel
+└── Домен: contracts.kube5s.ru (единый)
+```
+
+**Почему сверка с CRM — это v2, а не v1:**
+- Заказчик сам говорит: «сейчас и с подходом всё неясно»
+- Сначала надо доказать что LLM **вообще** может точно извлечь спецификацию из 100+ договоров
+- Потом, имея эталонные данные, строить сверку
+- Это снижает риск: не строим сложный matching для неподтверждённого качества извлечения
+
+---
+
+### 4.3 Цикл обратной связи
+
+```mermaid
+graph TB
+ A[LLM извлекает спецификацию] --> B[Результат показан эксперту]
+ B --> C{Эксперт: верно?}
+ C -->|✅ Да| D[Сохраняем как
положительный пример]
+ C -->|❌ Нет| E[Эксперт исправляет]
+ E --> F[Сохраняем пару
❌ было → ✅ стало]
+ F --> G[Аналитика ошибок
какие типы ошибок частые?]
+ G --> H{Можно исправить
промптом?}
+ H -->|Да| I[Правим промпт
новая версия]
+ H -->|Нет| J[Меняем подход
алгоритм / модель]
+ I --> K[Few-shot примеры
в промпт]
+ D --> K
+ K --> A
+
+ style C fill:#fff3e0
+ style G fill:#e3f2fd
+```
+
+**Конкретная реализация:**
+
+```sql
+-- Таблица коррекций эксперта
+CREATE TABLE expert_corrections (
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
+ event_id UUID REFERENCES spec_events(id), -- какое событие LLM
+ corrected_values JSONB NOT NULL, -- что исправил эксперт
+ correction_type TEXT, -- 'name_fix', 'date_fix', 'price_fix', 'missing_row', 'extra_row'
+ expert_comment TEXT,
+ used_in_prompt BOOLEAN DEFAULT false, -- включено в few-shot?
+ created_at TIMESTAMPTZ DEFAULT NOW()
+);
+```
+
+**Как это использовать:**
+
+1. **Быстрый цикл (часы):** эксперт исправил → сохранили в `expert_corrections` → аналитика показывает «5 из 10 ошибок — даты» → правим промпт (добавляем акцент на даты) → активируем новую версию → лучше.
+
+2. **Few-shot обучение (дни):** накопили 20+ коррекций одного типа → добавляем в промпт как few-shot примеры:
+ ```
+ ПРИМЕРЫ ОШИБОК (НЕ ПОВТОРЯЙ):
+ ❌ Было: "Аренда стойко-места" с date_start: null
+ ✅ Верно: "Аренда стойко-места" с date_start: "2025-01-01" (дата в преамбуле договора)
+ ```
+
+3. **A/B тестирование промптов (недели):** запускаем старый и новый промпт на одном документе → сравниваем результаты → выбираем лучший.
+
+**Ключевое:** не пытаемся «обучить модель» (это не наша модель, gpt-oss-120b — API). Вместо этого:
+- Улучшаем промпты
+- Добавляем few-shot примеры
+- Меняем подход к извлечению
+- **Фиксируем все решения в БД** — чтобы через месяц понять что работало, а что нет
+
+---
+
+## Итоговая архитектура (сводка)
+
+```mermaid
+graph TB
+ subgraph "v1: Извлечение (MVP)"
+ U1[100+ файлов] --> F1[Фильтр мусора ⚡0ms]
+ F1 --> P1[Парсинг Python ⚡500ms]
+ P1 --> C1[Классификация LLM 🔥2-10s]
+ C1 --> G1[Группировка Python ⚡100ms]
+ G1 --> E1[Извлечение LLM 🔥30s]
+ E1 --> S1[(spec_current)]
+ end
+
+ subgraph "v2: Сверка"
+ CRM[CRM-выгрузка] --> AD[CRM Adapter]
+ S1 --> MC[Matcher ⚡хеш → fuzzy → LLM]
+ AD --> MC
+ MC --> RPT[Отчёт о расхождениях]
+ end
+
+ subgraph "Обратная связь"
+ RPT --> EXP[Эксперт]
+ EXP --> CORR[expert_corrections]
+ CORR --> PROMPT[Улучшение промптов]
+ PROMPT -.-> E1
+ end
+
+ style F1 fill:#e8f5e9
+ style G1 fill:#e8f5e9
+ style C1 fill:#fff3e0
+ style E1 fill:#fff3e0
+ style MC fill:#e3f2fd
+```
+
+---
+
+## Практические рекомендации для DeepSeek V4 Pro
+
+### Что кодить СЕЙЧАС (Фаза 0: изоляция)
+
+Как описано в `opus-plan-review-2026-06-27.md`, план Opus из 6 фаз — правильный вектор. Но предлагаю **упростить Фазу 0**:
+
+**Ф0. Предпосылки (1-2 часа):**
+1. `pg_dump --schema-only` с продакшена → `schema.sql`
+2. Создать новую БД `contracts_flask` на ВМ, применить `schema.sql`
+3. DNS `check.kube5s.ru` → уже есть ✅
+4. Пустой репо `contracts-flask` → уже есть ✅
+5. `LLM_KEY` → должен быть в `.env` на ВМ
+
+**Ф1. Бэкенд на ВМ (основная работа):**
+- `~/contracts-flask/` — копия `deploy/` (db/, services/, llm_prompt.py, convert_server.py)
+- ИСПРАВИТЬ `llm_prompt.py` — убрать зависимость от Lucee (`_fetch_prompt()` → `db.prompts.get_active()`)
+- ИСПРАВИТЬ `services/llm.py` — взять версию из репы (свежее)
+- Порт 8777, БД `contracts_flask`, systemd-юнит `contracts-flask.service`
+
+**Ф2-Ф3. Nginx + JS:**
+- Nginx: `check.kube5s.ru` → :8777
+- JS: скопировать 6 файлов, поменять `VM_API='https://check.kube5s.ru'`
+
+**Ф4+ отложить** — морда на managed Flask не нужна для v1 MVP. Используем текущий index.cfm как есть, или минимальный HTML на ВМ.
diff --git a/History/architecture/vm-layout.md b/History/architecture/vm-layout.md
new file mode 100644
index 0000000..a374fe3
--- /dev/null
+++ b/History/architecture/vm-layout.md
@@ -0,0 +1,172 @@
+# Устройство ВМ contracts.kube5s.ru
+
+> **ВАЖНО**: Это актуальная документация. Старый `architecture.md` описывает мёртвый Flask `contracts-app/site/` — к ВМ отношения не имеет.
+
+---
+
+## Где что лежит
+
+```
+РЕПОЗИТОРИЙ (локально) ВМ (5.172.178.213)
+/home/naeel/nubes/contracts/ /home/naeel/contracts/
+│ │
+├── contractor/ │
+│ └── deploy/ ◀─── sync.sh ───▶ ВСЁ содержимое deploy/
+│ ├── app.js ├── app.js
+│ ├── app_utils.js ├── app_utils.js
+│ ├── compare.js ├── compare.js
+│ ├── files.js ├── files.js
+│ ├── groups.js ├── groups.js
+│ ├── state.js ├── state.js
+│ ├── tests.js ├── (нет на ВМ)
+│ ├── classify_worker.py ├── classify_worker.py
+│ ├── convert_doc.py ├── convert_doc.py
+│ ├── convert_server.py ◀── ГЛАВНЫЙ ──▶ convert_server.py (порт 8766)
+│ ├── llm_prompt.py ├── llm_prompt.py
+│ ├── nginx-contracts.conf ├── nginx-contracts.conf
+│ ├── db/ ├── db/
+│ │ ├── __init__.py │ ├── __init__.py
+│ │ ├── connection.py │ ├── connection.py
+│ │ ├── contracts.py │ ├── contracts.py
+│ │ ├── documents.py │ ├── documents.py
+│ │ ├── prompts.py │ ├── prompts.py
+│ │ ├── spec_current.py │ ├── spec_current.py
+│ │ ├── spec_events.py │ ├── spec_events.py
+│ │ └── supplements.py │ └── supplements.py
+│ ├── services/ ├── services/
+│ │ ├── __init__.py │ ├── __init__.py
+│ │ ├── classify.py │ ├── classify.py
+│ │ ├── grouping.py │ ├── grouping.py
+│ │ ├── llm.py │ ├── llm.py
+│ │ ├── parse.py │ ├── parse.py
+│ │ ├── process.py │ ├── process.py
+│ │ ├── unzip.py │ ├── unzip.py
+│ │ └── upload.py │ └── upload.py
+│ └── sync.sh │
+│ ├── site/ ← Flask UI (НЕ из deploy)
+│ ├── .env ← VM-специфично
+│ ├── gunicorn.conf.py
+│ ├── start.sh
+│ ├── logs/
+│ │
+│ ├── classify.py ← ⛔ МУСОР (не юзается)
+│ ├── grouping.py ← ⛔ МУСОР (не юзается)
+│ ├── prompts.py ← ⛔ МУСОР (не юзается)
+│ └── unzip.py ← ⛔ МУСОР (не юзается)
+│
+├── contracts-app/ ← ⛔ МЁРТВЫЙ Flask v1, к ВМ отношения НЕ ИМЕЕТ
+│
+├── contractor/ ← ColdFusion/Lucee (старый бекенд, не на ВМ)
+│
+└── contracts-vm/ ← ?
+```
+
+---
+
+## ⛔ КОРНЕВЫЕ .py НА ВМ — МУСОР
+
+На ВМ в `~/contracts/` лежат файлы:
+- `classify.py`
+- `grouping.py`
+- `prompts.py`
+- `unzip.py`
+
+**Эти файлы НЕ ИМПОРТИРУЮТСЯ и НЕ ИСПОЛЬЗУЮТСЯ.** Они остались от старой плоской структуры.
+
+Реально используемые версии лежат в подпапках:
+- `services/classify.py` ✅
+- `services/grouping.py` ✅
+- `db/prompts.py` ✅
+- `services/unzip.py` ✅
+
+`convert_server.py` импортирует ТОЛЬКО из `db/` и `services/`:
+```python
+from db import prompts as db_prompts
+from db.connection import DB_CONFIG, execute
+from db import supplements as db_supplements
+from db import documents as db_documents
+from db import spec_current as db_spec_current
+from services.upload import handle_upload
+from services.unzip import handle_unzip
+from services.process import run_pipeline
+from llm_prompt import build_prompt
+```
+
+---
+
+## Архитектура на ВМ
+
+```
+Браузер
+ │
+ ▼
+Nginx :443 (nginx-contracts.conf)
+ │
+ ├── / → 127.0.0.1:5001 (Flask, gunicorn)
+ │ site/app.py — отдаёт HTML (index.html, upload.html)
+ │ Статика: templates/, static/
+ │
+ └── /upload, /convert-doc, /unzip-upload,
+ /llm-ops, /process-v2, /parse-pdf, /static/
+ → 127.0.0.1:8766 (convert_server.py)
+ │
+ ├── db/ — PostgreSQL через psycopg2
+ ├── services/ — бизнес-логика
+ └── llm_prompt.py — сборка промптов
+```
+
+### Три процесса
+
+| Процесс | Порт | Что | Запуск |
+|---|---|---|---|
+| gunicorn | 5001 | Flask UI | `start.sh` |
+| convert_server.py | 8766 | **Главный API-сервер** | `sync.sh` (после деплоя) |
+| nginx | 443 | Прокси + SSL | systemd |
+
+### JS-файлы — клиентские
+
+`app.js`, `files.js`, `groups.js`, `state.js`, `compare.js`, `app_utils.js` — это **клиентский JavaScript**. Они не запускаются на ВМ как процесс. Их отдаёт Flask через HTML-шаблоны, и они выполняются в браузере.
+
+---
+
+## Деплой (sync.sh)
+
+```bash
+# Запускать из contractor/
+bash deploy/sync.sh
+```
+
+**Сейчас деплоит только 2 файла:**
+- `convert_server.py`
+- `convert_doc.py`
+
+**Должен деплоить ВСЁ из `deploy/`** (кроме `sync.sh` и `__pycache__`).
+
+После деплоя — `pkill -f convert_server.py && nohup python3 convert_server.py &`
+
+### Что НЕ деплоить
+
+- `site/` — Flask UI, живёт своей жизнью
+- `.env` — переменные окружения ВМ
+- `gunicorn.conf.py`, `start.sh` — конфиги ВМ
+- `logs/` — рантайм
+
+---
+
+## Как проверять соответствие
+
+```bash
+# md5 всех файлов в db/ и services/ на ВМ
+ssh naeel@5.172.178.213 'md5sum ~/contracts/db/*.py ~/contracts/services/*.py'
+
+# Сравнить с локальными
+md5sum contractor/deploy/db/*.py contractor/deploy/services/*.py
+```
+
+Корневые `classify.py`, `grouping.py`, `prompts.py`, `unzip.py` — **НЕ проверять**, это мусор.
+
+---
+
+## Версия
+
+Актуально на 2026-06-27. При изменениях — обновлять.
diff --git a/History/features/teach-flow-vm-feedback-tests.md b/History/features/teach-flow-vm-feedback-tests.md
new file mode 100644
index 0000000..ec9e807
--- /dev/null
+++ b/History/features/teach-flow-vm-feedback-tests.md
@@ -0,0 +1,93 @@
+# Teach flow VM smoke tests
+
+Дата: 26.06.2026 | Проверка isolated teaching flow на VM после внедрения.
+
+## Что проверяли
+
+Проверялся Flask-слой VM через test client с подменой DB-слоя, чтобы подтвердить поведение новых endpoints без риска для боевой БД.
+
+### Проверенные маршруты
+
+- `GET /teach`
+- `GET /teach/api/meta`
+- `GET /teach/api/contracts`
+- `GET /teach/api/context?supplement_id=...`
+- `POST /teach/api/feedback`
+- `GET /teach/api/feedback?contract_id=...&supplement_id=...`
+
+## Результат smoke-test
+
+Все маршруты вернули `200 OK` в mocked окружении.
+
+### `/teach`
+
+- Страница отдала HTML с заголовком `Сверка договоров — обучение`.
+
+### `/teach/api/meta`
+
+Вернуло дефолтные метаданные:
+
+```json
+{
+ "ok": true,
+ "prompt_version": "vm-teach-v1",
+ "model_name": "gpt-oss-120b"
+}
+```
+
+### `/teach/api/contracts`
+
+Вернуло список договоров и допников в ожидаемой структуре:
+- `contract_id`
+- `contract_number`
+- `client`
+- `date_signed`
+- `supplements[]`
+
+### `/teach/api/context`
+
+Вернуло:
+- `supplement`
+- `current_rows`
+- `previous_rows`
+
+### `POST /teach/api/feedback`
+
+Проверена запись feedback со значениями:
+- `scope = row`
+- `verdict = error`
+- `error_type = wrong_price`
+- `field = price`
+- `llm_value` и `correct_value` как JSON
+- `prompt_version = vm-teach-v1`
+- `model_name = gpt-oss-120b`
+- `doc_mode = amendment`
+
+Запись успешно ушла в mock cursor и commit был вызван.
+
+### `GET /teach/api/feedback`
+
+Возвратил сохранённую запись в читаемом JSON виде.
+
+## Что всплыло по окружению
+
+Во время проверки не хватало runtime-зависимостей в VM-venv:
+- `python-dotenv`
+- `Flask`
+- `psycopg2-binary`
+- `httpx`
+- `pdfplumber`
+- `python-docx`
+- `redis`
+
+Эти пакеты были установлены в VM-venv, после чего smoke-test прошёл.
+
+## Вывод
+
+Isolated teaching flow на VM не только компилируется, но и проходит mocked smoke-test по основным endpoint'ам:
+- чтение метаданных,
+- загрузка списка договоров,
+- загрузка контекста допника,
+- запись и чтение feedback.
+
+Старый Lucee-frontend и основной compare pipeline при этом не затрагивались.
diff --git a/History/features/teach-flow-vm-feedback.md b/History/features/teach-flow-vm-feedback.md
new file mode 100644
index 0000000..43ef1fa
--- /dev/null
+++ b/History/features/teach-flow-vm-feedback.md
@@ -0,0 +1,115 @@
+# Feature: isolated teaching flow on VM
+
+Дата: 26.06.2026 | Переход от обсуждения идеи feedback-learning к реальному isolated-flow на VM.
+
+## Что решили
+
+- Lucee-mordа не трогаем.
+- Новая страница обучения живёт на VM по прямому URL.
+- Рабочий compare pipeline не меняем.
+- Feedback пишется только в отдельную таблицу `feedback`.
+- Для аналитики сохраняем `prompt_version` и `model_name`.
+- Значения по умолчанию берём из VM metadata endpoint / env, а не из Lucee.
+
+## Что сделано
+
+### 1. Изолированный Flask blueprint
+
+Добавлен новый blueprint `teach_bp` в VM-слой:
+- `/teach` — отдельная страница обучения.
+- `/teach/api/meta` — дефолтные метаданные для страницы.
+- `/teach/api/contracts` — список договоров и допников.
+- `/teach/api/context` — текущие строки спецификации и предыдущие строки для выбранного допника.
+- `/teach/api/feedback` — чтение и запись feedback.
+
+Файлы:
+- [contracts-app/site/teach.py](../../contracts-app/site/teach.py)
+- [contracts-app/site/app.py](../../contracts-app/site/app.py)
+
+### 2. Отдельная таблица feedback
+
+Добавлена таблица `feedback` в DDL приложения. В ней хранятся:
+- `contract_id`
+- `supplement_id`
+- `event_seq`
+- `scope`
+- `verdict`
+- `error_type`
+- `field`
+- `service_name`
+- `llm_value`
+- `correct_value`
+- `prompt_version`
+- `model_name`
+- `doc_mode`
+- `comment`
+
+Файл:
+- [contracts-app/site/schema.py](../../contracts-app/site/schema.py)
+
+### 3. Teach UI
+
+Добавлена отдельная страница:
+- список договоров и допников слева;
+- таблица строк спецификации справа;
+- кнопка `⚠` у строки для замечания;
+- кнопка `✓ Всё верно`;
+- кнопка `➕ Пропущена позиция`;
+- отдельная форма для комментария и correct value;
+- автоматический bootstrap metadata через `/teach/api/meta`.
+
+Файл:
+- [contracts-app/site/templates/teach.html](../../contracts-app/site/templates/teach.html)
+
+### 4. Метаданные обучения
+
+Сделан безопасный fallback:
+- `prompt_version` по умолчанию: `vm-teach-v1`
+- `model_name` по умолчанию: `gpt-oss-120b`
+- можно переопределить через URL: `?prompt_version=...&model=...`
+- можно переопределить через env:
+ - `TEACH_PROMPT_VERSION`
+ - `TEACH_MODEL_NAME`
+
+### 5. Миграции без риска
+
+Чтобы не ломать уже существующую БД, добавлены:
+- `ALTER TABLE feedback ADD COLUMN IF NOT EXISTS prompt_version TEXT`
+- `ALTER TABLE feedback ADD COLUMN IF NOT EXISTS model_name TEXT`
+
+## Проверки
+
+Синтаксис VM-файлов проверен через `python3 -m py_compile`:
+- `app.py`
+- `teach.py`
+- `schema.py`
+- `db.py`
+- `api.py`
+- `upload.py`
+- `extractor.py`
+- `differ.py`
+- `test_routes.py`
+
+Ошибок нет.
+
+## Что важно не перепутать
+
+- Старая Lucee-морда не нужна для этой фичи.
+- Обучение открывается по прямому URL на VM.
+- Никакого вмешательства в compare/upload pipeline нет.
+- `feedback` — отдельная аналитическая шина, не часть боевого event sourcing.
+
+## Что ещё осталось
+
+- Подключить реальный smoke-test к VM endpoint'ам и проверить insert/select на живой БД.
+- Если нужно, сделать отдельный read-only список накопленного feedback для агента.
+- При желании можно later подтянуть prompt/model metadata не из URL/env, а из отдельной VM-конфигурации.
+
+## Ключевые файлы
+
+- [contracts-app/site/app.py](../../contracts-app/site/app.py)
+- [contracts-app/site/schema.py](../../contracts-app/site/schema.py)
+- [contracts-app/site/teach.py](../../contracts-app/site/teach.py)
+- [contracts-app/site/templates/teach.html](../../contracts-app/site/templates/teach.html)
+- [contracts-app/site/llm_client.py](../../contracts-app/site/llm_client.py)
+- [contracts-app/site/extractor.py](../../contracts-app/site/extractor.py)
diff --git a/History/llm-analysis/opus-feedback-learning-answers.md b/History/llm-analysis/opus-feedback-learning-answers.md
new file mode 100644
index 0000000..2816d35
--- /dev/null
+++ b/History/llm-analysis/opus-feedback-learning-answers.md
@@ -0,0 +1,103 @@
+# Ответ Опуса — isolated training page / feedback storage
+
+Дата: 26.06.2026 | Ответ на вопросы по отдельной странице "обучения" для проекта «Сверка договоров».
+
+## Контекст
+
+- Стек: Lucee/CFML 6.0, файловая маршрутизация (`teach.cfm` → `/teach`), PostgreSQL `baza`.
+- Рабочий compare-пайплайн не трогаем: новая фича должна жить полностью изолированно.
+- Цель: отдельная страница с тем же UI-скелетом, что и compare results, но с возможностью оставлять замечания по строкам таблицы операций и сохранять их в БД для дальнейшего анализа агентом.
+
+## 1. Минимальная схема `feedback`
+
+Для MVP достаточно плоских колонок для агрегации и `JSONB` для значений:
+
+```sql
+CREATE TABLE IF NOT EXISTS feedback (
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
+ created_at TIMESTAMPTZ DEFAULT now(),
+
+ contract_id UUID,
+ supplement_id UUID,
+ event_seq INTEGER, -- NULL для missed/document
+
+ scope TEXT NOT NULL, -- 'row' | 'missed' | 'document'
+ verdict TEXT NOT NULL, -- 'correct' | 'error'
+ error_type TEXT, -- wrong_price|wrong_qty|wrong_name|wrong_date|extra_row|missed_row|wrong_action|wrong_mode
+ field TEXT, -- price|qty|sum|name|date_start|action|mode
+
+ service_name TEXT,
+ llm_value JSONB,
+ correct_value JSONB,
+
+ prompt_version TEXT,
+ doc_mode TEXT, -- partial | full_replace
+ comment TEXT
+);
+CREATE INDEX IF NOT EXISTS idx_feedback_error ON feedback(error_type);
+CREATE INDEX IF NOT EXISTS idx_feedback_prompt ON feedback(prompt_version);
+```
+
+Рекомендуемый фиксированный набор `error_type`:
+`wrong_price, wrong_qty, wrong_name, wrong_date, extra_row, missed_row, wrong_action, wrong_mode`.
+
+## 2. UI первой версии vs отложить
+
+### В MVP
+
+- Колонка `⚠` у каждой строки таблицы операций с мини-формой: выбор `error_type` + опционально правильное значение + комментарий.
+- Кнопка `[✓ всё верно]` на карточке ДС для положительного сигнала (`scope=document, verdict=correct`).
+- Кнопка `[➕ пропущена позиция]` для случая, когда позиция была пропущена (`scope=missed`).
+
+### Отложить
+
+- Инлайн-редактирование значений прямо в ячейках.
+- Статистика и дашборды на самой странице.
+- Модерация замечаний, роли, удаление чужих записей.
+- Разбор новых загрузок на этой странице; работать только по уже разобранным документам из БД.
+
+## 3. Связь с исходной операцией
+
+Правильная связь — логическая, без FK и без вмешательства в рабочий pipeline: `(contract_id, supplement_id, event_seq)`.
+
+Почему так:
+
+- `spec_events` уже хранит операции как `spec_events(contract_id, supplement_id, seq, action, new_values, ...)`.
+- `feedback` просто повторяет эти значения как обычные поля.
+- Без FK исключается риск связать feedback с жизненным циклом боевых событий или сломать вставки при повторном разборе.
+
+Если `event_seq` когда-то переедет из-за пересборки данных, в `llm_value` / `correct_value` нужно сохранять снимок операции (`action` + `new_values`), чтобы замечание оставалось интерпретируемым.
+
+## 4. `prompt_version`
+
+Версию промпта хранить нужно, иначе нельзя считать регрессию между изменениями.
+
+Но сейчас результат разбора не штампуется версией промпта, поэтому для MVP предлагается практический компромисс:
+
+- `/teach` при сохранении замечания пишет текущую активную версию промпта из БД.
+- Это честно помечается как версия на момент проверки, а не на момент исходного разбора.
+- Правильная фиксация `prompt_version` в результате разбора — отдельная задача основного pipeline, не часть isolated feedback flow.
+
+## 5. Что исключить
+
+### PII / обезличивание
+
+- Не хранить название/ИНН клиента, номер договора, ФИО, подписантов, реквизиты, email, телефоны.
+- Не хранить оригинальный текст документа и байты файла.
+
+### Изоляция рабочих данных
+
+- Не использовать `ALTER` существующих таблиц.
+- Не добавлять FK, триггеры или write-логики в `spec_events`, `spec_current`, `contracts`, `supplements`.
+- `/teach` должен писать только в `feedback` через отдельный параметризованный endpoint.
+
+## Итог
+
+Опус подтвердил, что MVP должен быть:
+
+1. Отдельной страницей с тем же UX-скелетом, что и compare results.
+2. Отдельным write endpoint и отдельной таблицей `feedback`.
+3. Полностью изолированным от рабочего compare-пайплайна.
+4. Приспособленным для SQL-аналитики агентом через плоские колонки + `JSONB`.
+
+Главный принцип: это не online-training, а сбор структурированного feedback для последующего анализа и планирования изменений.
\ No newline at end of file
diff --git a/History/llm-analysis/opus-feedback-learning-mode.md b/History/llm-analysis/opus-feedback-learning-mode.md
new file mode 100644
index 0000000..8f1604f
--- /dev/null
+++ b/History/llm-analysis/opus-feedback-learning-mode.md
@@ -0,0 +1,123 @@
+# Ответ Опуса — режим «исправление ошибок / обучение»
+
+Дата: 26.06.2025 | В ответ на обсуждение feedback-цикла.
+
+---
+
+## Суть
+
+Режим: **юзер загружает документы → система выдаёт результат → юзер правит ошибки → правки сохраняются**.
+
+Это не «обучение модели», а **накопление эталонных данных** (golden dataset) через естественный интерфейс исправления. Юзер не размечает абстрактно — он правит конкретный неверный результат.
+
+## Что уже есть под это
+
+- LLM возвращает поток операций: `ADD` / `UPDATE` / `DELETE` / `UNRESOLVED`
+- Применяются через событийную модель (`apply_events.cfm`)
+- Правка юзера = **исправленный поток операций**
+- Разница «LLM выдал» vs «юзер поправил» = **чистый сигнал ошибки**
+
+## Три уровня (не путать)
+
+| Уровень | Что | Когда |
+|---------|-----|-------|
+| **Регрессия** | Правки → golden-набор → прогон промпта → % ошибок | Сразу |
+| **Prompt-learning** | Анализ частых ошибок → правка промпта (few-shot/глоссарий) | Периодически |
+| **Fine-tuning** | Дообучение модели на сотнях/тысячах примеров | Не сейчас |
+
+## Чего НЕ делать
+
+- **Автоматически вкручивать правки в промпт** — переобучение, конфликты, рост токенов
+- Правильно: правки → накопитель → куратор анализирует агрегаты → batch-обновление промпта → регрессия
+
+## Что заложить в дизайн
+
+1. **Структурировать тип ошибки:** «пропущена строка», «неверная цена», «UPDATE вместо ADD», «ложный дубликат»
+2. **Версия промпта** при каждом результате — чтобы знать актуальность ошибки
+3. **Конфиденциальность** — реальные договоры с реквизитами копятся в БД, обсудить с заказчиком
+
+---
+
+**Решение:** отложить. Позже вернуться и спроектировать.
+
+---
+
+# Часть 2 — Конкретный UI для отметки ошибок
+
+Дата: 26.06.2025 | Опус изучил реальный вывод (карточка + таблица операций) и предложил привязку к элементам интерфейса.
+
+## Привязка: не к абзацам, а к строкам таблицы операций
+
+Результат — **таблица операций** (`Действие / Услуга / Цена / Кол-во / Сумма / Дата`), а не текст-простыня. Замечание цепляется к строке таблицы, а не к абзацу исходника.
+
+### Как выглядит (на реальном примере)
+
+```
+✓ допник-1-XXX002-01200_3.docx — 2 оп., partial (14с) [✓ всё верно]
++2 ~0 -0
+
+Действие Услуга Цена Кол-во Сумма Дата ⚠
+ADD WAF: Positive Technologies, в составе… 104021.67 1 104021.67 2026-03-30 [⚠]
+ADD Облачный диск Valo Cloud, в составе… 68700 1 68700 2026-02-01 [⚠]
+
+ [ ➕ Система пропустила позицию ]
+```
+
+### Клик по `[⚠]` → мини-форма под строкой
+
+```
+Тип ошибки: ( ) неверная цена/сумма
+ ( ) неверное кол-во
+ ( ) неверное наименование услуги
+ (•) лишняя строка — этой операции быть не должно
+ ( ) неверное действие (должно быть UPDATE/DELETE, а не ADD)
+ ( ) неверная дата
+Правильное значение: [_____________] (необязательно)
+Комментарий: [_____________] (необязательно)
+ [ Сохранить ]
+```
+
+### `[✓ всё верно]` — вверху карточки
+
+Один клик = положительный сигнал, без расписывания.
+
+### `[➕ Система пропустила позицию]` — под таблицей
+
+Для случая, когда услуга была в документе, но LLM её не извлекла. Открывает форму ввода пропущенной строки.
+
+## Что уходит в базу (обезличенно)
+
+```
+operation: ADD
+service: "Облачный диск Valo Cloud, в составе…" ← без названия клиента
+field: price
+llm_value: 68700
+correct: 68000
+error_type: wrong_price
+prompt_version: v1.0.178
+```
+
+Никаких названий компаний, ФИО, № договора — только структура услуги и числа.
+
+## Почему так, а не поле у каждого абзаца
+
+- Реальный вывод — **таблица**, а не текст. Абзацев нет, есть операции
+- Привязка к операции даёт агрегацию по типу ошибки
+- Готовая дельта «LLM выдала X → правильно Y» как обучающий сигнал
+- Ровная укладка в событийную модель (`apply_events.cfm`)
+
+---
+
+**Решение:** отложить. Ждать «делай» для реализации.
+
+---
+
+# Часть 3 — Текст для заказчика
+
+**Этап опытной эксплуатации (обучение системы)**
+
+На первое время предлагаем режим проверки: вы загружаете реальные документы, система выдаёт результат, а вы отмечаете ошибки — что распознано неверно (пропущена строка, неверная цена, неправильное сопоставление и т.п.).
+
+Эти отметки **в обезличенном виде** (без названий компаний, ФИО, реквизитов и иных персональных данных) накапливаются в базе и используются для дальнейшей настройки и обучения системы.
+
+Это позволит откалибровать сервис на ваших реальных договорах, а не на тестовых примерах, и системно повышать точность.
diff --git a/History/llm-analysis/opus-teach-full-responses.md b/History/llm-analysis/opus-teach-full-responses.md
new file mode 100644
index 0000000..36ee35a
--- /dev/null
+++ b/History/llm-analysis/opus-teach-full-responses.md
@@ -0,0 +1,515 @@
+# Полные ответы Опуса — изолированная страница /teach (feedback + обучение)
+
+Дата: 26.06.2026 | Сохранено post-factum из чат-сессии.
+
+---
+
+# ЧАСТЬ 1: Ответ про few-shot и эталонный датасет
+
+Хороший вопрос. Дай я сначала посмотрю, как сейчас устроены эти «6 файлов» и формирование промпта, чтобы мнение было по делу.
+
+Searching codebase for "формирование промпта для LLM на основе эталонных файлов few-shot examples"
+
+Searched for regex `6 (файл|file|example|эталон|reference)`, 1 result
+
+Searched for regex `few.?shot|эталон|example|пример|reference|опорн` (`**/contracts-app/**/*.py`), no results
+
+Searched for files matching `**/llm_prompt.py`, 1 match
+
+Searched for files matching `**/prompt*.{py,cfm}`, 2 matches
+
+Searched for regex `opus_cases|out_100files|6 файл|six`, 2 results
+
+Read file:///home/naeel/nubes/contracts/contractor/deploy/llm_prompt.py#1-1, lines 1 to 120
+
+Read file:///home/naeel/nubes/contracts/testgen/opus_cases
+
+Посмотрел, как сейчас устроено: промпт в llm_prompt.py — это хардкод-инструкция с доменным глоссарием и парой ручных few-shot примеров (`ПРИМЕР 1`, `ПРИМЕР 2`), а «6 файлов» — это по сути синтетические кейсы из opus_cases для проверки.
+
+Моё мнение по фиче.
+
+## Идея здравая, но есть важная развилка
+
+Реальные проверенные документы — это очень ценно. Синтетика (gen_opus.py) не воспроизводит грязь реальных договоров: кривые таблицы, объединённые ячейки, «мощность в составе названия», нестандартные формулировки ДС, опечатки в суммах. На 6 искусственных файлах легко получить иллюзию качества, которая рассыпется на первом же настоящем пакете.
+
+Но ключевой вопрос: **что именно строить из архива?** Тут смешаны две разные вещи.
+
+## 1. Эталонный датасет (input → проверенный output) — да, делать
+
+Самое полезное. Но важно: «загрузить архив документов» недостаточно. Чтобы это работало, на каждый документ нужен **проверенный правильный результат** (итоговая спецификация / список ops). Без эталонного ответа это просто куча PDF — не на чем калибровать.
+
+То есть фича должна быть не «загрузи архив», а «загрузи архив + зафиксируй проверенную спецификацию как ground-truth». Тогда получаем regression-набор: прогнал промпт → сравнил с эталоном → видишь реальный процент ошибок.
+
+## 2. Авто-генерация промпта из этих файлов — осторожно
+
+Полностью «формировать промпт» из загруженных файлов я бы не делал. Риски:
+- **Раздувание контекста** — если пихать реальные договоры целиком как few-shot, промпт пухнет, растёт цена/латентность, и модель начинает копировать формат конкретного образца.
+- **Переобучение под layout** — закладываешь форматы 3-х клиентов, ломается на четвёртом.
+- **Конфиденциальность** — реальные договоры с реквизитами/ценами уезжают в LLM на каждом запросе. Это надо явно проговорить с заказчиком.
+
+Лучше из архива **извлекать паттерны, а не сам текст**: новые единицы измерения, варианты названий услуг, типы формулировок ДС («изложить в новой редакции» и т.п.) — и обогащать ими глоссарий и правила в промпте. 1-2 самых показательных кейса можно добавить как few-shot, остальное — в eval-набор.
+
+## Что я бы предложил по факту
+
+- **Разделить два контура**: (а) `golden dataset` из реальных пар вход→проверенный_выход для регрессии; (б) сам промпт — оставить компактным, обогащать его выводами из анализа датасета, а не сырыми файлами.
+- Фича «загрузка архива» → ведёт в **разметку/верификацию** (человек подтверждает правильный результат), а не сразу в промпт.
+- На этом наборе гонять метрику (precision/recall по ops), и менять промпт только если метрика растёт.
+
+Резюме: направление правильное — уходить от 6 синтетических файлов надо. Но ценность не в «архиве документов», а в **архиве проверенных пар документ↔результат**. И этим калибровать/тестировать, а не механически генерировать промпт.
+
+Это обсуждение — код не трогаю. Скажешь «делай» по конкретному пункту — приступлю.
+
+---
+
+# ЧАСТЬ 2: Ответ про «типа обучение» (feedback-learning цикл)
+
+Да, это сильнее предыдущего варианта — и вот почему, плюс где подвох.
+
+## Почему это лучше «загрузки архива»
+
+Главная проблема прошлого варианта была: откуда взять «проверенный правильный ответ». Здесь она решается сама собой — **интерфейс исправления ошибок и есть инструмент разметки**. Юзер не размечает абстрактно, а правит конкретный результат системы. На выходе автоматически копится то самое: пара «вход → проверенный выход». Это правильный способ построить эталонный датасет.
+
+И что важно — у вас уже есть подходящая структура под это. Результат LLM — это поток операций (`ADD` / `UPDATE` / `DELETE` / `UNRESOLVED`) в llm_prompt.py, а применяются они через событийную модель (apply_events.cfm). Значит правка юзера — это **исправленный поток операций**, а разница «что выдала LLM» vs «как поправил юзер» = чистый сигнал ошибки. Это удобно: не свободный текст «тут неверно», а конкретная дельта.
+
+## Но «типа обучения» — это важная оговорка
+
+Само по себе это **не обучение модели**. Цикл «учится» только если правки где-то сохраняются и переиспользуются. Если правка живёт в рамках одной сессии и исчезает — система не умнеет, это просто ручная корректировка. Так что ключевое в фиче — не сам акт правки, а **накопление и переиспользование** правок.
+
+Дальше есть три уровня, и не надо их путать:
+
+1. **Регрессия** (самое дешёвое и полезное сразу) — правки копятся как golden-набор, на нём гоняется промпт, видно реальный % ошибок.
+2. **Prompt-learning** — из частых ошибок осознанно дополняется глоссарий/правила/few-shot в промпте. Это и есть практичное «обучение» в вашем случае.
+3. **Fine-tuning модели** — реальное дообучение. Дорого, нужен объём (сотни-тысячи примеров), отдельная инфраструктура. На старте не нужно.
+
+## Где подвох — не вкручивать правки в промпт автоматически
+
+Соблазн: «каждую правку юзера сразу добавлять в промпт». Так делать нельзя:
+- **переобучение** под конкретные форматы;
+- **рост контекста** и цены;
+- **конфликтующие правки** от разных юзеров → промпт деградирует.
+
+Правильнее: правки идут в накопитель → периодически человек/куратор смотрит агрегат → осознанно обновляет промпт → прогоняет регрессию. Batch, а не автоматом на каждый клик.
+
+## Что бы я ещё заложил в дизайн
+
+- **Структурировать тип ошибки**, а не свободный текст: «пропущена строка» / «неверная цена» / «надо было UPDATE, а не ADD» / «ложный дубликат вместо UNRESOLVED». Тогда правки можно агрегировать и видеть системные слабости промпта, а не разбирать вручную.
+- Фиксировать **версию промпта**, на которой получен результат — иначе через месяц непонятно, актуальна ли ещё ошибка.
+- Учесть **конфиденциальность**: реальные договоры с реквизитами/ценами копятся в БД — проговорить с заказчиком.
+
+Резюме: режим «загрузил → система выдала → юзер поправил → правка сохранилась» — да, делать стоит, это естественный сборщик эталонных данных и ловушка системных ошибок. Только «обучение» держать осознанным (batch-обновление промпта + регрессия), а не авто-инъекцией каждой правки.
+
+Код не трогаю — это обсуждение. Скажешь «делай» — распишу конкретную реализацию.
+
+---
+
+# ЧАСТЬ 3: Короткий вариант для заказчика
+
+Вот короткий вариант для заказчика:
+
+---
+
+**Этап опытной эксплуатации (обучение системы)**
+
+На первое время предлагаем режим проверки: вы загружаете реальные документы, система выдаёт результат, а вы отмечаете ошибки — что распознано неверно (пропущена строка, неверная цена, неправильное сопоставление и т.п.).
+
+Эти отметки **в обезличенном виде** (без названий компаний, ФИО, реквизитов и иных персональных данных) накапливаются в базе и используются для дальнейшей настройки и обучения системы.
+
+Это позволит откалибровать сервис на ваших реальных договорах, а не на тестовых примерах, и системно повышать точность.
+
+---
+
+Если нужно ещё короче (одним абзацем) или, наоборот, добавить пункт про согласие на обработку данных — скажи, поправлю.
+
+---
+
+# ЧАСТЬ 4: Ответ про привязку замечаний к строкам таблицы
+
+Дай гляну, как сейчас выглядит вывод результата — чтобы предложить привязку замечаний к реальным элементам интерфейса, а не абстрактно.
+
+Read file:///home/naeel/nubes/contracts/contractor/deploy/services
+
+Read memory file:///home/naeel/.vscode-server/data/User/workspaceStorage/dc98ff3e658364e1826ee6585ebe4b5e/GitHub.copilot-chat/memory-tool/memories/repo/contracts-two-panels.md
+
+Searched for regex `target_id|r1|new_row|action.*ADD|операц|spec|строк` (`**/contractor/view.cfm`), no results
+
+Searched for files matching `**/contracts-app/site/templates/**`, no matches
+
+Не «у каждого абзаца» — это привязка не к тому объекту. Результат работы системы — это не текст-простыня, а **таблица спецификации**: набор строк-услуг, каждая получена операцией LLM (`ADD`/`UPDATE`/`DELETE`/`UNRESOLVED`) и имеет свой id (`r1`, `r2`…). Замечание логично цеплять **к строке результата**, а не к абзацу исходника.
+
+## Как я бы это сделал
+
+**Уровень 1 — отметка на строке (основное).**
+У каждой строки итоговой таблицы — маленькая иконка «⚠ ошибка». Клик открывает мини-форму с **выбором типа ошибки** (не свободный текст):
+- неверная цена / объём / сумма
+- неверное наименование
+- лишняя строка (надо было удалить)
+- пропущена строка (есть в документе — нет в результате)
+- неверное сопоставление (надо UPDATE существующей, а не новая ADD; или наоборот)
+- неверный тип ДС / режим (partial vs новая редакция)
+
+Плюс необязательное поле «комментарий» свободным текстом.
+
+**Уровень 2 — отметка «пропущено».**
+Случай «строки вообще нет в результате» не покрывается иконкой на строке. Нужна отдельная кнопка «➕ Система пропустила позицию» над/под таблицей.
+
+**Уровень 3 — общая оценка (опционально).**
+Одна кнопка вверху: «Результат в целом верный / есть ошибки». Дёшево, но даёт быстрый сигнал по проценту удачных разборов.
+
+## Почему так, а не поле у абзаца
+
+- **Структурированный фидбек агрегируется.** «Тип ошибки = неверное сопоставление встречается в 40% ДС» → понятно, что чинить в промпте. Свободный текст у абзацев придётся разбирать вручную, аналитики ноль.
+- **Замечание = дельта операции.** Юзер фактически правит строку → разница «что выдала LLM» vs «как должно быть» и есть обучающий сигнал. Это ложится на вашу событийную модель (apply_events.cfm) почти без натяжки.
+- **Меньше шума.** Поле ввода под каждым абзацем визуально перегружает и провоцирует расплывчатые комментарии.
+
+## Что копится в базе (обезличенно)
+
+На каждое замечание:
+- тип ошибки + (опц.) комментарий
+- что выдала система по этой строке
+- как поправил юзер (правильное значение)
+- версия промпта
+- **без** названий компаний/ФИО/реквизитов — только структура и числа
+
+## Как это сформулировать заказчику (1 фраза)
+
+> В режиме проверки у каждой строки результата будет отметка «ошибка» с выбором типа (неверная цена, пропущена позиция, неверное сопоставление и т.п.). Эти отметки в обезличенном виде копятся в базе и используются для настройки точности.
+
+Хочешь — распишу конкретную реализацию (поля в БД, эндпоинт сохранения, изменения в шаблоне вывода). Код пока не трогаю, жду «делай».
+
+---
+
+# ЧАСТЬ 5: Ответ про UI с примером на допник-1-XXX002
+
+Отлично, теперь предметно. Твой вывод — это карточка документа + **таблица операций** (`Действие / Услуга / Цена / Кол-во / Сумма / Дата`). Замечания цепляем именно к строкам этой таблицы. Вот как это выглядело бы на твоём примере:
+
+```
+✓ допник-1-XXX002-01200_3.docx — 2 оп., partial (14с) [✓ всё верно]
++2 ~0 -0
+
+Действие Услуга Цена Кол-во Сумма Дата ⚠
+ADD WAF: Positive Technologies, в составе… 104021.67 1 104021.67 2026-03-30 [⚠]
+ADD Облачный диск Valo Cloud, в составе… 68700 1 68700 2026-02-01 [⚠]
+
+ [ ➕ Система пропустила позицию ]
+```
+
+**Клик по `[⚠]` в строке** → раскрывается мини-форма прямо под строкой:
+
+```
+Тип ошибки: ( ) неверная цена/сумма
+ ( ) неверное кол-во
+ ( ) неверное наименование услуги
+ (•) лишняя строка — этой операции быть не должно
+ ( ) неверное действие (должно быть UPDATE/DELETE, а не ADD)
+ ( ) неверная дата
+Правильное значение: [_____________] (необязательно)
+Комментарий: [_____________] (необязательно)
+ [ Сохранить ]
+```
+
+**Кнопка `[✓ всё верно]`** вверху карточки — если разбор документа корректен целиком. Один клик = положительный сигнал, ничего расписывать не надо.
+
+**Кнопка `➕ Система пропустила позицию`** — для случая, которого нет в таблице (услуга была в документе, но LLM её не извлекла). Открывает форму ввода пропущенной строки.
+
+## Что уходит в базу по одному замечанию (обезличенно)
+
+На примере второй строки, если юзер укажет «неверная цена, правильно 68000»:
+
+```
+operation: ADD
+service: "Облачный диск Valo Cloud, в составе…" ← наименование услуги, без названия клиента
+field: price
+llm_value: 68700
+correct: 68000
+error_type: wrong_price
+prompt_version: v1.0.178
+```
+
+Никаких «ЗАО ХХХ002», № договора, ФИО — только структура услуги и числа.
+
+## Почему так, а не поле под каждым абзацем
+
+Твой реальный вывод подтверждает: это **таблица**, а не текст. Поле у «абзаца» некуда вешать — абзацев тут нет, есть операции. А привязка к операции даёт:
+- агрегацию по типу ошибки (видно, что чинить в промпте),
+- готовую дельту «LLM выдала X → правильно Y» как обучающий сигнал,
+- ровную укладку в вашу событийную модель (apply_events.cfm).
+
+Хочешь — распишу конкретику реализации (колонка в таблице вывода, эндпоинт сохранения, структура таблицы в БД). Код не трогаю, жду «делай».
+
+---
+
+# ЧАСТЬ 6: ДЕТАЛЬНЫЙ ПЛАН — страница /teach
+
+Это план, не правка кода — изучу структуру, чтобы план лёг на реальный код, а не в воздух. Читаю ключевые файлы.
+
+Read file:///home/naeel/nubes/contracts/contractor/db.cfc
+
+Read file:///home/naeel/nubes/contracts/contractor/Application.cfc
+
+Read memory file:///home/naeel/.vscode-server/data/User/workspaceStorage/dc98ff3e658364e1826ee6585ebe4b5e/GitHub.copilot-chat/memory-tool/memories/repo/contracts-deploy.md
+
+Read memory file:///home/naeel/.vscode-server/data/User/workspaceStorage/dc98ff3e658364e1826ee6585ebe4b5e/GitHub.copilot-chat/memory-tool/memories/repo/contracts-lucee-test.md
+
+Изучил структуру. Lucee-приложение с файловой маршрутизацией (`teach.cfm` → `/teach`), PostgreSQL `baza`, есть событийная модель (`spec_events`, `spec_current`) и уже готовый read-only SQL-эндпоинт `/db/query?sql=…` в db.cfc — через него агент и будет читать БД. Вот детальный план.
+
+---
+
+# План: страница `/teach` (опросник + обучение)
+
+## 1. Принцип изоляции — что НЕ трогаем
+
+Маршрутизация в Lucee файловая → **новый путь = новый файл**, существующий код не редактируется вообще:
+
+| Не трогаем | Добавляем |
+|---|---|
+| `api.cfm`, `process*.cfm`, `apply_events.cfm`, db.cfc, Application.cfc, `upload.cfm`, `view.cfm` | `teach.cfm`, `teach_api.cfm`, `teach_schema.cfm` |
+| существующие таблицы (`spec_events`, `contracts`…) | **одна новая таблица** `feedback` |
+
+Никаких `ALTER` существующих таблиц, никаких правок в `db.cfc/schema()`. Только чтение чужих таблиц + запись в свою.
+
+## 2. Новые файлы (3 шт.)
+
+1. **`teach_schema.cfm`** — одноразовый: `CREATE TABLE IF NOT EXISTS feedback (…)`. Зашёл по URL один раз → таблица создана.
+2. **`teach.cfm`** — сама страница:
+ - список уже разобранных договоров/ДС (читает `contracts` + `supplements` + `spec_events`);
+ - выбрал ДС → рисует ту же таблицу операций (`Действие/Услуга/Цена/Кол-во/Сумма/Дата`), что в твоём примере, но с колонкой `⚠` и кнопками `[✓ всё верно]`, `[➕ пропущена позиция]`;
+ - JS отправляет замечание `POST`-ом на `teach_api.cfm`.
+3. **`teach_api.cfm`** — приём замечания: **параметризованный** `INSERT` в `feedback` (никакого конкатенированного SQL — защита от инъекций). Возвращает JSON `{ok:true}`.
+
+> Источник данных для таблицы операций — `spec_events` (`action`, `new_values` JSONB, `comment`, `seq`). При реализации сверим, что карточка «2 оп., partial» строится именно отсюда.
+
+## 3. Схема таблицы `feedback` (ядро — продумано под анализ агентом)
+
+Главные поля вынесены отдельными колонками (не в JSON), чтобы агент агрегировал простым SQL; значения — в JSONB.
+
+```sql
+CREATE TABLE IF NOT EXISTS feedback (
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
+ created_at TIMESTAMPTZ DEFAULT now(),
+
+ -- привязка (внутренняя трассировка, в обучающий экспорт НЕ идёт)
+ contract_id UUID, -- FK-логически на contracts, без жёсткого constraint
+ supplement_id UUID,
+ event_seq INTEGER, -- какая операция ДС (NULL для "пропущено"/"документ в целом")
+
+ -- уровень и вердикт
+ scope TEXT NOT NULL, -- 'row' | 'missed' | 'document'
+ verdict TEXT, -- 'correct' | 'error'
+
+ -- суть ошибки (для агрегации)
+ error_type TEXT, -- wrong_price|wrong_qty|wrong_name|wrong_date|
+ -- extra_row|missed_row|wrong_action|wrong_mode
+ field TEXT, -- price|qty|sum|name|date_start|action|mode
+
+ -- обучающий сигнал: что выдала система vs как правильно
+ service_name TEXT, -- наименование услуги (обезличено)
+ llm_value JSONB, -- что выдала LLM
+ correct_value JSONB, -- что указал юзер
+
+ -- контекст результата
+ prompt_version TEXT, -- версия промпта на момент разбора
+ doc_mode TEXT, -- partial | full_replace
+ comment TEXT, -- свободный комментарий юзера (необяз.)
+ reviewer TEXT -- обезличенный id сессии/юзера (необяз.)
+);
+CREATE INDEX IF NOT EXISTS idx_feedback_error ON feedback(error_type);
+CREATE INDEX IF NOT EXISTS idx_feedback_prompt ON feedback(prompt_version);
+```
+
+## 4. Что пишется по каждому типу замечания
+
+| Действие юзера | scope | verdict | error_type | llm_value → correct_value |
+|---|---|---|---|---|
+| `[✓ всё верно]` на карточке | `document` | `correct` | — | — |
+| `⚠` на строке: неверная цена | `row` | `error` | `wrong_price` | `{"price":68700}` → `{"price":68000}` |
+| `⚠`: лишняя строка | `row` | `error` | `extra_row` | вся операция → `null` |
+| `⚠`: должно быть UPDATE, а не ADD | `row` | `error` | `wrong_action` | `{"action":"ADD"}` → `{"action":"UPDATE"}` |
+| `[➕ пропущена позиция]` | `missed` | `error` | `missed_row` | `null` → `{name,price,qty,sum,date}` |
+| неверный режим ДС | `document` | `error` | `wrong_mode` | `{"mode":"partial"}` → `{"mode":"full_replace"}` |
+
+Каждая запись самодостаточна: видно **что было** и **как надо** → готовая обучающая пара.
+
+## 5. Обезличивание
+
+- В `feedback` **не копируем** название клиента, № договора, ФИО, реквизиты.
+- Храним только: `service_name` (тип услуги — «WAF Positive Technologies»), числа, тип ошибки, версию промпта.
+- `contract_id/supplement_id` — это UUID (не имя), для внутренней трассировки. В **обучающий экспорт** агента эти id не включаются — только структурные поля.
+- Для заказчика формулировка: *«в обезличенном виде, без названий компаний и персональных данных»* — соответствует фактической схеме.
+
+## 6. Как агент читает и анализирует (потом)
+
+Через уже существующий `/db/query?sql=…` (read-only) или напрямую psql. Примеры:
+
+**Системные слабости промпта:**
+```sql
+SELECT error_type, field, count(*) AS n
+FROM feedback WHERE verdict='error'
+GROUP BY error_type, field ORDER BY n DESC;
+```
+
+**Качество по версии промпта (регрессия):**
+```sql
+SELECT prompt_version,
+ count(*) FILTER (WHERE verdict='correct') AS ok,
+ count(*) FILTER (WHERE verdict='error') AS err
+FROM feedback GROUP BY prompt_version;
+```
+
+**Выгрузка обучающих пар (для правки промпта/few-shot):**
+```sql
+SELECT service_name, error_type, llm_value, correct_value
+FROM feedback WHERE verdict='error' AND scope='row';
+```
+
+Дальше агент: смотрит агрегат → предлагает правку промпта/глоссария → прогоняет на накопленных парах → сравнивает метрику до/после. **Никакой авто-инъекции** правок в промпт — только осознанное batch-обновление.
+
+## 7. Версия промпта — нюанс
+
+Сейчас результат разбора **не штампуется** версией промпта. Варианты:
+- (минимум, без правки основного кода) `/teach` пишет в `prompt_version` **текущую активную** версию промпта на момент замечания — приблизительно, с оговоркой;
+- (правильно, отдельной задачей позже) при разборе сохранять `prompt_version` в результат — но это уже касается основного пайплайна, делать отдельно и по «делай».
+
+Флажок: на старте берём активную версию, точность привязки уточним позже.
+
+## 8. Этапы внедрения
+
+1. `teach_schema.cfm` → создать таблицу `feedback`.
+2. `teach.cfm` (чтение разобранных ДС + рендер таблицы с `⚠`) — без сохранения, только UI.
+3. `teach_api.cfm` (параметризованный INSERT) + подключить JS-отправку.
+4. Проверка end-to-end на одном реальном ДС (как твой `допник-1-XXX002`).
+5. 2–3 контрольных SQL-запроса агента → убедиться, что данные читаемы и агрегируются.
+6. (позже, отдельно) первый цикл анализа → правка промпта → регрессия.
+
+## 9. Открытые вопросы к тебе
+
+1. **`/teach` работает только по уже разобранным** документам (из БД) — или нужна и загрузка-разбор прямо на странице? (загрузка-разбор = переиспользование основного пайплайна, сложнее).
+2. **Доступ к `/teach`** — нужен ли пароль/ограничение, или dev-стенд открыт?
+3. **`service_name`** — оставляем как есть (тип услуги) или дополнительно маскируем?
+4. **Кто reviewer** — нужно различать разных проверяющих или достаточно анонимной сессии?
+
+---
+
+Это план, код не трогал. Скажешь по каким пунктам «делай» (и ответишь на вопросы из §9) — начну с `teach_schema.cfm`.
+
+---
+
+# ЧАСТЬ 7: Ответ про промпт для Опуса
+
+Текст для Опуса хороший — чёткие требования и правильные ограничения (не трогать пайплайн, без online-training, обезличенность). Но в нынешнем виде Опус ответит **обобщённо**, потому что в промпте нет контекста вашего стека и текущей схемы. По каждому из 5 вопросов это критично. Что я бы добавил, прежде чем отправлять:
+
+## Чего не хватает Опусу для предметного ответа
+
+1. **Стек и маршрутизация.** Lucee/CFML 6.0, файловая маршрутизация (`teach.cfm` → `/teach`), PostgreSQL `baza`. Без этого Опус не поймёт, как «не трогать пайплайн» технически (а ответ — просто новый файл, без правок существующих).
+
+2. **Текущая схема — особенно для Q3.** У вас уже есть событийная модель:
+ - `spec_events(contract_id, supplement_id, seq, action, target_hash, new_values JSONB, comment, status)`
+ - `spec_current`, `contracts`, `supplements`.
+
+ Связь с исходной операцией = `(contract_id, supplement_id, event_seq)`, и это **уже отвечает на Q3**: ничего в пайплайне трогать не надо, фидбек просто ссылается на `seq` логически, без FK-constraint. Опус должен это знать, иначе предложит схему, не совпадающую с вашей.
+
+3. **Форма результата (для Q1/Q2).** Таблица операций `Действие / Услуга / Цена / Кол-во / Сумма / Дата`, режимы `partial | full_replace`. Дайте Опусу реальный пример вывода (твой `допник-1-XXX002`).
+
+4. **Ключевой факт для Q4.** Сейчас результат разбора **не штампуется версией промпта** — этого в промпте нет, а это определяет весь ответ на Q4. Без упоминания Опус посоветует «просто сохраняйте prompt_version», не зная, что её сейчас негде взять, кроме «текущей активной».
+
+5. **Как агент потом читает БД (для Q1).** Есть read-only эндпоинт `/db/query?sql=…` в db.cfc. Это аргумент за «плоские колонки `error_type/field/prompt_version` + значения в JSONB» — чтобы агрегировалось обычным SQL.
+
+## Мелочи по формулировкам
+
+- Q5 стоит разнести: отдельно **PII** (имена/реквизиты/№ договора — не хранить) и отдельно **изоляция рабочих данных** (никаких `ALTER`/FK на боевые таблицы, только своя `feedback`). Сейчас они слиты в один вопрос.
+- Добавь явно: *«предложи минимальный набор `error_type` как enum-значения»* — иначе Опус может уйти в свободный текст ошибок, который потом не агрегируется.
+
+---
+
+Хочешь — могу либо (а) переписать твой промпт для Опуса с этим контекстом, либо (б) сам ответить на все 5 вопросов по вашей реальной схеме (у меня он есть). Скажи, что полезнее.
+
+---
+
+# ЧАСТЬ 8: Финальный предметный ответ по 5 вопросам
+
+Понял, отвечаю по существу — по вашей реальной схеме.
+
+## 1. Минимальная схема `feedback` для MVP
+
+Плоские колонки для агрегации + JSONB для значений:
+
+```sql
+CREATE TABLE IF NOT EXISTS feedback (
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
+ created_at TIMESTAMPTZ DEFAULT now(),
+
+ -- связь с исходной операцией (логическая, без FK)
+ contract_id UUID,
+ supplement_id UUID,
+ event_seq INTEGER, -- NULL для missed/document
+
+ scope TEXT NOT NULL, -- 'row' | 'missed' | 'document'
+ verdict TEXT NOT NULL, -- 'correct' | 'error'
+ error_type TEXT, -- enum-значения, см. ниже
+ field TEXT, -- price|qty|sum|name|date_start|action|mode
+
+ service_name TEXT, -- наименование услуги (обезличено)
+ llm_value JSONB, -- что выдала система
+ correct_value JSONB, -- как правильно
+
+ prompt_version TEXT,
+ doc_mode TEXT, -- partial | full_replace
+ comment TEXT
+);
+CREATE INDEX IF NOT EXISTS idx_feedback_error ON feedback(error_type);
+CREATE INDEX IF NOT EXISTS idx_feedback_prompt ON feedback(prompt_version);
+```
+
+Фиксированный набор `error_type` (enum как CHECK или просто документированный список):
+`wrong_price, wrong_qty, wrong_name, wrong_date, extra_row, missed_row, wrong_action, wrong_mode`.
+
+Всё. Ни истории версий записи, ни статусов модерации в MVP не нужно.
+
+## 2. UI первой версии vs отложить
+
+**В MVP:**
+- колонка `⚠` у каждой строки таблицы операций → мини-форма: выбор `error_type` (радио) + опц. «правильное значение» + опц. комментарий;
+- кнопка `[✓ всё верно]` на карточке ДС (дешёвый положительный сигнал, `scope=document, verdict=correct`);
+- кнопка `[➕ пропущена позиция]` (`scope=missed`).
+
+**Отложить:**
+- инлайн-редактирование значений прямо в ячейке (пока хватает «правильное значение» в форме);
+- дашборд статистики на самой странице (агрегация — задача агента через SQL, не UI);
+- модерация/удаление чужих замечаний, роли, авторизация по пользователям;
+- разбор-на-лету (загрузка нового документа) — работаем по уже разобранным из БД.
+
+## 3. Связь с исходной операцией без вмешательства в pipeline
+
+Ключ — **логическая ссылка `(contract_id, supplement_id, event_seq)`**, БЕЗ внешнего ключа на `spec_events`. У вас операции уже лежат в `spec_events(contract_id, supplement_id, seq, action, new_values…)` — `seq` стабилен в рамках договора. `feedback` просто хранит эти три значения как обычные поля.
+
+Почему без FK:
+- FK на `spec_events` = риск, что повторный разбор/перезапись событий уронит вставку фидбека или каскадом заденет данные — это и есть «вмешательство». Логическая ссылка развязывает таблицы полностью.
+- `feedback` — отдельная таблица, ничего в боевых не меняем (никаких `ALTER`, никаких триггеров).
+
+Если `seq` теоретически может переехать при reset/reparse — дополнительно дублируем в `llm_value` снимок операции (`action` + `new_values`), чтобы замечание оставалось интерпретируемым даже без живого `spec_events`. Этого достаточно.
+
+## 4. prompt_version — хранить сразу, но честно
+
+Хранить **да** — без неё нельзя мерить регрессию (стало лучше/хуже после правки промпта). Но: сейчас результат разбора версией промпта **не штампуется**, взять её из результата негде.
+
+Поэтому для MVP, не трогая pipeline:
+- `/teach` на момент сохранения замечания читает **текущую активную** версию промпта из БД (ту, что Lucee отдаёт как активную) и пишет в `prompt_version`.
+- Это приближение: оно верно, если промпт не менялся между разбором и проверкой. Помечаем это как «версия на момент проверки», не «на момент разбора».
+
+Правильная привязка (штамповать `prompt_version` в результат при разборе) — отдельная задача по основному пайплайну, делается позже и по явному решению, не в этом изолированном flow.
+
+## 5. Что обязательно исключить
+
+**PII / обезличивание — не хранить:**
+- название/ИНН клиента, № договора, ФИО, подписантов, реквизиты, email/телефоны;
+- оригинальный текст документа и байты файла.
+
+Храним только тип услуги (`service_name`), числа, тип ошибки, версию промпта. `contract_id/supplement_id` — это UUID (не имена), для внутренней трассировки; в обучающий **экспорт** агента они не попадают.
+
+**Изоляция рабочих данных — исключить из дизайна:**
+- любые `ALTER` существующих таблиц, FK-constraint'ы на боевые таблицы, триггеры;
+- запись в `spec_events / spec_current / contracts / supplements` — `/teach` только читает их, пишет исключительно в `feedback`;
+- никакого generic-SQL write-эндпоинта на публичной странице — только параметризованный INSERT в `teach_api.cfm` (защита от инъекций).
+
+---
+
+Это ответ-проектирование, код не трогаю. Скажешь «делай» — начну с `teach_schema.cfm` (создание таблицы), затем `teach.cfm` и `teach_api.cfm`.
diff --git a/History/llm-analysis/opus-zip-plan-analysis.md b/History/llm-analysis/opus-zip-plan-analysis.md
new file mode 100644
index 0000000..aeee952
--- /dev/null
+++ b/History/llm-analysis/opus-zip-plan-analysis.md
@@ -0,0 +1,151 @@
+# Анализ ответа Опуса — zip_source, UI-режимы, промпты
+
+Дата: 26.06.2025 | Ответ на [opus-request-zip-plan.md](../Files/opus-request-zip-plan.md)
+
+---
+
+## 1. Оценка ответа в целом
+
+**Качество: высокое.** Опус прочитал реальный код, разобрался в архитектуре, дал конкретные диффы по слоям. Не «размышления вообще», а точные строки и функции. 95% рекомендаций — правильные.
+
+**Что упущено:**
+- Lucee-слой (`upload.cfm`, `api.cfm`) — тоже участвует в upload, но Опус его не проанализировал
+- `confidence` — предлагает сохранять в БД, но не говорит где именно брать (LLM возвращает? парсить из промпта?)
+- Порог 100 файлов для «Потока» — спорный, обсудим ниже
+
+---
+
+## 2. По пунктам
+
+### 2.1. `zip_source` — ✅ СОГЛАСЕН полностью
+
+План по слоям правильный. Ключевые моменты:
+
+- **`zip_source` не участвует в classify/group/compare** — верно. Чисто визуальный атрибут.
+- **Формат: имя ZIP с расширением** — да, `«Ромашка.zip»`.
+- **PK не трогаем (UUID)**, «ID = zip/filename» только для отображения — верно.
+- **Дедупликация по паре `(zip_source, name)`** — ⚠️ самый критичный момент. Опус прав: если не сменить ключ, одноимённые файлы из разных ZIP будут перезаписываться. Но **надо проверить**: текущий код в `addRegularFile()` (files.js) ищет по `f.name`. При добавлении `zip_source` нужно либо:
+ - Ключ = `zip_source + "/" + filename` (как предлагает заказчик)
+ - Или ключ = `(zip_source || "") + filename`
+
+ Я за вариант с конкатенацией в одну строку — проще для сравнения.
+
+- **unzip.py не трогаем** — верно. Имя ZIP уже есть на фронте (`file.name`).
+
+### 2.2. Вариант отображения — ✅ СОГЛАСЕН (Вариант А)
+
+Заголовок-секция ZIP + отступ `padding-left: 24px`.
+- Просто, без нового состояния
+- Соответствует тому что описал заказчик
+- Опциональное сворачивание — да, но не в первой итерации
+
+### 2.3. Два UI-режима — ⚠️ ЧАСТИЧНО СОГЛАСЕН
+
+**Плюсы:**
+- Бэкенд не меняется — правильно
+- `state.ui.mode` — хорошее место
+- Сводный отчёт («зелёное сворачиваем, красное показываем») — отличная идея
+- Авто-определение + ручной override — разумно
+
+**Спорные моменты:**
+- **Порог 100 файлов** — слишком низкий для автоматического предложения. При 100 файлах текущий UI работает нормально (скролл, 50vh). Реальный болевой порог — **200-300+**. Предлагаю порог **200**.
+- **«Поток» сейчас не нужен.** Если заказчик работает с 5-50 файлами, весь Stream Mode — оверинжиниринг. Но архитектурно заложить `state.ui.mode` — дёшево и правильно.
+
+### 2.4. Промпты — ✅ СОГЛАСЕН, с уточнениями
+
+#### Classify — проблемы А-Д:
+
+**А. Counterparty / блок про стороны НУБЕС** — 🔴 КРИТИЧНАЯ.
+Опус абсолютно прав. Промпт сейчас не говорит что НУБЕС = Исполнитель. LLM возвращает случайную сторону. Чинится одной вставкой в промпт. Делать **первым**.
+
+НО: Опус предлагает «ИНН 7727... (взять у заказчика)». Это перебор. Достаточно:
+```
+НУБЕС известен как: «НУБЕС», «ООО НУБЕС», «ООО "НУБЕС"», «Nubes».
+counterparty — ВСЕГДА вторая сторона, НИКОГДА не НУБЕС.
+```
+
+**Б. parent_number у contract** — ✅ верно. `parent_number = null` для contract.
+
+**В. Мусорные документы** — ✅ верно. Добавить примеры в `doc_type=other`.
+
+**Г. Few-shot примеры в classify** — ✅ верно. 1-2 примера улучшат точность.
+
+**Д. confidence** — ⚠️ спорно. Опус говорит «добавить колонку и показывать low в отчёте». Но `confidence` сейчас даже не сохраняется. Предлагаю **сначала убрать из промпта** (меньше путаницы), а потом, когда будет реальная потребность — добавить и колонку, и парсинг.
+
+#### Compare (diff) — ✅ СОГЛАСЕН
+
+- «UNRESOLVED вместо дубль-ADD при сомнении» — верно
+- `temperature=0.1` для diff — проверить (скорее всего уже)
+- `full_replace` → автоматическое удаление старых строк на стороне Python — **умная идея**, снижает нагрузку на LLM
+
+#### Разные промпты под сценарии — ✅ СОГЛАСЕН
+
+Не нужно. Один classify + один diff. Меньше рассинхрона.
+
+### 2.5. Гомоглифы — ⚠️ ОСТОРОЖНО
+
+Опус предлагает:
+- `С/C → C` (латиница)
+- `О/0` — «трактовать осторожно»
+- `Ё → Е`
+
+**Моё мнение:**
+- `С→C` и `Ё→Е` — **опасно**. Это меняет семантику номера. `МЭС-123` ≠ `МЭC-123`. Лучше: **не заменять, а добавить второй проход сравнения** — если точное совпадение не найдено, попробовать с гомоглифами. Или нормализовать ОБА варианта (и кириллицу, и латиницу) к единому представлению, но сохранять оригинал для отображения.
+- Конкретно для `Ё`: да, `Ё→Е` допустимо (в делопроизводстве Ё часто заменяют на Е). Но лучше сделать настраиваемым.
+
+### 2.6. Приоритеты — ✅ СОГЛАСЕН с корректировкой
+
+| Что | Приоритет Опуса | Моя оценка |
+|-----|----------------|-----------|
+| Counterparty в промпте | P0 | P0 ✅ |
+| doc_type=other мусор | P0 | P0 ✅ |
+| Гомоглифы | P0 | P1 ⚠️ (осторожно, не ломать) |
+| zip_source | P1 | P1 ✅ |
+| Группировка по ZIP в UI | P1 | P1 ✅ |
+| Режим «Поток» | P2 | P3 (отложить, нет потребности) |
+| confidence в БД | P3 | P3 (или убрать из промпта) |
+| diff-UNRESOLVED | P3 | P2 (дёшево, большой эффект) |
+
+---
+
+## 3. Что Опус упустил
+
+### 3.1. Lucee-слой
+
+`upload.cfm` и `api.cfm` на Lucee тоже обрабатывают загрузку. Если файл идёт через Lucee (а не напрямую на VM), `zip_source` нужно прокинуть и там. Надо проверить — идёт ли upload через Lucee или напрямую на VM.
+
+**Факт:** судя по `index.cfm`, JS грузится с VM (`contracts.kube5s.ru/static/app.js`), а upload идёт на `VM_API + '/upload'`. Значит Lucee в upload **не участвует**. `zip_source` в Lucee не нужен. Опус оказался прав молча.
+
+### 3.2. Промпты в БД vs хардкод
+
+Опус верно заметил: «промпты берутся из БД (Lucee), fallback — хардкод. Менять надо в БД через интерфейс промптов». Это **критично важно для исполнителя**: если просто поправить `FALLBACK_EXTRACT`/`FALLBACK_DIFF` в `llm_prompt.py` — в проде ничего не изменится, потому что используется версия из БД.
+
+**Порядок правки промптов:**
+1. Сначала в БД через UI (`/prompt.cfm`)
+2. Потом в хардкоде (для fallback)
+
+### 3.3. Порог для «Потока»
+
+100 файлов — слишком консервативно. Таблица с `max-height: 50vh` и `overflow-y: auto` нормально работает при 100-150 файлах. Предлагаю **200** как порог для автопредложения. Но лучше — **сделать настраиваемым** (константа в начале app.js).
+
+---
+
+## 4. Итоговое мнение
+
+**Ответ Опуса — хороший план.** 95% рекомендаций принимаю.
+
+**Что делаем прямо сейчас (P0):**
+1. Правка classify-промпта: блок про стороны НУБЕС + parent_number=null + примеры мусора
+2. Правка diff-промпта: UNRESOLVED вместо дубль-ADD, temperature проверка
+
+**Что делаем дальше (P1):**
+3. `zip_source` сквозь все слои (БД → Python → JS)
+4. Группировка по ZIP в таблице (Вариант А)
+5. Дедупликация по `(zip_source, filename)`
+
+**Что откладываем:**
+6. Режим «Поток» — пока нет потребности
+7. Гомоглифы — нужно больше примеров от заказчика
+8. confidence — убрать из промпта, вернуть когда будет нужно
+
+**Главный риск (ещё раз):** дедупликация в `addRegularFile()`. Без правки ключа на `(zip_source, name)` — фича сломается на первом же случае одинаковых имён в разных ZIP.
diff --git a/History/opus-architecture-research-2026-06-27-review.md b/History/opus-architecture-research-2026-06-27-review.md
new file mode 100644
index 0000000..251fef7
--- /dev/null
+++ b/History/opus-architecture-research-2026-06-27-review.md
@@ -0,0 +1,133 @@
+# Рецензия DeepSeek V4 Pro на отчёт Opus
+
+Дата: 27.06.2026
+
+---
+
+## Общая оценка: ⭐⭐⭐⭐⭐ (5/5)
+
+Opus сделал ТО, ЧТО НУЖНО: прочитал код, нашёл расхождения с документацией, не писал код, не рисовал графы. Это эталонный анализ.
+
+---
+
+## Что Opus нашёл такого, чего я НЕ заметил
+
+### 🔴 КРИТИЧЕСКОЕ: PyPDF2 теряет структуру таблиц
+
+Я читал `parse.py`, видел `PyPDF2`, но **не осознал** что он извлекает текст построчно и не сохраняет колонки. Для спецификаций — это катастрофа. Таблица «наименование | цена | кол-во | сумма | дата» после PyPDF2 станет плоским текстом, и LLM придётся угадывать где какая колонка.
+
+**Opus прав на 100%.** Это вероятно главный риск точности прямо сейчас.
+Надо: `pdfplumber` (для цифровых PDF) или `camelot`.
+
+### 🔴 КРИТИЧЕСКОЕ: Кода сверки CRM↔фискальная НЕТ
+
+Я знал это из ответов заказчика, но Opus проверил grep'ом (`crm|фискал|fiscal|сверк|PAYG|reconcil`) — нашёл только «акт сверки» как тип мусора. Это greenfield. Меняет планирование: не «эволюция», а «новый модуль».
+
+### 🟡 ВАЖНОЕ: service-description.md устарел
+
+Я дал Opus читать `service-description.md` как «актуальный». Opus обнаружил что он описывает парсинг через Lucee (Java POI/PDFBox), а **реальный код** (`upload.py` → `parse.py`) парсит на ВМ через PyPDF2/python-docx. Без Java. Без Lucee.
+
+**Моя ошибка:** я не перепроверил service-description.md на соответствие коду. Надо поправить доку.
+
+### 🟡 ВАЖНОЕ: llm_prompt.py зависит от Lucee
+
+`build_prompt()` (основной пайплайн) ходит HTTP в Lucee за активным промптом. А `build_classify_prompt()` берёт из БД напрямую. Непоследовательно + точка отказа.
+
+### 🟡 ВАЖНОЕ: chat.cfm содержит захардкоженный API-ключ
+
+Плюс ссылается на `spec_rows` вместо `spec_current` — сломан вдвойне.
+
+### 🟢 Безопасность: verify=False в llm.py
+
+TLS-проверка отключена. Надо включить.
+
+---
+
+## Где Opus прав, а где нет
+
+| Тезис Opus | Моё мнение |
+|------------|------------|
+| **Гипотеза 1 (гибрид): ⚠ частично** — «текущий код — чистый pipeline, и это правильно» | ✅ **Согласен.** 131 тест на детерминированные функции — сильный аргумент против агентов. |
+| **Гипотеза 2 (фильтр мусора): ⚠ частично** — «этап 1 по имени файла ненадёжен» | ⚠ **Частично.** Для имён типа «счет-фактура №123 от 01.01.2025.docx» regex надёжен. Но для «scan001.pdf» — да, бесполезен. Вывод: этап 1 — приоритизация, не жёсткий дроп. |
+| **Гипотеза 3 (парсинг на ВМ): ❌ отвергнута** | ✅ **Полностью согласен.** Моя гипотеза была основана на устаревшей документации. Парсинг уже на ВМ. |
+| **Гипотеза 4 (трёхуровневый matching): ⚠ частично** — «difflib опасен на коротких токенах» | ✅ **Согласен.** Примеры «IPv4↔IPv6», «10 кВт↔15 кВт» убедительны. Матчить по структурным атрибутам, не по сырой строке. |
+| **Гипотеза 5 (MVP): ⚠ частично** — «нужен CRM-прогон уже в v1» | ✅ **Согласен.** Иначе v1 будет «готов», а к цели не приблизимся. |
+| **Гипотеза 6 (PG-очередь): ✅ подтверждена** | ✅ **Согласен.** Плюс Opus добавил: сейчас очереди нет вообще, in-process ThreadPool. |
+| **«LLM бесплатный ≠ быстрый»** — не наращивать проходы | ✅ **Согласен.** 5-30s × 100+ файлов = узкое место. |
+| **«Не отказываться от детерминированного matching»** | ✅ **Согласен.** Воспроизводимость и аудит важнее «бесплатности». |
+
+### Единственное где я НЕ согласен:
+
+**Opus говорит «не использовать имя файла для фильтрации».**
+Я считаю: regex `(сч[её]т|акт|плат[её]ж|УПД|сверк|инвойс|invoice)` по имени файла — ДОСТАТОЧНО надёжен для пред-фильтрации. «Счет-фактура №123.docx» — это всегда мусор. Да, «scan001.pdf» не отфильтруется — но он уйдёт на этап 2 (первые 2KB текста). Риск ложного срабатывания на договоре с именем «счет-фактура» — ноль.
+
+---
+
+## Что Opus упустил
+
+1. **testgen/** — в репо есть `testgen/generate.py`, `testgen/pools.py`, `testgen/templates.py`. Это генератор синтетических тестовых данных. Opus предлагает «тестировать матчинг на синтетике», но не знает что инструмент уже есть.
+
+2. **convert_server.py роутинг** — Opus не проверил ВСЕ эндпоинты. Например, `/api/batch-progress`, `/api/apply-groups`, `/api/cleanup`, `/api/sync` — они есть, но не проанализированы.
+
+3. **services/llm.py — расхождение ВМ↔репо** — Opus заметил `verify=False`, но не предложил КАК выяснить какая версия каноническая (репо или ВМ). Я поднимал этот вопрос в `opus-plan-review-2026-06-27.md`.
+
+4. **LibreOffice — узкое место для .doc** — `/convert-doc` запускает LibreOffice subprocess последовательно с таймаутом 30с. На пачке старых .doc файлов это потенциально медленнее даже LLM. Но на практике .doc — редкость (заказчик говорил про docx/pdf).
+
+5. **OCR не нужен** — заказчик уточнил: «только текст, сканов не будет». Значит риск «потребуется внешний OCR-сервис» снимается.
+
+---
+
+## Дополнения из второго анализа (подтверждения)
+
+Второй анализ полностью подтвердил выводы Opus. Дополнительно акцентировано:
+
+- **Дрейф документации** — не только `service-description.md`, но и другие .md могут устареть. Правило: **код > документация**. Всегда проверять.
+- **base64 в БД** — `upload.py` кладёт файлы как base64 в `documents`. На 100+ многомегабайтных файлах это раздует БД. Риск масштабирования.
+- **Нет устойчивой очереди** — `classify.py` = `ThreadPoolExecutor` в одном процессе. Падение процесса = потеря прогресса. PG-очередь добавит durability.
+
+---
+
+## Итог: что я беру в работу
+
+### Немедленно (следующий чат, по команде «делай»):
+
+1. **Заменить PyPDF2 на pdfplumber** в `services/parse.py` — критично для точности
+2. **Починить chat.cfm** — убрать ключ, `spec_rows` → `spec_current`
+3. **Включить verify=True** в `services/llm.py`
+4. **Отвязать llm_prompt.py от Lucee** — `build_prompt` → `db.prompts.get_active()`
+5. **Поправить service-description.md** — парсинг на ВМ, не через Lucee
+
+### Вторая очередь:
+
+6. **Детерминированная проверка `sum == price*qty`** — бесплатный сигнал ошибок
+7. **Золотой набор** — 30-50 реальных документов с ручной разметкой
+8. **Метрики** — логировать `_safe_json_parse` срабатывания, UNRESOLVED, латентность
+
+### Третья очередь (после золотого набора):
+
+9. **Спроектировать нейтральную CanonicalRow** для CRM↔фискальная
+10. **Прототип матчинга** на синтетике из testgen/
+
+---
+
+## Вывод
+
+Opus дал **отличный анализ**. Главная ценность — нашёл расхождения кода и документации, которые я пропустил. PyPDF2 — критическая находка. То что кода CRM-сверки нет — меняет приоритеты: не «эволюция», а «новый модуль».
+
+**Что отсеялось после двух анализов:**
+- OCR не нужен (только текст, без сканов)
+- LLM не использовать агрессивнее (бесплатный ≠ быстрый, 5-30s/вызов)
+- Агенты не нужны (131 тест на pipeline, детерминизм > гибкость)
+- RabbitMQ не нужен (PG-очередь достаточна)
+
+**Что осталось в работе (5 немедленных + 3 второй очереди):**
+1. pdfplumber вместо PyPDF2
+2. chat.cfm: ключ + таблица
+3. llm.py: verify=True
+4. llm_prompt.py: отвязать от Lucee
+5. service-description.md: поправить
+6. Проверка sum==price*qty
+7. Золотой набор 30-50 документов
+8. Метрики (_safe_json_parse, UNRESOLVED, латентность)
+
+Следующий шаг: команда «делай» → правлю пункты 1-5.
diff --git a/History/opus-architecture-research-2026-06-27.md b/History/opus-architecture-research-2026-06-27.md
new file mode 100644
index 0000000..56f5e96
--- /dev/null
+++ b/History/opus-architecture-research-2026-06-27.md
@@ -0,0 +1,209 @@
+# Задание Opus: Архитектурное исследование — Сверка договоров v2
+
+Дата: 27.06.2026 | От: DeepSeek V4 Pro (через Крупского) | Кому: Opus
+
+---
+
+## ⛔ ЧЕГО НЕ ДЕЛАТЬ
+
+- **НЕ пиши код.** Ни строчки Python, SQL, JS, ничего. Только концепции.
+- **НЕ рисуй mermaid-диаграммы.** Только текст. Исполнитель (DeepSeek) сам нарисует если надо.
+- **НЕ предлагай «переписать с нуля».** Продакшен надо эволюционировать.
+- **НЕ лезь в файлы за пределами списка ниже.** Экономия токенов.
+
+## 📋 ЗАЧЕМ ЭТОТ АНАЛИЗ
+
+Ты — исследователь. Твой отчёт пойдёт **DeepSeek V4 Pro** (мне). Я буду по нему ПИСАТЬ КОД.
+
+У нас нет промежуточных результатов — неизвестна точность LLM на реальных 100+ документах,
+неизвестны типичные расхождения CRM↔фискальная. Нужна архитектура, которую можно
+итеративно улучшать по мере данных.
+
+Главное — **концептуальные решения**. Не реализации.
+
+---
+
+## ⛔ КАКИЕ ФАЙЛЫ СМОТРЕТЬ
+
+### Код (18 + 18 + 7 = 43 файла):
+```
+contractor/index.cfm — HTML/CSS скелет (v1.0.178)
+contractor/upload.cfm — приём файлов (form/JSON/multipart/iframe)
+contractor/chunk.cfm — чанковая загрузка
+contractor/process.cfm — SSE-пайплайн v1 (прямой LLM)
+contractor/process_v2.cfm — SSE-пайплайн v2 (→ ВМ, event sourcing)
+contractor/apply_events.cfm — ADD/UPDATE/DELETE → spec_current
+contractor/extractor.cfm — старый вариант извлечения spec_rows
+contractor/parser.cfm — парсинг docx/pdf (PDFBox + POI)
+contractor/differ.cfm — сравнение допников с базовым договором
+contractor/chat.cfm — Q&A (⚠ сломан: spec_rows вместо spec_current)
+contractor/api.cfm — создание схемы БД
+contractor/Application.cfc — конфигурация Lucee + datasource
+contractor/db.cfc — REST-обёртка БД
+contractor/view.cfm — просмотр текста документа
+contractor/prompt.cfm — версионирование промптов
+contractor/reset_contract.cfm — сброс контракта
+contractor/test.cfm — тест
+contractor/jars.cfm — проверка Java-библиотек
+
+contractor/deploy/convert_server.py — HTTP-роутер (:8766)
+contractor/deploy/llm_prompt.py — промпты + fallback (⚠ зависит от Lucee)
+contractor/deploy/classify_worker.py — фоновый процесс (subprocess)
+contractor/deploy/convert_doc.py — парсинг docx/pdf
+contractor/deploy/services/upload.py — загрузка
+contractor/deploy/services/unzip.py — ZIP
+contractor/deploy/services/classify.py — классификация (LLM: тип/номер/контрагент)
+contractor/deploy/services/process.py — пайплайн сравнения
+contractor/deploy/services/llm.py — вызов LLM API (⚠ ВМ↔репо расходятся)
+contractor/deploy/services/parse.py — PDF (PyPDF2) / DOCX (python-docx)
+contractor/deploy/services/grouping.py — группировка (гибрид LLM→Python)
+contractor/deploy/db/connection.py — psycopg2
+contractor/deploy/db/documents.py — CRUD документов
+contractor/deploy/db/supplements.py — CRUD дополнений
+contractor/deploy/db/spec_current.py — CRUD текущей спецификации
+contractor/deploy/db/spec_events.py — event sourcing (seq, reset)
+contractor/deploy/db/contracts.py — CRUD контрактов
+contractor/deploy/db/prompts.py — CRUD промптов (seed defaults)
+
+contractor/deploy/app.js — фронтенд (VM_API, render, stepper)
+contractor/deploy/app_utils.js — утилиты
+contractor/deploy/state.js — центральный state
+contractor/deploy/files.js — загрузка/рендер файлов
+contractor/deploy/groups.js — карточки групп
+contractor/deploy/compare.js — SSE-сравнение
+contractor/deploy/tests.js — 131 юнит-тест
+```
+
+### Документы (6 штук):
+```
+History/topics/customer-qa-2026-06-26.md — ответы заказчика (ОБЯЗАТЕЛЬНО)
+History/architecture/vm-layout.md — ⭐ раскладка ВМ (самый актуальный)
+History/architecture/service-description.md — описание сервиса
+History/architecture/two-panel-architecture.md — две панели просмотра
+History/opus-plan-review-2026-06-27.md — разбор предыдущего плана Opus
+History/llm-analysis/decoupling-final-plan.md — план размоноличивания JS
+```
+
+### ⛔ НЕ ЧИТАТЬ:
+```
+History/architecture/architecture.md — ⛔ МЁРТВЫЙ Flask contracts-app/site/
+History/architecture/block-diagram.md — ⛔ МЁРТВЫЙ Flask
+History/architecture/pipeline.md — ⛔ МЁРТВЫЙ Flask
+sim/ testgen/ dogovora/ FILES/ Files/ DOC/ contracts-flask/ contracts-vm/
+README.md sonnet-v2-request.md
+History/sessions/ History/features/ History/llm-analysis/*
+ (КРОМЕ decoupling-final-plan.md)
+```
+
+---
+
+## 📋 КОНТЕКСТ: что уже надумал DeepSeek
+
+Я (DeepSeek) уже провёл предварительный анализ. Ниже — мои гипотезы.
+**Твоя задача: подтвердить, опровергнуть или дополнить.** Не просто соглашаться.
+
+### Гипотеза 1: Гибрид pipeline + агент
+Pipeline для 95% стандартных случаев (дёшево, предсказуемо).
+Лёгкий оркестратор-агент — только для краевых случаев (нестандартный формат, ошибка парсинга).
+Pure agents — дорого: 100 файлов × 500 токенов оркестратора = 50K токенов только на планирование.
+
+### Гипотеза 2: Трёхэтапный фильтр мусора
+Этап 1 (0 токенов): regex по имени файла — «счёт», «акт», «платёж» → сразу garbage.
+Этап 2 (0 токенов): ключевые слова в первых 2KB текста — «СЧЕТ-ФАКТУРА», «АКТ СВЕРКИ» → garbage.
+Этап 3 (LLM): только оставшиеся → точный тип (contract/supplement/specification).
+Экономия: ~50% LLM-вызовов при 50% мусора в пачке.
+
+### Гипотеза 3: Перенос парсинга на ВМ
+Сейчас: JS → ВМ → Lucee (Java PDFBox/POI) → обратно на ВМ.
+Предложение: JS → ВМ → pdfplumber + python-docx (без Java, без Lucee).
+Убирает latency сетевого вызова, упрощает деплой.
+
+### Гипотеза 4: Трёхуровневый matching CRM↔фискальная
+Уровень 1 (0 токенов): хеш нормализованного названия.
+Уровень 2 (0 токенов): fuzzy string matching (difflib).
+Уровень 3 (LLM): только для 10-20% несопоставленных.
+80% строк матчатся без LLM.
+
+### Гипотеза 5: MVP-границы
+v1: извлечение спецификаций из договоров (без CRM-сверки). Доказать что LLM вообще справляется.
+v2: сверка CRM↔фискальная (когда качество извлечения подтверждено).
+v3: красивый UI, чат, автообучение на коррекциях.
+
+### Гипотеза 6: PostgreSQL-очередь вместо RabbitMQ
+Для 100+ файлов пару раз в неделю — хватит `documents.classify_status='pending'` + `FOR UPDATE SKIP LOCKED`. RabbitMQ — оверкилл.
+
+---
+
+## ❓ ВОПРОСЫ К OPUS (5 блоков)
+
+### Блок 1: Насколько агрессивно использовать LLM?
+
+**Контекст:** LLM заказчика — gpt-oss-120b через api.aillm.ru — **БЕСПЛАТНЫЙ**. Никаких затрат на токены.
+
+Сейчас LLM используется 3 раза за прогон:
+- Классификация (тип/номер/контрагент)
+- Извлечение спецификации
+- Сравнение ДС
+
+**Вопросы:**
+1.1. Имеет ли смысл использовать LLM **больше** раз на одном документе? Например:
+- Два прохода извлечения (второй — верификация первого)?
+- LLM-as-judge: проверять свой же результат и исправлять ошибки?
+- LLM для принятия решений по ходу пайплайна (оркестратор)?
+
+1.2. Где LLM **реально** добавляет ценность, а где детерминированный код справится лучше?
+(С учётом что LLM медленный: 2-30 секунд на вызов.)
+
+1.3. Если LLM бесплатный — может **вообще отказаться от детерминированного matching** (Гипотеза 4) и всё гонять через LLM? Плюсы/минусы.
+
+### Блок 2: Архитектура — pipeline или агент?
+
+2.1. Критика гибридного подхода (Гипотеза 1). Что я упустил? В каких сценариях чистый pipeline или чистые агенты были бы лучше?
+
+2.2. Если гибрид — где конкретно проходят границы? Какие решения принимает оркестратор, какие — pipeline?
+
+2.3. Есть ли смысл в нескольких специализированных агентах (parse-agent, classify-agent, compare-agent) вместо одного оркестратора? Плюсы/минусы.
+
+### Блок 3: Обработка 100+ файлов и фильтрация
+
+3.1. Критика трёхэтапного фильтра (Гипотеза 2). Какие документы он пропустит? Ложные срабатывания?
+
+3.2. Парсинг на ВМ (Гипотеза 3) — правильно ли отказываться от Java PDFBox/POI в пользу pdfplumber/python-docx? Качество парсинга таблиц упадёт или вырастет?
+
+3.3. Какие ещё узкие места будут при 100+ файлах, кроме классификации? Память? Сеть? Диск?
+
+### Блок 4: Сверка CRM ↔ фискальная
+
+4.1. Критика трёхуровневого matching (Гипотеза 4). В каких случаях fuzzy matching (difflib) даст ложные срабатывания? Примеры.
+
+4.2. PAYG (суффикс `-m`, нет артикулов): как сопоставлять? Достаточно убирать `-m` и сравнивать названия, или нужна более сложная логика?
+
+4.3. Если заказчик НЕ даст формат CRM в ближайшее время — как спроектировать модуль чтобы не простаивать? Что можно сделать уже сейчас (без CRM-данных)?
+
+### Блок 5: Итеративность и метрики
+
+5.1. Критика MVP-границ (Гипотеза 5). Может что-то из v2 нужно уже в v1? Или наоборот — что-то из v1 отложить?
+
+5.2. Какие **конкретные метрики** внедрить с первого дня? Не «точность» вообще, а что именно измерять? Как измерять без эталонных данных?
+
+5.3. Цикл обратной связи: заказчик проверяет → исправляет → система учитывает. Как это делать без дообучения модели (gpt-oss-120b — API, не наша)? Только few-shot в промпте? Или есть другие подходы?
+
+5.4. **Главный вопрос:** мы не знаем точность LLM. С чего начать? Просто прогнать 100 реальных документов через текущий код и посмотреть? Или сначала улучшить фильтрацию/промпты, а потом прогонять?
+
+---
+
+## 📐 ФОРМАТ ОТВЕТА
+
+1. **Executive Summary** — 3-5 абзацев, главные выводы
+2. **По каждой гипотезе** — вердикт (✅ подтверждаю / ⚠ частично / ❌ отвергаю) + аргументация
+3. **Ответы на вопросы** — по блокам, кратко
+4. **Что я упустил** — твои собственные идеи, которых нет в моих гипотезах
+5. **MVP-план** — что делать в ближайшие 2 недели (пункты, не код)
+6. **Риски** — что может пойти не так
+
+## ⚠️ ОГРАНИЧЕНИЯ
+
+- Конфиденциальность: никаких S3, только локал / файловая шара
+- Managed Lucee: не можем ставить pip-пакеты на нём
+- ВМ: 5.172.178.213, Ubuntu, Python 3.12, PostgreSQL 15
+- LLM: gpt-oss-120b, 8000 токенов, ~5-30s на вызов, БЕСПЛАТНО
diff --git a/History/opus-classify-async-answer.md b/History/opus-classify-async-answer.md
new file mode 100644
index 0000000..580e2e5
--- /dev/null
+++ b/History/opus-classify-async-answer.md
@@ -0,0 +1,100 @@
+# Ответ Опуса — фоновая классификация при 50+ файлах
+
+Ответ на `History/opus-classify-async-question.md` от 25.06.2026.
+
+---
+
+## Диагноз: что рвётся на самом деле
+
+**nginx НЕ виноват.** `classify-batch` отдаёт `202` мгновенно. `batch-progress` — короткие независимые запросы, укладываются в 30с. Классификация идёт `classify_worker → api.aillm.ru` напрямую, минуя nginx.
+
+Реальное узкое место — **симулятор сдаётся на 4-й минуте** (120 итераций × 2с), а 69 файлов при 4 воркерах не успевают: 69 / 4 ≈ 18 волн × 10-15с/LLM-вызов = 3-5 минут > лимита симулятора.
+
+---
+
+## Ответы
+
+### Q1. `subprocess.Popen` для прода — норм?
+
+Для масштаба сотни-тысячи файлов регулярно `Popen`-на-запрос **неадекватен**:
+- Нет супервизии: упал worker — никто не узнал (stdout/stderr в DEVNULL)
+- Нет авто-рестарта
+- Гонки при параллельных батчах с одним `batch_id`
+
+**Целевая архитектура:** постоянный worker-сервис под отдельным systemd-юнитом + очередь задач на основе БД.
+
+Почему БД-очередь, а не Redis:
+- Work items уже в БД (`documents.classify_status='pending'`)
+- Устойчиво к рестартам ВМ — resumable
+- Redis не установлен в проде
+- Для одной ВМ Celery/RQ — оверкилл
+
+Дополнительно при тысячах файлов:
+- retry/backoff на 429/5xx от api.aillm.ru
+- rate-limit к LLM API
+- Аккуратное повышение параллелизма (не 4 воркера, а 10-15)
+
+### Q2. nginx при долгих запросах
+
+nginx НЕ узкое место. classify-batch=202, batch-progress<30s, классификация идёт worker→LLM напрямую. Реальное узкое: throughput (4 воркера) + лимит симулятора. Прогресс durable в БД, UI не зависит от дедлайна.
+
+### Q3. Два classify подряд — два Popen
+
+- **Разные `batch_id`** — безопасно. Каждый процесс работает только над своими документами.
+- **Один и тот же `batch_id` дважды** — гонка! Оба сделают `reset_classify_status` + повторные LLM-вызовы → двойные траты токенов, неконсистентные счётчики.
+
+**Решение:** НЕ убивать предыдущий процесс (опасно — может испортить БД). Guard через проверку: если для `batch_id` уже есть running-процесс → вернуть `{"ok": false, "error": "already running"}`. Реализация: либо файл-лок (`/tmp/classify_.lock`), либо запись в БД.
+
+### Q4. Мониторинг/логирование
+
+DEVNULL недопустим на масштабе — падения невидимы.
+
+**Минимум сейчас:** лог-файл per batch `/home/naeel/contracts/logs/classify_.log` (Python logging) вместо DEVNULL.
+
+**Целевое:** job-таблица в БД:
+```sql
+CREATE TABLE classify_jobs (
+ batch_id UUID PRIMARY KEY,
+ started_at TIMESTAMP,
+ finished_at TIMESTAMP,
+ total INT, done INT, failed INT,
+ error_text TEXT,
+ pid INT
+);
+```
+Даёт: честный прогресс, видимость падений, per-doc retry, аудит.
+
+---
+
+## Целевая архитектура (будущее)
+
+```
+systemd: contracts.service (HTTP)
+systemd: classify-worker.service (фоновая классификация)
+
+flow:
+ HTTP → 202 + запись в classify_jobs (status='queued')
+ classify-worker (постоянно):
+ SELECT batch_id FROM classify_jobs WHERE status='queued' LIMIT 1
+ → status='running'
+ → classify_batch(batch_id)
+ → status='done' + метрики
+ → следующий батч
+
+ Прогресс: documents.classify_status (pending/classified/failed)
+ Поллинг: GET /api/batch-progress?batch=X → count by status
+```
+
+Преимущества:
+- Устойчиво к рестартам (состояние в БД)
+- Один процесс обрабатывает батчи последовательно — нет гонок
+- systemd мониторит и рестартует при падении
+- Логи systemd/journald
+
+---
+
+## Минимум сейчас (без переписывания архитектуры)
+
+1. **Лог-файл вместо DEVNULL:** `stdout=open(log_path, 'w')`
+2. **Лимит симулятора:** увеличить `MAX_WAIT` до 600с (10 мин) для bulk-тестов
+3. **Guard от двойного classify:** файл-лок `/tmp/classify_.lock` — если существует, вернуть "already running"
diff --git a/History/opus-classify-async-question.md b/History/opus-classify-async-question.md
new file mode 100644
index 0000000..98c3ca7
--- /dev/null
+++ b/History/opus-classify-async-question.md
@@ -0,0 +1,28 @@
+# Вопрос к Опусу — фоновая классификация при 50+ файлах
+
+## Что сделано
+
+`POST /api/classify-batch` получает `{batch_id}`, запускает `classify_worker.py`
+через `subprocess.Popen` и сразу возвращает `202 {total: N}`.
+Фронтенд поллит `/api/batch-progress` каждые 2с пока `done >= total`.
+
+`classify_worker.py` — отдельный питон-процесс, импортирует `services/classify.py`,
+у которого внутри `ThreadPoolExecutor(max_workers=4)` для параллельных
+LLM-вызовов (один файл = один LLM-запрос к api.aillm.ru).
+
+## Проблема
+
+С 4 файлами полный цикл работает. С 69 файлами:
+- HTTP-сервер жив, `/health` отвечает
+- `classify_worker` работает
+- Симулятор с внешней машины не дожидается конца — на 5+ минутах рвётся сеть/nginx
+
+## Вопросы
+
+1. Архитектурно `subprocess.Popen` норм для прода? Или что-то более надёжное (очередь, systemd-таймер, воркер-пул)?
+
+2. Что делать с nginx при долгих запросах? `batch-progress` — короткий поллинг, он не должен рваться. Но сам classify через фронтенд не идёт — только через воркер. Где узкое место?
+
+3. При двух быстрых классификациях подряд — второй `Popen` создаст второй процесс. Старый ещё не умер. Надо проверять и убивать предыдущий? Или пусть оба работают (разные batch_id)?
+
+4. Как правильно мониторить/логировать фоновый процесс? Сейчас stdout/stderr в `/dev/null`.
diff --git a/History/opus-plan-review-2026-06-27.md b/History/opus-plan-review-2026-06-27.md
new file mode 100644
index 0000000..331e138
--- /dev/null
+++ b/History/opus-plan-review-2026-06-27.md
@@ -0,0 +1,87 @@
+# Разбор плана Opus от 2026-06-27
+
+## Что произошло
+
+Opus'у дали задачу: «изучи код и напиши подробный план для DeepSeek V4 Pro по созданию параллельного Flask-стека 1:1 с Lucee↔ВМ, домен check.kube5s.ru».
+
+Opus изучил репозиторий (без доступа к ВМ), считая `contractor/deploy/` зеркалом продакшена, и выдал план из 6 фаз.
+
+---
+
+## Что Opus выяснил правильно
+
+### Архитектура продакшена
+- **Lucee — почти пустая морда.** Только `index.cfm` (HTML+CSS-скелет), `chat.cfm` (Q&A), `Application.cfc`. Вся логика — на ВМ.
+- **JS-файлы** (app.js, files.js, groups.js, state.js, compare.js, app_utils.js) — клиентские, загружаются через `