ada-pc: 2026-06-16 20:18:31 | 9
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
This commit is contained in:
@@ -0,0 +1,150 @@
|
||||
#!/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())
|
||||
Reference in New Issue
Block a user