22eda53d79
Affected files: .obsidian/graph.json 99 System/INDEX.md 99 System/Tools/agent-tools/AGENT_TOOLS.md 99 System/Tools/agent-tools/vault_audit.py 99 System/Tools/agent-tools/vault_common.py 99 System/Tools/agent-tools/vault_frontmatter.py 99 System/Tools/agent-tools/vault_index.py 99 System/Tools/agent-tools/vault_ingest.py AGENTS.md
151 lines
5.0 KiB
Python
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())
|