Files
knowledge-base/scripts/kb-objects-audit.py
dttb f0b7feadc1 Phase 7: kb-objects-audit + первый weekly report (score 84)
Новый скрипт scripts/kb-objects-audit.py — еженедельный health-check vault'а:
1. Каждый projects/<dir>/README.md имеет валидный frontmatter (type/status/aliases)
2. Каждый онлайн-netbird-пир привязан к проекту через aliases или собственную карточку
3. Битые wiki-ссылки [[...]] не указывают в небытие

Output: audit/YYYY-MM-DD-objects-audit.md со score (меньше = лучше).

Первый запуск 2026-05-06: score=84
- 12/12 проектов с frontmatter ✓
- 3 online orphan-пира (DESKTOP-2IOQS54 Saransk, DESKTOP-AGBMLPN Helsinki, DESKTOP-HL0BB05 Lipetsk)
- 26 битых wiki-ссылок выявлено

Phase 6: dreaming включён (cron 0 3 * * *), recall promote'нул 17/39, weekly cron на promote.
Phase 8: на 137 — minScore=0.4 в memorySearch.query, IDENTITY.md разводит двух Максимок,
INFRASTRUCTURE.md переписан как навигатор по vault'у (не дубль).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 16:39:45 +03:00

196 lines
7.3 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env python3
"""kb-objects-audit — еженедельный health-check vault'а.
Проверки:
1. Каждый онлайн-netbird-пир имеет привязку к проекту (через aliases в frontmatter
или собственную карточку). Orphans = клиентские машины без описания.
2. Каждый projects/<dir>/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/<slug>/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()