vault-cli: extract to standalone repo, add flake.nix, move one-off scripts to scripts/one-off/
This commit is contained in:
@@ -0,0 +1,80 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Scan /vault for [[wikilinks]] and report which targets don't resolve
|
||||
to an actual note. Obsidian resolves links by basename match anywhere
|
||||
in the vault (unless the link includes a path), so we build an index
|
||||
of all note basenames (and their full relative paths) first.
|
||||
"""
|
||||
import os, re, json
|
||||
from collections import defaultdict
|
||||
|
||||
VAULT = "/vault"
|
||||
LINK_RE = re.compile(r'\[\[([^\]|#]+)(?:#[^\]|]*)?(?:\|[^\]]*)?\]\]')
|
||||
|
||||
def all_notes():
|
||||
idx = defaultdict(list) # basename (no ext, lowercase) -> [full paths]
|
||||
all_paths = []
|
||||
for dp, dirnames, fns in os.walk(VAULT):
|
||||
if ".git" in dp.split(os.sep):
|
||||
continue
|
||||
for fn in fns:
|
||||
if fn.lower().endswith(".md"):
|
||||
p = os.path.join(dp, fn)
|
||||
base = fn[:-3]
|
||||
idx[base.lower()].append(p)
|
||||
all_paths.append(p)
|
||||
return idx, all_paths
|
||||
|
||||
def resolve(link_target, index, current_dir):
|
||||
# Obsidian: link may be a bare name, or a relative/vault path (with /)
|
||||
target = link_target.strip()
|
||||
if "/" in target:
|
||||
# path-style link; try resolving relative to vault root or current dir
|
||||
candidates = [
|
||||
os.path.join(VAULT, target + ".md"),
|
||||
os.path.join(VAULT, target),
|
||||
os.path.join(current_dir, target + ".md"),
|
||||
os.path.join(current_dir, target),
|
||||
]
|
||||
for c in candidates:
|
||||
if os.path.isfile(c):
|
||||
return True
|
||||
# also try basename match as fallback
|
||||
base = os.path.basename(target)
|
||||
return base.lower() in index
|
||||
else:
|
||||
return target.lower() in index
|
||||
|
||||
def main():
|
||||
index, all_paths = all_notes()
|
||||
unresolved = defaultdict(list) # target -> [source files]
|
||||
total_links = 0
|
||||
for p in all_paths:
|
||||
try:
|
||||
with open(p, "r", encoding="utf-8", errors="ignore") as f:
|
||||
text = f.read()
|
||||
except Exception:
|
||||
continue
|
||||
for m in LINK_RE.finditer(text):
|
||||
target = m.group(1)
|
||||
total_links += 1
|
||||
if not resolve(target, index, os.path.dirname(p)):
|
||||
unresolved[target].append(os.path.relpath(p, VAULT))
|
||||
|
||||
print(f"Total wikilinks scanned: {total_links}")
|
||||
print(f"Unique unresolved targets: {len(unresolved)}")
|
||||
print(f"Total unresolved link occurrences: {sum(len(v) for v in unresolved.values())}")
|
||||
|
||||
# dump full detail to json for further processing
|
||||
out = {t: srcs for t, srcs in unresolved.items()}
|
||||
with open("/tmp/unresolved_links.json", "w") as f:
|
||||
json.dump(out, f, indent=2)
|
||||
print("Full detail written to /tmp/unresolved_links.json")
|
||||
|
||||
# sorted by occurrence count desc
|
||||
by_count = sorted(unresolved.items(), key=lambda kv: -len(kv[1]))
|
||||
print("\n=== TOP 40 unresolved targets by occurrence count ===")
|
||||
for target, srcs in by_count[:40]:
|
||||
print(f" {len(srcs):4d}x [[{target}]]")
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user