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:
@@ -0,0 +1,132 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
detect-conflicts.py — 检测 wiki 页面间的矛盾
|
||||
|
||||
通过扫描 relations 中的 conflicts_with 关系,
|
||||
检查双方是否都引用了对方,并输出矛盾报告。
|
||||
|
||||
用法:
|
||||
python tools/scripts/detect-conflicts.py # 标准输出
|
||||
python tools/scripts/detect-conflicts.py --json # JSON 格式
|
||||
python tools/scripts/detect-conflicts.py --auto-callout # 自动添加 callout
|
||||
"""
|
||||
import argparse
|
||||
import json
|
||||
import re
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
try:
|
||||
import yaml
|
||||
except ImportError:
|
||||
yaml = None
|
||||
|
||||
WIKI = Path(r"D:\Applications\app\kepano-obsidian-main\wiki")
|
||||
|
||||
|
||||
def read_frontmatter(fp: Path) -> dict | None:
|
||||
"""读取 frontmatter,返回 dict 或 None"""
|
||||
content = fp.read_text(encoding="utf-8")
|
||||
m = re.match(r"^---\n(.*?)\n---", content, re.DOTALL)
|
||||
if not m or not yaml:
|
||||
return None
|
||||
try:
|
||||
return yaml.safe_load(m.group(1)) or {}
|
||||
except:
|
||||
return None
|
||||
|
||||
|
||||
def detect():
|
||||
"""检测所有 conflicts_with 关系,返回冲突报告列表"""
|
||||
conflicts = []
|
||||
pages = {}
|
||||
|
||||
for fp in WIKI.glob("*.md"):
|
||||
if fp.name in ("index.md", "log.md"):
|
||||
continue
|
||||
front = read_frontmatter(fp)
|
||||
if front is None:
|
||||
continue
|
||||
pages[fp.stem] = front
|
||||
for rel in front.get("relations", []):
|
||||
if rel["type"] == "conflicts_with":
|
||||
target = rel.get("target", "").strip("[]")
|
||||
conf_source = rel.get("confidence", 3)
|
||||
if target and target != fp.stem:
|
||||
conflicts.append({
|
||||
"source": fp.stem,
|
||||
"target": target,
|
||||
"confidence": conf_source,
|
||||
"bidirectional": False
|
||||
})
|
||||
|
||||
# 检查双向性
|
||||
for c in conflicts:
|
||||
target_front = pages.get(c["target"])
|
||||
if target_front:
|
||||
for rel in target_front.get("relations", []):
|
||||
if rel["type"] == "conflicts_with" and rel.get("target", "").strip("[]") == c["source"]:
|
||||
c["bidirectional"] = True
|
||||
break
|
||||
|
||||
return conflicts
|
||||
|
||||
|
||||
def generate_callout(source: str, target: str) -> str:
|
||||
return (
|
||||
f"> [!WARNING] 可能矛盾\n"
|
||||
f"> 本页的声明与 [[{target}]] 存在冲突。\n"
|
||||
f"> 需要人工复核并解决矛盾。\n"
|
||||
)
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description="Detect conflicts between wiki pages")
|
||||
parser.add_argument("--json", action="store_true", help="Output JSON")
|
||||
parser.add_argument("--auto-callout", action="store_true", help="Auto-add callout to pages")
|
||||
args = parser.parse_args()
|
||||
|
||||
conflicts = detect()
|
||||
|
||||
if args.json:
|
||||
print(json.dumps(conflicts, ensure_ascii=False, indent=2))
|
||||
return
|
||||
|
||||
bidirectional = [c for c in conflicts if c["bidirectional"]]
|
||||
unidirectional = [c for c in conflicts if not c["bidirectional"]]
|
||||
|
||||
print(f"=== Conflict Detection ===")
|
||||
print(f" Total conflict declarations: {len(conflicts)}")
|
||||
print(f" Bidirectional (both confirm): {len(bidirectional)}")
|
||||
print(f" Unidirectional (check needed): {len(unidirectional)}")
|
||||
|
||||
if bidirectional:
|
||||
print(f"\n Bidirectional conflicts:")
|
||||
for c in bidirectional:
|
||||
print(f" [[{c['source']}]] <--conflicts_with--> [[{c['target']}]]")
|
||||
|
||||
if unidirectional:
|
||||
print(f"\n Unidirectional conflicts (may need callout):")
|
||||
for c in unidirectional:
|
||||
print(f" [[{c['source']}]] --conflicts_with--> [[{c['target']}]] (unconfirmed)")
|
||||
|
||||
if args.auto_callout:
|
||||
added = 0
|
||||
for c in unidirectional:
|
||||
fp = WIKI / f"{c['source']}.md"
|
||||
if not fp.exists():
|
||||
continue
|
||||
content = fp.read_text(encoding="utf-8")
|
||||
callout = generate_callout(c["source"], c["target"])
|
||||
# Only add if not already present
|
||||
if callout.strip() not in content:
|
||||
content += f"\n\n{callout}"
|
||||
fp.write_text(content, encoding="utf-8")
|
||||
added += 1
|
||||
print(f" Added callout to [[{c['source']}]]")
|
||||
print(f" Callouts added: {added}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user