#!/usr/bin/env python3 """ Audit x-knowledge-base search index quality without mutating data. Checks for: - low-signal titles - placeholder/weak summaries - invalid or suspicious source URLs - legacy card paths - missing source URLs - duplicate source URLs Usage: python3 scripts/audit_index_quality.py python3 scripts/audit_index_quality.py --json """ from __future__ import annotations import argparse import json import os import re from collections import defaultdict import sys from pathlib import Path from typing import Any sys.path.insert(0, str(Path(__file__).resolve().parent)) import xkb_paths WORKSPACE = xkb_paths.WORKSPACE BOOKMARKS_DIR = xkb_paths.BOOKMARKS_DIR INDEX_FILE = xkb_paths.INDEX_FILE LOW_SIGNAL_SUMMARIES = {"", "(待整理)", "待整理", "todo", "tbd", "n/a"} def clean_summary(text: str) -> str: text = (text or "").strip() text = re.sub(r"^#+\s*", "", text) text = re.sub(r"^一句話摘要\s*", "", text) text = re.sub(r"^[-•]\s*", "", text) text = re.sub(r"\s+", " ", text) return text.strip() def is_valid_source_url(url: str) -> bool: url = (url or "").strip() if not url.startswith(("http://", "https://")): return False x_status = re.search(r"https?://(?:x|twitter)\.com/[^\s/]+/status/(\d{15,20})(?:\b|/|\?)", url) x_i_status = re.search(r"https?://x\.com/i/status/(\d{15,20})(?:\b|/|\?)", url) if ("x.com" in url or "twitter.com" in url) and not (x_status or x_i_status): return False return True def load_items() -> list[dict[str, Any]]: raw = json.loads(INDEX_FILE.read_text(encoding="utf-8")) return raw.get("items", raw) if isinstance(raw, dict) else raw def classify_item(item: dict[str, Any]) -> dict[str, Any]: title = (item.get("title") or "").strip() summary = clean_summary(item.get("summary") or "") source_url = (item.get("source_url") or "").strip() rel_path = (item.get("relative_path") or item.get("path") or "").strip() category = (item.get("category") or "").strip() issues: list[str] = [] if not title: issues.append("missing_title") elif re.fullmatch(r"\d{15,20}", title): issues.append("numeric_title") elif re.fullmatch(r"tweet\s+\d{15,20}", title.lower()): issues.append("tweet_numeric_title") elif re.match(r"^\d{4}-\d{2}-\d{2}-", title) and len(title) < 42: issues.append("date_slug_title") elif len(title) < 8: issues.append("very_short_title") if summary in LOW_SIGNAL_SUMMARIES: issues.append("low_signal_summary") elif len(summary) < 18: issues.append("very_short_summary") if not source_url: issues.append("missing_source_url") elif not is_valid_source_url(source_url): issues.append("invalid_source_url") if "/memory/cards/legacy-" in rel_path or rel_path.startswith("memory/cards/legacy-"): issues.append("legacy_card") elif "/legacy-" in rel_path or Path(rel_path).stem.startswith("legacy-"): issues.append("legacy_card") if rel_path.startswith("/"): issues.append("absolute_relative_path") if item.get("excluded"): issues.append("excluded") if item.get("duplicate_of"): issues.append("duplicate_entry") return { "title": title, "summary": summary, "source_url": source_url, "relative_path": rel_path, "category": category, "issues": issues, } def main() -> int: parser = argparse.ArgumentParser(description="Audit x-knowledge-base index quality") parser.add_argument("--json", action="store_true") args = parser.parse_args() items = load_items() issue_buckets: dict[str, list[dict[str, Any]]] = defaultdict(list) source_buckets: dict[str, list[dict[str, Any]]] = defaultdict(list) for item in items: audited = classify_item(item) for issue in audited["issues"]: issue_buckets[issue].append(audited) source_url = audited["source_url"] if source_url: source_buckets[source_url].append(audited) duplicates = {url: rows for url, rows in source_buckets.items() if len(rows) > 1} report = { "total_items": len(items), "issue_counts": {k: len(v) for k, v in sorted(issue_buckets.items())}, "duplicate_source_url_count": len(duplicates), "duplicate_source_url_examples": { k: [r["relative_path"] for r in v[:5]] for k, v in list(duplicates.items())[:10] }, "examples": { k: v[:8] for k, v in sorted(issue_buckets.items()) }, } if args.json: print(json.dumps(report, ensure_ascii=False, indent=2)) return 0 print(f"總項目數:{report['total_items']}") print("\n問題統計:") for key, count in report["issue_counts"].items(): print(f"- {key}: {count}") print(f"\n重複 source_url:{report['duplicate_source_url_count']}") for url, paths in report["duplicate_source_url_examples"].items(): print(f"- {url}") for path in paths: print(f" - {path}") print("\n各類問題範例:") for key, rows in report["examples"].items(): print(f"\n## {key} ({len(issue_buckets[key])})") for row in rows[:5]: print(f"- {row['relative_path']}") print(f" title: {row['title']}") print(f" summary: {row['summary'][:120]}") print(f" source: {row['source_url']}") return 0 import xkb_usage # noqa: E402 — 量測誰在跑,見 scripts/xkb_usage.py if __name__ == "__main__": xkb_usage.record(__file__) raise SystemExit(main())