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>
This commit is contained in:
dttb
2026-05-06 16:39:45 +03:00
parent d4433bd0a8
commit f0b7feadc1
2 changed files with 257 additions and 0 deletions

195
scripts/kb-objects-audit.py Normal file
View File

@@ -0,0 +1,195 @@
#!/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()