ara-cli 0.1.10.0__py3-none-any.whl → 0.1.13.3__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 (140) hide show
  1. ara_cli/__init__.py +51 -6
  2. ara_cli/__main__.py +270 -103
  3. ara_cli/ara_command_action.py +106 -63
  4. ara_cli/ara_config.py +187 -128
  5. ara_cli/ara_subcommands/__init__.py +0 -0
  6. ara_cli/ara_subcommands/autofix.py +26 -0
  7. ara_cli/ara_subcommands/chat.py +27 -0
  8. ara_cli/ara_subcommands/classifier_directory.py +16 -0
  9. ara_cli/ara_subcommands/common.py +100 -0
  10. ara_cli/ara_subcommands/config.py +221 -0
  11. ara_cli/ara_subcommands/convert.py +43 -0
  12. ara_cli/ara_subcommands/create.py +75 -0
  13. ara_cli/ara_subcommands/delete.py +22 -0
  14. ara_cli/ara_subcommands/extract.py +22 -0
  15. ara_cli/ara_subcommands/fetch.py +41 -0
  16. ara_cli/ara_subcommands/fetch_agents.py +22 -0
  17. ara_cli/ara_subcommands/fetch_scripts.py +19 -0
  18. ara_cli/ara_subcommands/fetch_templates.py +19 -0
  19. ara_cli/ara_subcommands/list.py +139 -0
  20. ara_cli/ara_subcommands/list_tags.py +25 -0
  21. ara_cli/ara_subcommands/load.py +48 -0
  22. ara_cli/ara_subcommands/prompt.py +136 -0
  23. ara_cli/ara_subcommands/read.py +47 -0
  24. ara_cli/ara_subcommands/read_status.py +20 -0
  25. ara_cli/ara_subcommands/read_user.py +20 -0
  26. ara_cli/ara_subcommands/reconnect.py +27 -0
  27. ara_cli/ara_subcommands/rename.py +22 -0
  28. ara_cli/ara_subcommands/scan.py +14 -0
  29. ara_cli/ara_subcommands/set_status.py +22 -0
  30. ara_cli/ara_subcommands/set_user.py +22 -0
  31. ara_cli/ara_subcommands/template.py +16 -0
  32. ara_cli/artefact_autofix.py +154 -63
  33. ara_cli/artefact_converter.py +256 -0
  34. ara_cli/artefact_models/artefact_model.py +106 -25
  35. ara_cli/artefact_models/artefact_templates.py +20 -10
  36. ara_cli/artefact_models/epic_artefact_model.py +11 -2
  37. ara_cli/artefact_models/feature_artefact_model.py +31 -1
  38. ara_cli/artefact_models/userstory_artefact_model.py +15 -3
  39. ara_cli/artefact_scan.py +2 -2
  40. ara_cli/chat.py +283 -80
  41. ara_cli/chat_agent/__init__.py +0 -0
  42. ara_cli/chat_agent/agent_process_manager.py +155 -0
  43. ara_cli/chat_script_runner/__init__.py +0 -0
  44. ara_cli/chat_script_runner/script_completer.py +23 -0
  45. ara_cli/chat_script_runner/script_finder.py +41 -0
  46. ara_cli/chat_script_runner/script_lister.py +36 -0
  47. ara_cli/chat_script_runner/script_runner.py +36 -0
  48. ara_cli/chat_web_search/__init__.py +0 -0
  49. ara_cli/chat_web_search/web_search.py +263 -0
  50. ara_cli/commands/agent_run_command.py +98 -0
  51. ara_cli/commands/fetch_agents_command.py +106 -0
  52. ara_cli/commands/fetch_scripts_command.py +43 -0
  53. ara_cli/commands/fetch_templates_command.py +39 -0
  54. ara_cli/commands/fetch_templates_commands.py +39 -0
  55. ara_cli/commands/list_agents_command.py +39 -0
  56. ara_cli/commands/read_command.py +17 -4
  57. ara_cli/completers.py +180 -0
  58. ara_cli/constants.py +2 -0
  59. ara_cli/directory_navigator.py +37 -4
  60. ara_cli/file_loaders/text_file_loader.py +2 -2
  61. ara_cli/global_file_lister.py +5 -15
  62. ara_cli/llm_utils.py +58 -0
  63. ara_cli/prompt_chat.py +20 -4
  64. ara_cli/prompt_extractor.py +199 -76
  65. ara_cli/prompt_handler.py +160 -59
  66. ara_cli/tag_extractor.py +38 -18
  67. ara_cli/template_loader.py +3 -2
  68. ara_cli/template_manager.py +52 -21
  69. ara_cli/templates/global-scripts/hello_global.py +1 -0
  70. ara_cli/templates/prompt-modules/commands/add_scenarios_for_new_behaviour.feature_creation_agent.commands.md +1 -0
  71. ara_cli/templates/prompt-modules/commands/align_feature_with_implementation_changes.interview_agent.commands.md +1 -0
  72. ara_cli/templates/prompt-modules/commands/analyze_codebase_and_plan_tasks.interview_agent.commands.md +1 -0
  73. ara_cli/templates/prompt-modules/commands/choose_best_parent_artefact.interview_agent.commands.md +1 -0
  74. ara_cli/templates/prompt-modules/commands/create_tasks_from_artefact_content.interview_agent.commands.md +1 -0
  75. ara_cli/templates/prompt-modules/commands/create_tests_for_uncovered_modules.test_generation_agent.commands.md +1 -0
  76. ara_cli/templates/prompt-modules/commands/derive_features_from_video_description.feature_creation_agent.commands.md +1 -0
  77. ara_cli/templates/prompt-modules/commands/describe_agent_capabilities.agent.commands.md +1 -0
  78. ara_cli/templates/prompt-modules/commands/empty.commands.md +2 -12
  79. ara_cli/templates/prompt-modules/commands/execute_scoped_todos_in_task.interview_agent.commands.md +1 -0
  80. ara_cli/templates/prompt-modules/commands/explain_single_file_purpose.interview_agent.commands.md +1 -0
  81. ara_cli/templates/prompt-modules/commands/extract_file_information_bullets.interview_agent.commands.md +1 -0
  82. ara_cli/templates/prompt-modules/commands/extract_general.commands.md +12 -0
  83. ara_cli/templates/prompt-modules/commands/extract_markdown.commands.md +11 -0
  84. ara_cli/templates/prompt-modules/commands/extract_python.commands.md +13 -0
  85. ara_cli/templates/prompt-modules/commands/feature_add_or_modifiy_specified_behavior.commands.md +36 -0
  86. ara_cli/templates/prompt-modules/commands/feature_generate_initial_specified_bevahior.commands.md +53 -0
  87. ara_cli/templates/prompt-modules/commands/fix_failing_behave_step_definitions.interview_agent.commands.md +1 -0
  88. ara_cli/templates/prompt-modules/commands/fix_failing_pytest_tests.interview_agent.commands.md +1 -0
  89. ara_cli/templates/prompt-modules/commands/general_instruction_policy.commands.md +47 -0
  90. ara_cli/templates/prompt-modules/commands/generate_and_fix_pytest_tests.test_generation_agent.commands.md +1 -0
  91. ara_cli/templates/prompt-modules/commands/prompt_template_tech_stack_transformer.commands.md +95 -0
  92. ara_cli/templates/prompt-modules/commands/python_bug_fixing_code.commands.md +34 -0
  93. ara_cli/templates/prompt-modules/commands/python_generate_code.commands.md +27 -0
  94. ara_cli/templates/prompt-modules/commands/python_refactoring_code.commands.md +39 -0
  95. ara_cli/templates/prompt-modules/commands/python_step_definitions_generation_and_fixing.commands.md +40 -0
  96. ara_cli/templates/prompt-modules/commands/python_unittest_generation_and_fixing.commands.md +48 -0
  97. ara_cli/templates/prompt-modules/commands/suggest_next_story_child_tasks.interview_agent.commands.md +1 -0
  98. ara_cli/templates/prompt-modules/commands/summarize_or_transcribe_media.interview_agent.commands.md +1 -0
  99. ara_cli/templates/prompt-modules/commands/update_feature_to_match_implementation.feature_creation_agent.commands.md +1 -0
  100. ara_cli/templates/prompt-modules/commands/update_user_story_with_requirements.interview_agent.commands.md +1 -0
  101. ara_cli/version.py +1 -1
  102. {ara_cli-0.1.10.0.dist-info → ara_cli-0.1.13.3.dist-info}/METADATA +34 -1
  103. {ara_cli-0.1.10.0.dist-info → ara_cli-0.1.13.3.dist-info}/RECORD +123 -54
  104. tests/test_ara_command_action.py +31 -19
  105. tests/test_ara_config.py +177 -90
  106. tests/test_artefact_autofix.py +170 -97
  107. tests/test_artefact_autofix_integration.py +495 -0
  108. tests/test_artefact_converter.py +357 -0
  109. tests/test_artefact_extraction.py +564 -0
  110. tests/test_artefact_scan.py +1 -1
  111. tests/test_chat.py +162 -126
  112. tests/test_chat_givens_images.py +603 -0
  113. tests/test_chat_script_runner.py +454 -0
  114. tests/test_global_file_lister.py +1 -1
  115. tests/test_llm_utils.py +164 -0
  116. tests/test_prompt_chat.py +343 -0
  117. tests/test_prompt_extractor.py +683 -0
  118. tests/test_prompt_handler.py +12 -4
  119. tests/test_tag_extractor.py +19 -13
  120. tests/test_web_search.py +467 -0
  121. ara_cli/ara_command_parser.py +0 -605
  122. ara_cli/templates/prompt-modules/blueprints/complete_pytest_unittest.blueprint.md +0 -27
  123. ara_cli/templates/prompt-modules/blueprints/task_todo_list_implement_feature_BDD_way.blueprint.md +0 -30
  124. ara_cli/templates/prompt-modules/commands/artefact_classification.commands.md +0 -9
  125. ara_cli/templates/prompt-modules/commands/artefact_extension.commands.md +0 -17
  126. ara_cli/templates/prompt-modules/commands/artefact_formulation.commands.md +0 -14
  127. ara_cli/templates/prompt-modules/commands/behave_step_generation.commands.md +0 -102
  128. ara_cli/templates/prompt-modules/commands/code_generation_complex.commands.md +0 -20
  129. ara_cli/templates/prompt-modules/commands/code_generation_simple.commands.md +0 -13
  130. ara_cli/templates/prompt-modules/commands/error_fixing.commands.md +0 -20
  131. ara_cli/templates/prompt-modules/commands/feature_file_update.commands.md +0 -18
  132. ara_cli/templates/prompt-modules/commands/feature_formulation.commands.md +0 -43
  133. ara_cli/templates/prompt-modules/commands/js_code_generation_simple.commands.md +0 -13
  134. ara_cli/templates/prompt-modules/commands/refactoring.commands.md +0 -15
  135. ara_cli/templates/prompt-modules/commands/refactoring_analysis.commands.md +0 -9
  136. ara_cli/templates/prompt-modules/commands/reverse_engineer_feature_file.commands.md +0 -15
  137. ara_cli/templates/prompt-modules/commands/reverse_engineer_program_flow.commands.md +0 -19
  138. {ara_cli-0.1.10.0.dist-info → ara_cli-0.1.13.3.dist-info}/WHEEL +0 -0
  139. {ara_cli-0.1.10.0.dist-info → ara_cli-0.1.13.3.dist-info}/entry_points.txt +0 -0
  140. {ara_cli-0.1.10.0.dist-info → ara_cli-0.1.13.3.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,256 @@
1
+ import os
2
+ import logging
3
+ from ara_cli.prompt_handler import LLMSingleton
4
+ from langfuse.api.resources.commons.errors import Error as LangfuseError, NotFoundError
5
+ from ara_cli.classifier import Classifier
6
+ from ara_cli.artefact_reader import ArtefactReader
7
+ from ara_cli.artefact_creator import ArtefactCreator
8
+ from ara_cli.error_handler import AraError
9
+ from ara_cli.directory_navigator import DirectoryNavigator
10
+ from ara_cli.artefact_deleter import ArtefactDeleter
11
+
12
+
13
+ class AraArtefactConverter:
14
+ def __init__(self, file_system=None):
15
+ self.file_system = file_system or os
16
+ self.reader = ArtefactReader()
17
+ self.creator = ArtefactCreator(self.file_system)
18
+
19
+ def convert(
20
+ self,
21
+ old_classifier: str,
22
+ artefact_name: str,
23
+ new_classifier: str,
24
+ merge: bool = False,
25
+ override: bool = False,
26
+ ):
27
+ try:
28
+ self._validate_classifiers(old_classifier, new_classifier)
29
+
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
+ )
37
+
38
+ target_content_existing = self._resolve_target_content(
39
+ artefact_name, new_classifier, merge, override
40
+ )
41
+
42
+ target_class = self._get_target_class(new_classifier)
43
+
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,
51
+ )
52
+
53
+ print(
54
+ f"{'Merging' if merge and target_content_existing else 'Converting'} '{artefact_name}' from {old_classifier} to {new_classifier}..."
55
+ )
56
+
57
+ converted_artefact = self._run_conversion_agent(prompt, target_class)
58
+ artefact_content = converted_artefact.serialize()
59
+
60
+ self._write_artefact(
61
+ new_classifier,
62
+ artefact_name,
63
+ artefact_content,
64
+ merge=merge,
65
+ override=override,
66
+ )
67
+
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)
74
+
75
+ except ValueError as e:
76
+ raise e
77
+ except AraError as e:
78
+ raise e
79
+ except Exception as e:
80
+ raise e
81
+
82
+ def _validate_classifiers(self, old_classifier: str, new_classifier: str):
83
+ if not Classifier.is_valid_classifier(old_classifier):
84
+ raise ValueError(f"Invalid classifier: {old_classifier}")
85
+ if not Classifier.is_valid_classifier(new_classifier):
86
+ raise ValueError(f"Invalid classifier: {new_classifier}")
87
+
88
+ def _move_data_folder_content(self, old_classifier, new_classifier, artefact_name):
89
+ import shutil
90
+
91
+ navigator = DirectoryNavigator()
92
+ navigator.navigate_to_target()
93
+
94
+ sub_directory_old = Classifier.get_sub_directory(old_classifier)
95
+ dir_path_old = self.file_system.path.join(
96
+ sub_directory_old, f"{artefact_name}.data"
97
+ )
98
+
99
+ sub_directory_new = Classifier.get_sub_directory(new_classifier)
100
+ dir_path_new = self.file_system.path.join(
101
+ sub_directory_new, f"{artefact_name}.data"
102
+ )
103
+
104
+ if self.file_system.path.exists(dir_path_old):
105
+ backup_folder_name = f"{artefact_name}.data.old"
106
+ destination_path = self.file_system.path.join(
107
+ dir_path_new, backup_folder_name
108
+ )
109
+
110
+ if not self.file_system.path.exists(dir_path_new):
111
+ os.makedirs(dir_path_new, exist_ok=True)
112
+
113
+ if self.file_system.path.exists(destination_path):
114
+ shutil.rmtree(destination_path)
115
+
116
+ try:
117
+ shutil.move(dir_path_old, destination_path)
118
+ print(f"Moved old data to {destination_path}")
119
+ except Exception as e:
120
+ print(f"Error moving data directory: {e}")
121
+
122
+ def _resolve_target_content(
123
+ self, artefact_name: str, new_classifier: str, merge: bool, override: bool
124
+ ):
125
+ target_content_existing = None
126
+ if not merge and not override:
127
+ _, new_artefact_info = self.reader.read_artefact_data(
128
+ artefact_name, new_classifier
129
+ )
130
+ if new_artefact_info:
131
+ raise ValueError(
132
+ f"Found already exiting {new_classifier} {artefact_name}. Rerun the command with --override or --merge."
133
+ )
134
+ elif merge:
135
+ target_content_existing, _ = self.reader.read_artefact_data(
136
+ artefact_name, new_classifier
137
+ )
138
+ return target_content_existing
139
+
140
+ def _get_target_class(self, new_classifier: str):
141
+ from ara_cli.artefact_models.artefact_mapping import artefact_type_mapping
142
+ from ara_cli.artefact_models.artefact_model import ArtefactType
143
+
144
+ target_type = ArtefactType(new_classifier)
145
+ target_class = artefact_type_mapping.get(target_type)
146
+
147
+ if not target_class:
148
+ raise AraError(f"No artefact class found for classifier: {new_classifier}")
149
+ return target_class
150
+
151
+ def _get_prompt(
152
+ self,
153
+ old_classifier,
154
+ new_classifier,
155
+ artefact_name,
156
+ content,
157
+ target_content_existing,
158
+ merge,
159
+ ):
160
+ try:
161
+ langfuse = LLMSingleton.get_instance().langfuse
162
+ if langfuse is None:
163
+ # This mimics the behavior if authentication failed or env vars missing in Singleton
164
+ raise Exception("Langfuse not initialized in Singleton")
165
+
166
+ if merge and target_content_existing:
167
+ prompt_template = langfuse.get_prompt("ara-cli/artefact-convert/merge")
168
+ return prompt_template.compile(
169
+ old_classifier=old_classifier,
170
+ new_classifier=new_classifier,
171
+ artefact_name=artefact_name,
172
+ content=content,
173
+ target_content_existing=target_content_existing,
174
+ )
175
+ else:
176
+ prompt_template = langfuse.get_prompt(
177
+ "ara-cli/artefact-convert/default"
178
+ )
179
+ return prompt_template.compile(
180
+ old_classifier=old_classifier,
181
+ new_classifier=new_classifier,
182
+ artefact_name=artefact_name,
183
+ content=content,
184
+ )
185
+
186
+ except (LangfuseError, NotFoundError, Exception) as e:
187
+ logging.info(f"Could not fetch Langfuse prompt: {e}. Using fallback.")
188
+ # Fallback prompts
189
+ formatting_instructions = (
190
+ "### Data Extraction Rules:\n"
191
+ "- **Users**: Extract usernames ONLY. Do NOT add '@' or 'user_' prefixes. "
192
+ "The system adds these automatically. (e.g., return 'hans', NOT '@user_hans').\n"
193
+ "- **Author**: Extract the author as 'creator_<name>'. Do NOT add the '@' symbol. "
194
+ "(e.g., return 'creator_unknown', NOT '@creator_unknown').\n"
195
+ f"- **Artefact Name**: Use strictly '{artefact_name}'.\n"
196
+ "- **Content**: Adapt the content to the target schema fields."
197
+ )
198
+
199
+ if merge and target_content_existing:
200
+ return (
201
+ f"Merge the following {old_classifier} artefact into the existing {new_classifier} artefact. "
202
+ "Combine the information from both, prioritizing the structure of the target artefact schema. "
203
+ "Ensure no critical information is lost. "
204
+ "\n\n"
205
+ f"{formatting_instructions}"
206
+ "\n\n"
207
+ f"Source Artefact ({old_classifier}):\n```\n{content}\n```"
208
+ f"\n\nTarget Artefact ({new_classifier}):\n```\n{target_content_existing}\n```"
209
+ )
210
+ else:
211
+ return (
212
+ f"Convert the following {old_classifier} artefact to a {new_classifier} artefact. "
213
+ "Preserve the core meaning, business value, and description. "
214
+ "Map the content to the fields required by the target schema. "
215
+ "\n\n"
216
+ f"{formatting_instructions}"
217
+ "\n\n"
218
+ f"Source Artefact Content:\n```\n{content}\n```"
219
+ )
220
+
221
+ def _run_conversion_agent(self, prompt, target_class):
222
+ from ara_cli.llm_utils import create_pydantic_ai_agent
223
+
224
+ agent = create_pydantic_ai_agent(output_type=target_class, instrument=True)
225
+ try:
226
+ result = agent.run_sync(prompt)
227
+ return result.output
228
+ except Exception as e:
229
+ raise AraError(f"LLM conversion failed: {e}")
230
+
231
+ def _write_artefact(
232
+ self, new_classifier, artefact_name, artefact_content, merge, override
233
+ ):
234
+ from shutil import rmtree
235
+
236
+ navigator = DirectoryNavigator()
237
+ navigator.navigate_to_target()
238
+
239
+ sub_directory = Classifier.get_sub_directory(new_classifier)
240
+ file_path = self.file_system.path.join(
241
+ sub_directory, f"{artefact_name}.{new_classifier}"
242
+ )
243
+ dir_path = self.file_system.path.join(sub_directory, f"{artefact_name}.data")
244
+
245
+ if self.file_system.path.exists(file_path) and not (override or merge):
246
+ raise ValueError(f"Target file {file_path} already exists.")
247
+
248
+ if not merge:
249
+ rmtree(dir_path, ignore_errors=True)
250
+
251
+ os.makedirs(dir_path, exist_ok=True)
252
+
253
+ with open(file_path, "w", encoding="utf-8") as f:
254
+ f.write(artefact_content)
255
+
256
+ print(f"Conversion successful. Created: {file_path}")
@@ -184,6 +184,10 @@ class Artefact(BaseModel, ABC):
184
184
  default=[],
