386 lines
13 KiB
Bash
Executable File
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
|