v1.0.177: History — объединение history+History, 5 подпапок

This commit is contained in:
“Naeel”
2026-06-25 07:53:56 +04:00
parent 14cca9f8cf
commit c40d8eb5a8
54 changed files with 0 additions and 0 deletions
+145
View File
@@ -0,0 +1,145 @@
Read [](file:///home/naeel/nubes/contracts/contractor/apply_events.cfm)
Read [](file:///home/naeel/nubes/contracts/contractor/db.cfc#1-1), lines 1 to 150
Read [](file:///home/naeel/nubes/contracts/contractor/deploy/llm_prompt.py)
Read [](file:///home/naeel/nubes/contracts/contractor/deploy/convert_server.py#1-1), lines 1 to 150
Read [](file:///home/naeel/nubes/contracts/contractor/deploy/convert_server.py#150-150), lines 150 to 250
Проанализировал все файлы. Вот подробный аудит.
---
## Аудит Event Sourcing — сверка договоров
---
### Находки Gemini — подтверждение/опровержение
**1. Race condition `MAX(seq)` — ✅ ПОДТВЕРЖДЕНО, критично**
apply_events.cfm: `SELECT COALESCE(MAX(seq), 0)` → `seq++` в цикле — это read-modify-write без блокировки. Два параллельных вызова (разные supplements) получат одинаковый `MAX(seq)` и сгенерируют одинаковые seq. В DDL есть `UNIQUE(contract_id, seq)` — транзакция упадёт с ошибкой дублирующегося ключа.
**Рекомендация:** Заменить на `nextval` через sequence (`CREATE SEQUENCE spec_events_seq`) или PostgreSQL `INSERT ... RETURNING` с `seq = (SELECT COALESCE(MAX(seq),0)+1 FROM spec_events WHERE contract_id=... FOR UPDATE)` — с `FOR UPDATE` для пессимистической блокировки.
---
**2. `full_replace` с пустым `ops` стирает спецификацию — ✅ ПОДТВЕРЖДЕНО, критично**
apply_events.cfm: Если LLM вернул `mode=full_replace` с пустым `ops[]` (например, не распознал таблицу), код сначала DELETE всех строк из `spec_current`, потом цикл по ops не выполняется. Спецификация обнулена без восстановления.
**Рекомендация:** Добавить guard: если `mode == full_replace` и `len(ops) == 0` — отклонить с ошибкой, не трогать `spec_current`.
---
**3. LLM возвращает строку вместо числа → `cf_sql_float` падает — ✅ ПОДТВЕРЖДЕНО**
apply_events.cfm: Переменные `pr`, `qt`, `sm` берутся напрямую из `op.new_row`. Промпт в llm_prompt.py говорит «ЧИСЛА, не строки», но LLM может вернуть `"1 000,00"` или `"null"` как строку. `cf_sql_float` с нечисловой строкой кидает исключение внутри транзакции — вся транзакция откатывается.
**Рекомендация:** Перед передачей в cfqueryparam привести к числу через `val()` или попытаться `javacast("double", pr)` с catch.
---
**4. N+1 SELECT в цикле UPDATE — ✅ ПОДТВЕРЖДЕНО**
apply_events.cfm: Для каждого UPDATE-op выполняется отдельный `SELECT price, qty, sum, date_start FROM spec_current WHERE name_hash=?`. Если ДС обновляет 50 строк — 50 SELECT-запросов внутри одной транзакции.
**Рекомендация:** Перед циклом собрать все `target_hash` UPDATE-ops, сделать один `SELECT ... WHERE name_hash = ANY(ARRAY[...])`, сложить результат в struct по hash.
---
**5. Построчный INSERT в `full_replace` — ✅ ПОДТВЕРЖДЕНО**
apply_events.cfm: В `full_replace`-блоке цикл по `curRows` выполняет по одному `INSERT INTO spec_events` на каждую строку. После — снова цикл по `ops`, каждый ADD — ещё 2 INSERT. Для 100-строчной спецификации: 100+100×2 = 300 отдельных INSERT внутри одной транзакции.
**Рекомендация:** Использовать `INSERT INTO spec_events ... SELECT unnest(...)` или сформировать multi-row VALUES через ColdFusion loop перед запросом.
---
**6. `name_hash` зависит от LLM-форматирования — ✅ ПОДТВЕРЖДЕНО, фундаментальная проблема**
apply_events.cfm: `md5(lower(trim(name)) || coalesce(date_start, ''))` вычисляется на стороне PostgreSQL из данных, которые LLM только что вернул. В llm_prompt.py: в `_build_diff` LLM видит `[hash: {hash}]` и должен вернуть этот же hash в `target_hash`. Но LLM может сократить название, изменить регистр или пробелы → `md5` будет другим → UPDATE-op не найдёт строку → `cfthrow "UPDATE target not found"`.
**Рекомендация:** Это структурная проблема. Варианты:
- Передавать LLM только `hash` как непрозрачный идентификатор (уже так и делается), но добавить fuzzy-matching на стороне apply: если `target_hash` не найден — искать по близкому имени и создавать UNRESOLVED вместо throw.
- Добавить отдельный endpoint для ручного разрешения UNRESOLVED.
---
**7. `last_event_id` UUID → `cf_sql_varchar` — ✅ ПОДТВЕРЖДЕНО**
apply_events.cfm: `<cfqueryparam value="#evt.id#" cfsqltype="cf_sql_varchar">` для колонки `last_event_id UUID`. Lucee отправит строку, PostgreSQL неявно приведёт к UUID — это работает, но неправильно. При включённом строгом режиме или нестандартном JDBC-драйвере может дать ошибку. Стоит использовать `cfsqltype="cf_sql_char"` с `maxlength="36"` или более явный тип.
---
### Упущенные проблемы
**8. SQL-инъекция в `_lucee_query` — КРИТИЧНО**
convert_server.py: Метод `_lucee_query` принимает готовую SQL-строку, которую формируют через f-string интерполяцию. Например:
```python
f"... WHERE s.contract_id='{cid}' ..."
```
`cid` приходит из GET-параметра `?contract_id=...` без какой-либо санитизации. Если атакующий передаст `cid = "'; DROP TABLE spec_current; --"` — запрос выполнится.
**Рекомендация:** Передавать `contract_id` как параметр через `params` API Lucee, а не конкатенацией в строку. Либо хотя бы проверять `cid` по regex UUID: `re.fullmatch(r'[0-9a-f-]{36}', cid)`.
---
**9. `elements_json` double-decode — баг надёжности**
convert_server.py:
```python
ej = docs[0]["elements_json"]
if isinstance(ej, dict) and "Value" in ej:
ej = ej["Value"]
elements = json.loads(ej)
```
Если Lucee вернул `elements_json` уже как распарсенный dict (без `"Value"` обёртки), то `json.loads(dict)` упадёт с `TypeError`. Условие `isinstance(ej, dict)` обрабатывает только Lucee-обёртку, но если `ej` уже список — код падает.
**Рекомендация:** `if isinstance(ej, (dict, list)): elements = ej if isinstance(ej, list) else json.loads(ej["Value"])`.
---
**10. `full_replace` DELETE не в правильном порядке в аудит-логе**
apply_events.cfm: DELETE-события для old строк записываются с `seq++` начиная с 0, затем ADD-события из `ops` продолжают seq. Это корректно. НО: если replay этих событий — сначала DELETE всего, потом ADD новых. Проблема: `UNIQUE(contract_id, seq)` не гарантирует порядок применения при replay, если seq не строго монотонный из-за прошлых partial-операций. Нет механизма replay/восстановления из событий.
**Рекомендация:** Либо добавить флаг `is_replayable` в архитектурный документ (явно задокументировать что replay не поддерживается), либо добавить `applied_at TIMESTAMPTZ` и `parent_seq` для правильного replay.
---
**11. LLM-ключ в открытом виде в коде**
convert_server.py: `LLM_KEY = "sk-ucI5YvOticoOQ9Kuj5K9mQ"` — hardcoded в файле. При любом git push ключ попадёт в историю.
**Рекомендация:** Перенести в переменную окружения `os.environ.get("LLM_KEY")`.
---
**12. Нет идемпотентности — повторный вызов дублирует данные**
Нет проверки что `supplement_id` уже обработан. Если `/process-v2` вызвать дважды для одного контракта — все operations применятся повторно: ADD-строки задублируются (или упадут на `UNIQUE name_hash`), seq увеличится.
**Рекомендация:** Добавить `status` колонку в `supplements` (`processed` / `pending`) и проверять в начале цикла. Либо проверять `SELECT COUNT(*) FROM spec_events WHERE supplement_id = ?` перед обработкой.
---
### Итоговая таблица приоритетов
| # | Проблема | Критичность |
|---|----------|-------------|
| 8 | SQL-инъекция в `_lucee_query` | 🔴 Критично |
| 1 | Race condition MAX(seq) | 🔴 Критично |
| 2 | full_replace + empty ops → потеря данных | 🔴 Критично |
| 12 | Нет идемпотентности | 🟠 Высокая |
| 6 | name_hash нестабилен | 🟠 Высокая |
| 3 | LLM строка вместо числа → crash | 🟠 Высокая |
| 11 | LLM-ключ в коде | 🟡 Средняя |
| 9 | elements_json double-decode | 🟡 Средняя |
| 4 | N+1 SELECT в UPDATE | 🟡 Средняя |
| 5 | Построчный INSERT в full_replace | 🟢 Низкая |
| 7 | UUID как cf_sql_varchar | 🟢 Низкая |
| 10 | Нет механизма replay | 🟢 Низкая |
@@ -0,0 +1,43 @@
# Анализ уязвимостей Event Sourcing — Gemini Web
**Дата:** 2026-06-21
## 🚨 Критические уязвимости
### 1. Race Condition в `seq`
`apply_events.cfm` — `MAX(seq)` + `seq++` в цикле. При параллельных запросах — одинаковые seq.
**Фикс:** `INSERT ... SELECT COALESCE(MAX(seq),0)+1` атомарно.
### 2. Опустошение контракта при full_replace
`apply_events.cfm` — DELETE из spec_current ДО проверки что ops не пуст. LLM упала → спецификация стёрта.
**Фикс:** валидация ops.length перед удалением.
### 3. Падение типизации price/qty
`apply_events.cfm` — LLM может вернуть строку вместо числа. `cf_sql_float` падает.
**Фикс:** `isNumeric()` перед float.
### 4. N+1 запросов в UPDATE
`apply_events.cfm` — для каждого UPDATE отдельный SELECT. 200 позиций = 200 запросов.
**Фикс:** один SELECT всех строк до цикла.
### 5. Построчный INSERT full_replace
`apply_events.cfm` — цикл INSERT вместо массового.
**Фикс:** `INSERT INTO ... SELECT`.
### 6. Хэш зависит от LLM-форматирования
`apply_events.cfm` — `md5(lower(trim(name)) || date_start)`. LLM написала «Услуги» vs «Услуга» → разный хэш → ADD/DELETE вместо UPDATE.
**Фикс:** LLM сама возвращает target_hash.
### 7. Несовместимость типов last_event_id
`apply_events.cfm` — UUID пишется как `cf_sql_varchar`.
**Фикс:** правильный UUID-тип.
## Файлы для анализа
| Файл | Роль |
|------|------|
| `contractor/apply_events.cfm` | Транзакционное применение ops |
| `contractor/db.cfc` | Схема таблиц spec_events, spec_current |
| `contractor/deploy/llm_prompt.py` | Промпты LLM (ВМ) |
| `contractor/deploy/convert_server.py` | SSE + вызов LLM + apply_events (ВМ) |
| `contractor/index.cfm` | UI, EventSource, раздел сравнения |
+62
View File
@@ -0,0 +1,62 @@
# Мой анализ ответов Opus (раунды 1 и 2)
**Дата:** 2026-06-23
---
## Раунд 1: Архитектурные вопросы
### Что взяли в работу (v1.0.108)
- Advisory lock для seq ✅
- NFKC-нормализация name_hash ✅
- ID-суррогаты r1..rN в промпте вместо хэшей ✅
- Обогащение ops именами из spec_current ✅
### Что отложили
- Параллельные вызовы LLM — семантически нельзя (допники кумулятивны)
- Таблица-счётчик для seq — advisory lock проще
- Эмбеддинги — NFKC достаточно
- Мульти-юзеры — преждевременно
---
## Раунд 2: Видение и неизведанное
### Что можно сделать прямо сейчас (v1.0.109)
| # | Что | Почему |
|---|-----|--------|
| 1 | `source_document_id` FK в spec_events | Костяк provenance. Связь интерпретации с исходным документом |
| 2 | `source_quote` в каждой op от LLM | Точная цитата из документа. Юрист видит источник |
| 3 | `confidence` 0.0-1.0 в каждой op | Self-reported моделью. Ниже 0.7 → перепроверить |
| 4 | `prompt_version` в spec_events | Какая версия промпта дала этот результат |
| 5 | `raw_llm_response` JSONB | Полный ответ модели для аудита |
### Что отложить на потом
| # | Что | Когда |
|---|-----|-------|
| 1 | Temporal UI (ползунок по датам) | После provenance, когда продукт показываем |
| 2 | Human-correction как событие | После базового UI |
| 3 | Confidence-gating (N прогонов → консенсус) | Когда точность станет критичной |
| 4 | Prompt CI (тесты для промптов) | Когда будет >1 версии промпта |
| 5 | Двухслойный лог (факты vs интерпретация) | v2.0 |
| 6 | Эмбеддинги (pgvector) | Только при доказанных промахах NFKC |
---
## Архитектурные принципы (выработанные)
1. **Сначала данные, потом UI.** source_document_id + source_quote дают основу. Красивый UI — потом.
2. **Минимальными средствами.** Одна FK-колонка вместо двухслойного лога. Self-confidence вместо сложного consensus.
3. **Домен решает.** Облачный провайдер + ЦОД — названия услуг копируются дословно. NFKC достаточно.
4. **Бесплатная LLM меняет экономику.** Можно делать N прогонов, не считая токены. Но лучше умно (только low-confidence).
---
## Открытые вопросы к Opus (будущие раунды)
1. **Prompt engineering для source_quote:** как попросить LLM вернуть точную цитату? Нужен пример промпта.
2. **Confidence calibration:** насколько self-reported confidence от gpt-oss-120b коррелирует с реальной точностью? Стоит ли калибровать?
3. **Temporal UI реализация:** как технически строить «спецификацию на дату» из spec_events? Реплей с фильтром по seq?
4. **Масштабирование LLM:** когда 120B станет узким горлышком, какие варианты? Fine-tune? Distill? Переход на большую модель?
+83
View File
@@ -0,0 +1,83 @@
# Opus 4.8 — Раунд 2: Практические ответы для реального проекта
**Дата:** 2026-06-23
---
## Вопрос 1: Одна идея с максимальным преимуществом
**Provenance с кликабельной трассировкой до исходного текста.**
Юрист подписывается под результатом. «Добавилась услуга X за 15000» без источника — бесполезно. «Добавилась услуга X ← допник №3, п.2.4, вот абзац» → проверка за 2 секунды → реальная экономия.
**Механика:** каждая op содержит `source_quote` (точная цитата из документа). Клик по строке → показать исходный абзац в документе.
---
## Вопрос 2: Self-consistency vs logprobs
**N=1 + self-reported confidence. Не logprobs.**
Причины:
- logprobs на JSON шумные — не отражают уверенность в смысле
- Модель сама возвращает `confidence: 0.0-1.0` — ноль инфраструктуры
- Self-consistency 3 прогона НЕ утраивает время: прогоны одного файла независимы → параллельно ~80с, не 240с
**Оптимальный гибрид:** N=1 по умолчанию. Для ops с `confidence < 0.7` → выборочный повторный прогон. Низко-confidence → в очередь на ручную проверку.
---
## Вопрос 3: Минимальный двухслойный лог
**Слой 1 уже есть — таблица `documents`** (original_bytes + elements_json = сырые факты).
Минимальное: добавить в `spec_events` колонку **`source_document_id`** (FK → documents).
Не нужно `event_type`, не нужно `parent_event_id`. Одна FK-колонка даёт:
- Двухслойное разделение
- Provenance-костяк
- Возможность ре-интерпретации новыми моделями
---
## Вопрос 4: Эмбеддинги vs NFKC
**NFKC покрывает ~90-95%. Эмбеддинги преждевременны.**
NFKC ловит типографику (тире, пробелы, кавычки). Эмбеддинги нужны только для семантического перефраза — но в домене облачного провайдера допники копируют названия дословно. Плюс после подачи LLM `spec_current` с готовыми ID проблема матчинга почти исчезает.
**Рекомендация:** NFKC достаточно. pgvector добавлять только при доказанных промахах.
---
## Вопрос 5: Temporal UI
**Шаговый таймлайн:** `Договор → Допник1 → Допник2 → Допник3`.
Клик на узел = спецификация на этот момент.
**Две ручки выбора (from/to):** diff-таблица между версиями.
**Визуализация:** 🟢 добавлено / 🔴 удалено / 🟡 изменено с inline old→new.
**Каждая строка кликабельна** → provenance (исходный абзац).
**Два режима:** «Итог» (последняя vs база) и «Пошагово» (дельта каждого допника).
---
## Вопрос 6: Provenance MVP — колонки
Три колонки в `spec_events`:
1. **`source_document_id`** FK → documents — самая важная. Костяк всего.
2. **`prompt_version`** TEXT — какая версия промпта.
3. **`raw_llm_response`** JSONB — полный ответ модели.
Поле `source_quote` — внутри op (в raw_llm_response), не отдельная колонка.
---
## Сквозная логика
`source_document_id` + `source_quote` от LLM закрывают provenance, двухслойность и трассировку одновременно. Самый дешёвый и конкурентный ход. Temporal UI — следующий слой поверх той же истории.
+105
View File
@@ -0,0 +1,105 @@
# Ответ Opus 4.8 на вопросы по архитектуре
**Дата:** 2026-06-23
---
## Вопрос 1: Параллельные вызовы LLM
**Ключевой вопрос — допники кумулятивны или независимы.**
- Если допник №3 может менять услугу, добавленную допником №2 — цепочка семантически последовательна, распараллелить нельзя.
- Если каждый допник трогает свой непересекающийся набор услуг — параллелить можно.
**Рекомендация:** оставить последовательным. Реальный выигрыш:
- Фаза 1 (параллельно) — textify + подготовка контекста (CPU/IO, дёшево)
- Фаза 2 (последовательно) — LLM+apply по цепочке
- Или: более быстрая модель / меньше токенов / стриминг прогресса
При параллельном apply_events seq поедет в гонку — ещё одна причина не параллелить.
---
## Вопрос 2: Атомарный seq без гонки
**Рекомендация (по предпочтению):**
1. Таблица-счётчик `contract_seq(contract_id PK, last_seq)` + `UPDATE ... SET last_seq=last_seq+1 WHERE contract_id=? RETURNING last_seq`. Атомарно, без DDL, без гонок.
2. Advisory lock: `SELECT pg_advisory_xact_lock(contract_id)` в начале транзакции, затем MAX+1. Минимальная правка.
`INSERT ... SELECT MAX+1` не атомарен. `CREATE SEQUENCE` на контракт — DDL-мусор.
---
## Вопрос 3: Стабильность name_hash
**Рекомендация (комбинация):**
- В промпт подавать spec_current с готовыми идентификаторами (hash). LLM для UPDATE/DELETE выбирает из готового списка, не сочиняет имя.
- Хэш считать только на сервере из нормализованного имени: Unicode NFKC → схлопнуть пробелы → lower → trim.
- Fuzzy (`similarity()`) только как fallback-страховка, не основной механизм.
---
## Вопрос 4: Имена в UPDATE/DELETE в UI
**Рекомендация: вариант 2 — обогащать в Python перед SSE.**
- spec_current — источник истины, имя для любого target_hash известно.
- LLM вообще не должна возвращать имена для UPDATE/DELETE — только идентификатор.
- Вариант 1 (LLM возвращает имена) — лишние токены + риск рассинхрона.
- Вариант 3 (JS) — лишние round-trip'ы.
---
## Вопрос 5: Мульти-юзеры и рост данных
1. `prompts` одна таблица + `user_id TEXT` — норм для 10-10000 юзеров. Индекс `(user_id)`. Партиционирование преждевременно.
2. `spec_events` 10K строк — мизер для PG. Партиционирование ближе к 10M+ строк.
3. `BYTEA` 30MB — спокойно в PostgreSQL (TOAST). Вынести в S3 когда объём начнёт раздувать бэкапы.
---
## Сквозная связь
В3 и В4 решаются одним сдвигом — давать LLM spec_current с готовыми ID и считать/резолвить всё на сервере. Это убирает и нестабильные хэши, и пустые ячейки в UI.
**Уточнение Opus:** допники у вас кумулятивные (могут менять то, что добавили предыдущие) или независимые?
---
## Уточнения (23.06.2026)
### 1. Advisory lock в CFML
**Да, безопасен.** `pg_advisory_xact_lock` — транзакционный: снимается автоматически при завершении транзакции (и commit, и rollback).
Условия в Lucee:
- Лок и INSERT в одном `<cftransaction>` — одно соединение, одна транзакция
- `pg_advisory_xact_lock` требует быть внутри транзакции — `<cftransaction>` гарантирует
Ключ: `pg_advisory_xact_lock(hashtext(contract_id))`. Коллизии hashtext безвредны — два контракта просто сериализуются.
### 2. NFKC-нормализация — в PostgreSQL
Делать в PostgreSQL, в том же выражении что считает хэш. Python не дублировать.
NFKC НЕ объединяет разные тире. Нужна связка в одном выражении:
```sql
lower(trim(regexp_replace(translate(normalize(name, NFKC), '–—‑−«»""', '------""'), '\s+', ' ', 'g')))
```
Всё одним выражением — для INSERT-хэша и для lookup.
### 3. Формат spec_current в промпте с готовыми ID
Давать LLM компактный список с коротким суррогатным id (`r1, r2...`):
```json
{"current_spec": [
{"id": "r1", "name": "Аренда стойко-места...", "price": 15000, "qty": 2, "sum": 30000, "date_start": "2025-01-01"}
]}
```
Инструкция в промпте:
- UPDATE: `{"op":"UPDATE","target_id":"r1","new_values":{...}}`
- DELETE: `{"op":"DELETE","target_id":"r2"}`
- ADD: `{"op":"ADD","new_row":{...}}` (без id)
LLM не видит хэши — только выбирает из готового набора. Сервер маппит `id → name_hash`. Устойчивее, меньше токенов, нет шанса опечатки в md5.
@@ -0,0 +1,32 @@
# Opus 4.8 Analysis — 2026-06-24 — Auto-classification
## Главный вывод
Текущая модель — «один договор на сессию загрузки». upload.py: первый файл → contracts + supplement initial, остальные → additional к тому же. Связь документ→договор ТОЛЬКО через supplements. Авто-классификация — обратная задача: N файлов → M договоров. Это архитектурное изменение, не промпт.
## Q1: Поля в documents или staging?
**Поля в documents.** + batch_id (привязка к сессии загрузки) + parent_number (отдельно от own_number). 7 колонок:
doc_type, own_number, parent_number, doc_date (TEXT, не DATE), counterparty, classify_status, batch_id.
## Q2: classify в upload или отдельно?
**Отдельно.** Upload быстро (0.5с), классификация async. ThreadPoolExecutor(4-8) внутри /classify-batch. Статусная модель: classify_status='pending' → 'classified'/'failed'.
## Q3: header или весь документ?
**Умная выжимка.** header ~1500 симв + regex-хиты по маркерам (договор, №, от, соглашение) из всего документа + даты. Итого ~3000 симв на вход LLM.
## Q4: parent_contract_number — LLM или regex?
**Двухпроходный гибрид.** Проход 1: LLM извлекает строки per-doc. Проход 2: Python нормализует (regexp uppercase+буквы/цифры) и матчит supplements→contracts по parent_number. LLM не делает fuzzy-match.
## Q5: загрузка 2000 файлов
ZIP через /unzip-upload (переделать: store+parse серверно). Прогресс — polling /api/documents?batch=X, не SSE.
## Q6: группировка — фронт или бэк?
**Бэкенд.** Нормализация требует Python. GET /api/groups?batch=X возвращает готовые группы. apply-groups создаёт contracts+supplements.
## Q7: MVP
6 шагов с новыми файлами, ZIP не нужен. Multi-upload уже работает.
1. Миграция БД (7 колонок)
2. Слой данных (db/documents.py + seed classify prompt)
3. services/classify.py (выжимка + LLM + ThreadPoolExecutor)
4. services/grouping.py (normalize + group + apply)
5. Эндпоинты (/classify-batch, /api/groups, /apply-groups)
6. UI (загрузка → classify → polling → карточки групп → per-group Сравнить)
@@ -0,0 +1,18 @@
# Opus Analysis — 2026-06-24 — Почему 5/6 и нет договора
## Три бага
### 1. Договор failed классификацию (корневая причина)
Самый большой документ (3000 символов выжимки) + max_tokens=500 → LLM обрезает JSON → _safe_json_parse падает → classify_status='failed'.
Именно он — тот 1 из 6, который не прошёл.
### 2. Мёртвый код в grouping.py (failed не видны)
```python
unmatched += [d for d in classified if d.get("classify_status") != "classified"]
```
Итерация по `classified` (уже отфильтрованному), условие всегда ложно.
Должно быть: `...for d in docs...`
### 3. normalize_number ломает сопоставление
`"03700_1"` → `"037001"`, `"03700"` → `"03700"`. Не совпадают.
Допники никогда не матчатся к базовому договору из-за суффикса _N.
@@ -0,0 +1,103 @@
# Запрос для Opus — План: группировка, прогресс, промежуточные результаты
## Что сказал заказчик (дословно)
> Не распознано (8 файлов)
> [supplement] допник-1-XXX002-01200_3.docx ... — нет базового договора №01219_3
> ...
> зачем нам базовый договор? Вся информация есть в допниках и спеках
> Было бы хорошо пояснить или показать процесс, что в каком порядке происходит. Так видно только текущую операцию
> наверно я захочу иметь возможность посмотреть любые промежуточные результаты
## Что нужно
Заказчик хочет три улучшения (без фанатизма, главное — устойчивость и понятный UI):
### #1 — Группировка без базовых договоров
**Проблема:** сейчас `group_documents()` требует contract-файл как якорь группы. Без него допники/спеки попадают в unresolved с текстом «нет базового договора №X». Заказчик: «зачем нам базовый договор? Вся информация есть в допниках и спеках».
**Нужно:** группировать документы по `parent_number` / `own_number`, даже если contract-файл отсутствует в загрузке. Создавать «виртуальную» группу без contract-файла.
**Вопросы:**
1. Алгоритм: что приоритетнее — `parent_number` от допника или `own_number` от спеки? Если оба ссылаются на один нормализованный номер — это одна группа?
2. Если два допника с одним `parent_number`, но разными `counterparty` — одна группа или разные?
3. Как назвать группу без contract-файла: `"№01300_2 — ЗАО XXX003"` из данных классификации? Достаточно?
4. Минимальный diff в `group_documents()` — чтобы не сломать текущую логику с contract-файлами?
### #2 — Прогресс пайплайна
**Проблема:** юзер видит только статус текущей операции. Непонятно что уже сделано, что предстоит.
**Нужно:** визуальная шкала этапов с иконками статуса.
**Вопросы:**
1. Достаточно 4 этапов: Загрузка → Классификация → Группировка → Сравнение? (Парсинг — подэтап загрузки, не показывать отдельно)
2. Где разместить: в топбаре (всегда видно, не скроллится) или в карточке с результатами?
3. При переклассификации после удаления/добавления файла — сбрасывать всю шкалу или только затрагиваемые этапы?
### #3 — Промежуточные результаты
**Проблема:** юзер хочет видеть что LLM вернула на каждом шаге: сырой ответ, как определился номер/тип/дата.
**Нужно:** раскрывающийся блок с деталями для каждого файла.
**Вопросы:**
1. Что хранить: сырой ответ LLM + распарсенный JSON? Достаточно двух новых полей в `documents`?
2. Где показывать: раскрывающийся блок под строкой файла в таблице? Или модалка при клике на статус?
3. Нужно ли для сравнения (process-v2 SSE) или только для классификации?
### #4 — Общие ограничения
Что из трёх самое трудозатратное и что можно упростить без потери юзабилити?
---
## Релевантные файлы (читать)
### Группировка (#1)
- `contractor/deploy/services/grouping.py` — `group_documents()`, `normalize_number()`, `apply_groups()`
- `contractor/deploy/db/documents.py` — поля `doc_type`, `own_number`, `parent_number`, `counterparty`, `classify_status`
- `contractor/deploy/db/contracts.py` — `insert()`, поля `number`, `client`
- `contractor/deploy/db/supplements.py` — `insert()`, `list_by_contract()`
### Прогресс-бар (#2) + Промежуточные результаты (#3)
- `contractor/index.cfm` — HTML-оболочка, топбар (`.topbar`, `position: sticky`), карточки, модалки
- `contractor/deploy/app.js` — весь фронтенд: загрузка, `runClassify()`, `loadGroups()`, `runCompareForGroup()`, `showClassifyBtn()`, рендеринг таблицы и групп
- `contractor/deploy/app_utils.js` — утилиты: `removeFile()`, `renderTable()`
- `contractor/deploy/convert_server.py` — роутер (`do_GET`, `do_POST`, `do_DELETE`), SSE (`_handle_process_v2`), эндпоинты `/api/classify-batch`, `/api/groups`, `/api/batch-progress`, `/api/sync`
### Общий контекст
- `contractor/deploy/services/classify.py` — `classify_batch()`, `_smart_extract()`, `_call_llm_classify()`, `_safe_json_parse()`
- `contractor/deploy/services/process.py` — `run_pipeline()` (SSE для сравнения: extract/diff)
- `contractor/deploy/services/grouping.py` — `group_documents()`, `apply_groups()`
- `contractor/deploy/llm_prompt.py` — `build_prompt()`, `build_classify_prompt()`
- `contractor/deploy/db/connection.py` — `query()`, `execute()`, `execute_returning()`
---
## Игнорировать (не относится к делу)
- `contracts-app/` — старый Python-бэкенд (Flask), не используется
- `contracts-vm/` — старые конфиги ВМ
- `DOC/`, `FILES/` — документация, заметки
- `dogovora/` — тестовые файлы договоров
- `history/`, `History/` — старые сессионные заметки (кроме этого файла)
- `contractor/*.cfm` кроме `index.cfm` — старый Lucee-код, не используется
- `contractor/deploy/nginx-contracts.conf` — конфиг nginx
- `contractor/deploy/convert_doc.py` — конвертер .doc → .docx
- `contractor/deploy/sync.sh` — скрипт деплоя
- `contractor/upload.cfm`, `contractor/process.cfm`, `contractor/parser.cfm` и т.д. — старый Lucee-код
---
## Архитектура (кратко)
- **Фронт:** `index.cfm` (Lucee, только HTML-оболочка) → грузит `app.js` + `app_utils.js` с ВМ (`https://contracts.kube5s.ru/static/`)
- **Бэкенд:** Python 3.12 `http.server` + `ThreadingMixIn` на ВМ (5.172.178.213), порт 8766, systemd-сервис `contracts`
- **Все endpoint'ы:** в `convert_server.py` (один файл-роутер, ~400 строк)
- **БД:** PostgreSQL 16, прямой доступ через `psycopg2`, connection pool
- **Таблицы:** `documents`, `supplements`, `contracts`, `spec_events`, `spec_current`, `prompts`
- **LLM:** gpt-oss-120b через `api.aillm.ru`, httpx с http2, `temperature=0.1`, `max_tokens=8000`
+33
View File
@@ -0,0 +1,33 @@
# Ответ Opus на бриф — моё мнение
## Что Opus сделал хорошо
1. **Сверил бриф с кодом** — подтвердил что группировка матчит по `normalize_number`, что прогресс-данные уже есть в `/api/batch-progress`, что сырой LLM-ответ не хранится. Без этого был риск писать план «в воздух».
2. **Нашёл дубликат `_handle_cleanup`** — я знал про это но не зафиксировал. Opus заметил сам. Побочная находка, полезно.
3. **Риск baseline для виртуальных групп** — ключевое. Если первый документ виртуальной группы — допник, а не базовый договор, то `run_pipeline()` может построить baseline из допника, а не из полной спецификации. Opus прав: это надо проверить перед релизом #1.
4. **Приоритеты** — правильно оценил: #3 тяжелее всего (миграция БД + 3 слоя), #2 легче всего (только UI). Совпадает с моей оценкой.
## Где Opus ошибся или недоработал
1. **Схема БД** — написал «схема `documents` создаётся на ВМ, в репозитории её нет». Это правда, но миграция через ALTER TABLE на лету — хрупко. Лучше через `seed_defaults()` или отдельный `ensure_schema()`, как уже сделано для prompts. Opus не предложил механизм.
2. **Два допника с одним parent_number, разными counterparty** — Opus рекомендует разделять. Я бы наоборот: группировать по номеру, игнорировать counterparty. Потому что один договор может иметь одного контрагента в базовом договоре, а в допнике он может быть написан иначе (сокращение, другая оргформа). Риск ложного разделения выше чем риск ложного объединения.
3. **Прогресс-бар в топбаре** — идея ок, но топбар уже содержит лого + заголовок + «О сервисе». 4 этапа + текст займут место. Возможно лучше сделать отдельную строку под топбаром или внутри карточки результатов. Opus не учёл текущую вёрстку.
4. **classify_raw vs parsed** — Opus предлагает хранить только `classify_raw`. Я бы хранил и то и другое: `classify_raw` (текст) + `classify_json` (jsonb). Потому что сырой ответ может быть невалидным JSON, а для отладки нужны оба. Но это увеличивает трудозатраты — ок, можно только raw для начала.
## Что я бы сделал иначе
1. **Порядок реализации:** #1 → #3 → #2. Потому что #1 (группировка) — самое востребованное заказчиком прямо сейчас. #3 (промежуточные результаты) даст данные для отладки #1 если что-то пойдёт не так. #2 (прогресс-бар) — вишенка, можно последней.
2. **Для #1:** вместо «виртуальной группы» — просто создавать реальный contract с флагом `is_virtual=true` или `number` из `parent_number`. Тогда `apply_groups()` не нужно менять вообще — contract уже существует, supplement просто привязывается. Меньше спецкейсов.
3. **Для #3:** вместо ALTER TABLE на лету — добавить колонку в `ensure_schema()` который вызывается при старте. Идемпотентно: `ADD COLUMN IF NOT EXISTS`. Уже есть прецедент с `_ensure_classify_prompt()`.
## Вердикт
План Opus — добротный, можно брать за основу. Три поправки выше (порядок, виртуальный contract через флаг, механизм миграции) — и можно делать.
+61
View File
@@ -0,0 +1,61 @@
# Opus Prompt Improvement — Few-Shot + Domain Glossary
Дата: 2026-06-23 | Источник: Opus (раунд 3, старый чат)
Связано: llm_prompt.py, prompt-strategy.md, provenance-columns.md
---
## Что попросили у Opus
Улучшить промпты для extract (первый документ) и diff (сравнение ДС). Ключевые требования:
- Few-shot примеры на домене ЦОД/colocation
- Доменный глоссарий (кВт, юнит, стойко-место, IP, каналы)
- Edge-cases инструкция
- JSON-формат не менять
- Новые actions не добавлять
## Что Opus выдал
### EXTRACT
- Добавлен доменный глоссарий
- 1 few-shot пример (стойко-место + IP-адрес)
- Усилены правила чисел и null
### DIFF
- Добавлен доменный глоссарий
- 4 few-shot примера:
1. `partial`, UPDATE цены
2. `partial`, ADD + UPDATE qty + UPDATE мощности (внутри name)
3. `full_replace` (новая редакция приложения)
4. `UNRESOLVED` (не с чем сопоставить)
- Edge-cases: сопоставление по смыслу (не по символам), qty++ = UPDATE, мощность внутри name, приоритет full_replace, null при отсутствии данных
## Какие проблемы решает
| Проблема | Решение |
|----------|---------|
| Модель путает единицы (кВт vs шт.) | Глоссарий |
| Не понимает что «Аренда стойко-места» = «Colocation» | Сопоставление по смыслу |
| При увеличении qty создаёт ADD вместо UPDATE | Пример 2 |
| full_replace: пишет UPDATE вместо ADD | Пример 3 |
| Не создаёт UNRESOLVED для незнакомых услуг | Пример 4 |
| «55 000,00 руб.» → строка вместо числа | Правило чисел |
## Размещение
Два варианта:
1. Заменить `FALLBACK_EXTRACT` / `FALLBACK_DIFF` в `llm_prompt.py`
2. Обновить активный промпт в БД через UI (вкладка «⚙ Промпты»)
**Рекомендация:** оба. Fallback в Python — защита если БД недоступна. БД — основной источник. Сделать одновременно.
## ⚠️ Важно: фигурные скобки в Python
В Python-строке (тройные кавычки) `{` и `}` НЕ требуют удвоения — они не являются f-string placeholder'ами. Удвоение (`{{`, `}}`) нужно ТОЛЬКО если используется `.format()`. В текущем коде промпты — обычные строки в тройных кавычках, подстановка через `.replace()`. Поэтому фигурные скобки оставляем одинарными.
## Статус
- [x] Opus выдал готовые промпты
- [ ] Заменить в `llm_prompt.py`
- [ ] Обновить в БД (сохранить как новую версию)
- [ ] Bump + пуш + синк VM
+47
View File
@@ -0,0 +1,47 @@
# Вопросы Opus 4.8 — раунд 2: неизведанное для реального проекта
**Дата:** 2026-06-23
**Контекст:** свой облачный LLM (gpt-oss-120b), бесплатный, без structural output. Проект для сверки договоров облачного провайдера и ЦОД (colocation, стойко-места, каналы связи, IP-адреса, облачные ресурсы). Не универсальный юрист — узкая доменная специфика. Документы: базовые договоры + допсоглашения + спецификации услуг.
---
## Вопрос 1: Самое конкурентное из неизведанного
Ты дал 9 идей. У нас ограниченный ресурс (1 разработчик). Какую ОДНУ идею внедрить первой, чтобы получить максимальное конкурентное преимущество именно для сверки договоров? Не для галочки «у нас ES» — а чтобы юрист сказал «вау, без этого теперь не могу».
---
## Вопрос 2: Бесплатная LLM + confidence-gating
gpt-oss-120b — бесплатный, без ограничений по токенам. Self-consistency (N=3 прогона → консенсус) утроит время (80с → 240с), но токены бесплатны. Стоит ли? Или лучше N=1 + показывать confidence самой модели (logprobs)? Как проще всего вытащить confidence из gpt-oss-120b?
---
## Вопрос 3: Двухслойный лог без фанатизма
Твоя идея двухслойного лога правильна архитектурно. Но у нас Lucee CFML + PostgreSQL — не микросервисы с Kafka. Как сделать **минимальную** версию двухслойного лога, не переписывая всё? Может, просто добавить `event_type` в spec_events (`ingestion` vs `interpretation`) и `parent_event_id`?
---
## Вопрос 4: Эмбеддинги vs NFKC — что реально?
Мы уже сделали NFKC-нормализацию (normalize + translate тире + regexp). Эмбеддинги требуют pgvector + модель. Для 100-1000 услуг в спецификации — эмбеддинги реально дадут прирост точности матчинга строк по сравнению с NFKC? Или NFKC уже покрывает 95% случаев?
---
## Вопрос 5: Temporal UI — дизайн для юриста
«Спецификация на дату» — killer-фича. Как должен выглядеть UI? Ползунок? Календарь с выбором допника? И главное — как визуализировать НЕ только конечную спецификацию, но и дифф между двумя датами («что изменилось между допником 2 и 4»)?
---
## Вопрос 6: Provenance — минимально жизнеспособно
Что добавить в spec_events прямо сейчас (2-3 колонки) чтобы включить provenance, не раздувая таблицу? `llm_model`, `prompt_version`, `raw_llm_response` (JSONB)? Или что-то ещё?
---
## Важно
- НЕ делай код. Только анализ и рекомендации.
- Помни: стек Lucee CFML + PostgreSQL + Python ВМ. Не Kubernetes, не Kafka, не микросервисы.
- LLM свой, облачный, бесплатный, 120B, без structural output.
+122
View File
@@ -0,0 +1,122 @@
# Вопросы к Opus 4.8 по проекту Сверка договоров
**Дата:** 2026-06-23
**Версия:** v1.0.107
---
## Что за проект
Сервис для сверки договоров облачного провайдера. Юзер загружает договоры/допники (docx/pdf), парсер извлекает текст, LLM сравнивает с накопленной спецификацией по цепочке (Event Sourcing). На выходе — какие услуги добавились/изменились/удалились.
**Стек:** Lucee CFML 6.0 (сервер приложений) + PostgreSQL 15 + Python 3 на отдельной ВМ (nginx + convert_server.py). LLM: gpt-oss-120b через api.aillm.ru (бесплатный, без структурного вывода). Фронт: CFML-страницы + JavaScript (EventSource для SSE).
**Масштаб:** сейчас 1 тестовый пользователь. В планах Keycloak, мульти-юзеры.
**Производительность:** 5 файлов (3-35 KB docx) × последовательные вызовы LLM = 80-100 секунд. Каждый вызов LLM: 6-50 секунд.
---
## Вопрос 1: Параллельные вызовы LLM
**Текущая схема:** цикл по supplements последовательно. Для каждого: взять текущую spec_current → textify документа → вызвать LLM → получить ops → apply_events → следующий.
```python
for s in supps:
cur = get_current_spec() # из БД
text = textify(elements_json) # текст документа
ops = call_llm(cur, text) # HTTP к LLM, 6-50с
apply_events(ops) # HTTP к Lucee
# следующий видит обновлённую spec_current
```
**Проблема:** 5 файлов × (12+20+38+7+6)с = 80-100с. LLM-вызовы занимают 95% времени.
**Вопрос:** Можно ли распараллелить вызовы LLM, если известно что первый файл = исходная спецификация (база), а остальные — допники к ней? То есть: файл1 → LLM → накопили. Затем файлы 2-5 запустить параллельно, каждый сравнивается с результатом файла1? Или есть архитектурный паттерн лучше? Что будет с seq в spec_events при параллельных apply_events?
---
## Вопрос 2: Event Sourcing seq без гонки
**Текущая схема:** в apply_events.cfm (Lucee):
```cfm
<cfquery>SELECT COALESCE(MAX(seq),0) FROM spec_events WHERE contract_id=? FOR UPDATE</cfquery>
<cfset seq = result + 1>
<!-- цикл по ops, каждый seq++ -->
```
FOR UPDATE не работает с агрегатной функцией MAX() в PostgreSQL 15 — падает с ошибкой. Убрали.
**Структура:** `spec_events(contract_id, seq)`, UNIQUE(contract_id, seq). Seq монотонный в рамках контракта.
**Вопрос:** Как правильно сделать атомарный инкремент seq без гонки? Варианты:
1. `CREATE SEQUENCE spec_events_seq_{contract_id}` — но это динамический DDL на каждый контракт
2. `LOCK TABLE spec_events IN EXCLUSIVE MODE` — грубо
3. `INSERT ... SELECT COALESCE(MAX(seq),0)+1, ...` — атомарно ли?
4. Отдельная таблица-счётчик с `UPDATE ... RETURNING`
Что рекомендовано для PostgreSQL + CFML?
---
## Вопрос 3: name_hash — стабильность идентификации строк
**Текущая схема:** строка спецификации идентифицируется хэшем:
```sql
md5(lower(trim(name)) || coalesce(date_start, ''))
```
Вычисляется в INSERT (БД) из данных, которые LLM вернула в new_row.
Для UPDATE/DELETE LLM возвращает target_hash — указывает какую строку менять/удалить.
**Проблема:** LLM может написать название услуги чуть иначе в разных файлах:
- Файл1: "Аренда стойко-места, в составе: Номинальная мощность – 10 кВт"
- Файл2: "Аренда стойко-места, в составе: Номинальная мощность - 10 кВт" (другое тире)
- Хэши разные → LLM видит ADD/DELETE вместо UPDATE
**Вопрос:** Как стабилизировать? Варианты:
1. Нормализовать name перед хэшированием (убрать пунктуацию, лишние пробелы)
2. LLM возвращает не target_hash, а явно target_name + date_start, а хэш вычисляется сервером
3. Fuzzy matching: если target_hash не найден, искать близкое имя через `levenshtein()` или `similarity()`
4. Хранить исходный name из первого появления и использовать его как ключ
Что правильнее архитектурно?
---
## Вопрос 4: UPDATE/DELETE без имён в UI
**Проблема:** в результатах сравнения ADD-строки показывают все поля (услуга, цена, кол-во, сумма, дата). UPDATE и DELETE — пустые ячейки.
**Причина:** LLM для UPDATE/DELETE возвращает только target_hash и new_values (для UPDATE). Имени услуги нет — оно было в исходной спецификации. UI строит таблицу из d.ops, где для ADD есть new_row.name, а для UPDATE/DELETE — нет.
**Вопрос:** Где правильнее решать:
1. В промпте LLM — требовать всегда возвращать name в new_values для UPDATE и в отдельном поле для DELETE
2. В Python (convert_server.py) — перед отправкой ops в SSE, дополнить их именами из spec_current (доп. запрос к БД)
3. В UI (JavaScript) — перед рендерингом запрашивать spec_current и резолвить имена
Что даст меньше запросов и меньше шансов рассинхронизации?
---
## Вопрос 5: Мульти-юзеры и рост данных
**Планы:** Keycloak, каждый юзер имеет свои контракты и промпты.
**Таблицы:**
- `documents`, `supplements`, `contracts` — растут с каждым файлом
- `spec_events` — растёт с каждым сравнением (каждый ADD/UPDATE/DELETE — строка)
- `spec_current` — текущая спецификация, O(услуг в контракте)
- `prompts` — одна таблица на всех (в плане: `user_id + timestamp` как ID)
**Вопросы:**
1. `prompts` — одна таблица с `user_id TEXT` норм для масштаба 10-100 юзеров? Или партиционировать?
2. `spec_events` — для 100 контрактов по 20 услуг с 5 версиями каждый = 10K строк. Нужно ли партиционирование сейчас?
3. `documents` хранит бинарные байты (`original_bytes BYTEA`). Для 1000 документов по 30KB = 30MB — норм в PostgreSQL или вынести в S3-совместимое хранилище?
---
## Важно
- **НЕ делай код. Только анализ и рекомендации.**
- Ответь кратко по каждому вопросу: рекомендуемое решение и почему.
- Если нужно уточнение — спроси, но проект небольшой, контекста выше достаточно.
+75
View File
@@ -0,0 +1,75 @@
# Opus 4.8 — Видение: LLM-driven Event Sourcing нового поколения
**Дата:** 2026-06-23
---
## Оценка текущей архитектуры
**Частично соответствует лучшим практикам ES. Нарушает две аксиомы:**
### 1. Событие должно быть фактом, а не интерпретацией
Классика: событие — «что произошло» (PriceChanged), объективный факт.
У нас: ADD/UPDATE/DELETE — интерпретация документа моделью, не факт.
Настоящий факт: «загружен допник №3 с таким текстом».
### 2. События должны быть детерминированы
Классика: реплей даёт то же состояние.
У нас: та же модель + тот же документ = могут быть другие ops (стохастичность LLM).
**Вывод:** прагматичный гибрид. Не «неправильно», но неклассически.
---
## Двухслойный лог (ключевая идея)
- **Слой 1 — сырые факты:** `DocumentIngested(text_hash, bytes, order)`. Детерминированные, реплеятся идеально.
- **Слой 2 — интерпретация:** `SpecChangeProposed` с метаданными (model_id, prompt_version, input_hash, raw_response, temperature).
- `spec_current` — проекция слоя 2.
Даёт: перепрогнать слой 1 новой моделью без потери истории.
---
## Неизведанные идеи
### 1. Версионная ре-интерпретация
Апгрейд модели → перепрогон всего потока → параллельная spec_current_v2 → сравнение дрейфа на истории. Версионирование *понимания* данных.
### 2. Provenance каждой строки
«Эта услуга пришла из допника №3, вот исходный абзац, reasoning LLM, confidence 0.82». UI «почему здесь эта цифра».
### 3. Human-correction как событие → flywheel
Правка юзера = `SpecChangeCorrected(by=user)` = regression-тест + размеченный пример. Бесплатный датасет качества.
### 4. «Prompt CI» — промпты как код с тестами
Из human-corrections → golden dataset. Изменение промпта → eval-харнесс. Регрессионные тесты для промптов.
### 5. Confidence-gating + self-consistency
N прогонов (или 2-3 модели) → консенсус. Согласные ops → авто. Расхождения → ручная проверка. Лечит нестохастичность.
### 6. Эмбеддинги вместо name_hash
Векторный матчинг сущностей внутри event-проекции. «Аренда стойко-места…» с разными тире → один вектор.
### 7. Temporal queries — «спецификация на дату»
UI-ползунок по времени: «как выглядел договор на 2025-03-01». Технически возможно уже сейчас.
### 8. Outbox/Saga + idempotency keys
Exactly-once через HTTP-границы Lucee↔Python↔LLM. Для надёжности при мульти-юзерах.
### 9. Constrained decoding (GBNF-грамматика)
Форсировать валидный JSON ops на уровне декодера LLM. Убирает ошибки парсинга.
---
## Приоритеты (от Opus)
| Приоритет | Идея | Зачем |
|---|---|---|
| 🔥 Высокий | Двухслойный лог | Чинит детерминизм, открывает всё |
| 🔥 Высокий | Provenance-метаданные | Аудит, доверие, миграция моделей |
| 🔥 Высокий | Human-correction как событие | Flywheel: бесплатный eval-датасет |
| ⭐ Средний | Эмбеддинги для матчинга | Решает name_hash надёжнее |
| ⭐ Средний | Temporal UI | Killer-фича почти даром |
| ⭐ Средний | Confidence-gating | Убирает тихие ошибки |
| 🧪 Исслед. | Версионная ре-интерпретация | Сравнение моделей на истории |
@@ -0,0 +1,36 @@
# Sonnet Analysis — 2026-06-23 — app.js bugs
## Q1: Что сломано в app.js
Нужно убрать **ДВЕ** строки из app.js (не только `</script>`):
- строка 644: `</script>` — SyntaxError
- строка 845 старого index.cfm = `</body>` — вторая SyntaxError после первой
Обе попали из-за `sed -n '209,846p'`.
## Q2: lucide.createIcons()
**НЕ потерян.** В старом index.cfm был на строке 845 (перед `</script>`), попал в app.js. Также вызывается ещё в 4 местах внутри JS.
## Q3: Дубликаты var
`var UNZIP_URL` дублируется (стр. 6 и 7 app.js). Значения идентичны. `var` в JS допускает повторное объявление — не проблема.
`UPLOAD_URL` и `CONVERT_URL` НЕ дублируются — они на строках 207-208 старого cfm, sed начат с 209.
## Q4: CORS
`<script src>` **не требует CORS** — браузер грузит скрипты без проверки Origin.
fetch/XHR из app.js → contracts.kube5s.ru требуют CORS — Python отвечает `Access-Control-Allow-Origin: *` (OK).
## Q5: Скрытый баг — parseInfo в модале
Строки 484-499 app.js — правая панель "Распарсено" использует UPPERCASE ключи:
```javascript
d.ELEMENT_COUNT, d.PARAGRAPHS, d.TABLES, d.TABLE_ROWS,
d.PAGES, d.PARSE_TIME_MS, d.TEXT_LENGTH, d.ERRORS
```
Python возвращает lowercase: `element_count`, `elements` (без PARAGRAPHS/TABLES).
**Все числа в правой панели модала — undefined → 0.** Нужно исправить на lowercase.
## Q6: prompt.cfm и chat.cfm
Ходят на Lucee-домен (relative URL). Ответы ожидаются в uppercase (`d.OK`, `d.BODY` и т.д.) — корректно для CFML. Работает, если prompt.cfm/chat.cfm есть на Lucee.
## Приоритеты исправлений
1. Убрать `</script>` + `</body>` из app.js → JS выполнится
2. `parseInfo` поля: UPPERCASE → lowercase для корректной работы модала
3. prompt.cfm/chat.cfm — пока OK, потом перенести на VM
@@ -0,0 +1,210 @@
# Ответ: архитектура сервиса "Сверка договоров"
_Дата: 2026-06-13_
---
## 1. Правильно ли разбиты слои?
Разбивка в целом правильная. Принцип «один файл — одна ответственность» выдержан.
Критических проблем нет, но есть два момента, которые стоит учесть.
### Что хорошо
- `parser.py` — чисто I/O-слой: байты → JSON. Никакой логики.
- `textify.py` — чисто форматирование: JSON → текст. Никакой логики.
- `db.py` — чисто транспорт к БД. Не знает о бизнес-сущностях.
- `test_routes.py` — отдельный Blueprint, не засоряет app.py.
### Что стоит скорректировать
**`db.py` — разделить на транспорт и схему.**
Сейчас там `ensure_db()` — это уже «знание» о схеме. Когда появятся таблицы
(`contracts`, `supplements`, `spec_rows`, `spec_history`), их создание (DDL)
стоит вынести в отдельный `schema.py`. `db.py` остаётся просто `connect()` и `query()`.
**Слой LLM стоит разделить надвое:**
- `llm_client.py` — HTTP-клиент к aillm.ru: отправить промпт → получить строку ответа.
Не знает ни о договорах, ни о спецификациях.
- `extractor.py` — бизнес-логика: взять текст договора, сформировать промпт,
вызвать llm_client, распарсить ответ в строки спецификации.
Это важно: если поменяется LLM — меняем только `llm_client.py`.
Если поменяется формат ответа — только `extractor.py`.
**Итоговый состав слоёв:**
```
parser.py bytes → elements JSON (уже есть, не трогать)
textify.py elements → текст для LLM (уже есть, не трогать)
db.py connect() + query() (уже есть, убрать ensure_db)
schema.py DDL: CREATE TABLE IF NOT EXISTS
upload.py сохранить файл в БД (documents)
llm_client.py HTTP к aillm.ru → строка ответа
extractor.py текст → структурированные строки (промпт + парсинг ответа)
differ.py сравнение строк между допниками → список изменений
api.py Blueprint: /contracts, /supplements, /history
app.py сборка слоёв, Flask-приложение
test_routes.py Blueprint /test (уже есть)
```
---
## 2. В каком порядке создавать
Каждый этап самодостаточен и проверяем до перехода к следующему.
### Этап 1 — основа хранения
`schema.py` → DDL всех таблиц.
Запустить `python schema.py` — таблицы созданы. Проверить через `/test sql`.
### Этап 2 — загрузка файлов
`upload.py` → принять файл, вызвать `parser.parse()`, вызвать `textify.to_text()`,
сохранить в `documents(original_bytes, parsed_text, mime, filename)`.
Добавить POST `/upload` в `app.py`. Проверить curl-ом.
### Этап 3 — LLM клиент
`llm_client.py` → POST к aillm.ru, вернуть строку.
Проверить отдельно: `python llm_client.py` с тестовым промптом.
### Этап 4 — извлечение строк спецификации
`extractor.py` → взять `parsed_text`, сформировать промпт, вызвать `llm_client`,
распарсить ответ в список `spec_row`.
Проверить на одном docx через тест-скрипт.
### Этап 5 — сравнение (diff)
`differ.py` → взять два списка `spec_row`, вернуть изменения.
Это чистая функция: `diff(rows_old, rows_new) → changes`.
Проверить unit-тестом без БД.
### Этап 6 — API
`api.py` → Blueprint с GET/POST для договоров, допников, истории.
Подключить в `app.py`.
---
## 3. Поток данных между слоями
Правило: слои передают данные через простые Python-структуры (dict, list).
Никаких прямых вызовов «через слой» — только соседние слои.
```
Файл (bytes)
│
▼
parser.parse(bytes, mime) → {"elements": [...]}
│
▼
textify.to_text(elements) → str
│
▼
upload.py: сохранить в DB, получить document_id
│
▼
extractor.extract(parsed_text) → [{"pos": 1, "name": "...", "qty": 10, ...}]
│ (внутри вызывает llm_client.ask(prompt) → str)
│
▼
schema: сохранить строки в spec_rows(document_id, pos, ...)
│
▼
differ.diff(rows_v1, rows_v2) → [{"pos": 3, "field": "qty", "old": 5, "new": 10}]
│
▼
schema: сохранить в spec_history
```
Между слоями **нет импортов друг друга**, кроме:
- `upload.py` импортирует `parser` и `textify` (это нормально — upload оркеструет парсинг)
- `extractor.py` импортирует `llm_client` (клиент — зависимость экстрактора)
- `app.py` и `api.py` импортируют всё — они и есть точки сборки
`db.py` никто не импортирует напрямую, кроме `upload.py`, `extractor.py` и `api.py`.
`schema.py` вызывается только один раз при старте из `app.py`.
---
## 4. Как должен выглядеть app.py
`app.py` — точка входа и сборки. Бизнес-логики ноль.
```python
from flask import Flask
import db, schema
from test_routes import test_bp
from api import api_bp
def create_app():
app = Flask(__name__)
# 1. Инициализация схемы при старте
schema.ensure_schema()
# 2. Регистрация Blueprint-ов
app.register_blueprint(test_bp)
app.register_blueprint(api_bp)
# 3. Системные маршруты
@app.route("/health")
def health():
return "OK", 200
return app
if __name__ == "__main__":
create_app().run(host="0.0.0.0", port=5000)
```
Правило: если в `app.py` появляется `if`, `for` или бизнес-слово — это уже лишнее.
---
## 5. Потенциальные проблемы
### LLM не гарантирует структуру ответа
Самая острая проблема. Модель может вернуть текст в произвольном формате,
сломать JSON, пропустить поля, придумать данные.
**Решение:**
- В `extractor.py` — строгая схема промпта с примером ответа.
- Парсинг ответа через `try/except` с явным возвратом `{"error": "parse_failed", "raw": ответ}`.
- Никогда не падать — помечать строки как `unresolved`.
### Идентификация изменённой строки в допнике
Самая неоднозначная задача: иногда в допнике новое полное состояние,
иногда — дельта. Определить это автоматически сложно.
**Решение для `differ.py`:**
- Сначала попробовать детерминированный diff по позиции/артикулу.
- Если совпадение < порога — пометить как `ambiguous`, не фантазировать.
- Заказчик потом разбирает вручную `ambiguous`-записи.
### Сопоставление артикула с каталогом
Задача нетривиальная, код-имён в спецификациях нет, матч только по описанию.
**Решение:** вынести в отдельный `matcher.py`, реализовать как отдельный шаг после основного пайплайна. Пометить как `optional`, не блокировать основной поток.
### Размер документов vs контекст LLM
Большая спецификация (100+ строк) может не влезть в контекст.
**Решение в `extractor.py`:** разбивать таблицы на чанки, обрабатывать частями,
собирать результат. Это нужно заложить сразу — переделывать потом дороже.
### Транзакционность при загрузке
Файл загружен → парсинг ок → LLM вызов → ошибка → документ в БД наполовину.
**Решение:** хранить в `documents` поле `status` (`uploaded` / `parsed` / `extracted` / `error`).
Обновлять после каждого шага. Зависший `uploaded` — сигнал для повтора.
---
## Итог
| Вопрос | Ответ |
|--------|-------|
| Разбивка слоёв | Правильная. Добавить `schema.py`, разделить LLM на `llm_client` + `extractor` |
| Порядок | schema → upload → llm_client → extractor → differ → api |
| Поток данных | Через dict/list, нет перекрёстных импортов |
| app.py | Только сборка: `ensure_schema()` + `register_blueprint()` |
| Риски | LLM-нестабильность, diff-амбивалентность, чанкинг, транзакционность |
Всё, что не решается надёжно детерминированно — помечать `unresolved`, не фантазировать.
@@ -0,0 +1,81 @@
# Запрос к Claude Sonnet: архитектура сервиса "Сверка договоров"
## Контекст
Мы строим Flask-сервис для автоматизированной обработки договоров и допников.
Исходная постановка задачи (от заказчика):
1. Спецификации договоров разобрать до структурированного вида
2. Собрать из **цепочки допников кумулятивный статус договора** — состояние во времени.
Важно: иногда в допнике **новое состояние** договора целиком, иногда — **только изменения**,
и нужно идентифицировать изменённую строку.
3. (опционально, малый процент случаев) сопоставить артикул по описанию с каталогом услуг
(кодов в спецификациях нет)
Критичные ограничения:
- Данные строго конфиденциальны → LLM только своя (aillm.ru 120B), данные не покидают облако
- На каждом этапе возможны исключения — **не фантазировать**, а отметить конкретную
элементарную подзадачу как нерешаемую
- Рассматриваем как задачку для ИИ
## Ключевое требование заказчика
**НЕ МОНОЛИТИТЬ.** Всё делать небольшими НЕЗАВИСИМЫМИ слоями.
Каждый слой — отдельный Python-файл, своя зона ответственности.
Слои не должны зависеть друг от друга (или минимально).
## Что уже сделано
```
site/
├── app.py ← Flask-приложение, класс ContractsApp, только сборка слоёв
├── db.py ← слой БД: connect(), query(), _pg_connect()
├── test_routes.py ← Blueprint /test (статус БД, список таблиц, SQL, createdb)
├── parser.py ← слой парсера: parse(bytes, mime) → полный слепок docx/pdf
├── textify.py ← слой: elements JSON → линейный текст для LLM
├── static/
└── templates/
```
База: PostgreSQL, всё внутри облачного кластера.
LLM: aillm.ru (120B модель).
## Что предстоит сделать
1. **Слой загрузки** — приём файлов через API, сохранение в БД (original_bytes + parsed_text)
2. **Слой LLM** — отправка текста → модель → структурированные строки спецификации
3. **Слой хранения** — таблицы contracts, supplements, spec_rows, spec_history
4. **Слой сравнения (diff)** — сравнение строк между допниками, выявление изменений
5. **Слой API** — отдача истории договора, списка, etc.
## Вопрос
Разъясни план архитектуры:
1. Правильно ли разбиты слои? Может что-то объединить или наоборот — разделить?
2. В каком порядке их создавать?
3. Как организовать поток данных между слоями, чтобы они оставались независимыми?
4. Как должен выглядеть основной оркестратор (app.py) — без бизнес-логики?
5. Какие потенциальные проблемы ты видишь с таким подходом?
## Текущий код для ознакомления
### parser.py (bytes → JSON elements)
Использует python-docx для docx, libreoffice для .doc, pdfplumber для PDF, zipfile для zip.
Возвращает полный слепок: все абзацы (со стилями) + все таблицы (все строки).
Ничего не фильтрует, не теряет.
### textify.py (JSON elements → текст)
Форматирует elements в линейный текст: параграфы как `[Style] text`, таблицы как `| cell | cell |`.
Тоже не фильтрует, только форматирует.
### db.py (БД)
connect() в целевую БД, _pg_connect(dbname) в любую, query(sql) — выполнить произвольный запрос.
### test_routes.py (/test Blueprint)
GET /test — статус БД, POST /test с действиями: status, tables, sql, createdb.
Мост к БД извне для отладки.
## Ожидаемый формат ответа
Структурированный план архитектуры с пояснениями по каждому пункту вопроса.
@@ -0,0 +1,258 @@
# Аудит для Sonnet — пофайловый разбор
Ты должен прочитать КАЖДЫЙ файл из списка ниже и выдать по нему детальный отчёт.
## Формат отчёта ПОФАЙЛОВО
Для каждого файла:
```
### convert_server.py
| Строка | Тип | Серьёзность | Что не так | Как исправить |
|--------|-----|-------------|------------|---------------|
| 242 | dead code | low | Дубликат _handle_cleanup | Удалить второй |
```
После пофайлового разбора — СВОДНАЯ ТАБЛИЦА всех проблем по серьёзности: critical → high → medium → low.
---
## Файл 1: `contractor/deploy/convert_server.py`
**Что это:** HTTP роутер на http.server + ThreadingMixIn. Все endpoint'ы приложения.
**Что смотреть:**
- ВСЕ `do_GET`, `do_POST`, `do_DELETE` — правильная диспетчеризация? Нет мёртвых путей?
- `_handle_process_v2` — валидация UUID? SSE корректно закрывается при ошибке? Утечка соединений?
- `_handle_upload` — размер тела? Content-Type проверка? Таумаут?
- `_handle_cleanup` — ДВА определения (строки ~242 и ~255). Второй перекрывает первый. Dead code. Порядок DELETE правильный?
- `_handle_api_sync` — читает JSON body. Что если тело пустое/битое? Что если keep_ids содержит 10000 id? Нет лимита.
- `_handle_api_document` — doc_id из URL без валидации UUID → psycopg2.InvalidTextRepresentation на "fake-id".
- `_handle_api_document_delete` — cascade порядок: spec_current → spec_events → supplements → document. Правильный?
- `_handle_classify_batch` — batch_id из JSON без валидации UUID.
- `_handle_apply_groups` — валидация структуры groups?
- `_handle_api_prompts_*` — role из query params без валидации.
- `_json()` — экранирует ли спецсимволы в данных?
- `_sse()` — f-string с json.dumps. Данные от LLM могут содержать спецсимволы.
- Импорт `execute` на строке 12 — не конфликтует с другими импортами?
- `ALTER TABLE ADD COLUMN IF NOT EXISTS` — права на DDL? Идемпотентно?
- CORS — `_send_cors()` вызывается везде? OPTIONS?
---
## Файл 2: `contractor/deploy/app.js`
**Что это:** Весь фронтенд (42KB). Загрузка, классификация, группы, сравнение, промпты.
**Что смотреть:**
- `fileQueue`, `contractId`, `batchId` — глобальные. Где расходятся с сервером?
- `renderTable()` — `innerHTML` из `fileQueue[].name`, `fileQueue[].status`. XSS?
- `fileInput change` — гонка удаления старых + upload новых?
- `syncDB()` — fire-and-forget, без await, без проверки ответа.
- `runClassify()` — утечка таймеров при ошибке?
- `loadGroups()` — ЕДИНСТВЕННОЕ место где diffBody. Больше никто не должен.
- `runCompareForGroup()` — compareCard. Все ссылки compareBody/compareStatus?
- `llmBtn` — compareCard. То же.
- SSE handler'ы — ДВА почти идентичных. Дублирование.
- `showText()` — `escHtml(classify_raw)` ок, но `counterparty`, `own_number` — НЕ экранированы! XSS.
- `escHtml()` — экранирует `<>&"'`?
- `stepDone/Active/resetStepper` — innerHTML через replace. Спецсимволы в id?
- `window._groupsData` — устаревает после remove→re-classify. runCompareForGroup использует индекс.
- `showClassifyBtn()` — идемпотентно?
- `lucide.createIcons()` — не ломает onclick?
---
## Файл 3: `contractor/deploy/app_utils.js`
**Что это:** Утилиты: форматирование, removeFile, escHtml, модалки.
**Что смотреть:**
- `removeFile()` — syncDB+resetStepper+showClassifyBtn через typeof. Если нет — молча.
- `formatDate()`, `formatSize()` — null/undefined safe?
- `escHtml()` — все опасные символы: `< > & " '`?
- `moveUp/Down` — не вызывают syncDB (правильно, состав не меняется).
---
## Файл 4: `contractor/deploy/services/classify.py`
**Что это:** LLM-классификация. ThreadPoolExecutor, выжимка, вызов LLM.
**Что смотреть:**
- `classify_batch()` — reset + list_pending. Гонка между ними?
- `_classify_one()` — `_smart_extract` может упасть до LLM. Обрабатывается?
- `_smart_extract()` — неожиданная структура elements_json?
- `_call_llm_classify()` — httpx `verify=False`. MITM уязвимость.
- `_safe_json_parse()` — edge cases: Null/None, числа без кавычек, пустой ответ.
- `MAX_WORKERS=4` — 4 одновременных запроса создают каждый свой пул?
- `LLM_KEY` из env — если не задан?
- API key в коде? (берётся из env, ок)
---
## Файл 5: `contractor/deploy/services/grouping.py`
**Что это:** Группировка документов по контрактам + виртуальные группы.
**Что смотреть:**
- `group_documents()` — виртуальные группы: что если parent_number и own_number оба null?
- Коллизия normalize_number: разные номера → одинаковый нормализованный.
- Сортировка по doc_date: None у всех → нестабильный порядок.
- `apply_groups()` — нет проверки на существующий contract (дубликат при повторе).
- `supplements_list.remove(s)` внутри цикла for — может пропускать элементы.
---
## Файл 6: `contractor/deploy/services/process.py`
**Что это:** SSE пайплайн сравнения.
**Что смотреть:**
- `run_pipeline()` — битая ссылка supplement→document? timeout? retry?
- `call_llm()` — таймаут?
- SSE события — все ли обрабатываются на фронте?
---
## Файл 7: `contractor/deploy/services/parse.py`
**Что это:** Парсинг PDF/DOCX.
**Что смотреть:**
- Расширение: .PDF uppercase? Без расширения?
- PDF: битый/зашифрованный → исключение?
- DOCX: .doc (OLE) → исключение?
- Таблицы: пустые, объединённые ячейки?
- file_data = None?
---
## Файл 8: `contractor/deploy/services/upload.py`
**Что это:** Multipart загрузка через cgi.FieldStorage.
**Что смотреть:**
- cgi.FieldStorage deprecated. Большие файлы?
- content_length — отрицательное/огромное → rfile.read?
- filename — path traversal (`../../etc/passwd`)?
- 100MB файл → весь в памяти.
- contract_id без валидации → SQL.
- `delete_by_document` до создания нового → исключение = потеря старого без создания нового.
- base64 → +33% размер.
---
## Файл 9: `contractor/deploy/db/connection.py`
**Что это:** ThreadedConnectionPool.
**Что смотреть:**
- `DB_PASS` из env, пустой → ошибка подключения.
- `minconn=1, maxconn=10` — достаточно?
- getconn/putconn всегда в finally?
- `_connection` глобальная — сервер не стартует без БД.
---
## Файл 10: `contractor/deploy/db/documents.py`
**Что это:** CRUD documents + classify_raw.
**Что смотреть:**
- `insert()` — все параметры через %s?
- `set_classification()` — classify_raw=None → NULL.
- `set_classify_failed()` — не чистит старые поля классификации.
- `delete()` — без cascade!
- `get()` — SELECT *, может вернуть base64 original_bytes.
---
## Файл 11: `contractor/deploy/db/supplements.py`
**Что это:** CRUD supplements + cascade.
**Что смотреть:**
- `delete_by_document()` — spec_current WHERE contract_id удаляет ВСЁ для контракта. Если несколько supplements → остальные теряют spec_current.
- `list_by_contract()` — orphan supplements игнорируются.
- `insert()` — нет FK проверки.
---
## Файл 12: `contractor/deploy/db/contracts.py`
**Что это:** CRUD contracts.
**Что смотреть:**
- `insert()` — нет уникальности, можно дубликат.
- `delete_orphaned()` — вызывается? Где?
---
## Файл 13: `contractor/deploy/db/spec_events.py`
**Что это:** Event Sourcing.
**Что смотреть:**
- `get_next_seq()` — `MAX(seq)+1`. Гонка! Два потока → одинаковый seq.
- `reset()` — необратимо.
- `apply_ops()` — валидация структуры?
---
## Файл 14: `contractor/deploy/db/spec_current.py`
**Что это:** Текущая спецификация.
**Что смотреть:**
- Кто обновляет spec_current после удаления spec_events?
- `get_elements_json()` — где используется?
---
## Файл 15: `contractor/deploy/db/prompts.py`
**Что это:** CRUD промптов с версионированием.
**Что смотреть:**
- `seed_defaults()` — `if count>0: return` — если есть extract но нет classify → classify не создастся.
- `_ensure_classify_prompt()` — отдельный механизм, почему?
- `save_new_version()` — UPDATE+INSERT не атомарно. Гонка.
- `activate()` — гонка.
- `delete_prompt()` — проверка is_active потом DELETE. Гонка.
- `_serialize()` — мутирует словарь.
---
## Файл 16: `contractor/deploy/llm_prompt.py`
**Что это:** Билдер промптов.
**Что смотреть:**
- `build_prompt()` — f-string с данными парсинга. Безопасно?
- `_fetch_prompt()` — HTTP к Lucee. Таймаут? Fallback при недоступности?
- `build_classify_prompt()` — прямой доступ к БД, не через HTTP. Почему?
- FALLBACK_* хардкод — дублирование с БД.
---
## Файл 17: `contractor/index.cfm`
**Что это:** HTML-оболочка.
**Что смотреть:**
- `?v=1.0.175` хардкод — менять при каждом обновлении JS.
- pipelineStepper — ○⏳✓ через replace. Надёжно?
- XSS через prompt body в textarea/div?
- Z-index конфликты модалок?
- inline onclick — ломаются при перезагрузке JS?
---
## СВОДНАЯ ТАБЛИЦА (выдать после пофайлового разбора)
| # | Файл:строка | Серьёзность | Тип | Описание | Как исправить |
|---|-------------|-------------|-----|----------|---------------|
## ТОП-5 (срочно)
5 проблем, которые надо чинить прямо сейчас.
@@ -0,0 +1,45 @@
# Аудит Sonnet — результаты
## Ключевые цифры
- **48 проблем** найдено
- **14 CRITICAL**, **15 HIGH**, **19 MEDIUM**
- 17 файлов проанализировано
## ТОП-5 срочных
1. **upload.py:28** — Path Traversal: `filename` без `os.path.basename()` → `../../etc/passwd`
2. **app.js:79,495-520,497,703,708** — XSS × 5 мест: `innerHTML` без `escHtml()` на данных от LLM
3. **grouping.py:76** — `supplements_list.remove(s)` в итерации → пропуск элементов
4. **spec_events.py:15** — `MAX(seq)+1` без блокировки → race condition на seq
5. **prompts.py:40,73,86** — 3 race conditions: seed/save/activate без транзакций
## Что я понял
### Мои косяки (надо чинить)
1. **XSS в 5 местах** — `escHtml` не везде. `counterparty`, `own_number`, `filename`, `contract_number` — всё от LLM, всё в innerHTML без экранирования. Тупо пропустил.
2. **`supplements_list.remove(s)` в цикле** — реальный баг в grouping.py:76. При удалении элемента из списка во время итерации for пропускаются элементы. Может ломать группировку.
3. **`syncDB()` без await** — fire-and-forget. Если сервер не ответил — не узнаем. БД рассинхронится с таблицей.
4. **`delete_by_document` удаляет spec_current для ВСЕГО контракта** — если у контракта 3 supplements, удаление одного затирает spec_current для двух других. Серьёзный баг в supplements.py.
5. **`verify=False` в httpx** — отключена проверка SSL. MITM-уязвимость в classify.py и llm_prompt.py.
### Что НЕ надо чинить (не критично)
- Dead code (дубликат `_handle_cleanup`) — не влияет на работу.
- `_serialize` мутирует словарь — косметика.
- `v1.0.175` хардкод — пока сойдёт.
- `.doc` без fallback — формат редкость.
- `errors="ignore"` в парсинге — мелочь.
### Что Sonnet нашёл сверх моего анализа
- `MAX(seq)+1` race condition — я не подумал про параллельные запросы к spec_events.
- `apply_groups()` без транзакций — я не проверил атомарность.
- `_safe_json_parse()` возвращает None для `"null"` — edge case который я упустил.
- DoS через `keep_ids` без лимита — не подумал про O(N²).
- `get()` возвращает base64 original_bytes (133MB) — утечка памяти.
@@ -0,0 +1,38 @@
# Sonnet Analysis — 2026-06-24 — Auto-classification
## 1. Архитектура: отдельный /api/classify
Не встраивать в upload. Отдельный эндпоинт → фоновая классификация → SSE прогресс → UI подтверждение.
## 2. Промпт classify
Только header (первые 50 элементов / 2000 символов). Экономия токенов в 5-10 раз.
Поля: doc_type, contract_number, doc_date, counterparty, parent_contract_number.
## 3. Группировка
Новая таблица doc_classifications (staging). Группировка по (contract_number, counterparty) + fuzzy match.
Существующих contracts + supplements достаточно для хранения итога.
## 4. Схема БД
- ALTER supplements ADD sort_order
- CREATE TABLE doc_classifications (staging)
## 5. UI
Карточки групп (contract), внутри — сортированный список документов.
Действия: перенести, изменить тип, подтвердить, запустить сравнение.
## 6. Массовая загрузка
ZIP → batch upload → 5 параллельных воркеров (threading.Thread + queue.Queue).
2000 файлов × 6s / 5 воркеров ≈ 40 минут.
Память: BYTEA в PG для пилота ОК, для прода нужен S3.
## 7. Приоритеты
1. doc_classifications + промпт classify — низкая сложность, критично
2. /api/classify (один doc_id) — низкая, критично
3. Batch ZIP + workers + SSE — высокая, критично
4. Алгоритм группировки — средняя, критично
5. UI review — средняя, важно
6. /process-v2 per group — низкая, важно
7. Артикул→каталог — высокая, отложить
## Риски
- threading в http.server: на пилоте ОК, для прода нужен gunicorn
- Промпт classify надо обкатать на реальных документах ДО реализации
@@ -0,0 +1,37 @@
# Запрос: загрузка файлов с нуля
**Задача:** надёжная загрузка docx/pdf/zip (до 50MB) через веб-интерфейс.
## Ресурсы
- **Flask** (managed pythonk8s.services.ngcloud.ru, деплой = git push)
- **PostgreSQL 17.6** (внутренний кластер `postgresqlk8s-master...svc.cluster.local`)
- **Redis** (внутренний `redisk8s...svc.cluster.local`, admin/aLITloRefJEiCPqUc2xB)
- **БД:** пул psycopg2 (2-10, keepalive), таблицы есть (`documents`, `contracts`, `supplements`)
- **Парсеры:** python-docx (DOCX), pdfplumber (PDF), zipfile (ZIP), textify.to_text()
- **LLM:** aillm.ru (gpt-oss-120b), HTTP/2
- **Клиент:** браузер, XHR POST, JSON body
## Что НЕ работает (43 версии)
- POST с телом > 64KB → nginx/Ingress рвёт TCP (HTTP 000, ReadTimeout)
- 10-50KB: 100% надёжно
- FormData, JSON/base64, чанки, Redis, retry — ничего не помогло
- 64KB — жёсткий предел платформы
## Что нужно
1. **Архитектура загрузки** — гарантированно обходящая лимит 64KB на тело запроса
2. **Прогресс на клиенте** — интерактивно (имя файла, проценты, таймер)
3. **Парсинг после загрузки** — извлечение параграфов, таблиц, стилей
4. **ZIP** — показывать содержимое, парсить файлы внутри
## Вопросы
1. Как обойти 64KB лимит тела запроса?
2. Чанки — на клиенте или сервере?
3. Redis — нужен или нет для этой задачи?
4. Как гарантировать целостность файла при чанковой загрузке?
5. Какая структура БД оптимальна для хранения результатов парсинга?
**Ответь кратко: архитектура (3-5 пунктов), потом отвечу на вопросы.**
+79
View File
@@ -0,0 +1,79 @@
# Sonnet Analysis — ZIP Upload CORS + Multipart
Дата: 2026-06-23 | Источник: Sonnet (новый чат)
Связано: index.cfm, convert_server.py, nginx-contracts.conf
---
## Текущее состояние
- JS на `contractor.luceek8s.dev.nubes.ru` шлёт FormData через fetch на `contracts.kube5s.ru/unzip-upload`
- VM (Python, 8766) парсит multipart через `email.parser.BytesParser`
- Nginx проксирует `/unzip-upload` → VM:8766
- CURL работает, браузер — `Failed to fetch`
## Диагноз Sonnet
### 1. CORS в nginx — add_header внутри if не работает
`add_header` в родительском `location` не применяется к ответу из `if (...) { return 200; }` — это новый контекст.
**Исправление:** заголовки внутрь `if`:
```nginx
location /unzip-upload {
if ($request_method = OPTIONS) {
add_header Access-Control-Allow-Origin "*";
add_header Access-Control-Allow-Methods "POST, OPTIONS";
add_header Access-Control-Allow-Headers "*";
add_header Content-Length 0;
return 204;
}
add_header Access-Control-Allow-Origin "*";
proxy_pass http://127.0.0.1:8766;
client_max_body_size 100m;
}
```
### 2. Multipart парсинг — filename*= кодировка
Браузер для файлов с не-ASCII именами использует `filename*=UTF-8''...` (RFC 5987). `part.get_filename()` может вернуть None.
**Решение:** переход на raw binary (п.3) устраняет проблему полностью.
### 3. Raw binary — лучший вариант
**JS:**
```javascript
var zipResp = await fetch(UNZIP_URL, {
method: 'POST',
body: f,
headers: { 'Content-Type': 'application/zip' }
});
```
**Python:**
```python
if "multipart" in content_type:
# fallback для curl
...
else:
zip_data = self.rfile.read(length)
```
**Плюсы:**
- Нет multipart overhead
- Нет проблем с filename-кодировкой
- Нет проблем с boundary
**Минусы:**
- Нужен правильный CORS preflight (Content-Type: application/zip — не simple)
- Нужен `client_max_body_size` в nginx
## План реализации (когда «делай»)
1. nginx: перенести CORS-заголовки внутрь `if`-блока
2. JS: `fetch(UNZIP_URL, { method: 'POST', body: f, headers: {'Content-Type': 'application/zip'} })`
3. VM: raw binary как основной путь, multipart как fallback
4. Убрать FormData из JS
5. Bump, пуш, синк VM + nginx