Files
SecondBrain/99 System/Export/agent-tools/vault_audit.py
T
Dmitry 3b597aad7d ada-pc: 2026-06-16 20:13:30 | 8
Affected files:
99 System/Export/agent-tools/AGENT_TOOLS.md
99 System/Export/agent-tools/vault_audit.py
99 System/Export/agent-tools/vault_common.py
99 System/Export/agent-tools/vault_frontmatter.py
99 System/Export/agent-tools/vault_index.py
99 System/Export/agent-tools/vault_ingest.py
99 System/INDEX.md
AGENTS.md
2026-06-16 20:13:30 +03:00

151 lines
5.0 KiB
Python

#!/usr/bin/env python3
from __future__ import annotations
import argparse
from collections import Counter, defaultdict
from vault_common import all_records, json_print, known_note_names, source_values, wikilinks
def broken_links(records: list[dict[str, object]]) -> list[dict[str, object]]:
known = known_note_names(records)
result = []
for record in records:
links = wikilinks(record["body"])
missing = sorted({link for link in links if link not in known})
if missing:
result.append({"path": record["path"], "missing": missing})
return result
def orphans(records: list[dict[str, object]]) -> list[dict[str, object]]:
incoming: Counter[str] = Counter()
aliases: dict[str, str] = {}
path_by_name: dict[str, str] = {}
for record in records:
path_by_name[record["name"]] = record["path"]
aliases[record["name"]] = record["name"]
aliases[record["path"]] = record["name"]
for alias in record["aliases"]:
aliases[alias] = record["name"]
for record in records:
for link in wikilinks(record["body"]):
target = aliases.get(link)
if target and target != record["name"]:
incoming[target] += 1
result = []
for name, path in sorted(path_by_name.items(), key=lambda item: item[1]):
if incoming[name] == 0 and not path.startswith("99 System/Archive/"):
result.append({"path": path, "name": name, "incoming": 0})
return result
def source_links(records: list[dict[str, object]]) -> dict[str, object]:
known = known_note_names(records)
linked = []
missing = []
plain = []
for record in records:
for source in source_values(record["frontmatter"].get("source")):
links = wikilinks(source)
if links:
for link in links:
item = {"path": record["path"], "source": source, "target": link}
if link in known:
linked.append(item)
else:
missing.append(item)
else:
plain.append({"path": record["path"], "source": source})
return {"linked": linked, "missing": missing, "plain": plain}
def graph_components(records: list[dict[str, object]]) -> list[dict[str, object]]:
known = known_note_names(records)
name_by_link = {}
for record in records:
name_by_link[record["name"]] = record["name"]
name_by_link[record["path"]] = record["name"]
for alias in record["aliases"]:
name_by_link[alias] = record["name"]
graph: dict[str, set[str]] = defaultdict(set)
path_by_name = {record["name"]: record["path"] for record in records}
for record in records:
name = record["name"]
graph.setdefault(name, set())
for link in wikilinks(record["body"]):
if link not in known:
continue
target = name_by_link.get(link)
if target and target != name:
graph[name].add(target)
graph[target].add(name)
seen = set()
components = []
for name in sorted(graph):
if name in seen:
continue
stack = [name]
component = []
seen.add(name)
while stack:
current = stack.pop()
component.append(current)
for next_name in graph[current]:
if next_name not in seen:
seen.add(next_name)
stack.append(next_name)
components.append(
{
"size": len(component),
"notes": [path_by_name[item] for item in sorted(component)[:20]],
"truncated": len(component) > 20,
}
)
return sorted(components, key=lambda item: item["size"])
def main() -> int:
parser = argparse.ArgumentParser(description="Agent-facing vault audit helper")
sub = parser.add_subparsers(dest="command", required=True)
sub.add_parser("broken-links")
sub.add_parser("orphans")
sub.add_parser("source-links")
sub.add_parser("components")
sub.add_parser("all")
args = parser.parse_args()
records = all_records()
if args.command == "broken-links":
json_print({"ok": True, "broken_links": broken_links(records)})
elif args.command == "orphans":
json_print({"ok": True, "orphans": orphans(records)})
elif args.command == "source-links":
json_print({"ok": True, "source_links": source_links(records)})
elif args.command == "components":
json_print({"ok": True, "components": graph_components(records)})
elif args.command == "all":
json_print(
{
"ok": True,
"broken_links": broken_links(records),
"orphans": orphans(records),
"source_links": source_links(records),
"components": graph_components(records),
}
)
return 0
if __name__ == "__main__":
raise SystemExit(main())