docs: архитектурный анализ + History + gitignore (2026-06-27)
This commit is contained in:
@@ -0,0 +1,285 @@
|
||||
# Ответ Опуса — 30 прицельных тестовых кейсов
|
||||
|
||||
Ответ на `History/opus-testcases-request.md` от 25.06.2026.
|
||||
|
||||
Опус изучил реальный код пайплайна (classify → group → compare) и дал 30 кейсов,
|
||||
заточенных под конкретные уязвимости реализации.
|
||||
|
||||
---
|
||||
|
||||
## Моя оценка
|
||||
|
||||
### Сильные стороны
|
||||
|
||||
1. **Опус реально читал код.** Он нашёл `_smart_extract` (первые 1500 символов + regex-маркеры),
|
||||
`normalize_number` (диапазон `А-Я` не включает `Ё`), `new_values` содержит ТОЛЬКО изменённые поля.
|
||||
Это не общие рекомендации — это точечные удары по слабым местам.
|
||||
|
||||
2. **Кейсы 13-14 — золото.** Кириллическая `О` vs ноль `0`, латинская `C` vs кириллическая `С` —
|
||||
это реально ломает группировку. Опус предлагает их как «баг-детекторы»: не исправлять,
|
||||
а задокументировать текущее поведение и ждать fuzzy-нормализации.
|
||||
|
||||
3. **Кейс 15 (Ё)** — я даже не знал про диапазон `А-Я`. Опус нашёл.
|
||||
|
||||
4. **Приоритет:** Блок B (group) → Блок C (compare) → Блок A (classify). Правильно —
|
||||
group ломается детерминированно без LLM, баги воспроизводимы.
|
||||
|
||||
5. **Кейс 9 (слепая зона)** — 1500 символов выжимки. Практически важный кейс,
|
||||
может объяснить почему некоторые файлы не классифицируются.
|
||||
|
||||
### Что можно добавить
|
||||
|
||||
- **Кейс на batch-progress при падении воркера:** если `classify_worker` упал на середине,
|
||||
progress застревает на N/69 и никогда не достигнет total. Фронт висит вечно.
|
||||
|
||||
- **Кейс на два classify подряд с одним batch_id:** наш lock-файл должен вернуть 409.
|
||||
Стоит проверить что второй запрос действительно отклоняется.
|
||||
|
||||
### Итого
|
||||
|
||||
30 кейсов покрывают все три шага пайплайна. ~40% кейсов — group (самый хрупкий),
|
||||
~30% — compare (LLM-зависимый), ~30% — classify.
|
||||
Можно брать в реализацию. Я генерирую docx по этим шаблонам.
|
||||
|
||||
---
|
||||
|
||||
## Исходный ответ Опуса
|
||||
|
||||
*Далее — полный текст ответа Опуса без сокращений.*
|
||||
|
||||
|
||||
### Кейс 1: Эталонный договор (baseline)
|
||||
**Что проверяем:** базовое извлечение всех 6 полей из чистого договора.
|
||||
**Почему может сломаться:** если падает даже это — проблема не в данных, а в промпте/парсинге.
|
||||
**Файлы:** договор-XXX001-03700.docx
|
||||
**Ключевой текст договора:** "Договор № XXX001-03700 на оказание технологических услуг от 15 марта 2025 г. ООО «Облако-Сервис» (Исполнитель)…"
|
||||
**Ожидаем:** doc_type=contract, own_number="XXX001-03700", parent_number=null, doc_date="2025-03-15", counterparty="ООО «Облако-Сервис»", confidence=ok
|
||||
|
||||
### Кейс 2: Номер кириллицей
|
||||
**Что проверяем:** own_number с кириллическим префиксом и слешем.
|
||||
**Почему может сломаться:** LLM может «перевести» кириллицу в латиницу или отбросить год после слеша.
|
||||
**Файлы:** договор-МЭС.docx
|
||||
**Ключевой текст договора:** "Договор № МЭС-123/2024 от 10.01.2024 г."
|
||||
**Ожидаем:** own_number="МЭС-123/2024" дословно (важно для парного group-кейса 12).
|
||||
|
||||
### Кейс 3: Нестандартный заголовок (не слово «Договор»)
|
||||
**Что проверяем:** определение doc_type=contract, когда документ называется иначе.
|
||||
**Почему может сломаться:** LLM привязывается к слову «Договор»; «Соглашение об оказании услуг» может уехать в other.
|
||||
**Файлы:** договор-нестандарт.docx
|
||||
**Ключевой текст договора:** "СОГЛАШЕНИЕ об оказании услуг связи № SVC-77 от 01.02.2025"
|
||||
**Ожидаем:** doc_type=contract (а не other).
|
||||
|
||||
### Кейс 4: Допник с явным родителем
|
||||
**Что проверяем:** разделение own_number и parent_number.
|
||||
**Почему может сломаться:** LLM путает «свой» номер ДС и номер базового договора местами.
|
||||
**Файлы:** допник-1-XXX003-01300_2.docx
|
||||
**Ключевой текст договора:** "Дополнительное соглашение № 1 к Договору № XXX003-01300 от 05.06.2024"
|
||||
**Ожидаем:** doc_type=supplement, own_number="1", parent_number="XXX003-01300".
|
||||
|
||||
### Кейс 5: Спецификация как отдельный файл
|
||||
**Что проверяем:** doc_type=specification и привязка parent_number.
|
||||
**Почему может сломаться:** спека без слова «договор» в шапке → other; parent потеряется.
|
||||
**Файлы:** спецификация-XXX001-03700.docx
|
||||
**Ключевой текст договора:** "Спецификация № 1 к Договору № XXX001-03700"
|
||||
**Таблица спеки:** № / Наименование / Цена / Объём / Сумма / Дата.
|
||||
**Ожидаем:** doc_type=specification, parent_number="XXX001-03700".
|
||||
|
||||
### Кейс 6: Договор БЕЗ контрагента в шапке
|
||||
**Что проверяем:** поведение, когда counterparty не извлекается.
|
||||
**Почему может сломаться:** LLM «галлюцинирует» контрагента или ставит реквизиты вместо названия.
|
||||
**Файлы:** договор-без-стороны.docx
|
||||
**Ключевой текст договора:** "Договор № NC-09 от 03.03.2025 на оказание услуг" (стороны — только в конце документа, см. кейс 9).
|
||||
**Ожидаем:** counterparty=null/"" , confidence=low (а не выдуманное ООО).
|
||||
|
||||
### Кейс 7: Дата прописью и в нестандартном формате
|
||||
**Что проверяем:** нормализацию doc_date → YYYY-MM-DD.
|
||||
**Почему может сломаться:** «пятнадцатое марта две тысячи двадцать пятого года» или «15.03.25» (двузначный год).
|
||||
**Файлы:** договор-дата-прописью.docx
|
||||
**Ключевой текст договора:** "Договор № DT-15 от «пятнадцатого» марта 2025 года"
|
||||
**Ожидаем:** doc_date="2025-03-15".
|
||||
|
||||
### Кейс 8: Несколько дат в шапке (дата vs срок действия)
|
||||
**Что проверяем:** выбор ПРАВИЛЬНОЙ даты (дата заключения, а не «действует до»).
|
||||
**Почему может сломаться:** LLM хватает первую попавшуюся дату.
|
||||
**Файлы:** договор-две-даты.docx
|
||||
**Ключевой текст договора:** "Договор № TD-21 от 01.04.2025, действует до 31.12.2026"
|
||||
**Ожидаем:** doc_date="2025-04-01".
|
||||
|
||||
### Кейс 9: Реквизиты за пределами первых 1500 символов
|
||||
**Что проверяем:** «слепую зону» _smart_extract.
|
||||
**Почему может сломаться:** номер/контрагент стоят после длинной преамбулы (>1500 симв.) и далеко от regex-маркеров → в выжимку не попадут.
|
||||
**Файлы:** договор-длинная-преамбула.docx
|
||||
**Ключевой текст договора:** первые 2 страницы — общие положения без слова «№»; и только потом "Договор № LATE-99 … ООО «Поздний Контрагент»".
|
||||
**Ожидаем:** документ должен классифицироваться (маркер №/договор рядом с данными). Если падает — это сигнал расширить окно выжимки.
|
||||
|
||||
### Кейс 10: «Шумный» документ — несколько номеров на странице
|
||||
**Что проверяем:** выбор own_number среди нескольких «№».
|
||||
**Почему может сломаться:** в шапке есть «Исх. № 456», «Лиц. № 789» и сам «Договор № MN-01» → LLM берёт чужой номер.
|
||||
**Файлы:** договор-много-номеров.docx
|
||||
**Ключевой текст договора:** "Исх. № 456 от 12.05.2025 … Лицензия № 789 … ДОГОВОР № MN-01 от 12.05.2025"
|
||||
**Ожидаем:** own_number="MN-01".
|
||||
|
||||
### Кейс 11: doc_type=other (мусорный файл)
|
||||
**Что проверяем:** что не-договор уходит в other, а не натягивается на contract.
|
||||
**Почему может сломаться:** LLM «обязательно» хочет найти договор.
|
||||
**Файлы:** акт-сверки.docx
|
||||
**Ключевой текст договора:** "Акт сверки взаимных расчётов за 1 квартал 2025"
|
||||
**Ожидаем:** doc_type=other, confidence=low.
|
||||
|
||||
### Кейс 12: Разделители — нормализуются (позитив)
|
||||
**Что проверяем:** «МЭС-123/2024» и «МЭС 123/2024» → одна группа.
|
||||
**Почему может сломаться:** baseline нормализации; обе дают МЭС1232024.
|
||||
**Файлы:** договор-МЭС.docx (own="МЭС-123/2024") + допник-МЭС.docx (parent="МЭС 123/2024")
|
||||
**Ожидаем:** документы в ОДНОЙ группе.
|
||||
|
||||
### Кейс 13: Кириллическая «О» против нуля «0» (классическая опечатка)
|
||||
**Что проверяем:** «O3700» с кириллической О против «03700» с нулём.
|
||||
**Почему может сломаться:** код НЕ приравнивает кириллицу к цифрам → О3700 ≠ 03700 → допник осиротеет в __unresolved__.
|
||||
**Файлы:** договор.docx (own="XXX001-03700", цифра ноль) + допник.docx (parent="XXX001-О3700", кириллическая О)
|
||||
**Ожидаем (как баг-детектор):** сейчас попадут в РАЗНЫЕ группы. Кейс фиксирует поведение и проверяет, появится ли fuzzy-нормализация.
|
||||
|
||||
### Кейс 14: Латинская «C» против кириллической «С»
|
||||
**Что проверяем:** визуально одинаковые префиксы из разных алфавитов.
|
||||
**Почему может сломаться:** normalize_number сохраняет оба алфавита → CBC-10 (лат) ≠ СВС-10 (кир).
|
||||
**Файлы:** договор.docx (own="CBC-10", латиница) + допник.docx (parent="СВС-10", кириллица)
|
||||
**Ожидаем (баг-детектор):** разные группы. Маркер необходимости юникод-конфьюзабл нормализации.
|
||||
|
||||
### Кейс 15: Буква «Ё» в номере
|
||||
**Что проверяем:** диапазон А-Я не включает Ё.
|
||||
**Почему может сломаться:** normalize_number("ЁЖ-5")="Ж5" — буква Ё выпадает. Если в одном документе «ЁЖ-5», в другом «ЖЕ-5» — рассинхрон.
|
||||
**Файлы:** договор.docx (own="ЁЖ-5") + допник.docx (parent="ЁЖ-5")
|
||||
**Ожидаем:** оба теряют Ё одинаково → совпадут как Ж5 (позитив, но по «неправильной» причине — кейс это документирует).
|
||||
|
||||
### Кейс 16: parent_number отсутствует у допника
|
||||
**Что проверяем:** ветку «осиротевших» документов.
|
||||
**Почему может сломаться:** допник без parent и без own-номера уходит в __unresolved__.
|
||||
**Файлы:** допник-без-родителя.docx
|
||||
**Ключевой текст договора:** "Дополнительное соглашение к договору оказания услуг" (без номеров вообще)
|
||||
**Ожидаем:** документ в группе __unresolved__, не приклеен к случайному договору.
|
||||
|
||||
### Кейс 17: Допник ссылается на own_number, а не parent
|
||||
**Что проверяем:** ветку матчинга «parent==c_norm ИЛИ own==c_norm».
|
||||
**Почему может сломаться:** если LLM записал номер базового договора в own_number допника (а parent=null), группировка всё равно должна склеить.
|
||||
**Файлы:** договор.docx (own="GR-50") + допник.docx (own="GR-50", parent=null)
|
||||
**Ожидаем:** одна группа (срабатывает ветка own==own).
|
||||
|
||||
### Кейс 18: Два РАЗНЫХ договора с одинаковым нормализованным номером
|
||||
**Что проверяем:** коллизию якорей групп.
|
||||
**Почему может сломаться:** «AB-12» и «A-B12» → оба AB12; допник приклеится не к тому/к обоим.
|
||||
**Файлы:** договор-A.docx (own="AB-12") + договор-B.docx (own="A-B12") + допник.docx (parent="AB12")
|
||||
**Ожидаем:** видно недетерминированность/двойную привязку — кейс ловит коллизии нормализации.
|
||||
|
||||
### Кейс 19: Семья из 4 документов, разный порядок дат
|
||||
**Что проверяем:** сортировку внутри группы по doc_date и метку initial/additional.
|
||||
**Почему может сломаться:** если даты парсятся криво, «initial» может стать не самый ранний документ.
|
||||
**Файлы:** договор(2025-01-10) + допник-2(2025-05-01) + спека(2025-02-01) + допник-1(2025-03-01)
|
||||
**Ожидаем:** порядок initial=договор, далее по возрастанию даты; type первого = initial.
|
||||
|
||||
### Кейс 20: Допник с лишним суффиксом-копией в имени файла
|
||||
**Что проверяем:** что нормализуется НОМЕР, а не имя файла.
|
||||
**Почему может сломаться:** имя «допник-1-XXX003-01300_2.docx» содержит _2 (копия), это не должно влиять на own_number/parent.
|
||||
**Файлы:** допник-1-XXX003-01300_2.docx
|
||||
**Ключевой текст договора:** "Дополнительное соглашение № 1 к Договору № XXX003-01300"
|
||||
**Ожидаем:** own_number="1", parent_number="XXX003-01300"; _2 игнорируется.
|
||||
|
||||
### Кейс 21: Цена изменилась на 1 копейку
|
||||
**Что проверяем:** чувствительность UPDATE к микроизменению.
|
||||
**Почему может сломаться:** LLM сочтёт разницу «несущественной» и не выдаст UPDATE; или округлит.
|
||||
**Файлы:** спека-v1.docx + допник-цена.docx
|
||||
**Таблица спеки (current):** Аренда стойко-места | 50000.00 | 1 | 50000.00 | 2025-01-01
|
||||
**Текст допника:** "С 01.03.2025 стоимость аренды устанавливается 50 000,01 руб."
|
||||
**Ожидаем:** UPDATE r1 new_values={price:50000.01, sum:50000.01, date_start:"2025-03-01"}.
|
||||
|
||||
### Кейс 22: Объём с 3 на 0 — это UPDATE или DELETE?
|
||||
**Что проверяем:** трактовку «количество стало нулём».
|
||||
**Почему может сломаться:** граница UPDATE(qty=0) vs DELETE; разные модели решают по-разному.
|
||||
**Файлы:** спека-v1.docx + допник-обнуление.docx
|
||||
**Таблица спеки (current):** IP-адрес IPv4 | 300 | 3 | 900 | 2025-01-01
|
||||
**Текст допника:** "С 01.04.2025 услуга предоставления IP-адресов исключается (количество — 0)."
|
||||
**Ожидаем (фиксируем решение):** один из {DELETE r1} ИЛИ {UPDATE r1 qty=0,sum=0}. Кейс закрепляет ожидаемую трактовку и ловит непостоянство.
|
||||
|
||||
### Кейс 23: Услуга переименована, суть та же
|
||||
**Что проверяем:** семантический матч UPDATE по смыслу, а не по символам.
|
||||
**Почему может сломаться:** LLM не свяжет «Аренда стойко-места» и «Размещение оборудования в стойке» → выдаст ADD+DELETE вместо UPDATE.
|
||||
**Файлы:** спека-v1.docx + допник-переименование.docx
|
||||
**Таблица спеки (current):** Аренда стойко-места | 50000 | 1 | 50000 | 2025-01-01
|
||||
**Текст допника:** "Услугу «Размещение оборудования в стойке» с 01.05.2025 — 52 000 руб."
|
||||
**Ожидаем:** UPDATE r1 (а не ADD новой + DELETE старой).
|
||||
|
||||
### Кейс 24: Полная замена приложения (full_replace)
|
||||
**Что проверяем:** триггер mode=full_replace по фразе «изложить в следующей редакции».
|
||||
**Почему может сломаться:** LLM попытается diff'ить построчно (partial) вместо того, чтобы выдать все строки как ADD.
|
||||
**Файлы:** спека-v1.docx + допник-новая-редакция.docx
|
||||
**Таблица спеки (current):** r1 Аренда | 50000 | 1 | 50000; r2 IP | 300 | 8 | 2400
|
||||
**Текст допника:** "Приложение № 1 изложить в следующей редакции:" + новая таблица (Аренда 55000; IP 12 шт; +Резервное копирование 4000).
|
||||
**Ожидаем:** mode=full_replace, ВСЕ строки новой редакции как ADD, без UPDATE/DELETE.
|
||||
|
||||
### Кейс 25: Добавление новой услуги (чистый ADD)
|
||||
**Что проверяем:** распознавание строки, которой не было.
|
||||
**Почему может сломаться:** LLM попробует «прицепить» к похожей существующей через UPDATE.
|
||||
**Файлы:** спека-v1.docx + допник-добавление.docx
|
||||
**Таблица спеки (current):** Аренда | 50000 | 1 | 50000
|
||||
**Текст допника:** "С 01.06.2025 добавить услугу «Резервное копирование 1 ТБ» — 4 000 руб./мес., 1 шт."
|
||||
**Ожидаем:** ADD new_row={name:"Резервное копирование 1 ТБ", price:4000, qty:1, sum:4000, date_start:"2025-06-01"}.
|
||||
|
||||
### Кейс 26: Удаление услуги (чистый DELETE)
|
||||
**Что проверяем:** корректный target_id при удалении.
|
||||
**Почему может сломаться:** LLM удалит не ту строку (перепутает r1/r2) или выдаст UNRESOLVED.
|
||||
**Файлы:** спека-v1.docx + допник-удаление.docx
|
||||
**Таблица спеки (current):** r1 Аренда | 50000 | 1 | 50000; r2 Мониторинг | 2000 | 1 | 2000
|
||||
**Текст допника:** "С 01.07.2025 услуга «Мониторинг 24/7» исключается из спецификации."
|
||||
**Ожидаем:** DELETE r2 (именно r2).
|
||||
|
||||
### Кейс 27: Изменение только суммы при тех же цене×объёме (ловушка консистентности)
|
||||
**Что проверяем:** что LLM не «досчитывает» поля, которых нет в допнике.
|
||||
**Почему может сломаться:** допник меняет только qty, а LLM забывает пересчитать sum (или наоборот, лезет в price).
|
||||
**Файлы:** спека-v1.docx + допник-объём.docx
|
||||
**Таблица спеки (current):** IP-адрес | 300 | 8 | 2400 | 2025-01-01
|
||||
**Текст допника:** "Увеличить количество IP-адресов до 12 (с 01.08.2025)."
|
||||
**Ожидаем:** UPDATE r1 new_values={qty:12, sum:3600, date_start:"2025-08-01"} — price НЕ в new_values.
|
||||
|
||||
### Кейс 28: Допник меняет услугу, которой нет в спеке (UNRESOLVED)
|
||||
**Что проверяем:** ветку UNRESOLVED.
|
||||
**Почему может сломаться:** LLM «придумает» ADD вместо честного UNRESOLVED.
|
||||
**Файлы:** спека-v1.docx + допник-призрак.docx
|
||||
**Таблица спеки (current):** Аренда | 50000 | 1 | 50000
|
||||
**Текст допника:** "Стоимость услуги «Услуга миграции данных» снизить на 10%." (такой услуги в спеке нет)
|
||||
**Ожидаем:** UNRESOLVED reason="услуги нет в текущей спецификации".
|
||||
|
||||
### Кейс 29: Числа с пробелами-разделителями и запятой-десятичной
|
||||
**Что проверяем:** парсинг «55 000,00» → 55000.0.
|
||||
**Почему может сломаться:** LLM вернёт строку «55 000,00» или 55.0 (обрежет по запятой).
|
||||
**Файлы:** спека-v1.docx + допник-формат-чисел.docx
|
||||
**Таблица спеки (current):** Аренда | 50000 | 1 | 50000
|
||||
**Текст допника:** "Цена аренды с 01.09.2025 — 55 000,00 руб."
|
||||
**Ожидаем:** UPDATE r1 price=55000.0 (число, не строка).
|
||||
|
||||
### Кейс 30: Болтливый LLM-ответ / JSON в markdown (робастность парсера)
|
||||
**Что проверяем:** устойчивость парсинга на стороне Python (compare и classify).
|
||||
**Почему может сломаться:** ответ обёрнут в ```json ```, есть текст «Вот результат:», висячая запятая.
|
||||
**Файлы:** любой простой допник (UPDATE одной цены) — суть в форме ответа, не в данных.
|
||||
**Текст допника:** "Цена аренды — 51 000 руб. с 01.10.2025."
|
||||
**Ожидаем:** парсер извлекает JSON из markdown-блока и применяет UPDATE r1.
|
||||
|
||||
### Матрица покрытия
|
||||
|
||||
| Аспект | Кейсы |
|
||||
|---|---|
|
||||
| classify: все поля / baseline | 1, 5 |
|
||||
| classify: тип документа (contract/spec/other) | 3, 5, 11 |
|
||||
| classify: own vs parent | 4, 17 |
|
||||
| classify: дата | 7, 8 |
|
||||
| classify: контрагент | 1, 6 |
|
||||
| classify: слепая зона выжимки | 9, 10 |
|
||||
| group: нормализация разделителей | 12, 15 |
|
||||
| group: кириллица/латиница/цифры | 13, 14, 15 |
|
||||
| group: сироты / unresolved | 16 |
|
||||
| group: ветки матча и коллизии | 17, 18 |
|
||||
| group: сортировка/порядок | 19, 20 |
|
||||
| compare: UPDATE | 21, 23, 27, 29 |
|
||||
| compare: ADD / DELETE | 22, 25, 26 |
|
||||
| compare: full_replace | 24 |
|
||||
| compare: UNRESOLVED | 22, 28 |
|
||||
| robustness: JSON-парсинг | 30 |
|
||||
|
||||
**Рекомендация по приоритету:** сначала Блок B (кейсы 13–18) — там код ломается детерминированно и без LLM, баги воспроизводимы на 100%. Потом Блок C (LLM-логика), затем Блок A.
|
||||
Reference in New Issue
Block a user