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
+285
View File
@@ -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.