name: KB Lint on: push: paths: - 'kb/**/*.md' pull_request: paths: - 'kb/**/*.md' permissions: contents: read jobs: validate: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Validate KB Files run: | set -e # Colors for output RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[1;33m' NC='\033[0m' # No Color echo "Validating KB files..." # Find all KB files that changed if [ "${{ github.event_name }}" = "pull_request" ]; then # For pull requests, check changed files changed_files=$(git diff --name-only --diff-filter=ACMR origin/${{ github.base_ref }}...HEAD -- 'kb/**/*.md' || true) else # For push, check all KB files changed_files=$(git diff --name-only --diff-filter=ACMR HEAD~1...HEAD -- 'kb/**/*.md' || find kb -type f -name "*.md" 2>/dev/null || true) fi if [ -z "$changed_files" ]; then echo "No KB files changed, skipping validation" exit 0 fi errors=0 # Process each changed file while IFS= read -r file; do # Skip empty lines [ -z "$file" ] && continue # Skip if file doesn't exist (deleted files) [ ! -f "$file" ] && continue # Skip special directories and files if [[ "$file" == *"/_guides/"* ]] || \ [[ "$file" == *"/_templates/"* ]] || \ [[ "$file" == "kb/README.md" ]] || \ [[ "$file" == "kb/_index.md" ]] || \ [[ "$file" == "kb/CHANGELOG.md" ]]; then echo -e "${YELLOW}⏭️ Skipping: $file (excluded from validation)${NC}" continue fi filename=$(basename "$file") relative_path="${file#kb/}" file_errors=0 echo -e "\n${GREEN}Validating: $file${NC}" # Validate filename pattern if ! [[ "$filename" =~ ^[0-9]{4}-[0-9]{2}-[0-9]{2}--[a-z0-9-]+--(idea|note|spec|decision|howto|retro|meeting)(--p[0-9]+)?\.md$ ]]; then echo -e "${RED}❌ ERROR: Invalid filename pattern${NC}" echo " Expected: YYYY-MM-DD--slug--type.md" echo " Got: $filename" file_errors=$((file_errors + 1)) errors=$((errors + 1)) fi # Extract date and type from filename filename_date=$(echo "$filename" | sed -E 's/^([0-9]{4}-[0-9]{2}-[0-9]{2})--.*/\1/') filename_type=$(echo "$filename" | sed -E 's/.*--([a-z]+)(--p[0-9]+)?\.md$/\1/') # Check frontmatter exists if ! grep -q "^---$" "$file"; then echo -e "${RED}❌ ERROR: Missing frontmatter delimiter${NC}" echo " File must start with '---'" file_errors=$((file_errors + 1)) errors=$((errors + 1)) fi # Extract frontmatter frontmatter=$(sed -n '/^---$/,/^---$/p' "$file" | sed '1d;$d') if [ -z "$frontmatter" ]; then echo -e "${RED}❌ ERROR: Empty frontmatter${NC}" file_errors=$((file_errors + 1)) errors=$((errors + 1)) fi # Check required fields (14 base fields) REQUIRED_FIELDS=("title" "date" "author" "source" "project" "topics" "tags" "type" "status" "routing_hint" "proposed_path" "routing_confidence" "related" "summary") for field in "${REQUIRED_FIELDS[@]}"; do if ! echo "$frontmatter" | grep -q "^${field}:"; then echo -e "${RED}❌ ERROR: Missing required field: $field${NC}" file_errors=$((file_errors + 1)) errors=$((errors + 1)) fi done # Validate date matches frontmatter_date=$(echo "$frontmatter" | grep "^date:" | sed -E 's/^date:[[:space:]]*["'\'']?([^"'\'']+)["'\'']?.*/\1/' | tr -d ' ' | head -1) if [ "$frontmatter_date" != "$filename_date" ]; then echo -e "${RED}❌ ERROR: Date mismatch${NC}" echo " Filename date: $filename_date" echo " Frontmatter date: $frontmatter_date" file_errors=$((file_errors + 1)) errors=$((errors + 1)) fi # Validate type matches frontmatter_type=$(echo "$frontmatter" | grep "^type:" | sed -E 's/^type:[[:space:]]*["'\'']?([^"'\'']+)["'\'']?.*/\1/' | tr -d ' ' | head -1) if [ "$frontmatter_type" != "$filename_type" ]; then echo -e "${RED}❌ ERROR: Type mismatch${NC}" echo " Filename type: $filename_type" echo " Frontmatter type: $frontmatter_type" file_errors=$((file_errors + 1)) errors=$((errors + 1)) fi # Validate routing_confidence routing_confidence=$(echo "$frontmatter" | grep "^routing_confidence:" | sed -E 's/^routing_confidence:[[:space:]]*([0-9.]+).*/\1/' | tr -d ' ' | head -1) if ! awk -v conf="$routing_confidence" 'BEGIN {if (conf < 0.0 || conf > 1.0 || conf == "") exit 1}' 2>/dev/null; then echo -e "${RED}❌ ERROR: Invalid routing_confidence value${NC}" echo " Value: $routing_confidence" echo " Must be numeric between 0.00 and 1.00" file_errors=$((file_errors + 1)) errors=$((errors + 1)) fi # Enforce review queue policy if awk -v conf="$routing_confidence" 'BEGIN {exit !(conf < 0.60)}' 2>/dev/null; then if [[ "$file" != *"/_review_queue/"* ]]; then echo -e "${RED}❌ ERROR: File has routing_confidence < 0.60 but is not in kb/_review_queue/${NC}" echo " routing_confidence: $routing_confidence" echo " File path: $file" file_errors=$((file_errors + 1)) errors=$((errors + 1)) fi fi if [ $file_errors -eq 0 ]; then echo -e "${GREEN}✅ Valid: $file${NC}" fi done <<< "$changed_files" if [ $errors -gt 0 ]; then echo -e "\n${RED}Validation failed with $errors error(s)${NC}" exit 1 else echo -e "\n${GREEN}All KB files validated successfully${NC}" exit 0 fi