ara-cli 0.1.13.3__py3-none-any.whl → 0.1.14.0__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 (61) hide show
  1. ara_cli/__init__.py +1 -1
  2. ara_cli/ara_command_action.py +162 -112
  3. ara_cli/ara_config.py +1 -1
  4. ara_cli/ara_subcommands/convert.py +66 -2
  5. ara_cli/ara_subcommands/prompt.py +266 -106
  6. ara_cli/artefact_autofix.py +2 -2
  7. ara_cli/artefact_converter.py +152 -53
  8. ara_cli/artefact_creator.py +41 -17
  9. ara_cli/artefact_lister.py +3 -3
  10. ara_cli/artefact_models/artefact_model.py +1 -1
  11. ara_cli/artefact_models/artefact_templates.py +0 -9
  12. ara_cli/artefact_models/feature_artefact_model.py +8 -8
  13. ara_cli/artefact_reader.py +62 -43
  14. ara_cli/artefact_scan.py +39 -17
  15. ara_cli/chat.py +23 -15
  16. ara_cli/children_contribution_updater.py +737 -0
  17. ara_cli/classifier.py +34 -0
  18. ara_cli/commands/load_command.py +4 -3
  19. ara_cli/commands/load_image_command.py +1 -1
  20. ara_cli/commands/read_command.py +23 -27
  21. ara_cli/completers.py +24 -0
  22. ara_cli/error_handler.py +26 -11
  23. ara_cli/file_loaders/document_reader.py +0 -178
  24. ara_cli/file_loaders/factories/__init__.py +0 -0
  25. ara_cli/file_loaders/factories/document_reader_factory.py +32 -0
  26. ara_cli/file_loaders/factories/file_loader_factory.py +27 -0
  27. ara_cli/file_loaders/file_loader.py +1 -30
  28. ara_cli/file_loaders/loaders/__init__.py +0 -0
  29. ara_cli/file_loaders/{document_file_loader.py → loaders/document_file_loader.py} +1 -1
  30. ara_cli/file_loaders/loaders/text_file_loader.py +47 -0
  31. ara_cli/file_loaders/readers/__init__.py +0 -0
  32. ara_cli/file_loaders/readers/docx_reader.py +49 -0
  33. ara_cli/file_loaders/readers/excel_reader.py +27 -0
  34. ara_cli/file_loaders/{markdown_reader.py → readers/markdown_reader.py} +1 -1
  35. ara_cli/file_loaders/readers/odt_reader.py +59 -0
  36. ara_cli/file_loaders/readers/pdf_reader.py +54 -0
  37. ara_cli/file_loaders/readers/pptx_reader.py +104 -0
  38. ara_cli/file_loaders/tools/__init__.py +0 -0
  39. ara_cli/output_suppressor.py +53 -0
  40. ara_cli/prompt_handler.py +123 -17
  41. ara_cli/tag_extractor.py +8 -7
  42. ara_cli/version.py +1 -1
  43. {ara_cli-0.1.13.3.dist-info → ara_cli-0.1.14.0.dist-info}/METADATA +18 -12
  44. {ara_cli-0.1.13.3.dist-info → ara_cli-0.1.14.0.dist-info}/RECORD +58 -45
  45. {ara_cli-0.1.13.3.dist-info → ara_cli-0.1.14.0.dist-info}/WHEEL +1 -1
  46. tests/test_artefact_converter.py +1 -46
  47. tests/test_artefact_lister.py +11 -8
  48. tests/test_chat.py +4 -4
  49. tests/test_chat_givens_images.py +1 -1
  50. tests/test_children_contribution_updater.py +98 -0
  51. tests/test_document_loader_office.py +267 -0
  52. tests/test_prompt_handler.py +416 -214
  53. tests/test_setup_default_chat_prompt_mode.py +198 -0
  54. tests/test_tag_extractor.py +95 -49
  55. ara_cli/file_loaders/document_readers.py +0 -233
  56. ara_cli/file_loaders/file_loaders.py +0 -123
  57. ara_cli/file_loaders/text_file_loader.py +0 -187
  58. /ara_cli/file_loaders/{binary_file_loader.py → loaders/binary_file_loader.py} +0 -0
  59. /ara_cli/file_loaders/{image_processor.py → tools/image_processor.py} +0 -0
  60. {ara_cli-0.1.13.3.dist-info → ara_cli-0.1.14.0.dist-info}/entry_points.txt +0 -0
  61. {ara_cli-0.1.13.3.dist-info → ara_cli-0.1.14.0.dist-info}/top_level.txt +0 -0
