# Ответ Опуса — 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.