diff --git a/audit/2026-05-06-objects-audit.md b/audit/2026-05-06-objects-audit.md new file mode 100644 index 0000000..eeff08f --- /dev/null +++ b/audit/2026-05-06-objects-audit.md @@ -0,0 +1,62 @@ +--- +date: 2026-05-06 +type: audit +source: scripts/kb-objects-audit.py +tags: [audit, objects, frontmatter, links] +score: 84 +--- + +# KB objects audit — 2026-05-06 + +**Score (меньше = лучше): `84`** + +- Проектов с frontmatter: **12/12** (0 проблем) +- NetBird online-пиров без проектной карточки: **3** +- Битых wiki-ссылок `[[...]]`: **26** + +## Frontmatter в projects/ + +✅ все проекты имеют валидный frontmatter + +## Online netbird-пиры без проектной карточки + +Эти пиры онлайн в NetBird, но не привязаны ни к одной projects/-странице. +Бот не сможет ответить «найди X» осмысленно — нет файла или alias. + +Лечение: либо создать stub в `projects//README.md` (см. `projects/lipki/` как образец), +либо добавить имя пира как полную строку в `aliases` подходящего проекта. + +| NetBird-имя | IP | OS | Город | +|---|---|---|---| +| `DESKTOP-2IOQS54` | 100.70.82.83 | Windows 10 | Saransk | +| `DESKTOP-AGBMLPN` | 100.70.0.106 | Windows 11 | Helsinki | +| `DESKTOP-HL0BB05` | 100.70.235.80 | Windows 11 | Lipetsk | + +## Битые wiki-ссылки + +- [CLAUDE.md](CLAUDE.md) — `[[двойные скобки]` → нет такого файла +- [snippets/invoice-template.md](snippets/invoice-template.md) — `[[projects/dttb/znamenskoye-log.md]` → нет такого файла +- [decisions/2026-04-30-niikn-culture-gov-fakeip-fix.md](decisions/2026-04-30-niikn-culture-gov-fakeip-fix.md) — `[[notes/govru-diagnosis]` → нет такого файла +- [decisions/2026-05-02-apple-id-tj-via-residential-proxy.md](decisions/2026-05-02-apple-id-tj-via-residential-proxy.md) — `[[../snippets/clients/]` → нет такого файла +- [decisions/2026-04-29-rustdesk-client-deployment-package.md](decisions/2026-04-29-rustdesk-client-deployment-package.md) — `[[../snippets/clients/]` → нет такого файла +- [decisions/2026-04-28-niikn-uookn-sev-gov-fakeip-fix.md](decisions/2026-04-28-niikn-uookn-sev-gov-fakeip-fix.md) — `[[notes/govru-diagnosis]` → нет такого файла +- [decisions/2026-04-30-openwrt-homelab-agh-podkop-chain.md](decisions/2026-04-30-openwrt-homelab-agh-podkop-chain.md) — `[[../claude-memory/podkop]` → нет такого файла +- [audit/2026-05-03-health.md](audit/2026-05-03-health.md) — `[[таргет]` → нет такого файла +- [audit/2026-05-03-health.md](audit/2026-05-03-health.md) — `[[notes/govru-diagnosis]` → нет такого файла +- [audit/2026-05-03-health.md](audit/2026-05-03-health.md) — `[[../snippets/clients/]` → нет такого файла +- [audit/2026-05-03-health.md](audit/2026-05-03-health.md) — `[[../claude-memory/podkop]` → нет такого файла +- [audit/2026-05-03-health.md](audit/2026-05-03-health.md) — `[[../snippets/clients/]` → нет такого файла +- [audit/2026-05-03-health.md](audit/2026-05-03-health.md) — `[[notes/govru-diagnosis]` → нет такого файла +- [audit/2026-05-03-health.md](audit/2026-05-03-health.md) — `[[../../../knowledge-base/feedback_lxc_loadavg]` → нет такого файла +- [audit/2026-05-03-health.md](audit/2026-05-03-health.md) — `[[feedback_finland_security]` → нет такого файла +- [audit/2026-05-03-health.md](audit/2026-05-03-health.md) — `[[..]` → нет такого файла +- [projects/dttb/rustdesk.md](projects/dttb/rustdesk.md) — `[[../../../knowledge-base/feedback_lxc_loadavg]` → нет такого файла +- [projects/lipki/README.md](projects/lipki/README.md) — `[[../znamenskoye/]` → нет такого файла +- [projects/lipki/README.md](projects/lipki/README.md) — `[[../znamenskoye/]` → нет такого файла +- [projects/lipki/README.md](projects/lipki/README.md) — `[[../znamenskoye/]` → нет такого файла +- [projects/sergey/README.md](projects/sergey/README.md) — `[[../znamenskoye/]` → нет такого файла +- [projects/dttb/graphify-out/GRAPH_REPORT.md](projects/dttb/graphify-out/GRAPH_REPORT.md) — `[[_COMMUNITY_Community 0|` → нет такого файла +- [projects/dttb/graphify-out/GRAPH_REPORT.md](projects/dttb/graphify-out/GRAPH_REPORT.md) — `[[_COMMUNITY_Community 1|` → нет такого файла +- [projects/dttb/graphify-out/GRAPH_REPORT.md](projects/dttb/graphify-out/GRAPH_REPORT.md) — `[[_COMMUNITY_Community 2|` → нет такого файла +- [projects/dttb/graphify-out/GRAPH_REPORT.md](projects/dttb/graphify-out/GRAPH_REPORT.md) — `[[_COMMUNITY_Community 3|` → нет такого файла +- [snippets/clients/yaroslav-amnezia-setup.md](snippets/clients/yaroslav-amnezia-setup.md) — `[[feedback_finland_security]` → нет такого файла diff --git a/scripts/kb-objects-audit.py b/scripts/kb-objects-audit.py new file mode 100644 index 0000000..29754c9 --- /dev/null +++ b/scripts/kb-objects-audit.py @@ -0,0 +1,195 @@ +#!/usr/bin/env python3 +"""kb-objects-audit — еженедельный health-check vault'а. + +Проверки: +1. Каждый онлайн-netbird-пир имеет привязку к проекту (через aliases в frontmatter + или собственную карточку). Orphans = клиентские машины без описания. +2. Каждый projects//README.md имеет валидный frontmatter (type, status, + aliases как минимум). +3. Битые wiki-ссылки `[[...]]` в vault'е (указывают в небытие). +4. Дубли по нечёткому совпадению заголовков (опасные близнецы). + +Output: audit/YYYY-MM-DD-objects-audit.md (каждый запуск перезаписывает дневной). + +Запуск ручной или cron weekly: + cd ~/knowledge-base && python3 scripts/kb-objects-audit.py +""" +from datetime import date +from pathlib import Path +import json +import re +import sys + +VAULT = Path(__file__).resolve().parent.parent +OBJECTS_MAP = VAULT / "audit/objects-map.json" + +REQUIRED_FRONTMATTER_KEYS = ["type", "status"] +PROJECT_FRONTMATTER_KEYS = ["type", "status", "aliases"] + + +def load_map() -> list: + if not OBJECTS_MAP.exists(): + sys.exit(f"FATAL: нет {OBJECTS_MAP.relative_to(VAULT)} — запусти scripts/kb-objects-map.py сначала") + return json.loads(OBJECTS_MAP.read_text()) + + +def parse_fm(text: str) -> dict: + m = re.match(r"^---\n(.+?)\n---\n", text, re.S) + if not m: + return {} + fm = {} + for line in m.group(1).splitlines(): + if ":" not in line or line.startswith("#"): + continue + k, _, v = line.partition(":") + fm[k.strip()] = v.strip() + return fm + + +def find_md_files() -> list[Path]: + out = [] + for p in VAULT.rglob("*.md"): + rel = p.relative_to(VAULT) + if any(part.startswith(".") for part in rel.parts): + continue + if rel.parts[0] in ("audit",): + # audit-отчёты сами не аудируются + if "archive" in rel.parts: + continue + out.append(p) + return out + + +def check_project_frontmatter(objects: list) -> list[str]: + issues = [] + for o in objects: + if o["type"] != "project" or not o["file"]: + continue + path = VAULT / o["file"] + if not path.exists(): + issues.append(f"- `{o['id']}`: file missing — `{o['file']}`") + continue + fm = parse_fm(path.read_text()) + missing = [k for k in PROJECT_FRONTMATTER_KEYS if k not in fm] + if missing: + issues.append(f"- `{o['id']}`: frontmatter missing {missing} — `{o['file']}`") + return issues + + +def check_broken_wikilinks(files: list[Path]) -> list[tuple[str, str, str]]: + """Возвращает [(source, link, reason)] для битых [[...]] ссылок.""" + issues = [] + all_basenames = {f.stem for f in files} + all_relpaths = {str(f.relative_to(VAULT)).replace(".md", "") for f in files} + pat = re.compile(r"\[\[([^\]\|#]+?)(?:\||#|\])") + for f in files: + try: + text = f.read_text() + except Exception: + continue + for m in pat.finditer(text): + target = m.group(1).strip().rstrip("/") + if not target: + continue + # Allow absolute by relpath, or basename match, or relative-resolved + if target in all_relpaths: + continue + base = target.split("/")[-1] + if base in all_basenames: + continue + # try relative to source + src_dir = f.parent.relative_to(VAULT) + resolved = str(src_dir / target).replace("./", "") + if resolved in all_relpaths: + continue + issues.append((str(f.relative_to(VAULT)), m.group(0), "→ нет такого файла")) + return issues + + +def check_orphans(objects: list) -> list[dict]: + return [o for o in objects if o["type"] == "netbird-only" + and o["netbird_peers"] and o["netbird_peers"][0].get("online", True)] + + +def main() -> None: + objects = load_map() + files = find_md_files() + + fm_issues = check_project_frontmatter(objects) + orphans = check_orphans(objects) + wiki_issues = check_broken_wikilinks(files) + + # счётчики + n_proj = sum(1 for o in objects if o["type"] == "project") + n_proj_with_fm = n_proj - len(fm_issues) + n_orphan_online = len(orphans) + n_wiki_broken = len(wiki_issues) + score = len(fm_issues) * 5 + n_orphan_online * 2 + n_wiki_broken * 3 + today = date.today().isoformat() + out = VAULT / f"audit/{today}-objects-audit.md" + + md = [ + "---", + f"date: {today}", + "type: audit", + "source: scripts/kb-objects-audit.py", + "tags: [audit, objects, frontmatter, links]", + f"score: {score}", + "---", + "", + f"# KB objects audit — {today}", + "", + f"**Score (меньше = лучше): `{score}`**", + "", + f"- Проектов с frontmatter: **{n_proj_with_fm}/{n_proj}** ({len(fm_issues)} проблем)", + f"- NetBird online-пиров без проектной карточки: **{n_orphan_online}**", + f"- Битых wiki-ссылок `[[...]]`: **{n_wiki_broken}**", + "", + ] + + md.extend(["## Frontmatter в projects/", ""]) + if fm_issues: + md.extend(fm_issues) + else: + md.append("✅ все проекты имеют валидный frontmatter") + md.append("") + + md.extend(["## Online netbird-пиры без проектной карточки", + "", + "Эти пиры онлайн в NetBird, но не привязаны ни к одной projects/-странице. ", + "Бот не сможет ответить «найди X» осмысленно — нет файла или alias.", + "", + "Лечение: либо создать stub в `projects//README.md` (см. `projects/lipki/` как образец), ", + "либо добавить имя пира как полную строку в `aliases` подходящего проекта.", + "", + "| NetBird-имя | IP | OS | Город |", + "|---|---|---|---|"]) + if orphans: + for o in orphans: + p = o["netbird_peers"][0] + md.append(f"| `{p['name']}` | {p['ip']} | {p.get('os','')} | {p.get('city','')} |") + else: + md.append("| — | — | — | — |") + md.append("") + md.append("✅ все онлайн-пиры покрыты") + md.append("") + + md.extend(["## Битые wiki-ссылки", ""]) + if wiki_issues: + for src, link, reason in wiki_issues[:50]: + md.append(f"- [{src}]({src}) — `{link}` {reason}") + if len(wiki_issues) > 50: + md.append(f"- ... ещё {len(wiki_issues)-50} (truncated до 50)") + else: + md.append("✅ битых ссылок не найдено") + md.append("") + + out.write_text("\n".join(md)) + print(f"Wrote {out.relative_to(VAULT)} (score={score})") + print(f" frontmatter issues: {len(fm_issues)}") + print(f" orphan online peers: {n_orphan_online}") + print(f" broken wiki links: {n_wiki_broken}") + + +if __name__ == "__main__": + main()