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
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+647
View File
@@ -0,0 +1,647 @@
# ⛔ НЕ МЕНЯТЬ БЕЗ РАЗРЕШЕНИЯ НАЕЛЯ ⛔
"""
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()
+126 -17
View File
@@ -18,7 +18,7 @@ MANIFEST = {}
def ensure_dir(path):
os.makedirs(path, exist_ok=True)
def save_doc(doc, path):
def save_doc(doc, path, comment=""):
doc.save(path)
print(f"{os.path.basename(path)}")
@@ -111,7 +111,7 @@ def generate_valid():
}
# ── Допники (1-3) ──
n_add = random.randint(1, 3)
n_add = random.randint(2, 4)
for ai in range(1, n_add + 1):
inst, itot, itot_s = pools.pick_services(random.randint(1, 3))
mon, mtot, mtot_s = pools.pick_services(random.randint(2, 5))
@@ -272,8 +272,11 @@ def generate_errors():
corrupt.make_giant_docx(os.path.join(out, "giant_500.docx"), 500)
MANIFEST["giant_500.docx"] = {"type": "error", "error": "giant", "status": "expected_slow"}
# Мусор (3 шт.)
trash_types = ["Счёт-фактура", "Письмо-запрос", "Коммерческое предложение"]
# Мусор (11 шт.)
trash_types = ["Счёт-фактура", "Письмо-запрос", "Коммерческое предложение",
"Акт выполненных работ", "Накладная", "Платёжное поручение",
"Счёт на оплату", "Договор аренды офиса", "Служебная записка",
"Прайс-лист", "Гарантийное письмо"]
for i, tt in enumerate(trash_types):
fname = f"trash_{i+1}.docx"
corrupt.make_trash_docx(os.path.join(out, fname), tt)
@@ -293,7 +296,57 @@ def generate_errors():
save_doc(templates.build_spec(ctx_orphan), os.path.join(out, "orphan_spec.docx"))
MANIFEST["orphan_spec.docx"] = {"type": "error", "error": "orphan", "status": "expected_group_unresolved"}
print(f" Сгенерировано ошибочных: 13")
print(f" Сгенерировано ошибочных: 21 + 2 orphan")
# Добавим ещё сирот и edge-кейсов
for vi in range(3):
onum = f"GHOST{vi}"
osvcs, _, _ = pools.pick_services(2)
ctx_o = {"company": pools.COMPANIES[vi], "number": onum, "date": pools.random_date(),
"contract_date": pools.random_date(), "version": 1,
"services": osvcs, "total": sum(s["sum"] for s in osvcs),
"total_str": pools.format_price(sum(s["sum"] for s in osvcs)),
"label": "Абонентские услуги",
"comment": f"Спецификация-сирота {onum}: нет родительского договора."}
ctx_o["total"] = round(sum(s["sum"] for s in osvcs), 2)
ctx_o["total_str"] = pools.format_price(ctx_o["total"])
fn = f"orphan_spec_{vi+1}.docx"
save_doc(templates.build_spec(ctx_o), os.path.join(out, fn))
MANIFEST[fn] = {"type": "error", "error": "orphan", "number": onum, "status": "expected_group_unresolved"}
# Договор без таблицы реквизитов (edge case для парсера)
doc_no_table = templates.build_contract(ctx_tmp)
# Удаляем таблицы
for t in doc_no_table.tables:
t._element.getparent().remove(t._element)
save_doc(doc_no_table, os.path.join(out, "contract_no_rekv.docx"))
MANIFEST["contract_no_rekv.docx"] = {"type": "error", "error": "no_rekv_table", "status": "expected_parse_ok"}
print(f" ✓ доп. ошибки: +5")
# ── Кириллический контракт (ХХХ002) ──
out_v = os.path.join(OUT, "valid")
kc = pools.COMPANIES[-1] # ХХХ002
knum = "03700_K"
kdate = "15.03.2026"
ctx_k = {"company": kc, "number": knum, "date": kdate, "company_idx": 99,
"comment": f"Кириллический контракт {kc['prefix']}-{knum}. Edge-case: кириллица в названии компании."}
fk = f"договор-{kc['prefix']}-{knum}.docx"
save_doc(templates.build_contract(ctx_k), os.path.join(out_v, fk))
MANIFEST[fk] = {"type": "contract", "company": kc["prefix"], "number": knum, "date": kdate, "status": "valid"}
# Спека к кириллическому
ksvcs, ktot, ktot_s = pools.pick_services(6)
for s in ksvcs: s["date_start"] = pools.random_date(2026, 3, 3)
ctx_ks = {"company": kc, "number": knum, "date": kdate, "contract_date": kdate, "version": 1,
"services": ksvcs, "total": ktot, "total_str": ktot_s,
"label": f"Абонентские услуги",
"comment": f"Спецификация кириллического контракта {knum}."}
fks = f"спецификация-{kc['prefix']}-{knum}.docx"
save_doc(templates.build_spec(ctx_ks), os.path.join(out_v, fks))
MANIFEST[fks] = {"type": "spec", "company": kc["prefix"], "number": knum, "version": 1, "status": "valid"}
print(f" ✓ кириллический контракт: +2 файла")
# ═══════════════════════════════════════════════════════════════
@@ -381,15 +434,32 @@ def generate_zips():
with zipfile.ZipFile(zip_path, 'w', zipfile.ZIP_DEFLATED) as zf:
for f in files_for_zip:
zf.write(os.path.join(valid_dir, f), f)
print(f" ✓ ZIP: {os.path.basename(zip_path)} ({len(files_for_zip)} файлов)")
MANIFEST[os.path.basename(zip_path)] = {"type": "zip", "files": files_for_zip, "status": "valid"}
# ZIP: все файлы контракта XXX005
files5 = [f for f in os.listdir(valid_dir) if "XXX005" in f]
if files5:
zp = os.path.join(out, "contract_XXX005.zip")
with zipfile.ZipFile(zp, 'w', zipfile.ZIP_DEFLATED) as zf:
for f in files5:
zf.write(os.path.join(valid_dir, f), f)
print(f" ✓ ZIP XXX005: {os.path.basename(zp)} ({len(files5)} файлов)")
MANIFEST[os.path.basename(zp)] = {"type": "zip", "files": files5, "status": "valid"}
# ZIP: только спецификации (без договора)
specs = [f for f in os.listdir(valid_dir) if f.startswith("спецификация")][:4]
if specs:
zp2 = os.path.join(out, "only_specs.zip")
with zipfile.ZipFile(zp2, 'w', zipfile.ZIP_DEFLATED) as zf:
for f in specs:
zf.write(os.path.join(valid_dir, f), f)
print(f" ✓ ZIP только спеки: {os.path.basename(zp2)} ({len(specs)} файлов)")
MANIFEST[os.path.basename(zp2)] = {"type": "zip", "specs_only": True, "status": "valid"}
# ZIP с дубликатами имён внутри
if len(files_for_zip) >= 2:
if len(files_for_zip) >= 1:
zip_path2 = os.path.join(out, "duplicates_inside.zip")
with zipfile.ZipFile(zip_path2, 'w', zipfile.ZIP_DEFLATED) as zf:
zf.write(os.path.join(valid_dir, files_for_zip[0]), files_for_zip[0])
zf.write(os.path.join(valid_dir, files_for_zip[0]), files_for_zip[0]) # дубликат!
zf.write(os.path.join(valid_dir, files_for_zip[0]), files_for_zip[0])
print(f" ✓ ZIP с дубликатами: {os.path.basename(zip_path2)}")
MANIFEST[os.path.basename(zip_path2)] = {"type": "zip", "duplicates": True, "status": "valid"}
@@ -403,6 +473,8 @@ def generate_zips():
print(f" ✓ ZIP с битым: {os.path.basename(zip_path3)}")
MANIFEST[os.path.basename(zip_path3)] = {"type": "zip", "has_error": True, "status": "valid"}
print(f" ✓ ZIP-ы: 5 шт.")
# ═══════════════════════════════════════════════════════════════
# Дубликаты
@@ -418,16 +490,28 @@ def generate_duplicates():
files = [f for f in os.listdir(valid_dir) if f.endswith('.docx')]
if len(files) >= 3:
# Точная копия (другое имя)
src = os.path.join(valid_dir, files[0])
shutil.copy(src, os.path.join(out, "copy_of_" + files[0]))
MANIFEST["copy_of_" + files[0]] = {"type": "duplicate", "original": files[0], "kind": "content_copy"}
# Точная копия (другое имя) × 3
for i in range(min(3, len(files))):
src = os.path.join(valid_dir, files[i])
dst = os.path.join(out, f"copy_{i+1}_" + files[i])
shutil.copy(src, dst)
MANIFEST[os.path.basename(dst)] = {"type": "duplicate", "original": files[i], "kind": "content_copy"}
# Одинаковое имя, разный контент
shutil.copy(os.path.join(valid_dir, files[1]), os.path.join(out, files[0]))
MANIFEST[files[0]] = {"type": "duplicate", "original": files[0], "kind": "name_collision"}
# Одинаковое имя, разный контент × 3
for i in range(min(3, len(files)-1)):
shutil.copy(os.path.join(valid_dir, files[i+3]), os.path.join(out, files[i]))
MANIFEST[files[i]] = {"type": "duplicate", "original": files[i], "kind": "name_collision"}
print(f" ✓ Дубликаты: 2 шт.")
# Рассинхрон: имя файла ≠ номер в тексте
if len(files) >= 2:
src = os.path.join(valid_dir, files[0])
dst = os.path.join(out, "mismatch_" + files[1])
shutil.copy(src, dst)
MANIFEST[os.path.basename(dst)] = {"type": "error", "error": "name_content_mismatch", "status": "expected_classify_different"}
shutil.copy(os.path.join(valid_dir, files[1]), os.path.join(out, "mismatch_" + files[0]))
MANIFEST[os.path.basename("mismatch_" + files[0])] = {"type": "error", "error": "name_content_mismatch", "status": "expected_classify_different"}
print(f" ✓ Дубликаты + рассинхрон: 8 шт.")
# ═══════════════════════════════════════════════════════════════
@@ -459,6 +543,31 @@ def main():
with open(manifest_path, 'w', encoding='utf-8') as f:
json.dump(MANIFEST, f, ensure_ascii=False, indent=2)
# Сгенерировать human-readable README.md
readme_path = os.path.join(OUT, "README.md")
lines = ["# Тестовые файлы — описание\n"]
cats = {"contract": "📄 Договоры", "spec": "📋 Спецификации", "addendum": "📎 Допсоглашения",
"error": "❌ Ошибочные", "trash": "🗑 Мусор", "zip": "📦 ZIP-архивы", "duplicate": "📋 Дубликаты"}
by_cat = {}
for fname, info in MANIFEST.items():
cat = info.get("type", "?")
by_cat.setdefault(cat, []).append((fname, info))
for cat, title in cats.items():
items = by_cat.get(cat, [])
if not items: continue
lines.append(f"\n## {title} ({len(items)} шт.)\n")
for fname, info in sorted(items):
company = info.get("company", "")
number = info.get("number", "")
ver = f" v{info['version']}" if info.get("version") else ""
err = info.get("error", "")
label = info.get("label", "")
extra = f"{err}" if err else (f"{label}" if label else "")
lines.append(f"- `{fname}` {company} {number}{ver}{extra}")
lines.append(f"\n\nВсего: **{len(MANIFEST)}** файлов")
with open(readme_path, 'w', encoding='utf-8') as f:
f.write('\n'.join(lines))
total = len(MANIFEST)
print(f"\n{'=' * 60}")
print(f"Всего файлов в manifest.json: {total}")
+56
View File
@@ -0,0 +1,56 @@
# Тестовые файлы — 30 кейсов (Opus)
**Все документы — полноценные:** шапка, преамбула с реквизитами сторон, 2-4 раздела, таблицы услуг (8 строк), подписи.
## Блок A — Classify (кейсы 111)
Проверка классификации: тип документа, номер, контрагент, дата.
| Файл | Кейс | Контрагент |
|------|------|-----------|
| `договор-XXX001-03700.docx` | 1 | ЗАО "ТехноПлюс" |
| `договор-МЭС.docx` | 2 | АО "МосЭнергоСбыт" |
| `договор-нестандарт.docx` | 3 | ЗАО "Клиент-Сервис" |
| `допник-XXX003-01300.docx` | 4 | — (допник) |
| `спецификация-XXX001-03700.docx` | 5 | — (спецификация) |
| `договор-без-стороны.docx` | 6 | **null** (без контрагента) |
| `договор-дата-прописью.docx` | 7 | ООО "Ресурс-ЦОД" |
| `договор-две-даты.docx` | 8 | АО "ТехноДата" |
| `договор-длинная-преамбула.docx` | 9 | ООО "Поздний Контрагент" |
| `договор-много-номеров.docx` | 10 | ЗАО "МультиНомер" |
| `акт-сверки.docx` | 11 | **other** (мусор) |
## Блок B — Group (кейсы 1220)
Проверка группировки: нормализация номеров, коллизии, семьи.
| Файлы | Кейс | Суть |
|-------|------|------|
| `договор-МЭС-норм` + `допник-МЭС-норм` | 12 | Разделители: `-` vs `/` vs пробел |
| `договор-03700` + `допник-О3700` | 13 | Кириллическая О vs цифра 0 |
| `договор-CBC` + `допник-СВС` | 14 | Латинская C vs кириллическая С |
| `договор-ЁЖ` + `допник-ЁЖ` | 15 | Буква Ё |
| `допник-без-родителя` | 16 | Допник без parent → unresolved |
| `договор-GR50` + `допник-GR50` | 17 | Допник через own_number |
| `договор-AB12` + `A-B12` + `допник-AB12` | 18 | Коллизия якорей |
| `договор-FAM` + `спека-FAM` + `допник-1/2-FAM` | 19 | Семья из 4 документов |
| `допник-1-XXX003-01300_2` | 20 | Суффикс в имени файла |
## Блок C — Compare (кейсы 2130)
Проверка сравнения спецификаций. **8 строк услуг, изменена только v2.**
| Кейс | Суть | v2 отличия от v1 |
|------|------|-----------------|
| 21 | Цена +1 копейка | UPDATE — строка 1 (45 000,00 → 45 000,01) |
| 22 | Удаление услуги | DELETE — строка 3 (IP-адреса) |
| 23 | Переименование + цена | UPDATE — строка 5 (название + цена + дата) |
| 24 | Полная замена | FULL REPLACE — 3 новые строки |
| 25 | ADD | ADD — 2 строки (9, 10) |
| 26 | DELETE | DELETE — 2 строки (7, 8) |
| 27 | Только объём | UPDATE — строка 3 (8→12 IP) |
| 28 | Услуга-призрак | ADD — строка 9 (VPN, нет в v1) |
| 29 | Числа с пробелами | UPDATE — строка 1 (45 000→45 500) |
| 30 | Робастность парсера | UPDATE — строка 1 (45 000→51 000) |
---
Всего: **50 файлов** в трёх папках (`block_a/`, `block_b/`, `block_c/`).
Сгенерированы: `testgen/gen_opus.py`.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+320
View File
@@ -0,0 +1,320 @@
{
"договор-XXX001-03700.docx": {
"case": 1,
"type": "contract",
"own_number": "XXX001-03700",
"counterparty": "ЗАО \"ТехноПлюс\""
},
"спецификация-XXX001-03700_v1.docx": {
"case": 1,
"type": "specification",
"parent_number": "XXX001-03700"
},
"договор-МЭС.docx": {
"case": 2,
"type": "contract",
"own_number": "МЭС-123/2024",
"counterparty": "АО \"МосЭнергоСбыт\""
},
"спецификация-МЭС.docx": {
"case": 2,
"type": "specification",
"parent_number": "МЭС-123/2024"
},
"договор-нестандарт.docx": {
"case": 3,
"type": "contract",
"own_number": "SVC-77",
"counterparty": "ЗАО \"Клиент-Сервис\""
},
"спецификация-SVC-77.docx": {
"case": 3,
"type": "specification",
"parent_number": "SVC-77"
},
"допник-XXX003-01300.docx": {
"case": 4,
"type": "supplement",
"own_number": "1",
"parent_number": "XXX003-01300"
},
"договор-без-стороны.docx": {
"case": 6,
"type": "contract",
"counterparty": null
},
"спецификация-NC-09.docx": {
"case": 6,
"type": "specification",
"parent_number": "NC-09"
},
"договор-дата-прописью.docx": {
"case": 7,
"type": "contract",
"doc_date": "2025-03-15",
"counterparty": "ООО \"Ресурс-ЦОД\""
},
"спецификация-DT-15.docx": {
"case": 7,
"type": "specification",
"parent_number": "DT-15"
},
"договор-две-даты.docx": {
"case": 8,
"type": "contract",
"doc_date": "2025-04-01",
"counterparty": "АО \"ТехноДата\""
},
"спецификация-TD-21.docx": {
"case": 8,
"type": "specification",
"parent_number": "TD-21"
},
"договор-длинная-преамбула.docx": {
"case": 9,
"type": "contract",
"own_number": "LATE-99",
"counterparty": "ООО \"Поздний Контрагент\""
},
"спецификация-LATE-99.docx": {
"case": 9,
"type": "specification",
"parent_number": "LATE-99"
},
"договор-много-номеров.docx": {
"case": 10,
"type": "contract",
"own_number": "MN-01",
"counterparty": "ЗАО \"МультиНомер\""
},
"спецификация-MN-01.docx": {
"case": 10,
"type": "specification",
"parent_number": "MN-01"
},
"акт-сверки.docx": {
"case": 11,
"type": "other"
},
"договор-МЭС-норм.docx": {
"case": 12,
"own": "МЭС-123/2024",
"counterparty": "АО \"МосЭнергоСбыт\""
},
"спецификация-МЭС-норм.docx": {
"case": 12,
"type": "specification",
"parent_number": "МЭС-123/2024"
},
"допник-МЭС-норм.docx": {
"case": 12,
"parent": "МЭС 123/2024"
},
"договор-03700.docx": {
"case": 13,
"own": "XXX001-03700",
"counterparty": "ЗАО \"ТехноПлюс\""
},
"спецификация-03700.docx": {
"case": 13,
"type": "specification",
"parent_number": "XXX001-03700"
},
"допник-О3700.docx": {
"case": 13,
"parent": "XXX001-О3700"
},
"договор-CBC.docx": {
"case": 14,
"own": "CBC-10",
"counterparty": "ООО \"СиБиСи\""
},
"спецификация-CBC.docx": {
"case": 14,
"type": "specification",
"parent_number": "CBC-10"
},
"допник-СВС.docx": {
"case": 14,
"parent": "СВС-10"
},
"договор-ЁЖ.docx": {
"case": 15,
"own": "ЁЖ-5",
"counterparty": "ООО \"Ёжик-Телеком\""
},
"спецификация-ЁЖ.docx": {
"case": 15,
"type": "specification",
"parent_number": "ЁЖ-5"
},
"допник-ЁЖ.docx": {
"case": 15,
"parent": "ЁЖ-5"
},
"допник-без-родителя.docx": {
"case": 16,
"type": "supplement",
"parent": null
},
"договор-GR50.docx": {
"case": 17,
"own": "GR-50",
"counterparty": "ООО \"ГринРайт\""
},
"спецификация-GR50.docx": {
"case": 17,
"type": "specification",
"parent_number": "GR-50"
},
"допник-GR50.docx": {
"case": 17,
"own": "GR-50"
},
"договор-AB12.docx": {
"case": 18,
"own": "AB-12",
"counterparty": "ООО \"Альфа-Бета\""
},
"спецификация-AB12.docx": {
"case": 18,
"type": "specification",
"parent_number": "AB-12"
},
"договор-A-B12.docx": {
"case": 18,
"own": "A-B12",
"counterparty": "ООО \"А-Бет\""
},
"спецификация-A-B12.docx": {
"case": 18,
"type": "specification",
"parent_number": "A-B12"
},
"допник-AB12.docx": {
"case": 18,
"parent": "AB12"
},
"договор-FAM.docx": {
"case": 19,
"date": "2025-01-10",
"counterparty": "ЗАО \"Фэмили-Групп\""
},
"спека-FAM.docx": {
"case": 19,
"date": "2025-02-01"
},
"допник-1-FAM.docx": {
"case": 19,
"date": "2025-03-01"
},
"допник-2-FAM.docx": {
"case": 19,
"date": "2025-05-01"
},
"допник-1-XXX003-01300_2.docx": {
"case": 20,
"own": "1",
"parent": "XXX003-01300"
},
"спека-цена-v1.docx": {
"case": 21,
"version": 1,
"parent_number": "CMP-021"
},
"допник-цена.docx": {
"case": 21,
"type": "supplement",
"parent_number": "CMP-021"
},
"спека-обнуление-v1.docx": {
"case": 22,
"version": 1,
"parent_number": "CMP-022"
},
"допник-обнуление.docx": {
"case": 22,
"type": "supplement",
"parent_number": "CMP-022"
},
"спека-переименование-v1.docx": {
"case": 23,
"version": 1,
"parent_number": "CMP-023"
},
"допник-переименование.docx": {
"case": 23,
"type": "supplement",
"parent_number": "CMP-023"
},
"спека-полная-v1.docx": {
"case": 24,
"version": 1,
"parent_number": "CMP-024"
},
"допник-новая-редакция.docx": {
"case": 24,
"type": "supplement",
"parent_number": "CMP-024"
},
"спека-добавление-v1.docx": {
"case": 25,
"version": 1,
"parent_number": "CMP-025"
},
"допник-добавление.docx": {
"case": 25,
"type": "supplement",
"parent_number": "CMP-025"
},
"спека-удаление-v1.docx": {
"case": 26,
"version": 1,
"parent_number": "CMP-026"
},
"допник-удаление.docx": {
"case": 26,
"type": "supplement",
"parent_number": "CMP-026"
},
"спека-объём-v1.docx": {
"case": 27,
"version": 1,
"parent_number": "CMP-027"
},
"допник-объём.docx": {
"case": 27,
"type": "supplement",
"parent_number": "CMP-027"
},
"спека-призрак-v1.docx": {
"case": 28,
"version": 1,
"parent_number": "CMP-028"
},
"допник-призрак.docx": {
"case": 28,
"type": "supplement",
"parent_number": "CMP-028"
},
"спека-формат-v1.docx": {
"case": 29,
"version": 1,
"parent_number": "CMP-029"
},
"допник-формат-чисел.docx": {
"case": 29,
"type": "supplement",
"parent_number": "CMP-029"
},
"спека-роб-v1.docx": {
"case": 30,
"version": 1,
"parent_number": "CMP-030"
},
"допник-роб.docx": {
"case": 30,
"type": "supplement",
"parent_number": "CMP-030"
}
}
Binary file not shown.
Binary file not shown.
View File
Binary file not shown.
Binary file not shown.
Binary file not shown.
File diff suppressed because it is too large Load Diff
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.

Some files were not shown because too many files have changed in this diff Show More