# ⛔ НЕ МЕНЯТЬ БЕЗ РАЗРЕШЕНИЯ НАЕЛЯ ⛔ """ gen_opus.py — Генератор тестовых файлов по 30 кейсам Опуса. Блок A: classify (11 кейсов, ~11 файлов) Блок B: group (9 кейсов, ~19 файлов) Блок C: compare (10 кейсов, ~20 файлов) Все документы — полноценные: шапка, преамбула с реквизитами сторон, 2-4 раздела, таблицы услуг (6-10 строк), подписи. """ import os, json, sys sys.path.insert(0, os.path.dirname(__file__)) from docx import Document from docx.shared import Pt from docx.enum.text import WD_ALIGN_PARAGRAPH OUT = os.path.join(os.path.dirname(__file__), "opus_cases") MANIFEST = {} def ensure_dir(path): os.makedirs(path, exist_ok=True) def save(doc, folder, fname, info): path = os.path.join(OUT, folder, fname) ensure_dir(os.path.dirname(path)) doc.save(path) MANIFEST[fname] = info print(f" ✓ {folder}/{fname}") def _add_title(doc, text): p = doc.add_paragraph() p.alignment = WD_ALIGN_PARAGRAPH.CENTER run = p.add_run(text) run.bold = True run.font.size = Pt(14) def _add_para(doc, text): doc.add_paragraph(text) def _add_table(doc, rows_data, headers=None): if headers: rows_data = [headers] + rows_data table = doc.add_table(rows=len(rows_data), cols=len(rows_data[0])) table.style = 'Table Grid' for ri, row in enumerate(rows_data): for ci, val in enumerate(row): table.rows[ri].cells[ci].text = str(val) def _add_signatures(doc): _add_para(doc, "") _add_para(doc, "10. ПОДПИСИ СТОРОН") _add_para(doc, "") _add_para(doc, " ИСПОЛНИТЕЛЬ ЗАКАЗЧИК") _add_para(doc, " ____________________ ____________________") _add_para(doc, " Генеральный директор Генеральный директор") _add_para(doc, " ____________________ ____________________") _add_para(doc, " М.П. М.П.") def _preamble(doc, num, date, party1, party2): """Стандартная преамбула договора.""" _add_para(doc, f'ООО "НУБЕС", именуемое в дальнейшем "Исполнитель", в лице') _add_para(doc, f'Генерального директора Степаненко Василия Сергеевича,') _add_para(doc, f'действующего на основании Устава, с одной стороны, и') _add_para(doc, f'{party1}, именуемое в дальнейшем "Заказчик", в лице') _add_para(doc, f'{party2}, действующего на основании Устава, с другой стороны,') _add_para(doc, f'заключили настоящий Договор о нижеследующем:') def _contract_sections(doc): _add_para(doc, "") _add_para(doc, "1. ПРЕДМЕТ ДОГОВОРА") _add_para(doc, "1.1. Исполнитель обязуется оказывать услуги по предоставлению") _add_para(doc, "вычислительных ресурсов ЦОД, а Заказчик обязуется оплачивать") _add_para(doc, "оказанные услуги в порядке и на условиях настоящего Договора.") _add_para(doc, "1.2. Состав и стоимость Услуг определяются в Спецификации") _add_para(doc, "(Приложение № 1), являющейся неотъемлемой частью Договора.") _add_para(doc, "1.3. Место оказания услуг: ЦОД по адресу: г. Москва, ул. 8 Марта, д. 16, стр. 1.") _add_para(doc, "") _add_para(doc, "2. ПРАВА И ОБЯЗАННОСТИ СТОРОН") _add_para(doc, "2.1. Исполнитель обязуется: обеспечить круглосуточный доступ;") _add_para(doc, "поддерживать температурный режим и электропитание; осуществлять охрану.") _add_para(doc, "2.2. Заказчик обязуется: своевременно оплачивать услуги;") _add_para(doc, "соблюдать правила техники безопасности при посещении ЦОД.") _add_para(doc, "") _add_para(doc, "3. СТОИМОСТЬ УСЛУГ И ПОРЯДОК РАСЧЁТОВ") _add_para(doc, "3.1. Стоимость услуг определяется Спецификацией (Приложение № 1).") _add_para(doc, "3.2. Оплата производится ежемесячно до 10 числа месяца,") _add_para(doc, "следующего за отчётным, на основании счёта и акта.") _add_para(doc, "3.3. НДС не облагается (УСН).") _add_para(doc, "") _add_para(doc, "4. СРОК ДЕЙСТВИЯ ДОГОВОРА") _add_para(doc, "4.1. Договор вступает в силу с даты подписания") _add_para(doc, "и действует до 31 декабря текущего года.") _add_para(doc, "4.2. Автоматическая пролонгация на следующий год при отсутствии") _add_para(doc, "письменного уведомления о расторжении за 30 дней.") # --- Базовые спецификации --- BASE8 = [ ["1", "Аренда стойко-места 42U", "45 000,00", "2", "90 000,00", "01.01.2025"], ["2", "Блок розеток PDU 3Ф 32A", "3 200,00", "2", "6 400,00", "01.01.2025"], ["3", "IP-адрес IPv4 (1 шт.)", "350,00", "8", "2 800,00", "01.01.2025"], ["4", "Резервное копирование 100 ГБ", "1 500,00", "1", "1 500,00", "01.01.2025"], ["5", "Мониторинг 24/7 базовый", "2 000,00", "1", "2 000,00", "01.01.2025"], ["6", "Доступ в интернет 100 Мбит/с", "5 000,00", "1", "5 000,00", "01.01.2025"], ["7", "Кросс-соединение медное", "1 200,00", "2", "2 400,00", "01.01.2025"], ["8", "Аренда полки 19\"", "8 500,00", "1", "8 500,00", "01.01.2025"], ] def _gen_contract_spec(num, date): """Сгенерировать спецификацию к договору (8 строк).""" return _make_spec(num, SPEC8, date) def _make_contract(num, date, counterparty_full, counterparty_person, modify_preamble=None, hide_counterparty=False): """Полноценный договор.""" doc = Document() _add_title(doc, "ДОГОВОР") _add_para(doc, f"на оказание технологических услуг № {num}") _add_para(doc, "") _add_para(doc, f"г. Москва {date}") _add_para(doc, "") if hide_counterparty: _add_para(doc, "Настоящий Договор регулирует отношения Сторон по оказанию услуг ЦОД.") elif modify_preamble: modify_preamble(doc, num, date) else: _preamble(doc, num, date, counterparty_full, counterparty_person) _contract_sections(doc) _add_signatures(doc) return doc def _make_supplement(parent_num, text, parent_date=None): """Допсоглашение.""" doc = Document() _add_title(doc, "ДОПОЛНИТЕЛЬНОЕ СОГЛАШЕНИЕ") _add_para(doc, f"к Договору № {parent_num}" + (f" от {parent_date}" if parent_date else "")) _add_para(doc, "") _add_para(doc, 'ООО "НУБЕС" (Исполнитель) и Заказчик, именуемые совместно "Стороны",') _add_para(doc, 'заключили настоящее Дополнительное соглашение о нижеследующем:') _add_para(doc, "") _add_para(doc, text) _add_para(doc, "") _add_para(doc, "Настоящее Соглашение вступает в силу с даты подписания Сторонами") _add_para(doc, "и является неотъемлемой частью Договора.") _add_signatures(doc) return doc def _make_spec(parent_num, rows, parent_date=None): """Спецификация с таблицей.""" doc = Document() _add_title(doc, "Приложение № 1") _add_para(doc, f"к Договору № {parent_num}" + (f" от {parent_date}" if parent_date else "")) _add_para(doc, "Спецификация и стоимость услуг") _add_para(doc, "") _add_table(doc, rows, ["№", "Наименование", "Цена, руб.", "Объем", "Сумма, руб.", "Дата начала"]) _add_signatures(doc) return doc # ОБЩАЯ спецификация для всех договоров A/B SPEC8 = [ ["1", "Аренда стойко-места 42U", "45 000,00", "2", "90 000,00", "01.01.2025"], ["2", "Блок розеток PDU 3Ф 32A", "3 200,00", "2", "6 400,00", "01.01.2025"], ["3", "IP-адрес IPv4 (1 шт.)", "350,00", "8", "2 800,00", "01.01.2025"], ["4", "Резервное копирование 100 ГБ", "1 500,00", "1", "1 500,00", "01.01.2025"], ["5", "Мониторинг 24/7 базовый", "2 000,00", "1", "2 000,00", "01.01.2025"], ["6", "Доступ в интернет 100 Мбит/с", "5 000,00", "1", "5 000,00", "01.01.2025"], ["7", "Кросс-соединение медное", "1 200,00", "2", "2 400,00", "01.01.2025"], ["8", "Аренда полки 19\"", "8 500,00", "1", "8 500,00", "01.01.2025"], ] # ═══════════════════════════════════════════════════════════════ # БЛОК A — Classify (11 кейсов) # ═══════════════════════════════════════════════════════════════ def case1_baseline(): doc = _make_contract("XXX001-03700", "15 марта 2025 г.", 'ЗАО "ТехноПлюс"', 'Генерального директора ФИО') save(doc, "block_a", "договор-XXX001-03700.docx", {"case": 1, "type": "contract", "own_number": "XXX001-03700", "counterparty": 'ЗАО "ТехноПлюс"'}) save(_gen_contract_spec("XXX001-03700", "15.03.2025"), "block_a", "спецификация-XXX001-03700_v1.docx", {"case": 1, "type": "specification", "parent_number": "XXX001-03700"}) def case2_cyrillic(): doc = _make_contract("МЭС-123/2024", "10 января 2024 г.", 'АО "МосЭнергоСбыт"', 'Генерального директора Петрова А.В.') save(doc, "block_a", "договор-МЭС.docx", {"case": 2, "type": "contract", "own_number": "МЭС-123/2024", "counterparty": 'АО "МосЭнергоСбыт"'}) save(_gen_contract_spec("МЭС-123/2024", "10.01.2024"), "block_a", "спецификация-МЭС.docx", {"case": 2, "type": "specification", "parent_number": "МЭС-123/2024"}) def case3_nonstandard(): doc = Document() _add_title(doc, "СОГЛАШЕНИЕ об оказании услуг связи") _add_para(doc, "№ SVC-77 от 01.02.2025") _add_para(doc, "") _add_para(doc, 'ООО "НУБЕС" и ЗАО "Клиент-Сервис" заключили настоящее Соглашение.') _add_signatures(doc) save(doc, "block_a", "договор-нестандарт.docx", {"case": 3, "type": "contract", "own_number": "SVC-77", "counterparty": 'ЗАО "Клиент-Сервис"'}) save(_gen_contract_spec("SVC-77", "01.02.2025"), "block_a", "спецификация-SVC-77.docx", {"case": 3, "type": "specification", "parent_number": "SVC-77"}) def case4_addendum(): doc = _make_supplement("XXX003-01300", "Изменить Спецификацию. Утвердить новую редакцию Приложения № 1.", "05.06.2024") save(doc, "block_a", "допник-XXX003-01300.docx", {"case": 4, "type": "supplement", "own_number": "1", "parent_number": "XXX003-01300"}) def case5_spec(): """Кейс 5: Спецификация (генерируется в case1, здесь — заглушка для нумерации).""" pass def case6_no_counterparty(): doc = _make_contract("NC-09", "03 марта 2025 г.", "", "", hide_counterparty=True) save(doc, "block_a", "договор-без-стороны.docx", {"case": 6, "type": "contract", "counterparty": None}) save(_gen_contract_spec("NC-09", "03.03.2025"), "block_a", "спецификация-NC-09.docx", {"case": 6, "type": "specification", "parent_number": "NC-09"}) def case7_date_text(): doc = _make_contract("DT-15", '«пятнадцатого» марта 2025 года', 'ООО "Ресурс-ЦОД"', 'Генерального директора Сидорова С.С.') save(doc, "block_a", "договор-дата-прописью.docx", {"case": 7, "type": "contract", "doc_date": "2025-03-15", "counterparty": 'ООО "Ресурс-ЦОД"'}) save(_gen_contract_spec("DT-15", "15.03.2025"), "block_a", "спецификация-DT-15.docx", {"case": 7, "type": "specification", "parent_number": "DT-15"}) def case8_two_dates(): doc = _make_contract("TD-21", "01 апреля 2025 г.", 'АО "ТехноДата"', 'Генерального директора Козлова Д.Д.') # добавить дату окончания _add_para(doc, "4.3. Договор действует до 31.12.2026.") save(doc, "block_a", "договор-две-даты.docx", {"case": 8, "type": "contract", "doc_date": "2025-04-01", "counterparty": 'АО "ТехноДата"'}) save(_gen_contract_spec("TD-21", "01.04.2025"), "block_a", "спецификация-TD-21.docx", {"case": 8, "type": "specification", "parent_number": "TD-21"}) def case9_long_preamble(): doc = Document() _add_title(doc, "ДОГОВОР") _add_para(doc, "на оказание технологических услуг № LATE-99") _add_para(doc, "") lorem = ("Настоящий Договор разработан в соответствии с Гражданским кодексом РФ, " "Федеральным законом «О связи», Федеральным законом «Об информации», " "а также иными нормативными правовыми актами Российской Федерации. ") * 8 _add_para(doc, lorem[:1600]) _add_para(doc, "") _add_para(doc, 'ООО "Поздний Контрагент" и ООО "НУБЕС" заключили Договор.') _contract_sections(doc) _add_signatures(doc) save(doc, "block_a", "договор-длинная-преамбула.docx", {"case": 9, "type": "contract", "own_number": "LATE-99", "counterparty": 'ООО "Поздний Контрагент"'}) save(_gen_contract_spec("LATE-99", "15.06.2025"), "block_a", "спецификация-LATE-99.docx", {"case": 9, "type": "specification", "parent_number": "LATE-99"}) def case10_noisy(): doc = Document() _add_title(doc, "ДОГОВОР") _add_para(doc, "Исх. № 456 от 12.05.2025") _add_para(doc, "Лицензия № 789 от 10.01.2025") _add_para(doc, "Счёт № 321 от 15.05.2025") _add_para(doc, "") _add_para(doc, 'ООО "НУБЕС" и ЗАО "МультиНомер" заключили настоящий Договор.') _add_para(doc, "ДОГОВОР № MN-01 от 12.05.2025") _contract_sections(doc) _add_signatures(doc) save(doc, "block_a", "договор-много-номеров.docx", {"case": 10, "type": "contract", "own_number": "MN-01", "counterparty": 'ЗАО "МультиНомер"'}) save(_gen_contract_spec("MN-01", "12.05.2025"), "block_a", "спецификация-MN-01.docx", {"case": 10, "type": "specification", "parent_number": "MN-01"}) def case11_trash(): doc = Document() _add_title(doc, "Акт сверки взаимных расчётов") _add_para(doc, "за 1 квартал 2025 г.") _add_para(doc, "") _add_para(doc, "Стороны: ООО «НУБЕС» и ООО «Контрагент»") _add_para(doc, "Сальдо на 01.01.2025: 150 000,00 руб.") _add_para(doc, "Оборот за период: 45 000,00 руб.") _add_para(doc, "Сальдо на 31.03.2025: 195 000,00 руб.") _add_signatures(doc) save(doc, "block_a", "акт-сверки.docx", {"case": 11, "type": "other"}) # ═══════════════════════════════════════════════════════════════ # review case line numbers before running! print("hello gen_opus") # ═══════════════════════════════════════════════════════════════ # БЛОК B — Group (9 кейсов, ~19 файлов) # ═══════════════════════════════════════════════════════════════ def case12_normalize(): """Кейс 12: Разделители нормализуются""" d1 = _make_contract("МЭС-123/2024", "10 января 2024 г.", 'АО "МосЭнергоСбыт"', 'Генерального директора Петрова А.В.') save(d1, "block_b", "договор-МЭС-норм.docx", {"case": 12, "own": "МЭС-123/2024", "counterparty": 'АО "МосЭнергоСбыт"'}) save(_gen_contract_spec("МЭС-123/2024", "10.01.2024"), "block_b", "спецификация-МЭС-норм.docx", {"case": 12, "type": "specification", "parent_number": "МЭС-123/2024"}) d2 = _make_supplement("МЭС 123/2024", "Изменить стоимость услуг с 01.02.2024.", "10.01.2024") save(d2, "block_b", "допник-МЭС-норм.docx", {"case": 12, "parent": "МЭС 123/2024"}) def case13_cyrillic_O(): d1 = _make_contract("XXX001-03700", "01 января 2025 г.", 'ЗАО "ТехноПлюс"', 'ФИО') save(d1, "block_b", "договор-03700.docx", {"case": 13, "own": "XXX001-03700", "counterparty": 'ЗАО "ТехноПлюс"'}) save(_gen_contract_spec("XXX001-03700", "01.01.2025"), "block_b", "спецификация-03700.docx", {"case": 13, "type": "specification", "parent_number": "XXX001-03700"}) d2 = _make_supplement("XXX001-О3700", "Увеличить количество стоек до 3.") # кириллическая О save(d2, "block_b", "допник-О3700.docx", {"case": 13, "parent": "XXX001-О3700"}) def case14_latin_C(): d1 = _make_contract("CBC-10", "01 февраля 2025 г.", 'ООО "СиБиСи"', 'Генерального директора') save(d1, "block_b", "договор-CBC.docx", {"case": 14, "own": "CBC-10", "counterparty": 'ООО "СиБиСи"'}) save(_gen_contract_spec("CBC-10", "01.02.2025"), "block_b", "спецификация-CBC.docx", {"case": 14, "type": "specification", "parent_number": "CBC-10"}) d2 = _make_supplement("СВС-10", "Изменить тарифы.") # кириллическая С save(d2, "block_b", "допник-СВС.docx", {"case": 14, "parent": "СВС-10"}) def case15_yo(): d1 = _make_contract("ЁЖ-5", "01 марта 2025 г.", 'ООО "Ёжик-Телеком"', 'Генерального директора') save(d1, "block_b", "договор-ЁЖ.docx", {"case": 15, "own": "ЁЖ-5", "counterparty": 'ООО "Ёжик-Телеком"'}) save(_gen_contract_spec("ЁЖ-5", "01.03.2025"), "block_b", "спецификация-ЁЖ.docx", {"case": 15, "type": "specification", "parent_number": "ЁЖ-5"}) d2 = _make_supplement("ЁЖ-5", "Добавить услугу резервного копирования.") save(d2, "block_b", "допник-ЁЖ.docx", {"case": 15, "parent": "ЁЖ-5"}) def case16_no_parent(): doc = _make_supplement("договора оказания услуг", "Стороны договорились об изменении стоимости услуг.") save(doc, "block_b", "допник-без-родителя.docx", {"case": 16, "type": "supplement", "parent": None}) def case17_own_match(): d1 = _make_contract("GR-50", "01 апреля 2025 г.", 'ООО "ГринРайт"', 'Генерального директора') save(d1, "block_b", "договор-GR50.docx", {"case": 17, "own": "GR-50", "counterparty": 'ООО "ГринРайт"'}) save(_gen_contract_spec("GR-50", "01.04.2025"), "block_b", "спецификация-GR50.docx", {"case": 17, "type": "specification", "parent_number": "GR-50"}) d2 = _make_supplement("GR-50", "Изменить спецификацию с 15.04.2025.") # own=GR-50 save(d2, "block_b", "допник-GR50.docx", {"case": 17, "own": "GR-50"}) def case18_collision(): d1 = _make_contract("AB-12", "01 мая 2025 г.", 'ООО "Альфа-Бета"', 'Генерального директора') save(d1, "block_b", "договор-AB12.docx", {"case": 18, "own": "AB-12", "counterparty": 'ООО "Альфа-Бета"'}) save(_gen_contract_spec("AB-12", "01.05.2025"), "block_b", "спецификация-AB12.docx", {"case": 18, "type": "specification", "parent_number": "AB-12"}) d2 = _make_contract("A-B12", "01 мая 2025 г.", 'ООО "А-Бет"', 'Генерального директора') save(d2, "block_b", "договор-A-B12.docx", {"case": 18, "own": "A-B12", "counterparty": 'ООО "А-Бет"'}) save(_gen_contract_spec("A-B12", "01.05.2025"), "block_b", "спецификация-A-B12.docx", {"case": 18, "type": "specification", "parent_number": "A-B12"}) d3 = _make_supplement("AB12", "Изменить условия.") save(d3, "block_b", "допник-AB12.docx", {"case": 18, "parent": "AB12"}) def case19_family(): d1 = _make_contract("FAM-01", "10 января 2025 г.", 'ЗАО "Фэмили-Групп"', 'Генерального директора') save(d1, "block_b", "договор-FAM.docx", {"case": 19, "date": "2025-01-10", "counterparty": 'ЗАО "Фэмили-Групп"'}) d2 = _make_spec("FAM-01", SPEC8, "01.02.2025") save(d2, "block_b", "спека-FAM.docx", {"case": 19, "date": "2025-02-01"}) d3 = _make_supplement("FAM-01", "Дополнительное соглашение № 1. Увеличить количество стоек до 3.") save(d3, "block_b", "допник-1-FAM.docx", {"case": 19, "date": "2025-03-01"}) d4 = _make_supplement("FAM-01", "Дополнительное соглашение № 2. Добавить услугу резервного копирования.") save(d4, "block_b", "допник-2-FAM.docx", {"case": 19, "date": "2025-05-01"}) def case20_suffix(): doc = _make_supplement("XXX003-01300", "Изменить спецификацию. Утвердить новую редакцию.") save(doc, "block_b", "допник-1-XXX003-01300_2.docx", {"case": 20, "own": "1", "parent": "XXX003-01300"}) # ═══════════════════════════════════════════════════════════════ # БЛОК C — Compare (10 кейсов, ~20 файлов, 6-10 строк) # ═══════════════════════════════════════════════════════════════ def _compare_spec_v1(parent_num, rows): """Спецификация v1 для compare-кейса.""" return _make_spec(parent_num, rows) def _compare_spec_v2(parent_num, rows): """Изменённая спецификация для compare-кейса (подаётся как допник с таблицей).""" doc = Document() _add_title(doc, "Приложение № 1 (новая редакция)") _add_para(doc, f"к Дополнительному соглашению к Договору № {parent_num}") _add_para(doc, "Спецификация и стоимость услуг") _add_para(doc, "") _add_table(doc, rows, ["№", "Наименование", "Цена, руб.", "Объем", "Сумма, руб.", "Дата начала"]) _add_signatures(doc) return doc # ---- Базовая спецификация 8 строк для ВСЕХ compare-кейсов ---- # (одинаковая v1, меняется только v2 по кейсу) SPEC_V1 = [ ["1", "Аренда стойко-места 42U", "45 000,00", "2", "90 000,00", "01.01.2025"], ["2", "Блок розеток PDU 3Ф 32A", "3 200,00", "2", "6 400,00", "01.01.2025"], ["3", "IP-адрес IPv4 (1 шт.)", "350,00", "8", "2 800,00", "01.01.2025"], ["4", "Резервное копирование 100 ГБ", "1 500,00", "1", "1 500,00", "01.01.2025"], ["5", "Мониторинг 24/7 базовый", "2 000,00", "1", "2 000,00", "01.01.2025"], ["6", "Доступ в интернет 100 Мбит/с", "5 000,00", "1", "5 000,00", "01.01.2025"], ["7", "Кросс-соединение медное", "1 200,00", "2", "2 400,00", "01.01.2025"], ["8", "Аренда полки 19\"", "8 500,00", "1", "8 500,00", "01.01.2025"], ] # CASE 21: цена +1 копейка → UPDATE (1 строка) SPEC21_V2 = [ ["1", "Аренда стойко-места 42U", "45 000,01", "2", "90 000,02", "01.01.2025"], ["2", "Блок розеток PDU 3Ф 32A", "3 200,00", "2", "6 400,00", "01.01.2025"], ["3", "IP-адрес IPv4 (1 шт.)", "350,00", "8", "2 800,00", "01.01.2025"], ["4", "Резервное копирование 100 ГБ", "1 500,00", "1", "1 500,00", "01.01.2025"], ["5", "Мониторинг 24/7 базовый", "2 000,00", "1", "2 000,00", "01.01.2025"], ["6", "Доступ в интернет 100 Мбит/с", "5 000,00", "1", "5 000,00", "01.01.2025"], ["7", "Кросс-соединение медное", "1 200,00", "2", "2 400,00", "01.01.2025"], ["8", "Аренда полки 19\"", "8 500,00", "1", "8 500,00", "01.01.2025"], ] # CASE 22: объём → 0 для услуги 3 (DELETE) и цена меняется для услуги 3 (UPDATE) SPEC22_V2 = [ ["1", "Аренда стойко-места 42U", "45 000,00", "2", "90 000,00", "01.01.2025"], ["2", "Блок розеток PDU 3Ф 32A", "3 200,00", "2", "6 400,00", "01.01.2025"], # ["3", "IP-адрес IPv4 (1 шт.)"] — DELETE ["4", "Резервное копирование 100 ГБ", "1 500,00", "1", "1 500,00", "01.01.2025"], ["5", "Мониторинг 24/7 базовый", "2 000,00", "1", "2 000,00", "01.01.2025"], ["6", "Доступ в интернет 100 Мбит/с", "5 000,00", "1", "5 000,00", "01.01.2025"], ["7", "Кросс-соединение медное", "1 200,00", "2", "2 400,00", "01.01.2025"], ["8", "Аренда полки 19\"", "8 500,00", "1", "8 500,00", "01.01.2025"], ] # CASE 23: переименование строки 5 SPEC23_V2 = [ ["1", "Аренда стойко-места 42U", "45 000,00", "2", "90 000,00", "01.01.2025"], ["2", "Блок розеток PDU 3Ф 32A", "3 200,00", "2", "6 400,00", "01.01.2025"], ["3", "IP-адрес IPv4 (1 шт.)", "350,00", "8", "2 800,00", "01.01.2025"], ["4", "Резервное копирование 100 ГБ", "1 500,00", "1", "1 500,00", "01.01.2025"], ["5", "Мониторинг 24/7 расширенный", "3 000,00", "1", "3 000,00", "01.05.2025"], ["6", "Доступ в интернет 100 Мбит/с", "5 000,00", "1", "5 000,00", "01.01.2025"], ["7", "Кросс-соединение медное", "1 200,00", "2", "2 400,00", "01.01.2025"], ["8", "Аренда полки 19\"", "8 500,00", "1", "8 500,00", "01.01.2025"], ] # CASE 24: полная замена — 3 строки совсем другие SPEC24_V2 = [ ["1", "Выделенный сервер Dell R740xd", "85 000,00", "1", "85 000,00", "01.05.2025"], ["2", "Лицензия VMware vSphere 8", "12 000,00", "2", "24 000,00", "01.05.2025"], ["3", "Резервное копирование 1 ТБ", "4 000,00", "1", "4 000,00", "01.05.2025"], ] # CASE 25: ADD — +2 строки SPEC25_V2 = [ ["1", "Аренда стойко-места 42U", "45 000,00", "2", "90 000,00", "01.01.2025"], ["2", "Блок розеток PDU 3Ф 32A", "3 200,00", "2", "6 400,00", "01.01.2025"], ["3", "IP-адрес IPv4 (1 шт.)", "350,00", "8", "2 800,00", "01.01.2025"], ["4", "Резервное копирование 100 ГБ", "1 500,00", "1", "1 500,00", "01.01.2025"], ["5", "Мониторинг 24/7 базовый", "2 000,00", "1", "2 000,00", "01.01.2025"], ["6", "Доступ в интернет 100 Мбит/с", "5 000,00", "1", "5 000,00", "01.01.2025"], ["7", "Кросс-соединение медное", "1 200,00", "2", "2 400,00", "01.01.2025"], ["8", "Аренда полки 19\"", "8 500,00", "1", "8 500,00", "01.01.2025"], # ADD: ["9", "Резервное копирование 1 ТБ", "4 000,00", "1", "4 000,00", "01.06.2025"], ["10", "DDoS-защита 10 Гбит/с", "15 000,00", "1", "15 000,00", "01.06.2025"], ] # CASE 26: DELETE — без пункта 7 и 8 SPEC26_V2 = [ ["1", "Аренда стойко-места 42U", "45 000,00", "2", "90 000,00", "01.01.2025"], ["2", "Блок розеток PDU 3Ф 32A", "3 200,00", "2", "6 400,00", "01.01.2025"], ["3", "IP-адрес IPv4 (1 шт.)", "350,00", "8", "2 800,00", "01.01.2025"], ["4", "Резервное копирование 100 ГБ", "1 500,00", "1", "1 500,00", "01.01.2025"], ["5", "Мониторинг 24/7 базовый", "2 000,00", "1", "2 000,00", "01.01.2025"], ["6", "Доступ в интернет 100 Мбит/с", "5 000,00", "1", "5 000,00", "01.01.2025"], # ["7", "Кросс-соединение медное"] — DELETE # ["8", "Аренда полки 19\""] — DELETE ] # CASE 27: только объём — 3→8 для услуги 3 SPEC27_V2 = [ ["1", "Аренда стойко-места 42U", "45 000,00", "2", "90 000,00", "01.01.2025"], ["2", "Блок розеток PDU 3Ф 32A", "3 200,00", "2", "6 400,00", "01.01.2025"], ["3", "IP-адрес IPv4 (1 шт.)", "350,00", "12", "4 200,00", "01.08.2025"], ["4", "Резервное копирование 100 ГБ", "1 500,00", "1", "1 500,00", "01.01.2025"], ["5", "Мониторинг 24/7 базовый", "2 000,00", "1", "2 000,00", "01.01.2025"], ["6", "Доступ в интернет 100 Мбит/с", "5 000,00", "1", "5 000,00", "01.01.2025"], ["7", "Кросс-соединение медное", "1 200,00", "2", "2 400,00", "01.01.2025"], ["8", "Аренда полки 19\"", "8 500,00", "1", "8 500,00", "01.01.2025"], ] # CASE 28: UNRESOLVED — услуга-призрак (не совпадает ни с одной v1) SPEC28_V2 = [ ["1", "Аренда стойко-места 42U", "45 000,00", "2", "90 000,00", "01.01.2025"], ["2", "Блок розеток PDU 3Ф 32A", "3 200,00", "2", "6 400,00", "01.01.2025"], ["3", "IP-адрес IPv4 (1 шт.)", "350,00", "8", "2 800,00", "01.01.2025"], ["4", "Резервное копирование 100 ГБ", "1 500,00", "1", "1 500,00", "01.01.2025"], ["5", "Мониторинг 24/7 базовый", "2 000,00", "1", "2 000,00", "01.01.2025"], ["6", "Доступ в интернет 100 Мбит/с", "5 000,00", "1", "5 000,00", "01.01.2025"], ["7", "Кросс-соединение медное", "1 200,00", "2", "2 400,00", "01.01.2025"], ["8", "Аренда полки 19\"", "8 500,00", "1", "8 500,00", "01.01.2025"], # ADD — услуга-призрак (нет в v1) ["9", "VPN-туннель site-to-site", "3 000,00", "1", "3 000,00", "01.03.2025"], ] # CASE 29: формат чисел (пробелы) SPEC29_V2 = [ ["1", "Аренда стойко-места 42U", "45 500,00", "2", "91 000,00", "01.01.2025"], ["2", "Блок розеток PDU 3Ф 32A", "3 200,00", "2", "6 400,00", "01.01.2025"], ["3", "IP-адрес IPv4 (1 шт.)", "350,00", "8", "2 800,00", "01.01.2025"], ["4", "Резервное копирование 100 ГБ", "1 500,00", "1", "1 500,00", "01.01.2025"], ["5", "Мониторинг 24/7 базовый", "2 000,00", "1", "2 000,00", "01.01.2025"], ["6", "Доступ в интернет 100 Мбит/с", "5 000,00", "1", "5 000,00", "01.01.2025"], ["7", "Кросс-соединение медное", "1 200,00", "2", "2 400,00", "01.01.2025"], ["8", "Аренда полки 19\"", "8 500,00", "1", "8 500,00", "01.01.2025"], ] # CASE 30: робастность JSON — та же specs что и 21 SPEC30_V2 = [ ["1", "Аренда стойко-места 42U", "51 000,00", "2", "102 000,00", "01.10.2025"], ["2", "Блок розеток PDU 3Ф 32A", "3 200,00", "2", "6 400,00", "01.01.2025"], ["3", "IP-адрес IPv4 (1 шт.)", "350,00", "8", "2 800,00", "01.01.2025"], ["4", "Резервное копирование 100 ГБ", "1 500,00", "1", "1 500,00", "01.01.2025"], ["5", "Мониторинг 24/7 базовый", "2 000,00", "1", "2 000,00", "01.01.2025"], ["6", "Доступ в интернет 100 Мбит/с", "5 000,00", "1", "5 000,00", "01.01.2025"], ["7", "Кросс-соединение медное", "1 200,00", "2", "2 400,00", "01.01.2025"], ["8", "Аренда полки 19\"", "8 500,00", "1", "8 500,00", "01.01.2025"], ] def case21_kopeck(): save(_compare_spec_v1("CMP-021", SPEC_V1), "block_c", "спека-цена-v1.docx", {"case": 21, "version": 1, "parent_number": "CMP-021"}) save(_compare_spec_v2("CMP-021", SPEC21_V2), "block_c", "допник-цена.docx", {"case": 21, "type": "supplement", "parent_number": "CMP-021"}) def case22_zero_qty(): save(_compare_spec_v1("CMP-022", SPEC_V1), "block_c", "спека-обнуление-v1.docx", {"case": 22, "version": 1, "parent_number": "CMP-022"}) save(_compare_spec_v2("CMP-022", SPEC22_V2), "block_c", "допник-обнуление.docx", {"case": 22, "type": "supplement", "parent_number": "CMP-022"}) def case23_rename(): save(_compare_spec_v1("CMP-023", SPEC_V1), "block_c", "спека-переименование-v1.docx", {"case": 23, "version": 1, "parent_number": "CMP-023"}) save(_compare_spec_v2("CMP-023", SPEC23_V2), "block_c", "допник-переименование.docx", {"case": 23, "type": "supplement", "parent_number": "CMP-023"}) def case24_full_replace(): save(_compare_spec_v1("CMP-024", SPEC_V1), "block_c", "спека-полная-v1.docx", {"case": 24, "version": 1, "parent_number": "CMP-024"}) save(_compare_spec_v2("CMP-024", SPEC24_V2), "block_c", "допник-новая-редакция.docx", {"case": 24, "type": "supplement", "parent_number": "CMP-024"}) def case25_add(): save(_compare_spec_v1("CMP-025", SPEC_V1), "block_c", "спека-добавление-v1.docx", {"case": 25, "version": 1, "parent_number": "CMP-025"}) save(_compare_spec_v2("CMP-025", SPEC25_V2), "block_c", "допник-добавление.docx", {"case": 25, "type": "supplement", "parent_number": "CMP-025"}) def case26_delete(): save(_compare_spec_v1("CMP-026", SPEC_V1), "block_c", "спека-удаление-v1.docx", {"case": 26, "version": 1, "parent_number": "CMP-026"}) save(_compare_spec_v2("CMP-026", SPEC26_V2), "block_c", "допник-удаление.docx", {"case": 26, "type": "supplement", "parent_number": "CMP-026"}) def case27_qty_only(): save(_compare_spec_v1("CMP-027", SPEC_V1), "block_c", "спека-объём-v1.docx", {"case": 27, "version": 1, "parent_number": "CMP-027"}) save(_compare_spec_v2("CMP-027", SPEC27_V2), "block_c", "допник-объём.docx", {"case": 27, "type": "supplement", "parent_number": "CMP-027"}) def case28_unresolved(): save(_compare_spec_v1("CMP-028", SPEC_V1), "block_c", "спека-призрак-v1.docx", {"case": 28, "version": 1, "parent_number": "CMP-028"}) save(_compare_spec_v2("CMP-028", SPEC28_V2), "block_c", "допник-призрак.docx", {"case": 28, "type": "supplement", "parent_number": "CMP-028"}) def case29_format(): save(_compare_spec_v1("CMP-029", SPEC_V1), "block_c", "спека-формат-v1.docx", {"case": 29, "version": 1, "parent_number": "CMP-029"}) save(_compare_spec_v2("CMP-029", SPEC29_V2), "block_c", "допник-формат-чисел.docx", {"case": 29, "type": "supplement", "parent_number": "CMP-029"}) def case30_robust(): save(_compare_spec_v1("CMP-030", SPEC_V1), "block_c", "спека-роб-v1.docx", {"case": 30, "version": 1, "parent_number": "CMP-030"}) save(_compare_spec_v2("CMP-030", SPEC30_V2), "block_c", "допник-роб.docx", {"case": 30, "type": "supplement", "parent_number": "CMP-030"}) # ═══════════════════════════════════════════════════════════════ # Main # ═══════════════════════════════════════════════════════════════ def main(): print("=" * 50) print("Генератор кейсов Опуса") print("=" * 50) print("\n── Блок A: Classify ──") case1_baseline(); case2_cyrillic(); case3_nonstandard() case4_addendum(); case5_spec(); case6_no_counterparty() case7_date_text(); case8_two_dates(); case9_long_preamble() case10_noisy(); case11_trash() print("\n── Блок B: Group ──") case12_normalize(); case13_cyrillic_O(); case14_latin_C() case15_yo(); case16_no_parent(); case17_own_match() case18_collision(); case19_family(); case20_suffix() print("\n── Блок C: Compare ──") case21_kopeck(); case22_zero_qty(); case23_rename() case24_full_replace(); case25_add(); case26_delete() case27_qty_only(); case28_unresolved(); case29_format() case30_robust() manifest_path = os.path.join(OUT, "manifest.json") with open(manifest_path, 'w', encoding='utf-8') as f: json.dump(MANIFEST, f, ensure_ascii=False, indent=2) print(f"\n{'=' * 50}") print(f"Всего файлов: {len(MANIFEST)}") print(f"Вывод: {OUT}/") print(f"{'=' * 50}") if __name__ == "__main__": main()