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,219 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Markdown Parser for DevForgeAI Story Files
|
|
4
|
+
|
|
5
|
+
Extracts sections, checklists, and content from markdown documents.
|
|
6
|
+
Pattern based on industry research (SpecDriven AI, GitHub DoD Checker).
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import re
|
|
10
|
+
from typing import Any, Dict, List, Optional, Tuple
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def extract_section(content: str, section_name: str) -> Optional[str]:
|
|
14
|
+
"""
|
|
15
|
+
Extract a section from markdown content.
|
|
16
|
+
|
|
17
|
+
Args:
|
|
18
|
+
content: Full markdown content
|
|
19
|
+
section_name: Section header (without ## prefix)
|
|
20
|
+
|
|
21
|
+
Returns:
|
|
22
|
+
Section content (excluding header) or None if not found
|
|
23
|
+
|
|
24
|
+
Example:
|
|
25
|
+
>>> content = "## Introduction\\nText\\n## Details\\nMore text"
|
|
26
|
+
>>> extract_section(content, "Details")
|
|
27
|
+
'More text'
|
|
28
|
+
"""
|
|
29
|
+
# Match section: ## {name} until next ## or end of file
|
|
30
|
+
pattern = rf'^## {re.escape(section_name)}\s*\n(.*?)(?:\n##|\Z)'
|
|
31
|
+
match = re.search(pattern, content, re.MULTILINE | re.DOTALL)
|
|
32
|
+
|
|
33
|
+
if match:
|
|
34
|
+
return match.group(1).strip()
|
|
35
|
+
return None
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def parse_checklist(section_content: str) -> List[Dict[str, Any]]:
|
|
39
|
+
"""
|
|
40
|
+
Parse markdown checklist items from section content.
|
|
41
|
+
|
|
42
|
+
Args:
|
|
43
|
+
section_content: Content of a section containing checklist items
|
|
44
|
+
|
|
45
|
+
Returns:
|
|
46
|
+
List of dicts with: text, checked, line_number
|
|
47
|
+
|
|
48
|
+
Example:
|
|
49
|
+
>>> content = "- [x] Complete item\\n- [ ] Incomplete item"
|
|
50
|
+
>>> parse_checklist(content)
|
|
51
|
+
[
|
|
52
|
+
{'text': 'Complete item', 'checked': True, 'line_number': 1},
|
|
53
|
+
{'text': 'Incomplete item', 'checked': False, 'line_number': 2}
|
|
54
|
+
]
|
|
55
|
+
"""
|
|
56
|
+
items = []
|
|
57
|
+
|
|
58
|
+
# Match: - [x] or - [ ] followed by text
|
|
59
|
+
pattern = r'^-\s*\[([ x])\]\s*(.+)$'
|
|
60
|
+
|
|
61
|
+
for line_num, line in enumerate(section_content.split('\n'), start=1):
|
|
62
|
+
match = re.match(pattern, line.strip())
|
|
63
|
+
if match:
|
|
64
|
+
checkbox_char = match.group(1)
|
|
65
|
+
item_text = match.group(2).strip()
|
|
66
|
+
|
|
67
|
+
items.append({
|
|
68
|
+
'text': item_text,
|
|
69
|
+
'checked': checkbox_char == 'x',
|
|
70
|
+
'line_number': line_num,
|
|
71
|
+
'raw_line': line
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
return items
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def extract_all_sections(content: str) -> Dict[str, str]:
|
|
78
|
+
"""
|
|
79
|
+
Extract all ## sections from markdown.
|
|
80
|
+
|
|
81
|
+
Args:
|
|
82
|
+
content: Full markdown content
|
|
83
|
+
|
|
84
|
+
Returns:
|
|
85
|
+
Dict mapping section names to section content
|
|
86
|
+
"""
|
|
87
|
+
sections = {}
|
|
88
|
+
|
|
89
|
+
# Find all ## headers
|
|
90
|
+
pattern = r'^## (.+?)\s*$'
|
|
91
|
+
matches = list(re.finditer(pattern, content, re.MULTILINE))
|
|
92
|
+
|
|
93
|
+
for i, match in enumerate(matches):
|
|
94
|
+
section_name = match.group(1).strip()
|
|
95
|
+
start = match.end()
|
|
96
|
+
|
|
97
|
+
# End is next section or end of file
|
|
98
|
+
if i + 1 < len(matches):
|
|
99
|
+
end = matches[i + 1].start()
|
|
100
|
+
else:
|
|
101
|
+
end = len(content)
|
|
102
|
+
|
|
103
|
+
section_content = content[start:end].strip()
|
|
104
|
+
sections[section_name] = section_content
|
|
105
|
+
|
|
106
|
+
return sections
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def find_checklist_item_context(content: str, item_text: str, context_lines: int = 3) -> Optional[str]:
|
|
110
|
+
"""
|
|
111
|
+
Find checklist item and return surrounding context.
|
|
112
|
+
|
|
113
|
+
Args:
|
|
114
|
+
content: Full markdown content
|
|
115
|
+
item_text: Text of the checklist item to find
|
|
116
|
+
context_lines: Number of lines before/after to include
|
|
117
|
+
|
|
118
|
+
Returns:
|
|
119
|
+
Context around the item or None if not found
|
|
120
|
+
"""
|
|
121
|
+
lines = content.split('\n')
|
|
122
|
+
|
|
123
|
+
for i, line in enumerate(lines):
|
|
124
|
+
if item_text in line and re.match(r'^-\s*\[([ x])\]', line.strip()):
|
|
125
|
+
# Found the item, extract context
|
|
126
|
+
start = max(0, i - context_lines)
|
|
127
|
+
end = min(len(lines), i + context_lines + 1)
|
|
128
|
+
|
|
129
|
+
context = '\n'.join(lines[start:end])
|
|
130
|
+
return context
|
|
131
|
+
|
|
132
|
+
return None
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def extract_item_justification(impl_notes_content: str, item_text: str) -> Optional[str]:
|
|
136
|
+
"""
|
|
137
|
+
Extract justification text following a checklist item.
|
|
138
|
+
|
|
139
|
+
Args:
|
|
140
|
+
impl_notes_content: Implementation Notes section content
|
|
141
|
+
item_text: The checklist item to find justification for
|
|
142
|
+
|
|
143
|
+
Returns:
|
|
144
|
+
Justification text (lines following item until next item/section) or None
|
|
145
|
+
|
|
146
|
+
Example:
|
|
147
|
+
>>> content = '''
|
|
148
|
+
... - [ ] Item 1 - Deferred to STORY-042
|
|
149
|
+
... **User Approved:** YES
|
|
150
|
+
... **Rationale:** Complexity
|
|
151
|
+
... - [x] Item 2 - Completed
|
|
152
|
+
... '''
|
|
153
|
+
>>> extract_item_justification(content, "Item 1")
|
|
154
|
+
'Deferred to STORY-042\\n**User Approved:** YES\\n**Rationale:** Complexity'
|
|
155
|
+
"""
|
|
156
|
+
lines = impl_notes_content.split('\n')
|
|
157
|
+
|
|
158
|
+
for i, line in enumerate(lines):
|
|
159
|
+
# Find the item
|
|
160
|
+
if item_text in line and re.match(r'^-\s*\[([ x])\]', line.strip()):
|
|
161
|
+
# Extract text from this line until next checklist item
|
|
162
|
+
justification_lines = []
|
|
163
|
+
|
|
164
|
+
# Get remainder of current line after checkbox
|
|
165
|
+
match = re.match(r'^-\s*\[([ x])\]\s*(.+)$', line.strip())
|
|
166
|
+
if match:
|
|
167
|
+
remainder = match.group(2).strip()
|
|
168
|
+
# Remove item text itself, keep justification part
|
|
169
|
+
if ' - ' in remainder:
|
|
170
|
+
parts = remainder.split(' - ', 1)
|
|
171
|
+
if len(parts) > 1:
|
|
172
|
+
justification_lines.append(parts[1])
|
|
173
|
+
|
|
174
|
+
# Get following lines until next item or section
|
|
175
|
+
for j in range(i + 1, len(lines)):
|
|
176
|
+
next_line = lines[j].strip()
|
|
177
|
+
|
|
178
|
+
# Stop at next checklist item or section header
|
|
179
|
+
if re.match(r'^-\s*\[([ x])\]', next_line) or re.match(r'^##', next_line):
|
|
180
|
+
break
|
|
181
|
+
|
|
182
|
+
# Skip empty lines at start
|
|
183
|
+
if not justification_lines and not next_line:
|
|
184
|
+
continue
|
|
185
|
+
|
|
186
|
+
justification_lines.append(lines[j])
|
|
187
|
+
|
|
188
|
+
if justification_lines:
|
|
189
|
+
return '\n'.join(justification_lines).strip()
|
|
190
|
+
|
|
191
|
+
return None
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def count_lines_in_section(content: str, section_name: str) -> int:
|
|
195
|
+
"""
|
|
196
|
+
Count number of lines in a section.
|
|
197
|
+
|
|
198
|
+
Args:
|
|
199
|
+
content: Full markdown content
|
|
200
|
+
section_name: Name of section
|
|
201
|
+
|
|
202
|
+
Returns:
|
|
203
|
+
Number of lines in section
|
|
204
|
+
"""
|
|
205
|
+
section = extract_section(content, section_name)
|
|
206
|
+
if section:
|
|
207
|
+
return len(section.split('\n'))
|
|
208
|
+
return 0
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
# Module exports
|
|
212
|
+
__all__ = [
|
|
213
|
+
'extract_section',
|
|
214
|
+
'parse_checklist',
|
|
215
|
+
'extract_all_sections',
|
|
216
|
+
'find_checklist_item_context',
|
|
217
|
+
'extract_item_justification',
|
|
218
|
+
'count_lines_in_section'
|
|
219
|
+
]
|
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Story Analyzer for DevForgeAI Story Files
|
|
4
|
+
|
|
5
|
+
High-level analysis functions for story validation.
|
|
6
|
+
Combines markdown and YAML parsing for story-specific operations.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import re
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import Dict, List, Optional, Tuple
|
|
12
|
+
|
|
13
|
+
from .markdown_parser import (
|
|
14
|
+
extract_section,
|
|
15
|
+
parse_checklist,
|
|
16
|
+
extract_item_justification
|
|
17
|
+
)
|
|
18
|
+
from .yaml_parser import parse_frontmatter, extract_story_id
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def load_story_file(story_path: str) -> Tuple[Optional[Dict], str]:
|
|
22
|
+
"""
|
|
23
|
+
Load story file and parse frontmatter + content.
|
|
24
|
+
|
|
25
|
+
Args:
|
|
26
|
+
story_path: Path to story file
|
|
27
|
+
|
|
28
|
+
Returns:
|
|
29
|
+
Tuple of (frontmatter, content)
|
|
30
|
+
|
|
31
|
+
Raises:
|
|
32
|
+
FileNotFoundError if story doesn't exist
|
|
33
|
+
"""
|
|
34
|
+
path = Path(story_path)
|
|
35
|
+
if not path.exists():
|
|
36
|
+
raise FileNotFoundError(f"Story file not found: {story_path}")
|
|
37
|
+
|
|
38
|
+
content = path.read_text(encoding='utf-8')
|
|
39
|
+
frontmatter, body = parse_frontmatter(content)
|
|
40
|
+
|
|
41
|
+
return frontmatter, content
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def extract_dod_items(story_content: str) -> List[Dict]:
|
|
45
|
+
"""
|
|
46
|
+
Extract all Definition of Done checklist items.
|
|
47
|
+
|
|
48
|
+
Args:
|
|
49
|
+
story_content: Full story file content
|
|
50
|
+
|
|
51
|
+
Returns:
|
|
52
|
+
List of DoD items with text, checked status, line number
|
|
53
|
+
"""
|
|
54
|
+
dod_section = extract_section(story_content, "Definition of Done")
|
|
55
|
+
|
|
56
|
+
if not dod_section:
|
|
57
|
+
return []
|
|
58
|
+
|
|
59
|
+
return parse_checklist(dod_section)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def extract_impl_notes_items(story_content: str) -> List[Dict]:
|
|
63
|
+
"""
|
|
64
|
+
Extract all Implementation Notes checklist items.
|
|
65
|
+
|
|
66
|
+
Args:
|
|
67
|
+
story_content: Full story file content
|
|
68
|
+
|
|
69
|
+
Returns:
|
|
70
|
+
List of Implementation Notes items with text, checked status
|
|
71
|
+
"""
|
|
72
|
+
impl_section = extract_section(story_content, "Implementation Notes")
|
|
73
|
+
|
|
74
|
+
if not impl_section:
|
|
75
|
+
return []
|
|
76
|
+
|
|
77
|
+
# Implementation Notes can have subsections, extract from full section
|
|
78
|
+
return parse_checklist(impl_section)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def find_dod_impl_mismatch(story_content: str) -> List[Dict]:
|
|
82
|
+
"""
|
|
83
|
+
Find mismatches between DoD and Implementation Notes status.
|
|
84
|
+
|
|
85
|
+
Args:
|
|
86
|
+
story_content: Full story file content
|
|
87
|
+
|
|
88
|
+
Returns:
|
|
89
|
+
List of mismatches with item text, dod_checked, impl_checked
|
|
90
|
+
|
|
91
|
+
Example mismatch (autonomous deferral):
|
|
92
|
+
DoD: [x] Item completed
|
|
93
|
+
Impl: [ ] Item - Deferred to STORY-XXX
|
|
94
|
+
"""
|
|
95
|
+
dod_items = extract_dod_items(story_content)
|
|
96
|
+
impl_items = extract_impl_notes_items(story_content)
|
|
97
|
+
|
|
98
|
+
# Create lookup for impl items by text (extract base text before " - ")
|
|
99
|
+
impl_lookup = {}
|
|
100
|
+
for item in impl_items:
|
|
101
|
+
# Extract base text (everything before " - " if present)
|
|
102
|
+
base_text = item['text'].split(' - ')[0].strip()
|
|
103
|
+
impl_lookup[base_text] = item
|
|
104
|
+
|
|
105
|
+
mismatches = []
|
|
106
|
+
|
|
107
|
+
for dod_item in dod_items:
|
|
108
|
+
dod_text = dod_item['text'].strip()
|
|
109
|
+
|
|
110
|
+
# Try exact match first
|
|
111
|
+
impl_item = impl_lookup.get(dod_text)
|
|
112
|
+
|
|
113
|
+
# If no exact match, try partial match
|
|
114
|
+
if not impl_item:
|
|
115
|
+
for impl_key, impl_val in impl_lookup.items():
|
|
116
|
+
# Match if either contains the other (handles variations)
|
|
117
|
+
if (impl_key in dod_text or dod_text in impl_key) and len(impl_key) > 5:
|
|
118
|
+
impl_item = impl_val
|
|
119
|
+
break
|
|
120
|
+
|
|
121
|
+
if not impl_item:
|
|
122
|
+
# DoD item not found in Implementation Notes
|
|
123
|
+
mismatches.append({
|
|
124
|
+
'item': dod_text,
|
|
125
|
+
'dod_checked': dod_item['checked'],
|
|
126
|
+
'impl_found': False,
|
|
127
|
+
'impl_checked': None
|
|
128
|
+
})
|
|
129
|
+
elif dod_item['checked'] != impl_item['checked']:
|
|
130
|
+
# Status mismatch
|
|
131
|
+
mismatches.append({
|
|
132
|
+
'item': dod_text,
|
|
133
|
+
'dod_checked': dod_item['checked'],
|
|
134
|
+
'impl_found': True,
|
|
135
|
+
'impl_checked': impl_item['checked']
|
|
136
|
+
})
|
|
137
|
+
|
|
138
|
+
return mismatches
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def check_user_approval_marker(justification_text: str) -> Tuple[bool, Optional[str]]:
|
|
142
|
+
"""
|
|
143
|
+
Check if justification contains user approval marker.
|
|
144
|
+
|
|
145
|
+
Args:
|
|
146
|
+
justification_text: Text following deferred item
|
|
147
|
+
|
|
148
|
+
Returns:
|
|
149
|
+
Tuple of (has_approval, marker_type)
|
|
150
|
+
marker_type: "user_approved", "story_reference", "adr_reference", "external_blocker", None
|
|
151
|
+
|
|
152
|
+
Valid approval markers:
|
|
153
|
+
- "User approved:" or "User Approved:"
|
|
154
|
+
- STORY-XXX reference
|
|
155
|
+
- ADR-XXX reference
|
|
156
|
+
- "Blocked by: ... (external)"
|
|
157
|
+
"""
|
|
158
|
+
if not justification_text:
|
|
159
|
+
return False, None
|
|
160
|
+
|
|
161
|
+
# Check for explicit user approval
|
|
162
|
+
if re.search(r'User [Aa]pproved:', justification_text):
|
|
163
|
+
return True, "user_approved"
|
|
164
|
+
|
|
165
|
+
# Check for AskUserQuestion mention
|
|
166
|
+
if 'AskUserQuestion' in justification_text:
|
|
167
|
+
return True, "user_approved"
|
|
168
|
+
|
|
169
|
+
# Check for story reference
|
|
170
|
+
if re.search(r'STORY-\d+', justification_text):
|
|
171
|
+
return True, "story_reference"
|
|
172
|
+
|
|
173
|
+
# Check for ADR reference
|
|
174
|
+
if re.search(r'ADR-\d+', justification_text):
|
|
175
|
+
return True, "adr_reference"
|
|
176
|
+
|
|
177
|
+
# Check for external blocker
|
|
178
|
+
if 'Blocked by:' in justification_text and '(external)' in justification_text:
|
|
179
|
+
return True, "external_blocker"
|
|
180
|
+
|
|
181
|
+
return False, None
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def extract_story_references(text: str) -> List[str]:
|
|
185
|
+
"""
|
|
186
|
+
Extract all STORY-XXX references from text.
|
|
187
|
+
|
|
188
|
+
Args:
|
|
189
|
+
text: Text to search
|
|
190
|
+
|
|
191
|
+
Returns:
|
|
192
|
+
List of story IDs (e.g., ["STORY-042", "STORY-100"])
|
|
193
|
+
"""
|
|
194
|
+
return re.findall(r'STORY-\d+', text)
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
def extract_adr_references(text: str) -> List[str]:
|
|
198
|
+
"""
|
|
199
|
+
Extract all ADR-XXX references from text.
|
|
200
|
+
|
|
201
|
+
Args:
|
|
202
|
+
text: Text to search
|
|
203
|
+
|
|
204
|
+
Returns:
|
|
205
|
+
List of ADR IDs (e.g., ["ADR-001", "ADR-023"])
|
|
206
|
+
"""
|
|
207
|
+
return re.findall(r'ADR-\d+', text)
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
def has_implementation_notes(story_content: str) -> bool:
|
|
211
|
+
"""
|
|
212
|
+
Check if story has Implementation Notes section.
|
|
213
|
+
|
|
214
|
+
Args:
|
|
215
|
+
story_content: Full story file content
|
|
216
|
+
|
|
217
|
+
Returns:
|
|
218
|
+
True if Implementation Notes section exists
|
|
219
|
+
"""
|
|
220
|
+
impl_section = extract_section(story_content, "Implementation Notes")
|
|
221
|
+
return impl_section is not None and len(impl_section.strip()) > 0
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
def count_deferred_items(impl_notes_content: str) -> int:
|
|
225
|
+
"""
|
|
226
|
+
Count number of deferred items in Implementation Notes.
|
|
227
|
+
|
|
228
|
+
Args:
|
|
229
|
+
impl_notes_content: Implementation Notes section content
|
|
230
|
+
|
|
231
|
+
Returns:
|
|
232
|
+
Number of items marked [ ] (incomplete/deferred)
|
|
233
|
+
"""
|
|
234
|
+
items = parse_checklist(impl_notes_content)
|
|
235
|
+
return sum(1 for item in items if not item['checked'])
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
# Module exports
|
|
239
|
+
__all__ = [
|
|
240
|
+
'load_story_file',
|
|
241
|
+
'extract_dod_items',
|
|
242
|
+
'extract_impl_notes_items',
|
|
243
|
+
'find_dod_impl_mismatch',
|
|
244
|
+
'check_user_approval_marker',
|
|
245
|
+
'extract_story_references',
|
|
246
|
+
'extract_adr_references',
|
|
247
|
+
'has_implementation_notes',
|
|
248
|
+
'count_deferred_items'
|
|
249
|
+
]
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
YAML Parser for DevForgeAI Story Files
|
|
4
|
+
|
|
5
|
+
Extracts and validates YAML frontmatter from markdown documents.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import re
|
|
9
|
+
import yaml
|
|
10
|
+
from typing import Dict, List, Optional, Tuple
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def parse_frontmatter(content: str) -> Tuple[Optional[Dict], str]:
|
|
14
|
+
"""
|
|
15
|
+
Extract YAML frontmatter from markdown content.
|
|
16
|
+
|
|
17
|
+
Args:
|
|
18
|
+
content: Full markdown content
|
|
19
|
+
|
|
20
|
+
Returns:
|
|
21
|
+
Tuple of (frontmatter_dict, remaining_content)
|
|
22
|
+
frontmatter_dict is None if no frontmatter found
|
|
23
|
+
|
|
24
|
+
Example:
|
|
25
|
+
>>> content = '''---
|
|
26
|
+
... id: STORY-001
|
|
27
|
+
... title: Test
|
|
28
|
+
... ---
|
|
29
|
+
... # Story content'''
|
|
30
|
+
>>> fm, body = parse_frontmatter(content)
|
|
31
|
+
>>> fm['id']
|
|
32
|
+
'STORY-001'
|
|
33
|
+
"""
|
|
34
|
+
# Match YAML frontmatter: ---\n{yaml}\n---
|
|
35
|
+
pattern = r'^---\s*\n(.*?)\n---\s*\n(.*)$'
|
|
36
|
+
match = re.match(pattern, content, re.DOTALL)
|
|
37
|
+
|
|
38
|
+
if not match:
|
|
39
|
+
return None, content
|
|
40
|
+
|
|
41
|
+
try:
|
|
42
|
+
frontmatter_text = match.group(1)
|
|
43
|
+
remaining_content = match.group(2)
|
|
44
|
+
|
|
45
|
+
frontmatter = yaml.safe_load(frontmatter_text)
|
|
46
|
+
|
|
47
|
+
return frontmatter, remaining_content
|
|
48
|
+
|
|
49
|
+
except yaml.YAMLError as e:
|
|
50
|
+
# Invalid YAML
|
|
51
|
+
raise ValueError(f"Invalid YAML frontmatter: {e}")
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def validate_story_frontmatter(frontmatter: Dict) -> Tuple[bool, List[str]]:
|
|
55
|
+
"""
|
|
56
|
+
Validate DevForgeAI story frontmatter has required fields.
|
|
57
|
+
|
|
58
|
+
Args:
|
|
59
|
+
frontmatter: Parsed YAML frontmatter dict
|
|
60
|
+
|
|
61
|
+
Returns:
|
|
62
|
+
Tuple of (is_valid, error_messages)
|
|
63
|
+
|
|
64
|
+
Required fields for story:
|
|
65
|
+
- id: STORY-XXX format
|
|
66
|
+
- title: Non-empty string
|
|
67
|
+
- status: Valid workflow state
|
|
68
|
+
"""
|
|
69
|
+
errors = []
|
|
70
|
+
|
|
71
|
+
# Required fields
|
|
72
|
+
required_fields = ['id', 'title', 'status']
|
|
73
|
+
|
|
74
|
+
for field in required_fields:
|
|
75
|
+
if field not in frontmatter:
|
|
76
|
+
errors.append(f"Missing required field: {field}")
|
|
77
|
+
elif not frontmatter[field]:
|
|
78
|
+
errors.append(f"Field {field} is empty")
|
|
79
|
+
|
|
80
|
+
# Validate id format
|
|
81
|
+
if 'id' in frontmatter:
|
|
82
|
+
story_id = frontmatter['id']
|
|
83
|
+
if not re.match(r'^STORY-\d+$', story_id):
|
|
84
|
+
errors.append(f"Invalid id format: '{story_id}' (expected STORY-NNN)")
|
|
85
|
+
|
|
86
|
+
# Validate status
|
|
87
|
+
if 'status' in frontmatter:
|
|
88
|
+
valid_statuses = [
|
|
89
|
+
'Backlog', 'Ready for Dev', 'Architecture', 'In Development',
|
|
90
|
+
'Dev Complete', 'QA In Progress', 'QA Approved', 'QA Failed',
|
|
91
|
+
'Releasing', 'Released'
|
|
92
|
+
]
|
|
93
|
+
if frontmatter['status'] not in valid_statuses:
|
|
94
|
+
errors.append(f"Invalid status: '{frontmatter['status']}'")
|
|
95
|
+
|
|
96
|
+
# Validate depends_on format (if present) - STORY-090
|
|
97
|
+
if 'depends_on' in frontmatter:
|
|
98
|
+
depends_on = frontmatter['depends_on']
|
|
99
|
+
if depends_on is not None:
|
|
100
|
+
if not isinstance(depends_on, list):
|
|
101
|
+
errors.append(f"depends_on must be array, got: {type(depends_on).__name__}")
|
|
102
|
+
else:
|
|
103
|
+
from .depends_on_normalizer import is_valid_story_id
|
|
104
|
+
for idx, item in enumerate(depends_on):
|
|
105
|
+
if not is_valid_story_id(str(item)):
|
|
106
|
+
errors.append(f"Invalid depends_on[{idx}]: '{item}'")
|
|
107
|
+
|
|
108
|
+
return len(errors) == 0, errors
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def extract_story_id(content: str) -> Optional[str]:
|
|
112
|
+
"""
|
|
113
|
+
Extract story ID from frontmatter.
|
|
114
|
+
|
|
115
|
+
Args:
|
|
116
|
+
content: Full markdown content
|
|
117
|
+
|
|
118
|
+
Returns:
|
|
119
|
+
Story ID (e.g., "STORY-001") or None
|
|
120
|
+
"""
|
|
121
|
+
frontmatter, _ = parse_frontmatter(content)
|
|
122
|
+
|
|
123
|
+
if frontmatter and 'id' in frontmatter:
|
|
124
|
+
return frontmatter['id']
|
|
125
|
+
|
|
126
|
+
return None
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def has_valid_frontmatter(content: str) -> bool:
|
|
130
|
+
"""
|
|
131
|
+
Check if content has valid YAML frontmatter.
|
|
132
|
+
|
|
133
|
+
Args:
|
|
134
|
+
content: Full markdown content
|
|
135
|
+
|
|
136
|
+
Returns:
|
|
137
|
+
True if valid frontmatter exists
|
|
138
|
+
"""
|
|
139
|
+
try:
|
|
140
|
+
frontmatter, _ = parse_frontmatter(content)
|
|
141
|
+
return frontmatter is not None
|
|
142
|
+
except ValueError:
|
|
143
|
+
return False
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
# Module exports
|
|
147
|
+
__all__ = [
|
|
148
|
+
'parse_frontmatter',
|
|
149
|
+
'validate_story_frontmatter',
|
|
150
|
+
'extract_story_id',
|
|
151
|
+
'has_valid_frontmatter'
|
|
152
|
+
]
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"""Validator modules for DevForgeAI CLI."""
|
|
2
|
+
|
|
3
|
+
from .ast_grep_validator import (
|
|
4
|
+
AstGrepValidator,
|
|
5
|
+
parse_version,
|
|
6
|
+
detect_headless_mode,
|
|
7
|
+
VersionInfo,
|
|
8
|
+
InstallAction
|
|
9
|
+
)
|
|
10
|
+
from .grep_fallback import (
|
|
11
|
+
GrepFallbackAnalyzer,
|
|
12
|
+
GrepPattern,
|
|
13
|
+
Violation,
|
|
14
|
+
log_fallback_warning
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
__all__ = [
|
|
18
|
+
'AstGrepValidator',
|
|
19
|
+
'parse_version',
|
|
20
|
+
'detect_headless_mode',
|
|
21
|
+
'VersionInfo',
|
|
22
|
+
'InstallAction',
|
|
23
|
+
'GrepFallbackAnalyzer',
|
|
24
|
+
'GrepPattern',
|
|
25
|
+
'Violation',
|
|
26
|
+
'log_fallback_warning'
|
|
27
|
+
]
|