specpulse 1.0.4__py3-none-any.whl → 1.0.5__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- specpulse/__init__.py +1 -1
- specpulse/resources/commands/claude/plan.md +156 -45
- specpulse/resources/commands/claude/pulse.md +63 -24
- specpulse/resources/commands/claude/spec.md +110 -31
- specpulse/resources/commands/claude/task.md +202 -43
- specpulse/resources/commands/gemini/plan.toml +13 -5
- specpulse/resources/commands/gemini/pulse.toml +4 -1
- specpulse/resources/commands/gemini/spec.toml +12 -4
- specpulse/resources/commands/gemini/task.toml +17 -5
- specpulse/resources/memory/constitution.md +2 -2
- specpulse/resources/scripts/pulse-init.ps1 +186 -0
- specpulse/resources/scripts/pulse-init.py +171 -0
- specpulse/resources/scripts/pulse-init.sh +80 -21
- specpulse/resources/scripts/pulse-plan.ps1 +251 -0
- specpulse/resources/scripts/pulse-plan.py +191 -0
- specpulse/resources/scripts/pulse-plan.sh +113 -12
- specpulse/resources/scripts/pulse-spec.ps1 +185 -0
- specpulse/resources/scripts/pulse-spec.py +167 -0
- specpulse/resources/scripts/pulse-spec.sh +86 -11
- specpulse/resources/scripts/pulse-task.ps1 +263 -0
- specpulse/resources/scripts/pulse-task.py +237 -0
- specpulse/resources/scripts/pulse-task.sh +123 -9
- specpulse/resources/templates/plan.md +142 -287
- specpulse/resources/templates/spec.md +80 -246
- specpulse/resources/templates/task.md +114 -93
- {specpulse-1.0.4.dist-info → specpulse-1.0.5.dist-info}/METADATA +29 -10
- specpulse-1.0.5.dist-info/RECORD +41 -0
- specpulse-1.0.4.dist-info/RECORD +0 -33
- {specpulse-1.0.4.dist-info → specpulse-1.0.5.dist-info}/WHEEL +0 -0
- {specpulse-1.0.4.dist-info → specpulse-1.0.5.dist-info}/entry_points.txt +0 -0
- {specpulse-1.0.4.dist-info → specpulse-1.0.5.dist-info}/licenses/LICENSE +0 -0
- {specpulse-1.0.4.dist-info → specpulse-1.0.5.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,191 @@
|
|
1
|
+
#!/usr/bin/env python3
|
2
|
+
"""
|
3
|
+
SpecPulse Plan Generation Script
|
4
|
+
Cross-platform Python equivalent of pulse-plan.sh
|
5
|
+
"""
|
6
|
+
|
7
|
+
import os
|
8
|
+
import sys
|
9
|
+
import re
|
10
|
+
from pathlib import Path
|
11
|
+
import datetime
|
12
|
+
|
13
|
+
class SpecPulsePlan:
|
14
|
+
def __init__(self):
|
15
|
+
self.script_name = Path(__file__).name
|
16
|
+
self.project_root = Path(__file__).parent.parent.parent
|
17
|
+
self.memory_dir = self.project_root / "memory"
|
18
|
+
self.context_file = self.memory_dir / "context.md"
|
19
|
+
self.templates_dir = self.project_root / "resources" / "templates"
|
20
|
+
|
21
|
+
def log(self, message):
|
22
|
+
"""Log messages with timestamp"""
|
23
|
+
timestamp = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
24
|
+
try:
|
25
|
+
print(f"[{timestamp}] {self.script_name}: {message}", file=sys.stderr)
|
26
|
+
except UnicodeEncodeError:
|
27
|
+
# Fallback for Windows console encoding issues
|
28
|
+
message = message.encode('cp1252', errors='replace').decode('cp1252')
|
29
|
+
print(f"[{timestamp}] {self.script_name}: {message}", file=sys.stderr)
|
30
|
+
|
31
|
+
def error_exit(self, message):
|
32
|
+
"""Exit with error message"""
|
33
|
+
self.log(f"ERROR: {message}")
|
34
|
+
sys.exit(1)
|
35
|
+
|
36
|
+
def sanitize_feature_dir(self, feature_dir):
|
37
|
+
"""Sanitize feature directory name"""
|
38
|
+
if not feature_dir:
|
39
|
+
self.error_exit("Feature directory cannot be empty")
|
40
|
+
|
41
|
+
sanitized = re.sub(r'[^a-zA-Z0-9_-]', '', feature_dir)
|
42
|
+
|
43
|
+
if not sanitized:
|
44
|
+
self.error_exit(f"Invalid feature directory: '{feature_dir}'")
|
45
|
+
|
46
|
+
return sanitized
|
47
|
+
|
48
|
+
def find_feature_directory(self):
|
49
|
+
"""Find feature directory from context file"""
|
50
|
+
if not self.context_file.exists():
|
51
|
+
self.error_exit("No context file found and no feature directory provided")
|
52
|
+
|
53
|
+
try:
|
54
|
+
content = self.context_file.read_text(encoding='utf-8')
|
55
|
+
match = re.search(r'Active Feature:\s*(.+)', content)
|
56
|
+
if match:
|
57
|
+
feature_dir = match.group(1).strip()
|
58
|
+
self.log(f"Using active feature from context: {feature_dir}")
|
59
|
+
return feature_dir
|
60
|
+
else:
|
61
|
+
self.error_exit("No active feature found in context file")
|
62
|
+
except Exception as e:
|
63
|
+
self.error_exit(f"Failed to read context file: {e}")
|
64
|
+
|
65
|
+
def get_current_feature_dir(self, provided_dir):
|
66
|
+
"""Get current feature directory"""
|
67
|
+
if provided_dir:
|
68
|
+
return self.sanitize_feature_dir(provided_dir)
|
69
|
+
else:
|
70
|
+
return self.find_feature_directory()
|
71
|
+
|
72
|
+
def validate_plan_structure(self, plan_file):
|
73
|
+
"""Validate plan structure"""
|
74
|
+
required_sections = [
|
75
|
+
"## Implementation Plan:",
|
76
|
+
"## Specification Reference",
|
77
|
+
"## Phase -1: Pre-Implementation Gates",
|
78
|
+
"## Implementation Phases"
|
79
|
+
]
|
80
|
+
|
81
|
+
missing_sections = []
|
82
|
+
content = plan_file.read_text(encoding='utf-8')
|
83
|
+
|
84
|
+
for section in required_sections:
|
85
|
+
if section not in content:
|
86
|
+
missing_sections.append(section)
|
87
|
+
|
88
|
+
if missing_sections:
|
89
|
+
self.log(f"WARNING: Missing required sections: {', '.join(missing_sections)}")
|
90
|
+
|
91
|
+
return len(missing_sections)
|
92
|
+
|
93
|
+
def check_constitutional_gates(self, plan_file):
|
94
|
+
"""Check constitutional gates"""
|
95
|
+
constitutional_gates = [
|
96
|
+
"Simplicity Gate",
|
97
|
+
"Anti-Abstraction Gate",
|
98
|
+
"Test-First Gate",
|
99
|
+
"Integration-First Gate",
|
100
|
+
"Research Gate"
|
101
|
+
]
|
102
|
+
|
103
|
+
missing_gates = []
|
104
|
+
content = plan_file.read_text(encoding='utf-8')
|
105
|
+
|
106
|
+
for gate in constitutional_gates:
|
107
|
+
if gate not in content:
|
108
|
+
missing_gates.append(gate)
|
109
|
+
|
110
|
+
if missing_gates:
|
111
|
+
self.log(f"WARNING: Missing constitutional gates: {', '.join(missing_gates)}")
|
112
|
+
|
113
|
+
# Check gate status
|
114
|
+
gate_status_match = re.search(r'Gate Status:\s*\[([^\]]+)\]', content)
|
115
|
+
if gate_status_match:
|
116
|
+
gate_status = gate_status_match.group(1)
|
117
|
+
if gate_status.upper() != "COMPLETED":
|
118
|
+
self.log(f"WARNING: Constitutional gates not completed. Status: {gate_status}")
|
119
|
+
return gate_status
|
120
|
+
else:
|
121
|
+
self.log("WARNING: Constitutional gates status not found")
|
122
|
+
return "UNKNOWN"
|
123
|
+
|
124
|
+
return "COMPLETED"
|
125
|
+
|
126
|
+
def main(self, args):
|
127
|
+
"""Main execution function"""
|
128
|
+
if len(args) < 1:
|
129
|
+
self.error_exit("Usage: python pulse-plan.py <feature-dir>")
|
130
|
+
|
131
|
+
feature_dir = args[0]
|
132
|
+
|
133
|
+
self.log("Processing implementation plan...")
|
134
|
+
|
135
|
+
# Get and sanitize feature directory
|
136
|
+
sanitized_dir = self.get_current_feature_dir(feature_dir)
|
137
|
+
|
138
|
+
# Set file paths
|
139
|
+
plan_file = self.project_root / "plans" / sanitized_dir / "plan.md"
|
140
|
+
template_file = self.templates_dir / "plan.md"
|
141
|
+
spec_file = self.project_root / "specs" / sanitized_dir / "spec.md"
|
142
|
+
|
143
|
+
# Ensure plans directory exists
|
144
|
+
plan_file.parent.mkdir(parents=True, exist_ok=True)
|
145
|
+
|
146
|
+
# Check if specification exists first
|
147
|
+
if not spec_file.exists():
|
148
|
+
self.error_exit(f"Specification file not found: {spec_file}. Please create specification first.")
|
149
|
+
|
150
|
+
# Ensure plan template exists
|
151
|
+
if not template_file.exists():
|
152
|
+
self.error_exit(f"Template not found: {template_file}")
|
153
|
+
|
154
|
+
# Create plan if it doesn't exist
|
155
|
+
if not plan_file.exists():
|
156
|
+
self.log(f"Creating implementation plan from template: {plan_file}")
|
157
|
+
try:
|
158
|
+
plan_file.write_text(template_file.read_text(encoding='utf-8'))
|
159
|
+
except Exception as e:
|
160
|
+
self.error_exit(f"Failed to copy plan template: {e}")
|
161
|
+
else:
|
162
|
+
self.log(f"Implementation plan already exists: {plan_file}")
|
163
|
+
|
164
|
+
# Validate plan structure
|
165
|
+
self.log("Validating implementation plan...")
|
166
|
+
missing_sections = self.validate_plan_structure(plan_file)
|
167
|
+
|
168
|
+
# Check Constitutional Gates
|
169
|
+
self.log("Checking Constitutional Gates...")
|
170
|
+
gate_status = self.check_constitutional_gates(plan_file)
|
171
|
+
|
172
|
+
# Check if specification has clarifications needed
|
173
|
+
if spec_file.exists():
|
174
|
+
spec_content = spec_file.read_text(encoding='utf-8')
|
175
|
+
clarification_matches = re.findall(r'NEEDS CLARIFICATION', spec_content)
|
176
|
+
if clarification_matches:
|
177
|
+
clarification_count = len(clarification_matches)
|
178
|
+
self.log(f"WARNING: Specification has {clarification_count} clarifications needed - resolve before proceeding")
|
179
|
+
|
180
|
+
self.log("Implementation plan processing completed successfully")
|
181
|
+
|
182
|
+
# Output results
|
183
|
+
print(f"PLAN_FILE={plan_file}")
|
184
|
+
print(f"SPEC_FILE={spec_file}")
|
185
|
+
print(f"MISSING_SECTIONS={missing_sections}")
|
186
|
+
print(f"CONSTITUTIONAL_GATES_STATUS={gate_status}")
|
187
|
+
print("STATUS=ready")
|
188
|
+
|
189
|
+
if __name__ == "__main__":
|
190
|
+
plan = SpecPulsePlan()
|
191
|
+
plan.main(sys.argv[1:])
|
@@ -1,26 +1,127 @@
|
|
1
1
|
#!/bin/bash
|
2
2
|
# Generate implementation plan
|
3
3
|
|
4
|
-
|
4
|
+
set -euo pipefail # Exit on error, unset vars, pipe failures
|
5
5
|
|
6
|
+
# Configuration
|
7
|
+
SCRIPT_NAME="$(basename "$0")"
|
8
|
+
PROJECT_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
9
|
+
|
10
|
+
# Function to log messages
|
11
|
+
log() {
|
12
|
+
echo "[$(date '+%Y-%m-%d %H:%M:%S')] $SCRIPT_NAME: $1" >&2
|
13
|
+
}
|
14
|
+
|
15
|
+
# Function to handle errors
|
16
|
+
error_exit() {
|
17
|
+
log "ERROR: $1"
|
18
|
+
exit 1
|
19
|
+
}
|
20
|
+
|
21
|
+
# Validate arguments
|
22
|
+
if [ $# -eq 0 ]; then
|
23
|
+
error_exit "Usage: $SCRIPT_NAME <feature-dir>"
|
24
|
+
fi
|
25
|
+
|
26
|
+
FEATURE_DIR="$1"
|
27
|
+
|
28
|
+
# Sanitize feature directory
|
29
|
+
SANITIZED_DIR=$(echo "$FEATURE_DIR" | sed 's/[^a-zA-Z0-9_-]//g')
|
30
|
+
|
31
|
+
if [ -z "$SANITIZED_DIR" ]; then
|
32
|
+
error_exit "Invalid feature directory: '$FEATURE_DIR'"
|
33
|
+
fi
|
34
|
+
|
35
|
+
# Find feature directory if not provided
|
6
36
|
if [ -z "$FEATURE_DIR" ]; then
|
7
|
-
|
8
|
-
|
37
|
+
CONTEXT_FILE="$PROJECT_ROOT/memory/context.md"
|
38
|
+
if [ -f "$CONTEXT_FILE" ]; then
|
39
|
+
FEATURE_DIR=$(grep -A1 "Active Feature" "$CONTEXT_FILE" | tail -1 | cut -d: -f2 | xargs)
|
40
|
+
if [ -z "$FEATURE_DIR" ]; then
|
41
|
+
error_exit "No active feature found in context file"
|
42
|
+
fi
|
43
|
+
log "Using active feature from context: $FEATURE_DIR"
|
44
|
+
else
|
45
|
+
error_exit "No feature directory provided and no context file found"
|
46
|
+
fi
|
9
47
|
fi
|
10
48
|
|
11
|
-
PLAN_FILE="plans/${FEATURE_DIR}/plan.md"
|
49
|
+
PLAN_FILE="$PROJECT_ROOT/plans/${FEATURE_DIR}/plan.md"
|
50
|
+
TEMPLATE_FILE="$PROJECT_ROOT/templates/plan.md"
|
51
|
+
SPEC_FILE="$PROJECT_ROOT/specs/${FEATURE_DIR}/spec.md"
|
52
|
+
|
53
|
+
# Ensure plans directory exists
|
54
|
+
mkdir -p "$(dirname "$PLAN_FILE")"
|
55
|
+
|
56
|
+
# Check if specification exists first
|
57
|
+
if [ ! -f "$SPEC_FILE" ]; then
|
58
|
+
error_exit "Specification file not found: $SPEC_FILE. Please create specification first."
|
59
|
+
fi
|
60
|
+
|
61
|
+
# Ensure plan template exists
|
62
|
+
if [ ! -f "$TEMPLATE_FILE" ]; then
|
63
|
+
error_exit "Template not found: $TEMPLATE_FILE"
|
64
|
+
fi
|
12
65
|
|
13
|
-
#
|
66
|
+
# Create plan if it doesn't exist
|
14
67
|
if [ ! -f "$PLAN_FILE" ]; then
|
15
|
-
|
68
|
+
log "Creating implementation plan from template: $PLAN_FILE"
|
69
|
+
cp "$TEMPLATE_FILE" "$PLAN_FILE" || error_exit "Failed to copy plan template"
|
70
|
+
else
|
71
|
+
log "Implementation plan already exists: $PLAN_FILE"
|
72
|
+
fi
|
73
|
+
|
74
|
+
# Validate plan structure
|
75
|
+
log "Validating implementation plan..."
|
76
|
+
|
77
|
+
# Check for required sections
|
78
|
+
REQUIRED_SECTIONS=("## Implementation Plan:" "## Specification Reference" "## Phase -1: Pre-Implementation Gates" "## Implementation Phases")
|
79
|
+
MISSING_SECTIONS=()
|
80
|
+
|
81
|
+
for section in "${REQUIRED_SECTIONS[@]}"; do
|
82
|
+
if ! grep -q "$section" "$PLAN_FILE"; then
|
83
|
+
MISSING_SECTIONS+=("$section")
|
84
|
+
fi
|
85
|
+
done
|
86
|
+
|
87
|
+
if [ ${#MISSING_SECTIONS[@]} -gt 0 ]; then
|
88
|
+
log "WARNING: Missing required sections: ${MISSING_SECTIONS[*]}"
|
89
|
+
fi
|
90
|
+
|
91
|
+
# Check Constitutional Gates
|
92
|
+
log "Checking Constitutional Gates..."
|
93
|
+
|
94
|
+
CONSTITUTIONAL_GATES=(
|
95
|
+
"Simplicity Gate"
|
96
|
+
"Anti-Abstraction Gate"
|
97
|
+
"Test-First Gate"
|
98
|
+
"Integration-First Gate"
|
99
|
+
"Research Gate"
|
100
|
+
)
|
101
|
+
|
102
|
+
for gate in "${CONSTITUTIONAL_GATES[@]}"; do
|
103
|
+
if ! grep -q "$gate" "$PLAN_FILE"; then
|
104
|
+
log "WARNING: Missing constitutional gate: $gate"
|
105
|
+
fi
|
106
|
+
done
|
107
|
+
|
108
|
+
# Check if specification has clarifications needed
|
109
|
+
if grep -q "NEEDS CLARIFICATION" "$SPEC_FILE"; then
|
110
|
+
CLARIFICATION_COUNT=$(grep -c "NEEDS CLARIFICATION" "$SPEC_FILE")
|
111
|
+
log "WARNING: Specification has $CLARIFICATION_COUNT clarifications needed - resolve before proceeding"
|
112
|
+
fi
|
113
|
+
|
114
|
+
# Validate gate compliance
|
115
|
+
GATE_STATUS=$(grep -A5 "Gate Status:" "$PLAN_FILE" | tail -1 | sed 's/.*\[\(.*\)\].*/\1/' || echo "PENDING")
|
116
|
+
|
117
|
+
if [ "$GATE_STATUS" != "COMPLETED" ]; then
|
118
|
+
log "WARNING: Constitutional gates not completed. Status: $GATE_STATUS"
|
16
119
|
fi
|
17
120
|
|
18
|
-
|
19
|
-
echo "Checking Phase Gates..."
|
20
|
-
echo "- Constitutional compliance"
|
21
|
-
echo "- Simplicity check (≤3 modules)"
|
22
|
-
echo "- Test-first strategy"
|
23
|
-
echo "- Framework selection"
|
121
|
+
log "Implementation plan processing completed successfully"
|
24
122
|
|
25
123
|
echo "PLAN_FILE=$PLAN_FILE"
|
124
|
+
echo "SPEC_FILE=$SPEC_FILE"
|
125
|
+
echo "MISSING_SECTIONS=${#MISSING_SECTIONS[@]}"
|
126
|
+
echo "CONSTITUTIONAL_GATES_STATUS=$GATE_STATUS"
|
26
127
|
echo "STATUS=ready"
|
@@ -0,0 +1,185 @@
|
|
1
|
+
# SpecPulse Specification Generation Script
|
2
|
+
# Cross-platform PowerShell equivalent of pulse-spec.sh
|
3
|
+
|
4
|
+
param(
|
5
|
+
[Parameter(Mandatory=$true)]
|
6
|
+
[string]$FeatureDir,
|
7
|
+
|
8
|
+
[string]$SpecContent = ""
|
9
|
+
)
|
10
|
+
|
11
|
+
$ErrorActionPreference = "Stop"
|
12
|
+
$ProgressPreference = "SilentlyContinue"
|
13
|
+
|
14
|
+
# Configuration
|
15
|
+
$ScriptName = $MyInvocation.MyCommand.Name
|
16
|
+
$ProjectRoot = $PSScriptRoot | Split-Path -Parent | Split-Path -Parent
|
17
|
+
$MemoryDir = Join-Path $ProjectRoot "memory"
|
18
|
+
$ContextFile = Join-Path $MemoryDir "context.md"
|
19
|
+
$TemplatesDir = Join-Path $ProjectRoot "resources" "templates"
|
20
|
+
|
21
|
+
function Write-Log {
|
22
|
+
param([string]$Message)
|
23
|
+
$Timestamp = Get-Date -Format "yyyy-MM-dd HH:mm:ss"
|
24
|
+
Write-Host "[$Timestamp] $ScriptName : $Message" -ForegroundColor Yellow
|
25
|
+
}
|
26
|
+
|
27
|
+
function Exit-WithError {
|
28
|
+
param([string]$Message)
|
29
|
+
Write-Log "ERROR: $Message"
|
30
|
+
exit 1
|
31
|
+
}
|
32
|
+
|
33
|
+
function Sanitize-FeatureDir {
|
34
|
+
param([string]$Dir)
|
35
|
+
|
36
|
+
if ([string]::IsNullOrWhiteSpace($Dir)) {
|
37
|
+
Exit-WithError "Feature directory cannot be empty"
|
38
|
+
}
|
39
|
+
|
40
|
+
# Remove non-alphanumeric characters except hyphens and underscores
|
41
|
+
$Sanitized = $Dir -replace '[^a-zA-Z0-9_-]', ''
|
42
|
+
|
43
|
+
if ([string]::IsNullOrWhiteSpace($Sanitized)) {
|
44
|
+
Exit-WithError "Invalid feature directory: '$Dir'"
|
45
|
+
}
|
46
|
+
|
47
|
+
return $Sanitized
|
48
|
+
}
|
49
|
+
|
50
|
+
function Find-FeatureDirectory {
|
51
|
+
if (-not (Test-Path $ContextFile)) {
|
52
|
+
Exit-WithError "No context file found and no feature directory provided"
|
53
|
+
}
|
54
|
+
|
55
|
+
try {
|
56
|
+
$Content = Get-Content $ContextFile -Raw -Encoding UTF8
|
57
|
+
# Look for "Active Feature" section
|
58
|
+
if ($Content -match 'Active Feature:\s*(.+)') {
|
59
|
+
$FeatureDir = $matches[1].Trim()
|
60
|
+
Write-Log "Using active feature from context: $FeatureDir"
|
61
|
+
return $FeatureDir
|
62
|
+
} else {
|
63
|
+
Exit-WithError "No active feature found in context file"
|
64
|
+
}
|
65
|
+
} catch {
|
66
|
+
Exit-WithError "Failed to read context file: $_"
|
67
|
+
}
|
68
|
+
}
|
69
|
+
|
70
|
+
function Get-CurrentFeatureDir {
|
71
|
+
param([string]$ProvidedDir)
|
72
|
+
|
73
|
+
if ($ProvidedDir) {
|
74
|
+
return Sanitize-FeatureDir -Dir $ProvidedDir
|
75
|
+
} else {
|
76
|
+
return Find-FeatureDirectory
|
77
|
+
}
|
78
|
+
}
|
79
|
+
|
80
|
+
function Validate-Specification {
|
81
|
+
param([string]$SpecFile)
|
82
|
+
|
83
|
+
if (-not (Test-Path $SpecFile)) {
|
84
|
+
Exit-WithError "Specification file does not exist: $SpecFile"
|
85
|
+
}
|
86
|
+
|
87
|
+
# Required sections
|
88
|
+
$RequiredSections = @(
|
89
|
+
"## Specification:",
|
90
|
+
"## Metadata",
|
91
|
+
"## Functional Requirements",
|
92
|
+
"## Acceptance Scenarios"
|
93
|
+
)
|
94
|
+
|
95
|
+
$Content = Get-Content $SpecFile -Raw -Encoding UTF8
|
96
|
+
$MissingSections = @()
|
97
|
+
|
98
|
+
foreach ($Section in $RequiredSections) {
|
99
|
+
if ($Content -notmatch [regex]::Escape($Section)) {
|
100
|
+
$MissingSections += $Section
|
101
|
+
}
|
102
|
+
}
|
103
|
+
|
104
|
+
if ($MissingSections.Count -gt 0) {
|
105
|
+
Write-Log "WARNING: Missing required sections: $($MissingSections -join ', ')"
|
106
|
+
}
|
107
|
+
|
108
|
+
# Check for clarifications needed
|
109
|
+
$ClarificationMatches = [regex]::Matches($Content, 'NEEDS CLARIFICATION')
|
110
|
+
if ($ClarificationMatches.Count -gt 0) {
|
111
|
+
$ClarificationCount = $ClarificationMatches.Count
|
112
|
+
Write-Log "WARNING: Specification has $ClarificationCount clarifications needed"
|
113
|
+
return $ClarificationCount
|
114
|
+
}
|
115
|
+
|
116
|
+
return 0
|
117
|
+
}
|
118
|
+
|
119
|
+
# Main execution
|
120
|
+
Write-Log "Processing specification..."
|
121
|
+
|
122
|
+
# Get and sanitize feature directory
|
123
|
+
$SanitizedDir = Get-CurrentFeatureDir -ProvidedDir $FeatureDir
|
124
|
+
|
125
|
+
# Set file paths
|
126
|
+
$SpecFile = Join-Path $ProjectRoot "specs" $SanitizedDir "spec.md"
|
127
|
+
$TemplateFile = Join-Path $TemplatesDir "spec.md"
|
128
|
+
|
129
|
+
# Ensure specs directory exists
|
130
|
+
$SpecDir = Split-Path $SpecFile -Parent
|
131
|
+
New-Item -ItemType Directory -Path $SpecDir -Force | Out-Null
|
132
|
+
|
133
|
+
# Handle specification content
|
134
|
+
if ($SpecContent) {
|
135
|
+
Write-Log "Updating specification: $SpecFile"
|
136
|
+
try {
|
137
|
+
Set-Content -Path $SpecFile -Value $SpecContent -Encoding UTF8
|
138
|
+
} catch {
|
139
|
+
Exit-WithError "Failed to write specification content: $_"
|
140
|
+
}
|
141
|
+
} else {
|
142
|
+
# Ensure specification exists from template
|
143
|
+
if (-not (Test-Path $SpecFile)) {
|
144
|
+
if (-not (Test-Path $TemplateFile)) {
|
145
|
+
Exit-WithError "Template not found: $TemplateFile"
|
146
|
+
}
|
147
|
+
|
148
|
+
Write-Log "Creating specification from template: $SpecFile"
|
149
|
+
try {
|
150
|
+
Copy-Item $TemplateFile $SpecFile -Force
|
151
|
+
} catch {
|
152
|
+
Exit-WithError "Failed to copy specification template: $_"
|
153
|
+
}
|
154
|
+
} else {
|
155
|
+
Write-Log "Specification already exists: $SpecFile"
|
156
|
+
}
|
157
|
+
}
|
158
|
+
|
159
|
+
# Validate specification
|
160
|
+
Write-Log "Validating specification..."
|
161
|
+
$ClarificationCount = Validate-Specification -SpecFile $SpecFile
|
162
|
+
|
163
|
+
# Check for missing sections
|
164
|
+
$Content = Get-Content $SpecFile -Raw -Encoding UTF8
|
165
|
+
$RequiredSections = @(
|
166
|
+
"## Specification:",
|
167
|
+
"## Metadata",
|
168
|
+
"## Functional Requirements",
|
169
|
+
"## Acceptance Scenarios"
|
170
|
+
)
|
171
|
+
|
172
|
+
$MissingSections = @()
|
173
|
+
foreach ($Section in $RequiredSections) {
|
174
|
+
if ($Content -notmatch [regex]::Escape($Section)) {
|
175
|
+
$MissingSections += $Section
|
176
|
+
}
|
177
|
+
}
|
178
|
+
|
179
|
+
Write-Log "Specification processing completed successfully"
|
180
|
+
|
181
|
+
# Output results
|
182
|
+
Write-Host "SPEC_FILE=$SpecFile"
|
183
|
+
Write-Host "CLARIFICATIONS_NEEDED=$ClarificationCount"
|
184
|
+
Write-Host "MISSING_SECTIONS=$($MissingSections.Count)"
|
185
|
+
Write-Host "STATUS=updated"
|
@@ -0,0 +1,167 @@
|
|
1
|
+
#!/usr/bin/env python3
|
2
|
+
"""
|
3
|
+
SpecPulse Specification Generation Script
|
4
|
+
Cross-platform Python equivalent of pulse-spec.sh
|
5
|
+
"""
|
6
|
+
|
7
|
+
import os
|
8
|
+
import sys
|
9
|
+
import re
|
10
|
+
from pathlib import Path
|
11
|
+
import datetime
|
12
|
+
|
13
|
+
class SpecPulseSpec:
|
14
|
+
def __init__(self):
|
15
|
+
self.script_name = Path(__file__).name
|
16
|
+
self.project_root = Path(__file__).parent.parent.parent
|
17
|
+
self.memory_dir = self.project_root / "memory"
|
18
|
+
self.context_file = self.memory_dir / "context.md"
|
19
|
+
self.templates_dir = self.project_root / "resources" / "templates"
|
20
|
+
|
21
|
+
def log(self, message):
|
22
|
+
"""Log messages with timestamp"""
|
23
|
+
timestamp = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
24
|
+
print(f"[{timestamp}] {self.script_name}: {message}", file=sys.stderr)
|
25
|
+
|
26
|
+
def error_exit(self, message):
|
27
|
+
"""Exit with error message"""
|
28
|
+
self.log(f"ERROR: {message}")
|
29
|
+
sys.exit(1)
|
30
|
+
|
31
|
+
def sanitize_feature_dir(self, feature_dir):
|
32
|
+
"""Sanitize feature directory name"""
|
33
|
+
if not feature_dir:
|
34
|
+
self.error_exit("Feature directory cannot be empty")
|
35
|
+
|
36
|
+
# Remove non-alphanumeric characters except hyphens and underscores
|
37
|
+
sanitized = re.sub(r'[^a-zA-Z0-9_-]', '', feature_dir)
|
38
|
+
|
39
|
+
if not sanitized:
|
40
|
+
self.error_exit(f"Invalid feature directory: '{feature_dir}'")
|
41
|
+
|
42
|
+
return sanitized
|
43
|
+
|
44
|
+
def find_feature_directory(self):
|
45
|
+
"""Find feature directory from context file"""
|
46
|
+
if not self.context_file.exists():
|
47
|
+
self.error_exit("No context file found and no feature directory provided")
|
48
|
+
|
49
|
+
try:
|
50
|
+
content = self.context_file.read_text(encoding='utf-8')
|
51
|
+
# Look for "Active Feature" section
|
52
|
+
match = re.search(r'Active Feature:\s*(.+)', content)
|
53
|
+
if match:
|
54
|
+
feature_dir = match.group(1).strip()
|
55
|
+
self.log(f"Using active feature from context: {feature_dir}")
|
56
|
+
return feature_dir
|
57
|
+
else:
|
58
|
+
self.error_exit("No active feature found in context file")
|
59
|
+
except Exception as e:
|
60
|
+
self.error_exit(f"Failed to read context file: {e}")
|
61
|
+
|
62
|
+
def get_current_feature_dir(self, provided_dir):
|
63
|
+
"""Get current feature directory"""
|
64
|
+
if provided_dir:
|
65
|
+
return self.sanitize_feature_dir(provided_dir)
|
66
|
+
else:
|
67
|
+
return self.find_feature_directory()
|
68
|
+
|
69
|
+
def validate_specification(self, spec_file):
|
70
|
+
"""Validate specification structure"""
|
71
|
+
if not spec_file.exists():
|
72
|
+
self.error_exit(f"Specification file does not exist: {spec_file}")
|
73
|
+
|
74
|
+
# Required sections
|
75
|
+
required_sections = [
|
76
|
+
"## Specification:",
|
77
|
+
"## Metadata",
|
78
|
+
"## Functional Requirements",
|
79
|
+
"## Acceptance Scenarios"
|
80
|
+
]
|
81
|
+
|
82
|
+
missing_sections = []
|
83
|
+
content = spec_file.read_text(encoding='utf-8')
|
84
|
+
|
85
|
+
for section in required_sections:
|
86
|
+
if section not in content:
|
87
|
+
missing_sections.append(section)
|
88
|
+
|
89
|
+
if missing_sections:
|
90
|
+
self.log(f"WARNING: Missing required sections: {', '.join(missing_sections)}")
|
91
|
+
|
92
|
+
# Check for clarifications needed
|
93
|
+
clarification_matches = re.findall(r'NEEDS CLARIFICATION', content)
|
94
|
+
if clarification_matches:
|
95
|
+
clarification_count = len(clarification_matches)
|
96
|
+
self.log(f"WARNING: Specification has {clarification_count} clarifications needed")
|
97
|
+
return clarification_count
|
98
|
+
|
99
|
+
return 0
|
100
|
+
|
101
|
+
def main(self, args):
|
102
|
+
"""Main execution function"""
|
103
|
+
if len(args) < 1:
|
104
|
+
self.error_exit("Usage: python pulse-spec.py <feature-dir> [spec-content]")
|
105
|
+
|
106
|
+
feature_dir = args[0]
|
107
|
+
spec_content = args[1] if len(args) > 1 else None
|
108
|
+
|
109
|
+
self.log("Processing specification...")
|
110
|
+
|
111
|
+
# Get and sanitize feature directory
|
112
|
+
sanitized_dir = self.get_current_feature_dir(feature_dir)
|
113
|
+
|
114
|
+
# Set file paths
|
115
|
+
spec_file = self.project_root / "specs" / sanitized_dir / "spec.md"
|
116
|
+
template_file = self.templates_dir / "spec.md"
|
117
|
+
|
118
|
+
# Ensure specs directory exists
|
119
|
+
spec_file.parent.mkdir(parents=True, exist_ok=True)
|
120
|
+
|
121
|
+
# Handle specification content
|
122
|
+
if spec_content:
|
123
|
+
self.log(f"Updating specification: {spec_file}")
|
124
|
+
try:
|
125
|
+
spec_file.write_text(spec_content, encoding='utf-8')
|
126
|
+
except Exception as e:
|
127
|
+
self.error_exit(f"Failed to write specification content: {e}")
|
128
|
+
else:
|
129
|
+
# Ensure specification exists from template
|
130
|
+
if not spec_file.exists():
|
131
|
+
if not template_file.exists():
|
132
|
+
self.error_exit(f"Template not found: {template_file}")
|
133
|
+
|
134
|
+
self.log(f"Creating specification from template: {spec_file}")
|
135
|
+
try:
|
136
|
+
spec_file.write_text(template_file.read_text(encoding='utf-8'))
|
137
|
+
except Exception as e:
|
138
|
+
self.error_exit(f"Failed to copy specification template: {e}")
|
139
|
+
else:
|
140
|
+
self.log(f"Specification already exists: {spec_file}")
|
141
|
+
|
142
|
+
# Validate specification
|
143
|
+
self.log("Validating specification...")
|
144
|
+
clarification_count = self.validate_specification(spec_file)
|
145
|
+
|
146
|
+
# Check for missing sections
|
147
|
+
required_sections = [
|
148
|
+
"## Specification:",
|
149
|
+
"## Metadata",
|
150
|
+
"## Functional Requirements",
|
151
|
+
"## Acceptance Scenarios"
|
152
|
+
]
|
153
|
+
|
154
|
+
content = spec_file.read_text(encoding='utf-8')
|
155
|
+
missing_sections = [s for s in required_sections if s not in content]
|
156
|
+
|
157
|
+
self.log("Specification processing completed successfully")
|
158
|
+
|
159
|
+
# Output results
|
160
|
+
print(f"SPEC_FILE={spec_file}")
|
161
|
+
print(f"CLARIFICATIONS_NEEDED={clarification_count}")
|
162
|
+
print(f"MISSING_SECTIONS={len(missing_sections)}")
|
163
|
+
print("STATUS=updated")
|
164
|
+
|
165
|
+
if __name__ == "__main__":
|
166
|
+
spec = SpecPulseSpec()
|
167
|
+
spec.main(sys.argv[1:])
|