ara-cli 0.1.9.69__py3-none-any.whl → 0.1.9.71__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. ara_cli/ara_command_action.py +16 -12
  2. ara_cli/ara_config.py +24 -10
  3. ara_cli/artefact_autofix.py +278 -23
  4. ara_cli/artefact_creator.py +3 -3
  5. ara_cli/artefact_fuzzy_search.py +9 -4
  6. ara_cli/artefact_link_updater.py +4 -4
  7. ara_cli/artefact_models/artefact_model.py +14 -7
  8. ara_cli/artefact_models/artefact_templates.py +1 -1
  9. ara_cli/artefact_models/feature_artefact_model.py +72 -18
  10. ara_cli/artefact_models/serialize_helper.py +1 -1
  11. ara_cli/artefact_reader.py +16 -38
  12. ara_cli/artefact_renamer.py +2 -2
  13. ara_cli/artefact_scan.py +28 -3
  14. ara_cli/chat.py +1 -1
  15. ara_cli/file_classifier.py +3 -3
  16. ara_cli/file_lister.py +1 -1
  17. ara_cli/list_filter.py +1 -1
  18. ara_cli/output_suppressor.py +1 -1
  19. ara_cli/prompt_extractor.py +3 -3
  20. ara_cli/prompt_handler.py +9 -10
  21. ara_cli/prompt_rag.py +2 -2
  22. ara_cli/template_manager.py +2 -2
  23. ara_cli/templates/prompt-modules/blueprints/complete_pytest_unittest.blueprint.md +1 -1
  24. ara_cli/update_config_prompt.py +2 -2
  25. ara_cli/version.py +1 -1
  26. {ara_cli-0.1.9.69.dist-info → ara_cli-0.1.9.71.dist-info}/METADATA +1 -1
  27. {ara_cli-0.1.9.69.dist-info → ara_cli-0.1.9.71.dist-info}/RECORD +39 -39
  28. tests/test_ara_command_action.py +7 -7
  29. tests/{test_ara_autofix.py → test_artefact_autofix.py} +163 -29
  30. tests/test_artefact_link_updater.py +3 -3
  31. tests/test_artefact_renamer.py +2 -2
  32. tests/test_artefact_scan.py +52 -19
  33. tests/test_file_classifier.py +1 -1
  34. tests/test_file_lister.py +1 -1
  35. tests/test_list_filter.py +2 -2
  36. tests/test_update_config_prompt.py +2 -2
  37. {ara_cli-0.1.9.69.dist-info → ara_cli-0.1.9.71.dist-info}/WHEEL +0 -0
  38. {ara_cli-0.1.9.69.dist-info → ara_cli-0.1.9.71.dist-info}/entry_points.txt +0 -0
  39. {ara_cli-0.1.9.69.dist-info → ara_cli-0.1.9.71.dist-info}/top_level.txt +0 -0
@@ -33,7 +33,7 @@ def create_action(args):
33
33
  if parent_classifier and parent_name and rule:
34
34
  check_validity(Classifier.is_valid_classifier(parent_classifier), invalid_classifier_message)
35
35
  check_validity(is_valid_filename(parent_name), invalid_name_message)
36
- parent_artefact = ArtefactReader.read_single_artefact(artefact_name=parent_name, classifier=parent_classifier)
36
+ parent_artefact = ArtefactReader.read_artefact(artefact_name=parent_name, classifier=parent_classifier)
37
37
  rule = find_closest_rule(parent_artefact, rule)
38
38
  return parent_classifier, parent_name, rule
39
39
  if parent_classifier and parent_name:
@@ -359,7 +359,7 @@ def reconnect_action(args):
359
359
 
360
360
  feedback_message = f"Updated contribution of {classifier} '{artefact_name}' to {parent_classifier} '{parent_name}'"
361
361
 
362
- content, artefact_info = ArtefactReader.read_artefact(
362
+ content, artefact_info = ArtefactReader.read_artefact_data(
363
363
  artefact_name=artefact_name,
364
364
  classifier=classifier
365
365
  )
@@ -367,7 +367,7 @@ def reconnect_action(args):
367
367
  print(read_error_message)
368
368
  return
369
369
 
370
- parent_content, parent_info = ArtefactReader.read_artefact(
370
+ parent_content, parent_info = ArtefactReader.read_artefact_data(
371
371
  artefact_name=parent_name,
372
372
  classifier=parent_classifier
373
373
  )
@@ -399,7 +399,7 @@ def reconnect_action(args):
399
399
  exit(1)
400
400
 
401
401
  artefact.contribution = contribution
402
- with open(artefact.file_path, 'w') as file:
402
+ with open(artefact.file_path, 'w', encoding='utf-8') as file:
403
403
  artefact_content = artefact.serialize()
404
404
  file.write(artefact_content)
405
405
 
@@ -426,7 +426,7 @@ def read_status_action(args):
426
426
  lambda x: x["title"] == artefact_name, artefact_info_dicts
427
427
  ))
428
428
 
429
- with open(artefact_info["file_path"], 'r') as file:
429
+ with open(artefact_info["file_path"], 'r', encoding='utf-8') as file:
430
430
  content = file.read()
431
431
  artefact = artefact_from_content(content)
432
432
 
@@ -458,7 +458,7 @@ def read_user_action(args):
458
458
  lambda x: x["title"] == artefact_name, artefact_info_dicts
459
459
  ))
460
460
 
