v1.0.177: submodule bump + History docs (Opus, Sonnet audit, auto-classify)

This commit is contained in:
“Naeel”
2026-06-24 19:41:46 +04:00
parent d960e2cdfc
commit af1fed1077
10 changed files with 592 additions and 1 deletions
+28
View File
@@ -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
+32
View File
@@ -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 Сравнить)
+18
View File
@@ -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.
+103
View File
@@ -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 через флаг, механизм миграции) — и можно делать.
+36
View File
@@ -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`
+258
View File
@@ -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 проблем, которые надо чинить прямо сейчас.
+45
View File
@@ -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) — утечка памяти.
+38
View File
@@ -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 надо обкатать на реальных документах ДО реализации
Submodule contractor updated: 740f75a791...a58df099b6