From 7e4ed4b303bef4e1bd1ad80c3fb212ca3abc97b3 Mon Sep 17 00:00:00 2001 From: Ed_ Date: Mon, 24 Aug 2026 15:37:02 -0400 Subject: [PATCH] upate handle index script.. --- archive/threads/index.md | 3 + scripts/twitter/audit_handle_index.py | 46 +++++++- .../twitter/tests/test_audit_threads_index.py | 107 ++++++++++++++++++ 3 files changed, 150 insertions(+), 6 deletions(-) create mode 100644 scripts/twitter/tests/test_audit_threads_index.py diff --git a/archive/threads/index.md b/archive/threads/index.md index d22f2fc..96ade6a 100644 --- a/archive/threads/index.md +++ b/archive/threads/index.md @@ -14,4 +14,7 @@ How to read a folder: [[notes/How threads are organized]]. Example of the note shape (single spine, no branches): [[archive/threads/How to archive a thread]] - [[archive/threads/NOTimothyLottes]] +- [[archive/threads/VPCOMPRESSB]] +- [[archive/threads/VictorTaelin]] - [[archive/threads/rianflo]] +- [[archive/threads/winning_tactic]] diff --git a/scripts/twitter/audit_handle_index.py b/scripts/twitter/audit_handle_index.py index 830d8e2..84b888b 100644 --- a/scripts/twitter/audit_handle_index.py +++ b/scripts/twitter/audit_handle_index.py @@ -1,11 +1,19 @@ -"""Audit every handle index in the vault against real subdirectories. +"""Audit every handle index in the vault against real subdirectories, +and audit the top-level `archive/threads/index.md` against real +handle dirs. -Reports per handle: +For each handle dir: real — set of subdirectory basenames on disk links — set of basenames extracted from index.md wikilinks missing — subdirectories with no wikilink in index.md stale — wikilinks in index.md whose subdirectory does not exist +For the threads index (top-level archive/threads/index.md): + real — set of handle dir basenames on disk + links — set of handle basenames extracted from wikilinks + missing — handle dirs with no wikilink + stale — wikilinks whose handle dir does not exist + Usage: python scripts/twitter/audit_handle_index.py # text report python scripts/twitter/audit_handle_index.py --json # JSON report @@ -70,6 +78,26 @@ def audit_one(handle_dir: Path) -> dict[str, object]: } +def audit_threads_index(threads_root: Path) -> dict[str, object]: + """Audit the top-level archive/threads/index.md against real + handle dirs.""" + idx_path = threads_root / "index.md" + real = { + d.name for d in threads_root.iterdir() if d.is_dir() and not d.name.startswith(".") + } + links = _index_links(idx_path) + missing = sorted(real - links) + stale = sorted(links - real) + return { + "handle": "", + "has_index": idx_path.is_file(), + "real": sorted(real), + "links": sorted(links), + "missing": missing, + "stale": stale, + } + + def main(argv: list[str] | None = None) -> int: parser = argparse.ArgumentParser(description=__doc__) parser.add_argument( @@ -92,6 +120,7 @@ def main(argv: list[str] | None = None) -> int: if not entry.is_dir() or entry.name.startswith("."): continue reports.append(audit_one(entry)) + reports.append(audit_threads_index(threads_root)) if args.json: print(json.dumps(reports, indent=2, ensure_ascii=False)) @@ -120,20 +149,25 @@ def main(argv: list[str] | None = None) -> int: print() if flagged: - print(f"flagged {flagged} handle(s):") + print(f"flagged {flagged} entry/entries:") for r in reports: if not (r["missing"] or r["stale"]): continue + label = ( + "threads index" + if r["handle"] == "" + else f"{r['handle']} handle index" + ) if r["missing"]: missing_sample = ", ".join(r["missing"][:5]) if len(r["missing"]) > 5: missing_sample += f", ... (+{len(r['missing']) - 5} more)" - print(f" {r['handle']}: missing_in_index = {missing_sample}") + print(f" {label}: missing_in_index = {missing_sample}") if r["stale"]: stale_sample = ", ".join(r["stale"]) - print(f" {r['handle']}: stale_in_index = {stale_sample}") + print(f" {label}: stale_in_index = {stale_sample}") else: - print("clean: no handle has missing or stale wikilinks") + print("clean: no handle or threads index has missing or stale wikilinks") return 0 diff --git a/scripts/twitter/tests/test_audit_threads_index.py b/scripts/twitter/tests/test_audit_threads_index.py new file mode 100644 index 0000000..aef5632 --- /dev/null +++ b/scripts/twitter/tests/test_audit_threads_index.py @@ -0,0 +1,107 @@ +from __future__ import annotations + +import json +import shutil +import tempfile +import unittest +from pathlib import Path + +from twitter.audit_handle_index import ( + audit_one, + audit_threads_index, + main, +) + + +def _write_handle( + handle_dir: Path, + wikilinks: list[str] | None, + subdirs: list[str] | None, +) -> None: + handle_dir.mkdir(parents=True, exist_ok=True) + for name in subdirs or []: + (handle_dir / name).mkdir() + if wikilinks is not None: + body = "\n".join(f"- [[archive/threads/{handle_dir.name}/{w}]]" for w in wikilinks) + (handle_dir / "index.md").write_text( + "---\ntitle: x\n---\n\n" + body + "\n", + encoding="utf-8", + newline="\n", + ) + + +def _write_threads_index(threads_root: Path, handles: list[str]) -> None: + body = "\n".join(f"- [[archive/threads/{h}]]" for h in handles) + (threads_root / "index.md").write_text( + "---\ntitle: Threads\n---\n\n" + body + "\n", + encoding="utf-8", + newline="\n", + ) + + +class ThreadsIndexAuditTests(unittest.TestCase): + def setUp(self) -> None: + self.tmp = Path(tempfile.mkdtemp(prefix="threads_audit_")) + self.threads_root = self.tmp / "archive" / "threads" + self.threads_root.mkdir(parents=True) + + def tearDown(self) -> None: + shutil.rmtree(self.tmp, ignore_errors=True) + + def test_threads_index_clean(self) -> None: + _write_handle(self.threads_root / "alice", None, ["2024-01-01"]) + _write_handle(self.threads_root / "bob", None, ["2024-02-02"]) + _write_threads_index(self.threads_root, ["alice", "bob"]) + result = audit_threads_index(self.threads_root) + self.assertEqual(result["missing"], []) + self.assertEqual(result["stale"], []) + self.assertEqual(set(result["real"]), {"alice", "bob"}) + + def test_threads_index_missing_handle(self) -> None: + # bob exists on disk but is not wikilinked in threads index + _write_handle(self.threads_root / "alice", None, []) + _write_handle(self.threads_root / "bob", None, []) + _write_threads_index(self.threads_root, ["alice"]) + result = audit_threads_index(self.threads_root) + self.assertEqual(result["missing"], ["bob"]) + self.assertEqual(result["stale"], []) + + def test_threads_index_stale_wikilink(self) -> None: + # ghost is wikilinked but no handle dir exists + _write_handle(self.threads_root / "alice", None, []) + _write_threads_index(self.threads_root, ["alice", "ghost"]) + result = audit_threads_index(self.threads_root) + self.assertEqual(result["missing"], []) + self.assertEqual(result["stale"], ["ghost"]) + + +class AuditorCliTests(unittest.TestCase): + def setUp(self) -> None: + self.tmp = Path(tempfile.mkdtemp(prefix="threads_audit_cli_")) + self.threads_root = self.tmp / "archive" / "threads" + self.threads_root.mkdir(parents=True) + + def tearDown(self) -> None: + shutil.rmtree(self.tmp, ignore_errors=True) + + def test_main_text_includes_threads_index_entry(self) -> None: + _write_handle(self.threads_root / "alice", None, []) + _write_handle(self.threads_root / "bob", None, []) + _write_threads_index(self.threads_root, ["alice"]) + # bob is missing from the threads index; main() should flag it + import io + from contextlib import redirect_stdout + + buf = io.StringIO() + with redirect_stdout(buf): + rc = main(["--vault", str(self.tmp)]) + self.assertEqual(rc, 0) + out = buf.getvalue() + self.assertIn("alice", out) + self.assertIn("bob", out) + self.assertIn("threads index", out) + self.assertIn("missing_in_index", out) + + +if __name__ == "__main__": + unittest.main() \ No newline at end of file