docs: архитектурный анализ + History + gitignore (2026-06-27)

This commit is contained in:
“Naeel”
2026-06-27 13:00:18 +04:00
parent 4a21d77f51
commit 82c5c075f1
154 changed files with 4789 additions and 1443 deletions
+200
View File
@@ -0,0 +1,200 @@
# Запрос к Опусу — анализ и план развития Contracts App (v1.0.178)
Дата: 26.06.2025. **Только инструкции, код не менять.**
---
## 1. Контекст: что за сервис
Сервис **«Сверка договоров»** — обработка договоров облачного провайдера НУБЕС с контрагентами.
Пользователь загружает ZIP-архивы с документами (договоры, допсоглашения, спецификации),
система классифицирует, группирует по номерам договоров и сравнивает
спецификации услуг — показывает diff (ADD/UPDATE/DELETE/UNRESOLVED).
**Архитектура:**
- **Lucee (CFML)** — фронтенд, домен contractor.luceek8s.dev.nubes.ru
- **Python (VM)** — бэкенд, домен contracts.kube5s.ru, порт 8766
- **PostgreSQL** — хранение
- **nginx** — прокси
- **LLM** — api.aillm.ru, модель gpt-oss-120b, HTTP/2 через httpx
**Пайплайн:**
1. Upload (.docx/.pdf/.doc/.zip) → парсинг (python-docx/pdfplumber) → elements_json
2. Classify — LLM определяет: тип (contract/supplement/specification/other),
own_number, parent_number, counterparty, doc_date
3. Group — Python normalize_number() + matching по номеру договора
4. Compare — SSE, LLM сравнивает спецификации: ADD/UPDATE/DELETE/UNRESOLVED
---
## 2. Текущий код — ключевые файлы
### 2.1. Classify
**Файл:** `contractor/deploy/services/classify.py`
- `MAX_WORKERS = 4` — параллельные запросы к LLM
- `_smart_extract()` — выжимка текста: заголовок ~1500 симв + regex-хиты по маркерам
- `_call_llm_classify()` — вызов LLM, парсинг JSON из ответа
- `classify_batch()` — ThreadPoolExecutor, классифицирует все pending документы
- `_safe_json_parse()` — чинит битый JSON из LLM (markdown, trailing commas)
**Промпт classify:** `llm_prompt.py``build_classify_prompt(header_text)`.
Запрашивает у LLM:
```json
{"doc_type": "contract|supplement|specification|other",
"own_number": "XXX001-03700",
"parent_number": "XXX001-03700", // для допников/спек
"doc_date": "2025-01-01",
"counterparty": "ООО \"Ромашка\""}
```
**Проблема classify:** при 50+ файлах синхронный вызов таймаутился.
Сейчас: `subprocess.Popen` → classify_worker.py (async, 202 Accepted).
Но фронтенд (`app.js` `runClassify()`) не полностью синхронизирован с async-ответом.
### 2.2. Group
**Файл:** `contractor/deploy/services/grouping.py`
- `normalize_number()` — uppercase + только буквы/цифры.
«МЭС-123/2024» == «МЭС 123/2024» после нормализации.
- `group_documents()` — иерархический алгоритм:
1. contract → якорь группы
2. supplement/spec → matching по parent_number (нормализованный)
3. Оставшиеся → виртуальные группы по own_number
4. Без номеров → `__unresolved__`
- `apply_groups()` — создаёт записи в DB (contracts + supplements)
**Известные баги normalize_number:**
- Кириллическая `О` vs цифра `0` — не различаются
- Латинская `C` vs кириллическая `С` — не различаются
- `Ё` → выпадает
### 2.3. Compare
**Файлы:** `contractor/deploy/convert_server.py` (SSE endpoint `/process-v2`),
`contractor/deploy/compare.js` (фронтенд SSE-клиент).
**Промпты compare:** `llm_prompt.py`
- `FALLBACK_EXTRACT` — для первого документа (базовый договор/спецификация):
ADD всех строк.
- `FALLBACK_DIFF` — для допсоглашений: UPDATE/DELETE/ADD/UNRESOLVED.
Поддерживает два режима:
- `"mode": "partial"` — точечные изменения
- `"mode": "full_replace"` — полная замена спецификации
**Проблема compare:** оба режима — «ADD всех строк». LLM не всегда корректно
сопоставляет строки между v1 и v2 спецификации.
### 2.4. ZIP handling
**Файлы:** `contractor/deploy/files.js` (`addZipFile()`),
`contractor/deploy/convert_server.py` (`_handle_unzip_upload()`).
Текущая логика:
1. ZIP загружается через FormData на `/unzip-upload`
2. Бэкенд распаковывает, возвращает base64 каждого файла
3. Фронтенд для каждого: base64 → Blob → File → upload (как addRegularFile)
4. Дубликаты по имени: confirm-диалог, перезапись
**Чего нет:**
- `zip_source` — связь файла с родительским ZIP
- Группировка в таблице по ZIP-источнику
- ID файла = `zip_source + "/" + filename`
### 2.5. Фронтенд
**Ключевые JS-модули:** `contractor/deploy/` — state.js, files.js, groups.js, compare.js, app.js, app_utils.js
**Таблица файлов:** `files.js` `renderFiles()` — плоский список, без группировки по ZIP.
Уже есть `max-height: 50vh; overflow-y: auto` (скролл).
**Степпер:** `app.js` `renderStepper()` — 4 шага: Загрузка → Классификация → Группировка → Сравнение.
---
## 3. Что сказал заказчик (26.06.2025)
> «А что касается вводных — файлы могут быть в зипах. Как правило, по 1 контрагенту,
> но я считаю, что по контрагенту может быть несколько зипов, и теоретически
> могу представить ситуацию, когда будет в одном зипе по нескольким как-то
> связанным контрагентам (какие-то агентские схемы). Наверно, вероятность того,
> что в одном зипе один контрагент — весьма высокая, но не 100%.»
Также обсуждалось:
- Привязка файлов к родительскому ZIP
- В таблице — ZIP как группирующий заголовок, ниже со сдвигом — вложенные файлы
- ID файла = имя зипа + "/" + имя файла
---
## 4. Что обсуждали мы (ключевые выводы)
1. **НУБЕС — всегда Исполнитель.** Контрагенты — Заказчики.
2. **Один batch = один контрагент (почти всегда).**
Хоть 1 зип, хоть 5 зипов — всё по одному контрагенту.
3. **Агентские схемы — исключение**, не основная логика.
4. **`zip_source`** — для визуальной группировки в таблице.
Не влияет на логику classify/group/compare.
5. **Два сценария UI:**
- «Точный» (≤100 файлов) — текущий UI с таблицей и ручным контролем
- «Поток» (100+) — упрощённый UI: прогресс-бар, авто-пайплайн, сводный отчёт
(без таблицы — 1000 DOM-строк вешают браузер)
6. **Коллизия одинаковых имён из разных ZIP:**
- ID = `zip_source + "/" + filename` → разные сущности
- Compare покажет diff между версиями
---
## 5. Что нужно от Опуса
### 5.1. ПЛАН по zip_source
Как именно внедрить привязку файлов к родительскому ZIP на ВСЕХ слоях:
- `unzip.py` → возвращать `zip_source`
- `db/documents.py` → поле `zip_source`
- `files.js` `renderFiles()` → группировка + отступ
- `state.js` → поле `zip_source`
- `index.cfm` / Lucee → надо ли менять?
**Варианты отображения:**
- Вариант А: ZIP как секция-заголовок, под ним файлы с отступом
- Вариант Б: древовидная структура (раскрывающиеся ZIP'ы)
- Вариант В: цветовая маркировка по ZIP
Какой лучше для пользователя и почему?
### 5.2. ПЛАН по двум сценариям UI
Как архитектурно разделить «Точный» и «Поток» режимы:
- Общий код (classify/group/compare не меняются)
- Разный UI: что показывать, что скрывать
- Как переключаться между режимами (авто по кол-ву файлов? ручной выбор?)
- «Поток» — сводный отчёт: структура, что в нём
### 5.3. АНАЛИЗ текущих промптов
Внимательно изучи `llm_prompt.py`:
- Есть ли проблемы в классификации (counterparty не определяется)
- Достаточно ли хорош diff-промпт (compare)
- Нужны ли разные варианты промптов для разных сценариев:
- Только НУБЕС-один контрагент (стандартный)
- Два контрагента (агентские схемы)
- Мусорные документы (акты сверки, счета)
- Нужна ли корректировка глоссария, примеров
### 5.4. РЕКОМЕНДАЦИИ по улучшению
Что ещё можно улучшить, исходя из анализа кода и требований заказчика:
- Приоритеты: что делать в первую очередь
- Риски: что может сломаться
- Оценка трудозатрат (грубо: маленькая/средняя/большая задача)
---
## 6. Ограничения
- **Код не менять.** Только инструкции и план.
- Ответ — в чат, подробно, с обоснованием каждого решения.
- Если есть несколько вариантов — перечислить с плюсами/минусами.