670 lines
23 KiB
Python
670 lines
23 KiB
Python
"""Alignment assessment for brownfield projects.
|
|
|
|
This module analyzes codebase analysis results and assesses alignment with
|
|
autonomous-dev standards. It generates PROJECT.md drafts, calculates 12-Factor
|
|
App compliance scores, identifies alignment gaps, and prioritizes remediation.
|
|
|
|
Classes:
|
|
TwelveFactorScore: 12-Factor App methodology compliance scoring
|
|
AlignmentGap: Represents a gap between current and desired state
|
|
ProjectMdDraft: Draft PROJECT.md content with confidence scoring
|
|
AssessmentResult: Complete alignment assessment results
|
|
AlignmentAssessor: Main assessment coordinator
|
|
|
|
Security:
|
|
- CWE-22: Path validation via security_utils
|
|
- CWE-117: Audit logging with sanitization
|
|
- CWE-20: Input validation for all user inputs
|
|
|
|
Related:
|
|
- GitHub Issue #59: Brownfield retrofit command implementation
|
|
|
|
Relevant Skills:
|
|
- project-alignment-validation: Gap assessment methodology, alignment checklist
|
|
- error-handling-patterns: Exception hierarchy and error handling best practices
|
|
- library-design-patterns: Standardized design patterns
|
|
"""
|
|
|
|
from dataclasses import dataclass, field
|
|
from enum import Enum
|
|
from pathlib import Path
|
|
from typing import Dict, List, Optional
|
|
|
|
from .security_utils import audit_log, validate_path
|
|
from .codebase_analyzer import AnalysisReport
|
|
|
|
|
|
class Severity(Enum):
|
|
"""Gap severity levels."""
|
|
CRITICAL = "CRITICAL" # Blocks autonomous development
|
|
HIGH = "HIGH" # Major impediment, should fix soon
|
|
MEDIUM = "MEDIUM" # Moderate issue, can defer
|
|
LOW = "LOW" # Minor improvement, nice to have
|
|
|
|
|
|
@dataclass
|
|
class TwelveFactorScore:
|
|
"""12-Factor App methodology compliance score.
|
|
|
|
Attributes:
|
|
factors: Dict mapping factor name to score (0-10)
|
|
total_score: Sum of all factor scores (max 120)
|
|
compliance_percentage: Percentage compliance (0-100)
|
|
"""
|
|
factors: Dict[str, int] = field(default_factory=dict)
|
|
total_score: int = 0
|
|
compliance_percentage: float = 0.0
|
|
|
|
def __post_init__(self):
|
|
"""Calculate total score and compliance percentage."""
|
|
if self.factors:
|
|
self.total_score = sum(self.factors.values())
|
|
max_score = len(self.factors) * 10
|
|
self.compliance_percentage = (self.total_score / max_score * 100) if max_score > 0 else 0.0
|
|
|
|
|
|
@dataclass
|
|
class AlignmentGap:
|
|
"""Represents a gap between current and desired state.
|
|
|
|
Attributes:
|
|
category: Gap category (e.g., 'file-organization', 'testing')
|
|
severity: Gap severity level
|
|
description: Human-readable description
|
|
current_state: Current project state
|
|
desired_state: Target state for alignment
|
|
fix_steps: List of remediation steps
|
|
impact_score: Impact score (0-100, higher = more important)
|
|
effort_hours: Estimated effort to fix (hours)
|
|
"""
|
|
category: str
|
|
severity: Severity
|
|
description: str
|
|
current_state: str
|
|
desired_state: str
|
|
fix_steps: List[str]
|
|
impact_score: int = 0
|
|
effort_hours: float = 0.0
|
|
|
|
|
|
@dataclass
|
|
class ProjectMdDraft:
|
|
"""Draft PROJECT.md content with confidence scoring.
|
|
|
|
Attributes:
|
|
sections: Dict mapping section name to content
|
|
confidence: Confidence score (0.0-1.0)
|
|
source_files: List of files used to generate draft
|
|
"""
|
|
sections: Dict[str, str] = field(default_factory=dict)
|
|
confidence: float = 0.0
|
|
source_files: List[str] = field(default_factory=list)
|
|
|
|
def to_markdown(self) -> str:
|
|
"""Convert draft to PROJECT.md markdown format.
|
|
|
|
Returns:
|
|
Formatted PROJECT.md content
|
|
"""
|
|
lines = ["# Project Overview\n"]
|
|
|
|
# Add sections in standard order
|
|
section_order = [
|
|
"GOALS",
|
|
"SCOPE",
|
|
"CONSTRAINTS",
|
|
"ARCHITECTURE",
|
|
"DEPENDENCIES",
|
|
"DEVELOPMENT",
|
|
"TESTING",
|
|
"DEPLOYMENT"
|
|
]
|
|
|
|
for section_name in section_order:
|
|
if section_name in self.sections:
|
|
lines.append(f"\n## {section_name}\n")
|
|
lines.append(self.sections[section_name])
|
|
|
|
# Add any remaining sections
|
|
for section_name, content in self.sections.items():
|
|
if section_name not in section_order:
|
|
lines.append(f"\n## {section_name}\n")
|
|
lines.append(content)
|
|
|
|
# Add metadata footer
|
|
lines.append(f"\n---\n")
|
|
lines.append(f"<!-- Generated by /align-project-retrofit -->\n")
|
|
lines.append(f"<!-- Confidence: {self.confidence:.2f} -->\n")
|
|
lines.append(f"<!-- Source files: {len(self.source_files)} -->\n")
|
|
|
|
return "\n".join(lines)
|
|
|
|
|
|
@dataclass
|
|
class AssessmentResult:
|
|
"""Complete alignment assessment results.
|
|
|
|
Attributes:
|
|
project_md: Draft PROJECT.md content
|
|
twelve_factor_score: 12-Factor compliance scoring
|
|
gaps: List of identified alignment gaps
|
|
priority_list: Gaps sorted by priority (impact/effort)
|
|
"""
|
|
project_md: ProjectMdDraft
|
|
twelve_factor_score: TwelveFactorScore
|
|
gaps: List[AlignmentGap] = field(default_factory=list)
|
|
priority_list: List[AlignmentGap] = field(default_factory=list)
|
|
|
|
def to_dict(self) -> dict:
|
|
"""Convert to dictionary representation.
|
|
|
|
Returns:
|
|
Dictionary with all assessment data
|
|
"""
|
|
return {
|
|
"project_md": {
|
|
"sections": self.project_md.sections,
|
|
"confidence": self.project_md.confidence,
|
|
"source_files": self.project_md.source_files
|
|
},
|
|
"twelve_factor_score": {
|
|
"factors": self.twelve_factor_score.factors,
|
|
"total_score": self.twelve_factor_score.total_score,
|
|
"compliance_percentage": self.twelve_factor_score.compliance_percentage
|
|
},
|
|
"gaps": [
|
|
{
|
|
"category": gap.category,
|
|
"severity": gap.severity.value,
|
|
"description": gap.description,
|
|
"current_state": gap.current_state,
|
|
"desired_state": gap.desired_state,
|
|
"fix_steps": gap.fix_steps,
|
|
"impact_score": gap.impact_score,
|
|
"effort_hours": gap.effort_hours
|
|
}
|
|
for gap in self.gaps
|
|
],
|
|
"priority_list": [
|
|
{
|
|
"category": gap.category,
|
|
"severity": gap.severity.value,
|
|
"description": gap.description,
|
|
"impact_score": gap.impact_score,
|
|
"effort_hours": gap.effort_hours
|
|
}
|
|
for gap in self.priority_list
|
|
]
|
|
}
|
|
|
|
|
|
class AlignmentAssessor:
|
|
"""Main alignment assessment coordinator.
|
|
|
|
Analyzes codebase analysis results and generates comprehensive alignment
|
|
assessment including PROJECT.md drafts, 12-Factor scores, and gap analysis.
|
|
"""
|
|
|
|
def __init__(self, project_root: Path):
|
|
"""Initialize alignment assessor.
|
|
|
|
Args:
|
|
project_root: Path to project root directory
|
|
|
|
Raises:
|
|
ValueError: If project_root invalid
|
|
"""
|
|
# Security: Validate project root path (CWE-22)
|
|
validated_root = validate_path(
|
|
project_root,
|
|
"project_root",
|
|
allow_missing=False,
|
|
)
|
|
self.project_root = Path(validated_root)
|
|
|
|
# Audit log initialization
|
|
audit_log(
|
|
"alignment_assessor_init",
|
|
project_root=str(self.project_root),
|
|
success=True
|
|
)
|
|
|
|
def assess(self, analysis: AnalysisReport) -> AssessmentResult:
|
|
"""Perform complete alignment assessment.
|
|
|
|
Args:
|
|
analysis: Codebase analysis results
|
|
|
|
Returns:
|
|
Complete assessment results
|
|
|
|
Raises:
|
|
ValueError: If analysis invalid
|
|
"""
|
|
if not analysis:
|
|
raise ValueError("Analysis result required")
|
|
|
|
audit_log(
|
|
"alignment_assessment_start",
|
|
project_root=str(self.project_root),
|
|
has_tech_stack=bool(analysis.tech_stack),
|
|
has_structure=bool(analysis.structure)
|
|
)
|
|
|
|
try:
|
|
# Generate PROJECT.md draft
|
|
project_md = self.generate_project_md(analysis)
|
|
|
|
# Calculate 12-Factor compliance
|
|
twelve_factor = self.calculate_twelve_factor_score(analysis)
|
|
|
|
# Identify alignment gaps
|
|
gaps = self.identify_alignment_gaps(analysis, twelve_factor)
|
|
|
|
# Prioritize gaps
|
|
priority_list = self.prioritize_gaps(gaps)
|
|
|
|
result = AssessmentResult(
|
|
project_md=project_md,
|
|
twelve_factor_score=twelve_factor,
|
|
gaps=gaps,
|
|
priority_list=priority_list
|
|
)
|
|
|
|
audit_log(
|
|
"alignment_assessment_complete",
|
|
project_root=str(self.project_root),
|
|
gaps_found=len(gaps),
|
|
compliance_percentage=twelve_factor.compliance_percentage,
|
|
success=True
|
|
)
|
|
|
|
return result
|
|
|
|
except Exception as e:
|
|
audit_log(
|
|
"alignment_assessment_failed",
|
|
project_root=str(self.project_root),
|
|
error=str(e),
|
|
success=False
|
|
)
|
|
raise
|
|
|
|
def generate_project_md(self, analysis: AnalysisReport) -> ProjectMdDraft:
|
|
"""Generate PROJECT.md draft from analysis.
|
|
|
|
Args:
|
|
analysis: Codebase analysis results
|
|
|
|
Returns:
|
|
Draft PROJECT.md content with confidence score
|
|
"""
|
|
sections = {}
|
|
source_files = []
|
|
|
|
# GOALS section from README/docs
|
|
goals_content = self._extract_goals(analysis)
|
|
if goals_content:
|
|
sections["GOALS"] = goals_content
|
|
source_files.extend(["README.md", "docs/"])
|
|
|
|
# SCOPE section from tech stack
|
|
if analysis.tech_stack.primary_language:
|
|
scope_lines = [
|
|
f"**Primary Language**: {analysis.tech_stack.primary_language}",
|
|
f"**Framework**: {analysis.tech_stack.framework or 'None detected'}",
|
|
f"**Package Manager**: {analysis.tech_stack.package_manager or 'None detected'}",
|
|
]
|
|
sections["SCOPE"] = "\n".join(scope_lines)
|
|
source_files.append("Tech stack detection")
|
|
|
|
# CONSTRAINTS section
|
|
constraints = self._extract_constraints(analysis)
|
|
if constraints:
|
|
sections["CONSTRAINTS"] = constraints
|
|
|
|
# ARCHITECTURE section from structure
|
|
if analysis.structure.total_files > 0:
|
|
arch_lines = [
|
|
f"**Total Files**: {analysis.structure.total_files}",
|
|
f"**Source Files**: {analysis.structure.source_files}",
|
|
f"**Test Files**: {analysis.structure.test_files}",
|
|
f"**Documentation**: {analysis.structure.doc_files} files",
|
|
]
|
|
sections["ARCHITECTURE"] = "\n".join(arch_lines)
|
|
source_files.append("File structure analysis")
|
|
|
|
# DEPENDENCIES section
|
|
if analysis.tech_stack.dependencies:
|
|
dep_lines = ["**Key Dependencies**:"]
|
|
for dep in list(analysis.tech_stack.dependencies)[:10]: # Top 10
|
|
dep_lines.append(f"- {dep}")
|
|
sections["DEPENDENCIES"] = "\n".join(dep_lines)
|
|
source_files.append("Dependency files")
|
|
|
|
# TESTING section
|
|
if analysis.structure.test_files > 0:
|
|
test_lines = [
|
|
f"**Test Framework**: {analysis.tech_stack.test_framework or 'Detected from structure'}",
|
|
f"**Test Files**: {analysis.structure.test_files}",
|
|
f"**Test Coverage**: Unknown (run tests to detect)",
|
|
]
|
|
sections["TESTING"] = "\n".join(test_lines)
|
|
source_files.append("Test structure")
|
|
|
|
# Calculate confidence score (0.0-1.0)
|
|
confidence = self._calculate_confidence(sections, analysis)
|
|
|
|
return ProjectMdDraft(
|
|
sections=sections,
|
|
confidence=confidence,
|
|
source_files=list(set(source_files)) # Deduplicate
|
|
)
|
|
|
|
def calculate_twelve_factor_score(self, analysis: AnalysisReport) -> TwelveFactorScore:
|
|
"""Calculate 12-Factor App compliance score.
|
|
|
|
Each factor scored 0-10:
|
|
- 10: Full compliance
|
|
- 7-9: Partial compliance
|
|
- 4-6: Minimal compliance
|
|
- 0-3: Non-compliant
|
|
|
|
Args:
|
|
analysis: Codebase analysis results
|
|
|
|
Returns:
|
|
12-Factor compliance scoring
|
|
"""
|
|
factors = {}
|
|
|
|
# I. Codebase - Single codebase in version control
|
|
has_git = (self.project_root / ".git").is_dir()
|
|
factors["codebase"] = 10 if has_git else 3
|
|
|
|
# II. Dependencies - Explicitly declared
|
|
has_deps = bool(analysis.tech_stack.package_manager)
|
|
factors["dependencies"] = 10 if has_deps else 4
|
|
|
|
# III. Config - Store in environment
|
|
has_env = any(f for f in analysis.structure.config_files if ".env" in f.lower())
|
|
factors["config"] = 8 if has_env else 5
|
|
|
|
# IV. Backing services - Treat as attached resources
|
|
# Heuristic: Check for database/cache config
|
|
has_backing = any(
|
|
tech in str(analysis.tech_stack.dependencies).lower()
|
|
for tech in ["postgres", "redis", "mysql", "mongo"]
|
|
)
|
|
factors["backing_services"] = 8 if has_backing else 6
|
|
|
|
# V. Build, release, run - Strict separation
|
|
has_ci = any(f for f in analysis.structure.config_files if "ci" in f.lower() or "github" in f.lower())
|
|
factors["build_release_run"] = 9 if has_ci else 5
|
|
|
|
# VI. Processes - Execute as stateless processes
|
|
# Heuristic: No obvious state storage detected
|
|
factors["processes"] = 7 # Default moderate score
|
|
|
|
# VII. Port binding - Export via port binding
|
|
# Heuristic: Check for web framework
|
|
has_web = analysis.tech_stack.framework in ["flask", "django", "fastapi", "express"]
|
|
factors["port_binding"] = 9 if has_web else 6
|
|
|
|
# VIII. Concurrency - Scale via process model
|
|
factors["concurrency"] = 7 # Default moderate score
|
|
|
|
# IX. Disposability - Fast startup/graceful shutdown
|
|
factors["disposability"] = 7 # Default moderate score
|
|
|
|
# X. Dev/prod parity - Keep similar
|
|
has_docker = any(f for f in analysis.structure.config_files if "docker" in f.lower())
|
|
factors["dev_prod_parity"] = 9 if has_docker else 5
|
|
|
|
# XI. Logs - Treat as event streams
|
|
has_logging = any(
|
|
tech in str(analysis.tech_stack.dependencies).lower()
|
|
for tech in ["logging", "logger", "log"]
|
|
)
|
|
factors["logs"] = 8 if has_logging else 6
|
|
|
|
# XII. Admin processes - Run as one-off processes
|
|
has_scripts = analysis.structure.total_files > 0 # Has any scripts
|
|
factors["admin_processes"] = 7 if has_scripts else 5
|
|
|
|
return TwelveFactorScore(factors=factors)
|
|
|
|
def identify_alignment_gaps(
|
|
self,
|
|
analysis: AnalysisReport,
|
|
twelve_factor: TwelveFactorScore
|
|
) -> List[AlignmentGap]:
|
|
"""Identify alignment gaps between current and desired state.
|
|
|
|
Args:
|
|
analysis: Codebase analysis results
|
|
twelve_factor: 12-Factor compliance score
|
|
|
|
Returns:
|
|
List of identified gaps
|
|
"""
|
|
gaps = []
|
|
|
|
# Gap: Missing PROJECT.md
|
|
if not (self.project_root / ".claude" / "PROJECT.md").exists():
|
|
gaps.append(AlignmentGap(
|
|
category="documentation",
|
|
severity=Severity.CRITICAL,
|
|
description="Missing .claude/PROJECT.md file",
|
|
current_state="No PROJECT.md exists",
|
|
desired_state="PROJECT.md defines GOALS, SCOPE, CONSTRAINTS",
|
|
fix_steps=[
|
|
"Create .claude/ directory",
|
|
"Generate PROJECT.md from analysis",
|
|
"Review and customize content"
|
|
],
|
|
impact_score=100,
|
|
effort_hours=0.5
|
|
))
|
|
|
|
# Gap: Poor file organization
|
|
if not analysis.structure.has_src_dir and analysis.structure.source_files > 10:
|
|
gaps.append(AlignmentGap(
|
|
category="file-organization",
|
|
severity=Severity.HIGH,
|
|
description="No src/ directory structure",
|
|
current_state=f"{analysis.structure.source_files} files in root",
|
|
desired_state="Organized src/ directory structure",
|
|
fix_steps=[
|
|
"Create src/ directory",
|
|
"Move source files to src/",
|
|
"Update import paths"
|
|
],
|
|
impact_score=80,
|
|
effort_hours=2.0
|
|
))
|
|
|
|
# Gap: Missing tests
|
|
if analysis.structure.test_files == 0:
|
|
gaps.append(AlignmentGap(
|
|
category="testing",
|
|
severity=Severity.HIGH,
|
|
description="No test files detected",
|
|
current_state="0 test files",
|
|
desired_state="Test coverage > 80%",
|
|
fix_steps=[
|
|
"Create tests/ directory",
|
|
"Add test framework (pytest recommended)",
|
|
"Write initial test suite"
|
|
],
|
|
impact_score=90,
|
|
effort_hours=4.0
|
|
))
|
|
|
|
# Gap: Low test coverage
|
|
elif analysis.structure.test_files < analysis.structure.source_files * 0.5:
|
|
gaps.append(AlignmentGap(
|
|
category="testing",
|
|
severity=Severity.MEDIUM,
|
|
description="Insufficient test coverage",
|
|
current_state=f"{analysis.structure.test_files} test files vs {analysis.structure.source_files} source files",
|
|
desired_state="Test coverage > 80%",
|
|
fix_steps=[
|
|
"Identify untested modules",
|
|
"Add tests for critical paths",
|
|
"Set up coverage reporting"
|
|
],
|
|
impact_score=70,
|
|
effort_hours=8.0
|
|
))
|
|
|
|
# Gap: Missing CI/CD
|
|
has_ci = any(f for f in analysis.structure.config_files if "ci" in f.lower())
|
|
if not has_ci:
|
|
gaps.append(AlignmentGap(
|
|
category="automation",
|
|
severity=Severity.MEDIUM,
|
|
description="No CI/CD configuration",
|
|
current_state="No CI/CD detected",
|
|
desired_state="Automated testing and deployment",
|
|
fix_steps=[
|
|
"Add .github/workflows/ directory",
|
|
"Create test workflow",
|
|
"Configure deployment pipeline"
|
|
],
|
|
impact_score=75,
|
|
effort_hours=3.0
|
|
))
|
|
|
|
# Gap: 12-Factor compliance issues
|
|
for factor_name, score in twelve_factor.factors.items():
|
|
if score < 7: # Below good compliance threshold
|
|
gaps.append(AlignmentGap(
|
|
category="twelve-factor",
|
|
severity=Severity.LOW if score >= 4 else Severity.MEDIUM,
|
|
description=f"Low 12-Factor score: {factor_name}",
|
|
current_state=f"Score: {score}/10",
|
|
desired_state=f"Score: 8+/10",
|
|
fix_steps=[
|
|
f"Review 12-Factor methodology for '{factor_name}'",
|
|
f"Implement recommended practices",
|
|
f"Verify compliance"
|
|
],
|
|
impact_score=50 + score * 2, # Higher impact for lower scores
|
|
effort_hours=1.0 + (10 - score) * 0.5
|
|
))
|
|
|
|
return gaps
|
|
|
|
def prioritize_gaps(self, gaps: List[AlignmentGap]) -> List[AlignmentGap]:
|
|
"""Prioritize gaps by impact/effort ratio.
|
|
|
|
Args:
|
|
gaps: List of alignment gaps
|
|
|
|
Returns:
|
|
Gaps sorted by priority (highest first)
|
|
"""
|
|
# Calculate priority score for each gap
|
|
def priority_score(gap: AlignmentGap) -> float:
|
|
# Severity weight
|
|
severity_weight = {
|
|
Severity.CRITICAL: 100,
|
|
Severity.HIGH: 50,
|
|
Severity.MEDIUM: 25,
|
|
Severity.LOW: 10
|
|
}
|
|
|
|
# Impact/effort ratio (higher is better)
|
|
effort = max(gap.effort_hours, 0.1) # Avoid division by zero
|
|
ratio = gap.impact_score / effort
|
|
|
|
# Combined score
|
|
return severity_weight[gap.severity] + ratio
|
|
|
|
# Sort by priority score (highest first)
|
|
return sorted(gaps, key=priority_score, reverse=True)
|
|
|
|
# Private helper methods
|
|
|
|
def _extract_goals(self, analysis: AnalysisReport) -> Optional[str]:
|
|
"""Extract goals from README or documentation.
|
|
|
|
Args:
|
|
analysis: Codebase analysis results
|
|
|
|
Returns:
|
|
Goals content or None
|
|
"""
|
|
readme_path = self.project_root / "README.md"
|
|
if readme_path.exists():
|
|
try:
|
|
content = readme_path.read_text(encoding="utf-8")
|
|
# Look for common goal-related sections
|
|
for marker in ["## Goals", "## Purpose", "## Objectives"]:
|
|
if marker in content:
|
|
# Extract section content (simplified)
|
|
return f"*Extracted from README.md*\n\n{content[:500]}..."
|
|
except Exception:
|
|
pass
|
|
|
|
return "**TODO**: Define project goals and objectives"
|
|
|
|
def _extract_constraints(self, analysis: AnalysisReport) -> str:
|
|
"""Extract constraints from tech stack.
|
|
|
|
Args:
|
|
analysis: Codebase analysis results
|
|
|
|
Returns:
|
|
Constraints content
|
|
"""
|
|
constraints = []
|
|
|
|
if analysis.tech_stack.primary_language:
|
|
constraints.append(f"- **Language**: {analysis.tech_stack.primary_language}")
|
|
|
|
if analysis.tech_stack.framework:
|
|
constraints.append(f"- **Framework**: {analysis.tech_stack.framework}")
|
|
|
|
# Add default constraints
|
|
constraints.append("- **Code Quality**: 80%+ test coverage required")
|
|
constraints.append("- **Security**: No secrets in version control")
|
|
constraints.append("- **Documentation**: Keep CLAUDE.md and PROJECT.md in sync")
|
|
|
|
return "\n".join(constraints)
|
|
|
|
def _calculate_confidence(self, sections: Dict[str, str], analysis: AnalysisReport) -> float:
|
|
"""Calculate confidence score for generated PROJECT.md.
|
|
|
|
Args:
|
|
sections: Generated sections
|
|
analysis: Codebase analysis results
|
|
|
|
Returns:
|
|
Confidence score (0.0-1.0)
|
|
"""
|
|
score = 0.0
|
|
|
|
# Base score from sections generated
|
|
score += len(sections) * 0.1 # 0.1 per section
|
|
|
|
# Bonus for tech stack detection
|
|
if analysis.tech_stack.primary_language:
|
|
score += 0.15
|
|
|
|
# Bonus for framework detection
|
|
if analysis.tech_stack.framework:
|
|
score += 0.15
|
|
|
|
# Bonus for dependencies
|
|
if analysis.tech_stack.dependencies:
|
|
score += 0.1
|
|
|
|
# Bonus for tests
|
|
if analysis.structure.test_files > 0:
|
|
score += 0.1
|
|
|
|
# Cap at 1.0
|
|
return min(score, 1.0)
|