Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions .claude/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,19 @@
"Read(./.DS_Store)",
"Read(./.git/**)"
]
},
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "if echo \"$CLAUDE_TOOL_INPUT\" | jq -r '.command' 2>/dev/null | grep -q '^git push'; then ./.specify/hooks/validate-specs.sh \"$CLAUDE_TOOL_INPUT\"; fi",
"timeout": 30000
}
]
}
]
}
}
5 changes: 4 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,16 @@ mcp-server/node_modules/

# Spec Kit artifacts (created by specify init or StackShift)
.claude/commands/
.specify/
specs/
.stackshift-state.json
.stackshift/
analysis-report.md
docs/reverse-engineering/

# Local overrides (user-specific, not version controlled)
.specify/config/sync-rules.local.json
.specify/memory/

# Downloaded toolkits (session-specific)
.stackshift/
.speckit/
Expand Down
59 changes: 59 additions & 0 deletions .specify/config/sync-rules.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
{
"mode": "strict",
"autoFix": false,
"requireApproval": true,
"fileMappings": {},
"ignorePatterns": [
"**/*.test.ts",
"**/*.test.js",
"**/*.spec.ts",
"**/*.spec.js",
"**/node_modules/**",
"**/__tests__/**",
"**/.git/**",
"**/dist/**",
"**/build/**",
"**/coverage/**"
],
"rules": [
{
"name": "API changes require spec updates",
"id": "api_exports",
"filePattern": "src/**/*.{ts,js}",
"changePattern": "^[+-]\\s*export\\s+(function|class|interface|type|const)",
"requiresSpecUpdate": true,
"specSections": ["API Reference", "User Stories"],
"severity": "error",
"enabled": true,
"priority": 100
},
{
"name": "Feature additions require spec updates",
"id": "feature_additions",
"filePattern": "src/features/**/*",
"changeType": "added",
"requiresSpecUpdate": true,
"specSections": ["User Stories", "Functional Requirements"],
"severity": "error",
"enabled": true,
"priority": 90
},
{
"name": "Internal refactoring allowed",
"id": "internal_refactor",
"filePattern": "src/**/internal/**",
"requiresSpecUpdate": false,
"severity": "info",
"enabled": true,
"priority": 50
}
],
"exemptions": {
"branches": [],
"users": [],
"emergencyOverride": true
},
"timeout": 30000,
"parallel": true,
"maxParallel": 4
}
197 changes: 197 additions & 0 deletions .specify/hooks/modules/categorizer.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,197 @@
#!/usr/bin/env bash

# Change categorizer module
# Categorizes code changes to determine if spec updates are required

# Categorize code changes to determine spec update requirements
categorizer_analyze() {
local file="$1"
local diff="$2"
local config="${3:-{}}"

# Check if test file
if [[ "$file" =~ \.(test|spec)\.(ts|js|tsx|jsx|py|go|rs)$ ]] || [[ "$file" =~ /__tests__/ ]] || [[ "$file" =~ /tests?/ ]]; then
jq -n '{
type: "test_only",
requiresSpecUpdate: false,
confidence: "high",
evidence: {
exportChanges: false,
signatureChanges: false,
newFiles: false,
testFilesOnly: true,
commentsOnly: false
}
}'
return
fi

# Check if documentation file (but not spec.md)
if [[ "$file" =~ \.md$ ]] && [[ ! "$file" =~ spec\.md$ ]]; then
jq -n '{
type: "documentation",
requiresSpecUpdate: false,
confidence: "high",
evidence: {
exportChanges: false,
signatureChanges: false,
newFiles: false,
testFilesOnly: false,
commentsOnly: false
}
}'
return
fi

# Check for export changes (API surface changes)
if echo "$diff" | grep -qE '^[+-]\s*export\s+(function|class|interface|type|const|let|var|default|async|abstract)'; then
jq -n '{
type: "api_change",
requiresSpecUpdate: true,
confidence: "high",
evidence: {
exportChanges: true,
signatureChanges: true,
newFiles: false,
testFilesOnly: false,
commentsOnly: false
},
matchedRule: "Export changes detected"
}'
return
fi

# Check for new file in features/ directory
if [[ "$file" =~ /features/ ]]; then
# Check if this is a new file (added lines but no removed lines in diff header)
if echo "$diff" | head -5 | grep -q "new file mode"; then
jq -n '{
type: "feature_addition",
requiresSpecUpdate: true,
confidence: "medium",
evidence: {
exportChanges: false,
signatureChanges: false,
newFiles: true,
testFilesOnly: false,
commentsOnly: false
},
matchedRule: "New file in features directory"
}'
return
fi

# Existing feature file modified
jq -n '{
type: "feature_modification",
requiresSpecUpdate: true,
confidence: "medium",
evidence: {
exportChanges: false,
signatureChanges: false,
newFiles: false,
testFilesOnly: false,
commentsOnly: false
},
matchedRule: "Feature file modified"
}'
return
fi

# Check if only comments changed
local non_comment_lines=$(echo "$diff" | grep -E '^[+-]' | grep -vE '^\+\s*//' | grep -vE '^\+\s*/\*' | grep -vE '^\+\s*\*' | grep -vE '^\+\s*#' | wc -l | tr -d ' ')
if [ "$non_comment_lines" -eq 0 ]; then
jq -n '{
type: "comments_only",
requiresSpecUpdate: false,
confidence: "medium",
evidence: {
exportChanges: false,
signatureChanges: false,
newFiles: false,
testFilesOnly: false,
commentsOnly: true
}
}'
return
fi

