ara-cli 0.1.9.69__py3-none-any.whl → 0.1.10.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.
Potentially problematic release.
This version of ara-cli might be problematic. Click here for more details.
- ara_cli/__init__.py +18 -2
- ara_cli/__main__.py +248 -62
- ara_cli/ara_command_action.py +155 -86
- ara_cli/ara_config.py +226 -80
- ara_cli/ara_subcommands/__init__.py +0 -0
- ara_cli/ara_subcommands/autofix.py +26 -0
- ara_cli/ara_subcommands/chat.py +27 -0
- ara_cli/ara_subcommands/classifier_directory.py +16 -0
- ara_cli/ara_subcommands/common.py +100 -0
- ara_cli/ara_subcommands/create.py +75 -0
- ara_cli/ara_subcommands/delete.py +22 -0
- ara_cli/ara_subcommands/extract.py +22 -0
- ara_cli/ara_subcommands/fetch_templates.py +14 -0
- ara_cli/ara_subcommands/list.py +65 -0
- ara_cli/ara_subcommands/list_tags.py +25 -0
- ara_cli/ara_subcommands/load.py +48 -0
- ara_cli/ara_subcommands/prompt.py +136 -0
- ara_cli/ara_subcommands/read.py +47 -0
- ara_cli/ara_subcommands/read_status.py +20 -0
- ara_cli/ara_subcommands/read_user.py +20 -0
- ara_cli/ara_subcommands/reconnect.py +27 -0
- ara_cli/ara_subcommands/rename.py +22 -0
- ara_cli/ara_subcommands/scan.py +14 -0
- ara_cli/ara_subcommands/set_status.py +22 -0
- ara_cli/ara_subcommands/set_user.py +22 -0
- ara_cli/ara_subcommands/template.py +16 -0
- ara_cli/artefact_autofix.py +649 -68
- ara_cli/artefact_creator.py +8 -11
- ara_cli/artefact_deleter.py +2 -4
- ara_cli/artefact_fuzzy_search.py +22 -10
- ara_cli/artefact_link_updater.py +4 -4
- ara_cli/artefact_lister.py +29 -55
- ara_cli/artefact_models/artefact_data_retrieval.py +23 -0
- ara_cli/artefact_models/artefact_load.py +11 -3
- ara_cli/artefact_models/artefact_model.py +146 -39
- ara_cli/artefact_models/artefact_templates.py +70 -44
- ara_cli/artefact_models/businessgoal_artefact_model.py +23 -25
- ara_cli/artefact_models/epic_artefact_model.py +34 -26
- ara_cli/artefact_models/feature_artefact_model.py +203 -64
- ara_cli/artefact_models/keyfeature_artefact_model.py +21 -24
- ara_cli/artefact_models/serialize_helper.py +1 -1
- ara_cli/artefact_models/task_artefact_model.py +83 -15
- ara_cli/artefact_models/userstory_artefact_model.py +37 -27
- ara_cli/artefact_models/vision_artefact_model.py +23 -42
- ara_cli/artefact_reader.py +92 -91
- ara_cli/artefact_renamer.py +8 -4
- ara_cli/artefact_scan.py +66 -3
- ara_cli/chat.py +622 -162
- ara_cli/chat_agent/__init__.py +0 -0
- ara_cli/chat_agent/agent_communicator.py +62 -0
- ara_cli/chat_agent/agent_process_manager.py +211 -0
- ara_cli/chat_agent/agent_status_manager.py +73 -0
- ara_cli/chat_agent/agent_workspace_manager.py +76 -0
- ara_cli/commands/__init__.py +0 -0
- ara_cli/commands/command.py +7 -0
- ara_cli/commands/extract_command.py +15 -0
- ara_cli/commands/load_command.py +65 -0
- ara_cli/commands/load_image_command.py +34 -0
- ara_cli/commands/read_command.py +117 -0
- ara_cli/completers.py +144 -0
- ara_cli/directory_navigator.py +37 -4
- ara_cli/error_handler.py +134 -0
- ara_cli/file_classifier.py +6 -5
- ara_cli/file_lister.py +1 -1
- ara_cli/file_loaders/__init__.py +0 -0
- ara_cli/file_loaders/binary_file_loader.py +33 -0
- ara_cli/file_loaders/document_file_loader.py +34 -0
- ara_cli/file_loaders/document_reader.py +245 -0
- ara_cli/file_loaders/document_readers.py +233 -0
- ara_cli/file_loaders/file_loader.py +50 -0
- ara_cli/file_loaders/file_loaders.py +123 -0
- ara_cli/file_loaders/image_processor.py +89 -0
- ara_cli/file_loaders/markdown_reader.py +75 -0
- ara_cli/file_loaders/text_file_loader.py +187 -0
- ara_cli/global_file_lister.py +51 -0
- ara_cli/list_filter.py +1 -1
- ara_cli/output_suppressor.py +1 -1
- ara_cli/prompt_extractor.py +215 -88
- ara_cli/prompt_handler.py +521 -134
- ara_cli/prompt_rag.py +2 -2
- ara_cli/tag_extractor.py +83 -38
- ara_cli/template_loader.py +245 -0
- ara_cli/template_manager.py +18 -13
- ara_cli/templates/prompt-modules/commands/empty.commands.md +2 -12
- ara_cli/templates/prompt-modules/commands/extract_general.commands.md +12 -0
- ara_cli/templates/prompt-modules/commands/extract_markdown.commands.md +11 -0
- ara_cli/templates/prompt-modules/commands/extract_python.commands.md +13 -0
- ara_cli/templates/prompt-modules/commands/feature_add_or_modifiy_specified_behavior.commands.md +36 -0
- ara_cli/templates/prompt-modules/commands/feature_generate_initial_specified_bevahior.commands.md +53 -0
- ara_cli/templates/prompt-modules/commands/prompt_template_tech_stack_transformer.commands.md +95 -0
- ara_cli/templates/prompt-modules/commands/python_bug_fixing_code.commands.md +34 -0
- ara_cli/templates/prompt-modules/commands/python_generate_code.commands.md +27 -0
- ara_cli/templates/prompt-modules/commands/python_refactoring_code.commands.md +39 -0
- ara_cli/templates/prompt-modules/commands/python_step_definitions_generation_and_fixing.commands.md +40 -0
- ara_cli/templates/prompt-modules/commands/python_unittest_generation_and_fixing.commands.md +48 -0
- ara_cli/update_config_prompt.py +9 -3
- ara_cli/version.py +1 -1
- ara_cli-0.1.10.8.dist-info/METADATA +241 -0
- ara_cli-0.1.10.8.dist-info/RECORD +193 -0
- tests/test_ara_command_action.py +73 -59
- tests/test_ara_config.py +341 -36
- tests/test_artefact_autofix.py +1060 -0
- tests/test_artefact_link_updater.py +3 -3
- tests/test_artefact_lister.py +52 -132
- tests/test_artefact_renamer.py +2 -2
- tests/test_artefact_scan.py +327 -33
- tests/test_chat.py +2063 -498
- tests/test_file_classifier.py +24 -1
- tests/test_file_creator.py +3 -5
- tests/test_file_lister.py +1 -1
- tests/test_global_file_lister.py +131 -0
- tests/test_list_filter.py +2 -2
- tests/test_prompt_handler.py +746 -0
- tests/test_tag_extractor.py +19 -13
- tests/test_template_loader.py +192 -0
- tests/test_template_manager.py +5 -4
- tests/test_update_config_prompt.py +2 -2
- ara_cli/ara_command_parser.py +0 -327
- ara_cli/templates/prompt-modules/blueprints/complete_pytest_unittest.blueprint.md +0 -27
- ara_cli/templates/prompt-modules/blueprints/task_todo_list_implement_feature_BDD_way.blueprint.md +0 -30
- ara_cli/templates/prompt-modules/commands/artefact_classification.commands.md +0 -9
- ara_cli/templates/prompt-modules/commands/artefact_extension.commands.md +0 -17
- ara_cli/templates/prompt-modules/commands/artefact_formulation.commands.md +0 -14
- ara_cli/templates/prompt-modules/commands/behave_step_generation.commands.md +0 -102
- ara_cli/templates/prompt-modules/commands/code_generation_complex.commands.md +0 -20
- ara_cli/templates/prompt-modules/commands/code_generation_simple.commands.md +0 -13
- ara_cli/templates/prompt-modules/commands/error_fixing.commands.md +0 -20
- ara_cli/templates/prompt-modules/commands/feature_file_update.commands.md +0 -18
- ara_cli/templates/prompt-modules/commands/feature_formulation.commands.md +0 -43
- ara_cli/templates/prompt-modules/commands/js_code_generation_simple.commands.md +0 -13
- ara_cli/templates/prompt-modules/commands/refactoring.commands.md +0 -15
- ara_cli/templates/prompt-modules/commands/refactoring_analysis.commands.md +0 -9
- ara_cli/templates/prompt-modules/commands/reverse_engineer_feature_file.commands.md +0 -15
- ara_cli/templates/prompt-modules/commands/reverse_engineer_program_flow.commands.md +0 -19
- ara_cli/templates/template.businessgoal +0 -10
- ara_cli/templates/template.capability +0 -10
- ara_cli/templates/template.epic +0 -15
- ara_cli/templates/template.example +0 -6
- ara_cli/templates/template.feature +0 -26
- ara_cli/templates/template.issue +0 -14
- ara_cli/templates/template.keyfeature +0 -15
- ara_cli/templates/template.task +0 -6
- ara_cli/templates/template.userstory +0 -17
- ara_cli/templates/template.vision +0 -14
- ara_cli-0.1.9.69.dist-info/METADATA +0 -16
- ara_cli-0.1.9.69.dist-info/RECORD +0 -158
- tests/test_ara_autofix.py +0 -219
- {ara_cli-0.1.9.69.dist-info → ara_cli-0.1.10.8.dist-info}/WHEEL +0 -0
- {ara_cli-0.1.9.69.dist-info → ara_cli-0.1.10.8.dist-info}/entry_points.txt +0 -0
- {ara_cli-0.1.9.69.dist-info → ara_cli-0.1.10.8.dist-info}/top_level.txt +0 -0
ara_cli/artefact_autofix.py
CHANGED
|
@@ -1,5 +1,27 @@
|
|
|
1
|
+
from ara_cli.error_handler import AraError
|
|
2
|
+
from ara_cli.artefact_scan import check_file
|
|
3
|
+
from ara_cli.artefact_fuzzy_search import (
|
|
4
|
+
find_closest_name_matches,
|
|
5
|
+
extract_artefact_names_of_classifier,
|
|
6
|
+
)
|
|
7
|
+
from ara_cli.file_classifier import FileClassifier
|
|
8
|
+
from ara_cli.artefact_reader import ArtefactReader
|
|
9
|
+
from ara_cli.artefact_models.artefact_load import artefact_from_content
|
|
10
|
+
from ara_cli.artefact_models.artefact_model import Artefact
|
|
11
|
+
from typing import Optional, Dict, List, Tuple
|
|
12
|
+
import difflib
|
|
1
13
|
import os
|
|
2
|
-
|
|
14
|
+
import re
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def populate_classified_artefact_info(
|
|
18
|
+
classified_artefact_info: Optional[dict], force: bool = False
|
|
19
|
+
):
|
|
20
|
+
if not classified_artefact_info or force:
|
|
21
|
+
file_classifier = FileClassifier(os)
|
|
22
|
+
classified_artefact_info = file_classifier.classify_files()
|
|
23
|
+
return classified_artefact_info
|
|
24
|
+
|
|
3
25
|
|
|
4
26
|
def read_report_file():
|
|
5
27
|
file_path = "incompatible_artefacts_report.md"
|
|
@@ -7,7 +29,9 @@ def read_report_file():
|
|
|
7
29
|
with open(file_path, "r", encoding="utf-8") as f:
|
|
8
30
|
content = f.read()
|
|
9
31
|
except OSError:
|
|
10
|
-
print(
|
|
32
|
+
print(
|
|
33
|
+
'Artefact scan results file not found. Did you run the "ara scan" command?'
|
|
34
|
+
)
|
|
11
35
|
return None
|
|
12
36
|
return content
|
|
13
37
|
|
|
@@ -17,41 +41,53 @@ def parse_report(content: str) -> Dict[str, List[Tuple[str, str]]]:
|
|
|
17
41
|
Parses the incompatible artefacts report and returns structured data.
|
|
18
42
|
Returns a dictionary where keys are artefact classifiers, and values are lists of (file_path, reason) tuples.
|
|
19
43
|
"""
|
|
20
|
-
lines = content.splitlines()
|
|
21
|
-
issues = {}
|
|
22
|
-
current_classifier = None
|
|
23
|
-
|
|
24
|
-
if not lines or lines[0] != "# Artefact Check Report":
|
|
25
|
-
return issues
|
|
26
44
|
|
|
27
|
-
|
|
28
|
-
return
|
|
45
|
+
def is_valid_report(lines: List[str]) -> bool:
|
|
46
|
+
return bool(lines) and lines[0] == "# Artefact Check Report"
|
|
29
47
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
if not line:
|
|
33
|
-
continue
|
|
48
|
+
def has_no_problems(lines: List[str]) -> bool:
|
|
49
|
+
return len(lines) >= 3 and lines[2] == "No problems found."
|
|
34
50
|
|
|
51
|
+
def parse_classifier(line: str) -> Optional[str]:
|
|
35
52
|
if line.startswith("## "):
|
|
36
|
-
|
|
37
|
-
|
|
53
|
+
return line[3:].strip()
|
|
54
|
+
return None
|
|
38
55
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
56
|
+
def parse_issue(line: str) -> Optional[Tuple[str, str]]:
|
|
57
|
+
if not line.startswith("- "):
|
|
58
|
+
return None
|
|
59
|
+
parts = line.split("`", 2)
|
|
60
|
+
if len(parts) < 3:
|
|
61
|
+
return None
|
|
62
|
+
file_path = parts[1]
|
|
63
|
+
reason = parts[2].split(":", 1)[1].strip() if ":" in parts[2] else ""
|
|
64
|
+
return file_path, reason
|
|
43
65
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
66
|
+
lines = content.splitlines()
|
|
67
|
+
if not is_valid_report(lines) or has_no_problems(lines):
|
|
68
|
+
return {}
|
|
69
|
+
|
|
70
|
+
issues = {}
|
|
71
|
+
current_classifier = None
|
|
47
72
|
|
|
73
|
+
for line in map(str.strip, lines[1:]):
|
|
74
|
+
if not line:
|
|
75
|
+
continue
|
|
76
|
+
classifier = parse_classifier(line)
|
|
77
|
+
if classifier is not None:
|
|
78
|
+
current_classifier = classifier
|
|
79
|
+
issues[current_classifier] = []
|
|
80
|
+
continue
|
|
81
|
+
issue = parse_issue(line)
|
|
82
|
+
if issue and current_classifier is not None:
|
|
83
|
+
issues[current_classifier].append(issue)
|
|
48
84
|
return issues
|
|
49
85
|
|
|
50
86
|
|
|
51
87
|
def read_artefact(file_path):
|
|
52
88
|
"""Reads the artefact text from the given file path."""
|
|
53
89
|
try:
|
|
54
|
-
with open(file_path,
|
|
90
|
+
with open(file_path, "r", encoding="utf-8") as file:
|
|
55
91
|
return file.read()
|
|
56
92
|
except FileNotFoundError:
|
|
57
93
|
print(f"File not found: {file_path}")
|
|
@@ -70,8 +106,9 @@ def determine_artefact_type_and_class(classifier):
|
|
|
70
106
|
|
|
71
107
|
artefact_class = artefact_type_mapping.get(artefact_type)
|
|
72
108
|
if not artefact_class:
|
|
73
|
-
|
|
74
|
-
|
|
109
|
+
raise AraError(f"No artefact class found for {artefact_type}")
|
|
110
|
+
# print(f"No artefact class found for {artefact_type}")
|
|
111
|
+
# return None, None
|
|
75
112
|
|
|
76
113
|
return artefact_type, artefact_class
|
|
77
114
|
|
|
@@ -84,7 +121,7 @@ def construct_prompt(artefact_type, reason, file_path, artefact_text):
|
|
|
84
121
|
"Provide the corrected artefact. Do not reformulate the artefact, "
|
|
85
122
|
"just fix the pydantic model errors, use correct grammar. "
|
|
86
123
|
"You should follow the name of the file "
|
|
87
|
-
f"from its path {file_path} for naming the
|
|
124
|
+
f"from its path {file_path} for naming the artefact's title. "
|
|
88
125
|
"You are not allowed to use file extention in the artefact title. "
|
|
89
126
|
"You are not allowed to modify, delete or add tags. "
|
|
90
127
|
"User tag should be '@user_<username>'. The pydantic model already provides the '@user_' prefix. "
|
|
@@ -97,43 +134,384 @@ def construct_prompt(artefact_type, reason, file_path, artefact_text):
|
|
|
97
134
|
"then just delete those action items."
|
|
98
135
|
)
|
|
99
136
|
|
|
100
|
-
prompt +=
|
|
101
|
-
"\nThe current artefact is:\n"
|
|
102
|
-
"```\n"
|
|
103
|
-
f"{artefact_text}\n"
|
|
104
|
-
"```"
|
|
105
|
-
)
|
|
137
|
+
prompt += "\nThe current artefact is:\n" "```\n" f"{artefact_text}\n" "```"
|
|
106
138
|
|
|
107
139
|
return prompt
|
|
108
140
|
|
|
109
141
|
|
|
110
142
|
def run_agent(prompt, artefact_class):
|
|
111
143
|
from pydantic_ai import Agent
|
|
144
|
+
|
|
112
145
|
# gpt-4o
|
|
113
146
|
# anthropic:claude-3-7-sonnet-20250219
|
|
114
147
|
# anthropic:claude-4-sonnet-20250514
|
|
115
|
-
agent = Agent(
|
|
116
|
-
|
|
148
|
+
agent = Agent(
|
|
149
|
+
model="anthropic:claude-4-sonnet-20250514",
|
|
150
|
+
output_type=artefact_class,
|
|
151
|
+
instrument=True,
|
|
152
|
+
)
|
|
117
153
|
result = agent.run_sync(prompt)
|
|
118
|
-
return result.
|
|
154
|
+
return result.output
|
|
119
155
|
|
|
120
156
|
|
|
121
157
|
def write_corrected_artefact(file_path, corrected_text):
|
|
122
|
-
with open(file_path,
|
|
158
|
+
with open(file_path, "w", encoding="utf-8") as file:
|
|
123
159
|
file.write(corrected_text)
|
|
124
160
|
print(f"Fixed artefact at {file_path}")
|
|
125
161
|
|
|
126
162
|
|
|
127
|
-
def
|
|
163
|
+
def ask_for_correct_contribution(
|
|
164
|
+
artefact_info: Optional[tuple[str, str]] = None
|
|
165
|
+
) -> tuple[str, str]:
|
|
166
|
+
"""
|
|
167
|
+
Ask the user to provide a valid contribution when no match can be found.
|
|
168
|
+
|
|
169
|
+
Args:
|
|
170
|
+
artefact_info: Optional tuple containing (artefact_name, artefact_classifier)
|
|
171
|
+
|
|
172
|
+
Returns:
|
|
173
|
+
A tuple of (name, classifier) for the contribution
|
|
174
|
+
"""
|
|
175
|
+
|
|
176
|
+
artefact_name, artefact_classifier = (
|
|
177
|
+
artefact_info if artefact_info else (None, None)
|
|
178
|
+
)
|
|
179
|
+
contribution_message = (
|
|
180
|
+
f"of {artefact_classifier} artefact '{artefact_name}'" if artefact_name else ""
|
|
181
|
+
)
|
|
182
|
+
|
|
183
|
+
print(
|
|
184
|
+
f"Can not determine a match for contribution {contribution_message}. "
|
|
185
|
+
f"Please provide a valid contribution or contribution will be empty ([classifier] [file_name])."
|
|
186
|
+
)
|
|
187
|
+
|
|
188
|
+
user_input = input().strip()
|
|
189
|
+
|
|
190
|
+
if not user_input:
|
|
191
|
+
return None, None
|
|
192
|
+
|
|
193
|
+
parts = user_input.split(maxsplit=1)
|
|
194
|
+
if len(parts) != 2:
|
|
195
|
+
print("Invalid input format. Expected: <classifier> <file_name>")
|
|
196
|
+
return None, None
|
|
197
|
+
|
|
198
|
+
classifier, name = parts
|
|
199
|
+
return name, classifier
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def ask_for_contribution_choice(choices: List[str], artefact_info: Optional[tuple[str, str]] = None) -> Optional[str]:
|
|
203
|
+
artefact_name, artefact_classifier = artefact_info if artefact_info else (None, None)
|
|
204
|
+
message = "Found multiple close matches for the contribution"
|
|
205
|
+
if artefact_name and artefact_classifier:
|
|
206
|
+
message += f" of the {artefact_classifier} '{artefact_name}'"
|
|
207
|
+
message += "."
|
|
208
|
+
return get_user_choice(choices, message)
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
def _has_valid_contribution(artefact: Artefact) -> bool:
|
|
212
|
+
contribution = artefact.contribution
|
|
213
|
+
return contribution and contribution.artefact_name and contribution.classifier
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
def get_user_choice(choices: List[str], message: str) -> Optional[str]:
|
|
217
|
+
"""
|
|
218
|
+
Generic function to present user with a list of choices and return their selection.
|
|
219
|
+
|
|
220
|
+
Args:
|
|
221
|
+
choices: A list of strings representing the choices to display.
|
|
222
|
+
message: A message to display before listing the choices.
|
|
223
|
+
|
|
224
|
+
Returns:
|
|
225
|
+
The chosen item from the list or None if the input was invalid.
|
|
226
|
+
"""
|
|
227
|
+
print(message)
|
|
228
|
+
for i, choice in enumerate(choices):
|
|
229
|
+
print(f"{i + 1}: {choice}")
|
|
230
|
+
|
|
231
|
+
choice_number = input("Please enter your choice (number): ")
|
|
232
|
+
|
|
233
|
+
try:
|
|
234
|
+
choice_index = int(choice_number) - 1
|
|
235
|
+
if choice_index < 0 or choice_index >= len(choices):
|
|
236
|
+
print("Invalid choice. Aborting operation.")
|
|
237
|
+
return None
|
|
238
|
+
return choices[choice_index]
|
|
239
|
+
except ValueError:
|
|
240
|
+
print("Invalid input. Aborting operation.")
|
|
241
|
+
return None
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
def ask_for_rule_choice(matches: List[str]) -> Optional[str]:
|
|
245
|
+
"""Asks the user for a choice between multiple rule matches"""
|
|
246
|
+
message = "Multiple rule matches found:"
|
|
247
|
+
return get_user_choice(matches, message)
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
def _update_rule(
|
|
251
|
+
artefact: Artefact, name: str, classifier: str, classified_file_info: dict, delete_if_not_found: bool = False
|
|
252
|
+
) -> None:
|
|
253
|
+
"""Updates the rule in the contribution if a close match is found."""
|
|
254
|
+
rule = artefact.contribution.rule
|
|
255
|
+
|
|
256
|
+
content, artefact_data = ArtefactReader.read_artefact_data(
|
|
257
|
+
artefact_name=name,
|
|
258
|
+
classifier=classifier,
|
|
259
|
+
classified_file_info=classified_file_info,
|
|
260
|
+
)
|
|
261
|
+
|
|
262
|
+
parent = artefact_from_content(content=content)
|
|
263
|
+
rules = parent.rules
|
|
264
|
+
|
|
265
|
+
closest_rule_match = difflib.get_close_matches(rule, rules, cutoff=0.5)
|
|
266
|
+
if not closest_rule_match and delete_if_not_found:
|
|
267
|
+
artefact.contribution.rule = None
|
|
268
|
+
return
|
|
269
|
+
if not closest_rule_match:
|
|
270
|
+
return
|
|
271
|
+
if len(closest_rule_match) > 1:
|
|
272
|
+
artefact.contribution.rule = ask_for_rule_choice(closest_rule_match)
|
|
273
|
+
return
|
|
274
|
+
artefact.contribution.rule = closest_rule_match[0]
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
def _set_contribution_multiple_matches(
|
|
278
|
+
artefact: Artefact,
|
|
279
|
+
closest_matches: list,
|
|
280
|
+
artefact_tuple: tuple,
|
|
281
|
+
classified_file_info: dict,
|
|
282
|
+
) -> tuple[Artefact, bool]:
|
|
283
|
+
contribution = artefact.contribution
|
|
284
|
+
classifier = contribution.classifier
|
|
285
|
+
original_name = contribution.artefact_name
|
|
286
|
+
|
|
287
|
+
closest_match = closest_matches[0]
|
|
288
|
+
if len(closest_matches) > 1:
|
|
289
|
+
closest_match = ask_for_contribution_choice(closest_matches, artefact_tuple)
|
|
290
|
+
|
|
291
|
+
if not closest_match:
|
|
292
|
+
print(
|
|
293
|
+
f"Contribution of {artefact_tuple[1]} '{artefact_tuple[0]}' will be empty."
|
|
294
|
+
)
|
|
295
|
+
artefact.contribution = None
|
|
296
|
+
return artefact, True
|
|
297
|
+
|
|
298
|
+
print(
|
|
299
|
+
f"Updating contribution of {artefact_tuple[1]} '{artefact_tuple[0]}' to {classifier} '{closest_match}'"
|
|
300
|
+
)
|
|
301
|
+
contribution.artefact_name = closest_match
|
|
302
|
+
artefact.contribution = contribution
|
|
303
|
+
|
|
304
|
+
if contribution.rule:
|
|
305
|
+
_update_rule(artefact, original_name, classifier, classified_file_info)
|
|
306
|
+
|
|
307
|
+
return artefact, True
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
def set_closest_contribution(
|
|
311
|
+
artefact: Artefact, classified_file_info=None
|
|
312
|
+
) -> tuple[Artefact, bool]:
|
|
313
|
+
if not _has_valid_contribution(artefact):
|
|
314
|
+
return artefact, False
|
|
315
|
+
contribution = artefact.contribution
|
|
316
|
+
name = contribution.artefact_name
|
|
317
|
+
classifier = contribution.classifier
|
|
318
|
+
rule = contribution.rule
|
|
319
|
+
|
|
320
|
+
classified_file_info = populate_classified_artefact_info(classified_artefact_info=classified_file_info)
|
|
321
|
+
|
|
322
|
+
all_artefact_names = extract_artefact_names_of_classifier(
|
|
323
|
+
classified_files=classified_file_info, classifier=classifier
|
|
324
|
+
)
|
|
325
|
+
closest_matches = find_closest_name_matches(
|
|
326
|
+
artefact_name=name, all_artefact_names=all_artefact_names
|
|
327
|
+
)
|
|
328
|
+
|
|
329
|
+
artefact_tuple = (artefact.title, artefact._artefact_type().value)
|
|
330
|
+
|
|
331
|
+
if not closest_matches:
|
|
332
|
+
name, classifier = ask_for_correct_contribution(artefact_tuple)
|
|
333
|
+
if not name or not classifier:
|
|
334
|
+
artefact.contribution = None
|
|
335
|
+
return artefact, True
|
|
336
|
+
print(
|
|
337
|
+
f"Updating contribution of {artefact._artefact_type().value} '{artefact.title}' to {classifier} '{name}'"
|
|
338
|
+
)
|
|
339
|
+
contribution.artefact_name = name
|
|
340
|
+
contribution.classifier = classifier
|
|
341
|
+
artefact.contribution = contribution
|
|
342
|
+
return artefact, True
|
|
343
|
+
|
|
344
|
+
if closest_matches[0] == name:
|
|
345
|
+
return artefact, False
|
|
346
|
+
|
|
347
|
+
return _set_contribution_multiple_matches(
|
|
348
|
+
artefact=artefact,
|
|
349
|
+
closest_matches=closest_matches,
|
|
350
|
+
artefact_tuple=artefact_tuple,
|
|
351
|
+
classified_file_info=classified_file_info,
|
|
352
|
+
)
|
|
353
|
+
|
|
354
|
+
print(
|
|
355
|
+
f"Updating contribution of {artefact._artefact_type().value} '{artefact.title}' to {classifier} '{closest_match}'"
|
|
356
|
+
)
|
|
357
|
+
contribution.artefact_name = closest_match
|
|
358
|
+
artefact.contribution = contribution
|
|
359
|
+
|
|
360
|
+
if not rule:
|
|
361
|
+
return artefact, True
|
|
362
|
+
|
|
363
|
+
content, artefact = ArtefactReader.read_artefact_data(
|
|
364
|
+
artefact_name=name,
|
|
365
|
+
classifier=classifier,
|
|
366
|
+
classified_file_info=classified_file_info,
|
|
367
|
+
)
|
|
368
|
+
parent = artefact_from_content(content=content)
|
|
369
|
+
rules = parent.rules
|
|
370
|
+
|
|
371
|
+
closest_rule_match = difflib.get_close_matches(rule, rules, cutoff=0.5)
|
|
372
|
+
if closest_rule_match:
|
|
373
|
+
contribution.rule = closest_rule_match
|
|
374
|
+
artefact.contribution = contribution
|
|
375
|
+
return artefact, True
|
|
376
|
+
|
|
377
|
+
|
|
378
|
+
def fix_scenario_placeholder_mismatch(
|
|
379
|
+
file_path: str, artefact_text: str, artefact_class, **kwargs
|
|
380
|
+
) -> str:
|
|
381
|
+
"""
|
|
382
|
+
Converts a regular Scenario with placeholders to a Scenario Outline.
|
|
383
|
+
This is a deterministic fix that detects placeholders and converts the format.
|
|
384
|
+
"""
|
|
385
|
+
lines = artefact_text.splitlines()
|
|
386
|
+
new_lines = []
|
|
387
|
+
i = 0
|
|
388
|
+
|
|
389
|
+
while i < len(lines):
|
|
390
|
+
line = lines[i]
|
|
391
|
+
stripped_line = line.strip()
|
|
392
|
+
|
|
393
|
+
if stripped_line.startswith('Scenario:'):
|
|
394
|
+
scenario_lines, next_index = _extract_scenario_block(lines, i)
|
|
395
|
+
processed_lines = _process_scenario_block(scenario_lines)
|
|
396
|
+
new_lines.extend(processed_lines)
|
|
397
|
+
i = next_index
|
|
398
|
+
else:
|
|
399
|
+
new_lines.append(line)
|
|
400
|
+
i += 1
|
|
401
|
+
|
|
402
|
+
return "\n".join(new_lines)
|
|
403
|
+
|
|
404
|
+
|
|
405
|
+
def _extract_scenario_block(lines: list, start_index: int) -> tuple[list, int]:
|
|
406
|
+
"""Extract all lines belonging to a scenario block."""
|
|
407
|
+
scenario_lines = [lines[start_index]]
|
|
408
|
+
j = start_index + 1
|
|
409
|
+
|
|
410
|
+
while j < len(lines):
|
|
411
|
+
next_line = lines[j].strip()
|
|
412
|
+
if _is_scenario_boundary(next_line):
|
|
413
|
+
break
|
|
414
|
+
scenario_lines.append(lines[j])
|
|
415
|
+
j += 1
|
|
416
|
+
|
|
417
|
+
return scenario_lines, j
|
|
418
|
+
|
|
419
|
+
|
|
420
|
+
def _is_scenario_boundary(line: str) -> bool:
|
|
421
|
+
"""Check if a line marks the boundary of a scenario block."""
|
|
422
|
+
boundaries = ['Scenario:', 'Scenario Outline:', 'Background:', 'Feature:']
|
|
423
|
+
return any(line.startswith(boundary) for boundary in boundaries)
|
|
424
|
+
|
|
425
|
+
|
|
426
|
+
def _process_scenario_block(scenario_lines: list) -> list:
|
|
427
|
+
"""Process a scenario block and convert to outline if placeholders are found."""
|
|
428
|
+
if not scenario_lines:
|
|
429
|
+
return scenario_lines
|
|
430
|
+
|
|
431
|
+
first_line = scenario_lines[0]
|
|
432
|
+
indentation = _get_line_indentation(first_line)
|
|
433
|
+
placeholders = _extract_placeholders_from_scenario(scenario_lines[1:])
|
|
434
|
+
|
|
435
|
+
if not placeholders:
|
|
436
|
+
return scenario_lines
|
|
437
|
+
|
|
438
|
+
return _convert_to_scenario_outline(scenario_lines, placeholders, indentation)
|
|
439
|
+
|
|
440
|
+
|
|
441
|
+
def _get_line_indentation(line: str) -> str:
|
|
442
|
+
"""Get the indentation of a line."""
|
|
443
|
+
return line[:len(line) - len(line.lstrip())]
|
|
444
|
+
|
|
445
|
+
|
|
446
|
+
def _extract_placeholders_from_scenario(step_lines: list) -> set:
|
|
447
|
+
"""Extract placeholders from scenario step lines, ignoring docstrings."""
|
|
448
|
+
placeholders = set()
|
|
449
|
+
in_docstring = False
|
|
450
|
+
|
|
451
|
+
for line in step_lines:
|
|
452
|
+
step_line = line.strip()
|
|
453
|
+
if not step_line:
|
|
454
|
+
continue
|
|
455
|
+
|
|
456
|
+
in_docstring = _update_docstring_state(step_line, in_docstring)
|
|
457
|
+
|
|
458
|
+
if not in_docstring and '"""' not in step_line:
|
|
459
|
+
found = re.findall(r'<([^>]+)>', step_line)
|
|
460
|
+
placeholders.update(found)
|
|
461
|
+
|
|
462
|
+
return placeholders
|
|
463
|
+
|
|
464
|
+
|
|
465
|
+
def _update_docstring_state(line: str, current_state: bool) -> bool:
|
|
466
|
+
"""Update the docstring state based on the current line."""
|
|
467
|
+
if '"""' in line:
|
|
468
|
+
return not current_state
|
|
469
|
+
return current_state
|
|
470
|
+
|
|
471
|
+
|
|
472
|
+
def _convert_to_scenario_outline(scenario_lines: list, placeholders: set, indentation: str) -> list:
|
|
473
|
+
"""Convert scenario lines to scenario outline format with examples table."""
|
|
474
|
+
first_line = scenario_lines[0]
|
|
475
|
+
title = first_line.strip()[len('Scenario:'):].strip()
|
|
476
|
+
|
|
477
|
+
new_lines = [f"{indentation}Scenario Outline: {title}"]
|
|
478
|
+
new_lines.extend(scenario_lines[1:])
|
|
479
|
+
new_lines.append("")
|
|
480
|
+
|
|
481
|
+
examples_lines = _create_examples_table(placeholders, indentation)
|
|
482
|
+
new_lines.extend(examples_lines)
|
|
483
|
+
|
|
484
|
+
return new_lines
|
|
485
|
+
|
|
486
|
+
|
|
487
|
+
def _create_examples_table(placeholders: set, base_indentation: str) -> list:
|
|
488
|
+
"""Create the Examples table for the scenario outline."""
|
|
489
|
+
examples_indentation = base_indentation + " "
|
|
490
|
+
table_indentation = examples_indentation + " "
|
|
491
|
+
|
|
492
|
+
sorted_placeholders = sorted(placeholders)
|
|
493
|
+
header = "| " + " | ".join(sorted_placeholders) + " |"
|
|
494
|
+
sample_row = "| " + " | ".join(f"<{p}_value>" for p in sorted_placeholders) + " |"
|
|
495
|
+
|
|
496
|
+
return [
|
|
497
|
+
f"{examples_indentation}Examples:",
|
|
498
|
+
f"{table_indentation}{header}",
|
|
499
|
+
f"{table_indentation}{sample_row}"
|
|
500
|
+
]
|
|
501
|
+
|
|
502
|
+
|
|
503
|
+
def fix_title_mismatch(
|
|
504
|
+
file_path: str, artefact_text: str, artefact_class, **kwargs
|
|
505
|
+
) -> str:
|
|
128
506
|
"""
|
|
129
507
|
Deterministically fixes the title in the artefact text to match the filename.
|
|
130
508
|
"""
|
|
131
509
|
base_name = os.path.basename(file_path)
|
|
132
510
|
correct_title_underscores, _ = os.path.splitext(base_name)
|
|
133
|
-
correct_title_spaces = correct_title_underscores.replace(
|
|
511
|
+
correct_title_spaces = correct_title_underscores.replace("_", " ")
|
|
134
512
|
|
|
135
513
|
title_prefix = artefact_class._title_prefix()
|
|
136
|
-
|
|
514
|
+
|
|
137
515
|
lines = artefact_text.splitlines()
|
|
138
516
|
new_lines = []
|
|
139
517
|
title_found_and_replaced = False
|
|
@@ -144,48 +522,251 @@ def fix_title_mismatch(file_path: str, artefact_text: str, artefact_class) -> st
|
|
|
144
522
|
title_found_and_replaced = True
|
|
145
523
|
else:
|
|
146
524
|
new_lines.append(line)
|
|
147
|
-
|
|
525
|
+
|
|
148
526
|
if not title_found_and_replaced:
|
|
149
|
-
print(
|
|
527
|
+
print(
|
|
528
|
+
f"Warning: Title prefix '{title_prefix}' not found in {file_path}. Title could not be fixed."
|
|
529
|
+
)
|
|
150
530
|
return artefact_text
|
|
151
531
|
|
|
152
532
|
return "\n".join(new_lines)
|
|
153
533
|
|
|
154
534
|
|
|
155
|
-
def
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
535
|
+
def fix_contribution(
|
|
536
|
+
file_path: str,
|
|
537
|
+
artefact_text: str,
|
|
538
|
+
artefact_class: str,
|
|
539
|
+
classified_artefact_info: dict,
|
|
540
|
+
**kwargs,
|
|
541
|
+
):
|
|
542
|
+
classified_artefact_info = populate_classified_artefact_info(classified_artefact_info=classified_artefact_info)
|
|
543
|
+
artefact = artefact_class.deserialize(artefact_text)
|
|
544
|
+
artefact, _ = set_closest_contribution(artefact)
|
|
545
|
+
artefact_text = artefact.serialize()
|
|
546
|
+
return artefact_text
|
|
547
|
+
|
|
548
|
+
|
|
549
|
+
def fix_rule(
|
|
550
|
+
file_path: str,
|
|
551
|
+
artefact_text: str,
|
|
552
|
+
artefact_class: str,
|
|
553
|
+
classified_artefact_info: dict,
|
|
554
|
+
**kwargs,
|
|
555
|
+
):
|
|
556
|
+
classified_artefact_info = populate_classified_artefact_info(classified_artefact_info=classified_artefact_info)
|
|
557
|
+
artefact = artefact_class.deserialize(artefact_text)
|
|
558
|
+
contribution = artefact.contribution
|
|
559
|
+
assert contribution is not None
|
|
560
|
+
_update_rule(
|
|
561
|
+
artefact=artefact,
|
|
562
|
+
name=contribution.artefact_name,
|
|
563
|
+
classifier=contribution.classifier,
|
|
564
|
+
classified_file_info=classified_artefact_info,
|
|
565
|
+
delete_if_not_found=True
|
|
566
|
+
)
|
|
567
|
+
feedback_message = (f"Updating contribution of {artefact._artefact_type().value} "
|
|
568
|
+
f"'{artefact.title}' to {contribution.classifier} "
|
|
569
|
+
f"'{contribution.artefact_name}' ")
|
|
570
|
+
rule = contribution.rule
|
|
571
|
+
if rule:
|
|
572
|
+
feedback_message += f"with rule '{rule}'"
|
|
573
|
+
else:
|
|
574
|
+
feedback_message += "without a rule"
|
|
575
|
+
print(feedback_message)
|
|
576
|
+
return artefact.serialize()
|
|
577
|
+
|
|
578
|
+
|
|
579
|
+
def fix_misplaced_content(file_path: str, artefact_text: str, **kwargs) -> str:
|
|
580
|
+
"""
|
|
581
|
+
Deterministically fixes content like 'Rule:' or 'Estimate:' misplaced in the description.
|
|
582
|
+
"""
|
|
583
|
+
lines = artefact_text.splitlines()
|
|
159
584
|
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
585
|
+
desc_start_idx = -1
|
|
586
|
+
for i, line in enumerate(lines):
|
|
587
|
+
if line.strip().startswith("Description:"):
|
|
588
|
+
desc_start_idx = i
|
|
589
|
+
break
|
|
163
590
|
|
|
164
|
-
|
|
591
|
+
if desc_start_idx == -1:
|
|
592
|
+
return artefact_text # No description, nothing to fix.
|
|
165
593
|
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
594
|
+
pre_desc_lines = lines[:desc_start_idx]
|
|
595
|
+
desc_line = lines[desc_start_idx]
|
|
596
|
+
post_desc_lines = lines[desc_start_idx+1:]
|
|
597
|
+
|
|
598
|
+
misplaced_content = []
|
|
599
|
+
new_post_desc_lines = []
|
|
600
|
+
|
|
601
|
+
for line in post_desc_lines:
|
|
602
|
+
if line.strip().startswith("Rule:") or line.strip().startswith("Estimate:"):
|
|
603
|
+
misplaced_content.append(line)
|
|
604
|
+
else:
|
|
605
|
+
new_post_desc_lines.append(line)
|
|
606
|
+
|
|
607
|
+
if not misplaced_content:
|
|
608
|
+
return artefact_text
|
|
609
|
+
|
|
610
|
+
# Rebuild the file content
|
|
611
|
+
final_lines = pre_desc_lines + misplaced_content + [""] + [desc_line] + new_post_desc_lines
|
|
612
|
+
return "\n".join(final_lines)
|
|
613
|
+
|
|
614
|
+
|
|
615
|
+
def should_skip_issue(deterministic_issue, deterministic, non_deterministic, file_path) -> bool:
|
|
616
|
+
if not non_deterministic and not deterministic_issue:
|
|
617
|
+
print(f"Skipping non-deterministic fix for {file_path} as per request.")
|
|
618
|
+
return True
|
|
619
|
+
if not deterministic and deterministic_issue:
|
|
620
|
+
print(f"Skipping fix for {file_path} as per request flags.")
|
|
170
621
|
return True
|
|
622
|
+
return False
|
|
623
|
+
|
|
624
|
+
def determine_attempt_count(single_pass, file_path) -> int:
|
|
625
|
+
if single_pass:
|
|
626
|
+
print(f"Single-pass mode enabled for {file_path}. Running for 1 attempt.")
|
|
627
|
+
return 1
|
|
628
|
+
return 3
|
|
629
|
+
|
|
630
|
+
def apply_deterministic_fix(
|
|
631
|
+
deterministic, deterministic_issue, file_path, artefact_text, artefact_class, classified_artefact_info,
|
|
632
|
+
deterministic_markers_to_functions, corrected_text
|
|
633
|
+
) -> str:
|
|
634
|
+
if deterministic and deterministic_issue:
|
|
635
|
+
print(f"Applying deterministic fix for '{deterministic_issue}'...")
|
|
636
|
+
fix_function = deterministic_markers_to_functions[deterministic_issue]
|
|
637
|
+
return fix_function(
|
|
638
|
+
file_path=file_path,
|
|
639
|
+
artefact_text=artefact_text,
|
|
640
|
+
artefact_class=artefact_class,
|
|
641
|
+
classified_artefact_info=classified_artefact_info,
|
|
642
|
+
)
|
|
643
|
+
return corrected_text
|
|
171
644
|
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
645
|
+
def apply_non_deterministic_fix(
|
|
646
|
+
non_deterministic, deterministic_issue, corrected_text,
|
|
647
|
+
artefact_type, current_reason, file_path, artefact_text, artefact_class
|
|
648
|
+
) -> Optional[str]:
|
|
649
|
+
"""
|
|
650
|
+
Applies LLM fix. Return None in case of an exception
|
|
651
|
+
"""
|
|
652
|
+
if non_deterministic and not deterministic_issue:
|
|
653
|
+
print("Applying non-deterministic (LLM) fix...")
|
|
654
|
+
prompt = construct_prompt(
|
|
655
|
+
artefact_type, current_reason, file_path, artefact_text
|
|
656
|
+
)
|
|
176
657
|
try:
|
|
177
658
|
corrected_artefact = run_agent(prompt, artefact_class)
|
|
178
659
|
corrected_text = corrected_artefact.serialize()
|
|
179
|
-
write_corrected_artefact(file_path, corrected_text)
|
|
180
|
-
return True
|
|
181
660
|
except Exception as e:
|
|
182
|
-
print(f"LLM agent failed to fix artefact at {file_path}: {e}")
|
|
661
|
+
print(f" ❌ LLM agent failed to fix artefact at {file_path}: {e}")
|
|
662
|
+
return None
|
|
663
|
+
return corrected_text
|
|
664
|
+
|
|
665
|
+
def attempt_autofix_loop(
|
|
666
|
+
file_path: str,
|
|
667
|
+
artefact_type,
|
|
668
|
+
artefact_class,
|
|
669
|
+
deterministic_markers_to_functions,
|
|
670
|
+
max_attempts,
|
|
671
|
+
deterministic: bool,
|
|
672
|
+
non_deterministic: bool,
|
|
673
|
+
classified_artefact_info: Optional[Dict[str, List[Dict[str, str]]]],
|
|
674
|
+
) -> bool:
|
|
675
|
+
"""
|
|
676
|
+
Attempts to fix the artefact in a loop, up to max_attempts.
|
|
677
|
+
"""
|
|
678
|
+
for attempt in range(max_attempts):
|
|
679
|
+
is_valid, current_reason = check_file(
|
|
680
|
+
file_path, artefact_class, classified_artefact_info
|
|
681
|
+
)
|
|
682
|
+
|
|
683
|
+
if is_valid:
|
|
684
|
+
print(f"✅ Artefact at {file_path} is now valid.")
|
|
685
|
+
return True
|
|
686
|
+
|
|
687
|
+
print(
|
|
688
|
+
f"Attempting to fix {file_path} (Attempt {attempt + 1}/{max_attempts})..."
|
|
689
|
+
)
|
|
690
|
+
print(f" Reason: {current_reason}")
|
|
691
|
+
|
|
692
|
+
artefact_text = read_artefact(file_path)
|
|
693
|
+
if artefact_text is None:
|
|
183
694
|
return False
|
|
184
|
-
|
|
185
|
-
# Log if a fix was skipped due to flags
|
|
186
|
-
if is_deterministic_issue and not deterministic:
|
|
187
|
-
print(f"Skipping deterministic fix for {file_path} as per request.")
|
|
188
|
-
elif not is_deterministic_issue and not non_deterministic:
|
|
189
|
-
print(f"Skipping non-deterministic fix for {file_path} as per request.")
|
|
190
695
|
|
|
191
|
-
|
|
696
|
+
deterministic_issue = next(
|
|
697
|
+
(
|
|
698
|
+
marker
|
|
699
|
+
for marker in deterministic_markers_to_functions
|
|
700
|
+
if marker in current_reason
|
|
701
|
+
),
|
|
702
|
+
None,
|
|
703
|
+
)
|
|
704
|
+
|
|
705
|
+
if should_skip_issue(deterministic_issue, deterministic, non_deterministic, file_path):
|
|
706
|
+
return False
|
|
707
|
+
|
|
708
|
+
corrected_text = None
|
|
709
|
+
|
|
710
|
+
corrected_text = apply_deterministic_fix(
|
|
711
|
+
deterministic, deterministic_issue, file_path, artefact_text,
|
|
712
|
+
artefact_class, classified_artefact_info,
|
|
713
|
+
deterministic_markers_to_functions, corrected_text
|
|
714
|
+
)
|
|
715
|
+
corrected_text = apply_non_deterministic_fix(
|
|
716
|
+
non_deterministic, deterministic_issue, corrected_text,
|
|
717
|
+
artefact_type, current_reason, file_path, artefact_text, artefact_class
|
|
718
|
+
)
|
|
719
|
+
|
|
720
|
+
if corrected_text is None or corrected_text.strip() == artefact_text.strip():
|
|
721
|
+
print(
|
|
722
|
+
" Fixing attempt did not alter the file. Stopping to prevent infinite loop."
|
|
723
|
+
)
|
|
724
|
+
return False
|
|
725
|
+
|
|
726
|
+
write_corrected_artefact(file_path, corrected_text)
|
|
727
|
+
|
|
728
|
+
print(" File modified. Re-classifying artefact information for next check...")
|
|
729
|
+
classified_artefact_info = populate_classified_artefact_info(classified_artefact_info, force=True)
|
|
730
|
+
|
|
731
|
+
print(f"❌ Failed to fix {file_path} after {max_attempts} attempts.")
|
|
732
|
+
return False
|
|
733
|
+
|
|
734
|
+
def apply_autofix(
|
|
735
|
+
file_path: str,
|
|
736
|
+
classifier: str,
|
|
737
|
+
reason: str,
|
|
738
|
+
single_pass: bool = False,
|
|
739
|
+
deterministic: bool = True,
|
|
740
|
+
non_deterministic: bool = True,
|
|
741
|
+
classified_artefact_info: Optional[Dict[str, List[Dict[str, str]]]] = None,
|
|
742
|
+
) -> bool:
|
|
743
|
+
"""
|
|
744
|
+
Applies fixes to a single artefact file iteratively until it is valid
|
|
745
|
+
or a fix cannot be applied. If single_pass is True, it runs for only one attempt.
|
|
746
|
+
"""
|
|
747
|
+
deterministic_markers_to_functions = {
|
|
748
|
+
"Filename-Title Mismatch": fix_title_mismatch,
|
|
749
|
+
"Invalid Contribution Reference": fix_contribution,
|
|
750
|
+
"Rule Mismatch": fix_rule,
|
|
751
|
+
"Scenario Contains Placeholders": fix_scenario_placeholder_mismatch,
|
|
752
|
+
"Found 'Rule:' inside description": fix_misplaced_content,
|
|
753
|
+
"Found 'Estimate:' inside description": fix_misplaced_content,
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
artefact_type, artefact_class = determine_artefact_type_and_class(classifier)
|
|
757
|
+
if artefact_type is None or artefact_class is None:
|
|
758
|
+
return False
|
|
759
|
+
|
|
760
|
+
classified_artefact_info = populate_classified_artefact_info(classified_artefact_info)
|
|
761
|
+
max_attempts = determine_attempt_count(single_pass, file_path)
|
|
762
|
+
|
|
763
|
+
return attempt_autofix_loop(
|
|
764
|
+
file_path=file_path,
|
|
765
|
+
artefact_type=artefact_type,
|
|
766
|
+
artefact_class=artefact_class,
|
|
767
|
+
deterministic_markers_to_functions=deterministic_markers_to_functions,
|
|
768
|
+
max_attempts=max_attempts,
|
|
769
|
+
deterministic=deterministic,
|
|
770
|
+
non_deterministic=non_deterministic,
|
|
771
|
+
classified_artefact_info=classified_artefact_info,
|
|
772
|
+
)
|