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.
- ref_agents/__init__.py +9 -0
- ref_agents/api_keys.json.example +8 -0
- ref_agents/auth.py +129 -0
- ref_agents/codemap/..md +62 -0
- ref_agents/codemap/CODE_MAP.md +37 -0
- ref_agents/codemap/core.md +43 -0
- ref_agents/codemap/models.md +43 -0
- ref_agents/codemap/prompts.md +40 -0
- ref_agents/codemap/security.md +45 -0
- ref_agents/codemap/tools.md +94 -0
- ref_agents/codemap/tools_browser.md +44 -0
- ref_agents/codemap/utils.md +42 -0
- ref_agents/codemap/workflow.md +42 -0
- ref_agents/config/ai_patterns.yaml +101 -0
- ref_agents/config/frameworks/angular.yaml +104 -0
- ref_agents/config/frameworks/aspnet.yaml +84 -0
- ref_agents/config/frameworks/ef_core.yaml +81 -0
- ref_agents/config/frameworks/react.yaml +111 -0
- ref_agents/config/frameworks/spring_boot.yaml +117 -0
- ref_agents/config/languages/csharp.yaml +153 -0
- ref_agents/config/languages/java.yaml +188 -0
- ref_agents/config/languages/javascript.yaml +172 -0
- ref_agents/config/languages/python.yaml +153 -0
- ref_agents/config/languages/typescript.yaml +193 -0
- ref_agents/constants.py +553 -0
- ref_agents/core/__init__.py +15 -0
- ref_agents/core/config_loader.py +160 -0
- ref_agents/core/config_models.py +167 -0
- ref_agents/core/config_parsing.py +84 -0
- ref_agents/core/language_detector.py +388 -0
- ref_agents/core/validation_models.py +66 -0
- ref_agents/core/validation_primitives.py +176 -0
- ref_agents/errors.py +34 -0
- ref_agents/license_client.py +247 -0
- ref_agents/models/__init__.py +22 -0
- ref_agents/models/gherkin.py +45 -0
- ref_agents/models/hierarchy.py +80 -0
- ref_agents/models/invest.py +59 -0
- ref_agents/models/version.py +49 -0
- ref_agents/prompts/__init__.py +9 -0
- ref_agents/prompts/start_agent.py +772 -0
- ref_agents/rules/architecture/backend_patterns.md +43 -0
- ref_agents/rules/architecture/diagramming.md +100 -0
- ref_agents/rules/architecture/frontend_patterns.md +40 -0
- ref_agents/rules/architecture/impact_analysis.md +129 -0
- ref_agents/rules/architecture/migration_strategy.md +208 -0
- ref_agents/rules/architecture/regression_protocol.md +77 -0
- ref_agents/rules/architecture/system_design.md +97 -0
- ref_agents/rules/common/codemap_standard.md +97 -0
- ref_agents/rules/common/core_protocol.md +59 -0
- ref_agents/rules/common/prompt_engineering.md +294 -0
- ref_agents/rules/development/debugging.md +32 -0
- ref_agents/rules/development/implementation.md +205 -0
- ref_agents/rules/operations/completion.md +119 -0
- ref_agents/rules/operations/cutover_protocol.md +218 -0
- ref_agents/rules/operations/discovery.md +179 -0
- ref_agents/rules/operations/fix_workflow.md +87 -0
- ref_agents/rules/operations/forensics.md +278 -0
- ref_agents/rules/operations/platform.md +263 -0
- ref_agents/rules/operations/synchronous_flow.md +25 -0
- ref_agents/rules/product/ac_validation.md +25 -0
- ref_agents/rules/product/brainstorming.md +27 -0
- ref_agents/rules/product/ref_flow.md +101 -0
- ref_agents/rules/product/requirements_std.md +114 -0
- ref_agents/rules/product/spec_writing.md +235 -0
- ref_agents/rules/product/strategy.md +96 -0
- ref_agents/rules/quality/documentation_standards.md +46 -0
- ref_agents/rules/quality/parity_testing.md +234 -0
- ref_agents/rules/quality/project_documentation.md +56 -0
- ref_agents/rules/quality/qa_lead.md +111 -0
- ref_agents/rules/quality/test_design.md +146 -0
- ref_agents/rules/quality/testing_standards.md +293 -0
- ref_agents/rules/review/pr_review.md +116 -0
- ref_agents/rules/security/security_audit.md +83 -0
- ref_agents/security/__init__.py +22 -0
- ref_agents/security/dependency_audit.py +188 -0
- ref_agents/security/file_audit.py +208 -0
- ref_agents/security/network_scan.py +179 -0
- ref_agents/security/report_generator.py +313 -0
- ref_agents/security/secret_scan.py +252 -0
- ref_agents/security/url_scan.py +240 -0
- ref_agents/security_scan.py +236 -0
- ref_agents/server.py +1586 -0
- ref_agents/session.py +100 -0
- ref_agents/tool_names.py +55 -0
- ref_agents/tools/__init__.py +8 -0
- ref_agents/tools/agents_generator.py +315 -0
- ref_agents/tools/ai_pattern_detector.py +815 -0
- ref_agents/tools/brownfield_populator.py +529 -0
- ref_agents/tools/browser/__init__.py +50 -0
- ref_agents/tools/browser/evidence_verifier.py +302 -0
- ref_agents/tools/browser/execution_logger.py +249 -0
- ref_agents/tools/browser/playwright_mcp_client.py +259 -0
- ref_agents/tools/browser/screenshot_utils.py +184 -0
- ref_agents/tools/browser/test_executor.py +537 -0
- ref_agents/tools/code_quality_scanner.py +629 -0
- ref_agents/tools/codemap/..md +93 -0
- ref_agents/tools/codemap/CODE_MAP.md +30 -0
- ref_agents/tools/codemap/browser.md +44 -0
- ref_agents/tools/codemap.py +403 -0
- ref_agents/tools/codemap_freshness.py +234 -0
- ref_agents/tools/comment_smell_scanner.py +346 -0
- ref_agents/tools/complexity.py +436 -0
- ref_agents/tools/complexity_ast.py +333 -0
- ref_agents/tools/compliance.py +246 -0
- ref_agents/tools/compliance_remediation.py +846 -0
- ref_agents/tools/context_graph.py +839 -0
- ref_agents/tools/context_manager.py +550 -0
- ref_agents/tools/context_tools.py +121 -0
- ref_agents/tools/cross_repo_linker.py +393 -0
- ref_agents/tools/dead_code_scanner.py +637 -0
- ref_agents/tools/debt_scanner.py +1092 -0
- ref_agents/tools/dependency_graph.py +272 -0
- ref_agents/tools/discovery_audit.py +372 -0
- ref_agents/tools/docs_scanner.py +600 -0
- ref_agents/tools/evaluate_gate.py +119 -0
- ref_agents/tools/external_detector.py +524 -0
- ref_agents/tools/features_generator.py +282 -0
- ref_agents/tools/flow_gap_detector.py +373 -0
- ref_agents/tools/flow_mapper.py +327 -0
- ref_agents/tools/full_suite_runner.py +740 -0
- ref_agents/tools/gherkin_parser.py +227 -0
- ref_agents/tools/guard_tools.py +139 -0
- ref_agents/tools/handoff_tools.py +282 -0
- ref_agents/tools/health_scanner.py +1211 -0
- ref_agents/tools/hierarchy_manager.py +289 -0
- ref_agents/tools/invest_scorer.py +249 -0
- ref_agents/tools/jira_confluence_export.py +306 -0
- ref_agents/tools/json_output.py +76 -0
- ref_agents/tools/migration_mapper.py +946 -0
- ref_agents/tools/migration_readiness_scanner.py +209 -0
- ref_agents/tools/pattern_learner.py +522 -0
- ref_agents/tools/report_utils.py +155 -0
- ref_agents/tools/requirements_serializer.py +225 -0
- ref_agents/tools/security_audit_tool.py +106 -0
- ref_agents/tools/sequencing_engine.py +288 -0
- ref_agents/tools/summary_generator.py +275 -0
- ref_agents/tools/symbol_resolver.py +306 -0
- ref_agents/tools/symbol_smoke_runner.py +336 -0
- ref_agents/tools/test_plan_validator.py +189 -0
- ref_agents/tools/test_smell_walker.py +902 -0
- ref_agents/tools/tier1_fixer.py +502 -0
- ref_agents/tools/validators/__init__.py +419 -0
- ref_agents/tools/validators/architect.py +268 -0
- ref_agents/tools/validators/cutover_engineer.py +167 -0
- ref_agents/tools/validators/developer.py +180 -0
- ref_agents/tools/validators/discovery.py +150 -0
- ref_agents/tools/validators/forensic_engineer.py +191 -0
- ref_agents/tools/validators/impact_architect.py +181 -0
- ref_agents/tools/validators/migration_planner.py +166 -0
- ref_agents/tools/validators/parity_tester.py +155 -0
- ref_agents/tools/validators/platform_engineer.py +134 -0
- ref_agents/tools/validators/pr_reviewer.py +129 -0
- ref_agents/tools/validators/product_manager.py +291 -0
- ref_agents/tools/validators/qa_lead.py +172 -0
- ref_agents/tools/validators/scrum_master.py +212 -0
- ref_agents/tools/validators/security_owner.py +162 -0
- ref_agents/tools/validators/specifier.py +134 -0
- ref_agents/tools/validators/strategist.py +149 -0
- ref_agents/tools/validators/tester.py +121 -0
- ref_agents/tools/version_manager.py +202 -0
- ref_agents/tools/workflow_tools.py +1549 -0
- ref_agents/utils/__init__.py +21 -0
- ref_agents/utils/git_utils.py +351 -0
- ref_agents/utils/handoff_logger.py +368 -0
- ref_agents/utils/ignore_matcher.py +270 -0
- ref_agents/workflow/__init__.py +19 -0
- ref_agents/workflow/capabilities.py +328 -0
- ref_agents/workflow/state_machine.py +708 -0
- ref_agents/workflow/transitions.py +658 -0
- ref_agents-1.0.0.dist-info/METADATA +365 -0
- ref_agents-1.0.0.dist-info/RECORD +175 -0
- ref_agents-1.0.0.dist-info/WHEEL +4 -0
- ref_agents-1.0.0.dist-info/entry_points.txt +2 -0
- 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}"
|