ara-cli 0.1.9.77__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 +245 -66
- ara_cli/ara_command_action.py +128 -63
- ara_cli/ara_config.py +201 -177
- 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 +214 -28
- ara_cli/artefact_creator.py +5 -8
- ara_cli/artefact_deleter.py +2 -4
- ara_cli/artefact_fuzzy_search.py +13 -6
- ara_cli/artefact_lister.py +29 -55
- ara_cli/artefact_models/artefact_data_retrieval.py +23 -0
- ara_cli/artefact_models/artefact_model.py +106 -25
- ara_cli/artefact_models/artefact_templates.py +23 -13
- ara_cli/artefact_models/epic_artefact_model.py +11 -2
- ara_cli/artefact_models/feature_artefact_model.py +56 -1
- ara_cli/artefact_models/userstory_artefact_model.py +15 -3
- ara_cli/artefact_reader.py +4 -5
- ara_cli/artefact_renamer.py +6 -2
- ara_cli/artefact_scan.py +2 -2
- ara_cli/chat.py +594 -219
- 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 +3 -2
- 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/prompt_extractor.py +214 -87
- ara_cli/prompt_handler.py +508 -146
- ara_cli/tag_extractor.py +54 -24
- ara_cli/template_loader.py +245 -0
- ara_cli/template_manager.py +14 -4
- 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 +7 -1
- ara_cli/version.py +1 -1
- ara_cli-0.1.10.8.dist-info/METADATA +241 -0
- {ara_cli-0.1.9.77.dist-info → ara_cli-0.1.10.8.dist-info}/RECORD +104 -59
- tests/test_ara_command_action.py +66 -52
- tests/test_ara_config.py +200 -279
- tests/test_artefact_autofix.py +361 -5
- tests/test_artefact_lister.py +52 -132
- tests/test_artefact_scan.py +1 -1
- tests/test_chat.py +2009 -603
- tests/test_file_classifier.py +23 -0
- tests/test_file_creator.py +3 -5
- tests/test_global_file_lister.py +131 -0
- 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
- ara_cli/ara_command_parser.py +0 -536
- 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-0.1.9.77.dist-info/METADATA +0 -18
- {ara_cli-0.1.9.77.dist-info → ara_cli-0.1.10.8.dist-info}/WHEEL +0 -0
- {ara_cli-0.1.9.77.dist-info → ara_cli-0.1.10.8.dist-info}/entry_points.txt +0 -0
- {ara_cli-0.1.9.77.dist-info → ara_cli-0.1.10.8.dist-info}/top_level.txt +0 -0
ara_cli/artefact_autofix.py
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
from ara_cli.error_handler import AraError
|
|
1
2
|
from ara_cli.artefact_scan import check_file
|
|
2
3
|
from ara_cli.artefact_fuzzy_search import (
|
|
3
4
|
find_closest_name_matches,
|
|
@@ -10,6 +11,7 @@ from ara_cli.artefact_models.artefact_model import Artefact
|
|
|
10
11
|
from typing import Optional, Dict, List, Tuple
|
|
11
12
|
import difflib
|
|
12
13
|
import os
|
|
14
|
+
import re
|
|
13
15
|
|
|
14
16
|
|
|
15
17
|
def populate_classified_artefact_info(
|
|
@@ -104,8 +106,9 @@ def determine_artefact_type_and_class(classifier):
|
|
|
104
106
|
|
|
105
107
|
artefact_class = artefact_type_mapping.get(artefact_type)
|
|
106
108
|
if not artefact_class:
|
|
107
|
-
|
|
108
|
-
|
|
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
|
|
109
112
|
|
|
110
113
|
return artefact_type, artefact_class
|
|
111
114
|
|
|
@@ -144,11 +147,11 @@ def run_agent(prompt, artefact_class):
|
|
|
144
147
|
# anthropic:claude-4-sonnet-20250514
|
|
145
148
|
agent = Agent(
|
|
146
149
|
model="anthropic:claude-4-sonnet-20250514",
|
|
147
|
-
|
|
150
|
+
output_type=artefact_class,
|
|
148
151
|
instrument=True,
|
|
149
152
|
)
|
|
150
153
|
result = agent.run_sync(prompt)
|
|
151
|
-
return result.
|
|
154
|
+
return result.output
|
|
152
155
|
|
|
153
156
|
|
|
154
157
|
def write_corrected_artefact(file_path, corrected_text):
|
|
@@ -196,36 +199,52 @@ def ask_for_correct_contribution(
|
|
|
196
199
|
return name, classifier
|
|
197
200
|
|
|
198
201
|
|
|
199
|
-
def ask_for_contribution_choice(
|
|
200
|
-
|
|
201
|
-
) -> Optional[str]:
|
|
202
|
-
artefact_name, artefact_classifier = (
|
|
203
|
-
artefact_info if artefact_info else (None, None)
|
|
204
|
-
)
|
|
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)
|
|
205
204
|
message = "Found multiple close matches for the contribution"
|
|
206
205
|
if artefact_name and artefact_classifier:
|
|
207
206
|
message += f" of the {artefact_classifier} '{artefact_name}'"
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
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
|
+
|
|
214
233
|
try:
|
|
215
234
|
choice_index = int(choice_number) - 1
|
|
216
235
|
if choice_index < 0 or choice_index >= len(choices):
|
|
217
|
-
print("Invalid choice. Aborting
|
|
236
|
+
print("Invalid choice. Aborting operation.")
|
|
218
237
|
return None
|
|
219
|
-
|
|
238
|
+
return choices[choice_index]
|
|
220
239
|
except ValueError:
|
|
221
|
-
print("Invalid input. Aborting
|
|
240
|
+
print("Invalid input. Aborting operation.")
|
|
222
241
|
return None
|
|
223
|
-
return choice
|
|
224
242
|
|
|
225
243
|
|
|
226
|
-
def
|
|
227
|
-
|
|
228
|
-
|
|
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)
|
|
229
248
|
|
|
230
249
|
|
|
231
250
|
def _update_rule(
|
|
@@ -249,6 +268,9 @@ def _update_rule(
|
|
|
249
268
|
return
|
|
250
269
|
if not closest_rule_match:
|
|
251
270
|
return
|
|
271
|
+
if len(closest_rule_match) > 1:
|
|
272
|
+
artefact.contribution.rule = ask_for_rule_choice(closest_rule_match)
|
|
273
|
+
return
|
|
252
274
|
artefact.contribution.rule = closest_rule_match[0]
|
|
253
275
|
|
|
254
276
|
|
|
@@ -353,6 +375,131 @@ def set_closest_contribution(
|
|
|
353
375
|
return artefact, True
|
|
354
376
|
|
|
355
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
|
+
|
|
356
503
|
def fix_title_mismatch(
|
|
357
504
|
file_path: str, artefact_text: str, artefact_class, **kwargs
|
|
358
505
|
) -> str:
|
|
@@ -429,6 +576,42 @@ def fix_rule(
|
|
|
429
576
|
return artefact.serialize()
|
|
430
577
|
|
|
431
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()
|
|
584
|
+
|
|
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
|
|
590
|
+
|
|
591
|
+
if desc_start_idx == -1:
|
|
592
|
+
return artefact_text # No description, nothing to fix.
|
|
593
|
+
|
|
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
|
+
|
|
432
615
|
def should_skip_issue(deterministic_issue, deterministic, non_deterministic, file_path) -> bool:
|
|
433
616
|
if not non_deterministic and not deterministic_issue:
|
|
434
617
|
print(f"Skipping non-deterministic fix for {file_path} as per request.")
|
|
@@ -475,7 +658,7 @@ def apply_non_deterministic_fix(
|
|
|
475
658
|
corrected_artefact = run_agent(prompt, artefact_class)
|
|
476
659
|
corrected_text = corrected_artefact.serialize()
|
|
477
660
|
except Exception as e:
|
|
478
|
-
print(f"
|
|
661
|
+
print(f" ❌ LLM agent failed to fix artefact at {file_path}: {e}")
|
|
479
662
|
return None
|
|
480
663
|
return corrected_text
|
|
481
664
|
|
|
@@ -504,7 +687,7 @@ def attempt_autofix_loop(
|
|
|
504
687
|
print(
|
|
505
688
|
f"Attempting to fix {file_path} (Attempt {attempt + 1}/{max_attempts})..."
|
|
506
689
|
)
|
|
507
|
-
print(f"
|
|
690
|
+
print(f" Reason: {current_reason}")
|
|
508
691
|
|
|
509
692
|
artefact_text = read_artefact(file_path)
|
|
510
693
|
if artefact_text is None:
|
|
@@ -536,13 +719,13 @@ def attempt_autofix_loop(
|
|
|
536
719
|
|
|
537
720
|
if corrected_text is None or corrected_text.strip() == artefact_text.strip():
|
|
538
721
|
print(
|
|
539
|
-
"
|
|
722
|
+
" Fixing attempt did not alter the file. Stopping to prevent infinite loop."
|
|
540
723
|
)
|
|
541
724
|
return False
|
|
542
725
|
|
|
543
726
|
write_corrected_artefact(file_path, corrected_text)
|
|
544
727
|
|
|
545
|
-
print("
|
|
728
|
+
print(" File modified. Re-classifying artefact information for next check...")
|
|
546
729
|
classified_artefact_info = populate_classified_artefact_info(classified_artefact_info, force=True)
|
|
547
730
|
|
|
548
731
|
print(f"❌ Failed to fix {file_path} after {max_attempts} attempts.")
|
|
@@ -565,6 +748,9 @@ def apply_autofix(
|
|
|
565
748
|
"Filename-Title Mismatch": fix_title_mismatch,
|
|
566
749
|
"Invalid Contribution Reference": fix_contribution,
|
|
567
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,
|
|
568
754
|
}
|
|
569
755
|
|
|
570
756
|
artefact_type, artefact_class = determine_artefact_type_and_class(classifier)
|
|
@@ -583,4 +769,4 @@ def apply_autofix(
|
|
|
583
769
|
deterministic=deterministic,
|
|
584
770
|
non_deterministic=non_deterministic,
|
|
585
771
|
classified_artefact_info=classified_artefact_info,
|
|
586
|
-
)
|
|
772
|
+
)
|
ara_cli/artefact_creator.py
CHANGED
|
@@ -38,11 +38,9 @@ class ArtefactCreator:
|
|
|
38
38
|
destination = Path(dir_path) / dest_name
|
|
39
39
|
|
|
40
40
|
if not source.exists():
|
|
41
|
-
print("[ERROR] Source file does not exist!")
|
|
42
41
|
raise FileNotFoundError(f"Source file {source} not found!")
|
|
43
42
|
|
|
44
43
|
if not destination.parent.exists():
|
|
45
|
-
print("[ERROR] Destination directory does not exist!")
|
|
46
44
|
raise NotADirectoryError(f"Destination directory {destination.parent} does not exist!")
|
|
47
45
|
|
|
48
46
|
copyfile(source, destination)
|
|
@@ -70,9 +68,7 @@ class ArtefactCreator:
|
|
|
70
68
|
def validate_template(self, template_path, classifier):
|
|
71
69
|
template_name = f"template.{classifier}"
|
|
72
70
|
if not self.template_exists(template_path, template_name):
|
|
73
|
-
|
|
74
|
-
return False
|
|
75
|
-
return True
|
|
71
|
+
raise FileNotFoundError(f"Template file '{template_name}' not found in the specified template path.")
|
|
76
72
|
|
|
77
73
|
def set_artefact_parent(self, artefact, parent_classifier, parent_file_name) -> Artefact:
|
|
78
74
|
classified_artefacts = ArtefactReader.read_artefacts()
|
|
@@ -94,8 +90,7 @@ class ArtefactCreator:
|
|
|
94
90
|
navigator.navigate_to_target()
|
|
95
91
|
|
|
96
92
|
if not Classifier.is_valid_classifier(classifier):
|
|
97
|
-
|
|
98
|
-
return
|
|
93
|
+
raise ValueError("Invalid classifier provided. Please provide a valid classifier.")
|
|
99
94
|
|
|
100
95
|
sub_directory = Classifier.get_sub_directory(classifier)
|
|
101
96
|
file_path = self.file_system.path.join(sub_directory, f"{filename}.{classifier}")
|
|
@@ -106,7 +101,7 @@ class ArtefactCreator:
|
|
|
106
101
|
if not self.handle_existing_files(file_exists):
|
|
107
102
|
return
|
|
108
103
|
|
|
109
|
-
artefact = template_artefact_of_type(classifier, filename,
|
|
104
|
+
artefact = template_artefact_of_type(classifier, filename, True)
|
|
110
105
|
|
|
111
106
|
if parent_classifier and parent_name:
|
|
112
107
|
artefact.set_contribution(
|
|
@@ -114,6 +109,8 @@ class ArtefactCreator:
|
|
|
114
109
|
classifier=parent_classifier,
|
|
115
110
|
rule=rule
|
|
116
111
|
)
|
|
112
|
+
else:
|
|
113
|
+
artefact.set_contribution(None, None, None)
|
|
117
114
|
|
|
118
115
|
artefact_content = artefact.serialize()
|
|
119
116
|
rmtree(dir_path, ignore_errors=True)
|
ara_cli/artefact_deleter.py
CHANGED
|
@@ -20,16 +20,14 @@ class ArtefactDeleter:
|
|
|
20
20
|
self.navigate_to_target()
|
|
21
21
|
|
|
22
22
|
if not Classifier.is_valid_classifier(classifier):
|
|
23
|
-
|
|
24
|
-
return
|
|
23
|
+
raise ValueError("Invalid classifier provided. Please provide a valid classifier.")
|
|
25
24
|
|
|
26
25
|
sub_directory = Classifier.get_sub_directory(classifier)
|
|
27
26
|
file_path = self.file_system.path.join(sub_directory, f"{filename}.{classifier}")
|
|
28
27
|
dir_path = self.file_system.path.join(sub_directory, f"{filename}.data")
|
|
29
28
|
|
|
30
29
|
if not self.file_system.path.exists(file_path):
|
|
31
|
-
|
|
32
|
-
return
|
|
30
|
+
raise FileNotFoundError(f"Artefact {file_path} not found.")
|
|
33
31
|
if not force:
|
|
34
32
|
user_choice = input(f"Are you sure you want to delete the file {filename} and data directory if existing? (y/N): ")
|
|
35
33
|
|
ara_cli/artefact_fuzzy_search.py
CHANGED
|
@@ -1,11 +1,16 @@
|
|
|
1
1
|
import difflib
|
|
2
2
|
from textwrap import indent
|
|
3
3
|
from typing import Optional
|
|
4
|
+
from . import error_handler
|
|
5
|
+
from ara_cli.error_handler import AraError
|
|
4
6
|
|
|
5
7
|
|
|
6
|
-
def suggest_close_names(artefact_name: str, all_artefact_names: list[str], message: str, cutoff=0.5):
|
|
8
|
+
def suggest_close_names(artefact_name: str, all_artefact_names: list[str], message: str, cutoff=0.5, report_as_error: bool = False):
|
|
7
9
|
closest_matches = difflib.get_close_matches(artefact_name, all_artefact_names, cutoff=cutoff)
|
|
8
|
-
|
|
10
|
+
if report_as_error:
|
|
11
|
+
error_handler.report_error(AraError(message))
|
|
12
|
+
else:
|
|
13
|
+
print(message)
|
|
9
14
|
if not closest_matches:
|
|
10
15
|
return
|
|
11
16
|
print("Closest matches:")
|
|
@@ -13,23 +18,25 @@ def suggest_close_names(artefact_name: str, all_artefact_names: list[str], messa
|
|
|
13
18
|
print(f" - {match}")
|
|
14
19
|
|
|
15
20
|
|
|
16
|
-
def suggest_close_name_matches(artefact_name: str, all_artefact_names: list[str]):
|
|
21
|
+
def suggest_close_name_matches(artefact_name: str, all_artefact_names: list[str], report_as_error: bool = False):
|
|
17
22
|
message = f"No match found for artefact with name '{artefact_name}'"
|
|
18
23
|
|
|
19
24
|
suggest_close_names(
|
|
20
25
|
artefact_name=artefact_name,
|
|
21
26
|
all_artefact_names=all_artefact_names,
|
|
22
|
-
message=message
|
|
27
|
+
message=message,
|
|
28
|
+
report_as_error=report_as_error
|
|
23
29
|
)
|
|
24
30
|
|
|
25
31
|
|
|
26
|
-
def suggest_close_name_matches_for_parent(artefact_name: str, all_artefact_names: list[str], parent_name: str):
|
|
32
|
+
def suggest_close_name_matches_for_parent(artefact_name: str, all_artefact_names: list[str], parent_name: str, report_as_error: bool = False):
|
|
27
33
|
message = f"No match found for parent of '{artefact_name}' with name '{parent_name}'"
|
|
28
34
|
|
|
29
35
|
suggest_close_names(
|
|
30
36
|
artefact_name=parent_name,
|
|
31
37
|
all_artefact_names=all_artefact_names,
|
|
32
|
-
message=message
|
|
38
|
+
message=message,
|
|
39
|
+
report_as_error=report_as_error
|
|
33
40
|
)
|
|
34
41
|
|
|
35
42
|
|
ara_cli/artefact_lister.py
CHANGED
|
@@ -1,9 +1,13 @@
|
|
|
1
1
|
from ara_cli.file_classifier import FileClassifier
|
|
2
2
|
from ara_cli.artefact_reader import ArtefactReader
|
|
3
3
|
from ara_cli.file_lister import list_files_in_directory
|
|
4
|
-
from ara_cli.artefact_models.artefact_model import Artefact
|
|
5
4
|
from ara_cli.list_filter import ListFilter, filter_list
|
|
6
5
|
from ara_cli.artefact_fuzzy_search import suggest_close_name_matches
|
|
6
|
+
from ara_cli.artefact_models.artefact_data_retrieval import (
|
|
7
|
+
artefact_content_retrieval,
|
|
8
|
+
artefact_path_retrieval,
|
|
9
|
+
artefact_tags_retrieval,
|
|
10
|
+
)
|
|
7
11
|
import os
|
|
8
12
|
|
|
9
13
|
|
|
@@ -11,43 +15,18 @@ class ArtefactLister:
|
|
|
11
15
|
def __init__(self, file_system=None):
|
|
12
16
|
self.file_system = file_system or os
|
|
13
17
|
|
|
14
|
-
@staticmethod
|
|
15
|
-
def artefact_content_retrieval(artefact: Artefact):
|
|
16
|
-
content = artefact.serialize()
|
|
17
|
-
return content
|
|
18
|
-
|
|
19
|
-
@staticmethod
|
|
20
|
-
def artefact_path_retrieval(artefact: Artefact):
|
|
21
|
-
return artefact.file_path
|
|
22
|
-
|
|
23
|
-
@staticmethod
|
|
24
|
-
def artefact_tags_retrieval(artefact: Artefact):
|
|
25
|
-
final_tags = []
|
|
26
|
-
|
|
27
|
-
if not artefact:
|
|
28
|
-
return []
|
|
29
|
-
|
|
30
|
-
final_tags.extend([f"user_{user}" for user in artefact.users])
|
|
31
|
-
final_tags.append(artefact.status)
|
|
32
|
-
final_tags.extend(artefact.tags)
|
|
33
|
-
|
|
34
|
-
return final_tags
|
|
35
|
-
|
|
36
18
|
def filter_artefacts(self, classified_files: list, list_filter: ListFilter):
|
|
37
19
|
filtered_list = filter_list(
|
|
38
20
|
list_to_filter=classified_files,
|
|
39
21
|
list_filter=list_filter,
|
|
40
|
-
content_retrieval_strategy=
|
|
41
|
-
file_path_retrieval=
|
|
42
|
-
tag_retrieval=
|
|
22
|
+
content_retrieval_strategy=artefact_content_retrieval,
|
|
23
|
+
file_path_retrieval=artefact_path_retrieval,
|
|
24
|
+
tag_retrieval=artefact_tags_retrieval,
|
|
43
25
|
)
|
|
44
26
|
return filtered_list
|
|
45
27
|
|
|
46
28
|
def list_files(
|
|
47
|
-
self,
|
|
48
|
-
tags=None,
|
|
49
|
-
navigate_to_target=False,
|
|
50
|
-
list_filter: ListFilter | None = None
|
|
29
|
+
self, tags=None, navigate_to_target=False, list_filter: ListFilter | None = None
|
|
51
30
|
):
|
|
52
31
|
artefact_list = ArtefactReader.read_artefacts(tags=tags)
|
|
53
32
|
artefact_list = self.filter_artefacts(artefact_list, list_filter)
|
|
@@ -60,20 +39,18 @@ class ArtefactLister:
|
|
|
60
39
|
file_classifier.print_classified_files(filtered_artefact_list)
|
|
61
40
|
|
|
62
41
|
def list_branch(
|
|
63
|
-
self,
|
|
64
|
-
classifier,
|
|
65
|
-
artefact_name,
|
|
66
|
-
list_filter: ListFilter | None = None
|
|
42
|
+
self, classifier, artefact_name, list_filter: ListFilter | None = None
|
|
67
43
|
):
|
|
68
44
|
file_classifier = FileClassifier(os)
|
|
69
45
|
classified_artefacts = file_classifier.classify_files()
|
|
70
46
|
artefact_info = classified_artefacts.get(classifier, [])
|
|
71
|
-
matching_artefact_info = [
|
|
47
|
+
matching_artefact_info = [
|
|
48
|
+
p for p in artefact_info if p["title"] == artefact_name
|
|
49
|
+
]
|
|
72
50
|
|
|
73
51
|
if not matching_artefact_info:
|
|
74
52
|
suggest_close_name_matches(
|
|
75
|
-
artefact_name,
|
|
76
|
-
[info["title"] for info in artefact_info]
|
|
53
|
+
artefact_name, [info["title"] for info in artefact_info]
|
|
77
54
|
)
|
|
78
55
|
|
|
79
56
|
artefacts_by_classifier = {classifier: []}
|
|
@@ -82,29 +59,28 @@ class ArtefactLister:
|
|
|
82
59
|
classifier=classifier,
|
|
83
60
|
artefacts_by_classifier=artefacts_by_classifier,
|
|
84
61
|
)
|
|
85
|
-
artefacts_by_classifier = self.filter_artefacts(
|
|
62
|
+
artefacts_by_classifier = self.filter_artefacts(
|
|
63
|
+
artefacts_by_classifier, list_filter
|
|
64
|
+
)
|
|
86
65
|
file_classifier.print_classified_files(artefacts_by_classifier)
|
|
87
66
|
|
|
88
67
|
def list_children(
|
|
89
|
-
self,
|
|
90
|
-
classifier,
|
|
91
|
-
artefact_name,
|
|
92
|
-
list_filter: ListFilter | None = None
|
|
68
|
+
self, classifier, artefact_name, list_filter: ListFilter | None = None
|
|
93
69
|
):
|
|
94
70
|
file_classifier = FileClassifier(os)
|
|
95
71
|
classified_artefacts = file_classifier.classify_files()
|
|
96
72
|
artefact_info = classified_artefacts.get(classifier, [])
|
|
97
|
-
matching_artefact_info = [
|
|
73
|
+
matching_artefact_info = [
|
|
74
|
+
p for p in artefact_info if p["title"] == artefact_name
|
|
75
|
+
]
|
|
98
76
|
|
|
99
77
|
if not matching_artefact_info:
|
|
100
78
|
suggest_close_name_matches(
|
|
101
|
-
artefact_name,
|
|
102
|
-
[info["title"] for info in artefact_info]
|
|
79
|
+
artefact_name, [info["title"] for info in artefact_info]
|
|
103
80
|
)
|
|
104
81
|
|
|
105
82
|
child_artefacts = ArtefactReader.find_children(
|
|
106
|
-
artefact_name=artefact_name,
|
|
107
|
-
classifier=classifier
|
|
83
|
+
artefact_name=artefact_name, classifier=classifier
|
|
108
84
|
)
|
|
109
85
|
|
|
110
86
|
child_artefacts = self.filter_artefacts(child_artefacts, list_filter)
|
|
@@ -112,25 +88,23 @@ class ArtefactLister:
|
|
|
112
88
|
file_classifier.print_classified_files(child_artefacts)
|
|
113
89
|
|
|
114
90
|
def list_data(
|
|
115
|
-
self,
|
|
116
|
-
classifier,
|
|
117
|
-
artefact_name,
|
|
118
|
-
list_filter: ListFilter | None = None
|
|
91
|
+
self, classifier, artefact_name, list_filter: ListFilter | None = None
|
|
119
92
|
):
|
|
120
93
|
file_classifier = FileClassifier(os)
|
|
121
94
|
classified_artefact_info = file_classifier.classify_files()
|
|
122
95
|
artefact_info_dict = classified_artefact_info.get(classifier, [])
|
|
123
96
|
|
|
124
|
-
matching_info = [
|
|
97
|
+
matching_info = [
|
|
98
|
+
info for info in artefact_info_dict if info["title"] == artefact_name
|
|
99
|
+
]
|
|
125
100
|
|
|
126
101
|
if not matching_info:
|
|
127
102
|
suggest_close_name_matches(
|
|
128
|
-
artefact_name,
|
|
129
|
-
[info["title"] for info in artefact_info_dict]
|
|
103
|
+
artefact_name, [info["title"] for info in artefact_info_dict]
|
|
130
104
|
)
|
|
131
105
|
return
|
|
132
106
|
|
|
133
107
|
artefact_info = matching_info[0]
|
|
134
|
-
data_dir = os.path.splitext(artefact_info["file_path"])[0] +
|
|
108
|
+
data_dir = os.path.splitext(artefact_info["file_path"])[0] + ".data"
|
|
135
109
|
if os.path.exists(data_dir):
|
|
136
110
|
list_files_in_directory(data_dir, list_filter)
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
from ara_cli.artefact_models.artefact_model import Artefact
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def artefact_content_retrieval(artefact: Artefact):
|
|
5
|
+
content = artefact.serialize()
|
|
6
|
+
return content
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def artefact_path_retrieval(artefact: Artefact):
|
|
10
|
+
return artefact.file_path
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def artefact_tags_retrieval(artefact: Artefact):
|
|
14
|
+
final_tags = []
|
|
15
|
+
|
|
16
|
+
if not artefact:
|
|
17
|
+
return []
|
|
18
|
+
|
|
19
|
+
final_tags.extend([f"user_{user}" for user in artefact.users])
|
|
20
|
+
final_tags.append(artefact.status)
|
|
21
|
+
final_tags.extend(artefact.tags)
|
|
22
|
+
|
|
23
|
+
return final_tags
|