#!/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 - </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