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

@@ -12,6 +12,15 @@ import difflib
12
12
  import os
13
13
 
14
14
 
15
+ def populate_classified_artefact_info(
16
+ classified_artefact_info: Optional[dict], force: bool = False
17
+ ):
18
+ if not classified_artefact_info or force:
19
+ file_classifier = FileClassifier(os)
20
+ classified_artefact_info = file_classifier.classify_files()
21
+ return classified_artefact_info
22
+
23
+
15
24
  def read_report_file():
16
25
  file_path = "incompatible_artefacts_report.md"
17
26
  try:
@@ -30,6 +39,7 @@ def parse_report(content: str) -> Dict[str, List[Tuple[str, str]]]:
30
39
  Parses the incompatible artefacts report and returns structured data.
31
40
  Returns a dictionary where keys are artefact classifiers, and values are lists of (file_path, reason) tuples.
32
41
  """
42
+
33
43
  def is_valid_report(lines: List[str]) -> bool:
34
44
  return bool(lines) and lines[0] == "# Artefact Check Report"
35
45
 
@@ -219,7 +229,7 @@ def _has_valid_contribution(artefact: Artefact) -> bool:
219
229
 
220
230
 
221
231
  def _update_rule(
222
- artefact: Artefact, name: str, classifier: str, classified_file_info: dict
232
+ artefact: Artefact, name: str, classifier: str, classified_file_info: dict, delete_if_not_found: bool = False
223
233
  ) -> None:
224
234
  """Updates the rule in the contribution if a close match is found."""
225
235
  rule = artefact.contribution.rule
@@ -234,8 +244,12 @@ def _update_rule(
234
244
  rules = parent.rules
235
245
 
236
246
  closest_rule_match = difflib.get_close_matches(rule, rules, cutoff=0.5)
237
- if closest_rule_match:
238
- artefact.contribution.rule = closest_rule_match[0]
247
+ if not closest_rule_match and delete_if_not_found:
248
+ artefact.contribution.rule = None
249
+ return
250
+ if not closest_rule_match:
251
+ return
252
+ artefact.contribution.rule = closest_rule_match[0]
239
253
 
240
254
 
241
255
  def _set_contribution_multiple_matches(
@@ -281,9 +295,7 @@ def set_closest_contribution(
281
295
  classifier = contribution.classifier
282
296
  rule = contribution.rule
283
297
 
284
- if not classified_file_info:
285
- file_classifier = FileClassifier(os)
286
- classified_file_info = file_classifier.classify_files()
298
+ classified_file_info = populate_classified_artefact_info(classified_artefact_info=classified_file_info)
287
299
 
288
300
  all_artefact_names = extract_artefact_names_of_classifier(
289
301
  classified_files=classified_file_info, classifier=classifier
@@ -299,7 +311,9 @@ def set_closest_contribution(
299
311
  if not name or not classifier:
300
312
  artefact.contribution = None
301
313
  return artefact, True
302
- print(f"Updating contribution of {artefact._artefact_type().value} '{artefact.title}' to {classifier} '{name}'")
314
+ print(
315
+ f"Updating contribution of {artefact._artefact_type().value} '{artefact.title}' to {classifier} '{name}'"
316
+ )
303
317
  contribution.artefact_name = name
304
318
  contribution.classifier = classifier
305
319
  artefact.contribution = contribution
@@ -378,100 +392,118 @@ def fix_contribution(
378
392
  classified_artefact_info: dict,
379
393
  **kwargs,
380
394
  ):
381
- if not classified_artefact_info:
382
- file_classifier = FileClassifier(os)
383
- classified_artefact_info = file_classifier.classify_files()
395
+ classified_artefact_info = populate_classified_artefact_info(classified_artefact_info=classified_artefact_info)
384
396
  artefact = artefact_class.deserialize(artefact_text)
385
397
  artefact, _ = set_closest_contribution(artefact)
386
398
  artefact_text = artefact.serialize()
387
399
  return artefact_text
388
400
 
389
401
 
390
- def apply_autofix(
402
+ def fix_rule(
391
403
  file_path: str,
392
- classifier: str,
393
- reason: str,
394
- single_pass: bool = False,
395
- deterministic: bool = True,
396
- non_deterministic: bool = True,
397
- classified_artefact_info: Optional[Dict[str, List[Dict[str, str]]]] = None,
398
- ) -> bool:
399
- """
400
- Applies fixes to a single artefact file iteratively until it is valid
401
- or a fix cannot be applied. If single_pass is True, it runs for only one attempt.
402
- """
403
- deterministic_markers_to_functions = {
404
- "Filename-Title Mismatch": fix_title_mismatch,
405
- "Invalid Contribution Reference": fix_contribution,
406
- }
404
+ artefact_text: str,
405
+ artefact_class: str,
406
+ classified_artefact_info: dict,
407
+ **kwargs,
408
+ ):
409
+ classified_artefact_info = populate_classified_artefact_info(classified_artefact_info=classified_artefact_info)
410
+ artefact = artefact_class.deserialize(artefact_text)
411
+ contribution = artefact.contribution
412
+ assert contribution is not None
413
+ _update_rule(
414
+ artefact=artefact,
415
+ name=contribution.artefact_name,
416
+ classifier=contribution.classifier,
417
+ classified_file_info=classified_artefact_info,
418
+ delete_if_not_found=True
419
+ )
420
+ feedback_message = (f"Updating contribution of {artefact._artefact_type().value} "
421
+ f"'{artefact.title}' to {contribution.classifier} "
422
+ f"'{contribution.artefact_name}' ")
423
+ rule = contribution.rule
424
+ if rule:
425
+ feedback_message += f"with rule '{rule}'"
426
+ else:
427
+ feedback_message += "without a rule"
428
+ print(feedback_message)
429
+ return artefact.serialize()
430
+
431
+
432
+ def should_skip_issue(deterministic_issue, deterministic, non_deterministic, file_path) -> bool:
433
+ if not non_deterministic and not deterministic_issue:
434
+ print(f"Skipping non-deterministic fix for {file_path} as per request.")
435
+ return True
436
+ if not deterministic and deterministic_issue:
437
+ print(f"Skipping fix for {file_path} as per request flags.")
438
+ return True
439
+ return False
407
440
 
408
- def populate_classified_artefact_info(force: bool = False):
409
- nonlocal classified_artefact_info
410
- if force or classified_artefact_info is None:
411
- file_classifier = FileClassifier(os)
412
- classified_artefact_info = file_classifier.classify_files()
413
-
414
- def determine_attempt_count() -> int:
415
- nonlocal single_pass, file_path
416
- if single_pass:
417
- print(f"Single-pass mode enabled for {file_path}. Running for 1 attempt.")
418
- return 1
419
- return 3
420
-
421
- def apply_deterministic_fix() -> str:
422
- nonlocal deterministic, deterministic_issue, corrected_text, file_path, artefact_text, artefact_class, classified_artefact_info
423
- if deterministic and deterministic_issue:
424
- print(f"Applying deterministic fix for '{deterministic_issue}'...")
425
- fix_function = deterministic_markers_to_functions[deterministic_issue]
426
- return fix_function(
427
- file_path=file_path,
428
- artefact_text=artefact_text,
429
- artefact_class=artefact_class,
430
- classified_artefact_info=classified_artefact_info,
431
- )
432
- return corrected_text
433
-
434
- def apply_non_deterministic_fix() -> Optional[str]:
435
- """
436
- Applies LLM fix. Return None in case of an exception
437
- """
438
- nonlocal non_deterministic, deterministic_issue, corrected_text, artefact_type, current_reason, file_path, artefact_text
439
- if non_deterministic and not deterministic_issue:
440
- print("Applying non-deterministic (LLM) fix...")
441
- prompt = construct_prompt(artefact_type, current_reason, file_path, artefact_text)
442
- try:
443
- corrected_artefact = run_agent(prompt, artefact_class)
444
- corrected_text = corrected_artefact.serialize()
445
- except Exception as e:
446
- print(f" ❌ LLM agent failed to fix artefact at {file_path}: {e}")
447
- return None
448
- return corrected_text
449
-
450
- def should_skip() -> bool:
451
- nonlocal deterministic_issue, deterministic, non_deterministic
452
- if not non_deterministic and not deterministic_issue:
453
- print(f"Skipping non-deterministic fix for {file_path} as per request.")
454
- return True
455
- if not deterministic and deterministic_issue:
456
- print(f"Skipping fix for {file_path} as per request flags.")
457
- return True
458
- return False
441
+ def determine_attempt_count(single_pass, file_path) -> int:
442
+ if single_pass:
443
+ print(f"Single-pass mode enabled for {file_path}. Running for 1 attempt.")
444
+ return 1
445
+ return 3
459
446
 
460
- artefact_type, artefact_class = determine_artefact_type_and_class(classifier)
461
- if artefact_type is None or artefact_class is None:
462
- return False
447
+ def apply_deterministic_fix(
448
+ deterministic, deterministic_issue, file_path, artefact_text, artefact_class, classified_artefact_info,
449
+ deterministic_markers_to_functions, corrected_text
450
+ ) -> str:
451
+ if deterministic and deterministic_issue:
452
+ print(f"Applying deterministic fix for '{deterministic_issue}'...")
453
+ fix_function = deterministic_markers_to_functions[deterministic_issue]
454
+ return fix_function(
455
+ file_path=file_path,
456
+ artefact_text=artefact_text,
457
+ artefact_class=artefact_class,
458
+ classified_artefact_info=classified_artefact_info,
459
+ )
460
+ return corrected_text
463
461
 
464
- populate_classified_artefact_info()
465
- max_attempts = determine_attempt_count()
462
+ def apply_non_deterministic_fix(
463
+ non_deterministic, deterministic_issue, corrected_text,
464
+ artefact_type, current_reason, file_path, artefact_text, artefact_class
465
+ ) -> Optional[str]:
466
+ """
467
+ Applies LLM fix. Return None in case of an exception
468
+ """
469
+ if non_deterministic and not deterministic_issue:
470
+ print("Applying non-deterministic (LLM) fix...")
471
+ prompt = construct_prompt(
472
+ artefact_type, current_reason, file_path, artefact_text
473
+ )
474
+ try:
475
+ corrected_artefact = run_agent(prompt, artefact_class)
476
+ corrected_text = corrected_artefact.serialize()
477
+ except Exception as e:
478
+ print(f" ❌ LLM agent failed to fix artefact at {file_path}: {e}")
479
+ return None
480
+ return corrected_text
466
481
 
482
+ def attempt_autofix_loop(
483
+ file_path: str,
484
+ artefact_type,
485
+ artefact_class,
486
+ deterministic_markers_to_functions,
487
+ max_attempts,
488
+ deterministic: bool,
489
+ non_deterministic: bool,
490
+ classified_artefact_info: Optional[Dict[str, List[Dict[str, str]]]],
491
+ ) -> bool:
492
+ """
493
+ Attempts to fix the artefact in a loop, up to max_attempts.
494
+ """
467
495
  for attempt in range(max_attempts):
468
- is_valid, current_reason = check_file(file_path, artefact_class, classified_artefact_info)
496
+ is_valid, current_reason = check_file(
497
+ file_path, artefact_class, classified_artefact_info
498
+ )
469
499
 
470
500
  if is_valid:
471
501
  print(f"✅ Artefact at {file_path} is now valid.")
472
502
  return True
473
503
 
474
- print(f"Attempting to fix {file_path} (Attempt {attempt + 1}/{max_attempts})...")
504
+ print(
505
+ f"Attempting to fix {file_path} (Attempt {attempt + 1}/{max_attempts})..."
506
+ )
475
507
  print(f" Reason: {current_reason}")
476
508
 
477
509
  artefact_text = read_artefact(file_path)
@@ -487,22 +519,68 @@ def apply_autofix(
487
519
  None,
488
520
  )
489
521
 
490
- if should_skip():
522
+ if should_skip_issue(deterministic_issue, deterministic, non_deterministic, file_path):
491
523
  return False
492
524
 
493
525
  corrected_text = None
494
526
 
495
- corrected_text = apply_deterministic_fix()
496
- corrected_text = apply_non_deterministic_fix()
527
+ corrected_text = apply_deterministic_fix(
528
+ deterministic, deterministic_issue, file_path, artefact_text,
529
+ artefact_class, classified_artefact_info,
530
+ deterministic_markers_to_functions, corrected_text
531
+ )
532
+ corrected_text = apply_non_deterministic_fix(
533
+ non_deterministic, deterministic_issue, corrected_text,
534
+ artefact_type, current_reason, file_path, artefact_text, artefact_class
535
+ )
497
536
 
498
537
  if corrected_text is None or corrected_text.strip() == artefact_text.strip():
499
- print(" Fixing attempt did not alter the file. Stopping to prevent infinite loop.")
538
+ print(
539
+ " Fixing attempt did not alter the file. Stopping to prevent infinite loop."
540
+ )
500
541
  return False
501
542
 
502
543
  write_corrected_artefact(file_path, corrected_text)
503
544
 
504
545
  print(" File modified. Re-classifying artefact information for next check...")
505
- populate_classified_artefact_info(force=True)
546
+ classified_artefact_info = populate_classified_artefact_info(classified_artefact_info, force=True)
506
547
 
507
548
  print(f"❌ Failed to fix {file_path} after {max_attempts} attempts.")
508
549
  return False
550
+
551
+ def apply_autofix(
552
+ file_path: str,
553
+ classifier: str,
554
+ reason: str,
555
+ single_pass: bool = False,
556
+ deterministic: bool = True,
557
+ non_deterministic: bool = True,
558
+ classified_artefact_info: Optional[Dict[str, List[Dict[str, str]]]] = None,
559
+ ) -> bool:
560
+ """
561
+ Applies fixes to a single artefact file iteratively until it is valid
562
+ or a fix cannot be applied. If single_pass is True, it runs for only one attempt.
563
+ """
564
+ deterministic_markers_to_functions = {
565
+ "Filename-Title Mismatch": fix_title_mismatch,
566
+ "Invalid Contribution Reference": fix_contribution,
567
+ "Rule Mismatch": fix_rule,
568
+ }
569
+
570
+ artefact_type, artefact_class = determine_artefact_type_and_class(classifier)
571
+ if artefact_type is None or artefact_class is None:
572
+ return False
573
+
574
+ classified_artefact_info = populate_classified_artefact_info(classified_artefact_info)
575
+ max_attempts = determine_attempt_count(single_pass, file_path)
576
+
577
+ return attempt_autofix_loop(
578
+ file_path=file_path,
579
+ artefact_type=artefact_type,
580
+ artefact_class=artefact_class,
581
+ deterministic_markers_to_functions=deterministic_markers_to_functions,
582
+ max_attempts=max_attempts,
583
+ deterministic=deterministic,
584
+ non_deterministic=non_deterministic,
585
+ classified_artefact_info=classified_artefact_info,
586
+ )
@@ -2,9 +2,17 @@ from ara_cli.artefact_models.artefact_mapping import title_prefix_to_artefact_cl
2
2
 
3
3
 
4
4
  def artefact_from_content(content):
5
- relevant_lines = content.splitlines()[:2]
6
- for line in relevant_lines:
5
+ lines = content.splitlines()
6
+
7
+ # Look through more lines to find the title, skipping empty lines
8
+ for line in lines:
9
+ line = line.strip()
10
+ if not line: # Skip empty lines
11
+ continue
12
+ if line.startswith('@'): # Skip tag lines
13
+ continue
14
+
7
15
  for prefix, artefact_class in title_prefix_to_artefact_class.items():
8
- if line.strip().startswith(prefix):
16
+ if line.startswith(prefix):
9
17
  return artefact_class.deserialize(content)
10
18
  return None
@@ -74,47 +74,55 @@ class TaskArtefact(Artefact):
74
74
  artefact_type: ArtefactType = ArtefactType.task
75
75
  action_items: List[ActionItem] = Field(default_factory=list)
76
76
 
77
+ @classmethod
78
+ def _is_action_item_start(cls, line: str) -> bool:
79
+ return line.startswith('[@')
80
+
81
+ @classmethod
82
+ def _is_section_start(cls, line: str, description_marker: str, contribution_marker: str) -> bool:
83
+ return (
84
+ line.startswith(description_marker) or
85
+ line.startswith(contribution_marker)
86
+ )
87
+
88
+ @classmethod
89
+ def _collect_action_item_lines(cls, lines, start_idx, description_marker, contribution_marker):
90
+ action_item_lines = [lines[start_idx]]
91
+ j = start_idx + 1
92
+ while j < len(lines):
93
+ next_line = lines[j]
94
+ if (
95
+ cls._is_action_item_start(next_line) or
96
+ cls._is_section_start(next_line, description_marker, contribution_marker)
97
+ ):
98
+ break
99
+ action_item_lines.append(next_line)
100
+ j += 1
101
+ return action_item_lines, j
102
+
77
103
  @classmethod
78
104
  def _deserialize_action_items(cls, text) -> Tuple[List[ActionItem], List[str]]:
79
105
  lines = [line.strip() for line in text.strip().splitlines() if line.strip()]
80
-
81
106
  action_items = []
82
107
  remaining_lines = []
83
108
  i = 0
84
-
85
109
  contribution_marker = cls._contribution_starts_with()
86
110
  description_marker = cls._description_starts_with()
87
111
 
88
112
  while i < len(lines):
89
113
  line = lines[i]
90
-
91
- if line.startswith('[@'):
92
- # Collect all lines for this action item
93
- action_item_lines = [line]
94
- j = i + 1
95
- # Collect lines until we hit another action item or a known section
96
- while j < len(lines):
97
- next_line = lines[j]
98
- # Check if next line is a new action item or a known section
99
- if (next_line.startswith('[@') or
100
- next_line.startswith(description_marker) or
101
- next_line.startswith(contribution_marker)):
102
- break
103
- action_item_lines.append(next_line)
104
- j += 1
105
-
106
- # Join all lines and pass as a single string to deserialize
114
+ if cls._is_action_item_start(line):
115
+ action_item_lines, next_idx = cls._collect_action_item_lines(
116
+ lines, i, description_marker, contribution_marker
117
+ )
107
118
  action_item_text = '\n'.join(action_item_lines)
108
119
  try:
109
120
  action_item = ActionItem.deserialize(action_item_text)
110
121
  if action_item:
111
122
  action_items.append(action_item)
112
123
  except ValueError as e:
113
- # Re-raise with more context about where the error occurred
114
124
  raise ValueError(f"Error parsing action item: {e}")
115
-
116
- # Move index to the next unprocessed line
117
- i = j
125
+ i = next_idx
118
126
  else:
119
127
  remaining_lines.append(line)
120
128
  i += 1
@@ -94,7 +94,7 @@ class ArtefactReader:
94
94
  return artefacts
95
95
 
96
96
  @staticmethod
97
- def find_children(artefact_name, classifier, artefacts_by_classifier={}, classified_artefacts=None):
97
+ def find_children(artefact_name, classifier, artefacts_by_classifier=None, classified_artefacts=None):
98
98
  artefacts_by_classifier = artefacts_by_classifier or {}
99
99
  filtered_artefacts = {k: [] for k in artefacts_by_classifier.keys()}
100
100
 
@@ -103,73 +103,97 @@ class ArtefactReader:
103
103
 
104
104
  for artefact_classifier, artefacts in classified_artefacts.items():
105
105
  for artefact in artefacts:
106
- if not isinstance(artefact, Artefact):
107
- continue
108
-
109
- try:
110
- contribution = artefact.contribution
111
- if (contribution and
112
- contribution.artefact_name == artefact_name and
113
- contribution.classifier == classifier):
106
+ ArtefactReader._process_artefact(
107
+ artefact, artefact_name, classifier, filtered_artefacts
108
+ )
114
109
 
115
- file_classifier = artefact._file_path.split('.')[-1]
116
-
117
- if file_classifier not in filtered_artefacts:
118
- filtered_artefacts[file_classifier] = []
119
- filtered_artefacts[file_classifier].append(artefact)
110
+ return ArtefactReader.merge_dicts(artefacts_by_classifier, filtered_artefacts)
120
111
 
121
- except AttributeError as e:
122
- continue
112
+ @staticmethod
113
+ def _process_artefact(artefact, artefact_name, classifier, filtered_artefacts):
114
+ if not isinstance(artefact, Artefact):
115
+ return
116
+ contribution = getattr(artefact, 'contribution', None)
117
+ if not contribution:
118
+ return
119
+ if getattr(contribution, 'artefact_name', None) != artefact_name:
120
+ return
121
+ if getattr(contribution, 'classifier', None) != classifier:
122
+ return
123
123
 
124
- return ArtefactReader.merge_dicts(artefacts_by_classifier, filtered_artefacts)
124
+ file_classifier = getattr(artefact, '_file_path', '').split('.')[-1]
125
+ if file_classifier not in filtered_artefacts:
126
+ filtered_artefacts[file_classifier] = []
127
+ filtered_artefacts[file_classifier].append(artefact)
125
128
 
126
129
  @staticmethod
127
130
  def step_through_value_chain(
128
131
  artefact_name,
129
132
  classifier,
130
- artefacts_by_classifier={},
131
- classified_artefacts: dict[str, list[Artefact]] | None = None
133
+ artefacts_by_classifier=None,
134
+ classified_artefacts: dict[str, list['Artefact']] | None = None
132
135
  ):
133
136
  from ara_cli.artefact_models.artefact_load import artefact_from_content
134
137
 
138
+ artefacts_by_classifier = artefacts_by_classifier or {}
139
+
135
140
  if classified_artefacts is None:
136
141
  classified_artefacts = ArtefactReader.read_artefacts()
137
142
 
138
- if classifier not in artefacts_by_classifier:
139
- artefacts_by_classifier[classifier] = []
143
+ ArtefactReader._ensure_classifier_key(classifier, artefacts_by_classifier)
140
144
 
141
- artefact = next(filter(
142
- lambda x: x.title == artefact_name, classified_artefacts[classifier]
143
- ))
145
+ artefact = ArtefactReader._find_artefact_by_name(
146
+ artefact_name,
147
+ classified_artefacts.get(classifier, [])
148
+ )
144
149
 
145
- if not artefact:
146
- return
147
- if artefact in artefacts_by_classifier[classifier]:
150
+ if not artefact or artefact in artefacts_by_classifier[classifier]:
148
151
  return
149
152
 
150
153
  artefacts_by_classifier[classifier].append(artefact)
151
154
 
152
- parent = artefact.contribution
153
- if parent and parent.artefact_name and parent.classifier:
154
- parent_name = parent.artefact_name
155
- parent_classifier = parent.classifier
156
-
157
- parent_classifier_artefacts = classified_artefacts[parent_classifier]
158
- all_artefact_names = [x.title for x in parent_classifier_artefacts]
159
-
160
- if parent_name not in all_artefact_names:
161
- if parent_name is not None:
162
- suggest_close_name_matches_for_parent(
163
- artefact_name,
164
- all_artefact_names,
165
- parent_name
166
- )
167
- print()
168
- return
169
-
170
- ArtefactReader.step_through_value_chain(
171
- artefact_name=parent_name,
172
- classifier=parent_classifier,
173
- artefacts_by_classifier=artefacts_by_classifier,
174
- classified_artefacts=classified_artefacts
155
+ parent = getattr(artefact, 'contribution', None)
156
+ if not ArtefactReader._has_valid_parent(parent):
157
+ return
158
+
159
+ parent_name = parent.artefact_name
160
+ parent_classifier = parent.classifier
161
+
162
+ parent_classifier_artefacts = classified_artefacts.get(parent_classifier, [])
163
+ all_artefact_names = [x.title for x in parent_classifier_artefacts]
164
+
165
+ if parent_name not in all_artefact_names:
166
+ ArtefactReader._suggest_parent_name_match(
167
+ artefact_name, all_artefact_names, parent_name
168
+ )
169
+ print()
170
+ return
171
+
172
+ ArtefactReader.step_through_value_chain(
173
+ artefact_name=parent_name,
174
+ classifier=parent_classifier,
175
+ artefacts_by_classifier=artefacts_by_classifier,
176
+ classified_artefacts=classified_artefacts
177
+ )
178
+
179
+ @staticmethod
180
+ def _ensure_classifier_key(classifier, artefacts_by_classifier):
181
+ if classifier not in artefacts_by_classifier:
182
+ artefacts_by_classifier[classifier] = []
183
+
184
+ @staticmethod
185
+ def _find_artefact_by_name(artefact_name, artefacts):
186
+ return next((x for x in artefacts if x.title == artefact_name), None)
187
+
188
+ @staticmethod
189
+ def _has_valid_parent(parent):
190
+ return parent and getattr(parent, 'artefact_name', None) and getattr(parent, 'classifier', None)
191
+
192
+ @staticmethod
193
+ def _suggest_parent_name_match(artefact_name, all_artefact_names, parent_name):
194
+ if parent_name is not None:
195
+ suggest_close_name_matches_for_parent(
196
+ artefact_name,
197
+ all_artefact_names,
198
+ parent_name
175
199
  )