Files
wehub-resource-sync bb5c75ce05
Component Security Validation / Security Audit (push) Has been cancelled
Deploy to Cloudflare Pages / deploy (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:38:58 +08:00

92 lines
2.9 KiB
Bash

#!/bin/bash
# scope-guard.sh — Detect files modified outside the scope of an active spec
# Source: pm-workspace (https://github.com/gonzalezpazmonica/pm-workspace)
# License: MIT
#
# Compares git-modified files against files declared in the active .spec.md.
# Warns about out-of-scope modifications but does NOT block (exit 0).
# Essential for preventing scope creep during Spec-Driven Development.
INPUT=$(cat)
PROJECT_ROOT=$(git rev-parse --show-toplevel 2>/dev/null || echo ".")
# Get modified files (tracked unstaged + staged), handle safely
MODIFIED=$({ git diff --name-only 2>/dev/null; git diff --cached --name-only 2>/dev/null; } | sort -u)
if [ -z "$MODIFIED" ]; then
exit 0
fi
# Find active spec: most recently modified .spec.md (within last 60 minutes)
# Sort by modification time (-t) and take the newest one
SPEC_FILE=$(find "$PROJECT_ROOT" -name "*.spec.md" -mmin -60 -type f -printf '%T@ %p\n' 2>/dev/null \
| sort -rn | head -1 | cut -d' ' -f2-)
# Fallback for systems without -printf (macOS)
if [ -z "$SPEC_FILE" ]; then
SPEC_FILE=$(find "$PROJECT_ROOT" -name "*.spec.md" -mmin -60 -type f -print0 2>/dev/null \
| xargs -0 ls -t 2>/dev/null | head -1)
fi
# If no active spec, cannot verify scope
if [ -z "$SPEC_FILE" ] || [ ! -f "$SPEC_FILE" ]; then
exit 0
fi
# Extract declared files from spec using broader regex
# Matches paths like: src/Application/Orders/Handler.cs, tests/unit/test_foo.py
DECLARED=$(sed -n '/[Ff]icheros\|[Ff]iles to [Cc]reate\|[Ff]iles to [Mm]odify/,/^## /p' "$SPEC_FILE" \
| grep -oE '[a-zA-Z0-9_./@\-]+\.[a-zA-Z0-9]{1,8}' \
| sort -u)
if [ -z "$DECLARED" ]; then
exit 0
fi
# Compare: find modified files NOT in the declared list
# Use while-read to handle filenames with spaces safely
OUT_OF_SCOPE=""
while IFS= read -r FILE; do
[ -z "$FILE" ] && continue
BASENAME=$(basename "$FILE")
MATCH=0
while IFS= read -r DECL; do
[ -z "$DECL" ] && continue
# Exact path match
if [ "$FILE" = "$DECL" ]; then
MATCH=1
break
fi
# Check if declared path is a suffix of the modified file path
case "$FILE" in
*/"$DECL") MATCH=1; break ;;
esac
done <<< "$DECLARED"
if [ "$MATCH" -eq 0 ]; then
# Exclude files that are always legitimate outside scope
case "$BASENAME" in
*.spec.md|*.test.*|*Test.*|*Tests.*|*_test.*|test_*) continue ;;
*.md|*.json|*.yml|*.yaml|*.lock) continue ;;
.gitignore|Dockerfile|docker-compose*|*.csproj|*.sln|package.json) continue ;;
esac
case "$FILE" in
*/test/*|*/tests/*|*/Test/*|*/Tests/*|*/__tests__/*) continue ;;
esac
OUT_OF_SCOPE="${OUT_OF_SCOPE} - ${FILE}\n"
fi
done <<< "$MODIFIED"
if [ -n "$OUT_OF_SCOPE" ]; then
echo "SCOPE GUARD: Files modified OUTSIDE the scope of active spec ($(basename "$SPEC_FILE")):" >&2
printf "%b" "$OUT_OF_SCOPE" >&2
echo "" >&2
echo "Review if these changes are intentional or if the agent expanded the scope." >&2
# Warning only, does not block
exit 0
fi
exit 0