Files
2026-07-12 21:26:08 +08:00

218 lines
7.7 KiB
Bash
Executable File
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/bin/bash
# lint-runner.sh — wiki 机械健康检查
# 用法:bash scripts/lint-runner.sh <wiki_root>
# 输出:结构化文本报告(供 AI 后续分析使用)
# 退出码:0 = 运行完成,1 = 脚本错误(路径不存在、wiki 结构不完整)
set -u
shopt -s nullglob
WIKI_ROOT="${1:-.}"
WIKI_DIR="$WIKI_ROOT/wiki"
INDEX_FILE="$WIKI_ROOT/index.md"
if [ ! -d "$WIKI_DIR" ]; then
echo "ERROR: wiki 目录不存在:$WIKI_DIR" >&2
echo " 请确认路径正确,或先运行 init 工作流初始化知识库。" >&2
exit 1
fi
if [ ! -f "$INDEX_FILE" ]; then
echo "ERROR: index.md 不存在:$INDEX_FILE" >&2
exit 1
fi
index_has_entry() {
local entry="$1"
grep -ohE "\[\[[^]]+\]\]" "$INDEX_FILE" 2>/dev/null | \
sed -e 's/\[\[//g' -e 's/\]\]//g' -e 's/|.*//' | \
grep -Fxq "$entry"
}
echo "=== llm-wiki lint 报告 ==="
echo "时间:$(date '+%Y-%m-%d %H:%M')"
echo "检查路径:$WIKI_DIR"
echo ""
# 检查 1:孤立页面
# 定义:entities/、topics/、sources/ 下的页面,除了自己之外没有任何其他 wiki 页面用 [[名称]] 引用它
echo "--- 孤立页面(没有被其他页面引用) ---"
_ORPHANS=0
for _subdir in entities topics sources; do
for f in "$WIKI_DIR"/$_subdir/*.md; do
[ -f "$f" ] || continue
BASENAME=$(basename "$f" .md)
if ! grep -rlF "[[$BASENAME]]" "$WIKI_DIR" 2>/dev/null | grep -vxF "$f" | grep -q .; then
echo " 孤立: $_subdir/$BASENAME"
_ORPHANS=$((_ORPHANS + 1))
fi
done
done
[ "$_ORPHANS" -eq 0 ] && echo " (无孤立页面)"
echo ""
# 检查 2:断链
# 定义:wiki/ 下的页面里有 [[X]] 链接(支持 [[X|别名]] 语法),但 wiki/ 任意子目录找不到 X.md
echo "--- 断链(被链接但不存在的页面) ---"
_TMP_BROKEN=$(mktemp)
grep -rohE "\[\[[^]]+\]\]" "$WIKI_DIR" 2>/dev/null | \
sed -e 's/\[\[//g' -e 's/\]\]//g' -e 's/|.*//' | \
sort -u | \
while read -r LINK; do
[ -z "$LINK" ] && continue
if ! find "$WIKI_DIR" -name "$LINK.md" 2>/dev/null | grep -q .; then
echo " 断链: [[$LINK]]"
echo "$LINK" >> "$_TMP_BROKEN"
fi
done
if [ ! -s "$_TMP_BROKEN" ]; then
echo " (无断链)"
fi
rm -f "$_TMP_BROKEN"
echo ""
# 检查 3index 一致性
# 定义:index.md 里有 [[X]] 记录(去掉别名),但 wiki/ 任意子目录都找不到 X.md
echo "--- index 一致性(index.md 有记录但文件缺失) ---"
_TMP_MISSING=$(mktemp)
grep -ohE "\[\[[^]]+\]\]" "$INDEX_FILE" 2>/dev/null | \
sed -e 's/\[\[//g' -e 's/\]\]//g' -e 's/|.*//' | \
sort -u | \
while read -r ENTRY; do
[ -z "$ENTRY" ] && continue
if ! find "$WIKI_DIR" -name "$ENTRY.md" 2>/dev/null | grep -q .; then
echo " index 有但文件缺失: $ENTRY"
echo "$ENTRY" >> "$_TMP_MISSING"
fi
done
if [ ! -s "$_TMP_MISSING" ]; then
echo " index 与文件一致)"
fi
rm -f "$_TMP_MISSING"
echo ""
# 检查 4:反向 index 一致性
# 定义:wiki/ 下实际存在的页面,但 index.md 里没有 [[页面名]] 记录
# 排除 derived 页面(queries/、synthesis/sessions/
echo "--- 反向 index 一致性(文件存在但 index.md 未收录) ---"
_TMP_UNLISTED=$(mktemp)
for _subdir in entities topics sources comparisons synthesis; do
for f in "$WIKI_DIR"/$_subdir/*.md; do
[ -f "$f" ] || continue
BASENAME=$(basename "$f" .md)
# 跳过 derived 页面
case "$f" in
*/queries/*|*/sessions/*) continue ;;
esac
if ! index_has_entry "$BASENAME"; then
echo " 未收录: $_subdir/$BASENAME"
echo "$BASENAME" >> "$_TMP_UNLISTED"
fi
done
done
if [ ! -s "$_TMP_UNLISTED" ]; then
echo " (所有页面均已收录)"
fi
rm -f "$_TMP_UNLISTED"
echo ""
# 检查 5:图片资产一致性
# 定义:source 页面 frontmatter 中 image_paths 列出的文件,在知识库中是否实际存在
# 支持 block list 格式和 inline array 格式
echo "--- 图片资产一致性(image_paths 声明但文件缺失) ---"
_IMG_ISSUES=0
for f in "$WIKI_DIR"/sources/*.md; do
[ -f "$f" ] || continue
_BASENAME=$(basename "$f" .md)
# 提取 frontmatter 中 image_paths 的值
_IN_FM=false
_IN_IMG=false
_INLINE_VAL=""
while IFS= read -r line; do
case "$line" in
"---")
if [ "$_IN_FM" = true ]; then break; fi
_IN_FM=true
continue
;;
esac
[ "$_IN_FM" = true ] || continue
case "$line" in
image_paths:*)
# 检查是否有 inline value(如 image_paths: ["a.png", "b.jpg"]
_INLINE_VAL=$(echo "$line" | sed 's/^image_paths:[[:space:]]*//')
if [ -n "$_INLINE_VAL" ] && [ "$_INLINE_VAL" != "[]" ]; then
# 解析 inline array:去掉 [],按逗号分割
echo "$_INLINE_VAL" | tr -d '[]' | tr ',' '\n' | while IFS= read -r _ITEM; do
_PATH=$(echo "$_ITEM" | sed 's/^[[:space:]]*//' | sed 's/[[:space:]]*$//' | tr -d '"' | tr -d "'")
[ -z "$_PATH" ] && continue
if [ ! -f "$WIKI_ROOT/$_PATH" ]; then
echo " 缺失: $_BASENAME$_PATH"
fi
done
_INLINE_COUNT=$(echo "$_INLINE_VAL" | tr -d '[]' | tr ',' '\n' | while IFS= read -r _ITEM; do
_P=$(echo "$_ITEM" | sed 's/^[[:space:]]*//' | sed 's/[[:space:]]*$//' | tr -d '"' | tr -d "'")
[ -z "$_P" ] && continue
[ ! -f "$WIKI_ROOT/$_P" ] && echo "x"
done | wc -l | tr -d ' ')
_IMG_ISSUES=$((_IMG_ISSUES + _INLINE_COUNT))
_IN_IMG=false
else
_IN_IMG=true
fi
continue
;;
" - "*)
if [ "$_IN_IMG" = true ]; then
_PATH=$(echo "$line" | sed 's/^[[:space:]]*- //' | sed 's/^[[:space:]]*//' | sed 's/[[:space:]]*$//' | tr -d '"' | tr -d "'")
[ -z "$_PATH" ] && continue
if [ ! -f "$WIKI_ROOT/$_PATH" ]; then
echo " 缺失: $_BASENAME$_PATH"
_IMG_ISSUES=$((_IMG_ISSUES + 1))
fi
fi
;;
*) _IN_IMG=false ;;
esac
done < "$f"
done
[ "$_IMG_ISSUES" -eq 0 ] && echo " (无缺失图片)"
echo ""
# 检查 6source-signal 覆盖情况
echo "--- source-signal 覆盖情况 ---"
_COVERAGE_SCRIPT="$(cd "$(dirname "$0")" && pwd)/source-signal-coverage.js"
if [ -f "$_COVERAGE_SCRIPT" ] && command -v node >/dev/null 2>&1; then
_COVERAGE_JSON=$(node "$_COVERAGE_SCRIPT" "$WIKI_ROOT" 2>/dev/null)
if [ $? -eq 0 ] && [ -n "$_COVERAGE_JSON" ]; then
node -e '
const data = JSON.parse(require("fs").readFileSync("/dev/stdin", "utf8"));
const s = data.summary;
console.log(" 已参与:" + s.ok);
console.log(" 缺少 sources 字段:" + s.missing_sources);
console.log(" sources 为空:" + s.empty_sources);
console.log(" sources 格式无效:" + s.invalid_sources);
console.log(" 当前不参与:" + s.not_applicable);
const issues = data.pages.filter(p => p.reason !== "ok" && p.reason !== "not_applicable");
if (issues.length > 0) {
const byReason = { missing_sources: [], empty_sources: [], invalid_sources: [] };
for (const p of issues) { if (byReason[p.reason]) byReason[p.reason].push(p.path); }
for (const [reason, paths] of Object.entries(byReason)) {
if (paths.length === 0) continue;
const label = { missing_sources: "缺少 sources 字段", empty_sources: "sources 为空", invalid_sources: "sources 格式无效" }[reason];
console.log("");
console.log(" " + label + "");
for (const p of paths) console.log(" - " + p);
}
}
' <<< "$_COVERAGE_JSON"
else
echo " coverage 脚本执行失败,跳过覆盖检查)"
fi
else
echo " coverage 脚本或 node 不可用,跳过覆盖检查)"
fi
echo ""
echo "=== 机械检查完成。矛盾检测、交叉引用、置信度抽查由 AI 继续执行 ==="
exit 0