Spaces:
Running
Running
Update src/aibom-generator/utils.py
Browse files- 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": "
|
| 308 |
"description": COMPLETENESS_PROFILES["advanced"]["description"],
|
| 309 |
"satisfied": True
|
| 310 |
}
|
| 311 |
elif "standard" in satisfied_profiles:
|
| 312 |
return {
|
| 313 |
-
"name": "
|
| 314 |
"description": COMPLETENESS_PROFILES["standard"]["description"],
|
| 315 |
"satisfied": True
|
| 316 |
}
|
| 317 |
elif "basic" in satisfied_profiles:
|
| 318 |
return {
|
| 319 |
-
"name": "
|
| 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
|
| 349 |
penalty_reason = "Multiple critical fields missing"
|
| 350 |
-
elif missing_critical_count
|
| 351 |
-
penalty_factor
|
| 352 |
penalty_reason = "Some critical fields missing"
|
| 353 |
-
|
| 354 |
-
|
|
|
|
| 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
|
| 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
|
| 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 |
-
#
|
| 863 |
-
|
| 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 |
-
#
|
| 872 |
-
|
| 873 |
|
| 874 |
-
# Check if field is present
|
| 875 |
-
is_present = check_field_in_aibom(aibom, field)
|
| 876 |
|
| 877 |
if is_present:
|
| 878 |
-
|
| 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 |
-
#
|
| 887 |
-
|
| 888 |
-
for category in
|
| 889 |
-
if
|
| 890 |
-
#
|
| 891 |
-
|
| 892 |
-
|
| 893 |
-
|
| 894 |
-
|
|
|
|
|
|
|
| 895 |
|
| 896 |
-
# Calculate
|
| 897 |
-
|
| 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 |
-
#
|
| 904 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 905 |
|
| 906 |
# Ensure score is between 0 and 100
|
| 907 |
-
|
| 908 |
|
| 909 |
# Determine completeness profile
|
| 910 |
-
profile = determine_completeness_profile(aibom,
|
| 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 |
-
|
| 919 |
-
|
| 920 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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":
|
| 928 |
-
"penalty_reason":
|
| 929 |
-
"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
|