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,289 @@
|
|
|
1
|
+
"""Hierarchy manager for Epic → Feature → Story structure."""
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import Optional
|
|
5
|
+
|
|
6
|
+
import structlog
|
|
7
|
+
|
|
8
|
+
from ref_agents.models.hierarchy import Epic, Feature, Story
|
|
9
|
+
from ref_agents.tools.requirements_serializer import (
|
|
10
|
+
epic_to_json,
|
|
11
|
+
feature_to_json,
|
|
12
|
+
story_to_json,
|
|
13
|
+
)
|
|
14
|
+
from ref_agents.tools.json_output import generate_json_output
|
|
15
|
+
|
|
16
|
+
logger = structlog.get_logger(__name__)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def create_epic(
|
|
20
|
+
epic_id: str,
|
|
21
|
+
title: str,
|
|
22
|
+
description: str = "",
|
|
23
|
+
requirements_dir: Path | None = None,
|
|
24
|
+
) -> Epic:
|
|
25
|
+
"""Create a new epic.
|
|
26
|
+
|
|
27
|
+
Args:
|
|
28
|
+
epic_id: Epic identifier (e.g., EPIC-001)
|
|
29
|
+
title: Epic title
|
|
30
|
+
description: Epic description
|
|
31
|
+
requirements_dir: Directory for requirements. Defaults to docs/requirements.
|
|
32
|
+
|
|
33
|
+
Returns:
|
|
34
|
+
Created Epic model
|
|
35
|
+
"""
|
|
36
|
+
if requirements_dir is None:
|
|
37
|
+
requirements_dir = Path("docs/requirements")
|
|
38
|
+
|
|
39
|
+
epic = Epic(epic_id=epic_id, title=title, description=description)
|
|
40
|
+
|
|
41
|
+
# Write markdown file
|
|
42
|
+
epic_path = requirements_dir / f"{epic_id}.md"
|
|
43
|
+
epic_path.parent.mkdir(parents=True, exist_ok=True)
|
|
44
|
+
epic_path.write_text(_epic_to_markdown(epic), encoding="utf-8")
|
|
45
|
+
|
|
46
|
+
# Auto-generate JSON
|
|
47
|
+
epic_data = epic.to_dict()
|
|
48
|
+
generate_json_output("epic", epic_data, epic_path)
|
|
49
|
+
|
|
50
|
+
logger.info("Epic created", epic_id=epic_id, path=str(epic_path))
|
|
51
|
+
|
|
52
|
+
return epic
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def create_feature( # noqa: PLR0913 # TECH_DEBT: refactor to TypedDict — STORY-046
|
|
56
|
+
feature_id: str,
|
|
57
|
+
epic_id: str,
|
|
58
|
+
title: str,
|
|
59
|
+
description: str = "",
|
|
60
|
+
requirements_dir: Path | None = None,
|
|
61
|
+
) -> Feature:
|
|
62
|
+
"""Create a new feature under an epic.
|
|
63
|
+
|
|
64
|
+
Args:
|
|
65
|
+
feature_id: Feature identifier (e.g., FEAT-001)
|
|
66
|
+
epic_id: Parent epic identifier
|
|
67
|
+
title: Feature title
|
|
68
|
+
description: Feature description
|
|
69
|
+
requirements_dir: Directory for requirements. Defaults to docs/requirements.
|
|
70
|
+
|
|
71
|
+
Returns:
|
|
72
|
+
Created Feature model
|
|
73
|
+
"""
|
|
74
|
+
if requirements_dir is None:
|
|
75
|
+
requirements_dir = Path("docs/requirements")
|
|
76
|
+
|
|
77
|
+
feature = Feature(
|
|
78
|
+
feature_id=feature_id,
|
|
79
|
+
epic_id=epic_id,
|
|
80
|
+
title=title,
|
|
81
|
+
description=description,
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
# Write markdown file
|
|
85
|
+
feature_path = requirements_dir / f"{feature_id}.md"
|
|
86
|
+
feature_path.parent.mkdir(parents=True, exist_ok=True)
|
|
87
|
+
feature_path.write_text(_feature_to_markdown(feature), encoding="utf-8")
|
|
88
|
+
|
|
89
|
+
# Auto-generate JSON
|
|
90
|
+
feature_data = feature.to_dict()
|
|
91
|
+
generate_json_output("feature", feature_data, feature_path)
|
|
92
|
+
|
|
93
|
+
logger.info(
|
|
94
|
+
"Feature created",
|
|
95
|
+
feature_id=feature_id,
|
|
96
|
+
epic_id=epic_id,
|
|
97
|
+
path=str(feature_path),
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
return feature
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def create_story( # noqa: PLR0913 # TECH_DEBT: refactor to TypedDict — STORY-046
|
|
104
|
+
story_id: str,
|
|
105
|
+
title: str,
|
|
106
|
+
user_story: str,
|
|
107
|
+
epic_id: Optional[str] = None,
|
|
108
|
+
feature_id: Optional[str] = None,
|
|
109
|
+
description: str = "",
|
|
110
|
+
requirements_dir: Path | None = None,
|
|
111
|
+
requirements_text: str = "",
|
|
112
|
+
) -> Story:
|
|
113
|
+
"""Create a new story (can be flat or hierarchical).
|
|
114
|
+
|
|
115
|
+
Args:
|
|
116
|
+
story_id: Story identifier (e.g., STORY-001)
|
|
117
|
+
title: Story title
|
|
118
|
+
user_story: User story text (As a... I want... So that...)
|
|
119
|
+
epic_id: Optional parent epic identifier
|
|
120
|
+
feature_id: Optional parent feature identifier
|
|
121
|
+
description: Story description
|
|
122
|
+
requirements_dir: Directory for requirements. Defaults to docs/requirements.
|
|
123
|
+
requirements_text: Raw requirements text from terminal/chat. Appended under
|
|
124
|
+
"## Raw Requirements" when provided — normalises inline input to disk.
|
|
125
|
+
|
|
126
|
+
Returns:
|
|
127
|
+
Created Story model
|
|
128
|
+
"""
|
|
129
|
+
if requirements_dir is None:
|
|
130
|
+
requirements_dir = Path("docs/requirements")
|
|
131
|
+
|
|
132
|
+
story = Story(
|
|
133
|
+
story_id=story_id,
|
|
134
|
+
title=title,
|
|
135
|
+
user_story=user_story,
|
|
136
|
+
epic_id=epic_id,
|
|
137
|
+
feature_id=feature_id,
|
|
138
|
+
description=description,
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
# Write markdown file
|
|
142
|
+
story_path = requirements_dir / f"{story_id}.md"
|
|
143
|
+
story_path.parent.mkdir(parents=True, exist_ok=True)
|
|
144
|
+
markdown = _story_to_markdown(story)
|
|
145
|
+
if requirements_text:
|
|
146
|
+
markdown += "\n\n## Raw Requirements\n\n" + requirements_text
|
|
147
|
+
story_path.write_text(markdown, encoding="utf-8")
|
|
148
|
+
|
|
149
|
+
# Auto-generate JSON
|
|
150
|
+
story_data = story.to_dict()
|
|
151
|
+
generate_json_output("story", story_data, story_path)
|
|
152
|
+
|
|
153
|
+
logger.info(
|
|
154
|
+
"Story created",
|
|
155
|
+
story_id=story_id,
|
|
156
|
+
epic_id=epic_id,
|
|
157
|
+
feature_id=feature_id,
|
|
158
|
+
path=str(story_path),
|
|
159
|
+
)
|
|
160
|
+
|
|
161
|
+
return story
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def get_epic(epic_id: str, requirements_dir: Path | None = None) -> Optional[Epic]:
|
|
165
|
+
"""Get an epic by ID.
|
|
166
|
+
|
|
167
|
+
Args:
|
|
168
|
+
epic_id: Epic identifier
|
|
169
|
+
requirements_dir: Directory for requirements
|
|
170
|
+
|
|
171
|
+
Returns:
|
|
172
|
+
Epic model if found, None otherwise
|
|
173
|
+
"""
|
|
174
|
+
if requirements_dir is None:
|
|
175
|
+
requirements_dir = Path("docs/requirements")
|
|
176
|
+
|
|
177
|
+
epic_path = requirements_dir / f"{epic_id}.md"
|
|
178
|
+
if not epic_path.exists():
|
|
179
|
+
return None
|
|
180
|
+
|
|
181
|
+
epic_data = epic_to_json(epic_path)
|
|
182
|
+
return Epic(**epic_data)
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def get_feature(
|
|
186
|
+
feature_id: str, requirements_dir: Path | None = None
|
|
187
|
+
) -> Optional[Feature]:
|
|
188
|
+
"""Get a feature by ID.
|
|
189
|
+
|
|
190
|
+
Args:
|
|
191
|
+
feature_id: Feature identifier
|
|
192
|
+
requirements_dir: Directory for requirements
|
|
193
|
+
|
|
194
|
+
Returns:
|
|
195
|
+
Feature model if found, None otherwise
|
|
196
|
+
"""
|
|
197
|
+
if requirements_dir is None:
|
|
198
|
+
requirements_dir = Path("docs/requirements")
|
|
199
|
+
|
|
200
|
+
feature_path = requirements_dir / f"{feature_id}.md"
|
|
201
|
+
if not feature_path.exists():
|
|
202
|
+
return None
|
|
203
|
+
|
|
204
|
+
feature_data = feature_to_json(feature_path)
|
|
205
|
+
return Feature(**feature_data)
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
def get_story(story_id: str, requirements_dir: Path | None = None) -> Optional[Story]:
|
|
209
|
+
"""Get a story by ID.
|
|
210
|
+
|
|
211
|
+
Args:
|
|
212
|
+
story_id: Story identifier
|
|
213
|
+
requirements_dir: Directory for requirements
|
|
214
|
+
|
|
215
|
+
Returns:
|
|
216
|
+
Story model if found, None otherwise
|
|
217
|
+
"""
|
|
218
|
+
if requirements_dir is None:
|
|
219
|
+
requirements_dir = Path("docs/requirements")
|
|
220
|
+
|
|
221
|
+
story_path = requirements_dir / f"{story_id}.md"
|
|
222
|
+
if not story_path.exists():
|
|
223
|
+
return None
|
|
224
|
+
|
|
225
|
+
story_data = story_to_json(story_path)
|
|
226
|
+
return Story(**story_data)
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
def _epic_to_markdown(epic: Epic) -> str:
|
|
230
|
+
"""Convert epic to markdown format."""
|
|
231
|
+
lines = [
|
|
232
|
+
f"# Epic: [{epic.epic_id}] {epic.title}",
|
|
233
|
+
"",
|
|
234
|
+
f"**Status:** {epic.status}",
|
|
235
|
+
"",
|
|
236
|
+
"## Description",
|
|
237
|
+
"",
|
|
238
|
+
epic.description or "",
|
|
239
|
+
]
|
|
240
|
+
return "\n".join(lines)
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
def _feature_to_markdown(feature: Feature) -> str:
|
|
244
|
+
"""Convert feature to markdown format."""
|
|
245
|
+
lines = [
|
|
246
|
+
f"# Feature: [{feature.feature_id}] {feature.title}",
|
|
247
|
+
"",
|
|
248
|
+
f"**Epic:** {feature.epic_id}",
|
|
249
|
+
f"**Status:** {feature.status}",
|
|
250
|
+
"",
|
|
251
|
+
"## Description",
|
|
252
|
+
"",
|
|
253
|
+
feature.description or "",
|
|
254
|
+
]
|
|
255
|
+
return "\n".join(lines)
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
def _story_to_markdown(story: Story) -> str:
|
|
259
|
+
"""Convert story to markdown format."""
|
|
260
|
+
lines = [
|
|
261
|
+
f"# Requirement: [{story.story_id}] {story.title}",
|
|
262
|
+
"",
|
|
263
|
+
f"**Status:** {story.status}",
|
|
264
|
+
f"**Priority:** {story.priority}",
|
|
265
|
+
]
|
|
266
|
+
|
|
267
|
+
if story.epic_id:
|
|
268
|
+
lines.append(f"**Epic:** {story.epic_id}")
|
|
269
|
+
if story.feature_id:
|
|
270
|
+
lines.append(f"**Feature:** {story.feature_id}")
|
|
271
|
+
|
|
272
|
+
lines.extend(
|
|
273
|
+
[
|
|
274
|
+
"",
|
|
275
|
+
"## User Story",
|
|
276
|
+
"",
|
|
277
|
+
story.user_story or "",
|
|
278
|
+
"",
|
|
279
|
+
"## Description",
|
|
280
|
+
"",
|
|
281
|
+
story.description or "",
|
|
282
|
+
"",
|
|
283
|
+
"## Acceptance Criteria",
|
|
284
|
+
"",
|
|
285
|
+
"*Acceptance criteria will be added here*",
|
|
286
|
+
]
|
|
287
|
+
)
|
|
288
|
+
|
|
289
|
+
return "\n".join(lines)
|
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
"""INVEST scoring tool for user story quality assessment."""
|
|
2
|
+
|
|
3
|
+
from typing import Optional
|
|
4
|
+
|
|
5
|
+
import structlog
|
|
6
|
+
|
|
7
|
+
from ref_agents.models.invest import INVESTScores
|
|
8
|
+
|
|
9
|
+
logger = structlog.get_logger(__name__)
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def score_invest_principles(
|
|
13
|
+
user_story: str,
|
|
14
|
+
acceptance_criteria: list[str],
|
|
15
|
+
description: Optional[str] = None,
|
|
16
|
+
) -> INVESTScores:
|
|
17
|
+
"""Score a user story against INVEST principles.
|
|
18
|
+
|
|
19
|
+
Args:
|
|
20
|
+
user_story: User story text (As a... I want... So that...)
|
|
21
|
+
acceptance_criteria: List of acceptance criteria (Gherkin format preferred)
|
|
22
|
+
description: Optional story description
|
|
23
|
+
|
|
24
|
+
Returns:
|
|
25
|
+
INVESTScores with 0-5 scores for each principle
|
|
26
|
+
"""
|
|
27
|
+
story_text = f"{user_story} {description or ''} {' '.join(acceptance_criteria)}"
|
|
28
|
+
|
|
29
|
+
# Independent: Check for dependencies, blockers, prerequisites
|
|
30
|
+
independent_score = _score_independent(story_text, acceptance_criteria)
|
|
31
|
+
|
|
32
|
+
# Negotiable: Check for specific implementation details (should be absent)
|
|
33
|
+
negotiable_score = _score_negotiable(story_text)
|
|
34
|
+
|
|
35
|
+
# Valuable: Check for value statement ("So that...")
|
|
36
|
+
valuable_score = _score_valuable(user_story, description)
|
|
37
|
+
|
|
38
|
+
# Estimable: Check for clear AC, testable criteria
|
|
39
|
+
estimable_score = _score_estimable(acceptance_criteria)
|
|
40
|
+
|
|
41
|
+
# Small: Check story size (AC count, complexity indicators)
|
|
42
|
+
small_score = _score_small(acceptance_criteria, story_text)
|
|
43
|
+
|
|
44
|
+
# Testable: Check for Gherkin format, binary pass/fail criteria
|
|
45
|
+
testable_score = _score_testable(acceptance_criteria)
|
|
46
|
+
|
|
47
|
+
return INVESTScores(
|
|
48
|
+
independent=independent_score,
|
|
49
|
+
negotiable=negotiable_score,
|
|
50
|
+
valuable=valuable_score,
|
|
51
|
+
estimable=estimable_score,
|
|
52
|
+
small=small_score,
|
|
53
|
+
testable=testable_score,
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _score_by_keyword_count(
|
|
58
|
+
text: str, keywords: list[str], reverse: bool = False
|
|
59
|
+
) -> int:
|
|
60
|
+
"""Score based on keyword count in text.
|
|
61
|
+
|
|
62
|
+
Args:
|
|
63
|
+
text: Text to analyze
|
|
64
|
+
keywords: Keywords to search for
|
|
65
|
+
reverse: If True, higher count = higher score (default: False, higher count = lower score)
|
|
66
|
+
|
|
67
|
+
Returns:
|
|
68
|
+
Score 0-5 based on keyword count
|
|
69
|
+
"""
|
|
70
|
+
text_lower = text.lower()
|
|
71
|
+
count = sum(1 for kw in keywords if kw in text_lower)
|
|
72
|
+
|
|
73
|
+
if reverse:
|
|
74
|
+
# Higher count = higher score
|
|
75
|
+
if count == 0:
|
|
76
|
+
return 1
|
|
77
|
+
elif count == 1:
|
|
78
|
+
return 2
|
|
79
|
+
elif count == 2: # noqa: PLR2004 # TECH_DEBT: extract to named constant — G25
|
|
80
|
+
return 3
|
|
81
|
+
elif count <= 3: # noqa: PLR2004 # TECH_DEBT: extract to named constant — G25
|
|
82
|
+
return 4
|
|
83
|
+
else:
|
|
84
|
+
return 5
|
|
85
|
+
else:
|
|
86
|
+
# Higher count = lower score
|
|
87
|
+
if count == 0:
|
|
88
|
+
return 5
|
|
89
|
+
elif count == 1:
|
|
90
|
+
return 4
|
|
91
|
+
elif count == 2: # noqa: PLR2004 # TECH_DEBT: extract to named constant — G25
|
|
92
|
+
return 3
|
|
93
|
+
elif count <= 3: # noqa: PLR2004 # TECH_DEBT: extract to named constant — G25
|
|
94
|
+
return 2
|
|
95
|
+
else:
|
|
96
|
+
return 1
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def _score_independent(story_text: str, acceptance_criteria: list[str]) -> int:
|
|
100
|
+
"""Score Independent principle (0-5).
|
|
101
|
+
|
|
102
|
+
Lower score if story mentions dependencies, blockers, or prerequisites.
|
|
103
|
+
|
|
104
|
+
Args:
|
|
105
|
+
story_text: Story text to analyze
|
|
106
|
+
acceptance_criteria: Acceptance criteria (unused, kept for API consistency)
|
|
107
|
+
"""
|
|
108
|
+
dependency_keywords = [
|
|
109
|
+
"depends on",
|
|
110
|
+
"requires",
|
|
111
|
+
"blocked by",
|
|
112
|
+
"must complete first",
|
|
113
|
+
"prerequisite",
|
|
114
|
+
"after",
|
|
115
|
+
"before",
|
|
116
|
+
]
|
|
117
|
+
_ = acceptance_criteria # Unused but kept for API consistency
|
|
118
|
+
return _score_by_keyword_count(story_text, dependency_keywords, reverse=False)
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def _score_negotiable(story_text: str) -> int:
|
|
122
|
+
"""Score Negotiable principle (0-5).
|
|
123
|
+
|
|
124
|
+
Lower score if story contains specific implementation details.
|
|
125
|
+
"""
|
|
126
|
+
implementation_keywords = [
|
|
127
|
+
"must use",
|
|
128
|
+
"must implement",
|
|
129
|
+
"exactly",
|
|
130
|
+
"specifically",
|
|
131
|
+
"only",
|
|
132
|
+
"cannot use",
|
|
133
|
+
"forbidden",
|
|
134
|
+
]
|
|
135
|
+
return _score_by_keyword_count(story_text, implementation_keywords, reverse=False)
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def _score_valuable(user_story: str, description: Optional[str] = None) -> int:
|
|
139
|
+
"""Score Valuable principle (0-5).
|
|
140
|
+
|
|
141
|
+
Higher score if "So that..." clause is present and clear.
|
|
142
|
+
"""
|
|
143
|
+
text = f"{user_story} {description or ''}"
|
|
144
|
+
text_lower = text.lower()
|
|
145
|
+
|
|
146
|
+
# Check for value statement
|
|
147
|
+
if "so that" in text_lower:
|
|
148
|
+
# Check if value is specific (not generic)
|
|
149
|
+
generic_values = ["better", "good", "nice", "improve"]
|
|
150
|
+
has_generic = any(gv in text_lower for gv in generic_values)
|
|
151
|
+
|
|
152
|
+
if not has_generic:
|
|
153
|
+
return 5
|
|
154
|
+
else:
|
|
155
|
+
return 3
|
|
156
|
+
else:
|
|
157
|
+
return 2
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def _count_matches_in_list(items: list[str], keywords: list[str]) -> int:
|
|
161
|
+
"""Count items that contain any of the keywords.
|
|
162
|
+
|
|
163
|
+
Args:
|
|
164
|
+
items: List of strings to search
|
|
165
|
+
keywords: Keywords to search for
|
|
166
|
+
|
|
167
|
+
Returns:
|
|
168
|
+
Count of items containing at least one keyword
|
|
169
|
+
"""
|
|
170
|
+
return sum(
|
|
171
|
+
1 for item in items if any(keyword in item.lower() for keyword in keywords)
|
|
172
|
+
)
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def _score_estimable(acceptance_criteria: list[str]) -> int:
|
|
176
|
+
"""Score Estimable principle (0-5).
|
|
177
|
+
|
|
178
|
+
Higher score if AC are clear, specific, and measurable.
|
|
179
|
+
"""
|
|
180
|
+
if not acceptance_criteria:
|
|
181
|
+
return 1
|
|
182
|
+
|
|
183
|
+
# Check for specific, measurable criteria
|
|
184
|
+
specific_indicators = ["status:", "status code", "error", "timeout", "count"]
|
|
185
|
+
ambiguous_terms = ["fast", "quickly", "user-friendly", "easy", "intuitive"]
|
|
186
|
+
|
|
187
|
+
specific_count = _count_matches_in_list(acceptance_criteria, specific_indicators)
|
|
188
|
+
ambiguous_count = _count_matches_in_list(acceptance_criteria, ambiguous_terms)
|
|
189
|
+
|
|
190
|
+
if ambiguous_count > 0:
|
|
191
|
+
return max(1, 3 - ambiguous_count)
|
|
192
|
+
elif specific_count >= len(acceptance_criteria) * 0.5:
|
|
193
|
+
return 5
|
|
194
|
+
elif specific_count > 0:
|
|
195
|
+
return 4
|
|
196
|
+
else:
|
|
197
|
+
return 3
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
def _score_small(acceptance_criteria: list[str], story_text: str) -> int:
|
|
201
|
+
"""Score Small principle (0-5).
|
|
202
|
+
|
|
203
|
+
Higher score if story is appropriately sized (3-8 AC ideal).
|
|
204
|
+
|
|
205
|
+
Args:
|
|
206
|
+
acceptance_criteria: List of acceptance criteria
|
|
207
|
+
story_text: Story text (unused, kept for API consistency)
|
|
208
|
+
"""
|
|
209
|
+
ac_count = len(acceptance_criteria)
|
|
210
|
+
_ = story_text # Unused but kept for API consistency
|
|
211
|
+
|
|
212
|
+
# Ideal: 3-8 AC
|
|
213
|
+
if 3 <= ac_count <= 8: # noqa: PLR2004 # TECH_DEBT: extract to named constant — G25
|
|
214
|
+
return 5
|
|
215
|
+
elif ac_count == 2 or ac_count == 9: # noqa: PLR2004 # TECH_DEBT: extract to named constant — G25
|
|
216
|
+
return 4
|
|
217
|
+
elif ac_count == 1 or ac_count == 10: # noqa: PLR2004 # TECH_DEBT: extract to named constant — G25
|
|
218
|
+
return 3
|
|
219
|
+
elif ac_count == 0:
|
|
220
|
+
return 1
|
|
221
|
+
elif ac_count > 10: # noqa: PLR2004 # TECH_DEBT: extract to named constant — G25
|
|
222
|
+
return 2
|
|
223
|
+
else:
|
|
224
|
+
return 4
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
def _score_testable(acceptance_criteria: list[str]) -> int:
|
|
228
|
+
"""Score Testable principle (0-5).
|
|
229
|
+
|
|
230
|
+
Higher score if AC use Gherkin format and are binary (pass/fail).
|
|
231
|
+
"""
|
|
232
|
+
if not acceptance_criteria:
|
|
233
|
+
return 1
|
|
234
|
+
|
|
235
|
+
gherkin_keywords = ["given", "when", "then", "and", "but"]
|
|
236
|
+
gherkin_count = _count_matches_in_list(acceptance_criteria, gherkin_keywords)
|
|
237
|
+
|
|
238
|
+
gherkin_ratio = gherkin_count / len(acceptance_criteria)
|
|
239
|
+
|
|
240
|
+
if gherkin_ratio >= 0.8: # noqa: PLR2004 # TECH_DEBT: extract to named constant — G25
|
|
241
|
+
return 5
|
|
242
|
+
elif gherkin_ratio >= 0.6: # noqa: PLR2004 # TECH_DEBT: extract to named constant — G25
|
|
243
|
+
return 4
|
|
244
|
+
elif gherkin_ratio >= 0.4: # noqa: PLR2004 # TECH_DEBT: extract to named constant — G25
|
|
245
|
+
return 3
|
|
246
|
+
elif gherkin_ratio > 0:
|
|
247
|
+
return 2
|
|
248
|
+
else:
|
|
249
|
+
return 1
|