devforgeai 1.0.4 → 1.0.6
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.
- package/CLAUDE.md +120 -0
- package/package.json +9 -1
- package/src/CLAUDE.md +699 -0
- package/src/claude/scripts/README.md +396 -0
- package/src/claude/scripts/audit-command-skill-overlap.sh +67 -0
- package/src/claude/scripts/check-hooks-fast.sh +70 -0
- package/src/claude/scripts/devforgeai-validate +6 -0
- package/src/claude/scripts/devforgeai_cli/README.md +531 -0
- package/src/claude/scripts/devforgeai_cli/__init__.py +12 -0
- package/src/claude/scripts/devforgeai_cli/cli.py +716 -0
- package/src/claude/scripts/devforgeai_cli/commands/__init__.py +1 -0
- package/src/claude/scripts/devforgeai_cli/commands/check_hooks.py +384 -0
- package/src/claude/scripts/devforgeai_cli/commands/invoke_hooks.py +149 -0
- package/src/claude/scripts/devforgeai_cli/commands/phase_commands.py +731 -0
- package/src/claude/scripts/devforgeai_cli/commands/validate_installation.py +412 -0
- package/src/claude/scripts/devforgeai_cli/context_extraction.py +426 -0
- package/src/claude/scripts/devforgeai_cli/feedback/AC_TO_TEST_MAPPING.md +636 -0
- package/src/claude/scripts/devforgeai_cli/feedback/DELIVERY_SUMMARY.txt +329 -0
- package/src/claude/scripts/devforgeai_cli/feedback/README_TEST_SPECS.md +486 -0
- package/src/claude/scripts/devforgeai_cli/feedback/TEST_IMPLEMENTATION_GUIDE.md +529 -0
- package/src/claude/scripts/devforgeai_cli/feedback/TEST_SPECIFICATIONS.md +2652 -0
- package/src/claude/scripts/devforgeai_cli/feedback/TEST_SPECS_INDEX.md +398 -0
- package/src/claude/scripts/devforgeai_cli/feedback/__init__.py +34 -0
- package/src/claude/scripts/devforgeai_cli/feedback/adaptive_questioning_engine.py +581 -0
- package/src/claude/scripts/devforgeai_cli/feedback/aggregation.py +179 -0
- package/src/claude/scripts/devforgeai_cli/feedback/commands.py +535 -0
- package/src/claude/scripts/devforgeai_cli/feedback/config_defaults.py +58 -0
- package/src/claude/scripts/devforgeai_cli/feedback/config_manager.py +423 -0
- package/src/claude/scripts/devforgeai_cli/feedback/config_models.py +192 -0
- package/src/claude/scripts/devforgeai_cli/feedback/config_schema.py +140 -0
- package/src/claude/scripts/devforgeai_cli/feedback/coverage.json +1 -0
- package/src/claude/scripts/devforgeai_cli/feedback/feature_flag.py +152 -0
- package/src/claude/scripts/devforgeai_cli/feedback/feedback_indexer.py +394 -0
- package/src/claude/scripts/devforgeai_cli/feedback/hot_reload.py +226 -0
- package/src/claude/scripts/devforgeai_cli/feedback/longitudinal.py +115 -0
- package/src/claude/scripts/devforgeai_cli/feedback/models.py +67 -0
- package/src/claude/scripts/devforgeai_cli/feedback/question_router.py +236 -0
- package/src/claude/scripts/devforgeai_cli/feedback/retrospective.py +233 -0
- package/src/claude/scripts/devforgeai_cli/feedback/skip_tracker.py +177 -0
- package/src/claude/scripts/devforgeai_cli/feedback/skip_tracking.py +221 -0
- package/src/claude/scripts/devforgeai_cli/feedback/template_engine.py +549 -0
- package/src/claude/scripts/devforgeai_cli/feedback/validation.py +163 -0
- package/src/claude/scripts/devforgeai_cli/headless/__init__.py +30 -0
- package/src/claude/scripts/devforgeai_cli/headless/answer_models.py +206 -0
- package/src/claude/scripts/devforgeai_cli/headless/answer_resolver.py +204 -0
- package/src/claude/scripts/devforgeai_cli/headless/exceptions.py +36 -0
- package/src/claude/scripts/devforgeai_cli/headless/pattern_matcher.py +156 -0
- package/src/claude/scripts/devforgeai_cli/hooks.py +313 -0
- package/src/claude/scripts/devforgeai_cli/metrics/__init__.py +46 -0
- package/src/claude/scripts/devforgeai_cli/metrics/command_metrics.py +142 -0
- package/src/claude/scripts/devforgeai_cli/metrics/failure_modes.py +152 -0
- package/src/claude/scripts/devforgeai_cli/metrics/story_segmentation.py +181 -0
- package/src/claude/scripts/devforgeai_cli/orchestrate_hooks.py +780 -0
- package/src/claude/scripts/devforgeai_cli/phase_state.py +1229 -0
- package/src/claude/scripts/devforgeai_cli/session/__init__.py +30 -0
- package/src/claude/scripts/devforgeai_cli/session/checkpoint.py +268 -0
- package/src/claude/scripts/devforgeai_cli/tests/__init__.py +1 -0
- package/src/claude/scripts/devforgeai_cli/tests/conftest.py +29 -0
- package/src/claude/scripts/devforgeai_cli/tests/feedback/TEST_EXECUTION_GUIDE.md +298 -0
- package/src/claude/scripts/devforgeai_cli/tests/feedback/__init__.py +3 -0
- package/src/claude/scripts/devforgeai_cli/tests/feedback/test_adaptive_questioning_engine.py +2171 -0
- package/src/claude/scripts/devforgeai_cli/tests/feedback/test_aggregation.py +476 -0
- package/src/claude/scripts/devforgeai_cli/tests/feedback/test_config_defaults.py +133 -0
- package/src/claude/scripts/devforgeai_cli/tests/feedback/test_config_manager.py +592 -0
- package/src/claude/scripts/devforgeai_cli/tests/feedback/test_config_models.py +373 -0
- package/src/claude/scripts/devforgeai_cli/tests/feedback/test_config_schema.py +130 -0
- package/src/claude/scripts/devforgeai_cli/tests/feedback/test_configuration_management.py +1355 -0
- package/src/claude/scripts/devforgeai_cli/tests/feedback/test_edge_cases.py +308 -0
- package/src/claude/scripts/devforgeai_cli/tests/feedback/test_feature_flag.py +307 -0
- package/src/claude/scripts/devforgeai_cli/tests/feedback/test_feedback_indexer.py +384 -0
- package/src/claude/scripts/devforgeai_cli/tests/feedback/test_hot_reload.py +580 -0
- package/src/claude/scripts/devforgeai_cli/tests/feedback/test_integration.py +402 -0
- package/src/claude/scripts/devforgeai_cli/tests/feedback/test_models.py +105 -0
- package/src/claude/scripts/devforgeai_cli/tests/feedback/test_question_routing.py +262 -0
- package/src/claude/scripts/devforgeai_cli/tests/feedback/test_retrospective.py +333 -0
- package/src/claude/scripts/devforgeai_cli/tests/feedback/test_skip_tracker.py +410 -0
- package/src/claude/scripts/devforgeai_cli/tests/feedback/test_skip_tracking.py +159 -0
- package/src/claude/scripts/devforgeai_cli/tests/feedback/test_skip_tracking_integration.py +1155 -0
- package/src/claude/scripts/devforgeai_cli/tests/feedback/test_template_engine.py +1389 -0
- package/src/claude/scripts/devforgeai_cli/tests/feedback/test_validation_comprehensive.py +210 -0
- package/src/claude/scripts/devforgeai_cli/tests/fixtures/autonomous-deferral-story.md +46 -0
- package/src/claude/scripts/devforgeai_cli/tests/fixtures/missing-impl-notes.md +31 -0
- package/src/claude/scripts/devforgeai_cli/tests/fixtures/valid-deferral-story.md +46 -0
- package/src/claude/scripts/devforgeai_cli/tests/fixtures/valid-story-complete.md +48 -0
- package/src/claude/scripts/devforgeai_cli/tests/manual_test_invoke_hooks.sh +200 -0
- package/src/claude/scripts/devforgeai_cli/tests/session/DELIVERABLES.md +518 -0
- package/src/claude/scripts/devforgeai_cli/tests/session/TEST_SUMMARY.md +468 -0
- package/src/claude/scripts/devforgeai_cli/tests/session/__init__.py +6 -0
- package/src/claude/scripts/devforgeai_cli/tests/session/fixtures/corrupted-checkpoint.json +1 -0
- package/src/claude/scripts/devforgeai_cli/tests/session/fixtures/missing-fields-checkpoint.json +4 -0
- package/src/claude/scripts/devforgeai_cli/tests/session/fixtures/valid-checkpoint.json +15 -0
- package/src/claude/scripts/devforgeai_cli/tests/session/test_checkpoint.py +851 -0
- package/src/claude/scripts/devforgeai_cli/tests/test_check_hooks.py +1886 -0
- package/src/claude/scripts/devforgeai_cli/tests/test_depends_on_normalizer.py +171 -0
- package/src/claude/scripts/devforgeai_cli/tests/test_dod_validator.py +97 -0
- package/src/claude/scripts/devforgeai_cli/tests/test_invoke_hooks.py +1902 -0
- package/src/claude/scripts/devforgeai_cli/tests/test_phase_commands.py +320 -0
- package/src/claude/scripts/devforgeai_cli/tests/test_phase_commands_error_handling.py +1021 -0
- package/src/claude/scripts/devforgeai_cli/tests/test_phase_commands_import.py +697 -0
- package/src/claude/scripts/devforgeai_cli/tests/test_phase_state.py +2187 -0
- package/src/claude/scripts/devforgeai_cli/tests/test_skip_tracking.py +2141 -0
- package/src/claude/scripts/devforgeai_cli/tests/test_skip_tracking_coverage_gap.py +195 -0
- package/src/claude/scripts/devforgeai_cli/tests/test_subagent_enforcement.py +539 -0
- package/src/claude/scripts/devforgeai_cli/tests/test_validate_installation.py +361 -0
- package/src/claude/scripts/devforgeai_cli/utils/__init__.py +11 -0
- package/src/claude/scripts/devforgeai_cli/utils/depends_on_normalizer.py +149 -0
- package/src/claude/scripts/devforgeai_cli/utils/markdown_parser.py +219 -0
- package/src/claude/scripts/devforgeai_cli/utils/story_analyzer.py +249 -0
- package/src/claude/scripts/devforgeai_cli/utils/yaml_parser.py +152 -0
- package/src/claude/scripts/devforgeai_cli/validators/__init__.py +27 -0
- package/src/claude/scripts/devforgeai_cli/validators/ast_grep_validator.py +373 -0
- package/src/claude/scripts/devforgeai_cli/validators/context_validator.py +180 -0
- package/src/claude/scripts/devforgeai_cli/validators/dod_validator.py +309 -0
- package/src/claude/scripts/devforgeai_cli/validators/git_validator.py +107 -0
- package/src/claude/scripts/devforgeai_cli/validators/grep_fallback.py +300 -0
- package/src/claude/scripts/install_hooks.sh +186 -0
- package/src/claude/scripts/invoke_feedback_hooks.sh +59 -0
- package/src/claude/scripts/migrate-ac-headers.sh +122 -0
- package/src/claude/scripts/plan_file_kb.sh +704 -0
- package/src/claude/scripts/requirements.txt +8 -0
- package/src/claude/scripts/session_catalog.sh +543 -0
- package/src/claude/scripts/setup.py +55 -0
- package/src/claude/scripts/start-devforgeai.sh +16 -0
- package/src/claude/scripts/statusline.sh +27 -0
- package/src/claude/scripts/validate_deferrals.py +344 -0
- package/src/claude/skills/devforgeai-qa/SKILL.md +1 -1
- package/src/claude/skills/researching-market/SKILL.md +2 -1
- package/src/cli/lib/copier.js +13 -1
- package/src/claude/skills/designing-systems/scripts/__pycache__/detect_anti_patterns.cpython-312.pyc +0 -0
- package/src/claude/skills/designing-systems/scripts/__pycache__/validate_all_context.cpython-312.pyc +0 -0
- package/src/claude/skills/designing-systems/scripts/__pycache__/validate_architecture.cpython-312.pyc +0 -0
- package/src/claude/skills/designing-systems/scripts/__pycache__/validate_dependencies.cpython-312.pyc +0 -0
- package/src/claude/skills/devforgeai-story-creation/scripts/__pycache__/migrate_story_v1_to_v2.cpython-312.pyc +0 -0
- package/src/claude/skills/devforgeai-story-creation/scripts/tests/__pycache__/measure_accuracy.cpython-312.pyc +0 -0
|
@@ -0,0 +1,373 @@
|
|
|
1
|
+
"""
|
|
2
|
+
AstGrepValidator - ast-grep installation detection and management.
|
|
3
|
+
|
|
4
|
+
Handles installation detection, version compatibility checking,
|
|
5
|
+
interactive prompts, and pip-based installation for STORY-115.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import os
|
|
9
|
+
import sys
|
|
10
|
+
import re
|
|
11
|
+
import shutil
|
|
12
|
+
import subprocess
|
|
13
|
+
import logging
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
from typing import Tuple, List, Dict, Optional
|
|
16
|
+
from enum import Enum
|
|
17
|
+
from dataclasses import dataclass
|
|
18
|
+
|
|
19
|
+
try:
|
|
20
|
+
import yaml
|
|
21
|
+
except ImportError:
|
|
22
|
+
yaml = None # YAML is optional for basic functionality
|
|
23
|
+
|
|
24
|
+
logger = logging.getLogger(__name__)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
# =============================================================================
|
|
28
|
+
# Enums and Data Classes
|
|
29
|
+
# =============================================================================
|
|
30
|
+
|
|
31
|
+
class InstallAction(Enum):
|
|
32
|
+
"""User choices for missing ast-grep dependency."""
|
|
33
|
+
INSTALL_NOW = "install"
|
|
34
|
+
USE_FALLBACK = "fallback"
|
|
35
|
+
SKIP = "skip"
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@dataclass
|
|
39
|
+
class VersionInfo:
|
|
40
|
+
"""Parsed semantic version information."""
|
|
41
|
+
major: int
|
|
42
|
+
minor: int
|
|
43
|
+
patch: int
|
|
44
|
+
raw: str
|
|
45
|
+
|
|
46
|
+
def is_compatible(self, min_version: str = "0.40.0", max_version: str = "1.0.0") -> bool:
|
|
47
|
+
"""
|
|
48
|
+
Check if version is within compatible range [min_version, max_version).
|
|
49
|
+
|
|
50
|
+
Args:
|
|
51
|
+
min_version: Minimum version (inclusive)
|
|
52
|
+
max_version: Maximum version (exclusive)
|
|
53
|
+
|
|
54
|
+
Returns:
|
|
55
|
+
True if version is compatible
|
|
56
|
+
"""
|
|
57
|
+
min_ver = parse_version(min_version)
|
|
58
|
+
max_ver = parse_version(max_version)
|
|
59
|
+
|
|
60
|
+
if not min_ver or not max_ver:
|
|
61
|
+
return False
|
|
62
|
+
|
|
63
|
+
# Check minimum (inclusive)
|
|
64
|
+
if self.major < min_ver.major:
|
|
65
|
+
return False
|
|
66
|
+
if self.major == min_ver.major and self.minor < min_ver.minor:
|
|
67
|
+
return False
|
|
68
|
+
if self.major == min_ver.major and self.minor == min_ver.minor and self.patch < min_ver.patch:
|
|
69
|
+
return False
|
|
70
|
+
|
|
71
|
+
# Check maximum (exclusive)
|
|
72
|
+
if self.major > max_ver.major:
|
|
73
|
+
return False
|
|
74
|
+
if self.major == max_ver.major and self.minor >= max_ver.minor:
|
|
75
|
+
return False
|
|
76
|
+
|
|
77
|
+
return True
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
# =============================================================================
|
|
81
|
+
# Utility Functions
|
|
82
|
+
# =============================================================================
|
|
83
|
+
|
|
84
|
+
def parse_version(version_string: str) -> Optional[VersionInfo]:
|
|
85
|
+
"""
|
|
86
|
+
Parse semantic version string without external libraries.
|
|
87
|
+
|
|
88
|
+
Args:
|
|
89
|
+
version_string: Version string like "0.40.0" or "v0.45.0-beta"
|
|
90
|
+
|
|
91
|
+
Returns:
|
|
92
|
+
VersionInfo object or None if parsing fails
|
|
93
|
+
"""
|
|
94
|
+
if not version_string:
|
|
95
|
+
return None
|
|
96
|
+
|
|
97
|
+
# Remove 'v' prefix if present
|
|
98
|
+
version_string = version_string.strip()
|
|
99
|
+
if version_string.startswith('v'):
|
|
100
|
+
version_string = version_string[1:]
|
|
101
|
+
|
|
102
|
+
# Extract major.minor.patch (ignore prerelease/build metadata)
|
|
103
|
+
pattern = r'^(\d+)\.(\d+)\.(\d+)'
|
|
104
|
+
match = re.match(pattern, version_string)
|
|
105
|
+
|
|
106
|
+
if not match:
|
|
107
|
+
return None
|
|
108
|
+
|
|
109
|
+
try:
|
|
110
|
+
major = int(match.group(1))
|
|
111
|
+
minor = int(match.group(2))
|
|
112
|
+
patch = int(match.group(3))
|
|
113
|
+
return VersionInfo(major=major, minor=minor, patch=patch, raw=version_string)
|
|
114
|
+
except (ValueError, IndexError):
|
|
115
|
+
return None
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def detect_headless_mode() -> bool:
|
|
119
|
+
"""
|
|
120
|
+
Detect if running in CI/headless environment.
|
|
121
|
+
|
|
122
|
+
Checks for common CI environment variables and terminal availability.
|
|
123
|
+
|
|
124
|
+
Returns:
|
|
125
|
+
True if headless mode detected
|
|
126
|
+
"""
|
|
127
|
+
# CI environment variables
|
|
128
|
+
if os.environ.get("CI") == "true":
|
|
129
|
+
return True
|
|
130
|
+
if os.environ.get("DEVFORGEAI_HEADLESS") == "true":
|
|
131
|
+
return True
|
|
132
|
+
if os.environ.get("GITHUB_ACTIONS") == "true":
|
|
133
|
+
return True
|
|
134
|
+
|
|
135
|
+
# Non-interactive terminal
|
|
136
|
+
try:
|
|
137
|
+
return not os.isatty(sys.stdin.fileno())
|
|
138
|
+
except Exception:
|
|
139
|
+
return False
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def load_config(config_path: Optional[str] = None) -> Dict:
|
|
143
|
+
"""
|
|
144
|
+
Load configuration with defaults.
|
|
145
|
+
|
|
146
|
+
Args:
|
|
147
|
+
config_path: Path to config.yaml file
|
|
148
|
+
|
|
149
|
+
Returns:
|
|
150
|
+
Configuration dictionary with defaults
|
|
151
|
+
"""
|
|
152
|
+
default_config = {
|
|
153
|
+
'fallback_mode': False,
|
|
154
|
+
'min_version': '0.40.0',
|
|
155
|
+
'max_version': '1.0.0',
|
|
156
|
+
'allow_auto_install': False
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
if not config_path:
|
|
160
|
+
config_path = "devforgeai/ast-grep/config.yaml"
|
|
161
|
+
|
|
162
|
+
config_file = Path(config_path)
|
|
163
|
+
if not config_file.exists():
|
|
164
|
+
return default_config
|
|
165
|
+
|
|
166
|
+
if not yaml:
|
|
167
|
+
logger.warning("PyYAML not available, using default configuration")
|
|
168
|
+
return default_config
|
|
169
|
+
|
|
170
|
+
try:
|
|
171
|
+
with open(config_file, 'r') as f:
|
|
172
|
+
user_config = yaml.safe_load(f) or {}
|
|
173
|
+
# Merge with defaults
|
|
174
|
+
return {**default_config, **user_config}
|
|
175
|
+
except Exception as e:
|
|
176
|
+
logger.warning(f"Failed to load config from {config_path}: {e}")
|
|
177
|
+
return default_config
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
# =============================================================================
|
|
181
|
+
# Main Validator Class
|
|
182
|
+
# =============================================================================
|
|
183
|
+
|
|
184
|
+
class AstGrepValidator:
|
|
185
|
+
"""Manages ast-grep installation and validation."""
|
|
186
|
+
|
|
187
|
+
def __init__(self, config_path: Optional[str] = None, interactive: bool = True):
|
|
188
|
+
"""
|
|
189
|
+
Initialize validator.
|
|
190
|
+
|
|
191
|
+
Args:
|
|
192
|
+
config_path: Path to devforgeai/ast-grep/config.yaml
|
|
193
|
+
interactive: Whether to show prompts (False for CI/headless)
|
|
194
|
+
"""
|
|
195
|
+
self.config = load_config(config_path)
|
|
196
|
+
self.interactive = interactive and not detect_headless_mode()
|
|
197
|
+
self._ast_grep_available = None # Lazy initialization
|
|
198
|
+
|
|
199
|
+
def is_installed(self) -> bool:
|
|
200
|
+
"""
|
|
201
|
+
Check if ast-grep is available on system.
|
|
202
|
+
|
|
203
|
+
Returns:
|
|
204
|
+
True if ast-grep found in PATH
|
|
205
|
+
"""
|
|
206
|
+
return shutil.which("sg") is not None or shutil.which("ast-grep") is not None
|
|
207
|
+
|
|
208
|
+
def get_version(self) -> Optional[VersionInfo]:
|
|
209
|
+
"""
|
|
210
|
+
Get installed ast-grep version.
|
|
211
|
+
|
|
212
|
+
Returns:
|
|
213
|
+
VersionInfo object or None if detection fails
|
|
214
|
+
"""
|
|
215
|
+
ast_grep_cmd = shutil.which("sg") or shutil.which("ast-grep")
|
|
216
|
+
if not ast_grep_cmd:
|
|
217
|
+
return None
|
|
218
|
+
|
|
219
|
+
try:
|
|
220
|
+
result = subprocess.run(
|
|
221
|
+
[ast_grep_cmd, "--version"],
|
|
222
|
+
capture_output=True,
|
|
223
|
+
text=True,
|
|
224
|
+
timeout=5
|
|
225
|
+
)
|
|
226
|
+
|
|
227
|
+
if result.returncode == 0:
|
|
228
|
+
# Parse version from output like "ast-grep 0.45.0"
|
|
229
|
+
version_match = re.search(r'(\d+\.\d+\.\d+)', result.stdout)
|
|
230
|
+
if version_match:
|
|
231
|
+
return parse_version(version_match.group(1))
|
|
232
|
+
|
|
233
|
+
return None
|
|
234
|
+
except Exception as e:
|
|
235
|
+
logger.debug(f"Failed to get ast-grep version: {e}")
|
|
236
|
+
return None
|
|
237
|
+
|
|
238
|
+
def check_version_compatibility(self) -> Tuple[bool, str]:
|
|
239
|
+
"""
|
|
240
|
+
Verify version meets requirements.
|
|
241
|
+
|
|
242
|
+
Returns:
|
|
243
|
+
Tuple of (is_compatible, message)
|
|
244
|
+
"""
|
|
245
|
+
version = self.get_version()
|
|
246
|
+
|
|
247
|
+
if not version:
|
|
248
|
+
return False, "Could not determine ast-grep version"
|
|
249
|
+
|
|
250
|
+
min_version = self.config.get('min_version', '0.40.0')
|
|
251
|
+
max_version = self.config.get('max_version', '1.0.0')
|
|
252
|
+
|
|
253
|
+
if version.is_compatible(min_version, max_version):
|
|
254
|
+
return True, f"ast-grep version {version.raw} is compatible"
|
|
255
|
+
else:
|
|
256
|
+
return False, f"ast-grep version {version.raw} is not compatible (requires >={min_version}, <{max_version})"
|
|
257
|
+
|
|
258
|
+
def install_via_pip(self) -> Tuple[bool, str]:
|
|
259
|
+
"""
|
|
260
|
+
Install ast-grep-cli via pip.
|
|
261
|
+
|
|
262
|
+
Returns:
|
|
263
|
+
Tuple of (success, message)
|
|
264
|
+
"""
|
|
265
|
+
try:
|
|
266
|
+
result = subprocess.run(
|
|
267
|
+
[sys.executable, "-m", "pip", "install", "ast-grep-cli>=0.40.0,<1.0.0"],
|
|
268
|
+
capture_output=True,
|
|
269
|
+
text=True,
|
|
270
|
+
timeout=120
|
|
271
|
+
)
|
|
272
|
+
|
|
273
|
+
if result.returncode == 0:
|
|
274
|
+
return True, "Successfully installed ast-grep-cli"
|
|
275
|
+
else:
|
|
276
|
+
return False, f"Installation failed: {result.stderr}"
|
|
277
|
+
|
|
278
|
+
except subprocess.TimeoutExpired:
|
|
279
|
+
return False, "Installation timeout after 120 seconds"
|
|
280
|
+
except Exception as e:
|
|
281
|
+
return False, f"Installation error: {str(e)}"
|
|
282
|
+
|
|
283
|
+
def prompt_missing_dependency(self) -> InstallAction:
|
|
284
|
+
"""
|
|
285
|
+
Display interactive prompt for missing ast-grep.
|
|
286
|
+
|
|
287
|
+
Returns:
|
|
288
|
+
User's chosen action
|
|
289
|
+
"""
|
|
290
|
+
if not self.interactive:
|
|
291
|
+
# In headless mode, use config or default to fallback
|
|
292
|
+
if self.config.get('fallback_mode', False):
|
|
293
|
+
return InstallAction.USE_FALLBACK
|
|
294
|
+
return InstallAction.USE_FALLBACK
|
|
295
|
+
|
|
296
|
+
print("\n" + "=" * 60)
|
|
297
|
+
print(" ast-grep Not Found")
|
|
298
|
+
print("=" * 60)
|
|
299
|
+
print("\nast-grep provides semantic code analysis (90-95% accuracy).")
|
|
300
|
+
print("Without it, grep-based fallback will be used (60-75% accuracy).\n")
|
|
301
|
+
print("Options:")
|
|
302
|
+
print(" 1) Install now (pip install ast-grep-cli)")
|
|
303
|
+
print(" 2) Use fallback (grep-based analysis)")
|
|
304
|
+
print(" 3) Skip\n")
|
|
305
|
+
|
|
306
|
+
while True:
|
|
307
|
+
try:
|
|
308
|
+
choice = input("Select option [1-3]: ").strip()
|
|
309
|
+
|
|
310
|
+
if choice == "1":
|
|
311
|
+
return InstallAction.INSTALL_NOW
|
|
312
|
+
elif choice == "2":
|
|
313
|
+
return InstallAction.USE_FALLBACK
|
|
314
|
+
elif choice == "3":
|
|
315
|
+
return InstallAction.SKIP
|
|
316
|
+
else:
|
|
317
|
+
print("Invalid choice. Please enter 1, 2, or 3.")
|
|
318
|
+
except (EOFError, KeyboardInterrupt):
|
|
319
|
+
print("\nOperation cancelled.")
|
|
320
|
+
return InstallAction.SKIP
|
|
321
|
+
|
|
322
|
+
def validate(self, target_path: str) -> Tuple[bool, List[Dict]]:
|
|
323
|
+
"""
|
|
324
|
+
Main validation entry point.
|
|
325
|
+
|
|
326
|
+
Args:
|
|
327
|
+
target_path: Directory to scan
|
|
328
|
+
|
|
329
|
+
Returns:
|
|
330
|
+
Tuple of (success, violations)
|
|
331
|
+
"""
|
|
332
|
+
violations = []
|
|
333
|
+
|
|
334
|
+
# Check if ast-grep is installed
|
|
335
|
+
if not self.is_installed():
|
|
336
|
+
# Handle missing dependency
|
|
337
|
+
action = self.prompt_missing_dependency()
|
|
338
|
+
|
|
339
|
+
if action == InstallAction.INSTALL_NOW:
|
|
340
|
+
success, message = self.install_via_pip()
|
|
341
|
+
if not success:
|
|
342
|
+
violations.append({
|
|
343
|
+
"severity": "HIGH",
|
|
344
|
+
"error": "ast-grep installation failed",
|
|
345
|
+
"fix": message,
|
|
346
|
+
"analysis_method": "none"
|
|
347
|
+
})
|
|
348
|
+
return False, violations
|
|
349
|
+
|
|
350
|
+
elif action == InstallAction.SKIP:
|
|
351
|
+
violations.append({
|
|
352
|
+
"severity": "INFO",
|
|
353
|
+
"error": "ast-grep not available, scan skipped",
|
|
354
|
+
"fix": "Install ast-grep-cli to enable semantic analysis",
|
|
355
|
+
"analysis_method": "none"
|
|
356
|
+
})
|
|
357
|
+
return True, violations
|
|
358
|
+
|
|
359
|
+
# If USE_FALLBACK, will continue to use grep fallback below
|
|
360
|
+
|
|
361
|
+
# Check version compatibility
|
|
362
|
+
if self.is_installed():
|
|
363
|
+
is_compatible, message = self.check_version_compatibility()
|
|
364
|
+
if not is_compatible:
|
|
365
|
+
violations.append({
|
|
366
|
+
"severity": "MEDIUM",
|
|
367
|
+
"error": "ast-grep version incompatible",
|
|
368
|
+
"fix": message,
|
|
369
|
+
"analysis_method": "version-check"
|
|
370
|
+
})
|
|
371
|
+
|
|
372
|
+
# For now, return success (actual scanning will be implemented in integration)
|
|
373
|
+
return True, violations
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Context Files Validator
|
|
4
|
+
|
|
5
|
+
Validates all 6 DevForgeAI context files exist and are non-empty.
|
|
6
|
+
Quality gate before development begins.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import sys
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import Dict, List, Tuple
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
# The 6 required context files
|
|
15
|
+
REQUIRED_CONTEXT_FILES = [
|
|
16
|
+
'tech-stack.md',
|
|
17
|
+
'source-tree.md',
|
|
18
|
+
'dependencies.md',
|
|
19
|
+
'coding-standards.md',
|
|
20
|
+
'architecture-constraints.md',
|
|
21
|
+
'anti-patterns.md'
|
|
22
|
+
]
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def check_context_files(directory: str = ".") -> Tuple[bool, List[Dict]]:
|
|
26
|
+
"""
|
|
27
|
+
Check if all 6 context files exist and are non-empty.
|
|
28
|
+
|
|
29
|
+
Args:
|
|
30
|
+
directory: Project root directory
|
|
31
|
+
|
|
32
|
+
Returns:
|
|
33
|
+
Tuple of (all_valid, violations)
|
|
34
|
+
|
|
35
|
+
Violations include:
|
|
36
|
+
- CRITICAL: Missing context file
|
|
37
|
+
- HIGH: Empty context file (placeholder)
|
|
38
|
+
- MEDIUM: File exists but <100 bytes (likely incomplete)
|
|
39
|
+
"""
|
|
40
|
+
violations = []
|
|
41
|
+
project_root = Path(directory)
|
|
42
|
+
context_dir = project_root / "devforgeai" / "context"
|
|
43
|
+
|
|
44
|
+
# Check if context directory exists
|
|
45
|
+
if not context_dir.exists():
|
|
46
|
+
violations.append({
|
|
47
|
+
'severity': 'CRITICAL',
|
|
48
|
+
'file': 'devforgeai/specs/context/',
|
|
49
|
+
'error': 'Context directory does not exist',
|
|
50
|
+
'fix': 'Run /create-context to generate context files'
|
|
51
|
+
})
|
|
52
|
+
return False, violations
|
|
53
|
+
|
|
54
|
+
# Check each required file
|
|
55
|
+
for filename in REQUIRED_CONTEXT_FILES:
|
|
56
|
+
file_path = context_dir / filename
|
|
57
|
+
|
|
58
|
+
if not file_path.exists():
|
|
59
|
+
violations.append({
|
|
60
|
+
'severity': 'CRITICAL',
|
|
61
|
+
'file': filename,
|
|
62
|
+
'error': f'Context file missing',
|
|
63
|
+
'path': str(file_path),
|
|
64
|
+
'fix': f'Create {filename} or run /create-context'
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
elif file_path.stat().st_size == 0:
|
|
68
|
+
violations.append({
|
|
69
|
+
'severity': 'HIGH',
|
|
70
|
+
'file': filename,
|
|
71
|
+
'error': 'Context file is empty (placeholder)',
|
|
72
|
+
'path': str(file_path),
|
|
73
|
+
'fix': f'Populate {filename} with project-specific content'
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
elif file_path.stat().st_size < 100:
|
|
77
|
+
violations.append({
|
|
78
|
+
'severity': 'MEDIUM',
|
|
79
|
+
'file': filename,
|
|
80
|
+
'error': f'Context file is very small ({file_path.stat().st_size} bytes)',
|
|
81
|
+
'path': str(file_path),
|
|
82
|
+
'fix': f'Review and expand {filename} content'
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
is_valid = len([v for v in violations if v['severity'] in ['CRITICAL', 'HIGH']]) == 0
|
|
86
|
+
|
|
87
|
+
return is_valid, violations
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def validate_context(directory: str = ".", output_format: str = 'text') -> int:
|
|
91
|
+
"""
|
|
92
|
+
Main validator entry point.
|
|
93
|
+
|
|
94
|
+
Args:
|
|
95
|
+
directory: Project root directory
|
|
96
|
+
output_format: 'text' or 'json'
|
|
97
|
+
|
|
98
|
+
Returns:
|
|
99
|
+
Exit code: 0 = all files valid, 1 = violations found, 2 = error
|
|
100
|
+
"""
|
|
101
|
+
try:
|
|
102
|
+
is_valid, violations = check_context_files(directory)
|
|
103
|
+
|
|
104
|
+
if output_format == 'json':
|
|
105
|
+
import json
|
|
106
|
+
result = {
|
|
107
|
+
'valid': is_valid,
|
|
108
|
+
'violations': violations,
|
|
109
|
+
'directory': str(Path(directory).resolve())
|
|
110
|
+
}
|
|
111
|
+
print(json.dumps(result, indent=2))
|
|
112
|
+
|
|
113
|
+
else:
|
|
114
|
+
# Text output
|
|
115
|
+
if is_valid:
|
|
116
|
+
print(f"✅ All 6 context files validated")
|
|
117
|
+
print(f" Location: {Path(directory).resolve()}/devforgeai/specs/context/")
|
|
118
|
+
return 0
|
|
119
|
+
else:
|
|
120
|
+
print(f"❌ CONTEXT VALIDATION FAILED\n")
|
|
121
|
+
|
|
122
|
+
critical = [v for v in violations if v['severity'] == 'CRITICAL']
|
|
123
|
+
high = [v for v in violations if v['severity'] == 'HIGH']
|
|
124
|
+
medium = [v for v in violations if v['severity'] == 'MEDIUM']
|
|
125
|
+
|
|
126
|
+
if critical:
|
|
127
|
+
print("CRITICAL - Missing Files:")
|
|
128
|
+
for v in critical:
|
|
129
|
+
print(f" • {v['file']}")
|
|
130
|
+
print(f" Error: {v['error']}")
|
|
131
|
+
print(f" Fix: {v['fix']}")
|
|
132
|
+
print()
|
|
133
|
+
|
|
134
|
+
if high:
|
|
135
|
+
print("HIGH - Empty Files:")
|
|
136
|
+
for v in high:
|
|
137
|
+
print(f" • {v['file']}")
|
|
138
|
+
print(f" Path: {v['path']}")
|
|
139
|
+
print(f" Fix: {v['fix']}")
|
|
140
|
+
print()
|
|
141
|
+
|
|
142
|
+
if medium:
|
|
143
|
+
print("MEDIUM - Incomplete Files:")
|
|
144
|
+
for v in medium:
|
|
145
|
+
print(f" • {v['file']} ({v['error']})")
|
|
146
|
+
print()
|
|
147
|
+
|
|
148
|
+
print("=" * 80)
|
|
149
|
+
print("DEVELOPMENT BLOCKED - Context files required")
|
|
150
|
+
print("=" * 80)
|
|
151
|
+
print("\nAll 6 context files must exist:")
|
|
152
|
+
for filename in REQUIRED_CONTEXT_FILES:
|
|
153
|
+
status = "✅" if not any(v['file'] == filename for v in critical + high) else "❌"
|
|
154
|
+
print(f" {status} {filename}")
|
|
155
|
+
print("\nTo create context files:")
|
|
156
|
+
print(" Run: /create-context <project-name>")
|
|
157
|
+
print()
|
|
158
|
+
|
|
159
|
+
return 1
|
|
160
|
+
|
|
161
|
+
except Exception as e:
|
|
162
|
+
print(f"ERROR: Validation failed with exception: {e}", file=sys.stderr)
|
|
163
|
+
return 2
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
if __name__ == '__main__':
|
|
167
|
+
import argparse
|
|
168
|
+
|
|
169
|
+
parser = argparse.ArgumentParser(
|
|
170
|
+
description='Validate DevForgeAI context files exist and are non-empty'
|
|
171
|
+
)
|
|
172
|
+
parser.add_argument('--directory', default='.',
|
|
173
|
+
help='Project root directory (default: current directory)')
|
|
174
|
+
parser.add_argument('--format', choices=['text', 'json'], default='text',
|
|
175
|
+
help='Output format (default: text)')
|
|
176
|
+
|
|
177
|
+
args = parser.parse_args()
|
|
178
|
+
|
|
179
|
+
exit_code = validate_context(args.directory, args.format)
|
|
180
|
+
sys.exit(exit_code)
|