v1.0.177: submodule bump + History docs (Opus, Sonnet audit, auto-classify)
This commit is contained in:
@@ -0,0 +1,28 @@
|
||||
# MVP Auto-classification — план реализации
|
||||
|
||||
## Итоговый план (Опус + мои правки)
|
||||
|
||||
### Фаза 1: БД + данные
|
||||
1. ALTER documents — 7 колонок (doc_type, own_number, parent_number, doc_date TEXT, counterparty, classify_status, batch_id)
|
||||
2. db/documents.py — set_classification(), list_pending(batch), list_by_batch(batch)
|
||||
3. db/prompts.py — seed classify prompt
|
||||
|
||||
### Фаза 2: Классификация
|
||||
4. llm_prompt.py — build_classify_prompt (строгий JSON)
|
||||
5. services/classify.py — умная выжимка + LLM + ThreadPoolExecutor(4)
|
||||
|
||||
### Фаза 3: Группировка
|
||||
6. services/grouping.py — normalize_number + group_documents + apply_groups
|
||||
|
||||
### Фаза 4: Эндпоинты
|
||||
7. convert_server.py — POST /classify-batch, GET /api/groups, POST /apply-groups
|
||||
|
||||
### Фаза 5: UI
|
||||
8. app.js — batch_id, classify button, polling progress, group cards, per-group compare
|
||||
9. index.cfm — version bump only (HTML не меняем)
|
||||
|
||||
## Мои корректировки к плану Опуса
|
||||
- batch_id генерируется на клиенте (crypto.randomUUID()), передаётся в upload
|
||||
- ZIP — фаза 2, сначала multi-upload (уже работает)
|
||||
- doc_date TEXT (не DATE) — LLM нормализует в ISO, при провале null
|
||||
- parent_number отдельно от own_number
|
||||
@@ -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`
|
||||
@@ -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 через флаг, механизм миграции) — и можно делать.
|
||||
@@ -0,0 +1,36 @@
|
||||
# Session 13 — 2026-06-24 — Auto-classification planning
|
||||
|
||||
## Требование Мищука (из Telegram, 23.06.2026)
|
||||
|
||||
> «Должна быть массовая загрузка, система сама должна разбираться, кто к кому (там в документе вся эта инфа есть)»
|
||||
> «Я не должен распознавать ничего. Система должна сделать все.»
|
||||
> «я ей дам 2000 документов»
|
||||
|
||||
## Текущее состояние (v1.0.161)
|
||||
|
||||
- Загрузка по одному файлу
|
||||
- Ручная сортировка стрелками ↕
|
||||
- Первый в списке = базовый договор (жёстко)
|
||||
- LLM-сравнение: каждый следующий против spec_current
|
||||
- Нет автоопределения типа, нет группировки
|
||||
|
||||
## Что нужно
|
||||
|
||||
1. Массовая загрузка (папка, 2000+ файлов)
|
||||
2. LLM-классификация каждого файла: тип, номер, дата, контрагент
|
||||
3. Автогруппировка по контрактам
|
||||
4. Автопорядок внутри группы (хронология)
|
||||
5. UI подтверждения
|
||||
6. Пайплайн сравнения по группам
|
||||
|
||||
## Куда смотреть Соннету
|
||||
|
||||
- `contractor/deploy/convert_server.py` — роутер
|
||||
- `contractor/deploy/db/` — PostgreSQL CRUD
|
||||
- `contractor/deploy/services/` — бизнес-логика
|
||||
- `contractor/deploy/app.js` — фронтенд JS
|
||||
- `contractor/index.cfm` — Lucee HTML (191 строка)
|
||||
- VM: `/home/naeel/contracts/` — рабочий код
|
||||
- VM: `/etc/nginx/sites-enabled/nginx-contracts.conf`
|
||||
- VM: `/etc/systemd/system/contracts.service`
|
||||
- БД: VM `127.0.0.1:5432/baza`, схема в `db/*.py`
|
||||
@@ -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 надо обкатать на реальных документах ДО реализации
|
||||
+1
-1
Submodule contractor updated: 740f75a791...a58df099b6
Reference in New Issue
Block a user