ref-agents 1.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (175) hide show
  1. ref_agents/__init__.py +9 -0
  2. ref_agents/api_keys.json.example +8 -0
  3. ref_agents/auth.py +129 -0
  4. ref_agents/codemap/..md +62 -0
  5. ref_agents/codemap/CODE_MAP.md +37 -0
  6. ref_agents/codemap/core.md +43 -0
  7. ref_agents/codemap/models.md +43 -0
  8. ref_agents/codemap/prompts.md +40 -0
  9. ref_agents/codemap/security.md +45 -0
  10. ref_agents/codemap/tools.md +94 -0
  11. ref_agents/codemap/tools_browser.md +44 -0
  12. ref_agents/codemap/utils.md +42 -0
  13. ref_agents/codemap/workflow.md +42 -0
  14. ref_agents/config/ai_patterns.yaml +101 -0
  15. ref_agents/config/frameworks/angular.yaml +104 -0
  16. ref_agents/config/frameworks/aspnet.yaml +84 -0
  17. ref_agents/config/frameworks/ef_core.yaml +81 -0
  18. ref_agents/config/frameworks/react.yaml +111 -0
  19. ref_agents/config/frameworks/spring_boot.yaml +117 -0
  20. ref_agents/config/languages/csharp.yaml +153 -0
  21. ref_agents/config/languages/java.yaml +188 -0
  22. ref_agents/config/languages/javascript.yaml +172 -0
  23. ref_agents/config/languages/python.yaml +153 -0
  24. ref_agents/config/languages/typescript.yaml +193 -0
  25. ref_agents/constants.py +553 -0
  26. ref_agents/core/__init__.py +15 -0
  27. ref_agents/core/config_loader.py +160 -0
  28. ref_agents/core/config_models.py +167 -0
  29. ref_agents/core/config_parsing.py +84 -0
  30. ref_agents/core/language_detector.py +388 -0
  31. ref_agents/core/validation_models.py +66 -0
  32. ref_agents/core/validation_primitives.py +176 -0
  33. ref_agents/errors.py +34 -0
  34. ref_agents/license_client.py +247 -0
  35. ref_agents/models/__init__.py +22 -0
  36. ref_agents/models/gherkin.py +45 -0
  37. ref_agents/models/hierarchy.py +80 -0
  38. ref_agents/models/invest.py +59 -0
  39. ref_agents/models/version.py +49 -0
  40. ref_agents/prompts/__init__.py +9 -0
  41. ref_agents/prompts/start_agent.py +772 -0
  42. ref_agents/rules/architecture/backend_patterns.md +43 -0
  43. ref_agents/rules/architecture/diagramming.md +100 -0
  44. ref_agents/rules/architecture/frontend_patterns.md +40 -0
  45. ref_agents/rules/architecture/impact_analysis.md +129 -0
  46. ref_agents/rules/architecture/migration_strategy.md +208 -0
  47. ref_agents/rules/architecture/regression_protocol.md +77 -0
  48. ref_agents/rules/architecture/system_design.md +97 -0
  49. ref_agents/rules/common/codemap_standard.md +97 -0
  50. ref_agents/rules/common/core_protocol.md +59 -0
  51. ref_agents/rules/common/prompt_engineering.md +294 -0
  52. ref_agents/rules/development/debugging.md +32 -0
  53. ref_agents/rules/development/implementation.md +205 -0
  54. ref_agents/rules/operations/completion.md +119 -0
  55. ref_agents/rules/operations/cutover_protocol.md +218 -0
  56. ref_agents/rules/operations/discovery.md +179 -0
  57. ref_agents/rules/operations/fix_workflow.md +87 -0
  58. ref_agents/rules/operations/forensics.md +278 -0
  59. ref_agents/rules/operations/platform.md +263 -0
  60. ref_agents/rules/operations/synchronous_flow.md +25 -0
  61. ref_agents/rules/product/ac_validation.md +25 -0
  62. ref_agents/rules/product/brainstorming.md +27 -0
  63. ref_agents/rules/product/ref_flow.md +101 -0
  64. ref_agents/rules/product/requirements_std.md +114 -0
  65. ref_agents/rules/product/spec_writing.md +235 -0
  66. ref_agents/rules/product/strategy.md +96 -0
  67. ref_agents/rules/quality/documentation_standards.md +46 -0
  68. ref_agents/rules/quality/parity_testing.md +234 -0
  69. ref_agents/rules/quality/project_documentation.md +56 -0
  70. ref_agents/rules/quality/qa_lead.md +111 -0
  71. ref_agents/rules/quality/test_design.md +146 -0
  72. ref_agents/rules/quality/testing_standards.md +293 -0
  73. ref_agents/rules/review/pr_review.md +116 -0
  74. ref_agents/rules/security/security_audit.md +83 -0
  75. ref_agents/security/__init__.py +22 -0
  76. ref_agents/security/dependency_audit.py +188 -0
  77. ref_agents/security/file_audit.py +208 -0
  78. ref_agents/security/network_scan.py +179 -0
  79. ref_agents/security/report_generator.py +313 -0
  80. ref_agents/security/secret_scan.py +252 -0
  81. ref_agents/security/url_scan.py +240 -0
  82. ref_agents/security_scan.py +236 -0
  83. ref_agents/server.py +1586 -0
  84. ref_agents/session.py +100 -0
  85. ref_agents/tool_names.py +55 -0
  86. ref_agents/tools/__init__.py +8 -0
  87. ref_agents/tools/agents_generator.py +315 -0
  88. ref_agents/tools/ai_pattern_detector.py +815 -0
  89. ref_agents/tools/brownfield_populator.py +529 -0
  90. ref_agents/tools/browser/__init__.py +50 -0
  91. ref_agents/tools/browser/evidence_verifier.py +302 -0
  92. ref_agents/tools/browser/execution_logger.py +249 -0
  93. ref_agents/tools/browser/playwright_mcp_client.py +259 -0
  94. ref_agents/tools/browser/screenshot_utils.py +184 -0
  95. ref_agents/tools/browser/test_executor.py +537 -0
  96. ref_agents/tools/code_quality_scanner.py +629 -0
  97. ref_agents/tools/codemap/..md +93 -0
  98. ref_agents/tools/codemap/CODE_MAP.md +30 -0
  99. ref_agents/tools/codemap/browser.md +44 -0
  100. ref_agents/tools/codemap.py +403 -0
  101. ref_agents/tools/codemap_freshness.py +234 -0
  102. ref_agents/tools/comment_smell_scanner.py +346 -0
  103. ref_agents/tools/complexity.py +436 -0
  104. ref_agents/tools/complexity_ast.py +333 -0
  105. ref_agents/tools/compliance.py +246 -0
  106. ref_agents/tools/compliance_remediation.py +846 -0
  107. ref_agents/tools/context_graph.py +839 -0
  108. ref_agents/tools/context_manager.py +550 -0
  109. ref_agents/tools/context_tools.py +121 -0
  110. ref_agents/tools/cross_repo_linker.py +393 -0
  111. ref_agents/tools/dead_code_scanner.py +637 -0
  112. ref_agents/tools/debt_scanner.py +1092 -0
  113. ref_agents/tools/dependency_graph.py +272 -0
  114. ref_agents/tools/discovery_audit.py +372 -0
  115. ref_agents/tools/docs_scanner.py +600 -0
  116. ref_agents/tools/evaluate_gate.py +119 -0
  117. ref_agents/tools/external_detector.py +524 -0
  118. ref_agents/tools/features_generator.py +282 -0
  119. ref_agents/tools/flow_gap_detector.py +373 -0
  120. ref_agents/tools/flow_mapper.py +327 -0
  121. ref_agents/tools/full_suite_runner.py +740 -0
  122. ref_agents/tools/gherkin_parser.py +227 -0
  123. ref_agents/tools/guard_tools.py +139 -0
  124. ref_agents/tools/handoff_tools.py +282 -0
  125. ref_agents/tools/health_scanner.py +1211 -0
  126. ref_agents/tools/hierarchy_manager.py +289 -0
  127. ref_agents/tools/invest_scorer.py +249 -0
  128. ref_agents/tools/jira_confluence_export.py +306 -0
  129. ref_agents/tools/json_output.py +76 -0
  130. ref_agents/tools/migration_mapper.py +946 -0
  131. ref_agents/tools/migration_readiness_scanner.py +209 -0
  132. ref_agents/tools/pattern_learner.py +522 -0
  133. ref_agents/tools/report_utils.py +155 -0
  134. ref_agents/tools/requirements_serializer.py +225 -0
  135. ref_agents/tools/security_audit_tool.py +106 -0
  136. ref_agents/tools/sequencing_engine.py +288 -0
  137. ref_agents/tools/summary_generator.py +275 -0
  138. ref_agents/tools/symbol_resolver.py +306 -0
  139. ref_agents/tools/symbol_smoke_runner.py +336 -0
  140. ref_agents/tools/test_plan_validator.py +189 -0
  141. ref_agents/tools/test_smell_walker.py +902 -0
  142. ref_agents/tools/tier1_fixer.py +502 -0
  143. ref_agents/tools/validators/__init__.py +419 -0
  144. ref_agents/tools/validators/architect.py +268 -0
  145. ref_agents/tools/validators/cutover_engineer.py +167 -0
  146. ref_agents/tools/validators/developer.py +180 -0
  147. ref_agents/tools/validators/discovery.py +150 -0
  148. ref_agents/tools/validators/forensic_engineer.py +191 -0
  149. ref_agents/tools/validators/impact_architect.py +181 -0
  150. ref_agents/tools/validators/migration_planner.py +166 -0
  151. ref_agents/tools/validators/parity_tester.py +155 -0
  152. ref_agents/tools/validators/platform_engineer.py +134 -0
  153. ref_agents/tools/validators/pr_reviewer.py +129 -0
  154. ref_agents/tools/validators/product_manager.py +291 -0
  155. ref_agents/tools/validators/qa_lead.py +172 -0
  156. ref_agents/tools/validators/scrum_master.py +212 -0
  157. ref_agents/tools/validators/security_owner.py +162 -0
  158. ref_agents/tools/validators/specifier.py +134 -0
  159. ref_agents/tools/validators/strategist.py +149 -0
  160. ref_agents/tools/validators/tester.py +121 -0
  161. ref_agents/tools/version_manager.py +202 -0
  162. ref_agents/tools/workflow_tools.py +1549 -0
  163. ref_agents/utils/__init__.py +21 -0
  164. ref_agents/utils/git_utils.py +351 -0
  165. ref_agents/utils/handoff_logger.py +368 -0
  166. ref_agents/utils/ignore_matcher.py +270 -0
  167. ref_agents/workflow/__init__.py +19 -0
  168. ref_agents/workflow/capabilities.py +328 -0
  169. ref_agents/workflow/state_machine.py +708 -0
  170. ref_agents/workflow/transitions.py +658 -0
  171. ref_agents-1.0.0.dist-info/METADATA +365 -0
  172. ref_agents-1.0.0.dist-info/RECORD +175 -0
  173. ref_agents-1.0.0.dist-info/WHEEL +4 -0
  174. ref_agents-1.0.0.dist-info/entry_points.txt +2 -0
  175. ref_agents-1.0.0.dist-info/licenses/LICENSE +115 -0
