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
+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}")