461
- with open(artefact_info["file_path"], 'r') as file:
461
+ with open(artefact_info["file_path"], 'r', encoding='utf-8') as file:
462
462
  content = file.read()
463
463
  artefact = artefact_from_content(content)
464
464
 
@@ -500,14 +500,14 @@ def set_status_action(args):
500
500
  lambda x: x["title"] == artefact_name, classified_artefact_dict
501
501
  ))
502
502
 
503
- with open(artefact_info["file_path"], 'r') as file:
503
+ with open(artefact_info["file_path"], 'r', encoding='utf-8') as file:
504
504
  content = file.read()
505
505
  artefact = artefact_from_content(content)
506
506
 
507
507
  artefact.status = new_status
508
508
 
509
509
  serialized_content = artefact.serialize()
510
- with open(f"{artefact_info['file_path']}", 'w') as file:
510
+ with open(f"{artefact_info['file_path']}", 'w', encoding='utf-8') as file:
511
511
  file.write(serialized_content)
512
512
 
513
513
  print(f"Status of task '{artefact_name}' has been updated to '{new_status}'.")
@@ -537,7 +537,7 @@ def set_user_action(args):
537
537
  lambda x: x["title"] == artefact_name, classified_artefact_dict
538
538
  ))
539
539
 
540
- with open(artefact_info["file_path"], 'r') as file:
540
+ with open(artefact_info["file_path"], 'r', encoding='utf-8') as file:
541
541
  content = file.read()
542
542
  artefact = artefact_from_content(content)
543
543
 
@@ -545,7 +545,7 @@ def set_user_action(args):
545
545
 
546
546
  serialized_content = artefact.serialize()
547
547
 
548
- with open(artefact_info["file_path"], 'w') as file:
548
+ with open(artefact_info["file_path"], 'w', encoding='utf-8') as file:
549
549
  file.write(serialized_content)
550
550
 
551
551
  print(f"User of task '{artefact_name}' has been updated to '{new_user}'.")
@@ -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, Union, Optional
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, Dict[str, Union[str, float]]] = {
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
 
@@ -86,7 +100,7 @@ def ensure_directory_exists(directory: str):
86
100
 
87
101
 
88
102
  def validate_config_data(filepath: str):
89
- with open(filepath, "r") as file:
103
+ with open(filepath, "r", encoding="utf-8") as file:
90
104
  data = json.load(file)
91
105
  return data
92
106
 
@@ -98,8 +112,8 @@ def read_data(filepath: str) -> ARAconfig:
98
112
  # If file does not exist, create it with default values
99
113
  default_config = ARAconfig()
100
114
 
101
- with open(filepath, "w") as file:
102
- json.dump(default_config.model_dump(), file, indent=4)
115
+ with open(filepath, "w", encoding="utf-8") as file:
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"
@@ -112,8 +126,8 @@ def read_data(filepath: str) -> ARAconfig:
112
126
 
113
127
  # Function to save the modified configuration back to the JSON file
114
128
  def save_data(filepath: str, config: ARAconfig):
115
- with open(filepath, "w") as file:
116
- json.dump(config.dict(), file, indent=4)
129
+ with open(filepath, "w", encoding="utf-8") as file:
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
@@ -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
- from typing import Dict, List, Tuple
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('Artefact scan results file not found. Did you run the "ara scan" command?')
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, 'r', encoding="utf-8") as file:
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 arteafact's title. "
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(model="anthropic:claude-4-sonnet-20250514",
116
- result_type=artefact_class, instrument=True)
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, 'w', encoding="utf-8") as file:
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 fix_title_mismatch(file_path: str, artefact_text: str, artefact_class) -> str:
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_data(
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_data(
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(f"Warning: Title prefix '{title_prefix}' not found in {file_path}. Title could not be fixed.")
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 apply_autofix(file_path: str, classifier: str, reason: str, deterministic: bool, non_deterministic: bool) -> bool:
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
- is_deterministic_issue = "Filename-Title Mismatch" in reason
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 = fix_title_mismatch(file_path, artefact_text, artefact_class)
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
@@ -16,7 +16,7 @@ class ArtefactCreator:
16
16
 
17
17
  @lru_cache(maxsize=None)
18
18
  def read_template_content(self, template_file_path):
19
- with open(template_file_path, "r") as template_file:
19
+ with open(template_file_path, "r", encoding="utf-8") as template_file:
20
20
  return template_file.read()
21
21
 
22
22
  def create_artefact_prompt_files(self, dir_path, template_path, classifier):
@@ -118,7 +118,7 @@ class ArtefactCreator:
118
118
  artefact_content = artefact.serialize()
119
119
  rmtree(dir_path, ignore_errors=True)
120
120
  os.makedirs(dir_path, exist_ok=True)
121
- with open(file_path, 'w') as artefact_file:
121
+ with open(file_path, 'w', encoding='utf-8') as artefact_file:
122
122
  artefact_file.write(artefact_content)
123
123
 
124
124
  relative_file_path = os.path.relpath(file_path, original_directory)
@@ -138,7 +138,7 @@ class ArtefactCreator:
138
138
 
139
139
  title = Classifier.get_artefact_title(classifier)
140
140
 
141
- with open(file_path, "r") as file:
141
+ with open(file_path, "r", encoding="utf-8") as file:
142
142
  for line in file:
143
143
  if line.strip().startswith(title):
144
144
  return line.split(':')[1].strip()
@@ -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 find_closest_name_match(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, n=1)
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
- closest_match = closest_matches[0]
41
- return closest_match
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):