初始化
This commit is contained in:
347
scripts/context-doctor.sh
Normal file
347
scripts/context-doctor.sh
Normal file
@@ -0,0 +1,347 @@
|
||||
#!/usr/bin/env bash
|
||||
# =============================================================================
|
||||
# Context Doctor — 检查项目上下文健康状态
|
||||
# =============================================================================
|
||||
# 用法:
|
||||
# bash scripts/context-doctor.sh # 全量检查
|
||||
# bash scripts/context-doctor.sh --lite # 轻量检查(提交前)
|
||||
# bash scripts/context-doctor.sh --lite --strict
|
||||
# 功能: 检查规则文件/MCP配置/文档完整性/索引排除等
|
||||
# =============================================================================
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
BLUE='\033[0;34m'
|
||||
GREEN='\033[0;32m'
|
||||
RED='\033[0;31m'
|
||||
YELLOW='\033[1;33m'
|
||||
NC='\033[0m'
|
||||
|
||||
PASS=0
|
||||
WARN=0
|
||||
FAIL=0
|
||||
LITE_MODE=false
|
||||
STRICT_MODE=false
|
||||
|
||||
for arg in "$@"; do
|
||||
case "$arg" in
|
||||
--lite)
|
||||
LITE_MODE=true
|
||||
;;
|
||||
--strict)
|
||||
STRICT_MODE=true
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
check_exists() {
|
||||
local label="$1"
|
||||
local path="$2"
|
||||
local required="${3:-true}"
|
||||
|
||||
printf " %-40s" "$label"
|
||||
if [ -e "$path" ]; then
|
||||
echo -e "${GREEN}✅${NC}"
|
||||
PASS=$((PASS + 1))
|
||||
elif [ "$required" = "true" ]; then
|
||||
echo -e "${RED}❌ Missing${NC}"
|
||||
FAIL=$((FAIL + 1))
|
||||
else
|
||||
echo -e "${YELLOW}⚠️ Optional${NC}"
|
||||
WARN=$((WARN + 1))
|
||||
fi
|
||||
}
|
||||
|
||||
check_mcp_enabled() {
|
||||
local server="$1"
|
||||
local required="${2:-true}"
|
||||
|
||||
printf " %-40s" " $server"
|
||||
if [ ! -f ".cursor/mcp.json" ]; then
|
||||
if [ "$required" = "true" ]; then
|
||||
echo -e "${RED}❌ mcp.json missing${NC}"
|
||||
FAIL=$((FAIL + 1))
|
||||
else
|
||||
echo -e "${YELLOW}⚠️ mcp.json missing${NC}"
|
||||
WARN=$((WARN + 1))
|
||||
fi
|
||||
return
|
||||
fi
|
||||
|
||||
if grep -q "\"$server\"" .cursor/mcp.json; then
|
||||
if python3 - <<PY >/dev/null 2>&1
|
||||
import json
|
||||
with open(".cursor/mcp.json", "r", encoding="utf-8") as f:
|
||||
data = json.load(f)
|
||||
srv = data.get("mcpServers", {}).get("$server", {})
|
||||
raise SystemExit(0 if srv.get("disabled") is False else 1)
|
||||
PY
|
||||
then
|
||||
echo -e "${GREEN}✅ enabled${NC}"
|
||||
PASS=$((PASS + 1))
|
||||
else
|
||||
if [ "$required" = "true" ]; then
|
||||
echo -e "${YELLOW}⚠️ found but disabled${NC}"
|
||||
WARN=$((WARN + 1))
|
||||
else
|
||||
echo -e "${YELLOW}⚠️ disabled${NC}"
|
||||
WARN=$((WARN + 1))
|
||||
fi
|
||||
fi
|
||||
else
|
||||
if [ "$required" = "true" ]; then
|
||||
echo -e "${YELLOW}⚠️ not configured${NC}"
|
||||
WARN=$((WARN + 1))
|
||||
else
|
||||
echo -e "${YELLOW}⚠️ optional / not configured${NC}"
|
||||
WARN=$((WARN + 1))
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
echo ""
|
||||
echo -e "${BLUE}🩺 Context Doctor — Health Check${NC}"
|
||||
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||
|
||||
echo ""
|
||||
echo -e "${BLUE}📐 Cursor Rules${NC}"
|
||||
check_exists "Constitution" ".cursor/rules/000-constitution.mdc"
|
||||
check_exists "Workflow" ".cursor/rules/001-workflow.mdc"
|
||||
check_exists "Safety" ".cursor/rules/002-safety.mdc"
|
||||
check_exists "TypeScript" ".cursor/rules/010-typescript.mdc" "false"
|
||||
check_exists "Vue 3" ".cursor/rules/011-vue.mdc" "false"
|
||||
check_exists "Testing" ".cursor/rules/015-testing.mdc" "false"
|
||||
check_exists "Monitoring" ".cursor/rules/024-monitoring.mdc" "false"
|
||||
|
||||
echo ""
|
||||
echo -e "${BLUE}🔌 MCP Configuration${NC}"
|
||||
check_exists "MCP Config" ".cursor/mcp.json"
|
||||
|
||||
if [ -f ".cursor/mcp.json" ]; then
|
||||
MCP_COUNT=$(grep -c '"command"' .cursor/mcp.json 2>/dev/null || echo 0)
|
||||
ENABLED_COUNT=$(grep -c '"disabled": false' .cursor/mcp.json 2>/dev/null || echo 0)
|
||||
printf " %-40s" "MCP Servers (total/enabled)"
|
||||
echo -e "${GREEN}$MCP_COUNT / $ENABLED_COUNT${NC}"
|
||||
|
||||
printf " %-40s" "Core MCP status"
|
||||
echo ""
|
||||
# Core servers for this template's workflow quality
|
||||
check_mcp_enabled "filesystem" "true"
|
||||
check_mcp_enabled "git" "true"
|
||||
check_mcp_enabled "context7" "true"
|
||||
check_mcp_enabled "fetch" "true"
|
||||
check_mcp_enabled "memory" "true"
|
||||
check_mcp_enabled "sequential-thinking" "true"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo -e "${BLUE}📚 Documentation${NC}"
|
||||
if [ "$LITE_MODE" = "false" ]; then
|
||||
check_exists "PRD" "docs/vision/PRD.md"
|
||||
check_exists "System Design" "docs/architecture/system-design.md" "false"
|
||||
check_exists "Data Model" "docs/architecture/data-model.md" "false"
|
||||
check_exists "ADR Template" "docs/architecture/decisions/_template.md" "false"
|
||||
else
|
||||
check_exists "PRD" "docs/vision/PRD.md"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo -e "${BLUE}🧠 Agent Skills${NC}"
|
||||
check_exists "Skills Directory" ".cursor/skills"
|
||||
check_exists "Skills Rule" ".cursor/rules/003-skills.mdc"
|
||||
if [ -d ".cursor/skills" ] && [ "$LITE_MODE" = "false" ]; then
|
||||
SKILL_COUNT=0
|
||||
SKILL_ERRORS=0
|
||||
for skill_dir in .cursor/skills/*/; do
|
||||
[ -d "$skill_dir" ] || continue
|
||||
skill_name=$(basename "$skill_dir")
|
||||
if [ -f "$skill_dir/SKILL.md" ]; then
|
||||
SKILL_COUNT=$((SKILL_COUNT + 1))
|
||||
skill_ok=true
|
||||
# 检查 frontmatter 起始符
|
||||
if ! head -1 "$skill_dir/SKILL.md" | grep -q "^---$"; then
|
||||
printf " %-40s" " $skill_name"
|
||||
echo -e "${RED}❌ Missing frontmatter (---)${NC}"
|
||||
SKILL_ERRORS=$((SKILL_ERRORS + 1))
|
||||
FAIL=$((FAIL + 1))
|
||||
skill_ok=false
|
||||
fi
|
||||
# 检查 name 字段
|
||||
if ! grep -q "^name:" "$skill_dir/SKILL.md"; then
|
||||
printf " %-40s" " $skill_name"
|
||||
echo -e "${RED}❌ Missing 'name:' field${NC}"
|
||||
SKILL_ERRORS=$((SKILL_ERRORS + 1))
|
||||
FAIL=$((FAIL + 1))
|
||||
skill_ok=false
|
||||
fi
|
||||
# 检查 description 字段
|
||||
if ! grep -q "^description:" "$skill_dir/SKILL.md"; then
|
||||
printf " %-40s" " $skill_name"
|
||||
echo -e "${RED}❌ Missing 'description:' field${NC}"
|
||||
SKILL_ERRORS=$((SKILL_ERRORS + 1))
|
||||
FAIL=$((FAIL + 1))
|
||||
skill_ok=false
|
||||
fi
|
||||
# 检查 description 长度不超过 1024 字符
|
||||
if grep -q "^description:" "$skill_dir/SKILL.md"; then
|
||||
desc_len=$(awk '/^description:/,/^[a-z]/' "$skill_dir/SKILL.md" | wc -c)
|
||||
if [ "$desc_len" -gt 1024 ]; then
|
||||
printf " %-40s" " $skill_name"
|
||||
echo -e "${YELLOW}⚠️ description > 1024 chars ($desc_len)${NC}"
|
||||
WARN=$((WARN + 1))
|
||||
fi
|
||||
fi
|
||||
else
|
||||
printf " %-40s" " $skill_name"
|
||||
echo -e "${RED}❌ Missing SKILL.md${NC}"
|
||||
SKILL_ERRORS=$((SKILL_ERRORS + 1))
|
||||
FAIL=$((FAIL + 1))
|
||||
fi
|
||||
done
|
||||
printf " %-40s" "Skills installed"
|
||||
if [ "$SKILL_ERRORS" -eq 0 ]; then
|
||||
echo -e "${GREEN}$SKILL_COUNT skills (all valid)${NC}"
|
||||
else
|
||||
echo -e "${YELLOW}$SKILL_COUNT skills ($SKILL_ERRORS with errors)${NC}"
|
||||
fi
|
||||
# Token 成本估算
|
||||
SKILL_TOTAL_LINES=0
|
||||
for f in .cursor/skills/*/SKILL.md; do
|
||||
[ -f "$f" ] || continue
|
||||
lines=$(wc -l < "$f" 2>/dev/null || echo 0)
|
||||
SKILL_TOTAL_LINES=$((SKILL_TOTAL_LINES + lines))
|
||||
done
|
||||
printf " %-40s" "Total skill lines (SKILL.md only)"
|
||||
if [ "$SKILL_TOTAL_LINES" -lt 3000 ]; then
|
||||
echo -e "${GREEN}$SKILL_TOTAL_LINES lines${NC}"
|
||||
else
|
||||
echo -e "${YELLOW}$SKILL_TOTAL_LINES lines (consider trimming)${NC}"
|
||||
fi
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo -e "${BLUE}🛠️ Tooling${NC}"
|
||||
check_exists "AI Guard" "scripts/ai-guard.sh"
|
||||
check_exists ".cursorignore" ".cursorignore"
|
||||
check_exists ".editorconfig" ".editorconfig" "false"
|
||||
check_exists ".gitignore" ".gitignore"
|
||||
|
||||
echo ""
|
||||
echo -e "${BLUE}🔗 Reference Integrity${NC}"
|
||||
if command -v python3 >/dev/null 2>&1; then
|
||||
REF_CHECK=$(
|
||||
python3 - <<'PY'
|
||||
from pathlib import Path
|
||||
import re
|
||||
|
||||
root = Path(".")
|
||||
broken = []
|
||||
skill_refs = 0
|
||||
rule_refs = 0
|
||||
|
||||
# Skills: check references/*.md links in SKILL.md
|
||||
for skill in (root / ".cursor" / "skills").glob("*/SKILL.md"):
|
||||
text = skill.read_text(encoding="utf-8")
|
||||
refs = re.findall(r'`(references/[^`*]+\.md)`', text)
|
||||
for ref in refs:
|
||||
skill_refs += 1
|
||||
target = skill.parent / ref
|
||||
if not target.exists():
|
||||
broken.append((str(skill), ref))
|
||||
|
||||
# Rules: check .cursor/rules/references/*.md links in .mdc
|
||||
for rule in (root / ".cursor" / "rules").glob("*.mdc"):
|
||||
text = rule.read_text(encoding="utf-8")
|
||||
refs = re.findall(r'`(\.cursor/rules/references/[^`*]+\.md)`', text)
|
||||
for ref in refs:
|
||||
rule_refs += 1
|
||||
target = root / ref
|
||||
if not target.exists():
|
||||
broken.append((str(rule), ref))
|
||||
|
||||
print(f"SKILL_REFS={skill_refs}")
|
||||
print(f"RULE_REFS={rule_refs}")
|
||||
print(f"BROKEN={len(broken)}")
|
||||
for path, ref in broken[:10]:
|
||||
print(f"MISS:{path} -> {ref}")
|
||||
PY
|
||||
)
|
||||
|
||||
SKILL_REFS=$(echo "$REF_CHECK" | awk -F= '/^SKILL_REFS=/{print $2}')
|
||||
RULE_REFS=$(echo "$REF_CHECK" | awk -F= '/^RULE_REFS=/{print $2}')
|
||||
BROKEN_REFS=$(echo "$REF_CHECK" | awk -F= '/^BROKEN=/{print $2}')
|
||||
|
||||
printf " %-40s" "Skill reference links checked"
|
||||
echo -e "${GREEN}${SKILL_REFS:-0}${NC}"
|
||||
PASS=$((PASS + 1))
|
||||
|
||||
printf " %-40s" "Rule reference links checked"
|
||||
echo -e "${GREEN}${RULE_REFS:-0}${NC}"
|
||||
PASS=$((PASS + 1))
|
||||
|
||||
printf " %-40s" "Broken reference links"
|
||||
if [ "${BROKEN_REFS:-0}" -eq 0 ]; then
|
||||
echo -e "${GREEN}0${NC}"
|
||||
PASS=$((PASS + 1))
|
||||
else
|
||||
echo -e "${RED}${BROKEN_REFS} (see below)${NC}"
|
||||
FAIL=$((FAIL + 1))
|
||||
echo "$REF_CHECK" | awk '/^MISS:/ { sub(/^MISS:/, " - "); print }'
|
||||
fi
|
||||
else
|
||||
printf " %-40s" "Reference link check"
|
||||
echo -e "${YELLOW}⚠️ skipped (python3 not found)${NC}"
|
||||
WARN=$((WARN + 1))
|
||||
fi
|
||||
|
||||
if [ "$LITE_MODE" = "false" ]; then
|
||||
echo ""
|
||||
echo -e "${BLUE}📊 Rule File Sizes${NC}"
|
||||
fi
|
||||
if [ -d ".cursor/rules" ] && [ "$LITE_MODE" = "false" ]; then
|
||||
TOTAL_LINES=0
|
||||
for f in .cursor/rules/*.mdc; do
|
||||
lines=$(wc -l < "$f" 2>/dev/null || echo 0)
|
||||
TOTAL_LINES=$((TOTAL_LINES + lines))
|
||||
done
|
||||
RULE_REF_COUNT=0
|
||||
if [ -d ".cursor/rules/references" ]; then
|
||||
RULE_REF_COUNT=$(find .cursor/rules/references -name "*.md" | wc -l)
|
||||
fi
|
||||
printf " %-40s" "Total rule lines"
|
||||
# New baseline after core/deep split:
|
||||
# <1500: lean core rules
|
||||
# <2500: healthy for medium projects
|
||||
# <3500: still usable, but consider more split
|
||||
if [ "$TOTAL_LINES" -lt 1500 ]; then
|
||||
echo -e "${GREEN}$TOTAL_LINES lines (Lean — core rules only)${NC}"
|
||||
elif [ "$TOTAL_LINES" -lt 2500 ]; then
|
||||
echo -e "${GREEN}$TOTAL_LINES lines (Healthy — split mode)${NC}"
|
||||
elif [ "$TOTAL_LINES" -lt 3500 ]; then
|
||||
echo -e "${YELLOW}$TOTAL_LINES lines (Manageable — consider more split)${NC}"
|
||||
else
|
||||
echo -e "${RED}$TOTAL_LINES lines (Heavy — may waste tokens)${NC}"
|
||||
fi
|
||||
if [ "$RULE_REF_COUNT" -gt 0 ]; then
|
||||
printf " %-40s" "Rule deep references"
|
||||
echo -e "${GREEN}$RULE_REF_COUNT files${NC}"
|
||||
fi
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||
echo -e " ${GREEN}✅ $PASS${NC} ${RED}❌ $FAIL${NC} ${YELLOW}⚠️ $WARN${NC}"
|
||||
|
||||
if [ "$FAIL" -gt 0 ]; then
|
||||
echo ""
|
||||
echo -e "${RED}Some critical files are missing. Run: bash scripts/setup.sh${NC}"
|
||||
fi
|
||||
echo ""
|
||||
|
||||
if [ "$FAIL" -gt 0 ]; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ "$STRICT_MODE" = "true" ] && [ "$WARN" -gt 0 ]; then
|
||||
exit 1
|
||||
fi
|
||||
Reference in New Issue
Block a user