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.
Files changed (134) hide show
  1. package/CLAUDE.md +120 -0
  2. package/package.json +9 -1
  3. package/src/CLAUDE.md +699 -0
  4. package/src/claude/scripts/README.md +396 -0
  5. package/src/claude/scripts/audit-command-skill-overlap.sh +67 -0
  6. package/src/claude/scripts/check-hooks-fast.sh +70 -0
  7. package/src/claude/scripts/devforgeai-validate +6 -0
  8. package/src/claude/scripts/devforgeai_cli/README.md +531 -0
  9. package/src/claude/scripts/devforgeai_cli/__init__.py +12 -0
  10. package/src/claude/scripts/devforgeai_cli/cli.py +716 -0
  11. package/src/claude/scripts/devforgeai_cli/commands/__init__.py +1 -0
  12. package/src/claude/scripts/devforgeai_cli/commands/check_hooks.py +384 -0
  13. package/src/claude/scripts/devforgeai_cli/commands/invoke_hooks.py +149 -0
  14. package/src/claude/scripts/devforgeai_cli/commands/phase_commands.py +731 -0
  15. package/src/claude/scripts/devforgeai_cli/commands/validate_installation.py +412 -0
  16. package/src/claude/scripts/devforgeai_cli/context_extraction.py +426 -0
  17. package/src/claude/scripts/devforgeai_cli/feedback/AC_TO_TEST_MAPPING.md +636 -0
  18. package/src/claude/scripts/devforgeai_cli/feedback/DELIVERY_SUMMARY.txt +329 -0
  19. package/src/claude/scripts/devforgeai_cli/feedback/README_TEST_SPECS.md +486 -0
  20. package/src/claude/scripts/devforgeai_cli/feedback/TEST_IMPLEMENTATION_GUIDE.md +529 -0
  21. package/src/claude/scripts/devforgeai_cli/feedback/TEST_SPECIFICATIONS.md +2652 -0
  22. package/src/claude/scripts/devforgeai_cli/feedback/TEST_SPECS_INDEX.md +398 -0
  23. package/src/claude/scripts/devforgeai_cli/feedback/__init__.py +34 -0
  24. package/src/claude/scripts/devforgeai_cli/feedback/adaptive_questioning_engine.py +581 -0
  25. package/src/claude/scripts/devforgeai_cli/feedback/aggregation.py +179 -0
  26. package/src/claude/scripts/devforgeai_cli/feedback/commands.py +535 -0
  27. package/src/claude/scripts/devforgeai_cli/feedback/config_defaults.py +58 -0
  28. package/src/claude/scripts/devforgeai_cli/feedback/config_manager.py +423 -0
  29. package/src/claude/scripts/devforgeai_cli/feedback/config_models.py +192 -0
  30. package/src/claude/scripts/devforgeai_cli/feedback/config_schema.py +140 -0
  31. package/src/claude/scripts/devforgeai_cli/feedback/coverage.json +1 -0
  32. package/src/claude/scripts/devforgeai_cli/feedback/feature_flag.py +152 -0
  33. package/src/claude/scripts/devforgeai_cli/feedback/feedback_indexer.py +394 -0
  34. package/src/claude/scripts/devforgeai_cli/feedback/hot_reload.py +226 -0
  35. package/src/claude/scripts/devforgeai_cli/feedback/longitudinal.py +115 -0
  36. package/src/claude/scripts/devforgeai_cli/feedback/models.py +67 -0
  37. package/src/claude/scripts/devforgeai_cli/feedback/question_router.py +236 -0
  38. package/src/claude/scripts/devforgeai_cli/feedback/retrospective.py +233 -0
  39. package/src/claude/scripts/devforgeai_cli/feedback/skip_tracker.py +177 -0
  40. package/src/claude/scripts/devforgeai_cli/feedback/skip_tracking.py +221 -0
  41. package/src/claude/scripts/devforgeai_cli/feedback/template_engine.py +549 -0
  42. package/src/claude/scripts/devforgeai_cli/feedback/validation.py +163 -0
  43. package/src/claude/scripts/devforgeai_cli/headless/__init__.py +30 -0
  44. package/src/claude/scripts/devforgeai_cli/headless/answer_models.py +206 -0
  45. package/src/claude/scripts/devforgeai_cli/headless/answer_resolver.py +204 -0
  46. package/src/claude/scripts/devforgeai_cli/headless/exceptions.py +36 -0
  47. package/src/claude/scripts/devforgeai_cli/headless/pattern_matcher.py +156 -0
  48. package/src/claude/scripts/devforgeai_cli/hooks.py +313 -0
  49. package/src/claude/scripts/devforgeai_cli/metrics/__init__.py +46 -0
  50. package/src/claude/scripts/devforgeai_cli/metrics/command_metrics.py +142 -0
  51. package/src/claude/scripts/devforgeai_cli/metrics/failure_modes.py +152 -0
  52. package/src/claude/scripts/devforgeai_cli/metrics/story_segmentation.py +181 -0
  53. package/src/claude/scripts/devforgeai_cli/orchestrate_hooks.py +780 -0
  54. package/src/claude/scripts/devforgeai_cli/phase_state.py +1229 -0
  55. package/src/claude/scripts/devforgeai_cli/session/__init__.py +30 -0
  56. package/src/claude/scripts/devforgeai_cli/session/checkpoint.py +268 -0
  57. package/src/claude/scripts/devforgeai_cli/tests/__init__.py +1 -0
  58. package/src/claude/scripts/devforgeai_cli/tests/conftest.py +29 -0
  59. package/src/claude/scripts/devforgeai_cli/tests/feedback/TEST_EXECUTION_GUIDE.md +298 -0
  60. package/src/claude/scripts/devforgeai_cli/tests/feedback/__init__.py +3 -0
  61. package/src/claude/scripts/devforgeai_cli/tests/feedback/test_adaptive_questioning_engine.py +2171 -0
  62. package/src/claude/scripts/devforgeai_cli/tests/feedback/test_aggregation.py +476 -0
  63. package/src/claude/scripts/devforgeai_cli/tests/feedback/test_config_defaults.py +133 -0
  64. package/src/claude/scripts/devforgeai_cli/tests/feedback/test_config_manager.py +592 -0
  65. package/src/claude/scripts/devforgeai_cli/tests/feedback/test_config_models.py +373 -0
  66. package/src/claude/scripts/devforgeai_cli/tests/feedback/test_config_schema.py +130 -0
  67. package/src/claude/scripts/devforgeai_cli/tests/feedback/test_configuration_management.py +1355 -0
  68. package/src/claude/scripts/devforgeai_cli/tests/feedback/test_edge_cases.py +308 -0
  69. package/src/claude/scripts/devforgeai_cli/tests/feedback/test_feature_flag.py +307 -0
  70. package/src/claude/scripts/devforgeai_cli/tests/feedback/test_feedback_indexer.py +384 -0
  71. package/src/claude/scripts/devforgeai_cli/tests/feedback/test_hot_reload.py +580 -0
  72. package/src/claude/scripts/devforgeai_cli/tests/feedback/test_integration.py +402 -0
  73. package/src/claude/scripts/devforgeai_cli/tests/feedback/test_models.py +105 -0
  74. package/src/claude/scripts/devforgeai_cli/tests/feedback/test_question_routing.py +262 -0
  75. package/src/claude/scripts/devforgeai_cli/tests/feedback/test_retrospective.py +333 -0
  76. package/src/claude/scripts/devforgeai_cli/tests/feedback/test_skip_tracker.py +410 -0
  77. package/src/claude/scripts/devforgeai_cli/tests/feedback/test_skip_tracking.py +159 -0
  78. package/src/claude/scripts/devforgeai_cli/tests/feedback/test_skip_tracking_integration.py +1155 -0
  79. package/src/claude/scripts/devforgeai_cli/tests/feedback/test_template_engine.py +1389 -0
  80. package/src/claude/scripts/devforgeai_cli/tests/feedback/test_validation_comprehensive.py +210 -0
  81. package/src/claude/scripts/devforgeai_cli/tests/fixtures/autonomous-deferral-story.md +46 -0
  82. package/src/claude/scripts/devforgeai_cli/tests/fixtures/missing-impl-notes.md +31 -0
  83. package/src/claude/scripts/devforgeai_cli/tests/fixtures/valid-deferral-story.md +46 -0
  84. package/src/claude/scripts/devforgeai_cli/tests/fixtures/valid-story-complete.md +48 -0
  85. package/src/claude/scripts/devforgeai_cli/tests/manual_test_invoke_hooks.sh +200 -0
  86. package/src/claude/scripts/devforgeai_cli/tests/session/DELIVERABLES.md +518 -0
  87. package/src/claude/scripts/devforgeai_cli/tests/session/TEST_SUMMARY.md +468 -0
  88. package/src/claude/scripts/devforgeai_cli/tests/session/__init__.py +6 -0
  89. package/src/claude/scripts/devforgeai_cli/tests/session/fixtures/corrupted-checkpoint.json +1 -0
  90. package/src/claude/scripts/devforgeai_cli/tests/session/fixtures/missing-fields-checkpoint.json +4 -0
  91. package/src/claude/scripts/devforgeai_cli/tests/session/fixtures/valid-checkpoint.json +15 -0
  92. package/src/claude/scripts/devforgeai_cli/tests/session/test_checkpoint.py +851 -0
  93. package/src/claude/scripts/devforgeai_cli/tests/test_check_hooks.py +1886 -0
  94. package/src/claude/scripts/devforgeai_cli/tests/test_depends_on_normalizer.py +171 -0
  95. package/src/claude/scripts/devforgeai_cli/tests/test_dod_validator.py +97 -0
  96. package/src/claude/scripts/devforgeai_cli/tests/test_invoke_hooks.py +1902 -0
  97. package/src/claude/scripts/devforgeai_cli/tests/test_phase_commands.py +320 -0
  98. package/src/claude/scripts/devforgeai_cli/tests/test_phase_commands_error_handling.py +1021 -0
  99. package/src/claude/scripts/devforgeai_cli/tests/test_phase_commands_import.py +697 -0
  100. package/src/claude/scripts/devforgeai_cli/tests/test_phase_state.py +2187 -0
  101. package/src/claude/scripts/devforgeai_cli/tests/test_skip_tracking.py +2141 -0
  102. package/src/claude/scripts/devforgeai_cli/tests/test_skip_tracking_coverage_gap.py +195 -0
  103. package/src/claude/scripts/devforgeai_cli/tests/test_subagent_enforcement.py +539 -0
  104. package/src/claude/scripts/devforgeai_cli/tests/test_validate_installation.py +361 -0
  105. package/src/claude/scripts/devforgeai_cli/utils/__init__.py +11 -0
  106. package/src/claude/scripts/devforgeai_cli/utils/depends_on_normalizer.py +149 -0
  107. package/src/claude/scripts/devforgeai_cli/utils/markdown_parser.py +219 -0
  108. package/src/claude/scripts/devforgeai_cli/utils/story_analyzer.py +249 -0
  109. package/src/claude/scripts/devforgeai_cli/utils/yaml_parser.py +152 -0
  110. package/src/claude/scripts/devforgeai_cli/validators/__init__.py +27 -0
  111. package/src/claude/scripts/devforgeai_cli/validators/ast_grep_validator.py +373 -0
  112. package/src/claude/scripts/devforgeai_cli/validators/context_validator.py +180 -0
  113. package/src/claude/scripts/devforgeai_cli/validators/dod_validator.py +309 -0
  114. package/src/claude/scripts/devforgeai_cli/validators/git_validator.py +107 -0
  115. package/src/claude/scripts/devforgeai_cli/validators/grep_fallback.py +300 -0
  116. package/src/claude/scripts/install_hooks.sh +186 -0
  117. package/src/claude/scripts/invoke_feedback_hooks.sh +59 -0
  118. package/src/claude/scripts/migrate-ac-headers.sh +122 -0
  119. package/src/claude/scripts/plan_file_kb.sh +704 -0
  120. package/src/claude/scripts/requirements.txt +8 -0
  121. package/src/claude/scripts/session_catalog.sh +543 -0
  122. package/src/claude/scripts/setup.py +55 -0
  123. package/src/claude/scripts/start-devforgeai.sh +16 -0
  124. package/src/claude/scripts/statusline.sh +27 -0
  125. package/src/claude/scripts/validate_deferrals.py +344 -0
  126. package/src/claude/skills/devforgeai-qa/SKILL.md +1 -1
  127. package/src/claude/skills/researching-market/SKILL.md +2 -1
  128. package/src/cli/lib/copier.js +13 -1
  129. package/src/claude/skills/designing-systems/scripts/__pycache__/detect_anti_patterns.cpython-312.pyc +0 -0
  130. package/src/claude/skills/designing-systems/scripts/__pycache__/validate_all_context.cpython-312.pyc +0 -0
  131. package/src/claude/skills/designing-systems/scripts/__pycache__/validate_architecture.cpython-312.pyc +0 -0
  132. package/src/claude/skills/designing-systems/scripts/__pycache__/validate_dependencies.cpython-312.pyc +0 -0
  133. package/src/claude/skills/devforgeai-story-creation/scripts/__pycache__/migrate_story_v1_to_v2.cpython-312.pyc +0 -0
  134. package/src/claude/skills/devforgeai-story-creation/scripts/tests/__pycache__/measure_accuracy.cpython-312.pyc +0 -0
