ara-cli 0.1.9.73__py3-none-any.whl → 0.1.9.75__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.

Files changed (39) hide show
  1. ara_cli/ara_command_action.py +15 -15
  2. ara_cli/ara_command_parser.py +2 -1
  3. ara_cli/ara_config.py +181 -73
  4. ara_cli/artefact_autofix.py +130 -68
  5. ara_cli/artefact_creator.py +1 -1
  6. ara_cli/artefact_models/artefact_model.py +26 -7
  7. ara_cli/artefact_models/artefact_templates.py +47 -31
  8. ara_cli/artefact_models/businessgoal_artefact_model.py +23 -25
  9. ara_cli/artefact_models/epic_artefact_model.py +23 -24
  10. ara_cli/artefact_models/feature_artefact_model.py +76 -46
  11. ara_cli/artefact_models/keyfeature_artefact_model.py +21 -24
  12. ara_cli/artefact_models/task_artefact_model.py +73 -13
  13. ara_cli/artefact_models/userstory_artefact_model.py +22 -24
  14. ara_cli/artefact_models/vision_artefact_model.py +23 -42
  15. ara_cli/artefact_scan.py +55 -17
  16. ara_cli/chat.py +23 -5
  17. ara_cli/prompt_handler.py +4 -4
  18. ara_cli/tag_extractor.py +43 -28
  19. ara_cli/template_manager.py +3 -8
  20. ara_cli/version.py +1 -1
  21. {ara_cli-0.1.9.73.dist-info → ara_cli-0.1.9.75.dist-info}/METADATA +1 -1
  22. {ara_cli-0.1.9.73.dist-info → ara_cli-0.1.9.75.dist-info}/RECORD +29 -39
  23. tests/test_ara_config.py +420 -36
  24. tests/test_artefact_autofix.py +289 -25
  25. tests/test_artefact_scan.py +296 -35
  26. tests/test_chat.py +35 -15
  27. ara_cli/templates/template.businessgoal +0 -10
  28. ara_cli/templates/template.capability +0 -10
  29. ara_cli/templates/template.epic +0 -15
  30. ara_cli/templates/template.example +0 -6
  31. ara_cli/templates/template.feature +0 -26
  32. ara_cli/templates/template.issue +0 -14
  33. ara_cli/templates/template.keyfeature +0 -15
  34. ara_cli/templates/template.task +0 -6
  35. ara_cli/templates/template.userstory +0 -17
  36. ara_cli/templates/template.vision +0 -14
  37. {ara_cli-0.1.9.73.dist-info → ara_cli-0.1.9.75.dist-info}/WHEEL +0 -0
  38. {ara_cli-0.1.9.73.dist-info → ara_cli-0.1.9.75.dist-info}/entry_points.txt +0 -0
  39. {ara_cli-0.1.9.73.dist-info → ara_cli-0.1.9.75.dist-info}/top_level.txt +0 -0
@@ -347,6 +347,7 @@ def reconnect_action(args):
347
347
  from ara_cli.artefact_models.artefact_load import artefact_from_content
348
348
  from ara_cli.artefact_models.artefact_model import Contribution
349
349
  from ara_cli.artefact_reader import ArtefactReader
350
+ from ara_cli.file_classifier import FileClassifier
350
351
  from ara_cli.artefact_fuzzy_search import find_closest_rule
351
352
 
352
353
  classifier = args.classifier
@@ -359,31 +360,29 @@ def reconnect_action(args):
359
360
 
360
361
  feedback_message = f"Updated contribution of {classifier} '{artefact_name}' to {parent_classifier} '{parent_name}'"
361
362
 
