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.
- specfact_cli/__init__.py +1 -1
- specfact_cli/agents/analyze_agent.py +2 -3
- specfact_cli/analyzers/__init__.py +2 -1
- specfact_cli/analyzers/ambiguity_scanner.py +601 -0
- specfact_cli/analyzers/code_analyzer.py +462 -30
- specfact_cli/analyzers/constitution_evidence_extractor.py +491 -0
- specfact_cli/analyzers/contract_extractor.py +419 -0
- specfact_cli/analyzers/control_flow_analyzer.py +281 -0
- specfact_cli/analyzers/requirement_extractor.py +337 -0
- specfact_cli/analyzers/test_pattern_extractor.py +330 -0
- specfact_cli/cli.py +151 -206
- specfact_cli/commands/constitution.py +281 -0
- specfact_cli/commands/enforce.py +42 -34
- specfact_cli/commands/import_cmd.py +481 -152
- specfact_cli/commands/init.py +224 -55
- specfact_cli/commands/plan.py +2133 -547
- specfact_cli/commands/repro.py +100 -78
- specfact_cli/commands/sync.py +701 -186
- specfact_cli/enrichers/constitution_enricher.py +765 -0
- specfact_cli/enrichers/plan_enricher.py +294 -0
- specfact_cli/importers/speckit_converter.py +364 -48
- specfact_cli/importers/speckit_scanner.py +65 -0
- specfact_cli/models/plan.py +42 -0
- specfact_cli/resources/mappings/node-async.yaml +49 -0
- specfact_cli/resources/mappings/python-async.yaml +47 -0
- specfact_cli/resources/mappings/speckit-default.yaml +82 -0
- specfact_cli/resources/prompts/specfact-enforce.md +185 -0
- specfact_cli/resources/prompts/specfact-import-from-code.md +626 -0
- specfact_cli/resources/prompts/specfact-plan-add-feature.md +188 -0
- specfact_cli/resources/prompts/specfact-plan-add-story.md +212 -0
- specfact_cli/resources/prompts/specfact-plan-compare.md +571 -0
- specfact_cli/resources/prompts/specfact-plan-init.md +531 -0
- specfact_cli/resources/prompts/specfact-plan-promote.md +352 -0
- specfact_cli/resources/prompts/specfact-plan-review.md +1276 -0
- specfact_cli/resources/prompts/specfact-plan-select.md +401 -0
- specfact_cli/resources/prompts/specfact-plan-update-feature.md +242 -0
- specfact_cli/resources/prompts/specfact-plan-update-idea.md +211 -0
- specfact_cli/resources/prompts/specfact-repro.md +268 -0
- specfact_cli/resources/prompts/specfact-sync.md +497 -0
- specfact_cli/resources/schemas/deviation.schema.json +61 -0
- specfact_cli/resources/schemas/plan.schema.json +204 -0
- specfact_cli/resources/schemas/protocol.schema.json +53 -0
- specfact_cli/resources/templates/github-action.yml.j2 +140 -0
- specfact_cli/resources/templates/plan.bundle.yaml.j2 +141 -0
- specfact_cli/resources/templates/pr-template.md.j2 +58 -0
- specfact_cli/resources/templates/protocol.yaml.j2 +24 -0
- specfact_cli/resources/templates/telemetry.yaml.example +35 -0
- specfact_cli/sync/__init__.py +10 -1
- specfact_cli/sync/watcher.py +268 -0
- specfact_cli/telemetry.py +440 -0
- specfact_cli/utils/acceptance_criteria.py +127 -0
- specfact_cli/utils/enrichment_parser.py +445 -0
- specfact_cli/utils/feature_keys.py +12 -3
- specfact_cli/utils/ide_setup.py +170 -0
- specfact_cli/utils/structure.py +179 -2
- specfact_cli/utils/yaml_utils.py +33 -0
- specfact_cli/validators/repro_checker.py +22 -1
- specfact_cli/validators/schema.py +15 -4
- specfact_cli-0.6.8.dist-info/METADATA +456 -0
- specfact_cli-0.6.8.dist-info/RECORD +99 -0
- {specfact_cli-0.4.2.dist-info → specfact_cli-0.6.8.dist-info}/entry_points.txt +1 -0
- specfact_cli-0.6.8.dist-info/licenses/LICENSE.md +202 -0
- specfact_cli-0.4.2.dist-info/METADATA +0 -370
- specfact_cli-0.4.2.dist-info/RECORD +0 -62
- specfact_cli-0.4.2.dist-info/licenses/LICENSE.md +0 -61
- {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())
|