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,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
+ ]