# Check custom rules from config
if [ "$config" != "{}" ]; then
local rules=$(echo "$config" | jq -c '.rules[]?' 2>/dev/null)
while IFS= read -r rule; do
[ -z "$rule" ] && continue

local rule_enabled=$(echo "$rule" | jq -r '.enabled // true')
[ "$rule_enabled" != "true" ] && continue

local file_pattern=$(echo "$rule" | jq -r '.filePattern')
local change_pattern=$(echo "$rule" | jq -r '.changePattern // ""')
local requires_update=$(echo "$rule" | jq -r '.requiresSpecUpdate')
local rule_name=$(echo "$rule" | jq -r '.name')

# Check file pattern match (simplified glob matching)
local pattern_regex="${file_pattern//\*\*/.*}"
pattern_regex="${pattern_regex//\*/[^/]*}"
pattern_regex="^${pattern_regex}$"

if [[ "$file" =~ $pattern_regex ]]; then
# Check change pattern if specified
if [ -n "$change_pattern" ] && [ "$change_pattern" != "null" ]; then
if echo "$diff" | grep -qE "$change_pattern"; then
jq -n \
--arg ruleName "$rule_name" \
--argjson requiresUpdate "$requires_update" \
'{
type: "rule_matched",
requiresSpecUpdate: $requiresUpdate,
confidence: "high",
evidence: {
exportChanges: false,
signatureChanges: false,
newFiles: false,
testFilesOnly: false,
commentsOnly: false
},
matchedRule: $ruleName
}'
return
fi
else
# Pattern matched, no change pattern required
jq -n \
--arg ruleName "$rule_name" \
--argjson requiresUpdate "$requires_update" \
'{
type: "rule_matched",
requiresSpecUpdate: $requiresUpdate,
confidence: "high",
evidence: {
exportChanges: false,
signatureChanges: false,
newFiles: false,
testFilesOnly: false,
commentsOnly: false
},
matchedRule: $ruleName
}'
return
fi
fi
done <<< "$rules"
fi

# Default: internal refactor (no spec update needed)
jq -n '{
type: "internal_refactor",
requiresSpecUpdate: false,
confidence: "medium",
evidence: {
exportChanges: false,
signatureChanges: false,
newFiles: false,
testFilesOnly: false,
commentsOnly: false
}
}'
}
103 changes: 103 additions & 0 deletions .specify/hooks/modules/config.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
#!/usr/bin/env bash

# Configuration loader for spec synchronization
# Loads and merges configuration from multiple sources with precedence

# Load configuration with defaults and overrides
config_load() {
local config_file=".specify/config/sync-rules.json"
local local_config=".specify/config/sync-rules.local.json"

# Default configuration
local default_config='{
"mode": "lenient",
"autoFix": false,
"requireApproval": true,
"ignorePatterns": ["**/*.test.ts", "**/*.spec.ts"],
"rules": [],
"timeout": 30000,
"parallel": true,
"maxParallel": 4
}'

# Start with defaults
local config="$default_config"

# Merge project config if exists
if [ -f "$config_file" ]; then
config=$(jq -s '.[0] * .[1]' <(echo "$default_config") "$config_file" 2>/dev/null || echo "$default_config")
fi

# Merge local config if exists
if [ -f "$local_config" ]; then
config=$(jq -s '.[0] * .[1]' <(echo "$config") "$local_config" 2>/dev/null || echo "$config")
fi

# Apply environment variable overrides
if [ -n "$SPEC_SYNC_MODE" ]; then
config=$(echo "$config" | jq --arg mode "$SPEC_SYNC_MODE" '.mode = $mode')
fi

if [ -n "$SPEC_SYNC_AUTO_FIX" ]; then
local auto_fix="false"
[[ "$SPEC_SYNC_AUTO_FIX" == "true" || "$SPEC_SYNC_AUTO_FIX" == "1" ]] && auto_fix="true"
config=$(echo "$config" | jq --argjson autoFix "$auto_fix" '.autoFix = $autoFix')
fi

echo "$config"
}

# Get current validation mode (strict, lenient, off)
config_get_mode() {
local config=$(config_load)
echo "$config" | jq -r '.mode // "lenient"'
}

# Check if a file should be ignored based on ignore patterns
config_should_ignore() {
local file="$1"
local config=$(config_load)
local patterns=$(echo "$config" | jq -r '.ignorePatterns[]?' 2>/dev/null)

if [ -z "$patterns" ]; then
return 1 # Should not ignore (no patterns)
fi

while IFS= read -r pattern; do
# Simple glob matching (simplified - production would use more robust matching)
# Convert ** to .* and * to [^/]*
local regex_pattern="${pattern//\*\*/.*}"
regex_pattern="${regex_pattern//\*/[^/]*}"
regex_pattern="^${regex_pattern}$"

if [[ "$file" =~ $regex_pattern ]]; then
return 0 # Should ignore
fi
done <<< "$patterns"

return 1 # Should not ignore
}

# Get all validation rules from configuration
config_get_rules() {
local config=$(config_load)
echo "$config" | jq -c '.rules[]?' 2>/dev/null
}

# Check if a file matches a rule's file pattern
config_matches_pattern() {
local file="$1"
local pattern="$2"

# Convert glob pattern to regex
# Handle {ts,js} syntax
pattern="${pattern//\{/\(}"
pattern="${pattern//\}/\)}"
pattern="${pattern//,/\|}"
# Handle ** and *
pattern="${pattern//\*\*/.*}"
pattern="${pattern//\*/[^/]*}"
pattern="^${pattern}$"

[[ "$file" =~ $pattern ]]
}
Loading
Loading