Files
contracts/History/opus-testcases-answer.md
T

286 lines
25 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Ответ Опуса — 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.