Ctrl K
ComunidadGuías…

scripts/audit_index_quality.py

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
#!/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())