@@ -0,0 +1,361 @@
1
+ """
2
+ Tests for validate-installation command (STORY-314).
3
+
4
+ Validates post-install checks for DevForgeAI projects.
5
+
6
+ 6 Checks:
7
+ 1. CLI availability (devforgeai-validate --version)
8
+ 2. Context files (6 files in devforgeai/specs/context/)
9
+ 3. Hook installation (.git/hooks/pre-commit exists)
10
+ 4. PYTHONPATH configuration (CLI imports succeed)
11
+ 5. Git repository (.git/ exists)
12
+ 6. Settings file (.claude/settings.json exists)
13
+ """
14
+
15
+ import pytest
16
+ from pathlib import Path
17
+ from unittest.mock import patch, MagicMock
18
+ import subprocess
19
+
20
+
21
+ # =============================================================================
22
+ # AC#1: All 6 checks pass on valid installation
23
+ # =============================================================================
24
+
25
+ class TestValidInstallation:
26
+ """Tests for AC#1: All 6 checks pass on valid installation."""
27
+
28
+ def test_validate_installation_all_checks_pass_returns_zero(self, tmp_path):
29
+ """Test: Valid installation returns exit code 0."""
30
+ # Arrange: Create valid installation structure
31
+ _create_valid_installation(tmp_path)
32
+
33
+ # Act
34
+ from devforgeai_cli.commands.validate_installation import validate_installation_command
35
+ exit_code = validate_installation_command(project_root=str(tmp_path))
36
+
37
+ # Assert
38
+ assert exit_code == 0
39
+
40
+ def test_validate_installation_reports_6_of_6_passed(self, tmp_path, capsys):
41
+ """Test: Valid installation reports '6/6 checks passed'."""
42
+ # Arrange
43
+ _create_valid_installation(tmp_path)
44
+
45
+ # Act
46
+ from devforgeai_cli.commands.validate_installation import validate_installation_command
47
+ validate_installation_command(project_root=str(tmp_path))
48
+ captured = capsys.readouterr()
49
+
50
+ # Assert
51
+ assert "6/6" in captured.out or "PASS" in captured.out
52
+
53
+ def test_validate_installation_check_cli_available_success(self, tmp_path):
54
+ """Test: CLI availability check passes when devforgeai-validate exists."""
55
+ # Arrange
56
+ _create_valid_installation(tmp_path)
57
+
58
+ # Act
59
+ from devforgeai_cli.commands.validate_installation import check_cli_available
60
+ result = check_cli_available()
61
+
62
+ # Assert
63
+ assert result["passed"] is True
64
+ assert "devforgeai-validate" in result["message"]
65
+
66
+ def test_validate_installation_check_context_files_success(self, tmp_path):
67
+ """Test: Context files check passes when all 6 files exist."""
68
+ # Arrange
69
+ _create_valid_installation(tmp_path)
70
+
71
+ # Act
72
+ from devforgeai_cli.commands.validate_installation import check_context_files
73
+ result = check_context_files(project_root=str(tmp_path))
74
+
75
+ # Assert
76
+ assert result["passed"] is True
77
+ assert "6/6" in result["message"]
78
+
79
+ def test_validate_installation_check_hooks_success(self, tmp_path):
80
+ """Test: Hook check passes when .git/hooks/pre-commit exists."""
81
+ # Arrange
82
+ _create_valid_installation(tmp_path)
83
+
84
+ # Act
85
+ from devforgeai_cli.commands.validate_installation import check_hooks_installed
86
+ result = check_hooks_installed(project_root=str(tmp_path))
87
+
88
+ # Assert
89
+ assert result["passed"] is True
90
+
91
+ def test_validate_installation_check_pythonpath_success(self):
92
+ """Test: PYTHONPATH check passes when CLI imports succeed."""
93
+ # Act
94
+ from devforgeai_cli.commands.validate_installation import check_pythonpath
95
+ result = check_pythonpath()
96
+
97
+ # Assert
98
+ assert result["passed"] is True
99
+
100
+ def test_validate_installation_check_git_repo_success(self, tmp_path):
101
+ """Test: Git check passes when .git/ directory exists."""
102
+ # Arrange
103
+ _create_valid_installation(tmp_path)
104
+
105
+ # Act
106
+ from devforgeai_cli.commands.validate_installation import check_git_repository
107
+ result = check_git_repository(project_root=str(tmp_path))
108
+
109
+ # Assert
110
+ assert result["passed"] is True
111
+
112
+ def test_validate_installation_check_settings_success(self, tmp_path):
113
+ """Test: Settings check passes when .claude/settings.json exists."""
114
+ # Arrange
115
+ _create_valid_installation(tmp_path)
116
+
117
+ # Act
118
+ from devforgeai_cli.commands.validate_installation import check_settings_file
119
+ result = check_settings_file(project_root=str(tmp_path))
120
+
121
+ # Assert
122
+ assert result["passed"] is True
123
+
124
+
125
+ # =============================================================================
126
+ # AC#2: Clear error for incomplete installation
127
+ # =============================================================================
128
+
129
+ class TestIncompleteInstallation:
130
+ """Tests for AC#2: Clear error for incomplete installation."""
131
+
132
+ def test_validate_installation_missing_context_files_returns_nonzero(self, tmp_path):
133
+ """Test: Missing context files returns non-zero exit code."""
134
+ # Arrange: Create installation missing context files
135
+ _create_partial_installation(tmp_path, skip_context=True)
136
+
137
+ # Act
138
+ from devforgeai_cli.commands.validate_installation import validate_installation_command
139
+ exit_code = validate_installation_command(project_root=str(tmp_path))
140
+
141
+ # Assert
142
+ assert exit_code != 0
143
+
144
+ def test_validate_installation_missing_hooks_returns_nonzero(self, tmp_path):
145
+ """Test: Missing hooks returns non-zero exit code."""
146
+ # Arrange
147
+ _create_partial_installation(tmp_path, skip_hooks=True)
148
+
149
+ # Act
150
+ from devforgeai_cli.commands.validate_installation import validate_installation_command
151
+ exit_code = validate_installation_command(project_root=str(tmp_path))
152
+
153
+ # Assert
154
+ assert exit_code != 0
155
+
156
+ def test_validate_installation_missing_git_returns_nonzero(self, tmp_path):
157
+ """Test: Missing .git/ returns non-zero exit code."""
158
+ # Arrange
159
+ _create_partial_installation(tmp_path, skip_git=True)
160
+
161
+ # Act
162
+ from devforgeai_cli.commands.validate_installation import validate_installation_command
163
+ exit_code = validate_installation_command(project_root=str(tmp_path))
164
+
165
+ # Assert
166
+ assert exit_code != 0
167
+
168
+ def test_validate_installation_error_includes_to_fix_instruction(self, tmp_path, capsys):
169
+ """Test: Error output includes 'To fix:' instructions."""
170
+ # Arrange
171
+ _create_partial_installation(tmp_path, skip_context=True)
172
+
173
+ # Act
174
+ from devforgeai_cli.commands.validate_installation import validate_installation_command
175
+ validate_installation_command(project_root=str(tmp_path))
176
+ captured = capsys.readouterr()
177
+
178
+ # Assert
179
+ assert "To fix:" in captured.out or "to fix" in captured.out.lower()
180
+
181
+ def test_validate_installation_error_lists_missing_files(self, tmp_path, capsys):
182
+ """Test: Error output lists which files are missing."""
183
+ # Arrange
184
+ _create_partial_installation(tmp_path, skip_context=True)
185
+
186
+ # Act
187
+ from devforgeai_cli.commands.validate_installation import validate_installation_command
188
+ validate_installation_command(project_root=str(tmp_path))
189
+ captured = capsys.readouterr()
190
+
191
+ # Assert: Should mention at least one missing context file
192
+ assert any(f in captured.out for f in [
193
+ "tech-stack.md", "source-tree.md", "dependencies.md",
194
+ "coding-standards.md", "architecture-constraints.md", "anti-patterns.md"
195
+ ]) or "Missing" in captured.out
196
+
197
+ def test_validate_installation_check_context_files_lists_missing(self, tmp_path):
198
+ """Test: Context check result includes list of missing files."""
199
+ # Arrange
200
+ context_dir = tmp_path / "devforgeai" / "specs" / "context"
201
+ context_dir.mkdir(parents=True)
202
+ (context_dir / "tech-stack.md").write_text("# Tech Stack")
203
+ # Only 1 of 6 files created
204
+
205
+ # Act
206
+ from devforgeai_cli.commands.validate_installation import check_context_files
207
+ result = check_context_files(project_root=str(tmp_path))
208
+
209
+ # Assert
210
+ assert result["passed"] is False
211
+ assert "missing" in result.get("details", "").lower() or len(result.get("missing", [])) > 0
212
+
213
+
214
+ # =============================================================================
215
+ # Business Rules: BR-001, BR-002, BR-003
216
+ # =============================================================================
217
+
218
+ class TestBusinessRules:
219
+ """Tests for business rules from technical specification."""
220
+
221
+ def test_br001_each_check_returns_pass_fail_with_reason(self, tmp_path):
222
+ """BR-001: Each check must return pass/fail with clear reason."""
223
+ # Arrange
224
+ _create_valid_installation(tmp_path)
225
+
226
+ # Act
227
+ from devforgeai_cli.commands.validate_installation import check_git_repository
228
+ result = check_git_repository(project_root=str(tmp_path))
229
+
230
+ # Assert
231
+ assert "passed" in result
232
+ assert "message" in result
233
+ assert isinstance(result["passed"], bool)
234
+ assert isinstance(result["message"], str)
235
+
236
+ def test_br002_failed_check_suggests_fix(self, tmp_path):
237
+ """BR-002: Actionable fix must be suggested for each failure."""
238
+ # Arrange: No .git directory
239
+ tmp_path.mkdir(exist_ok=True)
240
+
241
+ # Act
242
+ from devforgeai_cli.commands.validate_installation import check_git_repository
243
+ result = check_git_repository(project_root=str(tmp_path))
244
+
245
+ # Assert
246
+ assert result["passed"] is False
247
+ assert "fix" in result.get("fix", "").lower() or "To fix" in result.get("message", "")
248
+
249
+ def test_br003_exit_code_zero_only_when_all_pass(self, tmp_path):
250
+ """BR-003: Exit code 0 only if all checks pass."""
251
+ # Arrange: Valid installation
252
+ _create_valid_installation(tmp_path)
253
+
254
+ # Act
255
+ from devforgeai_cli.commands.validate_installation import validate_installation_command
256
+ exit_code = validate_installation_command(project_root=str(tmp_path))
257
+
258
+ # Assert
259
+ assert exit_code == 0
260
+
261
+ def test_br003_exit_code_nonzero_when_any_fails(self, tmp_path):
262
+ """BR-003: Any failure results in non-zero exit code."""
263
+ # Arrange: Missing one component
264
+ _create_partial_installation(tmp_path, skip_settings=True)
265
+
266
+ # Act
267
+ from devforgeai_cli.commands.validate_installation import validate_installation_command
268
+ exit_code = validate_installation_command(project_root=str(tmp_path))
269
+
270
+ # Assert
271
+ assert exit_code != 0
272
+
273
+
274
+ # =============================================================================
275
+ # CLI Integration Tests
276
+ # =============================================================================
277
+
278
+ class TestCLIIntegration:
279
+ """Tests for CLI entry point integration."""
280
+
281
+ def test_cli_help_includes_validate_installation(self, capsys):
282
+ """Test: CLI --help lists validate-installation command."""
283
+ # This tests that cli.py was updated with the new subcommand
284
+ import sys
285
+ from io import StringIO
286
+
287
+ with patch.object(sys, 'argv', ['devforgeai-validate', '--help']):
288
+ with pytest.raises(SystemExit) as exc_info:
289
+ from devforgeai_cli.cli import main
290
+ main()
291
+
292
+ # Should exit 0 (help displayed successfully)
293
+ assert exc_info.value.code == 0
294
+
295
+ captured = capsys.readouterr()
296
+ assert "validate-installation" in captured.out
297
+
298
+
299
+ # =============================================================================
300
+ # Helper Functions
301
+ # =============================================================================
302
+
303
+ def _create_valid_installation(path: Path):
304
+ """Create a complete valid DevForgeAI installation structure."""
305
+ # Git directory
306
+ git_dir = path / ".git"
307
+ git_dir.mkdir(parents=True)
308
+
309
+ # Git hooks
310
+ hooks_dir = git_dir / "hooks"
311
+ hooks_dir.mkdir()
312
+ (hooks_dir / "pre-commit").write_text("#!/bin/bash\nexit 0")
313
+
314
+ # Context files (6 files)
315
+ context_dir = path / "devforgeai" / "specs" / "context"
316
+ context_dir.mkdir(parents=True)
317
+ for filename in [
318
+ "tech-stack.md", "source-tree.md", "dependencies.md",
319
+ "coding-standards.md", "architecture-constraints.md", "anti-patterns.md"
320
+ ]:
321
+ (context_dir / filename).write_text(f"# {filename}")
322
+
323
+ # Settings file
324
+ claude_dir = path / ".claude"
325
+ claude_dir.mkdir(exist_ok=True)
326
+ (claude_dir / "settings.json").write_text("{}")
327
+
328
+
329
+ def _create_partial_installation(
330
+ path: Path,
331
+ skip_context: bool = False,
332
+ skip_hooks: bool = False,
333
+ skip_git: bool = False,
334
+ skip_settings: bool = False
335
+ ):
336
+ """Create an incomplete installation for error testing."""
337
+ # Git directory (unless skipped)
338
+ if not skip_git:
339
+ git_dir = path / ".git"
340
+ git_dir.mkdir(parents=True)
341
+
342
+ if not skip_hooks:
343
+ hooks_dir = git_dir / "hooks"
344
+ hooks_dir.mkdir()
345
+ (hooks_dir / "pre-commit").write_text("#!/bin/bash\nexit 0")
346
+
347
+ # Context files (unless skipped)
348
+ if not skip_context:
349
+ context_dir = path / "devforgeai" / "specs" / "context"
350
+ context_dir.mkdir(parents=True)
351
+ for filename in [
352
+ "tech-stack.md", "source-tree.md", "dependencies.md",
353
+ "coding-standards.md", "architecture-constraints.md", "anti-patterns.md"
354
+ ]:
355
+ (context_dir / filename).write_text(f"# {filename}")
356
+
357
+ # Settings file (unless skipped)
358
+ if not skip_settings:
359
+ claude_dir = path / ".claude"
360
+ claude_dir.mkdir(exist_ok=True)
361
+ (claude_dir / "settings.json").write_text("{}")
@@ -0,0 +1,11 @@
1
+ """Utility modules for DevForgeAI CLI."""
2
+
3
+ from .markdown_parser import *
4
+ from .yaml_parser import *
5
+ from .story_analyzer import *
6
+ from .depends_on_normalizer import (
7
+ normalize_depends_on,
8
+ is_valid_story_id,
9
+ validate_depends_on_input,
10
+ STORY_ID_PATTERN
11
+ )
@@ -0,0 +1,149 @@
1
+ #!/usr/bin/env python3
2
+ r"""
3
+ Depends On Field Normalizer
4
+
5
+ Normalizes various input formats for the depends_on field to array format.
6
+ Supports STORY-090 v2.2 template format.
7
+
8
+ Validation: STORY-ID format ^STORY-\d{3,4}$
9
+ """
10
+
11
+ import re
12
+ from typing import List, Tuple, Union
13
+
14
+
15
+ # Valid STORY-ID regex pattern
16
+ STORY_ID_PATTERN = re.compile(r'^STORY-\d{3,4}$')
17
+
18
+
19
+ def normalize_depends_on(value: Union[str, List, None]) -> List[str]:
20
+ """
21
+ Normalize depends_on input to array format.
22
+
23
+ Args:
24
+ value: Input value (string, list, None, or other)
25
+
26
+ Returns:
27
+ List of validated STORY-IDs (empty list if none valid)
28
+
29
+ Examples:
30
+ >>> normalize_depends_on(None)
31
+ []
32
+ >>> normalize_depends_on("")
33
+ []
34
+ >>> normalize_depends_on("STORY-044")
35
+ ['STORY-044']
36
+ >>> normalize_depends_on("STORY-044, STORY-045")
37
+ ['STORY-044', 'STORY-045']
38
+ >>> normalize_depends_on(["STORY-044", "STORY-045"])
39
+ ['STORY-044', 'STORY-045']
40
+ """
41
+ # Handle null/None
42
+ if value is None:
43
+ return []
44
+
45
+ # Handle empty string or "none" variants
46
+ if isinstance(value, str):
47
+ stripped = value.strip().lower()
48
+ if stripped in ('', 'none', 'null', '[]'):
49
+ return []
50
+ return _parse_string_input(value)
51
+
52
+ # Handle list input
53
+ if isinstance(value, list):
54
+ return _validate_list(value)
55
+
56
+ # Unknown type - return empty
57
+ return []
58
+
59
+
60
+ def _parse_string_input(value: str) -> List[str]:
61
+ """Parse comma/space-separated string into validated list."""
62
+ parts = re.split(r'[,\s]+', value.strip())
63
+ validated = []
64
+ for part in parts:
65
+ cleaned = part.strip().upper()
66
+ if cleaned and is_valid_story_id(cleaned):
67
+ validated.append(cleaned)
68
+ return validated
69
+
70
+
71
+ def _validate_list(values: List) -> List[str]:
72
+ """Validate list of values, filtering invalid entries."""
73
+ validated = []
74
+ for value in values:
75
+ if isinstance(value, str):
76
+ cleaned = value.strip().upper()
77
+ if is_valid_story_id(cleaned):
78
+ validated.append(cleaned)
79
+ return validated
80
+
81
+
82
+ def is_valid_story_id(value: str) -> bool:
83
+ r"""
84
+ Check if string is valid STORY-ID format.
85
+
86
+ Args:
87
+ value: String to validate
88
+
89
+ Returns:
90
+ True if matches ^STORY-\d{3,4}$
91
+ """
92
+ if not value or not isinstance(value, str):
93
+ return False
94
+ return bool(STORY_ID_PATTERN.match(value))
95
+
96
+
97
+ def validate_depends_on_input(value: Union[str, List, None]) -> Tuple[List[str], List[str]]:
98
+ """
99
+ Validate input and return both valid and invalid entries.
100
+
101
+ Args:
102
+ value: Input value to validate
103
+
104
+ Returns:
105
+ Tuple of (valid_ids, invalid_entries)
106
+ """
107
+ if value is None:
108
+ return [], []
109
+
110
+ if isinstance(value, str):
111
+ stripped = value.strip().lower()
112
+ if stripped in ('', 'none', 'null', '[]'):
113
+ return [], []
114
+
115
+ parts = re.split(r'[,\s]+', value.strip())
116
+ valid, invalid = [], []
117
+
118
+ for part in parts:
119
+ cleaned = part.strip()
120
+ if cleaned:
121
+ upper = cleaned.upper()
122
+ if is_valid_story_id(upper):
123
+ valid.append(upper)
124
+ else:
125
+ invalid.append(cleaned)
126
+ return valid, invalid
127
+
128
+ if isinstance(value, list):
129
+ valid, invalid = [], []
130
+ for item in value:
131
+ if isinstance(item, str):
132
+ cleaned = item.strip()
133
+ if cleaned:
134
+ upper = cleaned.upper()
135
+ if is_valid_story_id(upper):
136
+ valid.append(upper)
137
+ else:
138
+ invalid.append(cleaned)
139
+ return valid, invalid
140
+
141
+ return [], []
142
+
143
+
144
+ __all__ = [
145
+ 'normalize_depends_on',
146
+ 'is_valid_story_id',
147
+ 'validate_depends_on_input',
148
+ 'STORY_ID_PATTERN'
149
+ ]