Phase 0-2: Schema cleanup, typed relations, event-driven automation

- Phase 0: AGENTS.md cleanup (dedup quotes, renumber sections, merge qmd)
- Phase 1: typed relations (manage-relations.py, graph-search.py, check-staleness.py, detect-conflicts.py)
- Phase 2: frontmatter validator, weekly lint, knowledge promotion, git hooks
- Fix .gitignore to track tools/ and .githooks/
- Fix git remote URL (remove plaintext token)
- New wiki pages: 504 pages, 34 raw sources
This commit is contained in:
hehaiguang1123
2026-07-01 08:05:43 +08:00
parent e544d6e04a
commit a6f05ab2d5
1067 changed files with 522992 additions and 819 deletions
@@ -0,0 +1,278 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import sys
import io
import re
import datetime
from pathlib import Path
from typing import Dict, List, Set, Tuple
from collections import defaultdict
if sys.platform == "win32":
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding="utf-8")
sys.stderr = io.TextIOWrapper(sys.stderr.buffer, encoding="utf-8")
class WikiLinkAnalyzer:
def __init__(self, wiki_dir: Path):
self.wiki_dir = wiki_dir
self.outgoing_links: Dict[str, Set[str]] = defaultdict(set)
self.incoming_links: Dict[str, Set[str]] = defaultdict(set)
self.all_files: Set[str] = set()
self.files_with_content: Dict[str, int] = {}
def extract_wikilinks(self, content: str) -> List[str]:
pattern = r"\[\[([^\]|]+)(?:\|[^\]]+)?\]\]"
return re.findall(pattern, content)
def normalize_link(self, link: str) -> str:
link = link.strip()
link = link.replace(" ", "-")
return link
def analyze_file(self, file_path: Path) -> Tuple[Set[str], int]:
try:
content = file_path.read_text(encoding="utf-8")
links = self.extract_wikilinks(content)
normalized_links = {self.normalize_link(l) for l in links}
return normalized_links, len(content)
except Exception as e:
return set(), 0
def analyze_all(self):
md_files = list(self.wiki_dir.glob("*.md"))
print(f"分析 {len(md_files)} 个Wiki文件...")
for md_file in md_files:
filename = md_file.stem
self.all_files.add(filename)
outgoing, content_length = self.analyze_file(md_file)
self.outgoing_links[filename] = outgoing
self.files_with_content[filename] = content_length
for link in outgoing:
self.incoming_links[link].add(filename)
def find_orphaned_files(self) -> List[str]:
orphaned = []
for filename in self.all_files:
if (
filename not in self.incoming_links
or len(self.incoming_links[filename]) == 0
):
orphaned.append(filename)
return orphaned
def find_dangling_links(self) -> Dict[str, List[str]]:
dangling = {}
for filename, links in self.outgoing_links.items():
broken = []
for link in links:
if link not in self.all_files:
broken.append(link)
if broken:
dangling[filename] = broken
return dangling
def calculate_link_stats(self) -> Dict:
stats = {
"total_files": len(self.all_files),
"files_with_outgoing": 0,
"files_with_incoming": 0,
"total_outgoing_links": 0,
"total_incoming_links": 0,
"avg_outgoing_per_file": 0,
"avg_incoming_per_file": 0,
}
for links in self.outgoing_links.values():
stats["total_outgoing_links"] += len(links)
for links in self.incoming_links.values():
stats["total_incoming_links"] += len(links)
stats["files_with_outgoing"] = sum(1 for l in self.outgoing_links.values() if l)
stats["files_with_incoming"] = sum(1 for l in self.incoming_links.values() if l)
if stats["total_files"] > 0:
stats["avg_outgoing_per_file"] = (
stats["total_outgoing_links"] / stats["total_files"]
)
stats["avg_incoming_per_file"] = (
stats["total_incoming_links"] / stats["total_files"]
)
return stats
def get_top_linked_files(self, limit: int = 20) -> List[Tuple[str, int]]:
files = [(f, len(links)) for f, links in self.incoming_links.items()]
files.sort(key=lambda x: x[1], reverse=True)
return files[:limit]
def generate_report(self) -> str:
stats = self.calculate_link_stats()
orphaned = self.find_orphaned_files()
dangling = self.find_dangling_links()
top_linked = self.get_top_linked_files()
report = f"""# Wiki双链分析报告
## 总体统计
| 指标 | 值 |
|------|-----|
| 总文件数 | {stats["total_files"]} |
| 有外链的文件 | {stats["files_with_outgoing"]} ({stats["files_with_outgoing"] / stats["total_files"] * 100:.1f}%) |
| 有入链的文件 | {stats["files_with_incoming"]} ({stats["files_with_incoming"] / stats["total_files"] * 100:.1f}%) |
| 外链总数 | {stats["total_outgoing_links"]} |
| 入链总数 | {stats["total_incoming_links"]} |
| 平均每文件外链 | {stats["avg_outgoing_per_file"]:.1f} |
| 平均每文件入链 | {stats["avg_incoming_per_file"]:.1f} |
## 孤立文件(无入链)
{len(orphaned)} 个文件没有任何页面引用:
"""
for f in sorted(orphaned):
content_size = self.files_with_content.get(f, 0)
outgoing = len(self.outgoing_links.get(f, []))
report += f"- [[{f}]] ({content_size}字, {outgoing}个外链)\n"
report += f"""
## 悬空链接(指向不存在的页面)
"""
if not dangling:
report += "无悬空链接 ✅\n"
else:
for filename, links in sorted(dangling.items()):
report += f"**{filename}**: {', '.join(links)}\n"
report += f"""
## 高链接页面(入链最多)
| 页面 | 入链数 |
|------|--------|
"""
for filename, count in top_linked:
report += f"| [[{filename}]] | {count} |\n"
report += f"""
## 双向链接分析
"""
bidirectional = 0
one_way = 0
no_links = 0
for filename in self.all_files:
outgoing = self.outgoing_links.get(filename, set())
incoming = self.incoming_links.get(filename, set())
common = outgoing & incoming
if common:
bidirectional += 1
elif outgoing or incoming:
one_way += 1
else:
no_links += 1
report += f"""| 类型 | 数量 | 占比 |
|------|------|------|
| 双向链接 | {bidirectional} | {bidirectional / stats["total_files"] * 100:.1f}% |
| 单向链接 | {one_way} | {one_way / stats["total_files"] * 100:.1f}% |
| 无链接 | {no_links} | {no_links / stats["total_files"] * 100:.1f}% |
## 改进建议
### 1. 消除孤立文件
- 为 {len(orphaned)} 个孤立文件添加相关引用
- 优先处理内容丰富的孤立文件
### 2. 修复悬空链接
"""
if dangling:
total_dangling = sum(len(links) for links in dangling.values())
report += f"- 修复 {total_dangling} 个悬空链接\n"
else:
report += "- 无需修复 ✅\n"
report += """
### 3. 建立双向链接
- 增加页面间的相互引用
- 建议每个概念页面至少有2-3个双向链接
### 4. 优化链接结构
- 入链最多的页面应该是核心概念页面
- 确保知识网络连通
---
*报告生成时间: {datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')}*
"""
return report
def save_report(self, output_path: Path):
report = self.generate_report()
output_path.parent.mkdir(parents=True, exist_ok=True)
with open(output_path, "w", encoding="utf-8") as f:
f.write(report)
print(f"✅ 分析报告已保存: {output_path}")
def main():
print("=" * 60)
print("Wiki双链深度分析工具")
print("=" * 60)
script_dir = Path(__file__).parent
project_root = script_dir.parent.parent.parent
wiki_dir = project_root / "wiki"
print(f"项目根目录: {project_root}")
print(f"Wiki目录: {wiki_dir}")
if not wiki_dir.exists():
print(f"❌ Wiki目录不存在: {wiki_dir}")
return
output_path = (
project_root
/ "raw"
/ "教育AI研究"
/ "outputs"
/ f"wiki_link_analysis_{datetime.date.today()}.md"
)
analyzer = WikiLinkAnalyzer(wiki_dir)
analyzer.analyze_all()
analyzer.save_report(output_path)
stats = analyzer.calculate_link_stats()
orphaned = len(analyzer.find_orphaned_files())
dangling = len(analyzer.find_dangling_links())
print()
print("=" * 60)
print("分析完成")
print("=" * 60)
print(f"总文件数: {stats['total_files']}")
print(f"平均每文件外链: {stats['avg_outgoing_per_file']:.1f}")
print(f"平均每文件入链: {stats['avg_incoming_per_file']:.1f}")
print(f"孤立文件数: {orphaned}")
print(f"悬空链接数: {dangling}")
print("=" * 60)
if __name__ == "__main__":
main()