@@ -1,136 +1,296 @@
1
1
  import typer
2
2
  from typing import Optional, List
3
- from .common import ClassifierEnum, MockArgs, ClassifierArgument, ArtefactNameArgument, ChatNameArgument
3
+ from enum import Enum
4
+ from .common import (
5
+ MockArgs,
6
+ ChatNameArgument,
7
+ )
4
8
  from ara_cli.ara_command_action import prompt_action
9
+ from ara_cli.completers import DynamicCompleters
10
+ from ara_cli.classifier import Classifier
11
+ from ara_cli.error_handler import AraError, ErrorLevel, ErrorHandler
5
12
 
6
13
 
7
- def prompt_init(
8
- classifier: ClassifierEnum = ClassifierArgument("Classifier of the artefact"),
9
- parameter: str = ArtefactNameArgument("Name of artefact data directory")
10
- ):
11
- """Initialize a macro prompt."""
12
- args = MockArgs(
13
- classifier=classifier.value,
14
- parameter=parameter,
15
- steps="init"
16
- )
17
- prompt_action(args)
14
+ # Define PromptStep enum for subcommand validation
15
+ class PromptStep(str, Enum):
16
+ init = "init"
17
+ load = "load"
18
+ send = "send"
19
+ load_and_send = "load-and-send"
20
+ extract = "extract"
21
+ update = "update"
22
+ chat = "chat"
23
+ init_rag = "init-rag"
18
24
 
19
25
 
20
- def prompt_load(
21
- classifier: ClassifierEnum = ClassifierArgument("Classifier of the artefact"),
22
- parameter: str = ArtefactNameArgument("Name of artefact data directory")
23
- ):
24
- """Load selected templates."""
25
- args = MockArgs(
26
- classifier=classifier.value,
27
- parameter=parameter,
28
- steps="load"
29
- )
30
- prompt_action(args)
26
+ # Valid step values for old format detection
27
+ VALID_STEPS = [step.value for step in PromptStep]
31
28
 
32
29
 
