specfact-cli 0.4.2__py3-none-any.whl → 0.6.8__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 (66) hide show
  1. specfact_cli/__init__.py +1 -1
  2. specfact_cli/agents/analyze_agent.py +2 -3
  3. specfact_cli/analyzers/__init__.py +2 -1
  4. specfact_cli/analyzers/ambiguity_scanner.py +601 -0
  5. specfact_cli/analyzers/code_analyzer.py +462 -30
  6. specfact_cli/analyzers/constitution_evidence_extractor.py +491 -0
  7. specfact_cli/analyzers/contract_extractor.py +419 -0
  8. specfact_cli/analyzers/control_flow_analyzer.py +281 -0
  9. specfact_cli/analyzers/requirement_extractor.py +337 -0
  10. specfact_cli/analyzers/test_pattern_extractor.py +330 -0
  11. specfact_cli/cli.py +151 -206
  12. specfact_cli/commands/constitution.py +281 -0
  13. specfact_cli/commands/enforce.py +42 -34
  14. specfact_cli/commands/import_cmd.py +481 -152
  15. specfact_cli/commands/init.py +224 -55
  16. specfact_cli/commands/plan.py +2133 -547
  17. specfact_cli/commands/repro.py +100 -78
  18. specfact_cli/commands/sync.py +701 -186
  19. specfact_cli/enrichers/constitution_enricher.py +765 -0
  20. specfact_cli/enrichers/plan_enricher.py +294 -0
  21. specfact_cli/importers/speckit_converter.py +364 -48
  22. specfact_cli/importers/speckit_scanner.py +65 -0
  23. specfact_cli/models/plan.py +42 -0
  24. specfact_cli/resources/mappings/node-async.yaml +49 -0
  25. specfact_cli/resources/mappings/python-async.yaml +47 -0
  26. specfact_cli/resources/mappings/speckit-default.yaml +82 -0
  27. specfact_cli/resources/prompts/specfact-enforce.md +185 -0
  28. specfact_cli/resources/prompts/specfact-import-from-code.md +626 -0
  29. specfact_cli/resources/prompts/specfact-plan-add-feature.md +188 -0
  30. specfact_cli/resources/prompts/specfact-plan-add-story.md +212 -0
  31. specfact_cli/resources/prompts/specfact-plan-compare.md +571 -0
  32. specfact_cli/resources/prompts/specfact-plan-init.md +531 -0
  33. specfact_cli/resources/prompts/specfact-plan-promote.md +352 -0
  34. specfact_cli/resources/prompts/specfact-plan-review.md +1276 -0
  35. specfact_cli/resources/prompts/specfact-plan-select.md +401 -0
  36. specfact_cli/resources/prompts/specfact-plan-update-feature.md +242 -0
  37. specfact_cli/resources/prompts/specfact-plan-update-idea.md +211 -0
  38. specfact_cli/resources/prompts/specfact-repro.md +268 -0
  39. specfact_cli/resources/prompts/specfact-sync.md +497 -0
  40. specfact_cli/resources/schemas/deviation.schema.json +61 -0
  41. specfact_cli/resources/schemas/plan.schema.json +204 -0
  42. specfact_cli/resources/schemas/protocol.schema.json +53 -0
  43. specfact_cli/resources/templates/github-action.yml.j2 +140 -0
  44. specfact_cli/resources/templates/plan.bundle.yaml.j2 +141 -0
  45. specfact_cli/resources/templates/pr-template.md.j2 +58 -0
  46. specfact_cli/resources/templates/protocol.yaml.j2 +24 -0
  47. specfact_cli/resources/templates/telemetry.yaml.example +35 -0
  48. specfact_cli/sync/__init__.py +10 -1
  49. specfact_cli/sync/watcher.py +268 -0
  50. specfact_cli/telemetry.py +440 -0
  51. specfact_cli/utils/acceptance_criteria.py +127 -0
  52. specfact_cli/utils/enrichment_parser.py +445 -0
  53. specfact_cli/utils/feature_keys.py +12 -3
  54. specfact_cli/utils/ide_setup.py +170 -0
  55. specfact_cli/utils/structure.py +179 -2
  56. specfact_cli/utils/yaml_utils.py +33 -0
  57. specfact_cli/validators/repro_checker.py +22 -1
  58. specfact_cli/validators/schema.py +15 -4
  59. specfact_cli-0.6.8.dist-info/METADATA +456 -0
  60. specfact_cli-0.6.8.dist-info/RECORD +99 -0
  61. {specfact_cli-0.4.2.dist-info → specfact_cli-0.6.8.dist-info}/entry_points.txt +1 -0
  62. specfact_cli-0.6.8.dist-info/licenses/LICENSE.md +202 -0
  63. specfact_cli-0.4.2.dist-info/METADATA +0 -370
  64. specfact_cli-0.4.2.dist-info/RECORD +0 -62
  65. specfact_cli-0.4.2.dist-info/licenses/LICENSE.md +0 -61
  66. {specfact_cli-0.4.2.dist-info → specfact_cli-0.6.8.dist-info}/WHEEL +0 -0
