ara-cli 0.1.9.61__py3-none-any.whl → 0.1.9.63__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/__main__.py +4 -2
- ara_cli/ara_command_action.py +28 -1
- ara_cli/ara_command_parser.py +6 -0
- ara_cli/artefact_autofix.py +146 -0
- ara_cli/artefact_models/artefact_model.py +1 -1
- ara_cli/artefact_models/businessgoal_artefact_model.py +6 -1
- ara_cli/artefact_models/epic_artefact_model.py +6 -1
- ara_cli/artefact_models/feature_artefact_model.py +8 -3
- ara_cli/artefact_models/keyfeature_artefact_model.py +6 -1
- ara_cli/artefact_models/serialize_helper.py +18 -0
- ara_cli/artefact_models/userstory_artefact_model.py +6 -1
- ara_cli/artefact_scan.py +2 -0
- ara_cli/tag_extractor.py +31 -6
- ara_cli/tests/test_ara_autofix.py +113 -0
- ara_cli/tests/test_ara_command_action.py +2 -1
- ara_cli/version.py +1 -1
- {ara_cli-0.1.9.61.dist-info → ara_cli-0.1.9.63.dist-info}/METADATA +2 -1
- {ara_cli-0.1.9.61.dist-info → ara_cli-0.1.9.63.dist-info}/RECORD +21 -19
- ara_cli/analyse_artefacts.py +0 -133
- {ara_cli-0.1.9.61.dist-info → ara_cli-0.1.9.63.dist-info}/WHEEL +0 -0
- {ara_cli-0.1.9.61.dist-info → ara_cli-0.1.9.63.dist-info}/entry_points.txt +0 -0
- {ara_cli-0.1.9.61.dist-info → ara_cli-0.1.9.63.dist-info}/top_level.txt +0 -0
ara_cli/__main__.py
CHANGED
|
@@ -18,7 +18,8 @@ from ara_cli.ara_command_action import (
|
|
|
18
18
|
set_status_action,
|
|
19
19
|
set_user_action,
|
|
20
20
|
classifier_directory_action,
|
|
21
|
-
scan_action
|
|
21
|
+
scan_action,
|
|
22
|
+
autofix_action
|
|
22
23
|
)
|
|
23
24
|
import argcomplete
|
|
24
25
|
import sys
|
|
@@ -42,7 +43,8 @@ def define_action_mapping():
|
|
|
42
43
|
"set-status": set_status_action,
|
|
43
44
|
"set-user": set_user_action,
|
|
44
45
|
"classifier-directory": classifier_directory_action,
|
|
45
|
-
"scan": scan_action
|
|
46
|
+
"scan": scan_action,
|
|
47
|
+
"autofix": autofix_action
|
|
46
48
|
}
|
|
47
49
|
|
|
48
50
|
|
ara_cli/ara_command_action.py
CHANGED
|
@@ -128,7 +128,10 @@ def list_tags_action(args):
|
|
|
128
128
|
tag_classifier = args.include_classifier
|
|
129
129
|
|
|
130
130
|
tag_extractor = TagExtractor()
|
|
131
|
-
tags = tag_extractor.extract_tags(
|
|
131
|
+
tags = tag_extractor.extract_tags(
|
|
132
|
+
include_classifier=tag_classifier,
|
|
133
|
+
filtered_extra_column=getattr(args, "filtered_extra_column", False)
|
|
134
|
+
)
|
|
132
135
|
|
|
133
136
|
if args.json:
|
|
134
137
|
output = json.dumps({"tags": tags})
|
|
@@ -538,3 +541,27 @@ def scan_action(args):
|
|
|
538
541
|
if invalid:
|
|
539
542
|
invalid_artefacts[classifier] = invalid
|
|
540
543
|
show_results(invalid_artefacts)
|
|
544
|
+
|
|
545
|
+
|
|
546
|
+
def autofix_action(args):
|
|
547
|
+
from ara_cli.artefact_autofix import parse_report, apply_autofix, read_report_file
|
|
548
|
+
|
|
549
|
+
content = read_report_file()
|
|
550
|
+
|
|
551
|
+
if not content:
|
|
552
|
+
return False
|
|
553
|
+
|
|
554
|
+
issues = parse_report(content)
|
|
555
|
+
|
|
556
|
+
if not issues:
|
|
557
|
+
print("No issues found in the report. Nothing to fix.")
|
|
558
|
+
return
|
|
559
|
+
|
|
560
|
+
# print("Found issues to fix:")
|
|
561
|
+
for classifier, files in issues.items():
|
|
562
|
+
print(f"\nClassifier: {classifier}")
|
|
563
|
+
for file_path, reason in files:
|
|
564
|
+
print(f"Attempting to fix {file_path} for reason: {reason}")
|
|
565
|
+
apply_autofix(file_path, classifier, reason)
|
|
566
|
+
|
|
567
|
+
print("\nAutofix process completed. Please review the changes.")
|
ara_cli/ara_command_parser.py
CHANGED
|
@@ -121,6 +121,7 @@ def list_tags_parser(subparsers):
|
|
|
121
121
|
tags_parser = subparsers.add_parser("list-tags", help="Show tags")
|
|
122
122
|
tags_parser.add_argument("--json", "-j", help="Output tags as JSON", action=argparse.BooleanOptionalAction)
|
|
123
123
|
tags_parser.add_argument("--include-classifier", choices=classifiers, help="Show tags for an artefact type")
|
|
124
|
+
tags_parser.add_argument("--filtered-extra-column", action="store_true", help="Filter tags for extra column")
|
|
124
125
|
|
|
125
126
|
|
|
126
127
|
def add_chat_arguments(chat_parser):
|
|
@@ -220,6 +221,10 @@ def scan_parser(subparsers):
|
|
|
220
221
|
subparsers.add_parser("scan", help="Scan ARA tree for incompatible artefacts.")
|
|
221
222
|
|
|
222
223
|
|
|
224
|
+
def autofix_parser(subparsers):
|
|
225
|
+
subparsers.add_parser("autofix", help="Fix ARA tree with llm models for scanned artefacts with ara scan command.")
|
|
226
|
+
|
|
227
|
+
|
|
223
228
|
class CustomHelpFormatter(argparse.HelpFormatter):
|
|
224
229
|
def format_help(self):
|
|
225
230
|
from sys import argv
|
|
@@ -307,5 +312,6 @@ def action_parser():
|
|
|
307
312
|
set_user_parser(subparsers)
|
|
308
313
|
classifier_directory_parser(subparsers)
|
|
309
314
|
scan_parser(subparsers)
|
|
315
|
+
autofix_parser(subparsers)
|
|
310
316
|
|
|
311
317
|
return parser
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
def read_report_file():
|
|
2
|
+
file_path = "incompatible_artefacts_report.md"
|
|
3
|
+
try:
|
|
4
|
+
with open(file_path, "r", encoding="utf-8") as f:
|
|
5
|
+
content = f.read()
|
|
6
|
+
except OSError:
|
|
7
|
+
print('Artefact scan results file not found. Did you run the "ara scan" command?')
|
|
8
|
+
return
|
|
9
|
+
|
|
10
|
+
return content
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def parse_report(content: str) -> dict:
|
|
14
|
+
"""
|
|
15
|
+
Parses the incompatible artefacts report and returns structured data.
|
|
16
|
+
Returns a dictionary where keys are artefact classifiers, and values are lists of (file_path, reason) tuples.
|
|
17
|
+
"""
|
|
18
|
+
lines = content.splitlines()
|
|
19
|
+
issues = {}
|
|
20
|
+
current_classifier = None
|
|
21
|
+
|
|
22
|
+
if not lines or lines[0] != "# Artefact Check Report":
|
|
23
|
+
return issues # Geçersiz rapor formatı
|
|
24
|
+
|
|
25
|
+
if len(lines) >= 3 and lines[2] == "No problems found.":
|
|
26
|
+
return issues # Hiç sorun bulunamadı
|
|
27
|
+
|
|
28
|
+
for line in lines[1:]: # Başlıktan sonraki satırları işle
|
|
29
|
+
line = line.strip()
|
|
30
|
+
if not line:
|
|
31
|
+
continue
|
|
32
|
+
|
|
33
|
+
# Classifier başlığı tespiti (## ile başlayan)
|
|
34
|
+
if line.startswith("## "):
|
|
35
|
+
current_classifier = line[3:].strip()
|
|
36
|
+
issues[current_classifier] = []
|
|
37
|
+
|
|
38
|
+
# Dosya listesi tespiti (- ile başlayan)
|
|
39
|
+
elif line.startswith("- ") and current_classifier is not None:
|
|
40
|
+
# Format: "- `file_path`: reason"
|
|
41
|
+
parts = line.split("`", 2)
|
|
42
|
+
if len(parts) < 3:
|
|
43
|
+
continue # Geçersiz format
|
|
44
|
+
|
|
45
|
+
file_path = parts[1]
|
|
46
|
+
reason = parts[2].split(
|
|
47
|
+
":", 1)[1].strip() if ":" in parts[2] else ""
|
|
48
|
+
issues[current_classifier].append((file_path, reason))
|
|
49
|
+
|
|
50
|
+
return issues
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def read_artefact(file_path):
|
|
54
|
+
"""Reads the artefact text from the given file path."""
|
|
55
|
+
try:
|
|
56
|
+
with open(file_path, 'r') as file:
|
|
57
|
+
return file.read()
|
|
58
|
+
except FileNotFoundError:
|
|
59
|
+
print(f"File not found: {file_path}")
|
|
60
|
+
return None
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def determine_artefact_type_and_class(classifier):
|
|
64
|
+
from ara_cli.artefact_models.artefact_mapping import artefact_type_mapping
|
|
65
|
+
from ara_cli.artefact_models.artefact_model import ArtefactType
|
|
66
|
+
|
|
67
|
+
try:
|
|
68
|
+
artefact_type = ArtefactType(classifier)
|
|
69
|
+
except ValueError:
|
|
70
|
+
print(f"Invalid classifier: {classifier}")
|
|
71
|
+
return None, None
|
|
72
|
+
|
|
73
|
+
artefact_class = artefact_type_mapping.get(artefact_type)
|
|
74
|
+
if not artefact_class:
|
|
75
|
+
print(f"No artefact class found for {artefact_type}")
|
|
76
|
+
return None, None
|
|
77
|
+
|
|
78
|
+
return artefact_type, artefact_class
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def construct_prompt(artefact_type, reason, file_path, artefact_text):
|
|
82
|
+
from ara_cli.artefact_models.artefact_model import ArtefactType
|
|
83
|
+
|
|
84
|
+
prompt = (
|
|
85
|
+
f"Correct the following {artefact_type} artefact to fix the issue: {reason}. "
|
|
86
|
+
"Provide the complete, corrected artefact. Do not reformulate the artefact, "
|
|
87
|
+
"just fix the pydantic model errors, use correct grammar. "
|
|
88
|
+
"Do not remove comments. "
|
|
89
|
+
"You should follow the name of the file "
|
|
90
|
+
f"from its path {file_path} for naming the arteafact's title. "
|
|
91
|
+
"You are not allowed to use file extention in the artefact title. "
|
|
92
|
+
"You are not allowed to modify, delete or add tags. "
|
|
93
|
+
"User tag should be '@user_<username>'. The pydantic model already provides the '@user_' prefix. "
|
|
94
|
+
"So you should be careful to not make it @user_user_<username>. "
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
if artefact_type == ArtefactType.task:
|
|
98
|
+
prompt += (
|
|
99
|
+
"For task artefacts, if the action items looks like template or empty "
|
|
100
|
+
"then just delete those action items."
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
prompt += (
|
|
104
|
+
"\nThe current artefact is:\n"
|
|
105
|
+
"```\n"
|
|
106
|
+
f"{artefact_text}\n"
|
|
107
|
+
"```"
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
return prompt
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def run_agent(prompt, artefact_class):
|
|
114
|
+
from pydantic_ai import Agent
|
|
115
|
+
# gpt-4o
|
|
116
|
+
# anthropic:claude-3-7-sonnet-20250219
|
|
117
|
+
agent = Agent(model="gpt-4o",
|
|
118
|
+
result_type=artefact_class, instrument=True)
|
|
119
|
+
result = agent.run_sync(prompt)
|
|
120
|
+
return result.data
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def write_corrected_artefact(file_path, corrected_text):
|
|
124
|
+
with open(file_path, 'w') as file:
|
|
125
|
+
file.write(corrected_text)
|
|
126
|
+
print(f"Fixed artefact at {file_path}")
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def apply_autofix(file_path, classifier, reason):
|
|
130
|
+
artefact_text = read_artefact(file_path)
|
|
131
|
+
if artefact_text is None:
|
|
132
|
+
return
|
|
133
|
+
|
|
134
|
+
artefact_type, artefact_class = determine_artefact_type_and_class(
|
|
135
|
+
classifier)
|
|
136
|
+
if artefact_type is None or artefact_class is None:
|
|
137
|
+
return
|
|
138
|
+
|
|
139
|
+
prompt = construct_prompt(artefact_type, reason, file_path, artefact_text)
|
|
140
|
+
|
|
141
|
+
try:
|
|
142
|
+
corrected_artefact = run_agent(prompt, artefact_class)
|
|
143
|
+
corrected_text = corrected_artefact.serialize()
|
|
144
|
+
write_corrected_artefact(file_path, corrected_text)
|
|
145
|
+
except Exception as e:
|
|
146
|
+
print(f"Failed to fix artefact at {file_path}: {e}")
|
|
@@ -179,7 +179,7 @@ class Artefact(BaseModel, ABC):
|
|
|
179
179
|
)
|
|
180
180
|
description: Optional[str] = Field(
|
|
181
181
|
default=None,
|
|
182
|
-
description="Optional further description to understand the artefact.
|
|
182
|
+
description="Optional further description to understand the artefact. The description should summerize the core intention of the artefact and give additional valuable information about the artefact."
|
|
183
183
|
)
|
|
184
184
|
|
|
185
185
|
@property
|
|
@@ -33,9 +33,14 @@ class BusinessgoalIntent(Intent):
|
|
|
33
33
|
return v
|
|
34
34
|
|
|
35
35
|
def serialize(self):
|
|
36
|
+
from ara_cli.artefact_models.serialize_helper import as_a_serializer
|
|
37
|
+
|
|
36
38
|
lines = []
|
|
39
|
+
|
|
40
|
+
as_a_line = as_a_serializer(self.as_a)
|
|
41
|
+
|
|
37
42
|
lines.append(f"In order to {self.in_order_to}")
|
|
38
|
-
lines.append(
|
|
43
|
+
lines.append(as_a_line)
|
|
39
44
|
lines.append(f"I want {self.i_want}")
|
|
40
45
|
|
|
41
46
|
return "\n".join(lines)
|
|
@@ -34,9 +34,14 @@ class EpicIntent(Intent):
|
|
|
34
34
|
return v
|
|
35
35
|
|
|
36
36
|
def serialize(self):
|
|
37
|
+
from ara_cli.artefact_models.serialize_helper import as_a_serializer
|
|
38
|
+
|
|
37
39
|
lines = []
|
|
40
|
+
|
|
41
|
+
as_a_line = as_a_serializer(self.as_a)
|
|
42
|
+
|
|
38
43
|
lines.append(f"In order to {self.in_order_to}")
|
|
39
|
-
lines.append(
|
|
44
|
+
lines.append(as_a_line)
|
|
40
45
|
lines.append(f"I want {self.i_want}")
|
|
41
46
|
|
|
42
47
|
return "\n".join(lines)
|
|
@@ -34,8 +34,13 @@ class FeatureIntent(Intent):
|
|
|
34
34
|
return v
|
|
35
35
|
|
|
36
36
|
def serialize(self):
|
|
37
|
+
from ara_cli.artefact_models.serialize_helper import as_a_serializer
|
|
38
|
+
|
|
37
39
|
lines = []
|
|
38
|
-
|
|
40
|
+
|
|
41
|
+
as_a_line = as_a_serializer(self.as_a)
|
|
42
|
+
|
|
43
|
+
lines.append(as_a_line)
|
|
39
44
|
lines.append(f"I want to {self.i_want_to}")
|
|
40
45
|
lines.append(f"So that {self.so_that}")
|
|
41
46
|
|
|
@@ -59,9 +64,9 @@ class FeatureIntent(Intent):
|
|
|
59
64
|
as_a = line[len(as_a_prefix):].strip()
|
|
60
65
|
if line.startswith(as_a_prefix_alt) and not as_a:
|
|
61
66
|
as_a = line[len(as_a_prefix_alt):].strip()
|
|
62
|
-
|
|
67
|
+
if line.startswith(i_want_to_prefix) and not i_want_to:
|
|
63
68
|
i_want_to = line[len(i_want_to_prefix):].strip()
|
|
64
|
-
|
|
69
|
+
if line.startswith(so_that_prefix) and not so_that:
|
|
65
70
|
so_that = line[len(so_that_prefix):].strip()
|
|
66
71
|
index += 1
|
|
67
72
|
|
|
@@ -33,9 +33,14 @@ class KeyfeatureIntent(Intent):
|
|
|
33
33
|
return v
|
|
34
34
|
|
|
35
35
|
def serialize(self):
|
|
36
|
+
from ara_cli.artefact_models.serialize_helper import as_a_serializer
|
|
37
|
+
|
|
36
38
|
lines = []
|
|
39
|
+
|
|
40
|
+
as_a_line = as_a_serializer(self.as_a)
|
|
41
|
+
|
|
37
42
|
lines.append(f"In order to {self.in_order_to}")
|
|
38
|
-
lines.append(
|
|
43
|
+
lines.append(as_a_line)
|
|
39
44
|
lines.append(f"I want {self.i_want}")
|
|
40
45
|
|
|
41
46
|
return "\n".join(lines)
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
def as_a_serializer(as_a):
|
|
2
|
+
role = as_a.strip()
|
|
3
|
+
|
|
4
|
+
exceptions_for_a = ('user', 'university', 'one-time', 'european', 'unit')
|
|
5
|
+
exceptions_for_an = ('hour', 'honest', 'heir')
|
|
6
|
+
role_lower = role.lower()
|
|
7
|
+
as_a_prefix = ""
|
|
8
|
+
|
|
9
|
+
if any(role_lower.startswith(e) for e in exceptions_for_a):
|
|
10
|
+
as_a_prefix = "As a"
|
|
11
|
+
elif any(role_lower.startswith(e) for e in exceptions_for_an):
|
|
12
|
+
as_a_prefix = "As an"
|
|
13
|
+
elif role_lower.startswith(('a', 'e', 'i', 'o', 'u')):
|
|
14
|
+
as_a_prefix = "As an"
|
|
15
|
+
else:
|
|
16
|
+
as_a_prefix = "As a"
|
|
17
|
+
|
|
18
|
+
return f"{as_a_prefix} {role}"
|
|
@@ -33,9 +33,14 @@ class UserstoryIntent(Intent):
|
|
|
33
33
|
return v
|
|
34
34
|
|
|
35
35
|
def serialize(self):
|
|
36
|
+
from ara_cli.artefact_models.serialize_helper import as_a_serializer
|
|
37
|
+
|
|
36
38
|
lines = []
|
|
39
|
+
|
|
40
|
+
as_a_line = as_a_serializer(self.as_a)
|
|
41
|
+
|
|
37
42
|
lines.append(f"In order to {self.in_order_to}")
|
|
38
|
-
lines.append(
|
|
43
|
+
lines.append(as_a_line)
|
|
39
44
|
lines.append(f"I want {self.i_want}")
|
|
40
45
|
|
|
41
46
|
return "\n".join(lines)
|
ara_cli/artefact_scan.py
CHANGED
|
@@ -25,6 +25,8 @@ def find_invalid_files(classified_artefact_info, classifier):
|
|
|
25
25
|
for artefact_info in classified_artefact_info[classifier]:
|
|
26
26
|
if "templates/" in artefact_info["file_path"]:
|
|
27
27
|
continue
|
|
28
|
+
if ".data" in artefact_info["file_path"]:
|
|
29
|
+
continue
|
|
28
30
|
is_valid, reason = check_file(artefact_info["file_path"], artefact_class)
|
|
29
31
|
if not is_valid:
|
|
30
32
|
invalid_files.append((artefact_info["file_path"], reason))
|
ara_cli/tag_extractor.py
CHANGED
|
@@ -6,7 +6,7 @@ class TagExtractor:
|
|
|
6
6
|
def __init__(self, file_system=None):
|
|
7
7
|
self.file_system = file_system or os
|
|
8
8
|
|
|
9
|
-
def extract_tags(self, navigate_to_target=False, include_classifier=None):
|
|
9
|
+
def extract_tags(self, navigate_to_target=False, include_classifier=None, filtered_extra_column=False):
|
|
10
10
|
from ara_cli.template_manager import DirectoryNavigator
|
|
11
11
|
from ara_cli.artefact_reader import ArtefactReader
|
|
12
12
|
|
|
@@ -21,11 +21,36 @@ class TagExtractor:
|
|
|
21
21
|
|
|
22
22
|
unique_tags = set()
|
|
23
23
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
24
|
+
if filtered_extra_column:
|
|
25
|
+
status_tags = {"to-do", "in-progress", "review", "done", "closed"}
|
|
26
|
+
filtered_artefacts = []
|
|
27
|
+
|
|
28
|
+
for artefact_list in artefacts.values():
|
|
29
|
+
for artefact in artefact_list:
|
|
30
|
+
tags = artefact.tags + \
|
|
31
|
+
[artefact.status] if artefact.status else artefact.tags
|
|
32
|
+
tag_set = set(tag for tag in tags if tag is not None)
|
|
33
|
+
if not tag_set & status_tags:
|
|
34
|
+
filtered_artefacts.append(artefact)
|
|
35
|
+
|
|
36
|
+
for artefact in filtered_artefacts:
|
|
37
|
+
tags = [tag for tag in (
|
|
38
|
+
artefact.tags + [artefact.status]) if tag is not None]
|
|
39
|
+
for tag in tags:
|
|
40
|
+
if (
|
|
41
|
+
tag in status_tags
|
|
42
|
+
or tag.startswith("priority_")
|
|
43
|
+
or tag.startswith("user_")
|
|
44
|
+
):
|
|
45
|
+
continue
|
|
46
|
+
unique_tags.add(tag)
|
|
47
|
+
|
|
48
|
+
else:
|
|
49
|
+
for artefact_list in artefacts.values():
|
|
50
|
+
for artefact in artefact_list:
|
|
51
|
+
user_tags = [f"user_{tag}" for tag in artefact.users]
|
|
52
|
+
tags = [tag for tag in (artefact.tags + [artefact.status] + user_tags) if tag is not None]
|
|
53
|
+
unique_tags.update(tags)
|
|
29
54
|
|
|
30
55
|
sorted_tags = sorted(unique_tags)
|
|
31
56
|
return sorted_tags
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import pytest
|
|
2
|
+
from unittest.mock import patch, mock_open, MagicMock
|
|
3
|
+
from ara_cli.artefact_autofix import (
|
|
4
|
+
read_report_file,
|
|
5
|
+
parse_report,
|
|
6
|
+
apply_autofix,
|
|
7
|
+
read_artefact,
|
|
8
|
+
determine_artefact_type_and_class,
|
|
9
|
+
run_agent,
|
|
10
|
+
write_corrected_artefact
|
|
11
|
+
)
|
|
12
|
+
from ara_cli.ara_command_action import autofix_action
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def test_read_report_file():
|
|
16
|
+
mock_content = "# Artefact Check Report\n\n## classifier\n- `file_path`: reason\n"
|
|
17
|
+
with patch("builtins.open", mock_open(read_data=mock_content)) as m:
|
|
18
|
+
content = read_report_file()
|
|
19
|
+
assert content == mock_content
|
|
20
|
+
m.assert_called_once_with(
|
|
21
|
+
"incompatible_artefacts_report.md", "r", encoding="utf-8")
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def test_parse_report():
|
|
25
|
+
content = "# Artefact Check Report\n\n## classifier\n- `file_path`: reason\n"
|
|
26
|
+
expected_issues = {"classifier": [("file_path", "reason")]}
|
|
27
|
+
issues = parse_report(content)
|
|
28
|
+
assert issues == expected_issues
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@patch("ara_cli.artefact_autofix.run_agent")
|
|
32
|
+
@patch("ara_cli.artefact_autofix.write_corrected_artefact")
|
|
33
|
+
def test_apply_autofix(mock_write_corrected_artefact, mock_run_agent):
|
|
34
|
+
mock_run_agent.return_value.serialize.return_value = "corrected content"
|
|
35
|
+
with patch("ara_cli.artefact_autofix.read_artefact", return_value="artefact text"):
|
|
36
|
+
with patch("ara_cli.artefact_autofix.determine_artefact_type_and_class", return_value=("ArtefactType", MagicMock())):
|
|
37
|
+
apply_autofix("file_path", "classifier", "reason")
|
|
38
|
+
mock_run_agent.assert_called_once()
|
|
39
|
+
mock_write_corrected_artefact.assert_called_once_with(
|
|
40
|
+
"file_path", "corrected content")
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
@patch("ara_cli.artefact_autofix.apply_autofix")
|
|
44
|
+
@patch("ara_cli.artefact_autofix.parse_report")
|
|
45
|
+
@patch("ara_cli.artefact_autofix.read_report_file")
|
|
46
|
+
def test_autofix_action(mock_read_report_file, mock_parse_report, mock_apply_autofix, capsys):
|
|
47
|
+
mock_read_report_file.return_value = "# Artefact Check Report\n\n## classifier\n- `file_path`: reason\n"
|
|
48
|
+
mock_parse_report.return_value = {"classifier": [("file_path", "reason")]}
|
|
49
|
+
|
|
50
|
+
args = MagicMock()
|
|
51
|
+
autofix_action(args)
|
|
52
|
+
|
|
53
|
+
captured = capsys.readouterr()
|
|
54
|
+
assert "Attempting to fix file_path for reason: reason" in captured.out
|
|
55
|
+
mock_apply_autofix.assert_called_once_with(
|
|
56
|
+
"file_path", "classifier", "reason")
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def test_read_artefact():
|
|
60
|
+
mock_content = "artefact content"
|
|
61
|
+
with patch("builtins.open", mock_open(read_data=mock_content)) as m:
|
|
62
|
+
content = read_artefact("file_path")
|
|
63
|
+
assert content == mock_content
|
|
64
|
+
m.assert_called_once_with("file_path", "r")
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def test_read_report_file_not_found(capsys):
|
|
68
|
+
with patch("builtins.open", side_effect=OSError("File not found")):
|
|
69
|
+
content = read_report_file()
|
|
70
|
+
captured = capsys.readouterr()
|
|
71
|
+
assert content is None
|
|
72
|
+
assert "Artefact scan results file not found" in captured.out
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def test_parse_report_no_issues():
|
|
76
|
+
content = "# Artefact Check Report\n\nNo problems found.\n"
|
|
77
|
+
issues = parse_report(content)
|
|
78
|
+
assert issues == {}
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def test_parse_report_invalid_format():
|
|
82
|
+
content = "Invalid Format"
|
|
83
|
+
issues = parse_report(content)
|
|
84
|
+
assert issues == {}
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def test_determine_artefact_type_and_class_invalid():
|
|
88
|
+
artefact_type, artefact_class = determine_artefact_type_and_class(
|
|
89
|
+
"invalid_classifier")
|
|
90
|
+
assert artefact_type is None
|
|
91
|
+
assert artefact_class is None
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def test_write_corrected_artefact():
|
|
95
|
+
corrected_content = "corrected content"
|
|
96
|
+
with patch("builtins.open", mock_open()) as m:
|
|
97
|
+
write_corrected_artefact("file_path", corrected_content)
|
|
98
|
+
m.assert_called_once_with("file_path", "w")
|
|
99
|
+
handle = m()
|
|
100
|
+
handle.write.assert_called_once_with(corrected_content)
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
@patch("ara_cli.artefact_autofix.read_artefact", return_value=None)
|
|
104
|
+
def test_apply_autofix_file_not_found(mock_read_artefact):
|
|
105
|
+
apply_autofix("file_path", "classifier", "reason")
|
|
106
|
+
mock_read_artefact.assert_called_once_with("file_path")
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
@patch("pydantic_ai.Agent")
|
|
110
|
+
def test_run_agent_exception_handling(mock_agent):
|
|
111
|
+
mock_agent.return_value.run_sync.side_effect = Exception("Agent error")
|
|
112
|
+
with pytest.raises(Exception, match="Agent error"):
|
|
113
|
+
run_agent("prompt", MagicMock())
|
ara_cli/version.py
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
# version.py
|
|
2
|
-
__version__ = "0.1.9.
|
|
2
|
+
__version__ = "0.1.9.63" # fith parameter like .0 for local install test purposes only. official numbers should be 4 digit numbers
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: ara_cli
|
|
3
|
-
Version: 0.1.9.
|
|
3
|
+
Version: 0.1.9.63
|
|
4
4
|
Requires-Dist: litellm
|
|
5
5
|
Requires-Dist: llama-index
|
|
6
6
|
Requires-Dist: llama-index-llms-openai
|
|
@@ -12,4 +12,5 @@ Requires-Dist: argparse
|
|
|
12
12
|
Requires-Dist: argcomplete
|
|
13
13
|
Requires-Dist: cmd2>=2.5
|
|
14
14
|
Requires-Dist: pydantic
|
|
15
|
+
Requires-Dist: pydantic_ai
|
|
15
16
|
Dynamic: requires-dist
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
ara_cli/__init__.py,sha256=0zl7IegxTid26EBGLav_fXZ4CCIV3H5TfAoFQiOHjvg,148
|
|
2
|
-
ara_cli/__main__.py,sha256=
|
|
3
|
-
ara_cli/
|
|
4
|
-
ara_cli/
|
|
5
|
-
ara_cli/ara_command_parser.py,sha256=HluFJimQbxS_yuZ2IzLcsfUPrmIJbKJB71YvsGiUXQE,16883
|
|
2
|
+
ara_cli/__main__.py,sha256=Z6XYWRLceIoZPvfC-X9EXouSZdtFOOe84kKVWJGA4r4,1861
|
|
3
|
+
ara_cli/ara_command_action.py,sha256=SfWcXsLjaUdOpME2qJJ0pcCSXKY5M-zbSyj0W3u_8h4,19709
|
|
4
|
+
ara_cli/ara_command_parser.py,sha256=aRxqKr0-pAuI45IuM-H4-ouc101h1_xTuEVJHXocPV0,17182
|
|
6
5
|
ara_cli/ara_config.py,sha256=_Arkr-b9XnrNHbBlFKb9tAo3OmdP4ZZiWvbY9m6Sbo0,4178
|
|
6
|
+
ara_cli/artefact_autofix.py,sha256=kkmf9hhvxqYicAm4kUDxFD1LoTr8KSPI5l12KRoZvqw,4900
|
|
7
7
|
ara_cli/artefact_creator.py,sha256=mkxKHkVIK2GdmUrKHAjKvhq66eg21S3x_cvK1ZA9DPw,5964
|
|
8
8
|
ara_cli/artefact_deleter.py,sha256=Co4wwCH3yW8H9NrOq7_2p5571EeHr0TsfE-H8KqoOfY,1900
|
|
9
9
|
ara_cli/artefact_fuzzy_search.py,sha256=XAvoiRafd1u21uKbX5-bow7hdq7uiLLy1KtxHNAFbCk,1337
|
|
@@ -11,7 +11,7 @@ ara_cli/artefact_link_updater.py,sha256=itMS_Z64jE8bBly9WA01z8PqkBeNW6ntTO7ryMeC
|
|
|
11
11
|
ara_cli/artefact_lister.py,sha256=jhk4n4eqp7hDIq07q43QzS7-36BM3OfZ4EABxCeOGcw,4764
|
|
12
12
|
ara_cli/artefact_reader.py,sha256=qNaMPWShmWtDU5LLdh9efFB27djI4NAoq6zEFwdTd38,6983
|
|
13
13
|
ara_cli/artefact_renamer.py,sha256=loIn1DF9kVnjhH7wP1v5qUvt3s0uKeWXuQPrHXenQGE,4025
|
|
14
|
-
ara_cli/artefact_scan.py,sha256=
|
|
14
|
+
ara_cli/artefact_scan.py,sha256=7PH7TE2B2Py7wGQxl7EYM3r6Fr9kVLAFtNErP0QRoXM,1950
|
|
15
15
|
ara_cli/chat.py,sha256=7xTtPEDk052_wmIzoti7GavEJ1vpRxe5c084WQ1C7dg,28617
|
|
16
16
|
ara_cli/classifier.py,sha256=zWskj7rBYdqYBGjksBm46iTgVU5IIf2PZsJr4qeiwVU,1878
|
|
17
17
|
ara_cli/codefusionretriever.py,sha256=fCHgXdIBRzkVAnapX-KI2NQ44XbrrF4tEQmn5J6clUI,1980
|
|
@@ -28,23 +28,24 @@ ara_cli/prompt_extractor.py,sha256=gidrCI8wTLfPL0ktqiXyPeGdQEB0S0sZegSOiF1Nn0Y,7
|
|
|
28
28
|
ara_cli/prompt_handler.py,sha256=PzHoIPTAWtRleOMtprhyYlFfo59S5T_kzHHkrwL-cNU,17155
|
|
29
29
|
ara_cli/prompt_rag.py,sha256=vmlt4-rSboWibwgO_KUF79TK99YXT5KXjmbD9FeWdZY,7449
|
|
30
30
|
ara_cli/run_file_lister.py,sha256=XbrrDTJXp1LFGx9Lv91SNsEHZPP-PyEMBF_P4btjbDA,2360
|
|
31
|
-
ara_cli/tag_extractor.py,sha256=
|
|
31
|
+
ara_cli/tag_extractor.py,sha256=IrjX8R8pdas3_qWuDsXXnwCVDD0uUrgqjzfqTD81t3I,2173
|
|
32
32
|
ara_cli/template_manager.py,sha256=YXPj2jGNDb-diIHFEK_vGJ-ZucodnXSGAPofKTnOofI,6633
|
|
33
33
|
ara_cli/update_config_prompt.py,sha256=PZgNIN3dTw6p80GyX8Sp5apkAhSoykwnkEbHo3IOkUo,4571
|
|
34
|
-
ara_cli/version.py,sha256=
|
|
34
|
+
ara_cli/version.py,sha256=BPYHhB5DXV3V9HaoG9jWNjJoI6MwS8Abg07dp1EjppU,146
|
|
35
35
|
ara_cli/artefact_models/artefact_load.py,sha256=dNcwZDW2Dk0bts9YnPZ0ESmWD2NbsLIvl4Z-qQeGmTQ,401
|
|
36
36
|
ara_cli/artefact_models/artefact_mapping.py,sha256=8aD0spBjkJ8toMAmFawc6UTUxB6-tEEViZXv2I-r88Q,1874
|
|
37
|
-
ara_cli/artefact_models/artefact_model.py,sha256=
|
|
37
|
+
ara_cli/artefact_models/artefact_model.py,sha256=W_jxafOd7d-1SxvB2G8dKG7_rT5t-EZo1-PnvlPHqso,14915
|
|
38
38
|
ara_cli/artefact_models/artefact_templates.py,sha256=Vd7SwoRVKNGKZmxBKS6f9FE1ThUOCqZLScu0ClPfIu8,8321
|
|
39
|
-
ara_cli/artefact_models/businessgoal_artefact_model.py,sha256=
|
|
39
|
+
ara_cli/artefact_models/businessgoal_artefact_model.py,sha256=jqYFMXjWle0YW9RvcFLDBAwy61bdT5VuDT_6lTOFzMw,4853
|
|
40
40
|
ara_cli/artefact_models/capability_artefact_model.py,sha256=SZqHx4O2mj4urn77Stnj4_Jxtlq3-LgBBU9SMkByppI,3079
|
|
41
|
-
ara_cli/artefact_models/epic_artefact_model.py,sha256=
|
|
41
|
+
ara_cli/artefact_models/epic_artefact_model.py,sha256=IadQWs6SWNcLgwvtOQWmYDyV9xLr3WwAsx-YMFan5fA,5765
|
|
42
42
|
ara_cli/artefact_models/example_artefact_model.py,sha256=UXrKbaPotg1jwcrVSdCeo-XH4tTD_-U1e3giaBn5_xg,1384
|
|
43
|
-
ara_cli/artefact_models/feature_artefact_model.py,sha256=
|
|
43
|
+
ara_cli/artefact_models/feature_artefact_model.py,sha256=TWJ7N0P2Wd8VtpbBPwJoiDnDSbHSRPPjyDfpvA-IqYc,13238
|
|
44
44
|
ara_cli/artefact_models/issue_artefact_model.py,sha256=v6CpKnkqiUh6Wch2kkEmyyW49c8ysdy1qz8l1Ft9uJA,2552
|
|
45
|
-
ara_cli/artefact_models/keyfeature_artefact_model.py,sha256=
|
|
45
|
+
ara_cli/artefact_models/keyfeature_artefact_model.py,sha256=a3MyAiePN9n_GTN6QkTvamdsaorwVUff6w-9CdRZSlo,4243
|
|
46
|
+
ara_cli/artefact_models/serialize_helper.py,sha256=0XCruO70-fyfLfTn7pnt8NrSQe79eYNUAjuQaV8K6_8,586
|
|
46
47
|
ara_cli/artefact_models/task_artefact_model.py,sha256=kHMw_Tr-Ud3EeHWpRWy4jI0xFnPzGZ-FT52c5rSrT1k,3558
|
|
47
|
-
ara_cli/artefact_models/userstory_artefact_model.py,sha256=
|
|
48
|
+
ara_cli/artefact_models/userstory_artefact_model.py,sha256=u6G8wdeE2EpOsg1OPR-s8uShB4A77GfqN0vkSSuthFI,6582
|
|
48
49
|
ara_cli/artefact_models/vision_artefact_model.py,sha256=KcNE3QQjyT29ZMMhCQo4pOcXKTkI6pXLvyfqoN2kuUQ,5920
|
|
49
50
|
ara_cli/templates/agile.artefacts,sha256=nTA8dp98HWKAD-0qhmNpVYIfkVGoJshZqMJGnphiOsE,7932
|
|
50
51
|
ara_cli/templates/template.businessgoal,sha256=3OU-y8dOCRbRsB9ovBzwFPxHSbG0dqbkok0uJnZIOd4,524
|
|
@@ -130,7 +131,8 @@ ara_cli/templates/specification_breakdown_files/template.step.md,sha256=nzDRl9Xo
|
|
|
130
131
|
ara_cli/templates/specification_breakdown_files/template.technology.exploration.md,sha256=zQyiJcmbUfXdte-5uZwZUpT6ey0zwfZ00P4VwI97jQk,2274
|
|
131
132
|
ara_cli/templates/specification_breakdown_files/template.technology.md,sha256=bySiksz-8xtq0Nnj4svqe2MgUftWrVkbK9AcrDUE3KY,952
|
|
132
133
|
ara_cli/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
133
|
-
ara_cli/tests/
|
|
134
|
+
ara_cli/tests/test_ara_autofix.py,sha256=8AaSuherD2dvo2wMFafgHnkLhSnMs6ZW6RtX-5VKsls,4248
|
|
135
|
+
ara_cli/tests/test_ara_command_action.py,sha256=s7XlNMX1lohnwlRwdCHMKFpJdM7BxyEjYuOcba2BClw,25494
|
|
134
136
|
ara_cli/tests/test_ara_config.py,sha256=1LWby_iSestTIIqK-1clggL8kmbGGbtlYfsxAHaMMF8,2232
|
|
135
137
|
ara_cli/tests/test_artefact_fuzzy_search.py,sha256=5Sh3_l9QK8-WHn6JpGPU1b6h4QEnl2JoMq1Tdp2cj1U,1261
|
|
136
138
|
ara_cli/tests/test_artefact_link_updater.py,sha256=gN5KFF1uY7OoBh8Mr5jWpqXp02YCU5OSIpSU76Rm4Gs,2137
|
|
@@ -148,8 +150,8 @@ ara_cli/tests/test_list_filter.py,sha256=gSRKirTtFuhRS3QlFHqWl89WvCvAdVEnFsCWTYm
|
|
|
148
150
|
ara_cli/tests/test_tag_extractor.py,sha256=n2xNApbDciqKO3QuaveEWSPXU1PCUa_EhxlZMrukONw,2074
|
|
149
151
|
ara_cli/tests/test_template_manager.py,sha256=bRxka6cxHsCAOvXjfG8MrVO8qSZXhxW01tnph80UtNk,3143
|
|
150
152
|
ara_cli/tests/test_update_config_prompt.py,sha256=vSsLvc18HZdVjVM93qXWVbJt752xTLL6VGjSVCrPufk,6729
|
|
151
|
-
ara_cli-0.1.9.
|
|
152
|
-
ara_cli-0.1.9.
|
|
153
|
-
ara_cli-0.1.9.
|
|
154
|
-
ara_cli-0.1.9.
|
|
155
|
-
ara_cli-0.1.9.
|
|
153
|
+
ara_cli-0.1.9.63.dist-info/METADATA,sha256=gxNdNrzLFrDWHMMskXW_kMFj0qBeoxoyFxilCHDsQiQ,415
|
|
154
|
+
ara_cli-0.1.9.63.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
155
|
+
ara_cli-0.1.9.63.dist-info/entry_points.txt,sha256=v4h7MzysTgSIDYfEo3oj4Kz_8lzsRa3hq-KJHEcLVX8,45
|
|
156
|
+
ara_cli-0.1.9.63.dist-info/top_level.txt,sha256=zzee_PwFmKqfBi9XgIunP6xy2S4TIt593CLLxenNaAE,8
|
|
157
|
+
ara_cli-0.1.9.63.dist-info/RECORD,,
|
ara_cli/analyse_artefacts.py
DELETED
|
@@ -1,133 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env python3
|
|
2
|
-
"""
|
|
3
|
-
analyse_artefacts.py
|
|
4
|
-
|
|
5
|
-
Walk a directory tree, try to deserialize every *.vision *.epic *.task … file
|
|
6
|
-
with the appropriate Pydantic artefact model, and write the list of files that
|
|
7
|
-
fail validation (plus the reason) to 'invalid_artefacts.txt'.
|
|
8
|
-
"""
|
|
9
|
-
|
|
10
|
-
"""
|
|
11
|
-
python analyse_artefacts.py <path_to_ara_folder ex: ./ara/>
|
|
12
|
-
"""
|
|
13
|
-
|
|
14
|
-
from ara_cli.artefact_models import businessgoal_artefact_model, capability_artefact_model, epic_artefact_model, example_artefact_model, feature_artefact_model, issue_artefact_model, keyfeature_artefact_model, userstory_artefact_model, task_artefact_model, vision_artefact_model
|
|
15
|
-
from ara_cli.artefact_models.artefact_model import Artefact, ArtefactType
|
|
16
|
-
from pydantic import ValidationError
|
|
17
|
-
from typing import Dict, Type, List, Tuple
|
|
18
|
-
from pathlib import Path
|
|
19
|
-
import os
|
|
20
|
-
import sys
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
# --- import your domain model ----------------------------------------------
|
|
24
|
-
# Make sure this import path matches your project layout.
|
|
25
|
-
# (e.g. from ara_cli.artefact_model import Artefact, ArtefactType)
|
|
26
|
-
# ---------------------------------------------------------------------------
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
def build_type_map() -> Dict[ArtefactType, Type[Artefact]]:
|
|
30
|
-
type_map: Dict[ArtefactType, Type[Artefact]] = {}
|
|
31
|
-
queue: List[Type[Artefact]] = list(Artefact.__subclasses__())
|
|
32
|
-
while queue:
|
|
33
|
-
cls = queue.pop()
|
|
34
|
-
try:
|
|
35
|
-
artefact_type = cls._artefact_type()
|
|
36
|
-
type_map[artefact_type] = cls
|
|
37
|
-
except Exception:
|
|
38
|
-
pass # abstract / helper subclass
|
|
39
|
-
queue.extend(cls.__subclasses__())
|
|
40
|
-
if not type_map:
|
|
41
|
-
raise RuntimeError("No concrete Artefact subclasses found!")
|
|
42
|
-
return type_map
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
def find_artefact_files(root: Path, valid_exts: List[str]) -> List[Path]:
|
|
46
|
-
return [
|
|
47
|
-
p for p in root.rglob("*")
|
|
48
|
-
if p.is_file() and p.suffix.lstrip(".") in valid_exts
|
|
49
|
-
]
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
def scan_folder(
|
|
53
|
-
root_folder: Path,
|
|
54
|
-
detailed_report: Path,
|
|
55
|
-
names_only_report: Path,
|
|
56
|
-
checklist_report: Path
|
|
57
|
-
) -> Tuple[int, int]:
|
|
58
|
-
type_map = build_type_map()
|
|
59
|
-
valid_exts = [t.value for t in type_map]
|
|
60
|
-
|
|
61
|
-
artefact_files = find_artefact_files(root_folder, valid_exts)
|
|
62
|
-
bad: List[Tuple[Path, str]] = []
|
|
63
|
-
|
|
64
|
-
for file_path in artefact_files:
|
|
65
|
-
artefact_type = ArtefactType(file_path.suffix.lstrip("."))
|
|
66
|
-
artefact_cls = type_map[artefact_type]
|
|
67
|
-
text = file_path.read_text(encoding="utf-8")
|
|
68
|
-
|
|
69
|
-
try:
|
|
70
|
-
artefact_cls.deserialize(text)
|
|
71
|
-
except (ValidationError, ValueError, AssertionError) as e:
|
|
72
|
-
bad.append((file_path, str(e)))
|
|
73
|
-
except Exception as e:
|
|
74
|
-
bad.append((file_path, f"Unexpected error: {e!r}"))
|
|
75
|
-
|
|
76
|
-
# ───────────── write reports ────────────────────────────────────────────
|
|
77
|
-
if bad:
|
|
78
|
-
# 1) detailed txt
|
|
79
|
-
with detailed_report.open("w", encoding="utf-8") as f:
|
|
80
|
-
f.write("Invalid artefacts (file → reason)\n\n")
|
|
81
|
-
for path, err in bad:
|
|
82
|
-
f.write(f"{path} --> {err}\n")
|
|
83
|
-
|
|
84
|
-
# 2) names-only txt
|
|
85
|
-
with names_only_report.open("w", encoding="utf-8") as f:
|
|
86
|
-
for path, _ in bad:
|
|
87
|
-
f.write(f"{path}\n")
|
|
88
|
-
|
|
89
|
-
# 3) markdown checklist
|
|
90
|
-
with checklist_report.open("w", encoding="utf-8") as f:
|
|
91
|
-
f.write("# 📋 Artefact-fix checklist\n\n")
|
|
92
|
-
f.write("Tick a box once you’ve fixed & validated the file.\n\n")
|
|
93
|
-
for path, err in bad:
|
|
94
|
-
f.write(f"- [ ] `{path}` – {err}\n")
|
|
95
|
-
|
|
96
|
-
print(
|
|
97
|
-
f"\nFinished. {len(bad)}/{len(artefact_files)} files are invalid."
|
|
98
|
-
f"\nReports generated:"
|
|
99
|
-
f"\n • {detailed_report}"
|
|
100
|
-
f"\n • {names_only_report}"
|
|
101
|
-
f"\n • {checklist_report}"
|
|
102
|
-
)
|
|
103
|
-
else:
|
|
104
|
-
print(f"\nFinished. All {len(artefact_files)} artefacts are valid ✔️")
|
|
105
|
-
# clean up stale files from previous runs
|
|
106
|
-
for p in (detailed_report, names_only_report, checklist_report):
|
|
107
|
-
if p.exists():
|
|
108
|
-
p.unlink()
|
|
109
|
-
|
|
110
|
-
return len(artefact_files), len(bad)
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
# ─────────────────────────────── main ──────────────────────────────────────
|
|
114
|
-
def main() -> None:
|
|
115
|
-
if len(sys.argv) < 2:
|
|
116
|
-
print("Usage: python scan_artefacts.py <folder_to_scan>")
|
|
117
|
-
sys.exit(1)
|
|
118
|
-
|
|
119
|
-
root_folder = Path(sys.argv[1]).expanduser().resolve()
|
|
120
|
-
if not root_folder.is_dir():
|
|
121
|
-
print(f"Error: '{root_folder}' is not a directory.")
|
|
122
|
-
sys.exit(1)
|
|
123
|
-
|
|
124
|
-
scan_folder(
|
|
125
|
-
root_folder=root_folder,
|
|
126
|
-
detailed_report=Path("invalid_artefacts.txt"),
|
|
127
|
-
names_only_report=Path("invalid_artefact_names.txt"),
|
|
128
|
-
checklist_report=Path("invalid_artefacts_checklist.md"),
|
|
129
|
-
)
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
if __name__ == "__main__":
|
|
133
|
-
main()
|
|
File without changes
|
|
File without changes
|
|
File without changes
|