sicua
Version:
A tool for analyzing project structure and dependencies
351 lines (350 loc) • 18.2 kB
JavaScript
;
/**
* Updated scoring aggregator with file type exclusions and new metrics
* Combines multiple metric scores into a final component score
* Applies weights, multipliers, and file type adjustments
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.ScoringAggregator = void 0;
const ScoringCriteria_1 = require("./ScoringCriteria");
const ScoringMetrics_1 = require("./ScoringMetrics");
class ScoringAggregator {
constructor(weights = ScoringCriteria_1.DEFAULT_SCORING_WEIGHTS, multipliers = ScoringCriteria_1.DEFAULT_SCORING_MULTIPLIERS) {
this.weights = weights;
this.multipliers = multipliers;
}
/**
* Calculate the final score for a component with file type exclusions
*/
calculateScore(context) {
// Determine file type for exclusions and adjustments
const fileType = (0, ScoringCriteria_1.getFileType)(context.component.name, context.component.fullPath);
// Calculate individual metric scores
const rawScores = this.calculateIndividualScores(context);
// Apply file type exclusions
const { adjustedScores, excludedMetrics } = this.applyFileTypeExclusions(rawScores, fileType);
// Apply file type specific weight adjustments
const { adjustedWeights, appliedWeightAdjustments } = this.applyFileTypeAdjustments(this.weights, fileType);
// Calculate weighted score using adjusted weights and scores
const weightedScore = this.calculateWeightedScore(adjustedScores, adjustedWeights);
// Calculate weighted contributions for analysis
const weightedContributions = this.calculateWeightedContributions(adjustedScores, adjustedWeights);
// Apply multipliers
const { finalScore, appliedMultipliers } = this.applyMultipliers(weightedScore, adjustedScores, context, fileType);
return {
finalScore: Math.round(finalScore * 100) / 100, // Round to 2 decimal places
individualScores: rawScores, // Keep original scores for debugging
appliedMultipliers,
fileType,
weightedContributions,
excludedMetrics,
appliedWeightAdjustments,
};
}
/**
* Calculate all individual metric scores including new React-specific metrics
*/
calculateIndividualScores(context) {
const complexityScore = (0, ScoringMetrics_1.calculateComplexityScore)(context);
return {
circularDependencies: (0, ScoringMetrics_1.calculateCircularDependencyScore)(context),
errorHandlingGaps: (0, ScoringMetrics_1.calculateErrorHandlingScore)(context),
cyclomaticComplexity: complexityScore, // This includes both cyclomatic and cognitive
cognitiveComplexity: 0, // Already included in cyclomaticComplexity
maintainabilityIndex: (0, ScoringMetrics_1.calculateMaintainabilityScore)(context),
typeIssues: (0, ScoringMetrics_1.calculateTypeIssuesScore)(context),
accessibilityIssues: (0, ScoringMetrics_1.calculateAccessibilityScore)(context),
performanceIssues: (0, ScoringMetrics_1.calculatePerformanceScore)(context),
seoProblems: (0, ScoringMetrics_1.calculateSEOScore)(context), // Uses updated version
couplingDegree: (0, ScoringMetrics_1.calculateCouplingScore)(context),
zombieCode: (0, ScoringMetrics_1.calculateZombieCodeScore)(context),
translationIssues: (0, ScoringMetrics_1.calculateTranslationScore)(context),
componentFlowComplexity: (0, ScoringMetrics_1.calculateComponentFlowScore)(context),
deduplicationOpportunities: (0, ScoringMetrics_1.calculateDeduplicationScore)(context),
codeMetrics: (0, ScoringMetrics_1.calculateCodeMetricsScore)(context),
magicNumbers: (0, ScoringMetrics_1.calculateMagicNumbersScore)(context),
reactComplexity: (0, ScoringMetrics_1.calculateReactComplexityScore)(context), // NEW
containerComplexity: (0, ScoringMetrics_1.calculateContainerComplexityScore)(context), // NEW
};
}
/**
* Apply file type exclusions to remove inappropriate metrics
*/
applyFileTypeExclusions(scores, fileType) {
const exclusions = ScoringCriteria_1.FILE_TYPE_EXCLUSIONS[fileType];
const excludedMetrics = [];
const adjustedScores = { ...scores };
if (exclusions &&
"excludeMetrics" in exclusions &&
exclusions.excludeMetrics) {
for (const metric of exclusions.excludeMetrics) {
if (metric in adjustedScores) {
adjustedScores[metric] = 0;
excludedMetrics.push(metric);
}
}
}
return { adjustedScores, excludedMetrics };
}
/**
* Apply file type specific weight adjustments with renormalization
*/
applyFileTypeAdjustments(baseWeights, fileType) {
const exclusions = ScoringCriteria_1.FILE_TYPE_EXCLUSIONS[fileType];
const adjustedWeights = { ...baseWeights };
const appliedWeightAdjustments = {};
if (!exclusions) {
return { adjustedWeights, appliedWeightAdjustments };
}
// Apply weight reductions
if ("reduceWeights" in exclusions && exclusions.reduceWeights) {
for (const [metric, multiplier] of Object.entries(exclusions.reduceWeights)) {
if (metric in adjustedWeights && typeof multiplier === "number") {
adjustedWeights[metric] *= multiplier;
appliedWeightAdjustments[metric] = multiplier;
}
}
}
// Apply weight increases
if ("increaseWeights" in exclusions && exclusions.increaseWeights) {
for (const [metric, multiplier] of Object.entries(exclusions.increaseWeights)) {
if (metric in adjustedWeights && typeof multiplier === "number") {
adjustedWeights[metric] *= multiplier;
appliedWeightAdjustments[metric] = multiplier;
}
}
}
// Zero out weights for excluded metrics
if ("excludeMetrics" in exclusions && exclusions.excludeMetrics) {
for (const metric of exclusions.excludeMetrics) {
if (metric in adjustedWeights) {
adjustedWeights[metric] = 0;
appliedWeightAdjustments[metric] = 0;
}
}
}
// Renormalize weights to sum to 1
const totalWeight = Object.values(adjustedWeights).reduce((sum, weight) => sum + weight, 0);
if (totalWeight > 0) {
for (const key in adjustedWeights) {
adjustedWeights[key] /= totalWeight;
}
}
return { adjustedWeights, appliedWeightAdjustments };
}
/**
* Calculate weighted score using adjusted weights including new metrics
*/
calculateWeightedScore(scores, weights) {
let weightedSum = 0;
weightedSum += scores.circularDependencies * weights.circularDependencies;
weightedSum += scores.errorHandlingGaps * weights.errorHandlingGaps;
weightedSum += scores.cyclomaticComplexity * weights.cyclomaticComplexity;
weightedSum += scores.cognitiveComplexity * weights.cognitiveComplexity;
weightedSum += scores.maintainabilityIndex * weights.maintainabilityIndex;
weightedSum += scores.typeIssues * weights.typeIssues;
weightedSum += scores.accessibilityIssues * weights.accessibilityIssues;
weightedSum += scores.performanceIssues * weights.performanceIssues;
weightedSum += scores.seoProblems * weights.seoProblems;
weightedSum += scores.couplingDegree * weights.couplingDegree;
weightedSum += scores.zombieCode * weights.zombieCode;
weightedSum += scores.translationIssues * weights.translationIssues;
weightedSum +=
scores.componentFlowComplexity * weights.componentFlowComplexity;
weightedSum +=
scores.deduplicationOpportunities * weights.deduplicationOpportunities;
weightedSum += scores.codeMetrics * weights.codeMetrics;
weightedSum += scores.magicNumbers * weights.magicNumbers;
weightedSum += scores.reactComplexity * weights.reactComplexity; // NEW
weightedSum += scores.containerComplexity * weights.containerComplexity; // NEW
return weightedSum;
}
/**
* Calculate weighted contributions for each metric including new metrics
*/
calculateWeightedContributions(scores, weights) {
return {
circularDependencies: scores.circularDependencies * weights.circularDependencies,
errorHandlingGaps: scores.errorHandlingGaps * weights.errorHandlingGaps,
cyclomaticComplexity: scores.cyclomaticComplexity * weights.cyclomaticComplexity,
cognitiveComplexity: scores.cognitiveComplexity * weights.cognitiveComplexity,
maintainabilityIndex: scores.maintainabilityIndex * weights.maintainabilityIndex,
typeIssues: scores.typeIssues * weights.typeIssues,
accessibilityIssues: scores.accessibilityIssues * weights.accessibilityIssues,
performanceIssues: scores.performanceIssues * weights.performanceIssues,
seoProblems: scores.seoProblems * weights.seoProblems,
couplingDegree: scores.couplingDegree * weights.couplingDegree,
zombieCode: scores.zombieCode * weights.zombieCode,
translationIssues: scores.translationIssues * weights.translationIssues,
componentFlowComplexity: scores.componentFlowComplexity * weights.componentFlowComplexity,
deduplicationOpportunities: scores.deduplicationOpportunities * weights.deduplicationOpportunities,
codeMetrics: scores.codeMetrics * weights.codeMetrics,
magicNumbers: scores.magicNumbers * weights.magicNumbers,
reactComplexity: scores.reactComplexity * weights.reactComplexity, // NEW
containerComplexity: scores.containerComplexity * weights.containerComplexity, // NEW
};
}
/**
* Apply multipliers based on critical conditions with file type awareness
*/
applyMultipliers(baseScore, scores, context, fileType) {
let finalScore = baseScore;
const appliedMultipliers = [];
// Critical penalty multipliers
if (scores.circularDependencies > 0) {
finalScore *= this.multipliers.inCircularDependency;
appliedMultipliers.push(`Circular Dependency Penalty (×${this.multipliers.inCircularDependency})`);
}
if (scores.errorHandlingGaps > 70) {
// High error handling gaps
finalScore *= this.multipliers.noErrorHandlingForRiskyOps;
appliedMultipliers.push(`Critical Error Handling Gap (×${this.multipliers.noErrorHandlingForRiskyOps})`);
}
if (scores.typeIssues > 60) {
// High type issues (likely has any types)
finalScore *= this.multipliers.hasAnyType;
appliedMultipliers.push(`Type Safety Issues (×${this.multipliers.hasAnyType})`);
}
// NEW: Critical SEO multiplier for pages
if (fileType === "page" && scores.seoProblems > 60) {
finalScore *= this.multipliers.criticalSeoMissing;
appliedMultipliers.push(`Critical SEO Missing (×${this.multipliers.criticalSeoMissing})`);
}
// NEW: Critical accessibility multiplier
if (scores.accessibilityIssues > 50) {
finalScore *= this.multipliers.criticalAccessibilityMissing;
appliedMultipliers.push(`Critical Accessibility Missing (×${this.multipliers.criticalAccessibilityMissing})`);
}
// Bonus multipliers for good practices
if (scores.errorHandlingGaps < 20 && this.hasProperErrorHandling(context)) {
finalScore *= this.multipliers.hasProperErrorHandling;
appliedMultipliers.push(`Good Error Handling (×${this.multipliers.hasProperErrorHandling})`);
}
if (scores.typeIssues < 20 && this.hasGoodTypeAnnotations(context)) {
finalScore *= this.multipliers.hasGoodTypeAnnotations;
appliedMultipliers.push(`Good Type Annotations (×${this.multipliers.hasGoodTypeAnnotations})`);
}
if (scores.cyclomaticComplexity < 30 && scores.maintainabilityIndex < 20) {
finalScore *= this.multipliers.lowComplexity;
appliedMultipliers.push(`Low Complexity Bonus (×${this.multipliers.lowComplexity})`);
}
// NEW: SEO bonus for pages with good implementation
if (fileType === "page" &&
scores.seoProblems < 20 &&
this.hasGoodSeoImplementation(context)) {
finalScore *= this.multipliers.goodSeoImplementation;
appliedMultipliers.push(`Good SEO Implementation (×${this.multipliers.goodSeoImplementation})`);
}
// NEW: Accessibility bonus
if (scores.accessibilityIssues < 15 &&
this.hasGoodAccessibilityImplementation(context)) {
finalScore *= this.multipliers.goodAccessibilityImplementation;
appliedMultipliers.push(`Good Accessibility Implementation (×${this.multipliers.goodAccessibilityImplementation})`);
}
// Ensure score doesn't exceed 100
finalScore = Math.min(finalScore, 100);
return { finalScore, appliedMultipliers };
}
/**
* Check if component has proper error handling
*/
hasProperErrorHandling(context) {
if (!context.errorHandlingAnalysis)
return false;
const componentResult = context.errorHandlingAnalysis.componentResults[context.component.name];
if (!componentResult)
return false;
// Has error boundaries or comprehensive try-catch coverage
return (componentResult.errorBoundaries.length > 0 ||
componentResult.tryCatchBlocks.length > 0 ||
componentResult.errorStates.length > 0);
}
/**
* Check if component has good type annotations
*/
hasGoodTypeAnnotations(context) {
if (!context.typeAnalysis)
return false;
const componentName = context.component.name;
// Component has prop types and is not in the "without prop types" list
return (!context.typeAnalysis.componentsWithoutPropTypes.includes(componentName) &&
context.component.props !== undefined &&
context.component.props.length > 0);
}
/**
* NEW: Check if component has good SEO implementation
*/
hasGoodSeoImplementation(context) {
if (!context.seoAnalysis)
return false;
const componentPath = context.component.fullPath;
const pageMetaTags = context.seoAnalysis.metaTags.pages[componentPath];
if (!pageMetaTags)
return false;
// Good SEO means: title, description, Open Graph, proper lengths
return (pageMetaTags.title.present &&
pageMetaTags.description.present &&
pageMetaTags.openGraph.present &&
pageMetaTags.title.length >= 30 &&
pageMetaTags.title.length <= 60 &&
pageMetaTags.description.length >= 120 &&
pageMetaTags.description.length <= 160);
}
/**
* NEW: Check if component has good accessibility implementation
*/
hasGoodAccessibilityImplementation(context) {
if (!context.seoAnalysis)
return false;
const componentPath = context.component.fullPath;
// Check for images with alt texts
const images = context.seoAnalysis.imageOptimization.images.filter((img) => img.usedInPages.includes(componentPath));
const imagesWithAlt = images.filter((img) => img.attributes.alt &&
!img.issues.some((issue) => issue.type === "missing-alt"));
// Good accessibility means: most images have alt text, no ARIA misuse
const imageAltRatio = images.length > 0 ? imagesWithAlt.length / images.length : 1;
const ariaIssues = context.seoAnalysis.semanticStructure.accessibility.aria.potentialMisuse
.length;
return imageAltRatio >= 0.8 && ariaIssues === 0;
}
/**
* Get a breakdown of the scoring for debugging/analysis with file type info
*/
getScoreBreakdown(result) {
const breakdown = [];
breakdown.push(`Final Score: ${result.finalScore}/100 (${result.fileType} file)`);
breakdown.push("");
if (result.excludedMetrics.length > 0) {
breakdown.push("Excluded Metrics (file type specific):");
result.excludedMetrics.forEach((metric) => {
breakdown.push(` • ${metric} (not applicable for ${result.fileType} files)`);
});
breakdown.push("");
}
if (Object.keys(result.appliedWeightAdjustments).length > 0) {
breakdown.push("Applied Weight Adjustments:");
Object.entries(result.appliedWeightAdjustments).forEach(([metric, multiplier]) => {
breakdown.push(` • ${metric}: ×${multiplier}`);
});
breakdown.push("");
}
breakdown.push("Individual Contributions:");
// Sort contributions by impact
const sortedContributions = Object.entries(result.weightedContributions)
.sort(([, a], [, b]) => b - a)
.filter(([, contribution]) => contribution > 0);
for (const [metric, contribution] of sortedContributions) {
const rawScore = result.individualScores[metric];
breakdown.push(` ${metric}: ${contribution.toFixed(2)} (raw: ${rawScore})`);
}
if (result.appliedMultipliers.length > 0) {
breakdown.push("");
breakdown.push("Applied Multipliers:");
result.appliedMultipliers.forEach((multiplier) => {
breakdown.push(` • ${multiplier}`);
});
}
return breakdown.join("\n");
}
}
exports.ScoringAggregator = ScoringAggregator;