@@ -0,0 +1,281 @@
1
+ """Control flow analyzer for extracting scenarios from code AST.
2
+
3
+ Extracts Primary, Alternate, Exception, and Recovery scenarios from code control flow
4
+ patterns (if/else, try/except, loops, retry logic).
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import ast
10
+ from collections.abc import Sequence
11
+
12
+ from beartype import beartype
13
+ from icontract import ensure, require
14
+
15
+
16
+ class ControlFlowAnalyzer:
17
+ """
18
+ Analyzes AST to extract control flow patterns and generate scenarios.
19
+
20
+ Extracts scenarios from:
21
+ - if/else branches → Alternate scenarios
22
+ - try/except blocks → Exception and Recovery scenarios
23
+ - Happy paths → Primary scenarios
24
+ - Retry logic → Recovery scenarios
25
+ """
26
+
27
+ @beartype
28
+ def __init__(self) -> None:
29
+ """Initialize control flow analyzer."""
30
+ self.scenarios: dict[str, list[str]] = {
31
+ "primary": [],
32
+ "alternate": [],
33
+ "exception": [],
34
+ "recovery": [],
35
+ }
36
+
37
+ @beartype
38
+ @require(lambda method_node: isinstance(method_node, ast.FunctionDef), "Method must be FunctionDef node")
39
+ @ensure(lambda result: isinstance(result, dict), "Must return dict")
40
+ @ensure(
41
+ lambda result: "primary" in result and "alternate" in result and "exception" in result and "recovery" in result,
42
+ "Must have all scenario types",
43
+ )
44
+ def extract_scenarios_from_method(
45
+ self, method_node: ast.FunctionDef, class_name: str, method_name: str
46
+ ) -> dict[str, list[str]]:
47
+ """
48
+ Extract scenarios from a method's control flow.
49
+
50
+ Args:
51
+ method_node: AST node for the method
52
+ class_name: Name of the class containing the method
53
+ method_name: Name of the method
54
+
55
+ Returns:
56
+ Dictionary with scenario types as keys and lists of Given/When/Then scenarios as values
57
+ """
58
+ scenarios: dict[str, list[str]] = {
59
+ "primary": [],
60
+ "alternate": [],
61
+ "exception": [],
62
+ "recovery": [],
63
+ }
64
+
65
+ # Analyze method body for control flow
66
+ self._analyze_node(method_node.body, scenarios, class_name, method_name)
67
+
68
+ # If no scenarios found, generate default primary scenario
69
+ if not any(scenarios.values()):
70
+ scenarios["primary"].append(
71
+ f"Given {class_name} instance, When {method_name} is called, Then method executes successfully"
72
+ )
73
+
74
+ return scenarios
75
+
76
+ @beartype
77
+ def _analyze_node(
78
+ self, nodes: Sequence[ast.AST], scenarios: dict[str, list[str]], class_name: str, method_name: str
79
+ ) -> None:
80
+ """Recursively analyze AST nodes for control flow patterns."""
81
+ for node in nodes:
82
+ if isinstance(node, ast.If):
83
+ # if/else → Alternate scenario
84
+ self._extract_if_scenario(node, scenarios, class_name, method_name)
85
+ elif isinstance(node, ast.Try):
86
+ # try/except → Exception and Recovery scenarios
87
+ self._extract_try_scenario(node, scenarios, class_name, method_name)
88
+ elif isinstance(node, (ast.For, ast.While)):
89
+ # Loops might contain retry logic → Recovery scenario
90
+ self._extract_loop_scenario(node, scenarios, class_name, method_name)
91
+ elif isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
92
+ # Recursively analyze nested functions
93
+ self._analyze_node(node.body, scenarios, class_name, method_name)
94
+
95
+ @beartype
96
+ def _extract_if_scenario(
97
+ self, if_node: ast.If, scenarios: dict[str, list[str]], class_name: str, method_name: str
98
+ ) -> None:
99
+ """Extract scenario from if/else statement."""
100
+ # Extract condition
101
+ condition = self._extract_condition(if_node.test)
102
+
103
+ # Primary scenario: if branch (happy path)
104
+ if if_node.body:
105
+ primary_action = self._extract_action_from_body(if_node.body)
106
+ scenarios["primary"].append(
107
+ f"Given {class_name} instance, When {method_name} is called with {condition}, Then {primary_action}"
108
+ )
109
+
110
+ # Alternate scenario: else branch
111
+ if if_node.orelse:
112
+ alternate_action = self._extract_action_from_body(if_node.orelse)
113
+ scenarios["alternate"].append(
114
+ f"Given {class_name} instance, When {method_name} is called with {self._negate_condition(condition)}, Then {alternate_action}"
115
+ )
116
+
117
+ @beartype
118
+ def _extract_try_scenario(
119
+ self, try_node: ast.Try, scenarios: dict[str, list[str]], class_name: str, method_name: str
120
+ ) -> None:
121
+ """Extract scenarios from try/except block."""
122
+ # Primary scenario: try block (happy path)
123
+ if try_node.body:
124
+ primary_action = self._extract_action_from_body(try_node.body)
125
+ scenarios["primary"].append(
126
+ f"Given {class_name} instance, When {method_name} is called, Then {primary_action}"
127
+ )
128
+
129
+ # Exception scenarios: except blocks
130
+ for handler in try_node.handlers:
131
+ exception_type = "Exception"
132
+ if handler.type:
133
+ exception_type = self._extract_exception_type(handler.type)
134
+
135
+ exception_action = self._extract_action_from_body(handler.body) if handler.body else "error is handled"
136
+ scenarios["exception"].append(
137
+ f"Given {class_name} instance, When {method_name} is called and {exception_type} occurs, Then {exception_action}"
138
+ )
139
+
140
+ # Check for retry/recovery logic in exception handler
141
+ if self._has_retry_logic(handler.body):
142
+ scenarios["recovery"].append(
143
+ f"Given {class_name} instance, When {method_name} fails with {exception_type}, Then system retries and recovers"
144
+ )
145
+
146
+ # Recovery scenario: finally block or retry logic
147
+ if try_node.finalbody:
148
+ recovery_action = self._extract_action_from_body(try_node.finalbody)
149
+ scenarios["recovery"].append(
150
+ f"Given {class_name} instance, When {method_name} completes or fails, Then {recovery_action}"
151
+ )
152
+
153
+ @beartype
154
+ def _extract_loop_scenario(
155
+ self, loop_node: ast.For | ast.While, scenarios: dict[str, list[str]], class_name: str, method_name: str
156
+ ) -> None:
157
+ """Extract scenario from loop (might indicate retry logic)."""
158
+ # Check if loop contains retry/retry logic
159
+ if self._has_retry_logic(loop_node.body):
160
+ scenarios["recovery"].append(
161
+ f"Given {class_name} instance, When {method_name} is called, Then system retries on failure until success"
162
+ )
163
+
164
+ @beartype
165
+ def _extract_condition(self, test_node: ast.AST) -> str:
166
+ """Extract human-readable condition from AST node."""
167
+ if isinstance(test_node, ast.Compare):
168
+ left = self._extract_expression(test_node.left)
169
+ ops = [op.__class__.__name__ for op in test_node.ops]
170
+ comparators = [self._extract_expression(comp) for comp in test_node.comparators]
171
+
172
+ op_map = {
173
+ "Eq": "equals",
174
+ "NotEq": "does not equal",
175
+ "Lt": "is less than",
176
+ "LtE": "is less than or equal to",
177
+ "Gt": "is greater than",
178
+ "GtE": "is greater than or equal to",
179
+ "In": "is in",
180
+ "NotIn": "is not in",
181
+ }
182
+
183
+ if ops and comparators:
184
+ op_name = op_map.get(ops[0], "matches")
185
+ return f"{left} {op_name} {comparators[0]}"
186
+
187
+ elif isinstance(test_node, ast.Name):
188
+ return f"{test_node.id} is true"
189
+
190
+ elif isinstance(test_node, ast.Call):
191
+ return f"{self._extract_expression(test_node.func)} is called"
192
+
193
+ return "condition is met"
194
+
195
+ @beartype
196
+ def _extract_expression(self, node: ast.AST) -> str:
197
+ """Extract human-readable expression from AST node."""
198
+ if isinstance(node, ast.Name):
199
+ return node.id
200
+ if isinstance(node, ast.Attribute):
201
+ return f"{self._extract_expression(node.value)}.{node.attr}"
202
+ if isinstance(node, ast.Constant):
203
+ return repr(node.value)
204
+ if isinstance(node, ast.Call):
205
+ func_name = self._extract_expression(node.func)
206
+ return f"{func_name}()"
207
+
208
+ return "value"
209
+
210
+ @beartype
211
+ def _negate_condition(self, condition: str) -> str:
212
+ """Negate a condition for else branch."""
213
+ if "equals" in condition:
214
+ return condition.replace("equals", "does not equal")
215
+ if "is true" in condition:
216
+ return condition.replace("is true", "is false")
217
+ if "is less than" in condition:
218
+ return condition.replace("is less than", "is greater than or equal to")
219
+ if "is greater than" in condition:
220
+ return condition.replace("is greater than", "is less than or equal to")
221
+
222
+ return f"not ({condition})"
223
+
224
+ @beartype
225
+ def _extract_action_from_body(self, body: Sequence[ast.AST]) -> str:
226
+ """Extract action description from method body."""
227
+ actions: list[str] = []
228
+
229
+ for node in body[:3]: # Limit to first 3 statements
230
+ if isinstance(node, ast.Return):
231
+ if node.value:
232
+ value = self._extract_expression(node.value)
233
+ actions.append(f"returns {value}")
234
+ else:
235
+ actions.append("returns None")
236
+ elif isinstance(node, ast.Assign):
237
+ if node.targets:
238
+ target = self._extract_expression(node.targets[0])
239
+ if node.value:
240
+ value = self._extract_expression(node.value)
241
+ actions.append(f"sets {target} to {value}")
242
+ elif isinstance(node, ast.Expr) and isinstance(node.value, ast.Call):
243
+ func_name = self._extract_expression(node.value.func)
244
+ actions.append(f"calls {func_name}")
245
+
246
+ return " and ".join(actions) if actions else "operation completes"
247
+
248
+ @beartype
249
+ def _extract_exception_type(self, type_node: ast.AST) -> str:
250
+ """Extract exception type name from AST node."""
251
+ if isinstance(type_node, ast.Name):
252
+ return type_node.id
253
+ if isinstance(type_node, ast.Tuple):
254
+ # Multiple exception types
255
+ types = [self._extract_exception_type(el) for el in type_node.elts]
256
+ return " or ".join(types)
257
+
258
+ return "Exception"
259
+
260
+ @beartype
261
+ def _has_retry_logic(self, body: Sequence[ast.AST] | None) -> bool:
262
+ """Check if body contains retry logic patterns."""
263
+ if not body:
264
+ return False
265
+
266
+ retry_keywords = ["retry", "retries", "again", "recover", "fallback"]
267
+ # Walk through body nodes directly
268
+ for node in body:
269
+ for subnode in ast.walk(node):
270
+ if isinstance(subnode, ast.Name) and subnode.id.lower() in retry_keywords:
271
+ return True
272
+ if isinstance(subnode, ast.Attribute) and subnode.attr.lower() in retry_keywords:
273
+ return True
274
+ if (
275
+ isinstance(subnode, ast.Constant)
276
+ and isinstance(subnode.value, str)
277
+ and any(keyword in subnode.value.lower() for keyword in retry_keywords)
278
+ ):
279
+ return True
280
+
281
+ return False
@@ -0,0 +1,337 @@
1
+ """Requirement extractor for generating complete requirements from code semantics."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import ast
6
+ import re
7
+
8
+ from beartype import beartype
9
+ from icontract import ensure, require
10
+
11
+
12
+ class RequirementExtractor:
13
+ """
14
+ Extracts complete requirements from code semantics.
15
+
16
+ Generates requirement statements in the format:
17
+ Subject + Modal verb + Action verb + Object + Outcome
18
+
19
+ Also extracts Non-Functional Requirements (NFRs) from code patterns.
20
+ """
21
+
22
+ # Modal verbs for requirement statements
23
+ MODAL_VERBS = ["must", "shall", "should", "will", "can", "may"]
24
+
25
+ # Action verbs commonly used in requirements
26
+ ACTION_VERBS = [
27
+ "provide",
28
+ "support",
29
+ "enable",
30
+ "allow",
31
+ "ensure",
32
+ "validate",
33
+ "handle",
34
+ "process",
35
+ "generate",
36
+ "extract",
37
+ "analyze",
38
+ "transform",
39
+ "store",
40
+ "retrieve",
41
+ "display",
42
+ "execute",
43
+ "implement",
44
+ "perform",
45
+ ]
46
+
47
+ # NFR patterns
48
+ PERFORMANCE_PATTERNS = [
49
+ "async",
50
+ "await",
51
+ "cache",
52
+ "parallel",
53
+ "concurrent",
54
+ "thread",
55
+ "pool",
56
+ "queue",
57
+ "batch",
58
+ "optimize",
59
+ "lazy",
60
+ "defer",
61
+ ]
62
+
63
+ SECURITY_PATTERNS = [
64
+ "auth",
65
+ "authenticate",
66
+ "authorize",
67
+ "encrypt",
68
+ "decrypt",
69
+ "hash",
70
+ "token",
71
+ "secret",
72
+ "password",
73
+ "credential",
74
+ "permission",
75
+ "role",
76
+ "access",
77
+ "secure",
78
+ ]
79
+
80
+ RELIABILITY_PATTERNS = [
81
+ "retry",
82
+ "retries",
83
+ "timeout",
84
+ "fallback",
85
+ "circuit",
86
+ "breaker",
87
+ "resilient",
88
+ "recover",
89
+ "error",
90
+ "exception",
91
+ "handle",
92
+ "validate",
93
+ "verify",
94
+ ]
95
+
96
+ MAINTAINABILITY_PATTERNS = [
97
+ "docstring",
98
+ "documentation",
99
+ "comment",
100
+ "type",
101
+ "hint",
102
+ "annotation",
103
+ "interface",
104
+ "abstract",
105
+ "protocol",
106
+ "test",
107
+ "mock",
108
+ "fixture",
109
+ ]
110
+
111
+ @beartype
112
+ def __init__(self) -> None:
113
+ """Initialize requirement extractor."""
114
+
115
+ @beartype
116
+ @require(lambda class_node: isinstance(class_node, ast.ClassDef), "Class must be ClassDef node")
117
+ @ensure(lambda result: isinstance(result, str), "Must return string")
118
+ def extract_complete_requirement(self, class_node: ast.ClassDef) -> str:
119
+ """
120
+ Extract complete requirement statement from class.
121
+
122
+ Format: Subject + Modal + Action + Object + Outcome
123
+
124
+ Args:
125
+ class_node: AST node for the class
126
+
127
+ Returns:
128
+ Complete requirement statement
129
+ """
130
+ # Extract subject (class name)
131
+ subject = self._humanize_name(class_node.name)
132
+
133
+ # Extract from docstring
134
+ docstring = ast.get_docstring(class_node)
135
+ if docstring:
136
+ requirement = self._parse_docstring_to_requirement(docstring, subject)
137
+ if requirement:
138
+ return requirement
139
+
140
+ # Extract from class name patterns
141
+ requirement = self._infer_requirement_from_name(class_node.name, subject)
142
+ if requirement:
143
+ return requirement
144
+
145
+ # Default requirement
146
+ return f"The system {subject.lower()} must provide {subject.lower()} functionality"
147
+
148
+ @beartype
149
+ @require(lambda method_node: isinstance(method_node, ast.FunctionDef), "Method must be FunctionDef node")
150
+ @ensure(lambda result: isinstance(result, str), "Must return string")
151
+ def extract_method_requirement(self, method_node: ast.FunctionDef, class_name: str) -> str:
152
+ """
153
+ Extract complete requirement statement from method.
154
+
155
+ Args:
156
+ method_node: AST node for the method
157
+ class_name: Name of the class containing the method
158
+
159
+ Returns:
160
+ Complete requirement statement
161
+ """
162
+ method_name = method_node.name
163
+ subject = class_name
164
+
165
+ # Extract from docstring
166
+ docstring = ast.get_docstring(method_node)
167
+ if docstring:
168
+ requirement = self._parse_docstring_to_requirement(docstring, subject, method_name)
169
+ if requirement:
170
+ return requirement
171
+
172
+ # Extract from method name patterns
173
+ requirement = self._infer_requirement_from_name(method_name, subject, method_name)
174
+ if requirement:
175
+ return requirement
176
+
177
+ # Default requirement
178
+ action = self._extract_action_from_method_name(method_name)
179
+ return f"The system {subject.lower()} must {action} {method_name.replace('_', ' ')}"
180
+
181
+ @beartype
182
+ @require(lambda class_node: isinstance(class_node, ast.ClassDef), "Class must be ClassDef node")
183
+ @ensure(lambda result: isinstance(result, list), "Must return list")
184
+ def extract_nfrs(self, class_node: ast.ClassDef) -> list[str]:
185
+ """
186
+ Extract Non-Functional Requirements from code patterns.
187
+
188
+ Args:
189
+ class_node: AST node for the class
190
+
191
+ Returns:
192
+ List of NFR statements
193
+ """
194
+ nfrs: list[str] = []
195
+
196
+ # Analyze class body for NFR patterns
197
+ class_code = ast.unparse(class_node) if hasattr(ast, "unparse") else str(class_node)
198
+ class_code_lower = class_code.lower()
199
+
200
+ # Performance NFRs
201
+ if any(pattern in class_code_lower for pattern in self.PERFORMANCE_PATTERNS):
202
+ nfrs.append("The system must meet performance requirements (async operations, caching, optimization)")
203
+
204
+ # Security NFRs
205
+ if any(pattern in class_code_lower for pattern in self.SECURITY_PATTERNS):
206
+ nfrs.append("The system must meet security requirements (authentication, authorization, encryption)")
207
+
208
+ # Reliability NFRs
209
+ if any(pattern in class_code_lower for pattern in self.RELIABILITY_PATTERNS):
210
+ nfrs.append("The system must meet reliability requirements (error handling, retry logic, resilience)")
211
+
212
+ # Maintainability NFRs
213
+ if any(pattern in class_code_lower for pattern in self.MAINTAINABILITY_PATTERNS):
214
+ nfrs.append("The system must meet maintainability requirements (documentation, type hints, testing)")
215
+
216
+ # Check for async methods
217
+ async_methods = [item for item in class_node.body if isinstance(item, ast.AsyncFunctionDef)]
218
+ if async_methods:
219
+ nfrs.append("The system must support asynchronous operations for improved performance")
220
+
221
+ # Check for type hints
222
+ has_type_hints = False
223
+ for item in class_node.body:
224
+ if isinstance(item, ast.FunctionDef) and (item.returns or any(arg.annotation for arg in item.args.args)):
225
+ has_type_hints = True
226
+ break
227
+ if has_type_hints:
228
+ nfrs.append("The system must use type hints for improved code maintainability and IDE support")
229
+
230
+ return nfrs
231
+
232
+ @beartype
233
+ def _parse_docstring_to_requirement(
234
+ self, docstring: str, subject: str, method_name: str | None = None
235
+ ) -> str | None:
236
+ """
237
+ Parse docstring to extract complete requirement statement.
238
+
239
+ Args:
240
+ docstring: Class or method docstring
241
+ subject: Subject of the requirement (class name)
242
+ method_name: Optional method name
243
+
244
+ Returns:
245
+ Complete requirement statement or None
246
+ """
247
+ # Clean docstring
248
+ docstring = docstring.strip()
249
+ first_sentence = docstring.split(".")[0].strip()
250
+
251
+ # Check if already in requirement format
252
+ if any(modal in first_sentence.lower() for modal in self.MODAL_VERBS):
253
+ # Already has modal verb, return as-is
254
+ return first_sentence
255
+
256
+ # Try to extract action and object
257
+ action_match = re.search(
258
+ r"(?:provides?|supports?|enables?|allows?|ensures?|validates?|handles?|processes?|generates?|extracts?|analyzes?|transforms?|stores?|retrieves?|displays?|executes?|implements?|performs?)\s+(.+?)(?:\.|$)",
259
+ first_sentence.lower(),
260
+ )
261
+ if action_match:
262
+ action = action_match.group(0).split()[0] # Get the action verb
263
+ object_part = action_match.group(1).strip()
264
+ return f"The system {subject.lower()} must {action} {object_part}"
265
+
266
+ # Try to extract from "This class/method..." pattern
267
+ this_match = re.search(
268
+ r"(?:this|the)\s+(?:class|method|function)\s+(?:provides?|supports?|enables?|allows?|ensures?)\s+(.+?)(?:\.|$)",
269
+ first_sentence.lower(),
270
+ )
271
+ if this_match:
272
+ object_part = this_match.group(1).strip()
273
+ action = "provide"
274
+ return f"The system {subject.lower()} must {action} {object_part}"
275
+
276
+ return None
277
+
278
+ @beartype
279
+ def _infer_requirement_from_name(self, name: str, subject: str, method_name: str | None = None) -> str | None:
280
+ """
281
+ Infer requirement from class or method name patterns.
282
+
283
+ Args:
284
+ name: Class or method name
285
+ subject: Subject of the requirement
286
+ method_name: Optional method name (for method requirements)
287
+
288
+ Returns:
289
+ Complete requirement statement or None
290
+ """
291
+ name_lower = name.lower()
292
+
293
+ # Validation patterns
294
+ if any(keyword in name_lower for keyword in ["validate", "check", "verify"]):
295
+ target = name.replace("validate", "").replace("check", "").replace("verify", "").strip()
296
+ return f"The system {subject.lower()} must validate {target.replace('_', ' ')}"
297
+
298
+ # Processing patterns
299
+ if any(keyword in name_lower for keyword in ["process", "handle", "manage"]):
300
+ target = name.replace("process", "").replace("handle", "").replace("manage", "").strip()
301
+ return f"The system {subject.lower()} must {name_lower.split('_')[0]} {target.replace('_', ' ')}"
302
+
303
+ # Get/Set patterns
304
+ if name_lower.startswith("get_"):
305
+ target = name.replace("get_", "").replace("_", " ")
306
+ return f"The system {subject.lower()} must retrieve {target}"
307
+
308
+ if name_lower.startswith(("set_", "update_")):
309
+ target = name.replace("set_", "").replace("update_", "").replace("_", " ")
310
+ return f"The system {subject.lower()} must update {target}"
311
+
312
+ return None
313
+
314
+ @beartype
315
+ def _extract_action_from_method_name(self, method_name: str) -> str:
316
+ """Extract action verb from method name."""
317
+ method_lower = method_name.lower()
318
+
319
+ for action in self.ACTION_VERBS:
320
+ if method_lower.startswith(action) or action in method_lower:
321
+ return action
322
+
323
+ # Default action
324
+ return "execute"
325
+
326
+ @beartype
327
+ def _humanize_name(self, name: str) -> str:
328
+ """Convert camelCase or snake_case to human-readable name."""
329
+ # Handle camelCase
330
+ if re.search(r"[a-z][A-Z]", name):
331
+ name = re.sub(r"([a-z])([A-Z])", r"\1 \2", name)
332
+
333
+ # Handle snake_case
334
+ name = name.replace("_", " ")
335
+
336
+ # Capitalize words
337
+ return " ".join(word.capitalize() for word in name.split())