diff --git a/README.md b/README.md index 0c61529..e33fff8 100644 --- a/README.md +++ b/README.md @@ -76,12 +76,13 @@ SUBSCORES === 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 .................................... ★★★★☆ 15 +— Repo stars .................................... ★★★★☆ 10 — Contributors .................................. ★★★★☆ 5 ``` @@ -152,6 +153,7 @@ Signals that are visible in the repo/package that showcases quality: * Documentation Completeness: High quality document * Tests checklist (unit/snapshot): Tests ensure correctness and prevent regressions. +* Author Track Record: Measures how many packages the author has published, more published packages often indicate greater experience. * Changelog includes feats/fixes: Checks if there are feats/fixes published in the release notes. * Stable versioning (>=1.x.x, not deprecated): Indicates API maturity and stability.ation makes the project easier to adopt and use (README, API References, Usage Examples). diff --git a/src/lib/config.ts b/src/lib/config.ts index 30b9c94..c5a6d3a 100644 --- a/src/lib/config.ts +++ b/src/lib/config.ts @@ -44,7 +44,7 @@ export const CONFIG: Config = { signals: [ { name: 'documentationCompleteness', - defaultWeight: 10, + defaultWeight: 5, description: 'Presence of README, API reference, and usage examples', benchmarks: (docData: DocumentationCompleteness) => categorizeByChecklist( { @@ -64,6 +64,12 @@ export const CONFIG: Config = { snapshotTests: { present: testsData.hasSnapshotTests, value: 2 }, }), }, + { + name: 'authorPackageCount', + defaultWeight: 5, + description: 'Highest package count among authors', + benchmarks: (packageCount: number) => categorizeHigherIsBetter([20, 11, 5, 2], packageCount), + }, { name: 'releaseNotesIncludeFeatsAndFixes', defaultWeight: 5, diff --git a/src/lib/data/collect.ts b/src/lib/data/collect.ts index 098ca7f..0070a82 100644 --- a/src/lib/data/collect.ts +++ b/src/lib/data/collect.ts @@ -11,6 +11,7 @@ import { calculateReleaseFrequency } from '../utils/releases'; interface RawPackageData { readonly npm: NpmPackageData; readonly downloads: NpmDownloadData; + readonly authorPackageCount?: number; readonly github?: GitHubRepository; } @@ -23,6 +24,7 @@ async function fetchAllData(packageName: string): Promise { await npmCollector.fetchPackage(packageName); const npmData = npmCollector.getPackageData(); const downloadData = await npmCollector.fetchDownloadData(); + const authorPackageCountData = await npmCollector.fetchAuthorPackageCount(); const repoInfo = extractRepoInfo(npmData.repository.url); const githubRepo = new GitHubRepo(repoInfo.owner, repoInfo.repo); @@ -43,6 +45,7 @@ async function fetchAllData(packageName: string): Promise { return { npm: npmData, downloads: downloadData, + authorPackageCount: authorPackageCountData, ...(githubData && { github: githubData }), }; } @@ -57,6 +60,7 @@ function processPackageData(rawData: RawPackageData): PackageData { return { version: rawData.npm.version, weeklyDownloads: rawData.downloads.downloads, + authorPackageCount: rawData.authorPackageCount, stableVersioning: { isStableMajorVersion: parseInt(majorVersion, 10) >= 1, hasMinorReleases: parseInt(minorVersion, 10) >= 1, @@ -73,6 +77,7 @@ function processPackageData(rawData: RawPackageData): PackageData { numberOfContributors_Maintenance: processContributorsData(repository.commits), documentationCompleteness: analyzeDocumentationCompleteness(repository), testsChecklist: analyzeTestsPresence(repository), + authorPackageCount: rawData.authorPackageCount, releaseNotesIncludeFeatsAndFixes: analyzeReleaseNotesContent(repository), weeklyDownloads: rawData.downloads.downloads, githubStars: repository.stargazerCount ?? 0, diff --git a/src/lib/data/npm.ts b/src/lib/data/npm.ts index 0fe177c..bce9f5b 100644 --- a/src/lib/data/npm.ts +++ b/src/lib/data/npm.ts @@ -8,6 +8,7 @@ export interface NpmPackageData { }; readonly isDeprecated: boolean; readonly hasProvenance?: boolean; + readonly maintainers?: Array<{ name: string }>; } export interface NpmDownloadData { @@ -37,6 +38,7 @@ export class NpmCollector { repository: response.repository, isDeprecated: Boolean(versionData?.deprecated), hasProvenance, + maintainers: response.maintainers, }; } @@ -59,4 +61,38 @@ export class NpmCollector { return await response.json() as NpmDownloadData; } + + async fetchAuthorPackageCount(): Promise { + // Get the best (highest package count) maintainer + const maintainers = this.packageData?.maintainers; + if (!maintainers || maintainers.length === 0) { + return undefined; + } + + try { + const counts = await Promise.all( + maintainers.map(async (maintainer) => { + if (!maintainer?.name) { + return 0; + } + try { + const response = await fetch(`https://registry.npmjs.org/-/v1/search?text=maintainer:${encodeURIComponent(maintainer.name)}&size=1`); + if (!response.ok) { + return undefined; + } + const data = await response.json() as any; + + return data.total ?? 0; + } catch { + return 0; + } + }), + ); + const maxCount = Math.max(...counts); + // Return undefined if all requests failed + return maxCount > 0 ? maxCount : undefined; + } catch { + return undefined; + } + } } \ No newline at end of file diff --git a/src/lib/types.ts b/src/lib/types.ts index 131d452..a391959 100644 --- a/src/lib/types.ts +++ b/src/lib/types.ts @@ -51,6 +51,7 @@ export type PackageData = { readonly numberOfContributors_Maintenance?: number; readonly documentationCompleteness?: DocumentationCompleteness; readonly testsChecklist?: TestsData; + readonly authorTrackRecord?: number; readonly releaseNotesIncludeFeatsAndFixes?: ReleaseNotesData; readonly weeklyDownloads?: number; readonly githubStars?: number; diff --git a/test/lib/data/collect.test.ts b/test/lib/data/collect.test.ts index 40217fd..d44f6b9 100644 --- a/test/lib/data/collect.test.ts +++ b/test/lib/data/collect.test.ts @@ -42,6 +42,7 @@ describe('collectPackageData', () => { fetchPackage: jest.fn().mockResolvedValue(undefined), getPackageData: jest.fn().mockReturnValue(mockNpmData), fetchDownloadData: jest.fn().mockResolvedValue(mockDownloadData), + fetchAuthorPackageCount: jest.fn().mockResolvedValue(42), }; const mockGitHubData = { @@ -99,6 +100,7 @@ describe('collectPackageData', () => { hasUnitTests: false, hasSnapshotTests: false, }, + authorPackageCount: 42, releaseNotesIncludeFeatsAndFixes: { hasFeats: true, hasFixes: true, @@ -122,6 +124,7 @@ describe('collectPackageData', () => { fetchPackage: jest.fn().mockResolvedValue(undefined), getPackageData: jest.fn().mockReturnValue(mockNpmData), fetchDownloadData: jest.fn().mockResolvedValue(mockDownloadData), + fetchAuthorPackageCount: jest.fn().mockResolvedValue(42), }; const mockGitHubInstance = { @@ -137,6 +140,7 @@ describe('collectPackageData', () => { expect(result).toEqual({ version: '1.0.0', + authorPackageCount: 42, weeklyDownloads: 10000, stableVersioning: { isStableMajorVersion: true, diff --git a/test/lib/data/npm.test.ts b/test/lib/data/npm.test.ts index 9024c7e..b0369a1 100644 --- a/test/lib/data/npm.test.ts +++ b/test/lib/data/npm.test.ts @@ -285,4 +285,151 @@ describe('NpmCollector', () => { ); }); }); + + describe('fetchAuthorPackageCount', () => { + test('should fetch author package count successfully', async () => { + const mockPackageResponse = { + 'name': 'test-package', + 'dist-tags': { latest: '1.0.0' }, + 'repository': { + type: 'git', + url: 'https://github.com/test/repo', + }, + 'maintainers': [ + { name: 'johndoe' }, + ], + 'versions': { + '1.0.0': { + name: 'test-package', + version: '1.0.0', + }, + }, + }; + + mockedFetch.mockResolvedValueOnce({ + ok: true, + json: async () => mockPackageResponse, + } as Response); + + await collector.fetchPackage('test-package'); + + const mockSearchResponse = { + objects: [ + { package: { name: 'package1' } }, + { package: { name: 'package2' } }, + ], + total: 42, + }; + + mockedFetch.mockResolvedValueOnce({ + ok: true, + json: async () => mockSearchResponse, + } as Response); + + const result = await collector.fetchAuthorPackageCount(); + + expect(mockedFetch).toHaveBeenCalledWith( + 'https://registry.npmjs.org/-/v1/search?text=maintainer:johndoe&size=1', + ); + expect(result).toBe(42); + }); + + test('should return undefined when no maintainers', async () => { + const mockPackageResponse = { + 'name': 'test-package', + 'dist-tags': { latest: '1.0.0' }, + 'repository': { + type: 'git', + url: 'https://github.com/test/repo', + }, + 'versions': { + '1.0.0': { + name: 'test-package', + version: '1.0.0', + }, + }, + }; + + mockedFetch.mockResolvedValueOnce({ + ok: true, + json: async () => mockPackageResponse, + } as Response); + + await collector.fetchPackage('test-package'); + + const result = await collector.fetchAuthorPackageCount(); + + expect(result).toBeUndefined(); + }); + + test('should return undefined when search API fails', async () => { + const mockPackageResponse = { + 'name': 'test-package', + 'dist-tags': { latest: '1.0.0' }, + 'repository': { + type: 'git', + url: 'https://github.com/test/repo', + }, + 'maintainers': [ + { name: 'johndoe' }, + ], + 'versions': { + '1.0.0': { + name: 'test-package', + version: '1.0.0', + }, + }, + }; + + mockedFetch.mockResolvedValueOnce({ + ok: true, + json: async () => mockPackageResponse, + } as Response); + + await collector.fetchPackage('test-package'); + + mockedFetch.mockResolvedValueOnce({ + ok: false, + status: 500, + statusText: 'Internal Server Error', + } as Response); + + const result = await collector.fetchAuthorPackageCount(); + + expect(result).toBeUndefined(); + }); + + test('should return undefined when search API throws error', async () => { + const mockPackageResponse = { + 'name': 'test-package', + 'dist-tags': { latest: '1.0.0' }, + 'repository': { + type: 'git', + url: 'https://github.com/test/repo', + }, + 'maintainers': [ + { name: 'johndoe' }, + ], + 'versions': { + '1.0.0': { + name: 'test-package', + version: '1.0.0', + }, + }, + }; + + mockedFetch.mockResolvedValueOnce({ + ok: true, + json: async () => mockPackageResponse, + } as Response); + + await collector.fetchPackage('test-package'); + + mockedFetch.mockRejectedValueOnce(new Error('Network error')); + + const result = await collector.fetchAuthorPackageCount(); + + expect(result).toBeUndefined(); + }); + }); }); \ No newline at end of file