362
- content, artefact_info = ArtefactReader.read_artefact_data(
363
+ file_classifier = FileClassifier(os)
364
+ classified_file_info = file_classifier.classify_files()
365
+
366
+ artefact = ArtefactReader.read_artefact(
363
367
  artefact_name=artefact_name,
364
- classifier=classifier
368
+ classifier=classifier,
369
+ classified_file_info=classified_file_info
365
370
  )
366
- if not content:
371
+
372
+ if not artefact:
367
373
  print(read_error_message)
368
374
  return
369
375
 
370
- parent_content, parent_info = ArtefactReader.read_artefact_data(
376
+ parent = ArtefactReader.read_artefact(
371
377
  artefact_name=parent_name,
372
- classifier=parent_classifier
378
+ classifier=parent_classifier,
379
+ classified_file_info=classified_file_info
373
380
  )
374
- if not parent_content:
381
+
382
+ if not parent:
375
383
  print(read_error_message)
376
384
  return
377
385
 
378
- artefact = artefact_from_content(
379
- content=content,
380
- )
381
- artefact._file_path = artefact_info["file_path"]
382
-
383
- parent = artefact_from_content(
384
- content=parent_content
385
- )
386
-
387
386
  contribution = Contribution(
388
387
  artefact_name=parent.title,
389
388
  classifier=parent.artefact_type
@@ -603,6 +602,7 @@ def autofix_action(args):
603
602
  file_path,
604
603
  classifier,
605
604
  reason,
605
+ single_pass=args.single_pass,
606
606
  deterministic=run_deterministic,
607
607
  non_deterministic=run_non_deterministic,
608
608
  classified_artefact_info=classified_artefact_info
@@ -229,7 +229,8 @@ def scan_parser(subparsers):
229
229
 
230
230
 
231
231
  def autofix_parser(subparsers):
232
- autofix_parser = subparsers.add_parser("autofix", help="Fix ARA tree with llm models for scanned artefacts with ara scan command.")
232
+ autofix_parser = subparsers.add_parser("autofix", help="Fix ARA tree with llm models for scanned artefacts with ara scan command. By default three attemps for every file.")
233
+ autofix_parser.add_argument("--single-pass", action="store_true", help="Run the autofix once for every scaned file.")
233
234
  determinism_group = autofix_parser.add_mutually_exclusive_group()
234
235
  determinism_group.add_argument("--deterministic", "-d", action="store_true", help="Run only deterministic fixes e.g Title-FileName Mismatch fix")
235
236
  determinism_group.add_argument("--non-deterministic", "-nd", action="store_true", help="Run only non-deterministic fixes")
ara_cli/ara_config.py CHANGED
@@ -1,33 +1,43 @@
1
- from typing import List, Dict, Optional
2
- from pydantic import BaseModel
1
+ from typing import List, Dict, Optional, Any
2
+ from pydantic import BaseModel, ValidationError, Field, field_validator, model_validator
3
3
  import json
4
4
  import os
5
5
  from os.path import exists, dirname
6
6
  from os import makedirs
7
7
  from functools import lru_cache
8
-
8
+ import sys
9
9
 
10
10
  DEFAULT_CONFIG_LOCATION = "./ara/.araconfig/ara_config.json"
11
11
 
12
-
13
12
  class LLMConfigItem(BaseModel):
14
13
  provider: str
15
14
  model: str
16
- temperature: float
15
+ temperature: float = Field(ge=0.0, le=1.0)
17
16
  max_tokens: Optional[int] = None
17
+
18
+ @field_validator('temperature')
19
+ @classmethod
20
+ def validate_temperature(cls, v: float, info) -> float:
21
+ if not 0.0 <= v <= 1.0:
22
+ print(f"Warning: Temperature is outside the 0.0 to 1.0 range")
23
+ # Return a valid default
24
+ return 0.8
25
+ return v
18
26
 
27
+ class ExtCodeDirItem(BaseModel):
28
+ source_dir: str
19
29
 
20
30
  class ARAconfig(BaseModel):
21
- ext_code_dirs: List[Dict[str, str]] = [
22
- {"source_dir_1": "./src"},
23
- {"source_dir_2": "./tests"},
24
- ]
31
+ ext_code_dirs: List[ExtCodeDirItem] = Field(default_factory=lambda: [
32
+ ExtCodeDirItem(source_dir="./src"),
33
+ ExtCodeDirItem(source_dir="./tests")
34
+ ])
25
35
  glossary_dir: str = "./glossary"
26
36
  doc_dir: str = "./docs"
27
37
  local_prompt_templates_dir: str = "./ara/.araconfig"
28
38
  custom_prompt_templates_subdir: Optional[str] = "custom-prompt-modules"
29
39
  local_ara_templates_dir: str = "./ara/.araconfig/templates/"
30
- ara_prompt_given_list_includes: List[str] = [
40
+ ara_prompt_given_list_includes: List[str] = Field(default_factory=lambda: [
31
41
  "*.businessgoal",
32
42
  "*.vision",
33
43
  "*.capability",
@@ -42,53 +52,76 @@ class ARAconfig(BaseModel):
42
52
  "*.png",
43
53
  "*.jpg",
44
54
  "*.jpeg",
45
- ]
46
- llm_config: Dict[str, LLMConfigItem] = {
47
- "gpt-4o": {
48
- "provider": "openai",
49
- "model": "openai/gpt-4o",
50
- "temperature": 0.8,
51
- "max_tokens": 16384
52
- },
53
- "gpt-4.1": {
54
- "provider": "openai",
55
- "model": "openai/gpt-4.1",
56
- "temperature": 0.8,
57
- "max_tokens": 1024
58
- },
59
- "o3-mini": {
60
- "provider": "openai",
61
- "model": "openai/o3-mini",
62
- "temperature": 1.0,
63
- "max_tokens": 1024
64
- },
65
- "opus-4": {
66
- "provider": "anthropic",
67
- "model": "anthropic/claude-opus-4-20250514",
68
- "temperature": 0.8,
69
- "max_tokens": 32000
70
- },
71
- "sonnet-4": {
72
- "provider": "anthropic",
73
- "model": "anthropic/claude-sonnet-4-20250514",
74
- "temperature": 0.8,
75
- "max_tokens": 1024
76
- },
77
- "together-ai-llama-2": {
78
- "provider": "together_ai",
79
- "model": "together_ai/togethercomputer/llama-2-70b",
80
- "temperature": 0.8,
81
- "max_tokens": 1024
82
- },
83
- "groq-llama-3": {
84
- "provider": "groq",
85
- "model": "groq/llama3-70b-8192",
86
- "temperature": 0.8,
87
- "max_tokens": 1024
88
- }
89
- }
55
+ ])
56
+ llm_config: Dict[str, LLMConfigItem] = Field(default_factory=lambda: {
57
+ "gpt-4o": LLMConfigItem(
58
+ provider="openai",
59
+ model="openai/gpt-4o",
60
+ temperature=0.8,
61
+ max_tokens=16384
62
+ ),
63
+ "gpt-4.1": LLMConfigItem(
64
+ provider="openai",
65
+ model="openai/gpt-4.1",
66
+ temperature=0.8,
67
+ max_tokens=1024
68
+ ),
69
+ "o3-mini": LLMConfigItem(
70
+ provider="openai",
71
+ model="openai/o3-mini",
72
+ temperature=1.0,
73
+ max_tokens=1024
74
+ ),
75
+ "opus-4": LLMConfigItem(
76
+ provider="anthropic",
77
+ model="anthropic/claude-opus-4-20250514",
78
+ temperature=0.8,
79
+ max_tokens=32000
80
+ ),
81
+ "sonnet-4": LLMConfigItem(
82
+ provider="anthropic",
83
+ model="anthropic/claude-sonnet-4-20250514",
84
+ temperature=0.8,
85
+ max_tokens=1024
86
+ ),
87
+ "together-ai-llama-2": LLMConfigItem(
88
+ provider="together_ai",
89
+ model="together_ai/togethercomputer/llama-2-70b",
90
+ temperature=0.8,
91
+ max_tokens=1024
92
+ ),
93
+ "groq-llama-3": LLMConfigItem(
94
+ provider="groq",
95
+ model="groq/llama3-70b-8192",
96
+ temperature=0.8,
97
+ max_tokens=1024
98
+ )
99
+ })
90
100
  default_llm: Optional[str] = "gpt-4o"
101
+
102
+ model_config = {
103
+ "extra": "forbid" # This will help identify unrecognized keys
104
+ }
91
105
 
106
+ @model_validator(mode='after')
107
+ def check_critical_fields(self) -> 'ARAconfig':
108
+ """Check for empty critical fields and use defaults if needed"""
109
+ critical_fields = {
110
+ 'ext_code_dirs': [ExtCodeDirItem(source_dir="./src"), ExtCodeDirItem(source_dir="./tests")],
111
+ 'local_ara_templates_dir': "./ara/.araconfig/templates/",
112
+ 'local_prompt_templates_dir': "./ara/.araconfig",
113
+ 'glossary_dir': "./glossary"
114
+ }
115
+
116
+ for field, default_value in critical_fields.items():
117
+ current_value = getattr(self, field)
118
+ if (not current_value or
119
+ (isinstance(current_value, list) and len(current_value) == 0) or
120
+ (isinstance(current_value, str) and current_value.strip() == "")):
121
+ print(f"Warning: Value for '{field}' is missing or empty.")
122
+ setattr(self, field, default_value)
123
+
124
+ return self
92
125
 
93
126
  # Function to ensure the necessary directories exist
94
127
  @lru_cache(maxsize=None)
@@ -98,37 +131,106 @@ def ensure_directory_exists(directory: str):
98
131
  print(f"New directory created at {directory}")
99
132
  return directory
100
133
 
101
-
102
- def validate_config_data(filepath: str):
103
- with open(filepath, "r", encoding="utf-8") as file:
104
- data = json.load(file)
134
+ def handle_unrecognized_keys(data: dict, known_fields: set) -> dict:
135
+ """Remove unrecognized keys and warn the user"""
136
+ cleaned_data = {}
137
+ for key, value in data.items():
138
+ if key not in known_fields:
139
+ print(f"Warning: {key} is not recognized as a valid configuration option.")
140
+ else:
141
+ cleaned_data[key] = value
142
+ return cleaned_data
143
+
144
+ def fix_llm_temperatures(data: dict) -> dict:
145
+ """Fix invalid temperatures in LLM configurations"""
146
+ if 'llm_config' in data:
147
+ for model_key, model_config in data['llm_config'].items():
148
+ if isinstance(model_config, dict) and 'temperature' in model_config:
149
+ temp = model_config['temperature']
150
+ if not 0.0 <= temp <= 1.0:
151
+ print(f"Warning: Temperature for model '{model_key}' is outside the 0.0 to 1.0 range")
152
+ model_config['temperature'] = 0.8
105
153
  return data
106
154
 
155
+ def validate_and_fix_config_data(filepath: str) -> dict:
156
+ """Load, validate, and fix configuration data"""
157
+ try:
158
+ with open(filepath, "r", encoding="utf-8") as file:
159
+ data = json.load(file)
160
+
161
+ # Get known fields from the ARAconfig model
162
+ known_fields = set(ARAconfig.model_fields.keys())
163
+
164
+ # Handle unrecognized keys
165
+ data = handle_unrecognized_keys(data, known_fields)
166
+
167
+ # Fix LLM temperatures before validation
168
+ data = fix_llm_temperatures(data)
169
+
170
+ return data
171
+ except json.JSONDecodeError as e:
172
+ print(f"Error: Invalid JSON in configuration file: {e}")
173
+ print("Creating new configuration with defaults...")
174
+ return {}
175
+ except Exception as e:
176
+ print(f"Error reading configuration file: {e}")
177
+ return {}
107
178
 
108
179
  # Function to read the JSON file and return an ARAconfig model
109
180
  @lru_cache(maxsize=1)
110
181
  def read_data(filepath: str) -> ARAconfig:
182
+ # Ensure the directory for the config file exists
183
+ config_dir = dirname(filepath)
184
+ ensure_directory_exists(config_dir)
185
+
111
186
  if not exists(filepath):
112
- # If file does not exist, create it with default values
187
+ # If the file does not exist, create it with default values
113
188
  default_config = ARAconfig()
114
-
115
- with open(filepath, "w", encoding="utf-8") as file:
116
- json.dump(default_config.model_dump(mode='json'), file, indent=4)
117
-
189
+ save_data(filepath, default_config)
118
190
  print(
119
- f"ara-cli configuration file '{filepath}' created with default configuration. Please modify it as needed and re-run your command"
191
+ f"ara-cli configuration file '{filepath}' created with default configuration."
192
+ f" Please modify it as needed and re-run your command"
120
193
  )
121
- exit() # Exit the application
122
-
123
- data = validate_config_data(filepath)
124
- return ARAconfig(**data)
125
-
194
+ sys.exit(0) # Exit the application
195
+
196
+ # Validate and load the existing configuration
197
+ data = validate_and_fix_config_data(filepath)
198
+
199
+ try:
200
+ # Try to create the config with the loaded data
201
+ config = ARAconfig(**data)
202
+
203
+ # Save the potentially fixed configuration back
204
+ save_data(filepath, config)
205
+
206
+ return config
207
+ except ValidationError as e:
208
+ print(f"ValidationError: {e}")
209
+ print("Correcting configuration with default values...")
210
+
211
+ # Create a default config
212
+ default_config = ARAconfig()
213
+
214
+ # Try to preserve valid fields from the original data
215
+ for field_name, field_value in data.items():
216
+ if field_name in ARAconfig.model_fields:
217
+ try:
218
+ # Attempt to set the field value
219
+ setattr(default_config, field_name, field_value)
220
+ except:
221
+ # If it fails, keep the default
222
+ pass
223
+
224
+ # Save the corrected configuration
225
+ save_data(filepath, default_config)
226
+ print("Fixed configuration saved to file.")
227
+
228
+ return default_config
126
229
 
127
230
  # Function to save the modified configuration back to the JSON file
128
231
  def save_data(filepath: str, config: ARAconfig):
129
232
  with open(filepath, "w", encoding="utf-8") as file:
130
- json.dump(config.model_dump(mode='json'), file, indent=4)
131
-
233
+ json.dump(config.model_dump(), file, indent=4)
132
234
 
133
235
  # Singleton for configuration management
134
236
  class ConfigManager:
@@ -143,4 +245,10 @@ class ConfigManager:
143
245
  makedirs(config_dir)
144
246
 
145
247
  cls._config_instance = read_data(filepath)
146
- return cls._config_instance
248
+ return cls._config_instance
249
+
250
+ @classmethod
251
+ def reset(cls):
252
+ """Reset the configuration instance (useful for testing)"""
253
+ cls._config_instance = None
254
+ read_data.cache_clear()
@@ -1,3 +1,4 @@
1
+ from ara_cli.artefact_scan import check_file
1
2
  from ara_cli.artefact_fuzzy_search import (
2
3
  find_closest_name_matches,
3
4
  extract_artefact_names_of_classifier,
@@ -29,36 +30,45 @@ def parse_report(content: str) -> Dict[str, List[Tuple[str, str]]]:
29
30
  Parses the incompatible artefacts report and returns structured data.
30
31
  Returns a dictionary where keys are artefact classifiers, and values are lists of (file_path, reason) tuples.
31
32
  """
33
+ def is_valid_report(lines: List[str]) -> bool:
34
+ return bool(lines) and lines[0] == "# Artefact Check Report"
35
+
36
+ def has_no_problems(lines: List[str]) -> bool:
37
+ return len(lines) >= 3 and lines[2] == "No problems found."
38
+
39
+ def parse_classifier(line: str) -> Optional[str]:
40
+ if line.startswith("## "):
41
+ return line[3:].strip()
42
+ return None
43
+
44
+ def parse_issue(line: str) -> Optional[Tuple[str, str]]:
45
+ if not line.startswith("- "):
46
+ return None
47
+ parts = line.split("`", 2)
48
+ if len(parts) < 3:
49
+ return None
50
+ file_path = parts[1]
51
+ reason = parts[2].split(":", 1)[1].strip() if ":" in parts[2] else ""
52
+ return file_path, reason
53
+
32
54
  lines = content.splitlines()
55
+ if not is_valid_report(lines) or has_no_problems(lines):
56
+ return {}
57
+
33
58
  issues = {}
34
59
  current_classifier = None
35
60
 
36
- if not lines or lines[0] != "# Artefact Check Report":
37
- return issues
38
- return issues
39
-
40
- if len(lines) >= 3 and lines[2] == "No problems found.":
41
- return issues
42
- return issues
43
-
44
- for line in lines[1:]:
45
- line = line.strip()
61
+ for line in map(str.strip, lines[1:]):
46
62
  if not line:
47
63
  continue
48
-
49
- if line.startswith("## "):
50
- current_classifier = line[3:].strip()
64
+ classifier = parse_classifier(line)
65
+ if classifier is not None:
66
+ current_classifier = classifier
51
67
  issues[current_classifier] = []
52
-
53
- elif line.startswith("- ") and current_classifier is not None:
54
- parts = line.split("`", 2)
55
- if len(parts) < 3:
56
- continue
57
-
58
- file_path = parts[1]
59
- reason = parts[2].split(":", 1)[1].strip() if ":" in parts[2] else ""
60
- issues[current_classifier].append((file_path, reason))
61
-
68
+ continue
69
+ issue = parse_issue(line)
70
+ if issue and current_classifier is not None:
71
+ issues[current_classifier].append(issue)
62
72
  return issues
63
73
 
64
74
 
@@ -159,7 +169,7 @@ def ask_for_correct_contribution(
159
169
 
160
170
  print(
161
171
  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>)."
172
+ f"Please provide a valid contribution or contribution will be empty ([classifier] [file_name])."
163
173
  )
164
174
 
165
175
  user_input = input().strip()
@@ -179,7 +189,9 @@ def ask_for_correct_contribution(
179
189
  def ask_for_contribution_choice(
180
190
  choices, artefact_info: Optional[tuple[str, str]] = None
181
191
  ) -> Optional[str]:
182
- artefact_name, artefact_classifier = artefact_info
192
+ artefact_name, artefact_classifier = (
193
+ artefact_info if artefact_info else (None, None)
194
+ )
183
195
  message = "Found multiple close matches for the contribution"
184
196
  if artefact_name and artefact_classifier:
185
197
  message += f" of the {artefact_classifier} '{artefact_name}'"
@@ -379,68 +391,118 @@ def apply_autofix(
379
391
  file_path: str,
380
392
  classifier: str,
381
393
  reason: str,
394
+ single_pass: bool = False,
382
395
  deterministic: bool = True,
383
396
  non_deterministic: bool = True,
384
397
  classified_artefact_info: Optional[Dict[str, List[Dict[str, str]]]] = None,
385
398
  ) -> bool:
386
- artefact_text = read_artefact(file_path)
387
- if artefact_text is None:
399
+ """
400
+ Applies fixes to a single artefact file iteratively until it is valid
401
+ or a fix cannot be applied. If single_pass is True, it runs for only one attempt.
402
+ """
403
+ deterministic_markers_to_functions = {
404
+ "Filename-Title Mismatch": fix_title_mismatch,
405
+ "Invalid Contribution Reference": fix_contribution,
406
+ }
407
+
408
+ def populate_classified_artefact_info(force: bool = False):
409
+ nonlocal classified_artefact_info
410
+ if force or classified_artefact_info is None:
411
+ file_classifier = FileClassifier(os)
412
+ classified_artefact_info = file_classifier.classify_files()
413
+
414
+ def determine_attempt_count() -> int:
415
+ nonlocal single_pass, file_path
416
+ if single_pass:
417
+ print(f"Single-pass mode enabled for {file_path}. Running for 1 attempt.")
418
+ return 1
419
+ return 3
420
+
421
+ def apply_deterministic_fix() -> str:
422
+ nonlocal deterministic, deterministic_issue, corrected_text, file_path, artefact_text, artefact_class, classified_artefact_info
423
+ if deterministic and deterministic_issue:
424
+ print(f"Applying deterministic fix for '{deterministic_issue}'...")
425
+ fix_function = deterministic_markers_to_functions[deterministic_issue]
426
+ return fix_function(
427
+ file_path=file_path,
428
+ artefact_text=artefact_text,
429
+ artefact_class=artefact_class,
430
+ classified_artefact_info=classified_artefact_info,
431
+ )
432
+ return corrected_text
433
+
434
+ def apply_non_deterministic_fix() -> Optional[str]:
435
+ """
436
+ Applies LLM fix. Return None in case of an exception
437
+ """
438
+ nonlocal non_deterministic, deterministic_issue, corrected_text, artefact_type, current_reason, file_path, artefact_text
439
+ if non_deterministic and not deterministic_issue:
440
+ print("Applying non-deterministic (LLM) fix...")
441
+ prompt = construct_prompt(artefact_type, current_reason, file_path, artefact_text)
442
+ try:
443
+ corrected_artefact = run_agent(prompt, artefact_class)
444
+ corrected_text = corrected_artefact.serialize()
445
+ except Exception as e:
446
+ print(f" ❌ LLM agent failed to fix artefact at {file_path}: {e}")
447
+ return None
448
+ return corrected_text
449
+
450
+ def should_skip() -> bool:
451
+ nonlocal deterministic_issue, deterministic, non_deterministic
452
+ if not non_deterministic and not deterministic_issue:
453
+ print(f"Skipping non-deterministic fix for {file_path} as per request.")
454
+ return True
455
+ if not deterministic and deterministic_issue:
456
+ print(f"Skipping fix for {file_path} as per request flags.")
457
+ return True
388
458
  return False
389
459
 
390
460
  artefact_type, artefact_class = determine_artefact_type_and_class(classifier)
391
461
  if artefact_type is None or artefact_class is None:
392
462
  return False
393
463
 
394
- if classified_artefact_info is None:
395
- file_classifier = FileClassifier(os)
396
- classified_file_info = file_classifier.classified_files()
464
+ populate_classified_artefact_info()
465
+ max_attempts = determine_attempt_count()
397
466
 
398
- deterministic_markers_to_functions = {
399
- "Filename-Title Mismatch": fix_title_mismatch,
400
- "Invalid Contribution Reference": fix_contribution,
401
- }
467
+ for attempt in range(max_attempts):
468
+ is_valid, current_reason = check_file(file_path, artefact_class, classified_artefact_info)
469
+
470
+ if is_valid:
471
+ print(f"✅ Artefact at {file_path} is now valid.")
472
+ return True
473
+
474
+ print(f"Attempting to fix {file_path} (Attempt {attempt + 1}/{max_attempts})...")
475
+ print(f" Reason: {current_reason}")
476
+
477
+ artefact_text = read_artefact(file_path)
478
+ if artefact_text is None:
479
+ return False
402
480
 
403
- try:
404
481
  deterministic_issue = next(
405
482
  (
406
483
  marker
407
- for marker in deterministic_markers_to_functions.keys()
408
- if marker in reason
484
+ for marker in deterministic_markers_to_functions
485
+ if marker in current_reason
409
486
  ),
410
487
  None,
411
488
  )
412
- except StopIteration:
413
- pass
414
- is_deterministic_issue = deterministic_issue is not None
415
-
416
- if deterministic and is_deterministic_issue:
417
- print(f"Attempting deterministic fix for {file_path}...")
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
- )
424
- write_corrected_artefact(file_path, corrected_text)
425
- return True
426
-
427
- # Attempt non-deterministic fix if requested and the issue is NOT deterministic
428
- if non_deterministic and not is_deterministic_issue:
429
- print(f"Attempting non-deterministic (LLM) fix for {file_path}...")
430
- prompt = construct_prompt(artefact_type, reason, file_path, artefact_text)
431
- try:
432
- corrected_artefact = run_agent(prompt, artefact_class)
433
- corrected_text = corrected_artefact.serialize()
434
- write_corrected_artefact(file_path, corrected_text)
435
- return True
436
- except Exception as e:
437
- print(f"LLM agent failed to fix artefact at {file_path}: {e}")
489
+
490
+ if should_skip():
438
491
  return False
439
492
 
440
- # Log if a fix was skipped due to flags
441
- if is_deterministic_issue and not deterministic:
442
- print(f"Skipping deterministic fix for {file_path} as per request.")
443
- elif not is_deterministic_issue and not non_deterministic:
444
- print(f"Skipping non-deterministic fix for {file_path} as per request.")
493
+ corrected_text = None
494
+
495
+ corrected_text = apply_deterministic_fix()
496
+ corrected_text = apply_non_deterministic_fix()
497
+
498
+ if corrected_text is None or corrected_text.strip() == artefact_text.strip():
499
+ print(" Fixing attempt did not alter the file. Stopping to prevent infinite loop.")
500
+ return False
501
+
502
+ write_corrected_artefact(file_path, corrected_text)
503
+
504
+ print(" File modified. Re-classifying artefact information for next check...")
505
+ populate_classified_artefact_info(force=True)
445
506
 
507
+ print(f"❌ Failed to fix {file_path} after {max_attempts} attempts.")
446
508
  return False
@@ -106,7 +106,7 @@ class ArtefactCreator:
106
106
  if not self.handle_existing_files(file_exists):
107
107
  return
108
108
 
109
- artefact = template_artefact_of_type(classifier, filename)
109
+ artefact = template_artefact_of_type(classifier, filename, False)
110
110
 
111
111
  if parent_classifier and parent_name:
112
112
  artefact.set_contribution(