commit be3ae18be72b39f75fae6f06ea07d9f965ff0438 Author: estorozhenko Date: Sun Sep 6 13:51:19 2026 +0000 Initial commit: Hermes skill file-tree-catalog diff --git a/SKILL.md b/SKILL.md new file mode 100644 index 0000000..435b5a4 --- /dev/null +++ b/SKILL.md @@ -0,0 +1,34 @@ +--- +name: file-tree-catalog +description: Catalog a project/file tree into Markdown programmatically. +--- + +# File-Tree Catalog + +Create a human-navigable Markdown catalog of a large local file tree (engineering project archives, design-document sets, media libraries). This is **mechanical filesystem work — do it with Python, do NOT burn tokens on an LLM**. Deliverables: a `КАТАЛОГ_проекта.md` in the root of the tree. + +## When to use +- User asks to "каталогизировать", "сделать каталог/структуру/содержимое" of a folder, "понять что лежит в проекте X". +- User hands you a path to an archive of documents (СКС/ЛВС/КТСБ drawings, PDF sets, etc.) and wants structure + contents overview. + +## Workflow +1. **Confirm the target** (what kind of catalog) with one `clarify` if ambiguous — but structural cataloging is usually unambiguous: structure + contents. Don't over-ask. +2. **Enumerate** the tree with `execute_code` (Python, not shell): walk `os.walk`, collect extensions, sizes, counts. Get a feel for scale first (files, GB, top-level dirs, distribution by extension) before building the document. +3. **Build the catalog procedurally** (see bundled script). Key decisions: + - Group by immediate subdirectories; recurse 2-3 levels max so the doc stays readable. + - Summarize `note+dwg/` (or similar bulk design subfolders) as `N файлов` instead of listing 50+ CAD files. + - Tag each file with its *role* derived from extension + naming: PDF="итоговый комплект / итоговый PDF", DOCX="список изменений", XLSX="замечания/ответы", TXT="изменения". + - Prepend an **Общая статистика** section (total files, total size, per-extension counts) and a **Состав проекта** section (what the top-level systems/objects are). +4. **Write** the `.md` into the tree root with `open(out,"w")`. Report the absolute path. + +## Pitfalls +- **Don't use an LLM for the catalog itself** — building a `dict`/tree + markdown join is deterministic and faster. Reserve a local model only for *semantic* additions (expanding abbreviations), and only if it actually answers. +- **Don't fabricate expansions of unknown codes.** If a шифр/abbreviation (e.g. `Н-КВ`) isn't verifiable, state the *fact* ("belongs to the Кубань-Вино object") and explicitly note it's unverified — do NOT invent "новый корпус" etc. +- **Filter service/junk files** or the doc drowns. Consistent engineering-archive junk on this host: `Thumbs.db`, `~$…` (Office lock files), `.dwl`/`.dwl2` (AutoCAD locks), `.bak`, `plot.log`, `.db`. Count them separately in stats but omit from the body. +- **Ordinal/numbering**: sort dirs with `key=str.lower` for natural locale ordering (КТСБ.1 < КТСБ.2, зоны in order). +- Be careful writing the markdown with `%`-format strings or plain concatenation — **nested f-strings with quotes inside a list comprehension raise a SyntaxError**; use `%`-formatting or build lines imperatively. +- Optional user-request: "use the local model to save tokens" — for mechanical cataloging the honest answer is "no model needed"; say so rather than calling ollama pointlessly. Local qwen3 on this host returns empty `content` (reasoning-only replies), so don't count on it for prose. + +## Files +- `scripts/build_catalog.py` — reusable generator (stats + tree + role tagging + junk filter). Edit the constant `ROOT` and run; adapt `maxdepth`/`junk` sets as needed. +- `references/engineering-doc-archive.md` — conventions and domain notes for Russian design/СКС-ЛВС-КТСБ document archives. \ No newline at end of file diff --git a/references/engineering-doc-archive.md b/references/engineering-doc-archive.md new file mode 100644 index 0000000..99656d4 --- /dev/null +++ b/references/engineering-doc-archive.md @@ -0,0 +1,48 @@ +# Russian design-document archive conventions (СКС / ЛВС / КТСБ) + +Domain notes from cataloging the "Винный город" project archive. Useful for +sensing what files mean by name and for expanding systems/objects in catalogs. + +## Systems (шифр prefix) +- **СКС** — структурированная кабельная система +- **ЛВС** — локально-вычислительная сеть +- **КТСБ** — комплекс технических средств безопасности (СКУД, видеонаблюдение, охранная сигнализация и т.п.) +- **ГП2 / ГП** — генплан (used in «Уличный бар» folder) + +## Document шифр pattern +`1289-1-1-Р-<СИСТЕМА>.<объект>[.подраздел]_<вариант>.pdf` where: +- `1289-1-1-Р` — project / series / stage (Р = «рабочая документация») +- `<объект>` — building/zone number (1, 2, 3, 4, 6, 7, 9; note: 5 and 8 have no folders — don't assume they exist) +- `.1` suffix — sub-division of a system/zone (СКС.1 vs СКС.1.1, ЛВС.7 vs ЛВС.7.1) +- Optional `Н-КВ` after project: `1289-1-1-Р-Н-КВ-СКС.7` — belongs to the **Кубань-Вино** object subset. + +## Companion files (role by suffix / extension) +- `*_Изменения_Изм.N_От .docx` — change/revision list (docx) +- `*_Замечания*_Ответ We-On.xlsx` — review comments + contractor response (xlsx) +- `*_Изменения.txt` — plain-text change notes +- `*_ЖАН.pdf` — отдельный жан/выпуск (a ЖАН issue — versioned sub-release of the doc) +- `*_Лист регистрации изменения.docx` — change-registration sheet +- `*_Обложка+ТЛ*.docx` — cover + title/technical sheet +- Bulk CAD+schema folder **`note+dwg/`** — contains per-sheet drawings: + - `*_л.N.dwg` — leaf/sheet N (single) or `л.2-5.dwg` (range) + - `*_л.1_ОД.dwg` — ОД (общая/общие данные?) + - `*_СО*.docx` — спецификация оборудования (equipment schedule) + - `*_КЖ.doc/.docx` — ? + - `*_Зд1/Зд2.xlsx`, `*_ТП1/ТП2/ТП3.xlsx` — данные по зданиям / техпроцессу (Excel) + - `*_П1..П7.dwg/.pdf/.docx` — планы (floor plans) various + - `*_РР.dwg`, `*_И1.dwg` — расчётные/исполнительные чертежи + +## Contractors (in document headers) +- **WE-ON** — ГИП Рыкова +- **Велесстрой** — ГИП Кузнецов + +## Folder layout +`<СИСТЕМА.N>/_Rev.XX/` — one folder per revision peak. Top-level +also may contain special objects: **Кубань-Вино** (systems of zone 7 done for a +separate client), **Уличный бар**. + +## Local-model note +Local `qwen3:8b` on this host answers ONLY via `reasoning_content`; `content` +comes back empty even with `think:false` / `options.num_think:0` attempts. Do +not rely on it for generated prose. For semantic expansion of abbreviations, +prefer stating verified facts over inventing meaning. diff --git a/scripts/build_catalog.py b/scripts/build_catalog.py new file mode 100644 index 0000000..21ab6f7 --- /dev/null +++ b/scripts/build_catalog.py @@ -0,0 +1,135 @@ +#!/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), "байт") \ No newline at end of file