@jerryjiao/knowflow 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,285 @@
1
+ #!/usr/bin/env python3
2
+ """Wiki Health Check — validates links, file sizes, and orphan pages.
3
+
4
+ Usage:
5
+ python scripts/wiki-health.py [WIKI_DIR]
6
+
7
+ Checks:
8
+ 1. Broken links: [[wiki-links]] and [markdown](links) pointing to missing files
9
+ 2. Tiny files: .md files under 100 bytes
10
+ 3. Orphan pages: .md files not linked from any other page (excludes index.md, topics.md, overview.md, log.md)
11
+
12
+ Exit codes:
13
+ 0 — all checks pass (or only warnings)
14
+ 1 — broken links found
15
+ 2 — errors during execution
16
+ """
17
+
18
+ import os
19
+ import re
20
+ import sys
21
+ import urllib.request
22
+ import urllib.parse
23
+ from pathlib import Path
24
+ from collections import defaultdict
25
+
26
+ # --- Configuration ---
27
+ WIKI_ROOT = Path(sys.argv[1]) if len(sys.argv) > 1 else Path("wiki")
28
+ TINY_THRESHOLD = 100 # bytes
29
+ INDEX_FILES = {"index.md", "topics.md", "overview.md", "log.md"}
30
+
31
+ # --- Helpers ---
32
+
33
+ def resolve_wiki_link(link_target: str, source_file: Path, wiki_root: Path) -> Path:
34
+ """Resolve a [[wiki-link]] target to an actual file path.
35
+
36
+ Wiki-links are relative to wiki_root, not to the source file.
37
+ Supports optional display text: [[target|Display Text]] -> target
38
+ """
39
+ # Strip optional display text: [[target|display]] -> target
40
+ target = link_target.split("|")[0].strip()
41
+ if not target:
42
+ return None
43
+
44
+ # Wiki-links are relative to wiki root
45
+ resolved = wiki_root / target
46
+
47
+ # Try exact path first
48
+ if resolved.is_file():
49
+ return resolved
50
+
51
+ # Try with .md extension
52
+ md_path = resolved.with_suffix(".md")
53
+ if md_path.is_file():
54
+ return md_path
55
+
56
+ return None
57
+
58
+
59
+ def resolve_md_link(link_target: str, source_file: Path, wiki_root: Path) -> Path:
60
+ """Resolve a [markdown](link) target to an actual file path.
61
+
62
+ Markdown links are relative to the source file's directory.
63
+ """
64
+ if not link_target or link_target.startswith(("http://", "https://", "#", "mailto:")):
65
+ return None # External or anchor links — skip
66
+
67
+ # URL-decode for Chinese filenames
68
+ decoded = urllib.parse.unquote(link_target)
69
+
70
+ # Markdown links are relative to the source file directory
71
+ source_dir = source_file.parent
72
+ resolved = (source_dir / decoded).resolve()
73
+
74
+ if resolved.is_file():
75
+ return resolved
76
+
77
+ return None
78
+
79
+
80
+ def strip_code_blocks(content: str) -> str:
81
+ """Remove fenced code blocks and inline code to avoid false positive links."""
82
+ # Remove fenced code blocks (```...```)
83
+ content = re.sub(r'```.*?```', '', content, flags=re.DOTALL)
84
+ # Remove inline code (`...`)
85
+ content = re.sub(r'`[^`]+`', '', content)
86
+ return content
87
+
88
+
89
+ def extract_links(content: str, source_file: Path, wiki_root: Path):
90
+ """Extract all link targets from markdown content.
91
+
92
+ Returns:
93
+ wiki_links: set of (raw_target, resolved_path_or_None) for [[]] links
94
+ md_links: set of (raw_target, resolved_path_or_None) for []() links
95
+ """
96
+ wiki_links = set()
97
+ md_links = set()
98
+
99
+ # Strip code blocks to avoid false positives
100
+ clean = strip_code_blocks(content)
101
+
102
+ # [[wiki-links]] — may contain | for display text
103
+ for match in re.finditer(r'\[\[([^\]]+)\]\]', clean):
104
+ raw = match.group(1)
105
+ target = raw.split("|")[0].strip()
106
+ resolved = resolve_wiki_link(target, source_file, wiki_root)
107
+ wiki_links.add((target, resolved))
108
+
109
+ # [markdown](links) — standard markdown
110
+ for match in re.finditer(r'\[([^\]]*)\]\(([^)]+)\)', clean):
111
+ raw_target = match.group(2).strip()
112
+ resolved = resolve_md_link(raw_target, source_file, wiki_root)
113
+ if resolved is not None or not raw_target.startswith(("http://", "https://", "#", "mailto:")):
114
+ md_links.add((raw_target, resolved))
115
+
116
+ return wiki_links, md_links
117
+
118
+
119
+ # --- Checks ---
120
+
121
+ def check_broken_links(wiki_root: Path) -> list[dict]:
122
+ """Find all broken links across the wiki."""
123
+ broken = []
124
+
125
+ for md_file in sorted(wiki_root.rglob("*.md")):
126
+ try:
127
+ content = md_file.read_text(encoding="utf-8")
128
+ except Exception as e:
129
+ broken.append({
130
+ "file": str(md_file.relative_to(wiki_root)),
131
+ "error": f"Cannot read file: {e}"
132
+ })
133
+ continue
134
+
135
+ wiki_links, md_links = extract_links(content, md_file, wiki_root)
136
+
137
+ rel_path = str(md_file.relative_to(wiki_root))
138
+
139
+ for raw_target, resolved in wiki_links:
140
+ if resolved is None:
141
+ broken.append({
142
+ "type": "wiki-link",
143
+ "file": rel_path,
144
+ "target": raw_target,
145
+ "detail": f"[[{raw_target}]] -> file not found"
146
+ })
147
+
148
+ for raw_target, resolved in md_links:
149
+ # Skip external links
150
+ if raw_target.startswith(("http://", "https://", "#", "mailto:")):
151
+ continue
152
+ if resolved is None:
153
+ broken.append({
154
+ "type": "md-link",
155
+ "file": rel_path,
156
+ "target": raw_target,
157
+ "detail": f"[]({raw_target}) -> file not found"
158
+ })
159
+
160
+ return broken
161
+
162
+
163
+ def check_tiny_files(wiki_root: Path, threshold: int = TINY_THRESHOLD) -> list[dict]:
164
+ """Find .md files smaller than threshold bytes."""
165
+ tiny = []
166
+
167
+ for md_file in sorted(wiki_root.rglob("*.md")):
168
+ try:
169
+ size = md_file.stat().st_size
170
+ except OSError:
171
+ continue
172
+
173
+ if size < threshold:
174
+ tiny.append({
175
+ "file": str(md_file.relative_to(wiki_root)),
176
+ "size": size,
177
+ "detail": f"{size}B < {threshold}B threshold"
178
+ })
179
+
180
+ return tiny
181
+
182
+
183
+ def check_orphan_pages(wiki_root: Path) -> list[dict]:
184
+ """Find .md pages not referenced by any other page."""
185
+ # Collect all existing pages
186
+ all_pages = set()
187
+ for md_file in wiki_root.rglob("*.md"):
188
+ all_pages.add(md_file)
189
+
190
+ # Collect all link targets across all files
191
+ referenced = set()
192
+ for md_file in wiki_root.rglob("*.md"):
193
+ try:
194
+ content = md_file.read_text(encoding="utf-8")
195
+ except Exception:
196
+ continue
197
+
198
+ wiki_links, md_links = extract_links(content, md_file, wiki_root)
199
+
200
+ for _, resolved in wiki_links:
201
+ if resolved is not None:
202
+ referenced.add(resolved)
203
+
204
+ for _, resolved in md_links:
205
+ if resolved is not None:
206
+ referenced.add(resolved)
207
+
208
+ orphans = []
209
+ for page in sorted(all_pages):
210
+ rel = str(page.relative_to(wiki_root))
211
+ # Index files are never considered orphans
212
+ if page.name in INDEX_FILES:
213
+ continue
214
+ if page not in referenced:
215
+ orphans.append({
216
+ "file": rel,
217
+ "detail": "not linked from any other page"
218
+ })
219
+
220
+ return orphans
221
+
222
+
223
+ # --- Main ---
224
+
225
+ def main():
226
+ if not WIKI_ROOT.is_dir():
227
+ print(f"ERROR: wiki directory not found: {WIKI_ROOT}")
228
+ sys.exit(2)
229
+
230
+ print(f"Wiki Health Check — {WIKI_ROOT.resolve()}")
231
+ print(f"{'=' * 60}")
232
+
233
+ # Count files
234
+ md_files = list(WIKI_ROOT.rglob("*.md"))
235
+ print(f"Total .md files: {len(md_files)}")
236
+
237
+ exit_code = 0
238
+
239
+ # 1. Broken links
240
+ print(f"\n--- Broken Links ---")
241
+ broken = check_broken_links(WIKI_ROOT)
242
+ if broken:
243
+ errors = [b for b in broken if "error" in b]
244
+ link_issues = [b for b in broken if "error" not in b]
245
+ if link_issues:
246
+ print(f" BROKEN LINKS: {len(link_issues)}")
247
+ for item in link_issues:
248
+ print(f" [{item['type']}] {item['file']} -> {item['target']}")
249
+ exit_code = 1
250
+ if errors:
251
+ print(f" READ ERRORS: {len(errors)}")
252
+ for item in errors:
253
+ print(f" {item['file']}: {item['error']}")
254
+ else:
255
+ print(" OK — no broken links found")
256
+
257
+ # 2. Tiny files
258
+ print(f"\n--- Tiny Files (< {TINY_THRESHOLD}B) ---")
259
+ tiny = check_tiny_files(WIKI_ROOT)
260
+ if tiny:
261
+ print(f" TINY FILES: {len(tiny)}")
262
+ for item in tiny:
263
+ print(f" {item['file']} ({item['detail']})")
264
+ else:
265
+ print(" OK — no tiny files found")
266
+
267
+ # 3. Orphan pages
268
+ print(f"\n--- Orphan Pages ---")
269
+ orphans = check_orphan_pages(WIKI_ROOT)
270
+ if orphans:
271
+ print(f" ORPHAN PAGES: {len(orphans)}")
272
+ for item in orphans:
273
+ print(f" {item['file']}")
274
+ else:
275
+ print(" OK — no orphan pages found")
276
+
277
+ print(f"\n{'=' * 60}")
278
+ total_issues = len(broken) + len(tiny) + len(orphans)
279
+ print(f"Total issues: {total_issues}")
280
+
281
+ sys.exit(exit_code)
282
+
283
+
284
+ if __name__ == "__main__":
285
+ main()
@@ -0,0 +1,335 @@
1
+ #!/opt/homebrew/bin/bash
2
+ # knowflow health check — broken links, empty files, orphan pages
3
+ # Usage: bash scripts/wiki-health.sh [--wiki-root <path>] [--json]
4
+ #
5
+ # M2 改进:
6
+ # - 添加 --json 选项输出结构化 JSON(方便 CLI 解析)
7
+ # - 输出结构化格式,每行前缀统一
8
+ # - 保持向后兼容(默认人类可读模式不变)
9
+ #
10
+ # Checks:
11
+ # 1. Broken links — [[wikilinks]] and [md](path.md) that point to missing files
12
+ # 2. Empty files — .md files < 100 bytes
13
+ # 3. Orphan pages — .md files never referenced by any other page
14
+
15
+ set -euo pipefail
16
+
17
+ # ── Config ──────────────────────────────────────────────
18
+ SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
19
+ WIKI_ROOT="${KNOWFLOW_ROOT:-${WIKI_ROOT:-$(cd "$SCRIPT_DIR/.." && pwd)}}"
20
+ WIKI_DIR="${KNOWFLOW_WIKI_DIR:-$WIKI_ROOT/wiki}"
21
+ MIN_SIZE=100
22
+ OUTPUT_FORMAT="text" # text | json
23
+
24
+ # Parse arguments
25
+ while [[ $# -gt 0 ]]; do
26
+ case "$1" in
27
+ --wiki-root) WIKI_ROOT="$2"; WIKI_DIR="$2/wiki"; shift 2 ;;
28
+ --wiki-dir) WIKI_DIR="$2"; shift 2 ;;
29
+ --json) OUTPUT_FORMAT="json"; shift ;;
30
+ --min-size) MIN_SIZE="$2"; shift 2 ;;
31
+ *) echo "Unknown option: $1" >&2; exit 1 ;;
32
+ esac
33
+ done
34
+
35
+ if [ ! -d "$WIKI_DIR" ]; then
36
+ if [ "$OUTPUT_FORMAT" = "json" ]; then
37
+ echo "{\"status\":\"error\",\"message\":\"Wiki directory not found: $WIKI_DIR\"}"
38
+ else
39
+ echo "Error: wiki directory not found at $WIKI_DIR" >&2
40
+ fi
41
+ exit 1
42
+ fi
43
+
44
+ # ── Counters ────────────────────────────────────────────
45
+ BROKEN_COUNT=0
46
+ EMPTY_COUNT=0
47
+ ORPHAN_COUNT=0
48
+
49
+ # JSON output accumulators (used only when --json)
50
+ JSON_BROKEN=""
51
+ JSON_EMPTY=""
52
+ JSON_ORPHAN=""
53
+
54
+ # ── Helper: resolve a wikilink to an absolute path ──
55
+ resolve_link() {
56
+ local mode="$1"
57
+ local basedir="$2"
58
+ local target="$3"
59
+ local abs_path
60
+
61
+ if [[ "$target" == /* ]]; then
62
+ abs_path="$WIKI_DIR${target}"
63
+ elif [ "$mode" = "wiki" ]; then
64
+ abs_path="$WIKI_DIR/$target"
65
+ else
66
+ abs_path="$WIKI_DIR/$basedir/$target"
67
+ fi
68
+
69
+ local dir_part="$(dirname "$abs_path")"
70
+ local base_part="$(basename "$abs_path")"
71
+ local norm_dir
72
+ norm_dir="$(cd "$dir_part" 2>/dev/null && pwd)" 2>/dev/null || {
73
+ return 1
74
+ }
75
+ echo "${norm_dir}/${base_part}"
76
+ }
77
+
78
+ # ── Helper: extract [[wikilinks]] (macOS grep compatible) ──
79
+ # grep -o finds every link on a line; a sed s///gp approach would only catch
80
+ # one per line, which silently drops references on index-style pages that put
81
+ # dozens of links on a single line.
82
+ extract_wikilinks() {
83
+ grep -oE '\[\[[^]]+\]\]' "$1" 2>/dev/null \
84
+ | sed 's/^\[\[//; s/\]\]$//; s/|.*//' \
85
+ | grep -v '^[[:space:]]*$' \
86
+ | awk '!seen[$0]++' || true
87
+ }
88
+
89
+ # ── Helper: extract [text](path.md) markdown links ──
90
+ extract_mdlinks() {
91
+ grep -oE '\]\([^)]+\.md\)' "$1" 2>/dev/null \
92
+ | sed 's/^\](//; s/)$//' \
93
+ | awk '!seen[$0]++' || true
94
+ }
95
+
96
+ # ── Check 1: Broken Links ──────────────────────────────
97
+ check_broken_links() {
98
+ local found=0
99
+ local json_items=""
100
+
101
+ while IFS= read -r -d '' file; do
102
+ local relpath="${file#$WIKI_DIR/}"
103
+ local basedir
104
+ basedir="$(dirname "$relpath")"
105
+
106
+ # Check [[wikilinks]]
107
+ while IFS= read -r link; do
108
+ [ -z "$link" ] && continue
109
+ local target="${link%.md}.md"
110
+ local resolved
111
+ resolved="$(resolve_link wiki "$basedir" "$target")" || true
112
+
113
+ if [ -z "$resolved" ] || [ ! -f "$resolved" ]; then
114
+ if [ "$OUTPUT_FORMAT" = "json" ]; then
115
+ json_items="${json_items:+$json_items, }{\"file\":\"$relpath\",\"link\":\"$link\",\"type\":\"wikilink\"}"
116
+ else
117
+ echo " ✗ $relpath → [[$link]] (not found)"
118
+ fi
119
+ BROKEN_COUNT=$((BROKEN_COUNT + 1))
120
+ found=1
121
+ fi
122
+ done < <(extract_wikilinks "$file")
123
+
124
+ # Check [text](path.md) markdown links
125
+ while IFS= read -r link; do
126
+ [ -z "$link" ] && continue
127
+ case "$link" in http:*|https:*|mailto:*) continue ;; esac
128
+ local target="${link%.md}.md"
129
+ local resolved
130
+ resolved="$(resolve_link md "$basedir" "$target")" || true
131
+
132
+ if [ -z "$resolved" ] || [ ! -f "$resolved" ]; then
133
+ if [ "$OUTPUT_FORMAT" = "json" ]; then
134
+ json_items="${json_items:+$json_items, }{\"file\":\"$relpath\",\"link\":\"$link\",\"type:\"markdown\"}"
135
+ else
136
+ echo " ✗ $relpath → $link (not found)"
137
+ fi
138
+ BROKEN_COUNT=$((BROKEN_COUNT + 1))
139
+ found=1
140
+ fi
141
+ done < <(extract_mdlinks "$file")
142
+
143
+ done < <(find "$WIKI_DIR" -name '*.md' -not -path '*/.understand-anything/*' -print0 2>/dev/null)
144
+
145
+ if [ "$found" -eq 0 ]; then
146
+ if [ "$OUTPUT_FORMAT" != "json" ]; then
147
+ echo " ✓ All links resolve correctly"
148
+ fi
149
+ fi
150
+
151
+ JSON_BROKEN="$json_items"
152
+ }
153
+
154
+ # ── Check 2: Empty Files ───────────────────────────────
155
+ check_empty_files() {
156
+ local found=0
157
+ local json_items=""
158
+
159
+ while IFS= read -r -d '' file; do
160
+ local relpath="${file#$WIKI_DIR/}"
161
+ local size
162
+ size="$(wc -c < "$file" | tr -d ' ')"
163
+
164
+ if [ "$OUTPUT_FORMAT" = "json" ]; then
165
+ json_items="${json_items:+$json_items, }{\"file\":\"$relpath\",\"size\":$size}"
166
+ else
167
+ echo " ✗ $relpath ($size bytes)"
168
+ fi
169
+ EMPTY_COUNT=$((EMPTY_COUNT + 1))
170
+ found=1
171
+ done < <(find "$WIKI_DIR" -name '*.md' -not -path '*/.understand-anything/*' -size -${MIN_SIZE}c -print0 2>/dev/null)
172
+
173
+ if [ "$found" -eq 0 ]; then
174
+ if [ "$OUTPUT_FORMAT" != "json" ]; then
175
+ echo " ✓ No empty files found"
176
+ fi
177
+ fi
178
+
179
+ JSON_EMPTY="$json_items"
180
+ }
181
+
182
+ # ── Check 3: Orphan Pages ──────────────────────────────
183
+ # KNOWFLOW_HEALTH_EXCLUDE_ORPHAN (colon-separated relative dirs, e.g. "sources/:logs/")
184
+ # lists directories whose pages are expected to be unreferenced (feed/inbox
185
+ # pages) and should not count as orphans.
186
+ EXCLUDE_ORPHAN="${KNOWFLOW_HEALTH_EXCLUDE_ORPHAN:-}"
187
+
188
+ is_orphan_excluded() {
189
+ local rel="$1"
190
+ [ -z "$EXCLUDE_ORPHAN" ] && return 1
191
+ local parts part
192
+ IFS=':' read -ra parts <<< "$EXCLUDE_ORPHAN"
193
+ for part in "${parts[@]}"; do
194
+ part="${part%/}"
195
+ [[ -n "$part" && ( "$rel" == "$part" || "$rel" == "$part"/* ) ]] && return 0
196
+ done
197
+ return 1
198
+ }
199
+
200
+ check_orphan_pages() {
201
+ local found=0
202
+ local json_items=""
203
+
204
+ local ref_file
205
+ ref_file="$(mktemp)"
206
+
207
+ while IFS= read -r -d '' file; do
208
+ local relpath="${file#$WIKI_DIR/}"
209
+ local basedir
210
+ basedir="$(dirname "$relpath")"
211
+
212
+ while IFS= read -r link; do
213
+ [ -z "$link" ] && continue
214
+ local target="${link%.md}.md"
215
+ local resolved
216
+ resolved="$(resolve_link wiki "$basedir" "$target")" || continue
217
+ echo "${resolved#$WIKI_DIR/}" >> "$ref_file"
218
+ done < <(extract_wikilinks "$file")
219
+
220
+ while IFS= read -r link; do
221
+ [ -z "$link" ] && continue
222
+ case "$link" in http:*|https:*|mailto:*) continue ;; esac
223
+ local target="${link%.md}.md"
224
+ local resolved
225
+ resolved="$(resolve_link md "$basedir" "$target")" || continue
226
+ echo "${resolved#$WIKI_DIR/}" >> "$ref_file"
227
+ done < <(extract_mdlinks "$file")
228
+
229
+ done < <(find "$WIKI_DIR" -name '*.md' -not -path '*/.understand-anything/*' -print0 2>/dev/null)
230
+
231
+ while IFS= read -r -d '' file; do
232
+ local relpath="${file#$WIKI_DIR/}"
233
+ if [ "$relpath" = "index.md" ]; then
234
+ continue
235
+ fi
236
+ if is_orphan_excluded "$relpath"; then
237
+ continue
238
+ fi
239
+ if ! grep -qxF "$relpath" "$ref_file" 2>/dev/null; then
240
+ if [ "$OUTPUT_FORMAT" = "json" ]; then
241
+ json_items="${json_items:+$json_items, }{\"file\":\"$relpath\"}"
242
+ else
243
+ echo " ✗ $relpath"
244
+ fi
245
+ ORPHAN_COUNT=$((ORPHAN_COUNT + 1))
246
+ found=1
247
+ fi
248
+ done < <(find "$WIKI_DIR" -name '*.md' -not -path '*/.understand-anything/*' -print0 2>/dev/null)
249
+
250
+ rm -f "$ref_file"
251
+
252
+ if [ "$found" -eq 0 ]; then
253
+ if [ "$OUTPUT_FORMAT" != "json" ]; then
254
+ echo " ✓ All pages are referenced"
255
+ fi
256
+ fi
257
+
258
+ JSON_ORPHAN="$json_items"
259
+ }
260
+
261
+ # ── Main ────────────────────────────────────────────────
262
+ TIMESTAMP="$(date '+%Y-%m-%d %H:%M:%S')"
263
+ TOTAL=$((BROKEN_COUNT + EMPTY_COUNT + ORPHAN_COUNT))
264
+
265
+ if [ "$OUTPUT_FORMAT" = "json" ]; then
266
+ # ── JSON mode: run checks silently, output structured JSON ──
267
+ check_broken_links || { BROKEN_COUNT=0; }
268
+ check_empty_files || { EMPTY_COUNT=0; }
269
+ check_orphan_pages || { ORPHAN_COUNT=0; }
270
+
271
+ TOTAL=$((BROKEN_COUNT + EMPTY_COUNT + ORPHAN_COUNT))
272
+
273
+ cat << EOF
274
+ {
275
+ "status": "$([ $TOTAL -eq 0 ] && echo "ok" || echo "issues_found")",
276
+ "timestamp": "$TIMESTAMP",
277
+ "wiki_root": "$WIKI_DIR",
278
+ "summary": {
279
+ "broken_links": $BROKEN_COUNT,
280
+ "empty_files": $EMPTY_COUNT,
281
+ "orphan_pages": $ORPHAN_COUNT,
282
+ "total_issues": $TOTAL,
283
+ "min_size_bytes": $MIN_SIZE
284
+ },
285
+ "details": {
286
+ "broken_links": [$JSON_BROKEN],
287
+ "empty_files": [$JSON_EMPTY],
288
+ "orphan_pages": [$JSON_ORPHAN]
289
+ }
290
+ }
291
+ EOF
292
+
293
+ [ $TOTAL -eq 0 ] && exit 0 || exit 1
294
+
295
+ else
296
+ # ── Text mode (default, backward compatible) ──
297
+ echo "🏥 KnowFlow Wiki — Health Check"
298
+ echo "====================================="
299
+ echo "Wiki root: $WIKI_DIR"
300
+ echo "Time: $TIMESTAMP"
301
+ echo ""
302
+
303
+ echo "## 🔗 Broken Links"
304
+ echo ""
305
+ check_broken_links || { echo " ⚠️ Broken links check interrupted"; BROKEN_COUNT=0; }
306
+ echo ""
307
+
308
+ echo "## 📄 Empty Files (< ${MIN_SIZE} bytes)"
309
+ echo ""
310
+ check_empty_files || { echo " ⚠️ Empty files check interrupted"; EMPTY_COUNT=0; }
311
+ echo ""
312
+
313
+ echo "## 🏝️ Orphan Pages (never referenced)"
314
+ echo ""
315
+ check_orphan_pages || { echo " ⚠️ Orphan pages check interrupted"; ORPHAN_COUNT=0; }
316
+ echo ""
317
+
318
+ TOTAL=$((BROKEN_COUNT + EMPTY_COUNT + ORPHAN_COUNT))
319
+
320
+ echo "## 📊 Summary"
321
+ echo ""
322
+ echo " Broken links: $BROKEN_COUNT"
323
+ echo " Empty files: $EMPTY_COUNT"
324
+ echo " Orphan pages: $ORPHAN_COUNT"
325
+ echo " Total issues: $TOTAL"
326
+ echo ""
327
+
328
+ if [ "$TOTAL" -eq 0 ]; then
329
+ echo "✅ All checks passed"
330
+ exit 0
331
+ else
332
+ echo "❌ $TOTAL issue(s) found"
333
+ exit 1
334
+ fi
335
+ fi
@@ -0,0 +1,31 @@
1
+ # {{对比主题}}
2
+
3
+ > 对比分析 | 创建时间: {{YYYY-MM-DD}}
4
+
5
+ ## 📋 对比对象
6
+
7
+ | 维度 | {{对象A}} | {{对象B}} | {{对象C(可选)}} |
8
+ |------|-----------|-----------|----------------|
9
+ | 定位/定义 | ... | ... | ... |
10
+ | 核心优势 | ... | ... | ... |
11
+ | 核心劣势 | ... | ... | ... |
12
+ | 适用场景 | ... | ... | ... |
13
+ | 学习成本 | ... | ... | ... |
14
+ | 生态成熟度 | ... | ... | ... |
15
+ | 成本 | ... | ... | ... |
16
+
17
+ ## 🏆 结论
18
+
19
+ ### 最适合的场景
20
+ - 场景 A → 推荐 **{{对象X}}**,因为...
21
+ - 场景 B → 推荐 **{{对象Y}}**,因为...
22
+
23
+ ## 🔗 关联内容
24
+ - 实体: [[entities/...]], [[entities/...]]
25
+ - 概念: [[concepts/...]]
26
+ - 来源: [[sources/...]]
27
+
28
+ ## ⚠️ 局限性
29
+ > 本次对比基于以下来源,可能存在信息偏差:
30
+ > - [[sources/{{来源1}}]]
31
+ > - [[sources/{{来源2}}]]
@@ -0,0 +1,37 @@
1
+ # {{概念名称}}
2
+
3
+ > 定义 | 置信度: EXTRACTED
4
+
5
+ ## 📖 定义
6
+ {{一句话定义这个概念}}
7
+
8
+ ## 🎯 核心观点
9
+
10
+ ### 观点 1: {{标题}}
11
+ > 描述... (EXTRACTED)
12
+ >
13
+ > 来源: [[sources/{{来源}}]]
14
+
15
+ ### 观点 2: {{标题}}
16
+ > 描述... (INFERRED)
17
+
18
+ ### 观点 3: {{不同视角/反面观点}}
19
+ > 如果有争议或不同视角,记录在此
20
+
21
+ ## 🔬 不同来源的视角对比
22
+
23
+ | 来源 | 视角 | 关键论点 |
24
+ |------|------|---------|
25
+ | [[sources/...]] | ... | ... |
26
+ | [[sources/...]] | ... | ... |
27
+
28
+ ## 💡 实践应用
29
+ 1. **应用场景 1**: ...
30
+ 2. **应用场景 2**: ...
31
+
32
+ ## 🔗 关联内容
33
+ - 实体: [[entities/{{相关实体}}]]
34
+ - 概念: [[concepts/{{相关概念}}]]
35
+
36
+ ## 📚 延伸阅读
37
+ - [[sources/{{相关来源}}]]