@@ -0,0 +1,282 @@
1
+ """REF Features Generator.
2
+
3
+ Infers FEATURES.md from codemap/ directory structure.
4
+ Features are user-visible capabilities + modular structures.
5
+
6
+ Usage:
7
+ from ref_agents.tools.features_generator import generate_features_file
8
+ result = generate_features_file("/path/to/project")
9
+ """
10
+
11
+ from dataclasses import dataclass
12
+ from pathlib import Path
13
+
14
+ import structlog
15
+
16
+ logger = structlog.get_logger(__name__)
17
+
18
+ # Keywords indicating user-facing features
19
+ FEATURE_KEYWORDS = {
20
+ "api",
21
+ "auth",
22
+ "authentication",
23
+ "authorization",
24
+ "cache",
25
+ "client",
26
+ "config",
27
+ "connector",
28
+ "consumer",
29
+ "core",
30
+ "database",
31
+ "discovery",
32
+ "export",
33
+ "handler",
34
+ "import",
35
+ "index",
36
+ "indexing",
37
+ "integration",
38
+ "loader",
39
+ "manager",
40
+ "metrics",
41
+ "migration",
42
+ "notification",
43
+ "orchestrator",
44
+ "parser",
45
+ "pipeline",
46
+ "processor",
47
+ "provider",
48
+ "queue",
49
+ "report",
50
+ "scheduler",
51
+ "search",
52
+ "security",
53
+ "service",
54
+ "storage",
55
+ "sync",
56
+ "telemetry",
57
+ "transform",
58
+ "upload",
59
+ "validation",
60
+ "worker",
61
+ }
62
+
63
+
64
+ @dataclass
65
+ class Feature:
66
+ """Inferred feature from codemap."""
67
+
68
+ name: str
69
+ module: str
70
+ description: str
71
+ category: str
72
+
73
+
74
+ def _extract_purpose(content: str) -> str:
75
+ """Extract purpose section from codemap content.
76
+
77
+ Args:
78
+ content: Codemap markdown content.
79
+
80
+ Returns:
81
+ Purpose section text or empty string.
82
+ """
83
+ lines = content.splitlines()
84
+ in_purpose = False
85
+ purpose_lines: list[str] = []
86
+
87
+ for line in lines:
88
+ if line.startswith("## Purpose"):
89
+ in_purpose = True
90
+ continue
91
+ if in_purpose:
92
+ if line.startswith("## "):
93
+ break
94
+ if line.strip() and not line.startswith("<!--"):
95
+ purpose_lines.append(line.strip())
96
+
97
+ return " ".join(purpose_lines)
98
+
99
+
100
+ def _infer_category(module_name: str) -> str:
101
+ """Infer feature category from module name.
102
+
103
+ Args:
104
+ module_name: Module/package name.
105
+
106
+ Returns:
107
+ Category string.
108
+ """
109
+ name_lower = module_name.lower()
110
+
111
+ if any(k in name_lower for k in ["api", "client", "connector"]):
112
+ return "Integration"
113
+ if any(k in name_lower for k in ["auth", "security"]):
114
+ return "Security"
115
+ if any(k in name_lower for k in ["index", "search", "discovery"]):
116
+ return "Data Processing"
117
+ if any(k in name_lower for k in ["config", "core", "shared"]):
118
+ return "Core"
119
+ if any(k in name_lower for k in ["metrics", "telemetry", "report"]):
120
+ return "Observability"
121
+ if any(k in name_lower for k in ["queue", "consumer", "worker"]):
122
+ return "Messaging"
123
+
124
+ return "Utility"
125
+
126
+
127
+ def _format_feature_name(module_name: str) -> str:
128
+ """Convert module name to readable feature name.
129
+
130
+ Args:
131
+ module_name: Underscore-separated module name.
132
+
133
+ Returns:
134
+ Title-cased feature name.
135
+ """
136
+ # Remove common prefixes/suffixes
137
+ name = module_name.replace("_service", "").replace("_handler", "")
138
+ name = name.replace("_manager", "").replace("_processor", "")
139
+
140
+ # Title case
141
+ words = name.replace("_", " ").split()
142
+ return " ".join(word.capitalize() for word in words)
143
+
144
+
145
+ def infer_features(codemap_dir: Path) -> list[Feature]:
146
+ """Infer features from codemap directory.
147
+
148
+ Args:
149
+ codemap_dir: Path to codemap/ directory.
150
+
151
+ Returns:
152
+ List of inferred features.
153
+ """
154
+ features: list[Feature] = []
155
+
156
+ if not codemap_dir.exists():
157
+ return features
158
+
159
+ for map_file in codemap_dir.glob("*.md"):
160
+ if map_file.name == "CODE_MAP.md":
161
+ continue # Skip meta-map
162
+
163
+ try:
164
+ content = map_file.read_text(encoding="utf-8")
165
+ except (OSError, UnicodeDecodeError):
166
+ continue
167
+
168
+ # Module name from filename (e.g., app_discovery.md -> discovery)
169
+ module_name = map_file.stem
170
+ if "_" in module_name:
171
+ # Take last component (e.g., app_discovery -> discovery)
172
+ parts = module_name.split("_")
173
+ module_name = parts[-1] if len(parts) > 1 else module_name
174
+
175
+ # Check if module name suggests a feature
176
+ is_feature = any(kw in module_name.lower() for kw in FEATURE_KEYWORDS)
177
+
178
+ if is_feature:
179
+ purpose = _extract_purpose(content)
180
+ if not purpose or purpose.startswith("["):
181
+ purpose = (
182
+ f"Handles {_format_feature_name(module_name).lower()} functionality"
183
+ )
184
+
185
+ features.append(
186
+ Feature(
187
+ name=_format_feature_name(module_name),
188
+ module=map_file.stem,
189
+ description=purpose,
190
+ category=_infer_category(module_name),
191
+ )
192
+ )
193
+
194
+ # Sort by category then name
195
+ return sorted(features, key=lambda f: (f.category, f.name))
196
+
197
+
198
+ def generate_features_md(features: list[Feature]) -> str:
199
+ """Generate FEATURES.md content.
200
+
201
+ Args:
202
+ features: List of features.
203
+
204
+ Returns:
205
+ Markdown content.
206
+ """
207
+ content = """# FEATURES.md
208
+
209
+ ## Overview
210
+
211
+ This document lists user-facing features inferred from the codebase structure.
212
+
213
+ > **Note:** Features are identified by analyzing module names and purposes in `codemap/`.
214
+ > Update module purposes to refine this list.
215
+
216
+ ## Features by Category
217
+
218
+ """
219
+
220
+ # Group by category
221
+ categories: dict[str, list[Feature]] = {}
222
+ for feature in features:
223
+ if feature.category not in categories:
224
+ categories[feature.category] = []
225
+ categories[feature.category].append(feature)
226
+
227
+ for category in sorted(categories.keys()):
228
+ content += f"### {category}\n\n"
229
+ content += "| Feature | Module | Description |\n"
230
+ content += "|---------|--------|-------------|\n"
231
+ for f in categories[category]:
232
+ content += f"| **{f.name}** | `{f.module}` | {f.description} |\n"
233
+ content += "\n"
234
+
235
+ content += f"""## Summary
236
+
237
+ - **Total Features:** {len(features)}
238
+ - **Categories:** {len(categories)}
239
+
240
+ ---
241
+ *Auto-generated by discovery agent. Edit module purposes in `codemap/` to refine.*
242
+ """
243
+
244
+ return content
245
+
246
+
247
+ def generate_features_file(directory: str) -> str:
248
+ """Generate FEATURES.md at project root.
249
+
250
+ Args:
251
+ directory: Path to project root.
252
+
253
+ Returns:
254
+ Status message.
255
+ """
256
+ root = Path(directory)
257
+ codemap_dir = root / "codemap"
258
+
259
+ if not codemap_dir.exists():
260
+ return "Error: codemap/ directory not found. Run generate_codemaps first."
261
+
262
+ features = infer_features(codemap_dir)
263
+
264
+ if not features:
265
+ return (
266
+ "No features inferred. Ensure codemap/ contains module maps with purposes."
267
+ )
268
+
269
+ content = generate_features_md(features)
270
+
271
+ features_path = root / "FEATURES.md"
272
+ features_path.write_text(content, encoding="utf-8")
273
+
274
+ logger.info(
275
+ "features_generated", path=str(features_path), feature_count=len(features)
276
+ )
277
+
278
+ return (
279
+ f"✅ FEATURES.md generated at `{features_path}`\n\n"
280
+ f"**Features found:** {len(features)}\n"
281
+ f"**Categories:** {len({f.category for f in features})}"
282
+ )
@@ -0,0 +1,373 @@
1
+ """Flow Gap Detector for REF Agents.
2
+
3
+ Compares FLOWS.md definitions against REQ.md stories
4
+ to detect missing steps, orphan stories, and broken links.
5
+ """
6
+
7
+ import re
8
+ from dataclasses import dataclass, field
9
+ from pathlib import Path
10
+
11
+ import structlog
12
+
13
+ from ref_agents.constants import Severity, Status
14
+ from ref_agents.tools.flow_mapper import scan_requirements_for_flows
15
+
16
+ logger = structlog.get_logger(__name__)
17
+
18
+ # Regex for parsing FLOWS.md step tables
19
+ # Format: | Step | Name | Description | Story ID | Status |
20
+ STEP_TABLE_ROW = re.compile(
21
+ r"\|\s*(\d+)\s*\|\s*([^|]+)\s*\|\s*([^|]+)\s*\|\s*([^|]+)\s*\|\s*([^|]+)\s*\|"
22
+ )
23
+ FLOW_HEADER = re.compile(r"##\s*Flow:\s*(\S+)")
24
+ PROCESS_LINE = re.compile(r"\*\*Process:\*\*\s*(.+)")
25
+
26
+
27
+ @dataclass
28
+ class FlowDefinition:
29
+ """Flow definition from FLOWS.md."""
30
+
31
+ name: str
32
+ process: str = ""
33
+ steps: dict[str, dict[str, str]] = field(default_factory=dict)
34
+
35
+ def add_step( # noqa: PLR0913 # TECH_DEBT: refactor to TypedDict — STORY-046
36
+ self,
37
+ order: int,
38
+ name: str,
39
+ description: str,
40
+ story_id: str,
41
+ status: str,
42
+ ) -> None:
43
+ """Add a step to the flow definition."""
44
+ self.steps[name.strip()] = {
45
+ "order": str(order),
46
+ "name": name.strip(),
47
+ "description": description.strip(),
48
+ "story_id": story_id.strip(),
49
+ "status": status.strip(),
50
+ }
51
+
52
+
53
+ @dataclass
54
+ class Gap:
55
+ """Represents a detected gap."""
56
+
57
+ gap_type: str # missing_story, orphan_story, broken_link, status_mismatch
58
+ severity: str # high, medium, low
59
+ flow: str
60
+ step: str
61
+ description: str
62
+ story_id: str = ""
63
+
64
+ def to_dict(self) -> dict[str, str]:
65
+ """Convert to dictionary."""
66
+ return {
67
+ "type": self.gap_type,
68
+ "severity": self.severity,
69
+ "flow": self.flow,
70
+ "step": self.step,
71
+ "description": self.description,
72
+ "story_id": self.story_id,
73
+ }
74
+
75
+
76
+ @dataclass
77
+ class GapReport:
78
+ """Collection of detected gaps."""
79
+
80
+ gaps: list[Gap] = field(default_factory=list)
81
+ flows_checked: int = 0
82
+ steps_checked: int = 0
83
+ stories_checked: int = 0
84
+
85
+ def add_gap(self, gap: Gap) -> None:
86
+ """Add a gap to the report."""
87
+ self.gaps.append(gap)
88
+
89
+ @property
90
+ def high_severity_count(self) -> int:
91
+ """Count high severity gaps."""
92
+ return sum(1 for g in self.gaps if g.severity == "high")
93
+
94
+ @property
95
+ def medium_severity_count(self) -> int:
96
+ """Count medium severity gaps."""
97
+ return sum(1 for g in self.gaps if g.severity == "medium")
98
+
99
+ @property
100
+ def low_severity_count(self) -> int:
101
+ """Count low severity gaps."""
102
+ return sum(1 for g in self.gaps if g.severity == "low")
103
+
104
+ def to_markdown(self) -> str:
105
+ """Generate markdown report."""
106
+ lines = [
107
+ "# Flow Gap Analysis Report",
108
+ "",
109
+ "*Auto-generated by flow_gap_detector.py*",
110
+ "",
111
+ "## Summary",
112
+ "",
113
+ f"- Flows checked: {self.flows_checked}",
114
+ f"- Steps checked: {self.steps_checked}",
115
+ f"- Stories checked: {self.stories_checked}",
116
+ f"- **Total gaps: {len(self.gaps)}**",
117
+ f" - {Severity.HIGH_ICON} High: {self.high_severity_count}",
118
+ f" - {Severity.MEDIUM_ICON} Medium: {self.medium_severity_count}",
119
+ f" - {Severity.LOW_ICON} Low: {self.low_severity_count}",
120
+ "",
121
+ ]
122
+
123
+ if not self.gaps:
124
+ lines.append(
125
+ f"{Status.SUCCESS} **No gaps detected. All flows are complete.**"
126
+ )
127
+ return "\n".join(lines)
128
+
129
+ # Group by severity
130
+ for severity, icon in [
131
+ ("high", Severity.HIGH_ICON),
132
+ ("medium", Severity.MEDIUM_ICON),
133
+ ("low", Severity.LOW_ICON),
134
+ ]:
135
+ severity_gaps = [g for g in self.gaps if g.severity == severity]
136
+ if severity_gaps:
137
+ lines.append(f"## {icon} {severity.title()} Severity Gaps")
138
+ lines.append("")
139
+ lines.append("| Flow | Step | Type | Description |")
140
+ lines.append("|------|------|------|-------------|")
141
+ for gap in severity_gaps:
142
+ lines.append(
143
+ f"| {gap.flow} | {gap.step} | {gap.gap_type} | {gap.description} |"
144
+ )
145
+ lines.append("")
146
+
147
+ return "\n".join(lines)
148
+
149
+
150
+ def parse_flows_md(file_path: Path) -> dict[str, FlowDefinition]:
151
+ """Parse FLOWS.md file to extract flow definitions.
152
+
153
+ Args:
154
+ file_path: Path to FLOWS.md file.
155
+
156
+ Returns:
157
+ Dict mapping flow name to FlowDefinition.
158
+ """
159
+ flows: dict[str, FlowDefinition] = {}
160
+
161
+ try:
162
+ content = file_path.read_text(encoding="utf-8")
163
+ except OSError as e:
164
+ logger.warning("flows_file_read_error", file=str(file_path), error=str(e))
165
+ return flows
166
+
167
+ current_flow: FlowDefinition | None = None
168
+ lines = content.split("\n")
169
+
170
+ for i, line in enumerate(lines):
171
+ # Check for flow header
172
+ flow_match = FLOW_HEADER.match(line)
173
+ if flow_match:
174
+ flow_name = flow_match.group(1)
175
+ current_flow = FlowDefinition(name=flow_name)
176
+ flows[flow_name] = current_flow
177
+ continue
178
+
179
+ # Check for process line
180
+ if current_flow:
181
+ process_match = PROCESS_LINE.match(line)
182
+ if process_match:
183
+ current_flow.process = process_match.group(1).strip()
184
+ continue
185
+
186
+ # Check for step table row
187
+ if current_flow:
188
+ step_match = STEP_TABLE_ROW.match(line)
189
+ if step_match:
190
+ order = int(step_match.group(1))
191
+ name = step_match.group(2)
192
+ description = step_match.group(3)
193
+ story_id = step_match.group(4)
194
+ status = step_match.group(5)
195
+
196
+ # Skip header row
197
+ if name.strip().lower() == "name":
198
+ continue
199
+
200
+ current_flow.add_step(order, name, description, story_id, status)
201
+
202
+ logger.info("flows_md_parsed", flows_found=len(flows))
203
+ return flows
204
+
205
+
206
+ def detect_gaps(
207
+ flows_file: str | Path,
208
+ req_directory: str | Path,
209
+ ) -> GapReport:
210
+ """Detect gaps between FLOWS.md and REQ.md files.
211
+
212
+ Args:
213
+ flows_file: Path to FLOWS.md file.
214
+ req_directory: Path to requirements directory.
215
+
216
+ Returns:
217
+ GapReport with all detected gaps.
218
+ """
219
+ report = GapReport()
220
+
221
+ # Parse FLOWS.md
222
+ flows_path = Path(flows_file)
223
+ if not flows_path.exists():
224
+ report.gaps.append(
225
+ Gap(
226
+ gap_type="config_error",
227
+ severity="high",
228
+ flow="-",
229
+ step="-",
230
+ description=f"FLOWS.md not found: {flows_file}",
231
+ )
232
+ )
233
+ return report
234
+
235
+ expected_flows = parse_flows_md(flows_path)
236
+ report.flows_checked = len(expected_flows)
237
+
238
+ # Scan REQ.md files for actual flow context
239
+ actual_flow_map = scan_requirements_for_flows(req_directory)
240
+ report.stories_checked = len(actual_flow_map.orphan_stories) + sum(
241
+ len(f.steps) for f in actual_flow_map.flows.values()
242
+ )
243
+
244
+ # Check each expected flow
245
+ for flow_name, expected_flow in expected_flows.items():
246
+ actual_flow = actual_flow_map.flows.get(flow_name)
247
+ report.steps_checked += len(expected_flow.steps)
248
+
249
+ for step_name, expected_step in expected_flow.steps.items():
250
+ story_id = expected_step["story_id"]
251
+
252
+ # Gap 1: Missing story (step defined but no story linked)
253
+ if story_id in ("-", "", "N/A", "Not Started"):
254
+ report.add_gap(
255
+ Gap(
256
+ gap_type="missing_story",
257
+ severity="high",
258
+ flow=flow_name,
259
+ step=step_name,
260
+ description=f"Flow step '{step_name}' has no linked story",
261
+ )
262
+ )
263
+ continue
264
+
265
+ # Gap 2: Story exists in FLOWS.md but not in REQ.md
266
+ if actual_flow is None or step_name not in actual_flow.steps:
267
+ report.add_gap(
268
+ Gap(
269
+ gap_type="missing_implementation",
270
+ severity="medium",
271
+ flow=flow_name,
272
+ step=step_name,
273
+ story_id=story_id,
274
+ description=f"Story {story_id} not found in requirements or missing flow context",
275
+ )
276
+ )
277
+ continue
278
+
279
+ # Gap 3: Status mismatch
280
+ actual_step = actual_flow.steps[step_name]
281
+ expected_status = expected_step["status"].lower()
282
+ actual_status = actual_step.status.lower()
283
+
284
+ if expected_status != actual_status:
285
+ report.add_gap(
286
+ Gap(
287
+ gap_type="status_mismatch",
288
+ severity="low",
289
+ flow=flow_name,
290
+ step=step_name,
291
+ story_id=story_id,
292
+ description=f"Status mismatch: FLOWS.md says '{expected_step['status']}', story says '{actual_step.status}'",
293
+ )
294
+ )
295
+
296
+ # Check for orphan flow steps (in REQ.md but not in FLOWS.md)
297
+ for flow_name, actual_flow in actual_flow_map.flows.items():
298
+ expected_flow = expected_flows.get(flow_name)
299
+
300
+ if expected_flow is None:
301
+ # Entire flow not in FLOWS.md
302
+ for step_name, step in actual_flow.steps.items():
303
+ report.add_gap(
304
+ Gap(
305
+ gap_type="orphan_flow",
306
+ severity="medium",
307
+ flow=flow_name,
308
+ step=step_name,
309
+ story_id=step.story_id,
310
+ description=f"Flow '{flow_name}' not defined in FLOWS.md",
311
+ )
312
+ )
313
+ else:
314
+ # Check individual steps
315
+ for step_name, step in actual_flow.steps.items():
316
+ if step_name not in expected_flow.steps:
317
+ report.add_gap(
318
+ Gap(
319
+ gap_type="orphan_step",
320
+ severity="low",
321
+ flow=flow_name,
322
+ step=step_name,
323
+ story_id=step.story_id,
324
+ description=f"Step '{step_name}' in story but not in FLOWS.md",
325
+ )
326
+ )
327
+
328
+ logger.info(
329
+ "gap_detection_complete",
330
+ gaps_found=len(report.gaps),
331
+ high=report.high_severity_count,
332
+ medium=report.medium_severity_count,
333
+ low=report.low_severity_count,
334
+ )
335
+
336
+ return report
337
+
338
+
339
+ def detect_flow_gaps(flows_file: str, req_directory: str, summary: bool = False) -> str:
340
+ """Detect and report flow gaps.
341
+
342
+ Args:
343
+ flows_file: Path to FLOWS.md file.
344
+ req_directory: Path to requirements directory.
345
+ summary: If True, return brief summary. If False, return full report.
346
+
347
+ Returns:
348
+ Markdown report or brief summary of detected gaps.
349
+ """
350
+ report = detect_gaps(flows_file, req_directory)
351
+
352
+ # Summary mode
353
+ if summary:
354
+ if not report.gaps:
355
+ return f"{Status.SUCCESS} No flow gaps detected."
356
+
357
+ return (
358
+ f"{Status.WARNING} {len(report.gaps)} gaps found: "
359
+ f"{Severity.HIGH_ICON} {report.high_severity_count} high, "
360
+ f"{Severity.MEDIUM_ICON} {report.medium_severity_count} medium, "
361
+ f"{Severity.LOW_ICON} {report.low_severity_count} low"
362
+ )
363
+
364
+ # Full report mode
365
+ markdown = report.to_markdown()
366
+
367
+ # Write artifact
368
+ output_path = Path(req_directory) / "FLOW_GAP_REPORT.md"
369
+ try:
370
+ output_path.write_text(markdown, encoding="utf-8")
371
+ return f"Generated: `{output_path}`\n\n{markdown}"
372
+ except OSError as e:
373
+ return f"Failed to write artifact: {e}\n\n{markdown}"