Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 29 additions & 27 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,55 +35,56 @@ You can run it locally on any library published to npm by providing its package
> cdk-construct-analyzer cdk-ecr-deployment

LIBRARY: @cdklabs/cdk-ecr-deployment
VERSION: 0.0.421
VERSION: 4.0.3

OVERALL SCORE: 76/100

---

SUBSCORES
Maintenance : 66/100
Quality : 75/100
Popularity : 88/100
MAINTENANCE : 67/100
QUALITY : 80/100
POPULARITY : 88/100
```

##### Details
Add `--details` for a detailed breakdown:

```
> cdk-construct-analyzer cdk-ecr-deployment --details
> cdk-construct-analyzer cdk-ecr-deployment --details

LIBRARY: cdk-ecr-deployment
VERSION: 0.0.421
VERSION: 4.0.3

OVERALL SCORE: 76/100

---

SUBSCORES
Maintenance : 66/100
Quality : 75/100
Popularity : 88/100
MAINTENANCE : 67/100
QUALITY : 80/100
POPULARITY : 88/100

---

=== Maintenance === SCORE WEIGHT
— Time to first response......................... ★★☆☆☆ 15
— Provenance Verification ....................... ★★★★★ 10
— Release Frequency ............................. ★★★★☆ 10
— Number of Contributors ........................ ★★★★☆ 10

=== Quality === SCORE WEIGHT
— Documentation Completeness .................... ★★★★★ 5
— Tests checklist (unit/snapshot) ............... ★★★☆☆ 5
— Author Track Record ........................... ★★★★★ 5
— Stable versioning ............................. ★★★★★ 5
— Changelog includes feats/fixes ................ ★★★★★ 5

=== Popularity === SCORE WEIGHT
— Weekly Downloads .............................. ★★★★★ 15
— Repo stars .................................... ★★★★☆ 10
— Contributors .................................. ★★★★☆ 5
=== MAINTENANCE === SCORE WEIGHT
— Time To First Response ............................ ★★☆☆☆ 15
— Provenance Verification ........................... ★★★★★ 10
— Release Frequency ................................. ★★★★☆ 10
— Number Of Contributors - Maintenance .............. ★★★★☆ 5
— Open Issues Ratio ................................. ★★★★★ 5

=== QUALITY === SCORE WEIGHT
— Documentation Completeness ........................ ★★★★☆ 5
— Tests Checklist ................................... ★★★☆☆ 5
— Author Package Count .............................. ★★★★★ 5
— Release Notes Include Feats And Fixes ............. ★★★★★ 5
— Stable Versioning ................................. ★★★★☆ 5