33
- def prompt_send(
34
- classifier: ClassifierEnum = ClassifierArgument("Classifier of the artefact"),
35
- parameter: str = ArtefactNameArgument("Name of artefact data directory")
36
- ):
37
- """Send configured prompt to LLM."""
38
- args = MockArgs(
39
- classifier=classifier.value,
40
- parameter=parameter,
41
- steps="send"
30
+ def PromptStepArgument(help_text: str):
31
+ """Create a prompt step argument with autocompletion."""
32
+ return typer.Argument(
33
+ ...,
34
+ help=help_text,
35
+ autocompletion=DynamicCompleters.create_prompt_step_completer(),
42
36
  )
43
- prompt_action(args)
44
37
 
45
38
 
46
- def prompt_load_and_send(
47
- classifier: ClassifierEnum = ClassifierArgument("Classifier of the artefact"),
48
- parameter: str = ArtefactNameArgument("Name of artefact data directory")
49
- ):
50
- """Load templates and send prompt to LLM."""
51
- args = MockArgs(
52
- classifier=classifier.value,
53
- parameter=parameter,
54
- steps="load-and-send"
39
+ def _warn_unused_option(option_name: str, step: str, valid_for: str):
40
+ """Print warning for unused options."""
41
+ message = (
42
+ f"'{option_name}' option is ignored for '{step}' command. "
43
+ f"It is only valid for '{valid_for}'."
44
+ )
45
+ error_handler = ErrorHandler()
46
+ error_handler.report_error(
47
+ AraError(message, error_code=0, level=ErrorLevel.INFO)
55
48
  )
56
- prompt_action(args)
57
49
 
58
50
 
59
- def prompt_extract(
60
- classifier: ClassifierEnum = ClassifierArgument("Classifier of the artefact"),
61
- parameter: str = ArtefactNameArgument("Name of artefact data directory"),
62
- write: bool = typer.Option(False, "-w", "--write", help="Overwrite existing files without using LLM for merging")
63
- ):
64
- """Extract LLM response and save to disk."""
65
- args = MockArgs(
66
- classifier=classifier.value,
67
- parameter=parameter,
68
- steps="extract",
69
- write=write
51
+ def _warn_deprecated_format(old_cmd: str, new_cmd: str):
52
+ """Print deprecation warning for old command format."""
53
+ message = (
54
+ f"DEPRECATION: The command format has changed.\n"
55
+ f" Old: {old_cmd}\n"
56
+ f" New: {new_cmd}\n"
57
+ f" Please update your scripts. Old format will be removed in 0.1.14.0 version."
58
+ )
59
+ error_handler = ErrorHandler()
60
+ error_handler.report_error(
61
+ AraError(message, error_code=0, level=ErrorLevel.WARNING)
70
62
  )
71
- prompt_action(args)
72
63
 
73
64
 
74
- def prompt_update(
75
- classifier: ClassifierEnum = ClassifierArgument("Classifier of the artefact"),
76
- parameter: str = ArtefactNameArgument("Name of artefact data directory")
77
- ):
78
- """Update artefact config prompt files."""
79
- args = MockArgs(
80
- classifier=classifier.value,
81
- parameter=parameter,
82
- steps="update"
83
- )
84
- prompt_action(args)
65
+ def _is_old_format(first_arg: str) -> bool:
66
+ """Check if the first argument is a step (old format) instead of classifier (new format)."""
67
+ return first_arg in VALID_STEPS
85
68
 
86
69
 
87
- def prompt_chat(
88
- classifier: ClassifierEnum = ClassifierArgument("Classifier of the artefact"),
89
- parameter: str = ArtefactNameArgument("Name of artefact data directory"),
90
- chat_name: Optional[str] = ChatNameArgument("Optional name for a specific chat", None),
91
- reset: Optional[bool] = typer.Option(None, "-r", "--reset/--no-reset", help="Reset the chat file if it exists"),
92
- output_mode: bool = typer.Option(False, "--out", help="Output the contents of the chat file instead of entering interactive chat mode"),
93
- append: Optional[List[str]] = typer.Option(None, "--append", help="Append strings to the chat file"),
94
- restricted: Optional[bool] = typer.Option(None, "--restricted/--no-restricted", help="Start with a limited set of commands")
95
- ):
96
- """Start chat mode for the artefact."""
97
- args = MockArgs(
98
- classifier=classifier.value,
99
- parameter=parameter,
100
- steps="chat",
101
- chat_name=chat_name,
102
- reset=reset,
103
- output_mode=output_mode,
104
- append=append,
105
- restricted=restricted
70
+ def _validate_classifier(classifier: str) -> bool:
71
+ """Validate that the classifier is valid."""
72
+ return Classifier.is_valid_classifier(classifier)
73
+
74
+
75
+ def _validate_step(step: str) -> bool:
76
+ """Validate that the step is valid."""
77
+ return step in VALID_STEPS
78
+
79
+
80
+ def _handle_old_format(
81
+ step_value: str, classifier: str, artefact_name: Optional[str]
82
+ ) -> tuple[str, str, str]:
83
+ """Handle old format: ara prompt <step> <classifier> <artefact>."""
84
+ if artefact_name is None:
85
+ typer.echo(
86
+ "Error: Missing artefact name. "
87
+ "Usage: ara prompt <step> <classifier> <artefact_name>",
88
+ err=True,
89
+ )
90
+ raise typer.Exit(1)
91
+
92
+ if not _validate_classifier(classifier):
93
+ typer.echo(
94
+ f"Error: Invalid classifier '{classifier}'. "
95
+ f"Valid classifiers: {', '.join(Classifier.ordered_classifiers())}",
96
+ err=True,
97
+ )
98
+ raise typer.Exit(1)
99
+
100
+ # Show deprecation warning
101
+ old_cmd = f"ara prompt {step_value} {classifier} {artefact_name}"
102
+ new_cmd = f"ara prompt {classifier} {artefact_name} {step_value}"
103
+ _warn_deprecated_format(old_cmd, new_cmd)
104
+
105
+ return classifier, artefact_name, step_value
106
+
107
+
108
+ def _handle_new_format(
109
+ classifier: str, artefact_name: str, step_value: Optional[str]
110
+ ) -> tuple[str, str, str]:
111
+ """Handle new format: ara prompt <classifier> <artefact> <step>."""
112
+ if not _validate_classifier(classifier):
113
+ typer.echo(
114
+ f"Error: Invalid classifier '{classifier}'. "
115
+ f"Valid classifiers: {', '.join(Classifier.ordered_classifiers())}",
116
+ err=True,
117
+ )
118
+ raise typer.Exit(1)
119
+
120
+ if step_value is None:
121
+ typer.echo(
122
+ "Error: Missing step. "
123
+ f"Usage: ara prompt <classifier> <artefact_name> <step>\n"
124
+ f"Valid steps: {', '.join(VALID_STEPS)}",
125
+ err=True,
126
+ )
127
+ raise typer.Exit(1)
128
+
129
+ if not _validate_step(step_value):
130
+ typer.echo(
131
+ f"Error: Invalid step '{step_value}'. "
132
+ f"Valid steps: {', '.join(VALID_STEPS)}",
133
+ err=True,
134
+ )
135
+ raise typer.Exit(1)
136
+
137
+ return classifier, artefact_name, step_value
138
+
139
+
140
+ def _validate_step_options(
141
+ step_value: str,
142
+ write: bool,
143
+ reset: Optional[bool],
144
+ output_mode: bool,
145
+ append: Optional[List[str]],
146
+ restricted: Optional[bool],
147
+ chat_name: Optional[str],
148
+ ) -> None:
149
+ """Validate that options are used with correct steps."""
150
+ if step_value != "extract" and write:
151
+ _warn_unused_option("--write", step_value, "extract")
152
+
153
+ if step_value != "chat":
154
+ chat_only_options = [
155
+ (reset is not None, "--reset/--no-reset"),
156
+ (output_mode, "--out"),
157
+ (append is not None, "--append"),
158
+ (restricted is not None, "--restricted/--no-restricted"),
159
+ (chat_name is not None, "chat_name"),
160
+ ]
161
+ for is_set, option_name in chat_only_options:
162
+ if is_set:
163
+ _warn_unused_option(option_name, step_value, "chat")
164
+
165
+
166
+ def _build_prompt_args(
167
+ classifier: str,
168
+ artefact_name: str,
169
+ step_value: str,
170
+ write: bool,
171
+ chat_name: Optional[str],
172
+ reset: Optional[bool],
173
+ output_mode: bool,
174
+ append: Optional[List[str]],
175
+ restricted: Optional[bool],
176
+ ) -> MockArgs:
177
+ """Build MockArgs for prompt_action."""
178
+ is_extract = step_value == "extract"
179
+ is_chat = step_value == "chat"
180
+
181
+ return MockArgs(
182
+ classifier=classifier,
183
+ parameter=artefact_name,
184
+ steps=step_value,
185
+ write=write if is_extract else False,
186
+ chat_name=chat_name if is_chat else None,
187
+ reset=reset if is_chat else None,
188
+ output_mode=output_mode if is_chat else False,
189
+ append=append if is_chat else None,
190
+ restricted=restricted if is_chat else None,
106
191
  )
107
- prompt_action(args)
108
192
 
109
193
 
110
- def prompt_init_rag(
111
- classifier: ClassifierEnum = ClassifierArgument("Classifier of the artefact"),
112
- parameter: str = ArtefactNameArgument("Name of artefact data directory")
194
+ def prompt_main(
195
+ classifier: str = typer.Argument(
196
+ ...,
197
+ help="Classifier of the artefact (e.g., feature, task, userstory)",
198
+ autocompletion=DynamicCompleters.create_classifier_completer(),
199
+ ),
200
+ artefact_name: str = typer.Argument(
201
+ ...,
202
+ help="Name of the artefact",
203
+ autocompletion=DynamicCompleters.create_artefact_name_completer(),
204
+ ),
205
+ step: Optional[str] = typer.Argument(
206
+ None,
207
+ help="Action: init, load, send, load-and-send, extract, update, chat, init-rag",
208
+ autocompletion=DynamicCompleters.create_prompt_step_completer(),
209
+ ),
210
+ chat_name: Optional[str] = ChatNameArgument(
211
+ "[chat only] Optional name for a specific chat session", None
212
+ ),
213
+ # Extract-specific option
214
+ write: bool = typer.Option(
215
+ False,
216
+ "-w",
217
+ "--write",
218
+ help="[extract only] Overwrite existing files without using LLM for merging",
219
+ ),
220
+ # Chat-specific options
221
+ reset: Optional[bool] = typer.Option(
222
+ None,
223
+ "-r",
224
+ "--reset/--no-reset",
225
+ help="[chat only] Reset the chat file if it exists",
226
+ ),
227
+ output_mode: bool = typer.Option(
228
+ False,
229
+ "--out",
230
+ help="[chat only] Output the contents of the chat file instead of entering interactive chat mode",
231
+ ),
232
+ append: Optional[List[str]] = typer.Option(
233
+ None, "--append", help="[chat only] Append strings to the chat file"
234
+ ),
235
+ restricted: Optional[bool] = typer.Option(
236
+ None,
237
+ "--restricted/--no-restricted",
238
+ help="[chat only] Start with a limited set of commands",
239
+ ),
113
240
  ):
114
- """Initialize RAG prompt."""
115
- args = MockArgs(
116
- classifier=classifier.value,
117
- parameter=parameter,
118
- steps="init-rag"
241
+ """
242
+ Prompt interaction mode for artefacts.
243
+
244
+ Usage:
245
+ ara prompt <classifier> <artefact_name> <step> [chat_name] [options]
246
+
247
+ Steps:
248
+ init Initialize prompt templates
249
+ load Load selected templates
250
+ send Send configured prompt to LLM
251
+ load-and-send Load templates and send to LLM
252
+ extract Extract LLM response and save
253
+ update Update artefact config prompt files
254
+ chat Start interactive chat mode
255
+ init-rag Initialize RAG prompt
256
+
257
+ Examples:
258
+ ara prompt feature my_feature init
259
+ ara prompt task my_task chat --reset
260
+ ara prompt userstory my_story extract -w
261
+ ara prompt feature my_feature chat my_session --out
262
+ """
263
+ # Detect and handle old vs new format (backward compatibility)
264
+ if _is_old_format(classifier):
265
+ classifier_val, artefact_name_val, step_value = _handle_old_format(
266
+ classifier, artefact_name, step
267
+ )
268
+ else:
269
+ classifier_val, artefact_name_val, step_value = _handle_new_format(
270
+ classifier, artefact_name, step
271
+ )
272
+
273
+ # Validate options match the step
274
+ _validate_step_options(
275
+ step_value, write, reset, output_mode, append, restricted, chat_name
276
+ )
277
+
278
+ # Build args and execute
279
+ args = _build_prompt_args(
280
+ classifier_val,
281
+ artefact_name_val,
282
+ step_value,
283
+ write,
284
+ chat_name,
285
+ reset,
286
+ output_mode,
287
+ append,
288
+ restricted,
119
289
  )
120
290
  prompt_action(args)
121
291
 
122
292
 
123
293
  def register(parent: typer.Typer):
124
- prompt_app = typer.Typer(
125
- help="Base command for prompt interaction mode",
126
- add_completion=False # Disable completion on subcommand
294
+ parent.command(name="prompt", help="Prompt interaction mode for artefacts")(
295
+ prompt_main
127
296
  )
128
- prompt_app.command("init")(prompt_init)
129
- prompt_app.command("load")(prompt_load)
130
- prompt_app.command("send")(prompt_send)
131
- prompt_app.command("load-and-send")(prompt_load_and_send)
132
- prompt_app.command("extract")(prompt_extract)
133
- prompt_app.command("update")(prompt_update)
134
- prompt_app.command("chat")(prompt_chat)
135
- prompt_app.command("init-rag")(prompt_init_rag)
136
- parent.add_typer(prompt_app, name="prompt")
@@ -256,7 +256,7 @@ def _update_rule(
256
256
  """Updates the rule in the contribution if a close match is found."""
257
257
  rule = artefact.contribution.rule
258
258
 
259
- content, artefact_data = ArtefactReader.read_artefact_data(
259
+ content, artefact_data = ArtefactReader().read_artefact_data(
260
260
  artefact_name=name,
261
261
  classifier=classifier,
262
262
  classified_file_info=classified_file_info,
@@ -365,7 +365,7 @@ def set_closest_contribution(
365
365
  if not rule:
366
366
  return artefact, True
367
367
 
368
- content, artefact = ArtefactReader.read_artefact_data(
368
+ content, artefact = ArtefactReader().read_artefact_data(
369
369
  artefact_name=name,
370
370
  classifier=classifier,
371
371
  classified_file_info=classified_file_info,
@@ -1,5 +1,6 @@
1
1
  import os
2
2
  import logging
3
+ from typing import Optional
3
4
  from ara_cli.prompt_handler import LLMSingleton
4
5
  from langfuse.api.resources.commons.errors import Error as LangfuseError, NotFoundError
5
6
  from ara_cli.classifier import Classifier
@@ -8,6 +9,8 @@ from ara_cli.artefact_creator import ArtefactCreator
8
9
  from ara_cli.error_handler import AraError
9
10
  from ara_cli.directory_navigator import DirectoryNavigator
10
11
  from ara_cli.artefact_deleter import ArtefactDeleter
12
+ from ara_cli.children_contribution_updater import ChildrenContributionUpdater
13
+ from ara_cli.artefact_models.artefact_load import artefact_from_content
11
14
 
12
15
 
13
16
  class AraArtefactConverter:
@@ -15,6 +18,7 @@ class AraArtefactConverter:
15
18
  self.file_system = file_system or os
16
19
  self.reader = ArtefactReader()
17
20
  self.creator = ArtefactCreator(self.file_system)
21
+ self.children_updater = ChildrenContributionUpdater(self.file_system)
18
22
 
19
23
  def convert(
20
24
  self,
@@ -23,61 +27,147 @@ class AraArtefactConverter:
23
27
  new_classifier: str,
24
28
  merge: bool = False,
25
29
  override: bool = False,
30
+ force: bool = False,
31
+ children_action: Optional[str] = None,
32
+ new_parent_classifier: Optional[str] = None,
33
+ new_parent_name: Optional[str] = None,
34
+ json_output: bool = False,
26
35
  ):
27
- try:
28
- self._validate_classifiers(old_classifier, new_classifier)
36
+ self._validate_classifiers(old_classifier, new_classifier)
37
+
38
+ content = self._read_and_validate_source(artefact_name, old_classifier)
39
+
40
+ # Handle children contributions BEFORE conversion
41
+ if not self._handle_children_if_needed(
42
+ artefact_name,
43
+ old_classifier,
44
+ new_classifier,
45
+ force,
46
+ children_action,
47
+ new_parent_classifier,
48
+ new_parent_name,
49
+ json_output,
50
+ ):
51
+ return # User cancelled
52
+
53
+ target_content_existing = self._resolve_target_content(
54
+ artefact_name, new_classifier, merge, override
55
+ )
29
56
 
30
- content, artefact_info = self.reader.read_artefact_data(
31
- artefact_name, old_classifier
32
- )
33
- if not content or not artefact_info:
34
- raise AraError(
35
- f"Artefact '{artefact_name}' of type '{old_classifier}' not found"
36
- )
57
+ self._execute_conversion(
58
+ old_classifier,
59
+ new_classifier,
60
+ artefact_name,
61
+ content,
62
+ target_content_existing,
63
+ merge,
64
+ override,
65
+ )
66
+
67
+ self._cleanup_after_conversion(old_classifier, new_classifier, artefact_name)
37
68
 
38
- target_content_existing = self._resolve_target_content(
39
- artefact_name, new_classifier, merge, override
69
+ def _read_and_validate_source(self, artefact_name: str, classifier: str) -> str:
70
+ """Read source artefact and validate it can be parsed."""
71
+ content, artefact_info = self.reader.read_artefact_data(
72
+ artefact_name, classifier
73
+ )
74
+ if not content or not artefact_info:
75
+ raise AraError(
76
+ f"Artefact '{artefact_name}' of type '{classifier}' not found"
40
77
  )
41
78
 
42
- target_class = self._get_target_class(new_classifier)
79
+ self._validate_artefact_parseable(content, artefact_name, is_source=True)
80
+ return content
43
81
 
44
- prompt = self._get_prompt(
45
- old_classifier=old_classifier,
46
- new_classifier=new_classifier,
47
- artefact_name=artefact_name,
48
- content=content,
49
- target_content_existing=target_content_existing,
50
- merge=merge,
82
+ def _validate_artefact_parseable(
83
+ self, content: str, artefact_name: str, is_source: bool = True
84
+ ) -> None:
85
+ """Validate that artefact content can be parsed."""
86
+ artefact_type = "input" if is_source else "target"
87
+ try:
88
+ artefact = artefact_from_content(content)
89
+ if artefact is None:
90
+ raise AraError(
91
+ f'Invalid {artefact_type} artefact: {artefact_name}. Run "ara scan" and "ara autofix" first.'
92
+ )
93
+ except AraError:
94
+ raise
95
+ except Exception:
96
+ raise AraError(
97
+ f'Invalid {artefact_type} artefact: {artefact_name}. Run "ara scan" and "ara autofix" first.'
51
98
  )
52
99
 
53
- print(
54
- f"{'Merging' if merge and target_content_existing else 'Converting'} '{artefact_name}' from {old_classifier} to {new_classifier}..."
55
- )
100
+ def _handle_children_if_needed(
101
+ self,
102
+ artefact_name: str,
103
+ old_classifier: str,
104
+ new_classifier: str,
105
+ force: bool,
106
+ children_action: Optional[str],
107
+ new_parent_classifier: Optional[str],
108
+ new_parent_name: Optional[str],
109
+ json_output: bool,
110
+ ) -> bool:
111
+ """Handle children contributions if classifier is changing. Returns True to continue."""
112
+ if old_classifier == new_classifier:
113
+ return True
114
+ return self.children_updater.update_children_contributions(
115
+ artefact_name,
116
+ old_classifier,
117
+ new_classifier,
118
+ force,
119
+ children_action=children_action,
120
+ new_parent_classifier=new_parent_classifier,
121
+ new_parent_name=new_parent_name,
122
+ json_output=json_output,
123
+ )
56
124
 
57
- converted_artefact = self._run_conversion_agent(prompt, target_class)
58
- artefact_content = converted_artefact.serialize()
125
+ def _execute_conversion(
126
+ self,
127
+ old_classifier: str,
128
+ new_classifier: str,
129
+ artefact_name: str,
130
+ content: str,
131
+ target_content_existing: Optional[str],
132
+ merge: bool,
133
+ override: bool,
134
+ ) -> None:
135
+ """Execute the LLM conversion and write the result."""
136
+ target_class = self._get_target_class(new_classifier)
137
+
138
+ prompt = self._get_prompt(
139
+ old_classifier=old_classifier,
140
+ new_classifier=new_classifier,
141
+ artefact_name=artefact_name,
142
+ content=content,
143
+ target_content_existing=target_content_existing,
144
+ merge=merge,
145
+ )
59
146
 
60
- self._write_artefact(
61
- new_classifier,
62
- artefact_name,
63
- artefact_content,
64
- merge=merge,
65
- override=override,
66
- )
147
+ action = "Merging" if merge and target_content_existing else "Converting"
148
+ print(
149
+ f"\n{action} '{artefact_name}' from {old_classifier} to {new_classifier}..."
150
+ )
67
151
 
68
- if old_classifier != new_classifier:
69
- self._move_data_folder_content(
70
- old_classifier, new_classifier, artefact_name
71
- )
72
- deleter = ArtefactDeleter(self.file_system)
73
- deleter.delete(artefact_name, old_classifier, force=True)
152
+ converted_artefact = self._run_conversion_agent(prompt, target_class)
153
+ self._write_artefact(
154
+ new_classifier,
155
+ artefact_name,
156
+ converted_artefact.serialize(),
157
+ merge,
158
+ override,
159
+ )
74
160
 
75
- except ValueError as e:
76
- raise e
77
- except AraError as e:
78
- raise e
79
- except Exception as e:
80
- raise e
161
+ def _cleanup_after_conversion(
162
+ self, old_classifier: str, new_classifier: str, artefact_name: str
163
+ ) -> None:
164
+ """Move data folder and delete old artefact after successful conversion."""
165
+ if old_classifier == new_classifier:
166
+ return
167
+ self._move_data_folder_content(old_classifier, new_classifier, artefact_name)
168
+ ArtefactDeleter(self.file_system).delete(
169
+ artefact_name, old_classifier, force=True
170
+ )
81
171
 
82
172
  def _validate_classifiers(self, old_classifier: str, new_classifier: str):
83
173
  if not Classifier.is_valid_classifier(old_classifier):
@@ -122,20 +212,29 @@ class AraArtefactConverter:
122
212
  def _resolve_target_content(
123
213
  self, artefact_name: str, new_classifier: str, merge: bool, override: bool
124
214
  ):
125
- target_content_existing = None
126
- if not merge and not override:
127
- _, new_artefact_info = self.reader.read_artefact_data(
215
+ if merge:
216
+ target_content_existing, _ = self.reader.read_artefact_data(
128
217
  artefact_name, new_classifier
129
218
  )
130
- if new_artefact_info:
131
- raise ValueError(
132
- f"Found already exiting {new_classifier} {artefact_name}. Rerun the command with --override or --merge."
219
+ return target_content_existing
220
+
221
+ if override:
222
+ return None
223
+
224
+ # Check if target already exists
225
+ target_content, new_artefact_info = self.reader.read_artefact_data(
226
+ artefact_name, new_classifier
227
+ )
228
+ if new_artefact_info:
229
+ # Only validate if content is not None (artefact file exists and is readable)
230
+ if target_content is not None:
231
+ self._validate_artefact_parseable(
232
+ target_content, artefact_name, is_source=False
133
233
  )
134
- elif merge:
135
- target_content_existing, _ = self.reader.read_artefact_data(
136
- artefact_name, new_classifier
234
+ raise ValueError(
235
+ f"Found already existing {new_classifier} {artefact_name}. Rerun the command with --override or --merge."
137
236
  )
138
- return target_content_existing
237
+ return None
139
238
 
140
239
  def _get_target_class(self, new_classifier: str):
141
240
  from ara_cli.artefact_models.artefact_mapping import artefact_type_mapping