chore: track claude skills, tools, templates, reference code and research-wiki

- Add all claude skills (brainstorming, commit, debugging, TDD, etc.)
- Add claude hooks (pre-commit-guard, post-edit-quality)
- Add research templates (experiment plan, research brief, etc.)
- Add claude tools (arxiv/semantic_scholar/openalex fetch, wiki, exa)
- Add TRM4 reference implementation as algorithm fidelity baseline
- Add research-wiki content (plans, index, graph, query_pack)
- Update .gitignore to exclude .graphify_version runtime state
This commit is contained in:
2026-07-06 20:59:03 -04:00
parent 0616d16956
commit 6bdb802f01
98 changed files with 19321 additions and 0 deletions
+185
View File
@@ -0,0 +1,185 @@
"""arXiv 搜索与 PDF 下载的独立命令行工具。"""
from __future__ import annotations
import argparse
import json
import sys
import time
import xml.etree.ElementTree as ET
from pathlib import Path
from typing import Any
from urllib.parse import quote
from urllib.request import Request, urlopen
ATOM_NS = "http://www.w3.org/2005/Atom"
ARXIV_API_URL = (
"http://export.arxiv.org/api/query?search_query=all:{query}&max_results={max}"
)
ARXIV_PDF_URL = "https://arxiv.org/pdf/{arxiv_id}.pdf"
USER_AGENT = "arxiv_fetch.py/1.0"
MIN_PDF_SIZE = 10 * 1024
DOWNLOAD_DELAY_SECONDS = 1.0
def _clean_text(value: str) -> str:
"""压缩空白并清理文本内容。"""
return " ".join(value.split())
def _normalize_arxiv_id(raw_id: str) -> str:
"""将 arXiv 标识归一化为不带版本号的论文 ID。"""
value = raw_id.strip()
if value.startswith("http://") or value.startswith("https://"):
value = value.rsplit("/", 1)[-1]
if value.startswith("abs/") or value.startswith("pdf/"):
value = value.split("/", 1)[1]
if value.endswith(".pdf"):
value = value[:-4]
if value.startswith("arXiv:") or value.startswith("arxiv:"):
value = value.split(":", 1)[1]
if "v" in value:
head, tail = value.rsplit("v", 1)
if tail.isdigit():
value = head
return value
def _request_text(url: str) -> bytes:
"""发起 HTTP 请求并返回原始字节内容。"""
request = Request(url, headers={"User-Agent": USER_AGENT})
with urlopen(request) as response:
return response.read()
def _parse_arxiv_feed(xml_bytes: bytes) -> list[dict[str, Any]]:
"""解析 arXiv Atom Feed 并提取论文条目。"""
root = ET.fromstring(xml_bytes)
namespace = {"atom": ATOM_NS}
results: list[dict[str, Any]] = []
for entry in root.findall("atom:entry", namespace):
title = _clean_text(
entry.findtext("atom:title", default="", namespaces=namespace)
)
abstract = _clean_text(
entry.findtext("atom:summary", default="", namespaces=namespace)
)
published = _clean_text(
entry.findtext("atom:published", default="", namespaces=namespace)
)
if "T" in published:
published = published.split("T", 1)[0]
authors: list[str] = []
for author in entry.findall("atom:author", namespace):
name = _clean_text(
author.findtext("atom:name", default="", namespaces=namespace)
)
if name:
authors.append(name)
categories: list[str] = []
for category in entry.findall("atom:category", namespace):
term = category.get("term", "").strip()
if term and term not in categories:
categories.append(term)
raw_id = entry.findtext("atom:id", default="", namespaces=namespace)
arxiv_id = _normalize_arxiv_id(raw_id.rsplit("/", 1)[-1] if raw_id else "")
results.append(
{
"title": title,
"authors": authors,
"arxiv_id": arxiv_id,
"abstract": abstract,
"categories": categories,
"published": published,
}
)
return results
def search_arxiv(query: str, max_results: int) -> list[dict[str, Any]]:
"""搜索 arXiv 并返回结构化论文结果。"""
encoded_query = quote(query, safe="")
url = ARXIV_API_URL.format(query=encoded_query, max=max_results)
feed_bytes = _request_text(url)
return _parse_arxiv_feed(feed_bytes)
def download_arxiv_pdf(arxiv_id: str, target_dir: Path) -> Path:
"""下载指定 arXiv 论文的 PDF 并校验文件大小。"""
normalized_id = _normalize_arxiv_id(arxiv_id)
target_dir.mkdir(parents=True, exist_ok=True)
output_path = target_dir / f"{normalized_id}.pdf"
pdf_url = ARXIV_PDF_URL.format(arxiv_id=normalized_id)
request = Request(pdf_url, headers={"User-Agent": USER_AGENT})
time.sleep(DOWNLOAD_DELAY_SECONDS)
try:
with urlopen(request) as response, output_path.open("wb") as handle:
while True:
chunk = response.read(8192)
if not chunk:
break
handle.write(chunk)
except Exception:
if output_path.exists():
output_path.unlink()
raise
if output_path.stat().st_size <= MIN_PDF_SIZE:
output_path.unlink(missing_ok=True)
raise ValueError(f"下载文件过小,疑似失败: {output_path}")
return output_path
def build_parser() -> argparse.ArgumentParser:
"""构建命令行参数解析器。"""
parser = argparse.ArgumentParser(description="arXiv 搜索与 PDF 下载工具")
subparsers = parser.add_subparsers(dest="command", required=True)
search_parser = subparsers.add_parser("search", help="搜索 arXiv 论文")
search_parser.add_argument("query", help="搜索关键词")
search_parser.add_argument("--max", type=int, default=10, help="最大返回数量")
download_parser = subparsers.add_parser("download", help="下载 arXiv PDF")
download_parser.add_argument("arxiv_id", help="arXiv 论文 ID")
download_parser.add_argument(
"--dir", dest="target_dir", default=".", help="保存目录"
)
return parser
def main(argv: list[str] | None = None) -> int:
"""CLI 入口。"""
parser = build_parser()
args = parser.parse_args(argv)
try:
if args.command == "search":
results = search_arxiv(args.query, args.max)
json.dump(results, sys.stdout, ensure_ascii=False, indent=2)
sys.stdout.write("\n")
return 0
if args.command == "download":
output_path = download_arxiv_pdf(args.arxiv_id, Path(args.target_dir))
sys.stdout.write(f"{output_path}\n")
return 0
except Exception as exc:
print(f"错误: {exc}", file=sys.stderr)
return 1
parser.error(f"未知命令: {args.command}")
return 2
if __name__ == "__main__":
sys.exit(main())