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