From 4de2d92bbc2089cc1b736a6c8567010a2733ce1a Mon Sep 17 00:00:00 2001 From: Hermes Agent Date: Wed, 22 Jul 2026 11:36:51 -0400 Subject: [PATCH] vault-cli: TUI-style colored --pretty report --- __pycache__/vault-librarian.cpython-313.pyc | Bin 0 -> 23387 bytes vault-librarian.py | 254 ++++++++++++++++---- 2 files changed, 205 insertions(+), 49 deletions(-) create mode 100644 __pycache__/vault-librarian.cpython-313.pyc diff --git a/__pycache__/vault-librarian.cpython-313.pyc b/__pycache__/vault-librarian.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..8ae5df01471ca654b7434af1b521f1a62e610a68 GIT binary patch literal 23387 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 YEIb`mmzgDRvT(GzePCu_kphPp08Ld~!vFvP literal 0 HcmV?d00001 diff --git a/vault-librarian.py b/vault-librarian.py index c4bdea0..48d772a 100644 --- a/vault-librarian.py +++ b/vault-librarian.py @@ -147,66 +147,222 @@ def build_report(): } +# ───────────────────────────────────────────────────────────────────────────── +# TUI color helpers +# ───────────────────────────────────────────────────────────────────────────── +import shutil + +_TTY = sys.stdout.isatty() and os.environ.get("NO_COLOR") is None + +# ANSI color codes +_BOLD = "\033[1m" +_DIM = "\033[2m" +_RED = "\033[31m" +_GREEN = "\033[32m" +_YELLOW = "\033[33m" +_CYAN = "\033[36m" +_RESET = "\033[0m" + + +def c(text, code): + """Apply color code if TTY; return plain text otherwise.""" + if not _TTY: + return text + return f"{code}{text}{_RESET}" + + +def rule(title): + """Draw a full-width section header with title.""" + width = shutil.get_terminal_size((100, 20)).columns + # Format: "━ TITLE " padded to width with ━ + prefix = "━ " + suffix = " " + available = width - len(prefix) - len(suffix) - len(title) + if available < 0: + available = 0 + padding = "━" * available + return c(f"{prefix}{title}{suffix}{padding}", _CYAN) + + +def badge(count, good_if_zero=True): + """Return a color-coded count badge.""" + if count == 0: + return c("✓ none", _GREEN) if good_if_zero else c("0", _GREEN) + elif count <= 3: + return c(str(count), _YELLOW) + else: + return c(str(count), _RED) + + +def box_summary(counts): + """Render a boxed summary header with counts.""" + # Build content lines + items = [ + (c("Systems", _DIM), c(str(counts["systems"]), _CYAN + _BOLD)), + (c("Areas", _DIM), c(str(counts["areas"]), _CYAN + _BOLD)), + (c("Categories", _DIM), c(str(counts["categories"]), _CYAN + _BOLD)), + (c("IDs", _DIM), c(str(counts["ids"]), _CYAN + _BOLD)), + ] + # Calculate width accounting for ANSI codes (rough estimate) + box_width = shutil.get_terminal_size((100, 20)).columns + + # Build box + if _TTY: + top = f"┌─{c('─' * (box_width - 4), _CYAN)}─┐" + else: + top = f"┌─{'─' * (box_width - 4)}─┐" + + content = " " + " · ".join(f"{label} {value}" for label, value in items) + " " + mid = f"│ {content} │" + + if _TTY: + bot = f"└─{c('─' * (box_width - 4), _CYAN)}─┘" + else: + bot = f"└─{'─' * (box_width - 4)}─┘" + + return f"{top}\n{mid}\n{bot}" + + def pretty(r): L = [] - c = r["counts"] - L.append(f"# Vault Librarian report") - L.append(f"{c['systems']} systems · {c['areas']} areas · {c['categories']} categories · {c['ids']} IDs\n") - + c_counts = r["counts"] + + # Summary banner + L.append(box_summary(c_counts)) + L.append("") + + # Broken links bl = r["broken_links"] - L.append(f"## Broken wikilinks: {bl['count']}") - if bl["guessable"]: - L.append(f"**Likely fixable ({len(bl['guessable'])})** — target renamed, single candidate:") - for b in bl["guessable"][:30]: - L.append(f" - `{b['file']}`: [[{b['target']}]] → guess [[{b['guess']}]]") - if bl["unresolvable"]: - L.append(f"**Unresolvable ({len(bl['unresolvable'])})** — target never existed / dropped stub:") - for b in bl["unresolvable"][:30]: - L.append(f" - `{b['file']}`: [[{b['target']}]]") + if bl["count"] == 0: + L.append(c(rule("Broken wikilinks"), _GREEN)) + L.append(f" {c('✓ none', _GREEN)}") + else: + L.append(c(rule(f"Broken wikilinks — {bl['count']} found"), _RED)) + if bl["guessable"]: + guessable_len = len(bl["guessable"]) + L.append(f" {c(f'Likely fixable ({guessable_len})', _GREEN)} — target renamed, single candidate:") + for b in bl["guessable"][:30]: + target = b['target'] + guess = b['guess'] + L.append(f" {c(b['file'], _DIM)}: {c(f'[[{target}]]', _BOLD)} {c('→', _DIM)} guess {c(f'[[{guess}]]', _BOLD)}") + if len(bl["guessable"]) > 30: + more = len(bl["guessable"]) - 30 + L.append(f" {c(f'… and {more} more', _DIM)}") + if bl["unresolvable"]: + unresolvable_len = len(bl["unresolvable"]) + L.append(f" {c(f'Unresolvable ({unresolvable_len})', _YELLOW)} — target never existed / dropped stub:") + for b in bl["unresolvable"][:30]: + target = b['target'] + L.append(f" {c(b['file'], _DIM)}: {c(f'[[{target}]]', _BOLD)}") + if len(bl["unresolvable"]) > 30: + more = len(bl["unresolvable"]) - 30 + L.append(f" {c(f'… and {more} more', _DIM)}") L.append("") - + + # Topology t = r["topology"] - L.append("## Cross-tree topology (/vault vs /data)") - for level in ("systems", "areas", "cats", "ids"): - md = t[level]["missing_in_data"]; od = t[level]["orphan_in_data"] - if md: - L.append(f" - **{level} missing /data home ({len(md)}):** {', '.join(md[:20])}{' …' if len(md)>20 else ''}") - if od: - L.append(f" - **{level} orphan in /data ({len(od)}):** {', '.join(od[:20])}") + has_mismatch = any( + t[level]["missing_in_data"] or t[level]["orphan_in_data"] + for level in ("systems", "areas", "cats", "ids") + ) + + if not has_mismatch: + L.append(c(rule("Cross-tree topology (/vault vs /data)"), _GREEN)) + L.append(f" {c('✓ in sync', _GREEN)}") + else: + L.append(c(rule("Cross-tree topology — mismatches found"), _YELLOW)) + for level in ("systems", "areas", "cats", "ids"): + md = t[level]["missing_in_data"] + od = t[level]["orphan_in_data"] + if md: + md_str = ", ".join(md[:20]) + (f" {c('…', _DIM)}" if len(md) > 20 else "") + L.append(f" {c(f'{level} missing /data ({len(md)})', _YELLOW)}: {md_str}") + if od: + od_str = ", ".join(od[:20]) + (f" {c('…', _DIM)}" if len(od) > 20 else "") + L.append(f" {c(f'{level} orphan in /data ({len(od)})', _YELLOW)}: {od_str}") L.append("") - + + # Structure st = r["structure"] - L.append("## JD structural") - if st["extend_notation"]: - L.append(f" - **extend-notation `NN+` needing real IDs ({len(st['extend_notation'])}):**") - for e in st["extend_notation"]: - L.append(f" {e}") - if st["missing_dot00"]: - L.append(f" - **categories missing a .00 note ({len(st['missing_dot00'])}):** {', '.join(sorted(st['missing_dot00'])[:25])}") - if st["missing_folder_note"]: - L.append(f" - **folders missing a folder-note ({len(st['missing_folder_note'])}):** {len(st['missing_folder_note'])} (Folder Notes plugin)") - L.append("") - + has_struct_issues = bool(st["extend_notation"] or st["missing_dot00"] or st["missing_folder_note"]) + + if has_struct_issues: + L.append(c(rule("JD structural issues"), _YELLOW)) + if st["extend_notation"]: + extend_len = len(st["extend_notation"]) + L.append(f" {c(f'extend-notation `NN+` needing real IDs ({extend_len})', _YELLOW)}:") + for e in st["extend_notation"][:20]: + L.append(f" {c(e, _DIM)}") + if len(st["extend_notation"]) > 20: + more = len(st["extend_notation"]) - 20 + L.append(f" {c(f'… and {more} more', _DIM)}") + if st["missing_dot00"]: + codes = ", ".join(sorted(st["missing_dot00"])[:25]) + if len(st["missing_dot00"]) > 25: + codes += f" {c('…', _DIM)}" + dot00_len = len(st["missing_dot00"]) + L.append(f" {c(f'categories missing .00 note ({dot00_len})', _YELLOW)}: {codes}") + if st["missing_folder_note"]: + folder_len = len(st["missing_folder_note"]) + L.append(f" {c(f'folders missing folder-note ({folder_len})', _YELLOW)}") + L.append("") + + # To-dos td = r["todos"] - L.append(f"## To-dos: {td['count']} open ({len(td['pressing'])} pressing, {len(td['long_term'])} long-term)") - L.append(f"**Pressing ({len(td['pressing'])}):**") - for x in td["pressing"][:40]: - L.append(f" - `{x['file']}`:{x['line']} {x['text']}") - L.append(f"**Long-term ({len(td['long_term'])})** — standing checklists, not nagged:") - for x in td["long_term"][:15]: - L.append(f" - `{x['file']}`:{x['line']} {x['text']}") - if len(td["long_term"]) > 15: - L.append(f" … and {len(td['long_term'])-15} more long-term") + total_todos = td['count'] + L.append(c(rule(f"To-dos: {c(str(total_todos), _BOLD)} open"), _CYAN)) + pressing_len = len(td["pressing"]) + longterm_len = len(td["long_term"]) + L.append(f" {c(f'Pressing: {pressing_len}', _BOLD)} / {c(f'Long-term: {longterm_len}', _DIM)}") + + if td["pressing"]: + L.append(f" {c('Pressing:', _BOLD)}") + for x in td["pressing"][:40]: + file_info = c(f'{x["file"]}:{x["line"]}', _CYAN) + L.append(f" {file_info} {x['text']}") + if len(td["pressing"]) > 40: + more = len(td["pressing"]) - 40 + L.append(f" {c(f'… and {more} more', _DIM)}") + + if td["long_term"]: + L.append(f" {c('Long-term (standing checklists):', _DIM)}") + for x in td["long_term"][:15]: + file_info = c(f'{x["file"]}:{x["line"]}', _DIM) + text_info = c(x['text'], _DIM) + L.append(f" {file_info} {text_info}") + if len(td["long_term"]) > 15: + more = len(td["long_term"]) - 15 + L.append(f" {c(f'… and {more} more', _DIM)}") L.append("") - + + # Category health ch = r["category_health"] - if ch["near_id_limit"]: - L.append("## Category health") + if ch["near_id_limit"] or ch["duplicate_titles"]: + L.append(c(rule("Category health"), _YELLOW)) for x in ch["near_id_limit"]: - L.append(f" - `{x['category']}` has {x['ids']} IDs (near 10 limit)") - if ch["duplicate_titles"]: - L.append(f" - duplicate ID titles across systems ({len(ch['duplicate_titles'])}): " - + "; ".join(f"{t} {v}" for t, v in list(ch["duplicate_titles"].items())[:10])) + category = c(x['category'], _BOLD) + ids_count = c(str(x['ids']), _YELLOW) + L.append(f" {category} — {ids_count} IDs, near 10 limit") + if ch["duplicate_titles"]: + for t, codes in list(ch["duplicate_titles"].items())[:10]: + codes_str = ", ".join(codes) + L.append(f" {c('⚠ duplicate title:', _YELLOW)} {t} {c(codes_str, _DIM)}") + L.append("") + + # Footer status bar + issues_count = sum([ + bl["count"], + sum(1 for level in ("systems", "areas", "cats", "ids") + if t[level]["missing_in_data"] or t[level]["orphan_in_data"]), + len(st["extend_notation"]) + len(st["missing_dot00"]) + len(st["missing_folder_note"]), + len(ch["near_id_limit"]) + len(ch["duplicate_titles"]), + ]) + + if issues_count == 0: + L.append(c("● VAULT HEALTHY", _GREEN)) + else: + L.append(c(f"● {issues_count} item(s) need attention", _YELLOW)) + return "\n".join(L)