#!/usr/bin/env python3 """Build a navigable Markdown catalog (КАТАЛОГ_проекта.md) of a large file tree. Adapt ROOT (and optionally junk/MAP/docs) for each project, then run: python3 build_catalog.py Design rules (from real use): - Mechanical, deterministic: no LLM for the structure/stats. - Recurse top-level dirs + their immediate children only (maxdepth ~2) so the doc stays readable. Summarize bulk design subfolders (note+dwg) as "N файлов". - Tag each file's role from extension (+ naming conventions), Russian labels. - Filter service/junk files from the body but count them in stats. - Sort dirs with key=str.lower for natural locale ordering. Reproduce the exact structure used for the "Винный город" СКС/ЛВС/КТСБ archive: / <СИСТЕМА.N>/ e.g. КТСБ.1, ЛВС.2, СКС.3 <дата_Rev.XX>/ e.g. 2026.05.18_Rev.01 <шифр>.pdf итоговый комплект *_Изменения_*.docx список изменений *_Замечания*.xlsx замечания/ответы note+dwg/ bulk drawings + spec files (collapse to count) """ import os import sys import datetime import collections # >>> EDIT THESE for a new project >>> ROOT = "/mnt/vinogorod/ИТ/1. Винный город/Проекты/2026.05.22_Актуальные комплекты СКС, ЛВС, КТСБ" # subfolders to collapse to a count (engine+Cad sets) BULK_DIR_NAMES = ("note+dwg", "note", "dwg") # service files to exclude from the body (but still counted in the stats line) JUNK_NAMES = {"thumbs.db", "plot.log", "desktop.ini"} JUNK_EXTS = {".db", ".dwl", ".dwl2", ".bak"} # <<< END EDIT <<< # numeric suffix so .1/.2 sort after the base in locale order STRIP = str.lstrip # alias to avoid binding w/ layer in comprehension below def collect(base): """Return {name: ("FILE", ext_upper) | ("DIR", file_count)} for one level.""" out = {} for name in sorted(os.listdir(base), key=str.lower): p = os.path.join(base, name) if name.lower() in JUNK_NAMES or name.startswith("~$"): continue if os.path.isdir(p): out[name] = ("DIR", sum(len(x) for _, _, x in os.walk(p))) else: ext = os.path.splitext(name)[1].lstrip(".").upper() if ext in {e.lstrip(".").upper() for e in JUNK_EXTS}: continue out[name] = ("FILE", ext) return out def describe(ext): return { "PDF": "итоговый комплект PDF", "DOCX": "список изменений (docx)", "XLSX": "замечания/ответы (xlsx)", "TXT": "изменения (txt)", "DWG": "чертёж AutoCAD (dwg)", "DOC": "документ (doc)", "LOG": "лог", "MD": "каталог/описание (md)", "BAK": "резервная копия (bak)", }.get(ext, ext) def build(root): # --- stats --- ext_count = collections.Counter() total_files = total_size = junk = 0 for dirpath, _, filenames in os.walk(root): for f in filenames: ext = os.path.splitext(f)[1].lower() or "(нет)" ext_count[ext] += 1 total_files += 1 try: total_size += os.path.getsize(os.path.join(dirpath, f)) except OSError: pass if (f.startswith("~$") or f.lower() in JUNK_NAMES or ext in JUNK_EXTS): junk += 1 L = ["# Каталог проекта", "", "**Путь:** `%s`" % root, "", "**Сформирован:** %s" % datetime.date.today().isoformat(), "", "## Общая статистика", ""] L.append("- Всего файлов: **%d** (~%.2f ГБ)" % (total_files, total_size / 1024**3)) parts = [] for e, c in ext_count.most_common(): parts.append("**%s**: %d" % (e.lstrip(".").upper() or "без расширения", c)) L.append("- По типам: %s" % ", ".join(parts)) L.append("- Из них служебных (Thumbs.db, ~$, .dwl/.dwl2/.bak): %d (не включены)" % junk) L += ["", "## Структура и содержимое", ""] for top in sorted(os.listdir(root), key=str.lower): topp = os.path.join(root, top) if not os.path.isdir(topp): continue L.append("### %s" % top) L.append("") for name, (typ, val) in collect(topp).items(): if typ == "DIR": if name.lower() in BULK_DIR_NAMES: L.append("- **`%s/`** — %d файлов (чертежи + спецификации)" % (name, val)) else: L.append("**%s**" % name) L.append("") for sub, (styp, sval) in collect(os.path.join(topp, name)).items(): if styp == "DIR": if sub.lower() in BULK_DIR_NAMES: L.append("- **`%s/`** — %d файлов (чертежи + спецификации)" % (sub, sval)) else: L.append("- **`%s/`** — %d файлов" % (sub, sval)) else: L.append("- `%s` — %s" % (sub, describe(sval))) L.append("") else: L.append("- `%s` — %s" % (name, describe(val))) return "\n".join(L) if __name__ == "__main__": root = sys.argv[1] if len(sys.argv) > 1 else ROOT text = build(root) out = os.path.join(root, "КАТАЛОГ_проекта.md") with open(out, "w") as fh: fh.write(text) print("OK ->", out, os.path.getsize(out), "байт")