From ce270a2098668a0b009fbc33d9ed590c2c5b4d74 Mon Sep 17 00:00:00 2001 From: Benny Huang Date: Wed, 12 Nov 2025 15:31:32 -0500 Subject: [PATCH 1/3] refactored analyzer.ts --- src/lib/analyzer.ts | 96 ++++++++++++++------------------------------- src/lib/types.ts | 21 ++++++++++ 2 files changed, 51 insertions(+), 66 deletions(-) diff --git a/src/lib/analyzer.ts b/src/lib/analyzer.ts index f85a379..1585719 100644 --- a/src/lib/analyzer.ts +++ b/src/lib/analyzer.ts @@ -1,18 +1,6 @@ import { CONFIG } from './config'; import { collectPackageData } from './data/collect'; -import type { Config, PackageData } from './types'; - -/** - * Properties analyzer result - */ -export interface ScoreResult { - readonly packageName: string; // "aws-cdk" - readonly version: string; // "1.2.3" - readonly totalScore: number; // 85 - readonly pillarScores: Record; // { "popularity": 42 } - readonly signalScores: Record>; // { "popularity": { "weeklyDownloads": 4, "githubStars": 2 } } - readonly signalWeights: Record>; // { "popularity": { "weeklyDownloads": 3, "githubStars": 2 } } -} +import type { ScoreResult, Config, PackageData, PillarScores } from './types'; export class ConstructAnalyzer { private config: Config; @@ -23,36 +11,30 @@ export class ConstructAnalyzer { public async analyzePackage(packageName: string): Promise { const packageData = await collectPackageData(packageName); - const version = packageData.version; - const { signalScores, pillarScores } = await this.calculateSignalScores(packageData); const normalizedPillarScores = this.normalizePillarScores(pillarScores); - const totalScore = this.calculateTotalScore(normalizedPillarScores); - const signalWeights = this.getSignalWeights(); return { packageName, - version, - totalScore, + version: packageData.version, + totalScore: this.calculateTotalScore(normalizedPillarScores), pillarScores: normalizedPillarScores, signalScores, - signalWeights, + signalWeights: this.getSignalWeights(), }; } private async calculateSignalScores(packageData: PackageData) { const signalScores: Record> = {}; - const pillarScores: Record = {}; + const pillarScores: PillarScores = { MAINTENANCE: 0, QUALITY: 0, POPULARITY: 0 }; for (const pillar of this.config.pillars) { for (const signal of pillar.signals) { - const rawValue = packageData[signal.name]; - - const level = signal.benchmarks(rawValue); + const level = signal.benchmarks(packageData[signal.name]); const points = this.convertLevelToPoints(level, signal.name); - this.updateSignalScore(signalScores, pillar.name, signal.name, level ?? 1); - this.updatePillarScore(pillarScores, pillar.name, points, signal.weight); + (signalScores[pillar.name] ??= {})[signal.name] = level ?? 1; + pillarScores[pillar.name as keyof PillarScores] += points * signal.weight; } } @@ -67,62 +49,44 @@ export class ConstructAnalyzer { return (level - 1) * 25; } - private updateSignalScore(signalScores: Record>, pillar: string, signalName: string, starRating: number): void { - (signalScores[pillar] ??= {})[signalName] = starRating; - } - - private updatePillarScore(pillarScores: Record, pillar: string, points: number, weight: number): void { - const weightedScore = points * weight; - pillarScores[pillar] = (pillarScores[pillar] ?? 0) + weightedScore; - } - - private normalizePillarScores(pillarScores: Record): Record { - const normalizedScores: Record = {}; - - const pillarEntries = Object.entries(pillarScores); - for (const [pillar, weightedSum] of pillarEntries) { - const totalWeight = this.getTotalWeightForPillar(pillar); - const normalizedScore = totalWeight > 0 ? Math.min(100, weightedSum / totalWeight) : 0; - normalizedScores[pillar] = Math.round(normalizedScore); - } - - return normalizedScores; - } + private normalizePillarScores(pillarScores: PillarScores): PillarScores { + const normalize = (pillar: keyof PillarScores): number => { + const totalWeight = this.config.pillars + .find(p => p.name === pillar) + ?.signals.reduce((sum, signal) => sum + signal.weight, 0) ?? 0; - private getTotalWeightForPillar(pillarName: string): number { - const pillar = this.config.pillars.find(p => p.name === pillarName); - if (!pillar) return 0; + return totalWeight > 0 + ? Math.round(Math.min(100, pillarScores[pillar] / totalWeight)) + : 0; + }; - return pillar.signals.reduce((sum, signal) => sum + signal.weight, 0); + return { + MAINTENANCE: normalize('MAINTENANCE'), + QUALITY: normalize('QUALITY'), + POPULARITY: normalize('POPULARITY'), + }; } - private calculateTotalScore(pillarScores: Record): number { - if (Object.keys(pillarScores).length === 0) return 0; - + private calculateTotalScore(pillarScores: PillarScores): number { let weightedSum = 0; let totalWeight = 0; - for (const [pillarName, score] of Object.entries(pillarScores)) { - const pillar = this.config.pillars.find(p => p.name === pillarName); - if (pillar) { - weightedSum += score * pillar.weight; - totalWeight += pillar.weight; - } + for (const pillar of this.config.pillars) { + const score = pillarScores[pillar.name as keyof PillarScores]; + weightedSum += score * pillar.weight; + totalWeight += pillar.weight; } return totalWeight > 0 ? Math.round(weightedSum / totalWeight) : 0; } - /** - * Extract signal weights from config in the same structure as signalScores - */ private getSignalWeights(): Record> { const signalWeights: Record> = {}; for (const pillar of this.config.pillars) { - for (const signal of pillar.signals) { - (signalWeights[pillar.name] ??= {})[signal.name] = signal.weight; - } + signalWeights[pillar.name] = Object.fromEntries( + pillar.signals.map(signal => [signal.name, signal.weight]), + ); } return signalWeights; diff --git a/src/lib/types.ts b/src/lib/types.ts index 5ecebf2..af8ebb4 100644 --- a/src/lib/types.ts +++ b/src/lib/types.ts @@ -2,6 +2,18 @@ * Configuration type definitions for the CDK Construct Analyzer */ +/** + * Properties analyzer result + */ +export interface ScoreResult { + readonly packageName: string; // "aws-cdk" + readonly version: string; // "1.2.3" + readonly totalScore: number; // 85 + readonly pillarScores: Readonly; // { "MAINTENANCE": 42, "QUALITY": 85, "POPULARITY": 67 } + readonly signalScores: Record>; // { "popularity": { "weeklyDownloads": 4, "githubStars": 2 } } + readonly signalWeights: Record>; // { "popularity": { "weeklyDownloads": 3, "githubStars": 2 } } +} + /** * Benchmark function type for converting raw values to quality levels (1-5) */ @@ -34,6 +46,15 @@ export interface Config { readonly pillars: PillarConfig[]; } +/** + * Pillar scores for the three main evaluation pillars + */ +export interface PillarScores { + MAINTENANCE: number; + QUALITY: number; + POPULARITY: number; +} + /** * Checklist item configuration for scoring */ From a0b4ccf3318cf999c5e2d436684effe84ae77dfd Mon Sep 17 00:00:00 2001 From: Benny Huang Date: Thu, 13 Nov 2025 16:36:31 -0500 Subject: [PATCH 2/3] refactored so all hardcoded elements are in config.ts --- src/lib/analyzer.ts | 86 ++++++++++++++++++++++++++------------------- src/lib/config.ts | 9 +++++ src/lib/types.ts | 13 ++++--- 3 files changed, 66 insertions(+), 42 deletions(-) diff --git a/src/lib/analyzer.ts b/src/lib/analyzer.ts index 79e189c..a1b6969 100644 --- a/src/lib/analyzer.ts +++ b/src/lib/analyzer.ts @@ -1,6 +1,6 @@ -import { CONFIG } from './config'; +import { CONFIG, Pillar } from './config'; import { collectPackageData } from './data/collect'; -import type { ScoreResult, Config, PackageData, PillarScores } from './types'; +import type { ScoreResult, Config, PackageData, SignalWeights, PillarScores } from './types'; export class ConstructAnalyzer { private config: Config; @@ -11,30 +11,43 @@ export class ConstructAnalyzer { public async analyzePackage(packageName: string, weights?: SignalWeights): Promise { const packageData = await collectPackageData(packageName); - const { signalScores, pillarScores } = await this.calculateSignalScores(packageData); - const normalizedPillarScores = this.normalizePillarScores(pillarScores); + const version = packageData.version; + + const { signalScores, pillarScores, totalScore } = await this.calculateSignalScores(packageData, weights); + const normalizedPillarScores = this.normalizePillarScores(pillarScores, weights) as Readonly; + const signalWeights = this.getSignalWeights(weights); return { packageName, - version: packageData.version, - totalScore: this.calculateTotalScore(normalizedPillarScores), + version, + totalScore, pillarScores: normalizedPillarScores, signalScores, - signalWeights: this.getSignalWeights(), + signalWeights, }; } private async calculateSignalScores(packageData: PackageData, weights?: SignalWeights) { const signalScores: Record> = {}; - const pillarScores: PillarScores = { MAINTENANCE: 0, QUALITY: 0, POPULARITY: 0 }; + const pillarScores = Object.fromEntries( + Object.values(Pillar).map(pillar => [pillar, 0]), + ) as PillarScores; + + let totalWeightedSum = 0; + let totalWeight = 0; for (const pillar of this.config.pillars) { for (const signal of pillar.signals) { const level = signal.benchmarks(packageData[signal.name]); const points = this.convertLevelToPoints(level, signal.name); + const weight = weights?.[signal.name] ?? signal.defaultWeight; + (signalScores[pillar.name] ??= {})[signal.name] = level ?? 1; - pillarScores[pillar.name as keyof PillarScores] += points * signal.weight; + pillarScores[pillar.name as Pillar] = (pillarScores[pillar.name as Pillar] || 0) + points * weight; + + totalWeightedSum += points * weight; + totalWeight += weight; } } @@ -59,44 +72,43 @@ export class ConstructAnalyzer { return (level - 1) * 25; } - private normalizePillarScores(pillarScores: PillarScores): PillarScores { - const normalize = (pillar: keyof PillarScores): number => { - const totalWeight = this.config.pillars - .find(p => p.name === pillar) - ?.signals.reduce((sum, signal) => sum + signal.weight, 0) ?? 0; + private normalizePillarScores(pillarScores: Record, weights?: SignalWeights): PillarScores { + const normalizedScores = Object.fromEntries( + Object.values(Pillar).map(pillar => [pillar, 0]), + ) as PillarScores; - return totalWeight > 0 - ? Math.round(Math.min(100, pillarScores[pillar] / totalWeight)) - : 0; - }; + const pillarEntries = Object.entries(pillarScores); + for (const [pillar, weightedSum] of pillarEntries) { + const totalWeight = this.getTotalWeightForPillar(pillar, weights); + const normalizedScore = totalWeight > 0 ? Math.min(100, weightedSum / totalWeight) : 0; + normalizedScores[pillar as keyof PillarScores] = Math.round(normalizedScore); + } - return { - MAINTENANCE: normalize('MAINTENANCE'), - QUALITY: normalize('QUALITY'), - POPULARITY: normalize('POPULARITY'), - }; + return normalizedScores; } - private calculateTotalScore(pillarScores: PillarScores): number { - let weightedSum = 0; - let totalWeight = 0; - - for (const pillar of this.config.pillars) { - const score = pillarScores[pillar.name as keyof PillarScores]; - weightedSum += score * pillar.weight; - totalWeight += pillar.weight; - } + private getTotalWeightForPillar(pillarName: string, weights?: SignalWeights): number { + const pillar = this.config.pillars.find(p => p.name === pillarName); + if (!pillar) return 0; - return totalWeight > 0 ? Math.round(weightedSum / totalWeight) : 0; + return pillar.signals.reduce((sum, signal) => { + const weight = weights?.[signal.name] ?? signal.defaultWeight; + return sum + weight; + }, 0); } - private getSignalWeights(): Record> { + /** + * Extract signal weights from config in the same structure as signalScores + * Uses custom weights when provided, otherwise falls back to default weights + */ + private getSignalWeights(weights?: SignalWeights): Record> { const signalWeights: Record> = {}; for (const pillar of this.config.pillars) { - signalWeights[pillar.name] = Object.fromEntries( - pillar.signals.map(signal => [signal.name, signal.weight]), - ); + for (const signal of pillar.signals) { + const weight = weights?.[signal.name] ?? signal.defaultWeight; + (signalWeights[pillar.name] ??= {})[signal.name] = weight; + } } return signalWeights; diff --git a/src/lib/config.ts b/src/lib/config.ts index c5a6d3a..3e9339f 100644 --- a/src/lib/config.ts +++ b/src/lib/config.ts @@ -1,6 +1,15 @@ import { categorizeByChecklist, categorizeHigherIsBetter, categorizeLowerIsBetter } from './scoring'; import type { Config, DocumentationCompleteness, VersionStability, TestsData, ReleaseNotesData } from './types'; +/** + * Pillar enum - single source of truth for pillar names + */ +export enum Pillar { + MAINTENANCE = 'MAINTENANCE', + QUALITY = 'QUALITY', + POPULARITY = 'POPULARITY', +} + /** * Main configuration object with all signals and their benchmarks */ diff --git a/src/lib/types.ts b/src/lib/types.ts index 83cded2..7cc3aa3 100644 --- a/src/lib/types.ts +++ b/src/lib/types.ts @@ -2,6 +2,8 @@ * Configuration type definitions for the CDK Construct Analyzer */ +import type { Pillar } from './config'; + /** * Properties analyzer result */ @@ -47,12 +49,13 @@ export interface Config { /** * Pillar scores for the three main evaluation pillars + * Dynamically generated from Pillar enum */ -export interface PillarScores { - MAINTENANCE: number; - QUALITY: number; - POPULARITY: number; -} +export type PillarScores = { + [K in Pillar]: number; +}; + +export type SignalWeights = Record; /** * Checklist item configuration for scoring From feff7c8aed13bd4872e9d03029168419054c5ab4 Mon Sep 17 00:00:00 2001 From: Benny Huang Date: Mon, 17 Nov 2025 16:59:14 -0500 Subject: [PATCH 3/3] changed config pillar name to use enum --- src/lib/config.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/lib/config.ts b/src/lib/config.ts index 3e9339f..1a11004 100644 --- a/src/lib/config.ts +++ b/src/lib/config.ts @@ -16,7 +16,7 @@ export enum Pillar { export const CONFIG: Config = { pillars: [ { - name: 'MAINTENANCE', + name: Pillar.MAINTENANCE, description: 'Measures how actively maintained and updated the package is', signals: [ { @@ -48,7 +48,7 @@ export const CONFIG: Config = { ], }, { - name: 'QUALITY', + name: Pillar.QUALITY, description: 'Measures the overall quality and reliability of the package', signals: [ { @@ -101,7 +101,7 @@ export const CONFIG: Config = { ], }, { - name: 'POPULARITY', + name: Pillar.POPULARITY, description: 'Measures how widely adopted and used the package is', signals: [ {