diff --git a/History/mvp-auto-classify-plan.md b/History/mvp-auto-classify-plan.md new file mode 100644 index 0000000..4a1e4cc --- /dev/null +++ b/History/mvp-auto-classify-plan.md @@ -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 diff --git a/History/opus-auto-classify-analysis.md b/History/opus-auto-classify-analysis.md new file mode 100644 index 0000000..713d15c --- /dev/null +++ b/History/opus-auto-classify-analysis.md @@ -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 Сравнить) diff --git a/History/opus-classify-bugs.md b/History/opus-classify-bugs.md new file mode 100644 index 0000000..7d87b87 --- /dev/null +++ b/History/opus-classify-bugs.md @@ -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. diff --git a/History/opus-plan-groups-progress.md b/History/opus-plan-groups-progress.md new file mode 100644 index 0000000..87a862f --- /dev/null +++ b/History/opus-plan-groups-progress.md @@ -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` diff --git a/History/opus-plan-review.md b/History/opus-plan-review.md new file mode 100644 index 0000000..cecb66e --- /dev/null +++ b/History/opus-plan-review.md @@ -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 через флаг, механизм миграции) — и можно делать. diff --git a/History/session-13-auto-classify.md b/History/session-13-auto-classify.md new file mode 100644 index 0000000..e9315c7 --- /dev/null +++ b/History/session-13-auto-classify.md @@ -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` diff --git a/History/sonnet-audit-request.md b/History/sonnet-audit-request.md new file mode 100644 index 0000000..e9f5a02 --- /dev/null +++ b/History/sonnet-audit-request.md @@ -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 проблем, которые надо чинить прямо сейчас. diff --git a/History/sonnet-audit-results.md b/History/sonnet-audit-results.md new file mode 100644 index 0000000..3f2620b --- /dev/null +++ b/History/sonnet-audit-results.md @@ -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) — утечка памяти. diff --git a/History/sonnet-auto-classify-analysis.md b/History/sonnet-auto-classify-analysis.md new file mode 100644 index 0000000..21ccd83 --- /dev/null +++ b/History/sonnet-auto-classify-analysis.md @@ -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 надо обкатать на реальных документах ДО реализации diff --git a/contractor b/contractor index 740f75a..a58df09 160000 --- a/contractor +++ b/contractor @@ -1 +1 @@ -Subproject commit 740f75a79173bc7c6a3abf4e636ce2a8c8f58a36 +Subproject commit a58df099b69a5e639c9719fca50f056b6dbf45db