361 lines
12 KiB
Bash
361 lines
12 KiB
Bash
|
|
#!/usr/bin/env bash
|
||
|
|
# wiki-lint.sh — Mechanischer Wiki-Health-Check (kein LLM)
|
||
|
|
# Version: 0.1 (Draft 2026-06-16)
|
||
|
|
# Repo: knowledge-base
|
||
|
|
# Usage: ./scripts/wiki-lint.sh [repo-root] [output-file]
|
||
|
|
|
||
|
|
set -u # nounset; NICHT -e, weil wir alle Checks durchlaufen wollen
|
||
|
|
|
||
|
|
# --- Konfiguration (Schwellwerte) ---
|
||
|
|
STUB_WARN_PCT=10
|
||
|
|
STUB_FAIL_PCT=25
|
||
|
|
DIR_WARN_FILES=12
|
||
|
|
DIR_FAIL_FILES=20
|
||
|
|
STALE_DAYS=30
|
||
|
|
EXIT_OK=0
|
||
|
|
EXIT_ISSUES=1
|
||
|
|
EXIT_ERROR=2
|
||
|
|
|
||
|
|
REPO_ROOT="${1:-$(cd "$(dirname "$0")/.." && pwd)}"
|
||
|
|
OUTPUT="${2:-$REPO_ROOT/scripts/wiki-lint-report.md}"
|
||
|
|
TIMESTAMP="$(date -u +"%Y-%m-%d %H:%M:%S UTC")"
|
||
|
|
|
||
|
|
cd "$REPO_ROOT" || { echo "FAIL: cannot cd to $REPO_ROOT" >&2; exit 2; }
|
||
|
|
|
||
|
|
# --- Zähler ---
|
||
|
|
total_pages=0
|
||
|
|
issues=0
|
||
|
|
status_ok="✅"
|
||
|
|
status_warn="⚠️"
|
||
|
|
status_fail="❌"
|
||
|
|
|
||
|
|
# --- Ergebnis-Sammler ---
|
||
|
|
declare -a section_lines
|
||
|
|
current_section=""
|
||
|
|
|
||
|
|
# Helper: Sektion hinzufügen
|
||
|
|
add_line() { section_lines+=("$1"); }
|
||
|
|
new_section() {
|
||
|
|
current_section="$1"
|
||
|
|
section_lines+=("")
|
||
|
|
section_lines+=("### $1")
|
||
|
|
}
|
||
|
|
|
||
|
|
# --- Files sammeln ---
|
||
|
|
# Meta-Files und Tasks-Dir ausschließen
|
||
|
|
mapfile -t all_wiki < <(find wiki -name "*.md" -type f -not -path "wiki/tasks/*" 2>/dev/null | sort)
|
||
|
|
total_pages=${#all_wiki[@]}
|
||
|
|
|
||
|
|
# ============================================================
|
||
|
|
# CHECK 1: File-Count per Dir (Trigger für Refactor)
|
||
|
|
# ============================================================
|
||
|
|
new_section "File-Count per Dir (Refactor-Trigger)"
|
||
|
|
declare -A dir_count
|
||
|
|
while IFS= read -r f; do
|
||
|
|
dir=$(dirname "$f")
|
||
|
|
dir_count["$dir"]=$(( ${dir_count["$dir"]:-0} + 1 ))
|
||
|
|
done < <(printf '%s\n' "${all_wiki[@]}")
|
||
|
|
|
||
|
|
add_line "| Dir | Files | Trigger |"
|
||
|
|
add_line "|-----|-------|---------|"
|
||
|
|
for dir in $(printf '%s\n' "${!dir_count[@]}" | sort); do
|
||
|
|
cnt=${dir_count["$dir"]}
|
||
|
|
if [ "$cnt" -ge "$DIR_FAIL_FILES" ]; then
|
||
|
|
add_line "| $dir | $cnt | ❌ FAIL (refactor) |"
|
||
|
|
issues=$((issues+1))
|
||
|
|
elif [ "$cnt" -ge "$DIR_WARN_FILES" ]; then
|
||
|
|
add_line "| $dir | $cnt | ⚠️ warn (refactor) |"
|
||
|
|
else
|
||
|
|
add_line "| $dir | $cnt | ✅ |"
|
||
|
|
fi
|
||
|
|
done
|
||
|
|
|
||
|
|
# ============================================================
|
||
|
|
# CHECK 2: Stubs (<30 Zeilen) + Stub-Quote
|
||
|
|
# ============================================================
|
||
|
|
new_section "Stub-Quote (<30 Zeilen)"
|
||
|
|
stub_count=0
|
||
|
|
stub_files=()
|
||
|
|
for f in "${all_wiki[@]}"; do
|
||
|
|
lines=$(wc -l < "$f")
|
||
|
|
if [ "$lines" -lt 30 ]; then
|
||
|
|
# Meta-Files ausschließen
|
||
|
|
case "$(basename "$f")" in
|
||
|
|
index.md|log.md|ideas.md|tasks.md) continue ;;
|
||
|
|
esac
|
||
|
|
stub_count=$((stub_count+1))
|
||
|
|
stub_files+=("$f ($lines lines)")
|
||
|
|
fi
|
||
|
|
done
|
||
|
|
|
||
|
|
if [ "$total_pages" -gt 0 ]; then
|
||
|
|
stub_pct=$((stub_count * 100 / total_pages))
|
||
|
|
else
|
||
|
|
stub_pct=0
|
||
|
|
fi
|
||
|
|
|
||
|
|
if [ "$stub_pct" -ge "$STUB_FAIL_PCT" ]; then
|
||
|
|
add_line "$status_fail **$stub_count / $total_pages ($stub_pct%)** — threshold $STUB_WARN_PCT%, fail $STUB_FAIL_PCT%"
|
||
|
|
issues=$((issues+1))
|
||
|
|
elif [ "$stub_pct" -ge "$STUB_WARN_PCT" ]; then
|
||
|
|
add_line "$status_warn **$stub_count / $total_pages ($stub_pct%)** — threshold $STUB_WARN_PCT%"
|
||
|
|
else
|
||
|
|
add_line "$status_ok **$stub_count / $total_pages ($stub_pct%)**"
|
||
|
|
fi
|
||
|
|
|
||
|
|
if [ "${#stub_files[@]}" -gt 0 ] && [ "$stub_count" -le 15 ]; then
|
||
|
|
add_line ""
|
||
|
|
add_line "Stub-Files:"
|
||
|
|
for s in "${stub_files[@]}"; do add_line "- \`$s\`"; done
|
||
|
|
fi
|
||
|
|
|
||
|
|
# ============================================================
|
||
|
|
# CHECK 3: Frontmatter fehlt
|
||
|
|
# ============================================================
|
||
|
|
new_section "Frontmatter Präsenz"
|
||
|
|
missing_fm=()
|
||
|
|
present_fm=0
|
||
|
|
for f in "${all_wiki[@]}"; do
|
||
|
|
# Meta-Files dürfen ohne
|
||
|
|
case "$(basename "$f")" in
|
||
|
|
index.md|log.md|ideas.md) continue ;;
|
||
|
|
esac
|
||
|
|
first=$(head -1 "$f")
|
||
|
|
if [ "$first" = "---" ]; then
|
||
|
|
present_fm=$((present_fm+1))
|
||
|
|
else
|
||
|
|
missing_fm+=("$f")
|
||
|
|
fi
|
||
|
|
done
|
||
|
|
|
||
|
|
if [ "${#missing_fm[@]}" -gt 0 ]; then
|
||
|
|
add_line "$status_fail **${#missing_fm[@]} missing, $present_fm present**"
|
||
|
|
issues=$((issues+1))
|
||
|
|
add_line ""
|
||
|
|
add_line "Missing:"
|
||
|
|
for m in "${missing_fm[@]}"; do add_line "- \`$m\`"; done
|
||
|
|
else
|
||
|
|
add_line "$status_ok **$present_fm / $total_pages present**"
|
||
|
|
fi
|
||
|
|
|
||
|
|
# ============================================================
|
||
|
|
# CHECK 4: Broken Cross-Refs
|
||
|
|
# ============================================================
|
||
|
|
new_section "Broken Cross-References"
|
||
|
|
broken_refs=()
|
||
|
|
# Sammle alle .md-Pfade relativ zum Repo
|
||
|
|
mapfile -t all_md_rel < <(find . -name "*.md" -not -path "./.git/*" -type f 2>/dev/null | sed 's|^\./||' | sort)
|
||
|
|
|
||
|
|
# Grep nach ](path.md) in wiki-Files
|
||
|
|
for f in "${all_wiki[@]}"; do
|
||
|
|
while IFS= read -r match; do
|
||
|
|
# match ist z.B. "concepts/llm/foo.md" oder "../other/bar.md"
|
||
|
|
target=$(echo "$match" | sed 's|^(\.\./)*||' | sed 's|^wiki/||')
|
||
|
|
# Auflösen relativ zur File-Dir
|
||
|
|
fdir=$(dirname "$f")
|
||
|
|
if [ "$fdir" = "." ]; then
|
||
|
|
resolved="$target"
|
||
|
|
else
|
||
|
|
resolved="$fdir/$target"
|
||
|
|
fi
|
||
|
|
# Normalize
|
||
|
|
resolved=$(realpath -m --relative-to=. "$resolved" 2>/dev/null || echo "$resolved")
|
||
|
|
if [ ! -f "$resolved" ] && [ ! -f "wiki/$target" ] && [ ! -f "$target" ]; then
|
||
|
|
broken_refs+=("$f → $match")
|
||
|
|
fi
|
||
|
|
done < <(grep -oE '\]\(([^)]+\.md)\)' "$f" 2>/dev/null | sed 's|](||;s|)||')
|
||
|
|
done
|
||
|
|
|
||
|
|
if [ "${#broken_refs[@]}" -gt 0 ]; then
|
||
|
|
add_line "$status_fail **${#broken_refs[@]} broken refs**"
|
||
|
|
issues=$((issues+1))
|
||
|
|
for b in "${broken_refs[@]}"; do add_line "- \`$b\`"; done
|
||
|
|
else
|
||
|
|
add_line "$status_ok **0 broken**"
|
||
|
|
fi
|
||
|
|
|
||
|
|
# ============================================================
|
||
|
|
# CHECK 5: Orphan Pages (kein eingehender Link)
|
||
|
|
# ============================================================
|
||
|
|
new_section "Orphan Pages (kein eingehender Link)"
|
||
|
|
orphans=()
|
||
|
|
for f in "${all_wiki[@]}"; do
|
||
|
|
basename=$(basename "$f" .md)
|
||
|
|
# Meta-Files auslassen
|
||
|
|
case "$basename" in
|
||
|
|
index|log|ideas|tasks) continue ;;
|
||
|
|
esac
|
||
|
|
# Suche nach basename in allen anderen wiki-Files (ohne die Datei selbst)
|
||
|
|
if ! grep -rq "$basename" "${all_wiki[@]/$f/}" 2>/dev/null; then
|
||
|
|
orphans+=("$f")
|
||
|
|
fi
|
||
|
|
done
|
||
|
|
|
||
|
|
if [ "${#orphans[@]}" -gt 0 ]; then
|
||
|
|
add_line "$status_warn **${#orphans[@]} orphan pages**"
|
||
|
|
for o in "${orphans[@]}"; do add_line "- \`$o\`"; done
|
||
|
|
else
|
||
|
|
add_line "$status_ok **0 orphans**"
|
||
|
|
fi
|
||
|
|
|
||
|
|
# ============================================================
|
||
|
|
# CHECK 6: Dup-Topic-Cluster (Keyword-Suche)
|
||
|
|
# ============================================================
|
||
|
|
new_section "Dup-Topic Cluster (substantielle Vorkommen)"
|
||
|
|
keywords=("polymarket" "lightning" "iron.condor" "subconscious" "memory" "trading")
|
||
|
|
declare -A cluster_files
|
||
|
|
for kw in "${keywords[@]}"; do
|
||
|
|
cluster_files["$kw"]=""
|
||
|
|
done
|
||
|
|
for f in "${all_wiki[@]}"; do
|
||
|
|
case "$(basename "$f")" in
|
||
|
|
index.md|log.md|ideas.md|tasks.md) continue ;;
|
||
|
|
esac
|
||
|
|
for kw in "${keywords[@]}"; do
|
||
|
|
count=$(grep -ciE "\\b$kw\\b" "$f" 2>/dev/null)
|
||
|
|
count=${count:-0}
|
||
|
|
if [ "$count" -ge 3 ]; then
|
||
|
|
cluster_files["$kw"]="${cluster_files[$kw]}$f|$count
|
||
|
|
"
|
||
|
|
fi
|
||
|
|
done
|
||
|
|
done
|
||
|
|
cluster_found=0
|
||
|
|
for kw in "${keywords[@]}"; do
|
||
|
|
files_str="${cluster_files[$kw]}"
|
||
|
|
if [ -n "$files_str" ]; then
|
||
|
|
file_count=$(echo "$files_str" | grep -c "^[^-]")
|
||
|
|
if [ "$file_count" -ge 3 ]; then
|
||
|
|
cluster_found=$((cluster_found+1))
|
||
|
|
add_line "$status_warn **$kw**: $file_count Files (≥3 Vorkommen je File)"
|
||
|
|
while IFS='|' read -r fpath fcount; do
|
||
|
|
[ -n "$fpath" ] && add_line " - \`$fpath\` ($fcount Treffer)"
|
||
|
|
done <<< "$files_str"
|
||
|
|
fi
|
||
|
|
fi
|
||
|
|
done
|
||
|
|
if [ "$cluster_found" -eq 0 ]; then
|
||
|
|
add_line "$status_ok **keine signifikanten Cluster** (Schwelle: ≥3 Vorkommen in ≥3 Files)"
|
||
|
|
fi
|
||
|
|
|
||
|
|
# ============================================================
|
||
|
|
# CHECK 7: Stale Wiki (mtime > 30d ohne index-Update)
|
||
|
|
# ============================================================
|
||
|
|
new_section "Stale Pages (mtime > 30d)"
|
||
|
|
stale=()
|
||
|
|
thirty_days_ago=$(date -d "30 days ago" +%s 2>/dev/null || date -v-30d +%s 2>/dev/null || echo 0)
|
||
|
|
if [ "$thirty_days_ago" -gt 0 ]; then
|
||
|
|
while IFS= read -r f; do
|
||
|
|
mtime=$(stat -c %Y "$f" 2>/dev/null || stat -f %m "$f" 2>/dev/null || echo 0)
|
||
|
|
if [ "$mtime" -gt 0 ] && [ "$mtime" -lt "$thirty_days_ago" ]; then
|
||
|
|
stale+=("$f")
|
||
|
|
fi
|
||
|
|
done < <(printf '%s\n' "${all_wiki[@]}")
|
||
|
|
fi
|
||
|
|
|
||
|
|
if [ "${#stale[@]}" -gt 0 ]; then
|
||
|
|
add_line "$status_warn **${#stale[@]} stale pages** (>30d)"
|
||
|
|
for s in "${stale[@]:0:10}"; do add_line "- \`$s\`"; done
|
||
|
|
if [ "${#stale[@]}" -gt 10 ]; then add_line "- ... und $((${#stale[@]}-10)) weitere"; fi
|
||
|
|
else
|
||
|
|
add_line "$status_ok **0 stale**"
|
||
|
|
fi
|
||
|
|
|
||
|
|
# ============================================================
|
||
|
|
# CHECK 8: Uncovered Raw (raw/-Files ohne Wiki-Cross-Ref)
|
||
|
|
# ============================================================
|
||
|
|
new_section "Uncovered Raw Sources"
|
||
|
|
mapfile -t all_raw < <(find raw -name "*.md" -type f 2>/dev/null | sort)
|
||
|
|
uncovered=()
|
||
|
|
for r in "${all_raw[@]}"; do
|
||
|
|
rbase=$(basename "$r")
|
||
|
|
# Suche in Wiki nach dem Raw-Basename (ohne .md) oder nach dem Slug-Teil
|
||
|
|
slug=$(echo "$rbase" | sed 's/\.md$//' | sed 's/^[0-9-]*_//')
|
||
|
|
if ! grep -rq "$slug" wiki/ 2>/dev/null; then
|
||
|
|
uncovered+=("$r (slug: $slug)")
|
||
|
|
fi
|
||
|
|
done
|
||
|
|
|
||
|
|
if [ "${#uncovered[@]}" -gt 0 ]; then
|
||
|
|
add_line "$status_warn **${#uncovered[@]} uncovered raw**"
|
||
|
|
for u in "${uncovered[@]}"; do add_line "- \`$u\`"; done
|
||
|
|
else
|
||
|
|
add_line "$status_ok **${#all_raw[@]} raw, all covered**"
|
||
|
|
fi
|
||
|
|
|
||
|
|
# ============================================================
|
||
|
|
# Zusammenfassung + Empfehlung
|
||
|
|
# ============================================================
|
||
|
|
summary_section="## Summary"
|
||
|
|
section_lines+=("")
|
||
|
|
section_lines+=("$summary_section")
|
||
|
|
section_lines+=("")
|
||
|
|
section_lines+=("- Total Wiki-Pages: $total_pages")
|
||
|
|
section_lines+=("- Issues: $issues")
|
||
|
|
section_lines+=("")
|
||
|
|
if [ "$issues" -gt 0 ]; then
|
||
|
|
section_lines+=("→ **Action needed**: subagent spawn for fixes. See '## Recommended Subagent Spawns'.")
|
||
|
|
else
|
||
|
|
section_lines+=("→ Wiki is clean. No subagent spawn needed.")
|
||
|
|
fi
|
||
|
|
|
||
|
|
# ============================================================
|
||
|
|
# Empfohlene Subagent-Spawns (heuristisch)
|
||
|
|
# ============================================================
|
||
|
|
section_lines+=("")
|
||
|
|
section_lines+=("## Recommended Subagent Spawns")
|
||
|
|
section_lines+=("")
|
||
|
|
|
||
|
|
# Heuristik: Welche Subagents sind nötig?
|
||
|
|
if [ "${#missing_fm[@]}" -gt 0 ]; then
|
||
|
|
section_lines+=("- **frontmatter-migration** — ${#missing_fm[@]} files missing frontmatter")
|
||
|
|
fi
|
||
|
|
if [ "${#orphans[@]}" -gt 0 ]; then
|
||
|
|
section_lines+=("- **orphan-resolver** — ${#orphans[@]} orphan pages (link or delete)")
|
||
|
|
fi
|
||
|
|
if [ "${#stub_files[@]}" -gt 0 ] && [ "$stub_pct" -ge 10 ]; then
|
||
|
|
section_lines+=("- **stub-killer** — $stub_count stubs ($stub_pct%, threshold 10%)")
|
||
|
|
fi
|
||
|
|
# Dup-Topic-Cluster: bei polymarket/lightning/trading ≥3 hits
|
||
|
|
dup_warn=$(printf '%s\n' "${section_lines[@]}" | grep -c "^\*\*\(polymarket\|lightning\|iron\.condor\|trading\)\*\*" 2>/dev/null) || dup_warn=0
|
||
|
|
dup_warn=${dup_warn:-0}
|
||
|
|
if [ "$dup_warn" -gt 0 ]; then
|
||
|
|
section_lines+=("- **hub-page-creator** — dup-topic clusters detected")
|
||
|
|
fi
|
||
|
|
# Tools-Dir ≥12
|
||
|
|
if [ "${dir_count[wiki/tools]:-0}" -ge 12 ]; then
|
||
|
|
section_lines+=("- **tools-refactor** — wiki/tools/ hat ${dir_count[wiki/tools]} files")
|
||
|
|
fi
|
||
|
|
if [ "${#uncovered[@]}" -gt 0 ]; then
|
||
|
|
section_lines+=("- **raw-coverage-check** — ${#uncovered[@]} raw files ohne Wiki-Cross-Ref")
|
||
|
|
fi
|
||
|
|
|
||
|
|
if [ "$issues" -eq 0 ] && [ "${#orphans[@]}" -eq 0 ] && [ "$stub_pct" -lt 10 ]; then
|
||
|
|
section_lines+=("(keine — Wiki sauber)")
|
||
|
|
fi
|
||
|
|
|
||
|
|
# ============================================================
|
||
|
|
# Output schreiben
|
||
|
|
# ============================================================
|
||
|
|
{
|
||
|
|
echo "# Wiki Lint Report"
|
||
|
|
echo ""
|
||
|
|
echo "*Generated: $TIMESTAMP*"
|
||
|
|
echo "*Repo: $REPO_ROOT*"
|
||
|
|
echo "*Script: scripts/wiki-lint.sh v0.1*"
|
||
|
|
echo ""
|
||
|
|
printf '%s\n' "${section_lines[@]}"
|
||
|
|
} > "$OUTPUT"
|
||
|
|
|
||
|
|
# Console-Kurzfassung
|
||
|
|
echo "Wiki Lint — $TIMESTAMP"
|
||
|
|
echo "Total: $total_pages | Issues: $issues"
|
||
|
|
echo "Report: $OUTPUT"
|
||
|
|
|
||
|
|
# Exit-Code
|
||
|
|
if [ "$issues" -gt 0 ]; then
|
||
|
|
exit "$EXIT_ISSUES"
|
||
|
|
else
|
||
|
|
exit "$EXIT_OK"
|
||
|
|
fi
|