Files
gh-onezerocompany-claude-pr…/skills/spec-author/scripts/validate-spec.sh
2025-11-30 08:45:31 +08:00

386 lines
13 KiB
Bash
Executable File

#!/bin/bash
# Spec Author Validation Script
# Validates specification documents against required templates and standards
set -o pipefail
SPEC_FILE="${1:-.}"
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# Find git repo root for reliable path resolution
# This allows scripts to work when called from any directory in the repo
if git -C "$SCRIPT_DIR" rev-parse --git-dir > /dev/null 2>&1; then
PROJECT_ROOT="$(git -C "$SCRIPT_DIR" rev-parse --show-toplevel)"
else
# Fallback to relative path traversal (4 levels up from scripts/)
# From: project-root/project-basics/skills/spec-author/scripts/
# To: project-root/
PROJECT_ROOT="$(cd "$SCRIPT_DIR/../../../.." && pwd)"
fi
# Templates are located in the spec-author plugin directory
TEMPLATES_DIR="$SCRIPT_DIR/../templates"
# Check if file is a template file
is_template_file() {
local basename=$(basename "$SPEC_FILE")
# Check if file matches any template filename
[[ "$basename" == "api-contract.md" ]] ||
[[ "$basename" == "business-requirement.md" ]] ||
[[ "$basename" == "component.md" ]] ||
[[ "$basename" == "configuration-schema.md" ]] ||
[[ "$basename" == "constitution.md" ]] ||
[[ "$basename" == "data-model.md" ]] ||
[[ "$basename" == "deployment-procedure.md" ]] ||
[[ "$basename" == "design-document.md" ]] ||
[[ "$basename" == "flow-schematic.md" ]] ||
[[ "$basename" == "milestone.md" ]] ||
[[ "$basename" == "plan.md" ]] ||
[[ "$basename" == "technical-requirement.md" ]]
}
# Color codes
RED='\033[0;31m'
YELLOW='\033[1;33m'
GREEN='\033[0;32m'
BLUE='\033[0;34m'
NC='\033[0m' # No Color
# Check if file exists
if [ ! -f "$SPEC_FILE" ]; then
echo -e "${RED}✗ Error: Spec file not found: $SPEC_FILE${NC}"
exit 1
fi
# Skip validation for template files themselves
if is_template_file; then
echo -e "${BLUE}📋 SPEC VALIDATION REPORT${NC}"
echo "===================="
echo ""
echo "Spec: $(basename "$SPEC_FILE")"
echo "Status: ${GREEN}✓ TEMPLATE${NC} (templates are not validated, they are used as references)"
echo ""
exit 0
fi
# Determine spec type from filename or spec ID
determine_spec_type() {
local filename=$(basename "$SPEC_FILE" .md)
# First check by spec ID prefix in filename
case "$filename" in
brd-*) echo "business"; return ;;
prd-*) echo "technical"; return ;;
des-*) echo "design"; return ;;
api-*) echo "api"; return ;;
data-*) echo "data"; return ;;
cmp-*) echo "component"; return ;;
pln-*) echo "plan"; return ;;
mls-*) echo "milestone"; return ;;
flow-*) echo "flow"; return ;;
deploy-*) echo "deployment"; return ;;
config-*) echo "config"; return ;;
esac
# Then check by full template name
if [[ "$filename" == "design-document" ]] || [[ "$SPEC_FILE" =~ design-document ]]; then
echo "design"
elif [[ "$filename" == "technical-requirement" ]] || [[ "$SPEC_FILE" =~ technical-requirement ]]; then
echo "technical"
elif [[ "$filename" == "business-requirement" ]] || [[ "$SPEC_FILE" =~ business-requirement ]]; then
echo "business"
elif [[ "$filename" == "api-contract" ]] || [[ "$SPEC_FILE" =~ api-contract ]]; then
echo "api"
elif [[ "$filename" == "data-model" ]] || [[ "$SPEC_FILE" =~ data-model ]]; then
echo "data"
elif [[ "$filename" == "component" ]] || [[ "$SPEC_FILE" =~ component ]]; then
echo "component"
elif [[ "$filename" == "plan" ]] || [[ "$SPEC_FILE" =~ plan ]]; then
echo "plan"
elif [[ "$filename" == "milestone" ]] || [[ "$SPEC_FILE" =~ milestone ]]; then
echo "milestone"
elif [[ "$filename" == "flow-schematic" ]] || [[ "$SPEC_FILE" =~ flow-schematic ]]; then
echo "flow"
elif [[ "$filename" == "deployment-procedure" ]] || [[ "$SPEC_FILE" =~ deployment-procedure ]]; then
echo "deployment"
elif [[ "$filename" == "configuration-schema" ]] || [[ "$SPEC_FILE" =~ configuration-schema ]]; then
echo "config"
else
echo "unknown"
fi
}
# Get required sections for spec type
get_required_sections() {
local spec_type=$1
case $spec_type in
design)
echo "Executive Summary|Problem Statement|Goals & Success Criteria|Proposed Solution|Implementation Plan|Risk & Mitigation"
;;
technical)
echo "Overview|Requirements|Constraints|Success Criteria"
;;
business)
echo "Description|Business Value|Stakeholders|User Stories|Acceptance Criteria"
;;
api)
echo "Endpoints|Request/Response|Authentication|Error Handling"
;;
data)
echo "Entities|Fields|Relationships|Constraints"
;;
component)
echo "Description|Responsibilities|Interfaces|Configuration"
;;
plan)
echo "Overview|Phases|Deliverables|Success Metrics"
;;
milestone)
echo "Milestone|Deliverables|Success Criteria"
;;
flow)
echo "Overview|Process Flow|Steps|Decision Points"
;;
deployment)
echo "Prerequisites|Instructions|Rollback|Monitoring"
;;
config)
echo "Schema|Fields|Validation|Examples"
;;
*)
echo "Title"
;;
esac
}
# Count lines and TODOs
count_todos() {
grep -o "TODO" "$SPEC_FILE" | wc -l
}
# Count TODO items more accurately
count_todo_items() {
local count
count=$(grep -cE "(TODO|<!-- TODO|<!-- \[TODO)" "$SPEC_FILE" 2>/dev/null)
count=${count:-0}
echo "$count"
}
# Check for sections with only template guidance (not filled in by user)
check_template_only_sections() {
local spec_type=$(determine_spec_type)
local required_sections=$(get_required_sections "$spec_type")
local template_only_sections=()
while IFS= read -r section; do
if [ -z "$section" ]; then
continue
fi
# Escape special regex characters in section name
local escaped_section=$(echo "$section" | sed 's/[[\.*^$/]/\\&/g')
# Find section content between the section header and the next section
local section_content=$(awk "/^## $escaped_section/{found=1; next} /^## /{found=0} found" "$SPEC_FILE")
# Remove only leading/trailing whitespace, keep all content
local trimmed_content=$(echo "$section_content" | sed 's/^[[:space:]]*//g' | sed 's/[[:space:]]*$//g')
# Check if section is completely empty or ONLY contains HTML comments
# (i.e., user hasn't written any actual content)
local non_comment_content=$(echo "$trimmed_content" | grep -v '^<!--' | grep -v '^-->' | grep -v '^$' | head -1)
if [ -z "$non_comment_content" ]; then
template_only_sections+=("$section")
fi
done < <(echo "$required_sections" | tr '|' '\n')
printf '%s\n' "${template_only_sections[@]}"
}
# Calculate completion percentage
calculate_completion() {
local spec_type=$(determine_spec_type)
local required_sections=$(get_required_sections "$spec_type")
local total_required=$(echo "$required_sections" | tr '|' '\n' | wc -l)
local found_sections=0
while IFS= read -r section; do
if grep -qE "^## $section" "$SPEC_FILE"; then
((found_sections++))
fi
done < <(echo "$required_sections" | tr '|' '\n')
# Check for TODO items
local todo_count
todo_count=$(count_todo_items)
local completed_percentage=$((found_sections * 100 / total_required))
# Reduce percentage based on TODO count (max reduction of 25%)
if [ "$todo_count" -gt 0 ] 2>/dev/null; then
local todo_penalty=$((todo_count * 2))
[ $todo_penalty -gt 25 ] && todo_penalty=25
completed_percentage=$((completed_percentage - todo_penalty))
fi
[ $completed_percentage -lt 0 ] && completed_percentage=0
echo "$completed_percentage"
}
# Validate spec structure
validate_spec() {
local spec_type=$(determine_spec_type)
local required_sections=$(get_required_sections "$spec_type")
local content=$(cat "$SPEC_FILE")
local pass_count=0
local warn_count=0
local error_count=0
local pass_items=()
local warn_items=()
local error_items=()
# Check for proper formatting
if grep -qE "^# " "$SPEC_FILE"; then
pass_items+=("Title present")
((pass_count++))
else
error_items+=("Missing title (# Title)")
((error_count++))
fi
# Check required sections
while IFS= read -r section; do
if [ -z "$section" ]; then
continue
fi
if grep -qE "^## $section" "$SPEC_FILE"; then
pass_items+=("Section '$section' present")
((pass_count++))
else
error_items+=("Missing required section: '$section'")
((error_count++))
fi
done < <(echo "$required_sections" | tr '|' '\n')
# Check for TODO items - only flag non-comment TODOs
# TODOs inside HTML comments <!-- ... --> are just guidance and are acceptable
local todo_count
todo_count=$(grep -v "^<!--" "$SPEC_FILE" | grep -cE "TODO" 2>/dev/null || echo 0)
if [ "$todo_count" -gt 0 ] 2>/dev/null; then
error_items+=("Contains $todo_count TODO items that must be resolved")
((error_count++))
else
pass_items+=("No TODO items remaining")
((pass_count++))
fi
# Check for sections that still contain only template guidance (not user-written content)
local template_only_list=$(check_template_only_sections)
if [ -n "$template_only_list" ]; then
while IFS= read -r section; do
if [ -n "$section" ]; then
error_items+=("Section '$section' still contains only template guidance - must write your own content")
((error_count++))
fi
done < <(echo "$template_only_list")
else
pass_items+=("All required sections contain user-written content (not template placeholders)")
((pass_count++))
fi
# Check for placeholder text - only flag outside of HTML comments
# Brackets inside <!-- ... --> comments are just guidance templates and are acceptable
local placeholder_count
placeholder_count=$(grep -v "^<!--" "$SPEC_FILE" | grep -cE "FIXME|XXX" 2>/dev/null || echo 0)
if [ "$placeholder_count" -gt 0 ] 2>/dev/null; then
warn_items+=("Contains $placeholder_count placeholder markers (FIXME/XXX)")
((warn_count++))
else
pass_items+=("No placeholder text detected")
((pass_count++))
fi
# Check for empty code blocks
if grep -qE "^\`\`\`\s*$" "$SPEC_FILE"; then
warn_items+=("Contains empty code blocks")
((warn_count++))
fi
# Check for broken links
local broken_links=$(grep -o '\[[^]]*\]([^)]*)' "$SPEC_FILE" 2>/dev/null | grep -c '()$' 2>/dev/null || echo 0)
broken_links=${broken_links:-0}
if [ "$broken_links" -gt 0 ] 2>/dev/null; then
warn_items+=("Found $broken_links potentially broken links")
((warn_count++))
fi
# Output results
local completion=$(calculate_completion)
local status_emoji="✓"
local status_text="PASS"
if [ "$error_count" -gt 0 ]; then
status_emoji="✗"
status_text="INVALID"
elif [ "$warn_count" -gt 0 ]; then
status_emoji="⚠"
status_text="INCOMPLETE"
fi
echo ""
echo -e "${BLUE}📋 SPEC VALIDATION REPORT${NC}"
echo "===================="
echo ""
echo "Spec: $(basename "$SPEC_FILE")"
echo "Type: $spec_type"
echo "Status: $status_emoji $status_text ($completion% complete)"
echo ""
if [ "$pass_count" -gt 0 ]; then
echo -e "${GREEN}✓ PASS ($pass_count items)${NC}"
for item in "${pass_items[@]}"; do
echo " - $item"
done
echo ""
fi
if [ "$warn_count" -gt 0 ]; then
echo -e "${YELLOW}⚠ WARNINGS ($warn_count items)${NC}"
for item in "${warn_items[@]}"; do
echo " - $item"
done
echo ""
fi
if [ "$error_count" -gt 0 ]; then
echo -e "${RED}✗ ERRORS ($error_count items)${NC}"
for item in "${error_items[@]}"; do
echo " - $item"
done
echo ""
fi
# Return appropriate exit code
if [ "$error_count" -gt 0 ]; then
return 1
elif [ "$warn_count" -gt 0 ]; then
return 2
else
return 0
fi
}
# Main execution
validate_spec
exit_code=$?
# Return proper exit codes
case $exit_code in
0) exit 0 ;; # All pass
2) exit 0 ;; # Warnings (still acceptable)
1) exit 1 ;; # Errors
esac