185
185
  description="Optional list of tags (0-many)",
186
186
  )
187
+ author: Optional[str] = Field(
188
+ default="creator_unknown",
189
+ description="Author of the artefact, must be a single entry of the form 'creator_<someone>'."
190
+ )
187
191
  title: str = Field(
188
192
  ...,
189
193
  description="Descriptive Artefact title (mandatory)",
@@ -252,6 +256,17 @@ class Artefact(BaseModel, ABC):
252
256
  raise ValueError(f"Tag '{tag}' has the form of a status tag. Set `status` field instead of passing it with other tags")
253
257
  if tag.startswith("user_"):
254
258
  raise ValueError(f"Tag '{tag} has the form of a user tag. Set `users` field instead of passing it with other tags")
259
+ if tag.startswith("creator_"):
260
+ raise ValueError(f"Tag '{tag}' has the form of an author tag. Set `author` field instead of passing it with other tags")
261
+ return v
262
+
263
+ @field_validator('author')
264
+ def validate_author(cls, v):
265
+ if v:
266
+ if not v.startswith("creator_"):
267
+ raise ValueError(f"Author '{v}' must start with 'creator_'.")
268
+ if len(v) <= len("creator_"):
269
+ raise ValueError("Creator name cannot be empty in author tag.")
255
270
  return v
256
271
 
257
272
  @field_validator('title')
@@ -291,32 +306,81 @@ class Artefact(BaseModel, ABC):
291
306
  tag_line = lines[0]
292
307
  if not tag_line.startswith('@'):
293
308
  return {}, lines
309
+
294
310
  tags = tag_line.split()
311
+ tag_dict = cls._process_tags(tags)
312
+ return tag_dict, lines[1:]
313
+
314
+ @classmethod
315
+ def _process_tags(cls, tags) -> Dict[str, str]:
316
+ """Process a list of tags and return a dictionary with categorized tags."""
295
317
  status = None
296
318
  regular_tags = []
297
319
  users = []
298
- status_list = ["@to-do", "@in-progress", "@review", "@done", "@closed"]
299
- user_prefix = "@user_"
300
- user_prefix_length = len(user_prefix)
301
-
320
+ author = None
321
+
302
322
  for tag in tags:
303
- if not tag.startswith('@'):
304
- raise ValueError(f"Tag '{tag}' should start with '@' but started with '{tag[0]}'")
305
- if tag in status_list and status is not None:
306
- raise ValueError(f"Multiple status tags found: '@{status}' and '{tag}'")
307
- if tag in status_list:
308
- status = tag[1:]
309
- continue
310
- if tag.startswith("@user_") and len(tag) > user_prefix_length + 1:
311
- users.append(tag[user_prefix_length:])
312
- continue
313
- regular_tags.append(tag[1:])
314
- tag_dict = {
323
+ cls._validate_tag_format(tag)
324
+
325
+ if cls._is_status_tag(tag):
326
+ status = cls._process_status_tag(tag, status)
327
+ elif cls._is_user_tag(tag):
328
+ users.append(cls._extract_user_from_tag(tag))
329
+ elif cls._is_author_tag(tag):
330
+ author = cls._process_author_tag(tag, author)
331
+ else:
332
+ regular_tags.append(tag[1:])
333
+
334
+ return {
315
335
  "status": status,
316
336
  "users": users,
317
- "tags": regular_tags
337
+ "tags": regular_tags,
338
+ "author": author
318
339
  }
319
- return tag_dict, lines[1:]
340
+
341
+ @classmethod
342
+ def _validate_tag_format(cls, tag):
343
+ """Validate that tag starts with @."""
344
+ if not tag.startswith('@'):
345
+ raise ValueError(f"Tag '{tag}' should start with '@' but started with '{tag[0]}'")
346
+
347
+ @classmethod
348
+ def _is_status_tag(cls, tag) -> bool:
349
+ """Check if tag is a status tag."""
350
+ status_list = ["@to-do", "@in-progress", "@review", "@done", "@closed"]
351
+ return tag in status_list
352
+
353
+ @classmethod
354
+ def _process_status_tag(cls, tag, current_status):
355
+ """Process status tag and check for duplicates."""
356
+ if current_status is not None:
357
+ raise ValueError(f"Multiple status tags found: '@{current_status}' and '{tag}'")
358
+ return tag[1:] # Remove @ prefix
359
+
360
+ @classmethod
361
+ def _is_user_tag(cls, tag) -> bool:
362
+ """Check if tag is a user tag."""
363
+ user_prefix = "@user_"
364
+ return tag.startswith(user_prefix) and len(tag) > len(user_prefix)
365
+
366
+ @classmethod
367
+ def _extract_user_from_tag(cls, tag) -> str:
368
+ """Extract username from user tag."""
369
+ user_prefix = "@user_"
370
+ return tag[len(user_prefix):]
371
+
372
+ @classmethod
373
+ def _is_author_tag(cls, tag) -> bool:
374
+ """Check if tag is an author tag."""
375
+ creator_prefix = "@creator_"
376
+ return tag.startswith(creator_prefix) and len(tag) > len(creator_prefix)
377
+
378
+ @classmethod
379
+ def _process_author_tag(cls, tag, current_author):
380
+ """Process author tag and check for duplicates."""
381
+ if current_author is not None:
382
+ raise ValueError(f"Multiple author tags found: '@{current_author}' and '@{tag[1:]}'")
383
+ return tag[1:]
320
384
 
321
385
  @classmethod
322
386
  def _deserialize_title(cls, lines) -> (str, List[str]):
@@ -346,14 +410,26 @@ class Artefact(BaseModel, ABC):
346
410
  return contribution, lines
347
411
 
348
412
  @classmethod
349
- def _deserialize_description(cls, lines) -> (Optional[str], List[str]):
413
+ def _deserialize_description(cls, lines: List[str]) -> (Optional[str], List[str]):
350
414
  description_start = cls._description_starts_with()
415
+ start_index = -1
351
416
  for i, line in enumerate(lines):
352
417
  if line.startswith(description_start):
353
- description = line[len(description_start):].strip()
354
- del lines[i]
355
- return description, lines
356
- return None, lines
418
+ start_index = i
419
+ break
420
+
421
+ if start_index == -1:
422
+ return None, lines
423
+
424
+ first_line_content = lines[start_index][len(description_start):].strip()
425
+
426
+ description_lines = ([first_line_content] if first_line_content else []) + lines[start_index + 1:]
427
+
428
+ description = "\n".join(description_lines)
429
+
430
+ remaining_lines = lines[:start_index]
431
+
432
+ return (description if description else None), remaining_lines
357
433
 
358
434
  @classmethod
359
435
  def _parse_common_fields(cls, text: str) -> dict:
@@ -367,7 +443,7 @@ class Artefact(BaseModel, ABC):
367
443
  contribution, remaining_lines = cls._deserialize_contribution(remaining_lines)
368
444
  description, remaining_lines = cls._deserialize_description(remaining_lines)
369
445
 
370
- return {
446
+ fields = {
371
447
  'artefact_type': cls._artefact_type(),
372
448
  'tags': tags.get('tags', []),
373
449
  'users': tags.get('users', []),
@@ -376,6 +452,9 @@ class Artefact(BaseModel, ABC):
376
452
  'contribution': contribution,
377
453
  'description': description,
378
454
  }
455
+ if tags.get("author"):
456
+ fields["author"] = tags.get("author")
457
+ return fields
379
458
 
380
459
  @classmethod
381
460
  def deserialize(cls, text: str) -> 'Artefact':
@@ -407,6 +486,8 @@ class Artefact(BaseModel, ABC):
407
486
  tags.append(f"@{self.status}")
408
487
  for user in self.users:
409
488
  tags.append(f"@user_{user}")
489
+ if self.author:
490
+ tags.append(f"@{self.author}")
410
491
  for tag in self.tags:
411
492
  tags.append(f"@{tag}")
412
493
  return ' '.join(tags)
@@ -430,4 +511,4 @@ class Artefact(BaseModel, ABC):
430
511
  classifier=classifier,
431
512
  rule=rule
432
513
  )
433
- self.contribution = contribution
514
+ self.contribution = contribution
@@ -29,7 +29,8 @@ def _default_vision(title: str, use_default_contribution: bool) -> VisionArtefac
29
29
  our_product="<statement of primary differentiation>"
30
30
  )
31
31
  return VisionArtefact(
32
- tags=["sample_tag"],
32
+ tags=[],
33
+ author="creator_unknown",
33
34
  title=title,
34
35
  description="<further optional description to understand the vision, markdown capable text formatting>",
35
36
  intent=intent,
@@ -44,7 +45,8 @@ def _default_businessgoal(title: str, use_default_contribution: bool) -> Busines
44
45
  i_want="<something that helps me to reach my monetary goal>"
45
46
  )
46
47
  return BusinessgoalArtefact(
47
- tags=["sample_tag"],
48
+ tags=[],
49
+ author="creator_unknown",
48
50
  title=title,
49
51
  description="<further optional description to understand the businessgoal, markdown capable text formatting>",
50
52
  intent=intent,
@@ -57,7 +59,8 @@ def _default_capability(title: str, use_default_contribution: bool) -> Capabilit
57
59
  to_be_able_to="<needed capability for stakeholders that are the enablers/relevant for reaching the business goal>"
58
60
  )
59
61
  return CapabilityArtefact(
60
- tags=["sample_tag"],
62
+ tags=[],
63
+ author="creator_unknown",
61
64
  title=title,
62
65
  description="<further optional description to understand the capability, markdown capable text formatting>",
63
66
  intent=intent,
@@ -77,7 +80,8 @@ def _default_epic(title: str, use_default_contribution: bool) -> EpicArtefact:
77
80
  "<rule needed to fulfill the wanted product behavior>"
78
81
  ]
79
82
  return EpicArtefact(
80
- tags=["sample_tag"],
83
+ tags=[],
84
+ author="creator_unknown",
81
85
  title=title,
82
86
  description="<further optional description to understand the epic, markdown capable text formatting>",
83
87
  intent=intent,
@@ -98,7 +102,8 @@ def _default_userstory(title: str, use_default_contribution: bool) -> UserstoryA
98
102
  "<rule needed to fulfill the wanted product behavior>"
99
103
  ]
100
104
  return UserstoryArtefact(
101
- tags=["sample_tag"],
105
+ tags=[],
106
+ author="creator_unknown",
102
107
  title=title,
103
108
  description="<further optional description to understand the userstory, markdown capable text formatting>",
104
109
  intent=intent,
@@ -110,7 +115,8 @@ def _default_userstory(title: str, use_default_contribution: bool) -> UserstoryA
110
115
 
111
116
  def _default_example(title: str, use_default_contribution: bool) -> ExampleArtefact:
112
117
  return ExampleArtefact(
113
- tags=["sample_tag"],
118
+ tags=[],
119
+ author="creator_unknown",
114
120
  title=title,
115
121
  description="<further optional description to understand the example, markdown capable text formatting>",
116
122
  contribution=default_contribution() if use_default_contribution else None
@@ -130,7 +136,8 @@ def _default_keyfeature(title: str, use_default_contribution: bool) -> Keyfeatur
130
136
  THEN some result is to be expected
131
137
  AND some other result is to be expected>"""
132
138
  return KeyfeatureArtefact(
133
- tags=["sample_tag"],
139
+ tags=[],
140
+ author="creator_unknown",
134
141
  title=title,
135
142
  description=description,
136
143
  intent=intent,
@@ -185,7 +192,8 @@ def _default_feature(title: str, use_default_contribution: bool) -> FeatureArtef
185
192
  description = """<further optional description to understand the feature, no format defined, the example artefact is only a placeholder>"""
186
193
 
187
194
  return FeatureArtefact(
188
- tags=["sample_tag"],
195
+ tags=[],
196
+ author="creator_unknown",
189
197
  title=title,
190
198
  description=description,
191
199
  intent=intent,
@@ -196,7 +204,8 @@ def _default_feature(title: str, use_default_contribution: bool) -> FeatureArtef
196
204
 
197
205
  def _default_task(title: str, use_default_contribution: bool) -> TaskArtefact:
198
206
  return TaskArtefact(
199
- status="to-do",
207
+ tags=[],
208
+ status=None,
200
209
  title=title,
201
210
  description="<further optional description to understand the task, no format defined>",
202
211
  contribution=default_contribution() if use_default_contribution else None
@@ -214,7 +223,8 @@ def _default_issue(title: str, use_default_contribution: bool) -> IssueArtefact:
214
223
  *or optional free text description*"""
215
224
 
216
225
  return IssueArtefact(
217
- tags=["sample_tag"],
226
+ tags=[],
227
+ author="creator_unknown",
218
228
  title=title,
219
229
  description=description,
220
230
  additional_description=additional_description,
@@ -1,5 +1,5 @@
1
1
  from ara_cli.artefact_models.artefact_model import Artefact, ArtefactType, Intent
2
- from pydantic import Field, field_validator
2
+ from pydantic import Field, field_validator, model_validator
3
3
  from typing import List, Tuple, Optional
4
4
 
5
5
 
@@ -91,6 +91,15 @@ class EpicArtefact(Artefact):
91
91
  description="Rules the epic defines. It is recommended to create rules to clarify the desired outcome"
92
92
  )
93
93
 
94
+ @model_validator(mode='after')
95
+ def check_for_misplaced_rules(self) -> 'EpicArtefact':
96
+ if self.description:
97
+ desc_lines = self.description.split('\n')
98
+ for line in desc_lines:
99
+ if line.strip().startswith("Rule:"):
100
+ raise ValueError("Found 'Rule:' inside description. Rules must be defined before the 'Description:' section.")
101
+ return self
102
+
94
103
  @field_validator('artefact_type')
95
104
  def validate_artefact_type(cls, v):
96
105
  if v != ArtefactType.epic:
@@ -166,4 +175,4 @@ class EpicArtefact(Artefact):
166
175
  lines.append("")
167
176
  lines.append(description)
168
177
  lines.append("")
169
- return "\n".join(lines)
178
+ return "\n".join(lines)
@@ -301,6 +301,36 @@ class FeatureArtefact(Artefact):
301
301
  f"FeatureArtefact must have artefact_type of '{ArtefactType.feature}', not '{v}'")
302
302
  return v
303
303
 
304
+ @classmethod
305
+ def _deserialize_description(cls, lines: List[str]) -> (Optional[str], List[str]):
306
+ description_start = cls._description_starts_with()
307
+ scenario_markers = ["Scenario:", "Scenario Outline:"]
308
+
309
+ start_index = -1
310
+ for i, line in enumerate(lines):
311
+ if line.startswith(description_start):
312
+ start_index = i
313
+ break
314
+
315
+ if start_index == -1:
316
+ return None, lines
317
+
318
+ end_index = len(lines)
319
+ for i in range(start_index + 1, len(lines)):
320
+ if any(lines[i].startswith(marker) for marker in scenario_markers):
321
+ end_index = i
322
+ break
323
+
324
+ first_line_content = lines[start_index][len(description_start):].strip()
325
+
326
+ description_lines_list = [first_line_content] if first_line_content else []
327
+ description_lines_list.extend(lines[start_index+1:end_index])
328
+
329
+ description = "\n".join(description_lines_list).strip() or None
330
+
331
+ remaining_lines = lines[:start_index] + lines[end_index:]
332
+
333
+ return description, remaining_lines
304
334
 
305
335
  @classmethod
306
336
  def _title_prefix(cls) -> str:
@@ -519,4 +549,4 @@ class FeatureArtefact(Artefact):
519
549
  # or the placeholder is at the end of a line (e.g., "Then I see... __PLACEHOLDER__").
520
550
  step = step.replace(key, value)
521
551
  rehydrated_steps.append(step)
522
- return rehydrated_steps
552
+ return rehydrated_steps
@@ -1,5 +1,5 @@
1
1
  from ara_cli.artefact_models.artefact_model import Artefact, ArtefactType, Intent
2
- from pydantic import Field, field_validator
2
+ from pydantic import Field, field_validator, model_validator
3
3
  from typing import List, Tuple
4
4
 
5
5
 
@@ -92,6 +92,18 @@ class UserstoryArtefact(Artefact):
92
92
  default_factory=list,
93
93
  description="Rules the userstory defines. It is recommended to create rules to clarify the desired outcome")
94
94
 
95
+ @model_validator(mode='after')
96
+ def check_for_misplaced_content(self) -> 'UserstoryArtefact':
97
+ if self.description:
98
+ desc_lines = self.description.split('\n')
99
+ for line in desc_lines:
100
+ stripped_line = line.strip()
101
+ if stripped_line.startswith("Rule:"):
102
+ raise ValueError("Found 'Rule:' inside description. Rules must be defined before the 'Description:' section.")
103
+ if stripped_line.startswith("Estimate:"):
104
+ raise ValueError("Found 'Estimate:' inside description. Estimate must be defined before the 'Description:' section.")
105
+ return self
106
+
95
107
  @field_validator('artefact_type')
96
108
  def validate_artefact_type(cls, v):
97
109
  if v != ArtefactType.userstory:
@@ -171,7 +183,7 @@ class UserstoryArtefact(Artefact):
171
183
  rules = self._serialize_rules()
172
184
 
173
185
  lines = []
174
- if self.tags:
186
+ if tags: # Changed from self.tags to tags to include all tag types
175
187
  lines.append(tags)
176
188
  lines.append(title)
177
189
  lines.append("")
@@ -188,4 +200,4 @@ class UserstoryArtefact(Artefact):
188
200
  lines.append(description)
189
201
  lines.append("")
190
202
 
191
- return '\n'.join(lines)
203
+ return '\n'.join(lines)
ara_cli/artefact_scan.py CHANGED
@@ -25,10 +25,10 @@ def is_rule_valid(contribution, classified_artefact_info) -> bool:
25
25
  if not rule:
26
26
  return True
27
27
  parent = ArtefactReader.read_artefact(contribution.artefact_name, contribution.classifier)
28
- if not parent or not parent.rules:
28
+ if not parent:
29
29
  return True
30
30
  rules = parent.rules
31
- if rule not in rules:
31
+ if not rules or rule not in rules:
32
32
  return False
33
33
  return True
34
34