ara-cli 0.1.9.69__py3-none-any.whl → 0.1.9.70__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.
Potentially problematic release.
This version of ara-cli might be problematic. Click here for more details.
- ara_cli/ara_command_action.py +6 -2
- ara_cli/ara_config.py +21 -7
- ara_cli/artefact_autofix.py +278 -23
- ara_cli/artefact_fuzzy_search.py +9 -4
- ara_cli/artefact_models/feature_artefact_model.py +72 -18
- ara_cli/artefact_reader.py +4 -3
- ara_cli/artefact_scan.py +27 -2
- ara_cli/file_classifier.py +2 -2
- ara_cli/prompt_handler.py +9 -10
- ara_cli/version.py +1 -1
- {ara_cli-0.1.9.69.dist-info → ara_cli-0.1.9.70.dist-info}/METADATA +1 -1
- {ara_cli-0.1.9.69.dist-info → ara_cli-0.1.9.70.dist-info}/RECORD +18 -18
- tests/{test_ara_autofix.py → test_artefact_autofix.py} +163 -29
- tests/test_artefact_scan.py +50 -17
- tests/test_file_classifier.py +1 -1
- {ara_cli-0.1.9.69.dist-info → ara_cli-0.1.9.70.dist-info}/WHEEL +0 -0
- {ara_cli-0.1.9.69.dist-info → ara_cli-0.1.9.70.dist-info}/entry_points.txt +0 -0
- {ara_cli-0.1.9.69.dist-info → ara_cli-0.1.9.70.dist-info}/top_level.txt +0 -0
ara_cli/ara_command_action.py
CHANGED
|
@@ -575,6 +575,7 @@ def scan_action(args):
|
|
|
575
575
|
|
|
576
576
|
def autofix_action(args):
|
|
577
577
|
from ara_cli.artefact_autofix import parse_report, apply_autofix, read_report_file
|
|
578
|
+
from ara_cli.file_classifier import FileClassifier
|
|
578
579
|
|
|
579
580
|
# If the user passes --non-deterministic, only_deterministic_fix becomes False.
|
|
580
581
|
# If the user passes --deterministic, only_non_deterministic_fix becomes False.
|
|
@@ -591,6 +592,9 @@ def autofix_action(args):
|
|
|
591
592
|
print("No issues found in the report. Nothing to fix.")
|
|
592
593
|
return
|
|
593
594
|
|
|
595
|
+
file_classifier = FileClassifier(os)
|
|
596
|
+
classified_artefact_info = file_classifier.classify_files()
|
|
597
|
+
|
|
594
598
|
# print("\nStarting autofix process...")
|
|
595
599
|
for classifier, files in issues.items():
|
|
596
600
|
print(f"\nClassifier: {classifier}")
|
|
@@ -600,8 +604,8 @@ def autofix_action(args):
|
|
|
600
604
|
classifier,
|
|
601
605
|
reason,
|
|
602
606
|
deterministic=run_deterministic,
|
|
603
|
-
non_deterministic=run_non_deterministic
|
|
607
|
+
non_deterministic=run_non_deterministic,
|
|
608
|
+
classified_artefact_info=classified_artefact_info
|
|
604
609
|
)
|
|
605
610
|
|
|
606
611
|
print("\nAutofix process completed. Please review the changes.")
|
|
607
|
-
|
ara_cli/ara_config.py
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
from typing import List, Dict,
|
|
1
|
+
from typing import List, Dict, Optional
|
|
2
2
|
from pydantic import BaseModel
|
|
3
3
|
import json
|
|
4
4
|
import os
|
|
@@ -10,6 +10,13 @@ from functools import lru_cache
|
|
|
10
10
|
DEFAULT_CONFIG_LOCATION = "./ara/.araconfig/ara_config.json"
|
|
11
11
|
|
|
12
12
|
|
|
13
|
+
class LLMConfigItem(BaseModel):
|
|
14
|
+
provider: str
|
|
15
|
+
model: str
|
|
16
|
+
temperature: float
|
|
17
|
+
max_tokens: Optional[int] = None
|
|
18
|
+
|
|
19
|
+
|
|
13
20
|
class ARAconfig(BaseModel):
|
|
14
21
|
ext_code_dirs: List[Dict[str, str]] = [
|
|
15
22
|
{"source_dir_1": "./src"},
|
|
@@ -36,42 +43,49 @@ class ARAconfig(BaseModel):
|
|
|
36
43
|
"*.jpg",
|
|
37
44
|
"*.jpeg",
|
|
38
45
|
]
|
|
39
|
-
llm_config: Dict[str,
|
|
46
|
+
llm_config: Dict[str, LLMConfigItem] = {
|
|
40
47
|
"gpt-4o": {
|
|
41
48
|
"provider": "openai",
|
|
42
49
|
"model": "openai/gpt-4o",
|
|
43
|
-
"temperature": 0.8
|
|
50
|
+
"temperature": 0.8,
|
|
51
|
+
"max_tokens": 16384
|
|
44
52
|
},
|
|
45
53
|
"gpt-4.1": {
|
|
46
54
|
"provider": "openai",
|
|
47
55
|
"model": "openai/gpt-4.1",
|
|
48
56
|
"temperature": 0.8,
|
|
57
|
+
"max_tokens": 1024
|
|
49
58
|
},
|
|
50
59
|
"o3-mini": {
|
|
51
60
|
"provider": "openai",
|
|
52
61
|
"model": "openai/o3-mini",
|
|
53
62
|
"temperature": 1.0,
|
|
63
|
+
"max_tokens": 1024
|
|
54
64
|
},
|
|
55
65
|
"opus-4": {
|
|
56
66
|
"provider": "anthropic",
|
|
57
67
|
"model": "anthropic/claude-opus-4-20250514",
|
|
58
68
|
"temperature": 0.8,
|
|
69
|
+
"max_tokens": 32000
|
|
59
70
|
},
|
|
60
71
|
"sonnet-4": {
|
|
61
72
|
"provider": "anthropic",
|
|
62
73
|
"model": "anthropic/claude-sonnet-4-20250514",
|
|
63
74
|
"temperature": 0.8,
|
|
75
|
+
"max_tokens": 1024
|
|
64
76
|
},
|
|
65
77
|
"together-ai-llama-2": {
|
|
66
78
|
"provider": "together_ai",
|
|
67
79
|
"model": "together_ai/togethercomputer/llama-2-70b",
|
|
68
80
|
"temperature": 0.8,
|
|
81
|
+
"max_tokens": 1024
|
|
69
82
|
},
|
|
70
83
|
"groq-llama-3": {
|
|
71
84
|
"provider": "groq",
|
|
72
85
|
"model": "groq/llama3-70b-8192",
|
|
73
86
|
"temperature": 0.8,
|
|
74
|
-
|
|
87
|
+
"max_tokens": 1024
|
|
88
|
+
}
|
|
75
89
|
}
|
|
76
90
|
default_llm: Optional[str] = "gpt-4o"
|
|
77
91
|
|
|
@@ -99,7 +113,7 @@ def read_data(filepath: str) -> ARAconfig:
|
|
|
99
113
|
default_config = ARAconfig()
|
|
100
114
|
|
|
101
115
|
with open(filepath, "w") as file:
|
|
102
|
-
json.dump(default_config.model_dump(), file, indent=4)
|
|
116
|
+
json.dump(default_config.model_dump(mode='json'), file, indent=4)
|
|
103
117
|
|
|
104
118
|
print(
|
|
105
119
|
f"ara-cli configuration file '{filepath}' created with default configuration. Please modify it as needed and re-run your command"
|
|
@@ -113,7 +127,7 @@ def read_data(filepath: str) -> ARAconfig:
|
|
|
113
127
|
# Function to save the modified configuration back to the JSON file
|
|
114
128
|
def save_data(filepath: str, config: ARAconfig):
|
|
115
129
|
with open(filepath, "w") as file:
|
|
116
|
-
json.dump(config.
|
|
130
|
+
json.dump(config.model_dump(mode='json'), file, indent=4)
|
|
117
131
|
|
|
118
132
|
|
|
119
133
|
# Singleton for configuration management
|
|
@@ -129,4 +143,4 @@ class ConfigManager:
|
|
|
129
143
|
makedirs(config_dir)
|
|
130
144
|
|
|
131
145
|
cls._config_instance = read_data(filepath)
|
|
132
|
-
return cls._config_instance
|
|
146
|
+
return cls._config_instance
|
ara_cli/artefact_autofix.py
CHANGED
|
@@ -1,5 +1,15 @@
|
|
|
1
|
+
from ara_cli.artefact_fuzzy_search import (
|
|
2
|
+
find_closest_name_matches,
|
|
3
|
+
extract_artefact_names_of_classifier,
|
|
4
|
+
)
|
|
5
|
+
from ara_cli.file_classifier import FileClassifier
|
|
6
|
+
from ara_cli.artefact_reader import ArtefactReader
|
|
7
|
+
from ara_cli.artefact_models.artefact_load import artefact_from_content
|
|
8
|
+
from ara_cli.artefact_models.artefact_model import Artefact
|
|
9
|
+
from typing import Optional, Dict, List, Tuple
|
|
10
|
+
import difflib
|
|
1
11
|
import os
|
|
2
|
-
|
|
12
|
+
|
|
3
13
|
|
|
4
14
|
def read_report_file():
|
|
5
15
|
file_path = "incompatible_artefacts_report.md"
|
|
@@ -7,7 +17,9 @@ def read_report_file():
|
|
|
7
17
|
with open(file_path, "r", encoding="utf-8") as f:
|
|
8
18
|
content = f.read()
|
|
9
19
|
except OSError:
|
|
10
|
-
print(
|
|
20
|
+
print(
|
|
21
|
+
'Artefact scan results file not found. Did you run the "ara scan" command?'
|
|
22
|
+
)
|
|
11
23
|
return None
|
|
12
24
|
return content
|
|
13
25
|
|
|
@@ -23,9 +35,11 @@ def parse_report(content: str) -> Dict[str, List[Tuple[str, str]]]:
|
|
|
23
35
|
|
|
24
36
|
if not lines or lines[0] != "# Artefact Check Report":
|
|
25
37
|
return issues
|
|
38
|
+
return issues
|
|
26
39
|
|
|
27
40
|
if len(lines) >= 3 and lines[2] == "No problems found.":
|
|
28
41
|
return issues
|
|
42
|
+
return issues
|
|
29
43
|
|
|
30
44
|
for line in lines[1:]:
|
|
31
45
|
line = line.strip()
|
|
@@ -51,7 +65,7 @@ def parse_report(content: str) -> Dict[str, List[Tuple[str, str]]]:
|
|
|
51
65
|
def read_artefact(file_path):
|
|
52
66
|
"""Reads the artefact text from the given file path."""
|
|
53
67
|
try:
|
|
54
|
-
with open(file_path,
|
|
68
|
+
with open(file_path, "r", encoding="utf-8") as file:
|
|
55
69
|
return file.read()
|
|
56
70
|
except FileNotFoundError:
|
|
57
71
|
print(f"File not found: {file_path}")
|
|
@@ -84,7 +98,7 @@ def construct_prompt(artefact_type, reason, file_path, artefact_text):
|
|
|
84
98
|
"Provide the corrected artefact. Do not reformulate the artefact, "
|
|
85
99
|
"just fix the pydantic model errors, use correct grammar. "
|
|
86
100
|
"You should follow the name of the file "
|
|
87
|
-
f"from its path {file_path} for naming the
|
|
101
|
+
f"from its path {file_path} for naming the artefact's title. "
|
|
88
102
|
"You are not allowed to use file extention in the artefact title. "
|
|
89
103
|
"You are not allowed to modify, delete or add tags. "
|
|
90
104
|
"User tag should be '@user_<username>'. The pydantic model already provides the '@user_' prefix. "
|
|
@@ -97,43 +111,234 @@ def construct_prompt(artefact_type, reason, file_path, artefact_text):
|
|
|
97
111
|
"then just delete those action items."
|
|
98
112
|
)
|
|
99
113
|
|
|
100
|
-
prompt +=
|
|
101
|
-
"\nThe current artefact is:\n"
|
|
102
|
-
"```\n"
|
|
103
|
-
f"{artefact_text}\n"
|
|
104
|
-
"```"
|
|
105
|
-
)
|
|
114
|
+
prompt += "\nThe current artefact is:\n" "```\n" f"{artefact_text}\n" "```"
|
|
106
115
|
|
|
107
116
|
return prompt
|
|
108
117
|
|
|
109
118
|
|
|
110
119
|
def run_agent(prompt, artefact_class):
|
|
111
120
|
from pydantic_ai import Agent
|
|
121
|
+
|
|
112
122
|
# gpt-4o
|
|
113
123
|
# anthropic:claude-3-7-sonnet-20250219
|
|
114
124
|
# anthropic:claude-4-sonnet-20250514
|
|
115
|
-
agent = Agent(
|
|
116
|
-
|
|
125
|
+
agent = Agent(
|
|
126
|
+
model="anthropic:claude-4-sonnet-20250514",
|
|
127
|
+
result_type=artefact_class,
|
|
128
|
+
instrument=True,
|
|
129
|
+
)
|
|
117
130
|
result = agent.run_sync(prompt)
|
|
118
131
|
return result.data
|
|
119
132
|
|
|
120
133
|
|
|
121
134
|
def write_corrected_artefact(file_path, corrected_text):
|
|
122
|
-
with open(file_path,
|
|
135
|
+
with open(file_path, "w", encoding="utf-8") as file:
|
|
123
136
|
file.write(corrected_text)
|
|
124
137
|
print(f"Fixed artefact at {file_path}")
|
|
125
138
|
|
|
126
139
|
|
|
127
|
-
def
|
|
140
|
+
def ask_for_correct_contribution(
|
|
141
|
+
artefact_info: Optional[tuple[str, str]] = None
|
|
142
|
+
) -> tuple[str, str]:
|
|
143
|
+
"""
|
|
144
|
+
Ask the user to provide a valid contribution when no match can be found.
|
|
145
|
+
|
|
146
|
+
Args:
|
|
147
|
+
artefact_info: Optional tuple containing (artefact_name, artefact_classifier)
|
|
148
|
+
|
|
149
|
+
Returns:
|
|
150
|
+
A tuple of (name, classifier) for the contribution
|
|
151
|
+
"""
|
|
152
|
+
|
|
153
|
+
artefact_name, artefact_classifier = (
|
|
154
|
+
artefact_info if artefact_info else (None, None)
|
|
155
|
+
)
|
|
156
|
+
contribution_message = (
|
|
157
|
+
f"of {artefact_classifier} artefact '{artefact_name}'" if artefact_name else ""
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
print(
|
|
161
|
+
f"Can not determine a match for contribution {contribution_message}. "
|
|
162
|
+
f"Please provide a valid contribution or contribution will be empty (<classifier> <file_name>)."
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
user_input = input().strip()
|
|
166
|
+
|
|
167
|
+
if not user_input:
|
|
168
|
+
return None, None
|
|
169
|
+
|
|
170
|
+
parts = user_input.split(maxsplit=1)
|
|
171
|
+
if len(parts) != 2:
|
|
172
|
+
print("Invalid input format. Expected: <classifier> <file_name>")
|
|
173
|
+
return None, None
|
|
174
|
+
|
|
175
|
+
classifier, name = parts
|
|
176
|
+
return name, classifier
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def ask_for_contribution_choice(
|
|
180
|
+
choices, artefact_info: Optional[tuple[str, str]] = None
|
|
181
|
+
) -> Optional[str]:
|
|
182
|
+
artefact_name, artefact_classifier = artefact_info
|
|
183
|
+
message = "Found multiple close matches for the contribution"
|
|
184
|
+
if artefact_name and artefact_classifier:
|
|
185
|
+
message += f" of the {artefact_classifier} '{artefact_name}'"
|
|
186
|
+
print(f"{message}.")
|
|
187
|
+
for i, contribution in enumerate(choices):
|
|
188
|
+
print(f"{i + 1}: {contribution}")
|
|
189
|
+
choice_number = input(
|
|
190
|
+
"Please choose the artefact to use for contribution (enter number): "
|
|
191
|
+
)
|
|
192
|
+
try:
|
|
193
|
+
choice_index = int(choice_number) - 1
|
|
194
|
+
if choice_index < 0 or choice_index >= len(choices):
|
|
195
|
+
print("Invalid choice. Aborting contribution choice.")
|
|
196
|
+
return None
|
|
197
|
+
choice = choices[choice_index]
|
|
198
|
+
except ValueError:
|
|
199
|
+
print("Invalid input. Aborting contribution choice.")
|
|
200
|
+
return None
|
|
201
|
+
return choice
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
def _has_valid_contribution(artefact: Artefact) -> bool:
|
|
205
|
+
contribution = artefact.contribution
|
|
206
|
+
return contribution and contribution.artefact_name and contribution.classifier
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
def _update_rule(
|
|
210
|
+
artefact: Artefact, name: str, classifier: str, classified_file_info: dict
|
|
211
|
+
) -> None:
|
|
212
|
+
"""Updates the rule in the contribution if a close match is found."""
|
|
213
|
+
rule = artefact.contribution.rule
|
|
214
|
+
|
|
215
|
+
content, artefact_data = ArtefactReader.read_artefact(
|
|
216
|
+
artefact_name=name,
|
|
217
|
+
classifier=classifier,
|
|
218
|
+
classified_file_info=classified_file_info,
|
|
219
|
+
)
|
|
220
|
+
|
|
221
|
+
parent = artefact_from_content(content=content)
|
|
222
|
+
rules = parent.rules
|
|
223
|
+
|
|
224
|
+
closest_rule_match = difflib.get_close_matches(rule, rules, cutoff=0.5)
|
|
225
|
+
if closest_rule_match:
|
|
226
|
+
artefact.contribution.rule = closest_rule_match[0]
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
def _set_contribution_multiple_matches(
|
|
230
|
+
artefact: Artefact,
|
|
231
|
+
closest_matches: list,
|
|
232
|
+
artefact_tuple: tuple,
|
|
233
|
+
classified_file_info: dict,
|
|
234
|
+
) -> tuple[Artefact, bool]:
|
|
235
|
+
contribution = artefact.contribution
|
|
236
|
+
classifier = contribution.classifier
|
|
237
|
+
original_name = contribution.artefact_name
|
|
238
|
+
|
|
239
|
+
closest_match = closest_matches[0]
|
|
240
|
+
if len(closest_matches) > 1:
|
|
241
|
+
closest_match = ask_for_contribution_choice(closest_matches, artefact_tuple)
|
|
242
|
+
|
|
243
|
+
if not closest_match:
|
|
244
|
+
print(
|
|
245
|
+
f"Contribution of {artefact_tuple[1]} '{artefact_tuple[0]}' will be empty."
|
|
246
|
+
)
|
|
247
|
+
artefact.contribution = None
|
|
248
|
+
return artefact, True
|
|
249
|
+
|
|
250
|
+
print(
|
|
251
|
+
f"Updating contribution of {artefact_tuple[1]} '{artefact_tuple[0]}' to {classifier} '{closest_match}'"
|
|
252
|
+
)
|
|
253
|
+
contribution.artefact_name = closest_match
|
|
254
|
+
artefact.contribution = contribution
|
|
255
|
+
|
|
256
|
+
if contribution.rule:
|
|
257
|
+
_update_rule(artefact, original_name, classifier, classified_file_info)
|
|
258
|
+
|
|
259
|
+
return artefact, True
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
def set_closest_contribution(
|
|
263
|
+
artefact: Artefact, classified_file_info=None
|
|
264
|
+
) -> tuple[Artefact, bool]:
|
|
265
|
+
if not _has_valid_contribution(artefact):
|
|
266
|
+
return artefact, False
|
|
267
|
+
contribution = artefact.contribution
|
|
268
|
+
name = contribution.artefact_name
|
|
269
|
+
classifier = contribution.classifier
|
|
270
|
+
rule = contribution.rule
|
|
271
|
+
|
|
272
|
+
if not classified_file_info:
|
|
273
|
+
file_classifier = FileClassifier(os)
|
|
274
|
+
classified_file_info = file_classifier.classify_files()
|
|
275
|
+
|
|
276
|
+
all_artefact_names = extract_artefact_names_of_classifier(
|
|
277
|
+
classified_files=classified_file_info, classifier=classifier
|
|
278
|
+
)
|
|
279
|
+
closest_matches = find_closest_name_matches(
|
|
280
|
+
artefact_name=name, all_artefact_names=all_artefact_names
|
|
281
|
+
)
|
|
282
|
+
|
|
283
|
+
artefact_tuple = (artefact.title, artefact._artefact_type().value)
|
|
284
|
+
|
|
285
|
+
if not closest_matches:
|
|
286
|
+
name, classifier = ask_for_correct_contribution(artefact_tuple)
|
|
287
|
+
if not name or not classifier:
|
|
288
|
+
artefact.contribution = None
|
|
289
|
+
return artefact, True
|
|
290
|
+
print(f"Updating contribution of {artefact._artefact_type().value} '{artefact.title}' to {classifier} '{name}'")
|
|
291
|
+
contribution.artefact_name = name
|
|
292
|
+
contribution.classifier = classifier
|
|
293
|
+
artefact.contribution = contribution
|
|
294
|
+
return artefact, True
|
|
295
|
+
|
|
296
|
+
if closest_matches[0] == name:
|
|
297
|
+
return artefact, False
|
|
298
|
+
|
|
299
|
+
return _set_contribution_multiple_matches(
|
|
300
|
+
artefact=artefact,
|
|
301
|
+
closest_matches=closest_matches,
|
|
302
|
+
artefact_tuple=artefact_tuple,
|
|
303
|
+
classified_file_info=classified_file_info,
|
|
304
|
+
)
|
|
305
|
+
|
|
306
|
+
print(
|
|
307
|
+
f"Updating contribution of {artefact._artefact_type().value} '{artefact.title}' to {classifier} '{closest_match}'"
|
|
308
|
+
)
|
|
309
|
+
contribution.artefact_name = closest_match
|
|
310
|
+
artefact.contribution = contribution
|
|
311
|
+
|
|
312
|
+
if not rule:
|
|
313
|
+
return artefact, True
|
|
314
|
+
|
|
315
|
+
content, artefact = ArtefactReader.read_artefact(
|
|
316
|
+
artefact_name=name,
|
|
317
|
+
classifier=classifier,
|
|
318
|
+
classified_file_info=classified_file_info,
|
|
319
|
+
)
|
|
320
|
+
parent = artefact_from_content(content=content)
|
|
321
|
+
rules = parent.rules
|
|
322
|
+
|
|
323
|
+
closest_rule_match = difflib.get_close_matches(rule, rules, cutoff=0.5)
|
|
324
|
+
if closest_rule_match:
|
|
325
|
+
contribution.rule = closest_rule_match
|
|
326
|
+
artefact.contribution = contribution
|
|
327
|
+
return artefact, True
|
|
328
|
+
|
|
329
|
+
|
|
330
|
+
def fix_title_mismatch(
|
|
331
|
+
file_path: str, artefact_text: str, artefact_class, **kwargs
|
|
332
|
+
) -> str:
|
|
128
333
|
"""
|
|
129
334
|
Deterministically fixes the title in the artefact text to match the filename.
|
|
130
335
|
"""
|
|
131
336
|
base_name = os.path.basename(file_path)
|
|
132
337
|
correct_title_underscores, _ = os.path.splitext(base_name)
|
|
133
|
-
correct_title_spaces = correct_title_underscores.replace(
|
|
338
|
+
correct_title_spaces = correct_title_underscores.replace("_", " ")
|
|
134
339
|
|
|
135
340
|
title_prefix = artefact_class._title_prefix()
|
|
136
|
-
|
|
341
|
+
|
|
137
342
|
lines = artefact_text.splitlines()
|
|
138
343
|
new_lines = []
|
|
139
344
|
title_found_and_replaced = False
|
|
@@ -144,15 +349,40 @@ def fix_title_mismatch(file_path: str, artefact_text: str, artefact_class) -> st
|
|
|
144
349
|
title_found_and_replaced = True
|
|
145
350
|
else:
|
|
146
351
|
new_lines.append(line)
|
|
147
|
-
|
|
352
|
+
|
|
148
353
|
if not title_found_and_replaced:
|
|
149
|
-
print(
|
|
354
|
+
print(
|
|
355
|
+
f"Warning: Title prefix '{title_prefix}' not found in {file_path}. Title could not be fixed."
|
|
356
|
+
)
|
|
150
357
|
return artefact_text
|
|
151
358
|
|
|
152
359
|
return "\n".join(new_lines)
|
|
153
360
|
|
|
154
361
|
|
|
155
|
-
def
|
|
362
|
+
def fix_contribution(
|
|
363
|
+
file_path: str,
|
|
364
|
+
artefact_text: str,
|
|
365
|
+
artefact_class: str,
|
|
366
|
+
classified_artefact_info: dict,
|
|
367
|
+
**kwargs,
|
|
368
|
+
):
|
|
369
|
+
if not classified_artefact_info:
|
|
370
|
+
file_classifier = FileClassifier(os)
|
|
371
|
+
classified_artefact_info = file_classifier.classify_files()
|
|
372
|
+
artefact = artefact_class.deserialize(artefact_text)
|
|
373
|
+
artefact, _ = set_closest_contribution(artefact)
|
|
374
|
+
artefact_text = artefact.serialize()
|
|
375
|
+
return artefact_text
|
|
376
|
+
|
|
377
|
+
|
|
378
|
+
def apply_autofix(
|
|
379
|
+
file_path: str,
|
|
380
|
+
classifier: str,
|
|
381
|
+
reason: str,
|
|
382
|
+
deterministic: bool = True,
|
|
383
|
+
non_deterministic: bool = True,
|
|
384
|
+
classified_artefact_info: Optional[Dict[str, List[Dict[str, str]]]] = None,
|
|
385
|
+
) -> bool:
|
|
156
386
|
artefact_text = read_artefact(file_path)
|
|
157
387
|
if artefact_text is None:
|
|
158
388
|
return False
|
|
@@ -161,11 +391,36 @@ def apply_autofix(file_path: str, classifier: str, reason: str, deterministic: b
|
|
|
161
391
|
if artefact_type is None or artefact_class is None:
|
|
162
392
|
return False
|
|
163
393
|
|
|
164
|
-
|
|
394
|
+
if classified_artefact_info is None:
|
|
395
|
+
file_classifier = FileClassifier(os)
|
|
396
|
+
classified_file_info = file_classifier.classified_files()
|
|
397
|
+
|
|
398
|
+
deterministic_markers_to_functions = {
|
|
399
|
+
"Filename-Title Mismatch": fix_title_mismatch,
|
|
400
|
+
"Invalid Contribution Reference": fix_contribution,
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
try:
|
|
404
|
+
deterministic_issue = next(
|
|
405
|
+
(
|
|
406
|
+
marker
|
|
407
|
+
for marker in deterministic_markers_to_functions.keys()
|
|
408
|
+
if marker in reason
|
|
409
|
+
),
|
|
410
|
+
None,
|
|
411
|
+
)
|
|
412
|
+
except StopIteration:
|
|
413
|
+
pass
|
|
414
|
+
is_deterministic_issue = deterministic_issue is not None
|
|
165
415
|
|
|
166
416
|
if deterministic and is_deterministic_issue:
|
|
167
417
|
print(f"Attempting deterministic fix for {file_path}...")
|
|
168
|
-
corrected_text =
|
|
418
|
+
corrected_text = deterministic_markers_to_functions[deterministic_issue](
|
|
419
|
+
file_path=file_path,
|
|
420
|
+
artefact_text=artefact_text,
|
|
421
|
+
artefact_class=artefact_class,
|
|
422
|
+
classified_artefact_info=classified_artefact_info,
|
|
423
|
+
)
|
|
169
424
|
write_corrected_artefact(file_path, corrected_text)
|
|
170
425
|
return True
|
|
171
426
|
|
|
@@ -181,11 +436,11 @@ def apply_autofix(file_path: str, classifier: str, reason: str, deterministic: b
|
|
|
181
436
|
except Exception as e:
|
|
182
437
|
print(f"LLM agent failed to fix artefact at {file_path}: {e}")
|
|
183
438
|
return False
|
|
184
|
-
|
|
439
|
+
|
|
185
440
|
# Log if a fix was skipped due to flags
|
|
186
441
|
if is_deterministic_issue and not deterministic:
|
|
187
442
|
print(f"Skipping deterministic fix for {file_path} as per request.")
|
|
188
443
|
elif not is_deterministic_issue and not non_deterministic:
|
|
189
444
|
print(f"Skipping non-deterministic fix for {file_path} as per request.")
|
|
190
445
|
|
|
191
|
-
return False
|
|
446
|
+
return False
|
ara_cli/artefact_fuzzy_search.py
CHANGED
|
@@ -33,12 +33,17 @@ def suggest_close_name_matches_for_parent(artefact_name: str, all_artefact_names
|
|
|
33
33
|
)
|
|
34
34
|
|
|
35
35
|
|
|
36
|
-
def
|
|
37
|
-
closest_matches = difflib.get_close_matches(artefact_name, all_artefact_names, cutoff=0.5
|
|
36
|
+
def find_closest_name_matches(artefact_name: str, all_artefact_names: list[str]) -> Optional[str]:
|
|
37
|
+
closest_matches = difflib.get_close_matches(artefact_name, all_artefact_names, cutoff=0.5)
|
|
38
38
|
if not closest_matches:
|
|
39
39
|
return None
|
|
40
|
-
|
|
41
|
-
|
|
40
|
+
return closest_matches
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def extract_artefact_names_of_classifier(classified_files: dict[str, list[dict]], classifier: str):
|
|
44
|
+
artefact_info_of_classifier = classified_files.get(classifier, [])
|
|
45
|
+
titles = list(map(lambda artefact: artefact['title'], artefact_info_of_classifier))
|
|
46
|
+
return titles
|
|
42
47
|
|
|
43
48
|
|
|
44
49
|
def find_closest_rule(parent_artefact: 'Artefact', rule: str):
|
|
@@ -363,27 +363,43 @@ class FeatureArtefact(Artefact):
|
|
|
363
363
|
|
|
364
364
|
@classmethod
|
|
365
365
|
def deserialize(cls, text: str) -> 'FeatureArtefact':
|
|
366
|
-
|
|
366
|
+
"""
|
|
367
|
+
Deserializes the feature file using a robust extract-and-reinject strategy.
|
|
368
|
+
1. Hides all docstrings by replacing them with placeholders.
|
|
369
|
+
2. Parses the sanitized text using the original, simple parsing logic.
|
|
370
|
+
3. Re-injects the original docstring content back into the parsed objects.
|
|
371
|
+
This prevents the parser from ever being confused by content within docstrings.
|
|
372
|
+
"""
|
|
373
|
+
# 1. Hide all docstrings from the entire file text first.
|
|
374
|
+
sanitized_text, docstrings = cls._hide_docstrings(text)
|
|
375
|
+
|
|
376
|
+
# 2. Perform the original parsing logic on the SANITIZED text.
|
|
377
|
+
# This part of the code is now "safe" because it will never see a docstring.
|
|
378
|
+
fields = super()._parse_common_fields(sanitized_text)
|
|
379
|
+
intent = FeatureIntent.deserialize(sanitized_text)
|
|
380
|
+
background = cls.deserialize_background(sanitized_text)
|
|
381
|
+
scenarios = cls.deserialize_scenarios(sanitized_text)
|
|
367
382
|
|
|
368
|
-
intent = FeatureIntent.deserialize(text)
|
|
369
|
-
background = cls.deserialize_background(text)
|
|
370
|
-
scenarios = cls.deserialize_scenarios(text)
|
|
371
|
-
|
|
372
|
-
fields['scenarios'] = scenarios
|
|
373
|
-
fields['background'] = background
|
|
374
383
|
fields['intent'] = intent
|
|
384
|
+
fields['background'] = background
|
|
385
|
+
fields['scenarios'] = scenarios
|
|
386
|
+
|
|
387
|
+
# 3. Re-inject the docstrings back into the parsed scenarios.
|
|
388
|
+
if fields['scenarios'] and docstrings:
|
|
389
|
+
for scenario in fields['scenarios']:
|
|
390
|
+
if isinstance(scenario, (Scenario, ScenarioOutline)):
|
|
391
|
+
scenario.steps = cls._reinject_docstrings_into_steps(scenario.steps, docstrings)
|
|
375
392
|
|
|
376
393
|
return cls(**fields)
|
|
377
394
|
|
|
378
395
|
@classmethod
|
|
379
396
|
def deserialize_scenarios(cls, text):
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
397
|
+
if not text: return []
|
|
398
|
+
lines = [line.strip() for line in text.strip().splitlines() if line.strip()]
|
|
383
399
|
scenarios = []
|
|
384
400
|
idx = 0
|
|
385
401
|
while idx < len(lines):
|
|
386
|
-
line = lines[idx]
|
|
402
|
+
line = lines[idx]
|
|
387
403
|
if line.startswith('Scenario:'):
|
|
388
404
|
scenario, next_idx = Scenario.from_lines(lines, idx)
|
|
389
405
|
scenarios.append(scenario)
|
|
@@ -398,16 +414,54 @@ class FeatureArtefact(Artefact):
|
|
|
398
414
|
|
|
399
415
|
@classmethod
|
|
400
416
|
def deserialize_background(cls, text):
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
417
|
+
if not text: return None
|
|
418
|
+
lines = [line.strip() for line in text.strip().splitlines() if line.strip()]
|
|
404
419
|
background = None
|
|
405
420
|
idx = 0
|
|
406
421
|
while idx < len(lines):
|
|
407
|
-
line = lines[idx]
|
|
422
|
+
line = lines[idx]
|
|
408
423
|
if line.startswith('Background:'):
|
|
409
|
-
background,
|
|
424
|
+
background, _ = Background.from_lines(lines, idx)
|
|
410
425
|
break
|
|
411
|
-
|
|
412
|
-
idx += 1
|
|
426
|
+
idx += 1
|
|
413
427
|
return background
|
|
428
|
+
|
|
429
|
+
|
|
430
|
+
@staticmethod
|
|
431
|
+
def _hide_docstrings(text: str) -> Tuple[str, Dict[str, str]]:
|
|
432
|
+
"""
|
|
433
|
+
Finds all docstring blocks ('''...''') in the text,
|
|
434
|
+
replaces them with a unique placeholder, and returns the sanitized
|
|
435
|
+
text and a dictionary mapping placeholders to the original docstrings.
|
|
436
|
+
"""
|
|
437
|
+
docstrings = {}
|
|
438
|
+
placeholder_template = "__DOCSTRING_PLACEHOLDER_{}__"
|
|
439
|
+
|
|
440
|
+
def replacer(match):
|
|
441
|
+
# This function is called for each found docstring.
|
|
442
|
+
key = placeholder_template.format(len(docstrings))
|
|
443
|
+
docstrings[key] = match.group(0) # Store the full matched docstring
|
|
444
|
+
return key
|
|
445
|
+
|
|
446
|
+
# The regex finds ''' followed by any character (including newlines)
|
|
447
|
+
# in a non-greedy way (.*?) until the next '''.
|
|
448
|
+
sanitized_text = re.sub(r'"""[\s\S]*?"""', replacer, text)
|
|
449
|
+
|
|
450
|
+
return sanitized_text, docstrings
|
|
451
|
+
|
|
452
|
+
@staticmethod
|
|
453
|
+
def _reinject_docstrings_into_steps(steps: List[str], docstrings: Dict[str, str]) -> List[str]:
|
|
454
|
+
"""
|
|
455
|
+
Iterates through a list of steps, finds any placeholders,
|
|
456
|
+
and replaces them with their original docstring content.
|
|
457
|
+
"""
|
|
458
|
+
rehydrated_steps = []
|
|
459
|
+
for step in steps:
|
|
460
|
+
for key, value in docstrings.items():
|
|
461
|
+
if key in step:
|
|
462
|
+
# Replace the placeholder with the original, full docstring block.
|
|
463
|
+
# This handles cases where the step is just the placeholder,
|
|
464
|
+
# or the placeholder is at the end of a line (e.g., "Then I see... __PLACEHOLDER__").
|
|
465
|
+
step = step.replace(key, value)
|
|
466
|
+
rehydrated_steps.append(step)
|
|
467
|
+
return rehydrated_steps
|
ara_cli/artefact_reader.py
CHANGED
|
@@ -10,13 +10,14 @@ import re
|
|
|
10
10
|
|
|
11
11
|
class ArtefactReader:
|
|
12
12
|
@staticmethod
|
|
13
|
-
def read_artefact(artefact_name, classifier) -> tuple[str, dict[str, str]]:
|
|
13
|
+
def read_artefact(artefact_name, classifier, classified_file_info = None) -> tuple[str, dict[str, str]]:
|
|
14
14
|
if not Classifier.is_valid_classifier(classifier):
|
|
15
15
|
print("Invalid classifier provided. Please provide a valid classifier.")
|
|
16
16
|
return None, None
|
|
17
17
|
|
|
18
|
-
|
|
19
|
-
|
|
18
|
+
if not classified_file_info:
|
|
19
|
+
file_classifier = FileClassifier(os)
|
|
20
|
+
classified_file_info = file_classifier.classify_files()
|
|
20
21
|
artefact_info_of_classifier = classified_file_info.get(classifier, [])
|
|
21
22
|
|
|
22
23
|
for artefact_info in artefact_info_of_classifier:
|