ryanoakley commited on
Commit
5b6eb3b
·
verified ·
1 Parent(s): 79da033

Update src/aibom-generator/utils.py

Browse files
Files changed (1) hide show
  1. src/aibom-generator/utils.py +277 -56
src/aibom-generator/utils.py CHANGED
@@ -304,19 +304,19 @@ def determine_completeness_profile(aibom: Dict[str, Any], score: float) -> Dict[
304
  # Return the highest satisfied profile
305
  if "advanced" in satisfied_profiles:
306
  return {
307
- "name": "advanced",
308
  "description": COMPLETENESS_PROFILES["advanced"]["description"],
309
  "satisfied": True
310
  }
311
  elif "standard" in satisfied_profiles:
312
  return {
313
- "name": "standard",
314
  "description": COMPLETENESS_PROFILES["standard"]["description"],
315
  "satisfied": True
316
  }
317
  elif "basic" in satisfied_profiles:
318
  return {
319
- "name": "basic",
320
  "description": COMPLETENESS_PROFILES["basic"]["description"],
321
  "satisfied": True
322
  }
@@ -329,6 +329,7 @@ def determine_completeness_profile(aibom: Dict[str, Any], score: float) -> Dict[
329
 
330
 
331
  def apply_completeness_penalties(original_score: float, missing_fields: Dict[str, List[str]]) -> Dict[str, Any]:
 
332
  """
333
  Apply penalties based on missing critical fields.
334
 
@@ -339,24 +340,26 @@ def apply_completeness_penalties(original_score: float, missing_fields: Dict[str
339
  Returns:
340
  Dictionary with penalty information
341
  """
 
 
342
  # Count missing fields by tier
343
  missing_critical_count = len(missing_fields["critical"])
344
  missing_important_count = len(missing_fields["important"])
345
 
 
 
 
346
  # Calculate penalty based on missing critical fields
347
  if missing_critical_count > 3:
348
- penalty_factor = 0.8 # 20% penalty
349
  penalty_reason = "Multiple critical fields missing"
350
- elif missing_critical_count > 0:
351
- penalty_factor = 0.9 # 10% penalty
352
  penalty_reason = "Some critical fields missing"
353
- elif missing_important_count > 5:
354
- penalty_factor = 0.95 # 5% penalty
 
355
  penalty_reason = "Several important fields missing"
356
- else:
357
- # No penalty
358
- penalty_factor = 1.0
359
- penalty_reason = None
360
 
361
  adjusted_score = original_score * penalty_factor
362
 
@@ -735,7 +738,7 @@ def _generate_validation_recommendations(issues: List[Dict[str, Any]]) -> List[s
735
  recommendations.append("Add ethical considerations, limitations, and risks to the model card")
736
 
737
  if "MISSING_METADATA" in issue_codes:
738
- recommendations.append("Add metadata section to the AIBOM")
739
 
740
  if "MISSING_TOOLS" in issue_codes:
741
  recommendations.append("Include tools information in the metadata section")
@@ -835,7 +838,7 @@ def get_validation_summary(report: Dict[str, Any]) -> str:
835
 
836
  def calculate_industry_neutral_score(aibom: Dict[str, Any]) -> Dict[str, Any]:
837
  """
838
- Calculate completeness score using industry best practices without explicit standard references.
839
 
840
  Args:
841
  aibom: The AIBOM to score
@@ -844,6 +847,8 @@ def calculate_industry_neutral_score(aibom: Dict[str, Any]) -> Dict[str, Any]:
844
  Dictionary containing score and recommendations
845
  """
846
  field_checklist = {}
 
 
847
  max_scores = {
848
  "required_fields": 20,
849
  "metadata": 20,
@@ -852,30 +857,29 @@ def calculate_industry_neutral_score(aibom: Dict[str, Any]) -> Dict[str, Any]:
852
  "external_references": 10
853
  }
854
 
855
- # Track missing fields by tier
856
  missing_fields = {
857
  "critical": [],
858
  "important": [],
859
  "supplementary": []
860
  }
861
 
862
- # Score each field based on classification
863
- scores_by_category = {category: 0 for category in max_scores.keys()}
864
- max_possible_by_category = {category: 0 for category in max_scores.keys()}
865
 
 
866
  for field, classification in FIELD_CLASSIFICATION.items():
867
  tier = classification["tier"]
868
- weight = classification["weight"]
869
  category = classification["category"]
870
 
871
- # Add to max possible score for this category
872
- max_possible_by_category[category] += weight
873
 
874
- # Check if field is present
875
- is_present = check_field_in_aibom(aibom, field)
876
 
877
  if is_present:
878
- scores_by_category[category] += weight
879
  else:
880
  missing_fields[tier].append(field)
881
 
@@ -883,51 +887,150 @@ def calculate_industry_neutral_score(aibom: Dict[str, Any]) -> Dict[str, Any]:
883
  importance_indicator = "★★★" if tier == "critical" else "★★" if tier == "important" else "★"
884
  field_checklist[field] = f"{'✔' if is_present else '✘'} {importance_indicator}"
885
 
886
- # Normalize category scores to max_scores
887
- normalized_scores = {}
888
- for category in scores_by_category:
889
- if max_possible_by_category[category] > 0:
890
- # Normalize to the max score for this category
891
- normalized_score = (scores_by_category[category] / max_possible_by_category[category]) * max_scores[category]
892
- normalized_scores[category] = min(normalized_score, max_scores[category])
893
- else:
894
- normalized_scores[category] = 0
 
 
895
 
896
- # Calculate total score (sum of weighted normalized scores)
897
- total_score = 0
898
- for category, score in normalized_scores.items():
899
- # Each category contributes its percentage to the total
900
- category_weight = max_scores[category] / sum(max_scores.values())
901
- total_score += score * category_weight
902
 
903
- # Round to one decimal place
904
- total_score = round(total_score, 1)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
905
 
906
  # Ensure score is between 0 and 100
907
- total_score = max(0, min(total_score, 100))
908
 
909
  # Determine completeness profile
910
- profile = determine_completeness_profile(aibom, total_score)
911
-
912
- # Apply penalties for missing critical fields
913
- penalty_result = apply_completeness_penalties(total_score, missing_fields)
914
 
915
  # Generate recommendations
916
  recommendations = generate_field_recommendations(missing_fields)
917
 
918
- return {
919
- "total_score": penalty_result["adjusted_score"],
920
- "section_scores": normalized_scores,
 
 
 
 
 
 
 
 
 
 
 
921
  "max_scores": max_scores,
922
  "field_checklist": field_checklist,
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
923
  "field_categorization": get_field_categorization_for_display(aibom),
924
  "field_tiers": {field: info["tier"] for field, info in FIELD_CLASSIFICATION.items()},
925
  "missing_fields": missing_fields,
 
 
 
 
 
926
  "completeness_profile": profile,
927
- "penalty_applied": penalty_result["penalty_applied"],
928
- "penalty_reason": penalty_result["penalty_reason"],
929
- "recommendations": recommendations
 
 
 
 
 
 
 
 
 
 
 
 
 
 
930
  }
 
 
 
 
 
 
 
 
 
 
931
 
932
 
933
  def calculate_completeness_score(aibom: Dict[str, Any], validate: bool = True, use_best_practices: bool = True) -> Dict[str, Any]:
@@ -959,6 +1062,7 @@ def calculate_completeness_score(aibom: Dict[str, Any], validate: bool = True, u
959
  warning_count = validation_result["summary"]["warning_count"]
960
 
961
  # Apply penalties to the score
 
962
  if error_count > 0:
963
  # Severe penalty for errors (up to 50% reduction)
964
  error_penalty = min(0.5, error_count * 0.1)
@@ -969,7 +1073,7 @@ def calculate_completeness_score(aibom: Dict[str, Any], validate: bool = True, u
969
  warning_penalty = min(0.2, warning_count * 0.05)
970
  result["total_score"] = round(result["total_score"] * (1 - warning_penalty), 1)
971
  result["validation_penalty"] = f"-{int(warning_penalty * 100)}% due to {warning_count} schema warnings"
972
-
973
  result = add_enhanced_field_display_to_result(result, aibom)
974
 
975
  return result
@@ -1084,7 +1188,34 @@ def calculate_completeness_score(aibom: Dict[str, Any], validate: bool = True, u
1084
  "total_score": total_score,
1085
  "section_scores": section_scores,
1086
  "max_scores": max_scores,
1087
- "field_checklist": field_checklist
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1088
  }
1089
 
1090
  # Add validation if requested
@@ -1097,7 +1228,8 @@ def calculate_completeness_score(aibom: Dict[str, Any], validate: bool = True, u
1097
  # Count errors and warnings
1098
  error_count = validation_result["summary"]["error_count"]
1099
  warning_count = validation_result["summary"]["warning_count"]
1100
-
 
1101
  # Apply penalties to the score
1102
  if error_count > 0:
1103
  # Severe penalty for errors (up to 50% reduction)
@@ -1109,7 +1241,7 @@ def calculate_completeness_score(aibom: Dict[str, Any], validate: bool = True, u
1109
  warning_penalty = min(0.2, warning_count * 0.05)
1110
  result["total_score"] = round(result["total_score"] * (1 - warning_penalty), 1)
1111
  result["validation_penalty"] = f"-{int(warning_penalty * 100)}% due to {warning_count} schema warnings"
1112
-
1113
  result = add_enhanced_field_display_to_result(result, aibom)
1114
 
1115
  return result
@@ -1305,3 +1437,92 @@ def add_enhanced_field_display_to_result(result: Dict[str, Any], aibom: Dict[str
1305
  enhanced_result = result.copy()
1306
  enhanced_result["field_display"] = get_field_categorization_for_display(aibom)
1307
  return enhanced_result
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
304
  # Return the highest satisfied profile
305
  if "advanced" in satisfied_profiles:
306
  return {
307
+ "name": "Advanced",
308
  "description": COMPLETENESS_PROFILES["advanced"]["description"],
309
  "satisfied": True
310
  }
311
  elif "standard" in satisfied_profiles:
312
  return {
313
+ "name": "Standard",
314
  "description": COMPLETENESS_PROFILES["standard"]["description"],
315
  "satisfied": True
316
  }
317
  elif "basic" in satisfied_profiles:
318
  return {
319
+ "name": "Basic",
320
  "description": COMPLETENESS_PROFILES["basic"]["description"],
321
  "satisfied": True
322
  }
 
329
 
330
 
331
  def apply_completeness_penalties(original_score: float, missing_fields: Dict[str, List[str]]) -> Dict[str, Any]:
332
+
333
  """
334
  Apply penalties based on missing critical fields.
335
 
 
340
  Returns:
341
  Dictionary with penalty information
342
  """
343
+
344
+
345
  # Count missing fields by tier
346
  missing_critical_count = len(missing_fields["critical"])
347
  missing_important_count = len(missing_fields["important"])
348
 
349
+ penalty_factor = 1.0
350
+ penalty_reason = None
351
+
352
  # Calculate penalty based on missing critical fields
353
  if missing_critical_count > 3:
354
+ penalty_factor *= 0.8 # 20% penalty
355
  penalty_reason = "Multiple critical fields missing"
356
+ elif missing_critical_count >= 2: # if count is 2 - 3
357
+ penalty_factor *= 0.9 # 10% penalty
358
  penalty_reason = "Some critical fields missing"
359
+
360
+ if missing_important_count >= 5:
361
+ penalty_factor *= 0.95 # 5% penalty
362
  penalty_reason = "Several important fields missing"
 
 
 
 
363
 
364
  adjusted_score = original_score * penalty_factor
365
 
 
738
  recommendations.append("Add ethical considerations, limitations, and risks to the model card")
739
 
740
  if "MISSING_METADATA" in issue_codes:
741
+ recommendations.append("Add metadata section to the AI SBOM")
742
 
743
  if "MISSING_TOOLS" in issue_codes:
744
  recommendations.append("Include tools information in the metadata section")
 
838
 
839
  def calculate_industry_neutral_score(aibom: Dict[str, Any]) -> Dict[str, Any]:
840
  """
841
+ Calculate completeness score using industry best practices with proper normalization and penalties.
842
 
843
  Args:
844
  aibom: The AIBOM to score
 
847
  Dictionary containing score and recommendations
848
  """
849
  field_checklist = {}
850
+
851
+ # Maximum points for each category (these are the "weights")
852
  max_scores = {
853
  "required_fields": 20,
854
  "metadata": 20,
 
857
  "external_references": 10
858
  }
859
 
860
+ # Track missing fields by tier (for penalty calculation)
861
  missing_fields = {
862
  "critical": [],
863
  "important": [],
864
  "supplementary": []
865
  }
866
 
867
+ # Count fields by category
868
+ fields_by_category = {category: {"total": 0, "present": 0} for category in max_scores.keys()}
 
869
 
870
+ # Process each field and categorize
871
  for field, classification in FIELD_CLASSIFICATION.items():
872
  tier = classification["tier"]
 
873
  category = classification["category"]
874
 
875
+ # Count total fields in this category
876
+ fields_by_category[category]["total"] += 1
877
 
878
+ # Check if field is present (ensure boolean result)
879
+ is_present = bool(check_field_in_aibom(aibom, field))
880
 
881
  if is_present:
882
+ fields_by_category[category]["present"] += 1
883
  else:
884
  missing_fields[tier].append(field)
885
 
 
887
  importance_indicator = "★★★" if tier == "critical" else "★★" if tier == "important" else "★"
888
  field_checklist[field] = f"{'✔' if is_present else '✘'} {importance_indicator}"
889
 
890
+ # Calculate category scores using proper normalization
891
+ category_scores = {}
892
+ for category, counts in fields_by_category.items():
893
+ if counts["total"] > 0:
894
+ # Normalization: (Present Fields / Total Fields) × Maximum Points
895
+ raw_score = (counts["present"] / counts["total"]) * max_scores[category]
896
+ # Ensure raw_score is a number before rounding
897
+ if isinstance(raw_score, (int, float)) and not isinstance(raw_score, bool):
898
+ category_scores[category] = round(raw_score, 1)
899
+ else:
900
+ category_scores[category] = 0.0
901
 
902
+ # Calculate subtotal (sum of rounded category scores)
903
+ subtotal_score = sum(category_scores.values())
 
 
 
 
904
 
905
+ # Count missing fields by tier for penalty calculation
906
+ missing_critical_count = len(missing_fields["critical"])
907
+ missing_important_count = len(missing_fields["important"])
908
+
909
+ # Apply penalties based on missing critical and important fields
910
+ penalty_factor = 1.0
911
+ penalty_reasons = []
912
+
913
+ # Critical field penalties
914
+ if missing_critical_count > 3:
915
+ penalty_factor *= 0.8 # 20% penalty
916
+ penalty_reasons.append("Multiple critical fields missing")
917
+ elif missing_critical_count >= 2: # if count is 2-3
918
+ penalty_factor *= 0.9 # 10% penalty
919
+ penalty_reasons.append("Some critical fields missing")
920
+ # No penalty for missing_critical_count == 1
921
+
922
+ # Important field penalties (additional)
923
+ if missing_important_count >= 5:
924
+ penalty_factor *= 0.95 # Additional 5% penalty
925
+ penalty_reasons.append("Several important fields missing")
926
+
927
+ # Apply penalty to subtotal
928
+ final_score = subtotal_score * penalty_factor
929
+ final_score = round(final_score, 1)
930
+
931
+ # Debugging calculation:
932
+ print(f"DEBUG CATEGORIES:")
933
+ for category, score in category_scores.items():
934
+ print(f" {category}: {score}")
935
+ print(f"DEBUG: category_scores sum = {sum(category_scores.values())}")
936
+ print(f"DEBUG: subtotal_score = {subtotal_score}")
937
+ print(f"DEBUG: missing_critical_count = {missing_critical_count}")
938
+ print(f"DEBUG: missing_important_count = {missing_important_count}")
939
+ print(f"DEBUG: penalty_factor = {penalty_factor}")
940
+ print(f"DEBUG: penalty_reasons = {penalty_reasons}")
941
+ print(f"DEBUG: subtotal_score = {subtotal_score}")
942
+ print(f"DEBUG: final_score calculation = {subtotal_score} × {penalty_factor} = {subtotal_score * penalty_factor}")
943
+ print(f"DEBUG: final_score after round = {final_score}")
944
 
945
  # Ensure score is between 0 and 100
946
+ final_score = max(0.0, min(final_score, 100.0))
947
 
948
  # Determine completeness profile
949
+ profile = determine_completeness_profile(aibom, final_score)
 
 
 
950
 
951
  # Generate recommendations
952
  recommendations = generate_field_recommendations(missing_fields)
953
 
954
+ # Prepare penalty information
955
+ penalty_applied = penalty_factor < 1.0
956
+ penalty_reason = " and ".join(penalty_reasons) if penalty_reasons else None
957
+ penalty_percentage = round((1.0 - penalty_factor) * 100, 1) if penalty_applied else 0.0
958
+
959
+ # DEBUG: Print the result structure before returning
960
+ print("DEBUG: Final result structure:")
961
+ print(f" total_score: {final_score}")
962
+ print(f" section_scores keys: {list(category_scores.keys())}")
963
+
964
+ result = {
965
+ "total_score": final_score,
966
+ "subtotal_score": subtotal_score,
967
+ "section_scores": category_scores,
968
  "max_scores": max_scores,
969
  "field_checklist": field_checklist,
970
+ "category_details": {
971
+ "required_fields": {
972
+ "present_fields": fields_by_category["required_fields"]["present"],
973
+ "total_fields": fields_by_category["required_fields"]["total"],
974
+ "percentage": round((fields_by_category["required_fields"]["present"] / fields_by_category["required_fields"]["total"]) * 100, 1)
975
+ },
976
+ "metadata": {
977
+ "present_fields": fields_by_category["metadata"]["present"],
978
+ "total_fields": fields_by_category["metadata"]["total"],
979
+ "percentage": round((fields_by_category["metadata"]["present"] / fields_by_category["metadata"]["total"]) * 100, 1)
980
+ },
981
+ "component_basic": {
982
+ "present_fields": fields_by_category["component_basic"]["present"],
983
+ "total_fields": fields_by_category["component_basic"]["total"],
984
+ "percentage": round((fields_by_category["component_basic"]["present"] / fields_by_category["component_basic"]["total"]) * 100, 1)
985
+ },
986
+ "component_model_card": {
987
+ "present_fields": fields_by_category["component_model_card"]["present"],
988
+ "total_fields": fields_by_category["component_model_card"]["total"],
989
+ "percentage": round((fields_by_category["component_model_card"]["present"] / fields_by_category["component_model_card"]["total"]) * 100, 1)
990
+ },
991
+ "external_references": {
992
+ "present_fields": fields_by_category["external_references"]["present"],
993
+ "total_fields": fields_by_category["external_references"]["total"],
994
+ "percentage": round((fields_by_category["external_references"]["present"] / fields_by_category["external_references"]["total"]) * 100, 1)
995
+ }
996
+ },
997
  "field_categorization": get_field_categorization_for_display(aibom),
998
  "field_tiers": {field: info["tier"] for field, info in FIELD_CLASSIFICATION.items()},
999
  "missing_fields": missing_fields,
1000
+ "missing_counts": {
1001
+ "critical": missing_critical_count,
1002
+ "important": missing_important_count,
1003
+ "supplementary": len(missing_fields["supplementary"])
1004
+ },
1005
  "completeness_profile": profile,
1006
+ "penalty_applied": penalty_applied,
1007
+ "penalty_reason": penalty_reason,
1008
+ "penalty_percentage": penalty_percentage,
1009
+ "penalty_factor": penalty_factor,
1010
+ "recommendations": recommendations,
1011
+ "calculation_details": {
1012
+ "category_breakdown": {
1013
+ category: {
1014
+ "present_fields": counts["present"],
1015
+ "total_fields": counts["total"],
1016
+ "percentage": round((counts["present"] / counts["total"]) * 100, 1) if counts["total"] > 0 else 0.0,
1017
+ "points": category_scores[category],
1018
+ "max_points": max_scores[category]
1019
+ }
1020
+ for category, counts in fields_by_category.items()
1021
+ }
1022
+ }
1023
  }
1024
+
1025
+ # Debug the final result
1026
+ if 'category_details' in result:
1027
+ print(f" category_details exists: {list(result['category_details'].keys())}")
1028
+ print(f" required_fields details: {result['category_details'].get('required_fields')}")
1029
+ print(f" metadata details: {result['category_details'].get('metadata')}")
1030
+ else:
1031
+ print(" category_details: MISSING!")
1032
+
1033
+ return result
1034
 
1035
 
1036
  def calculate_completeness_score(aibom: Dict[str, Any], validate: bool = True, use_best_practices: bool = True) -> Dict[str, Any]:
 
1062
  warning_count = validation_result["summary"]["warning_count"]
1063
 
1064
  # Apply penalties to the score
1065
+ """
1066
  if error_count > 0:
1067
  # Severe penalty for errors (up to 50% reduction)
1068
  error_penalty = min(0.5, error_count * 0.1)
 
1073
  warning_penalty = min(0.2, warning_count * 0.05)
1074
  result["total_score"] = round(result["total_score"] * (1 - warning_penalty), 1)
1075
  result["validation_penalty"] = f"-{int(warning_penalty * 100)}% due to {warning_count} schema warnings"
1076
+ """
1077
  result = add_enhanced_field_display_to_result(result, aibom)
1078
 
1079
  return result
 
1188
  "total_score": total_score,
1189
  "section_scores": section_scores,
1190
  "max_scores": max_scores,
1191
+ "field_checklist": field_checklist,
1192
+ "category_details": {
1193
+ "required_fields": {
1194
+ "present_fields": fields_by_category["required_fields"]["present"],
1195
+ "total_fields": fields_by_category["required_fields"]["total"],
1196
+ "percentage": round((fields_by_category["required_fields"]["present"] / fields_by_category["required_fields"]["total"]) * 100, 1)
1197
+ },
1198
+ "metadata": {
1199
+ "present_fields": fields_by_category["metadata"]["present"],
1200
+ "total_fields": fields_by_category["metadata"]["total"],
1201
+ "percentage": round((fields_by_category["metadata"]["present"] / fields_by_category["metadata"]["total"]) * 100, 1)
1202
+ },
1203
+ "component_basic": {
1204
+ "present_fields": fields_by_category["component_basic"]["present"],
1205
+ "total_fields": fields_by_category["component_basic"]["total"],
1206
+ "percentage": round((fields_by_category["component_basic"]["present"] / fields_by_category["component_basic"]["total"]) * 100, 1)
1207
+ },
1208
+ "component_model_card": {
1209
+ "present_fields": fields_by_category["component_model_card"]["present"],
1210
+ "total_fields": fields_by_category["component_model_card"]["total"],
1211
+ "percentage": round((fields_by_category["component_model_card"]["present"] / fields_by_category["component_model_card"]["total"]) * 100, 1)
1212
+ },
1213
+ "external_references": {
1214
+ "present_fields": fields_by_category["external_references"]["present"],
1215
+ "total_fields": fields_by_category["external_references"]["total"],
1216
+ "percentage": round((fields_by_category["external_references"]["present"] / fields_by_category["external_references"]["total"]) * 100, 1)
1217
+ }
1218
+ }
1219
  }
1220
 
1221
  # Add validation if requested
 
1228
  # Count errors and warnings
1229
  error_count = validation_result["summary"]["error_count"]
1230
  warning_count = validation_result["summary"]["warning_count"]
1231
+
1232
+ """
1233
  # Apply penalties to the score
1234
  if error_count > 0:
1235
  # Severe penalty for errors (up to 50% reduction)
 
1241
  warning_penalty = min(0.2, warning_count * 0.05)
1242
  result["total_score"] = round(result["total_score"] * (1 - warning_penalty), 1)
1243
  result["validation_penalty"] = f"-{int(warning_penalty * 100)}% due to {warning_count} schema warnings"
1244
+ """
1245
  result = add_enhanced_field_display_to_result(result, aibom)
1246
 
1247
  return result
 
1437
  enhanced_result = result.copy()
1438
  enhanced_result["field_display"] = get_field_categorization_for_display(aibom)
1439
  return enhanced_result
1440
+
1441
+
1442
+ def get_score_display_info(score_result: Dict[str, Any]) -> Dict[str, Any]:
1443
+ """
1444
+ Generate user-friendly display information for the score.
1445
+
1446
+ Args:
1447
+ score_result: Result from calculate_industry_neutral_score
1448
+
1449
+ Returns:
1450
+ Dictionary with display-friendly information
1451
+ """
1452
+ display_info = {
1453
+ "category_display": [],
1454
+ "penalty_display": None,
1455
+ "total_display": None
1456
+ }
1457
+
1458
+ # Format category scores for display
1459
+ for category, score in score_result["section_scores"].items():
1460
+ max_score = score_result["max_scores"][category]
1461
+ category_name = category.replace("_", " ").title()
1462
+
1463
+ display_info["category_display"].append({
1464
+ "name": category_name,
1465
+ "score": f"{score}/{max_score}",
1466
+ "percentage": round((score / max_score) * 100, 1) if max_score > 0 else 0.0
1467
+ })
1468
+
1469
+ # Format penalty display
1470
+ if score_result["penalty_applied"]:
1471
+ display_info["penalty_display"] = {
1472
+ "message": f"Penalty Applied: -{score_result['penalty_percentage']}% ({score_result['penalty_reason']})",
1473
+ "subtotal": f"{score_result['subtotal_score']}/100",
1474
+ "final": f"{score_result['total_score']}/100"
1475
+ }
1476
+
1477
+ # Format total display
1478
+ display_info["total_display"] = {
1479
+ "score": f"{score_result['total_score']}/100",
1480
+ "percentage": round(score_result['total_score'], 1)
1481
+ }
1482
+
1483
+ return display_info
1484
+
1485
+
1486
+ def format_score_summary(score_result: Dict[str, Any]) -> str:
1487
+ """
1488
+ Generate a human-readable summary of the scoring results.
1489
+
1490
+ Args:
1491
+ score_result: Result from calculate_industry_neutral_score
1492
+
1493
+ Returns:
1494
+ Formatted summary string
1495
+ """
1496
+ summary = "AI SBOM Completeness Score Summary\n"
1497
+ summary += "=" * 40 + "\n\n"
1498
+
1499
+ # Category breakdown
1500
+ summary += "Category Breakdown:\n"
1501
+ for category, score in score_result["section_scores"].items():
1502
+ max_score = score_result["max_scores"][category]
1503
+ category_name = category.replace("_", " ").title()
1504
+ percentage = round((score / max_score) * 100, 1) if max_score > 0 else 0.0
1505
+ summary += f"- {category_name}: {score}/{max_score} ({percentage}%)\n"
1506
+
1507
+ summary += f"\nSubtotal: {score_result['subtotal_score']}/100\n"
1508
+
1509
+ # Penalty information
1510
+ if score_result["penalty_applied"]:
1511
+ summary += f"\nPenalty Applied: -{score_result['penalty_percentage']}%\n"
1512
+ summary += f"Reason: {score_result['penalty_reason']}\n"
1513
+ summary += f"Final Score: {score_result['total_score']}/100\n"
1514
+ else:
1515
+ summary += f"Final Score: {score_result['total_score']}/100 (No penalties applied)\n"
1516
+
1517
+ # Missing field counts
1518
+ summary += f"\nMissing Fields Summary:\n"
1519
+ summary += f"- Critical: {score_result['missing_counts']['critical']}\n"
1520
+ summary += f"- Important: {score_result['missing_counts']['important']}\n"
1521
+ summary += f"- Supplementary: {score_result['missing_counts']['supplementary']}\n"
1522
+
1523
+ # Completeness profile
1524
+ profile = score_result["completeness_profile"]
1525
+ summary += f"\nCompleteness Profile: {profile['name']}\n"
1526
+ summary += f"Description: {profile['description']}\n"
1527
+
1528
+ return summary