=== POPULARITY === SCORE WEIGHT
— Weekly Downloads .................................. ★★★★★ 15
— Github Stars ...................................... ★★★★☆ 10
— Number Of Contributors - Popularity ............... ★★★★☆ 5
```

#### Programmatic Access
Expand Down Expand Up @@ -144,6 +145,7 @@ Helps determine if the project is active and healthy, or abandoned. Signals incl

* Time to first response: Fast issue resolution reflects active, responsive maintainers.
* Provenance Verification: Verifies package authenticity and supply chain security.
* Open issues / total issues: A lower ratio of open issues indicates backlog health and follow through normalized by repository popularity. Note: 0 total issues scores worst (100% ratio) as it suggests no community engagement.
* Release Frequency: Regular releases signal iteration, patching, and progress.
* Number of Contributors: More contributors reduce risk of abandonment and reflect shared maintenance.

Expand Down
9 changes: 8 additions & 1 deletion src/lib/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,10 +32,17 @@ export const CONFIG: Config = {
},
{
name: 'numberOfContributors_Maintenance',
defaultWeight: 10,
defaultWeight: 5,
description: 'Number of Contributors in the past year',
benchmarks: (contributors: number) => categorizeHigherIsBetter([8, 2, 1, 1], contributors),
},
{
name: 'openIssuesRatio',
defaultWeight: 5,
description: 'Open issues / total issues',
benchmarks: (ratio: number) => categorizeLowerIsBetter([25, 50, 75, 90], ratio),
// ratio will be 100 if 0 total issues
},
],
},
{
Expand Down
14 changes: 11 additions & 3 deletions src/lib/data/collect.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,16 @@
import { PackageData, GitHubRepository } from '../types';
import { GitHubRepo } from './github-repo';
import { NpmCollector, NpmPackageData, NpmDownloadData } from './npm';
import { extractRepoInfo, processContributorsData, analyzeDocumentationCompleteness, analyzeTestsPresence, analyzeReleaseNotesContent } from '../utils';
import { calculateTimeToFirstResponse } from '../utils/issues';
import { calculateReleaseFrequency } from '../utils/releases';
import {
extractRepoInfo,
processContributorsData,
analyzeDocumentationCompleteness,
analyzeTestsPresence,
calculateTimeToFirstResponse,
calculateOpenIssuesRatio,
calculateReleaseFrequency,
analyzeReleaseNotesContent,
} from '../utils';

/**
* Raw data fetched from external APIs before processing
Expand Down Expand Up @@ -90,6 +97,7 @@ function processPackageData(rawData: RawPackageData): PackageData {
provenanceVerification: rawData.npm.hasProvenance,
numberOfContributors_Popularity: processContributorsData(repository.commits),
releaseFrequency: calculateReleaseFrequency(repository.releases),
openIssuesRatio: calculateOpenIssuesRatio(repository.openIssuesCount, repository.totalIssuesCount),
};
}

Expand Down
13 changes: 13 additions & 0 deletions src/lib/data/github-repo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,15 @@ export class GitHubRepo {
}
}

# Get issue counts for maintenance signal
openIssues: issues(states: OPEN) {
totalCount
}

allIssues: issues {
totalCount
}

# Get releases from the last year for release frequency calculation
releases(first: 100, orderBy: {field: CREATED_AT, direction: DESC}) {
nodes {
Expand Down Expand Up @@ -130,6 +139,8 @@ export class GitHubRepo {
commits: repository.defaultBranchRef?.target?.history?.nodes ?? [],
issues: repository.issues?.nodes ?? [],
releases: repository.releases?.nodes ?? [],
openIssuesCount: repository.openIssues?.totalCount,
totalIssuesCount: repository.allIssues?.totalCount,
} as GitHubRepository,
},
};
Expand Down Expand Up @@ -170,6 +181,8 @@ export class GitHubRepo {
commits: repository.defaultBranchRef?.target?.history?.nodes ?? [],
issues: repository.issues?.nodes ?? [],
releases: repository.releases?.nodes ?? [],
openIssuesCount: repository.openIssues?.totalCount,
totalIssuesCount: repository.allIssues?.totalCount,
} as GitHubRepository,
},
};
Expand Down
3 changes: 3 additions & 0 deletions src/lib/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ export type PackageData = {
readonly timeToFirstResponse?: number;
readonly provenanceVerification?: boolean;
readonly releaseFrequency?: number;
readonly openIssuesRatio?: number;
} & Record<string, any>;

export type VersionStability = {
Expand Down Expand Up @@ -142,4 +143,6 @@ export interface GitHubRepository {
readonly commits?: GitHubCommit[];
readonly issues?: GitHubIssue[];
readonly releases?: GitHubRelease[];
readonly openIssuesCount?: number;
readonly totalIssuesCount?: number;
}
4 changes: 3 additions & 1 deletion src/lib/utils/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,6 @@ export * from './contributors';
export * from './documentation';
export * from './tests';
export * from './releases';
export * from './releaseNotes';
export * from './issues';
export * from './contributors';
export * from './releaseNotes';
20 changes: 20 additions & 0 deletions src/lib/utils/issues.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,3 +50,23 @@ export function calculateTimeToFirstResponse(issues?: GitHubIssue[]): number | u
const average = responseTimes.reduce((sum, time) => sum + time, 0) / responseTimes.length;
return Math.round(average * 10) / 10; // Round to 1 decimal places for better precision
}

/**
* Calculate the percentage of open issues relative to total issues
*
* Returns undefined when:
* - openIssuesCount or totalIssuesCount is undefined
* - totalIssuesCount is 0 (no issues to calculate ratio)
*/
export function calculateOpenIssuesRatio(openIssuesCount?: number, totalIssuesCount?: number): number {
if (openIssuesCount === undefined || totalIssuesCount === undefined) {
return 100;
}

if (totalIssuesCount === 0) {
return 100;
}

const ratio = (openIssuesCount / totalIssuesCount) * 100;
return Math.round(ratio * 10) / 10; // Round to 1 decimal place
}
3 changes: 3 additions & 0 deletions test/lib/data/collect.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,8 @@ describe('collectPackageData', () => {
{ publishedAt: '2025-09-01T00:00:00Z', tagName: 'v0.9.0', description: 'Minor updates and documentation' },
{ publishedAt: '2025-08-01T00:00:00Z', tagName: 'v0.8.0', description: 'Initial release' },
],
openIssuesCount: 10,
totalIssuesCount: 100,
};

const mockGitHubInstance = {
Expand Down Expand Up @@ -116,6 +118,7 @@ describe('collectPackageData', () => {
provenanceVerification: true,
releaseFrequency: 3,
timeToFirstResponse: undefined,
openIssuesRatio: 10,
});
});

Expand Down
38 changes: 37 additions & 1 deletion test/lib/utils/issues.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { GitHubIssue } from '../../../src/lib/types';
import { calculateTimeToFirstResponse } from '../../../src/lib/utils/issues';
import { calculateTimeToFirstResponse, calculateOpenIssuesRatio } from '../../../src/lib/utils/issues';

describe('calculateTimeToFirstResponse', () => {
const createMockIssue = (
Expand Down Expand Up @@ -84,4 +84,40 @@ describe('calculateTimeToFirstResponse', () => {
const result = calculateTimeToFirstResponse(issues);
expect(result).toBeCloseTo(0.0, 1);
});
});

describe('calculateOpenIssuesRatio', () => {
test('should return 100 when openIssuesCount is undefined', () => {
expect(calculateOpenIssuesRatio(undefined, 100)).toBe(100.0);
});

test('should return 100 when totalIssuesCount is undefined', () => {
expect(calculateOpenIssuesRatio(10, undefined)).toBe(100.0);
});

test('should return 100 when both counts are undefined', () => {
expect(calculateOpenIssuesRatio(undefined, undefined)).toBe(100.0);
});

test('should return 100 when totalIssuesCount is 0', () => {
expect(calculateOpenIssuesRatio(0, 0)).toBe(100.0);
});

test('should calculate ratio correctly for all issues open', () => {
expect(calculateOpenIssuesRatio(100, 100)).toBe(100.0);
});

test('should calculate ratio correctly for no open issues', () => {
expect(calculateOpenIssuesRatio(0, 100)).toBe(0.0);
});

test('should round to 1 decimal place', () => {
expect(calculateOpenIssuesRatio(33, 100)).toBe(33.0);
expect(calculateOpenIssuesRatio(1, 3)).toBe(33.3);
});

test('should calculate awkward ratios correctly', () => {
expect(calculateOpenIssuesRatio(6, 35)).toBe(17.1);
expect(calculateOpenIssuesRatio(7, 23)).toBe(30.4);
});
});
Loading