Skip to content
Open
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
2,474 changes: 2,399 additions & 75 deletions package-lock.json

Large diffs are not rendered by default.

12 changes: 10 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview"
"preview": "vite preview",
"test": "vitest",
"coverage": "vitest run --coverage"
},
"dependencies": {
"@tailwindcss/vite": "^4.3.0",
Expand All @@ -19,7 +21,13 @@
"tailwindcss": "^4.3.0"
},
"devDependencies": {
"@testing-library/jest-dom": "^7.0.0",
"@testing-library/react": "^16.3.2",
"@testing-library/user-event": "^14.6.1",
"@vitejs/plugin-react": "^4.3.1",
"vite": "^5.4.6"
"@vitest/coverage-v8": "^4.1.10",
"jsdom": "^29.1.1",
"vite": "^5.4.6",
"vitest": "^4.1.10"
Comment on lines +24 to +31

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🌐 Web query:

Does vitest 4.1.10 require vite 6 or higher as a peer dependency?

💡 Result:

No, Vitest 4.1.10 does not require Vite 6 (or higher) specifically as a peer dependency. Instead, Vite is listed as a standard dependency in the Vitest package, meaning it is required as a core component for Vitest to function [1][2][3]. While Vitest 4.0 and later versions require Vite >= 6.0.0 to operate [4][5], this requirement is enforced through the package's dependencies rather than as a peer dependency [2][3]. Some internal sub-packages of Vitest (such as @vitest/mocker) do utilize peer dependencies to manage their relationship with Vite [6][7], but the main vitest package itself maintains Vite as a direct dependency [1]. In summary, you must have a compatible version of Vite (Vite 6 or higher for Vitest 4.x) installed in your project for Vitest to work, but it is not technically a peer dependency of the main vitest package [1][2][4].

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== package.json relevant dependency lines =="
if [ -f package.json ]; then
  nl -ba package.json | sed -n '1,80p'
else
  echo "package.json not found"
fi

echo
echo "== Vitest package metadata for 4.1.10 =="
if command -v npm >/dev/null 2>&1; then
  npm view vitest@4.1.10 dependencies peerDependencies engines --json
fi

echo
echo "== `@vitest/coverage-v8` package metadata for 4.1.10 =="
if command -v npm >/dev/null 2>&1; then
  npm view `@vitest/coverage-v8`@4.1.10 dependencies peerDependencies engines --json
fi

Repository: AOSSIE-Org/OrgExplorer

Length of output: 245


🏁 Script executed:

#!/bin/sh
set -euo pipefail

echo "== package.json relevant dependency lines =="
if [ -f package.json ]; then
  awk '{ printf "%5d\t%s\n", NR, $0 }' package.json | sed -n '1,80p'
else
  echo "package.json not found"
fi

echo
echo "== Vitest package metadata for 4.1.10 =="
if command -v npm >/dev/null 2>&1; then
  npm view vitest@4.1.10 dependencies peerDependencies engines --json
else
  echo "npm not available"
fi

echo
echo "== `@vitest/coverage-v8` package metadata for 4.1.10 =="
if command -v npm >/dev/null 2>&1; then
  npm view `@vitest/coverage-v8`@4.1.10 dependencies peerDependencies engines --json
else
  echo "npm not available"
fi

Repository: AOSSIE-Org/OrgExplorer

Length of output: 3531


Align Vite with Vitest’s supported range.

vitest@4.1.10 declares Vite ^6.0.0 || ^7.0.0 || ^8.0.0, so vite@^5.4.6 leaves the test suite with incompatible Vite versions / unresolved Vitest runtime dependencies.

🔧 Suggested fix
-    "vite": "^5.4.6",
+    "vite": "^6.0.0",
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
"@testing-library/jest-dom": "^7.0.0",
"@testing-library/react": "^16.3.2",
"@testing-library/user-event": "^14.6.1",
"@vitejs/plugin-react": "^4.3.1",
"vite": "^5.4.6"
"@vitest/coverage-v8": "^4.1.10",
"jsdom": "^29.1.1",
"vite": "^5.4.6",
"vitest": "^4.1.10"
"`@testing-library/jest-dom`": "^7.0.0",
"`@testing-library/react`": "^16.3.2",
"`@testing-library/user-event`": "^14.6.1",
"`@vitejs/plugin-react`": "^4.3.1",
"`@vitest/coverage-v8`": "^4.1.10",
"jsdom": "^29.1.1",
"vite": "^6.0.0",
"vitest": "^4.1.10"
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@package.json` around lines 24 - 31, Update the vite dependency in
package.json to a version within vitest@^4.1.10’s supported range (^6.0.0,
^7.0.0, or ^8.0.0), while leaving the existing Vitest dependency unchanged.

}
}
9 changes: 5 additions & 4 deletions src/pages/OverviewPage.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import SocialShareButton from '../components/SocialShareButton';
import { AiOutlineInfoCircle } from "react-icons/ai";
import AnalysisBanner from '../components/AnalysisBanner'
import { OverviewSkeleton } from '../components/Orgexplorerskeletons'
import {formatNumber} from '../utils/formatNumber'

const LANG_COLORS = ['#22c55e', '#f5c518', '#3b82f6', '#ef4444', '#a855f7', '#f97316', '#06b6d4']
const fmt = n => n > 999 ? (n / 1000).toFixed(1) + 'k' : String(n)
Expand Down Expand Up @@ -113,10 +114,10 @@ export default function OverviewPage() {

{/* Stats */}
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(4,1fr)', gap: 12, marginBottom: 24 }}>
<StatCard label="Total Repos" value={totalRepo.toLocaleString()} />
<StatCard label="Total Stars" value={fmt(totalStars)} />
<StatCard label="Total Forks" value={fmt(totalForks)} />
<StatCard label="Active Repos" value={activeRepos} sub={`${Math.round(activeRepos / totalRepo * 100)}% of total`} />
<StatCard label="Total Repos" value={formatNumber(totalRepo)} />
<StatCard label="Total Stars" value={formatNumber(totalStars)} />
<StatCard label="Total Forks" value={formatNumber(totalForks)} />
<StatCard label="Active Repos" value={formatNumber(activeRepos)} sub={`${Math.round(activeRepos / totalRepo * 100)}% of total`} />
</div>

{/* Language + top repos */}
Expand Down
165 changes: 165 additions & 0 deletions src/services/analytics.buildAnalyticalModel.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
import { describe, it, expect } from 'vitest'
import { buildAnalyticalModel } from './analytics'

function makeRepo(name, overrides = {}) {
return {
name,
pushed_at: new Date().toISOString(),
open_issues_count: 0,
stargazers_count: 0,
forks_count: 0,
...overrides,
}
}

describe('buildAnalyticalModel', () => {
it('populates totalRepos from totalReposPerOrg, including repos with no contributor data', () => {
const orgs = [{ login: 'org-a' }]
const repoA = makeRepo('repo-a')
const repoB = makeRepo('repo-b') // exists only in the "total" fetch, not scoped

const reposPerOrg = { 'org-a': [repoA] } // scoped (e.g. top-10 without PAT)
const totalReposPerOrg = { 'org-a': [repoA, repoB] } // full fetch
const contribsPerRepo = {
'org-a/repo-a': [{ login: 'alice', contributions: 10 }],
// no entry for repo-b
}

const result = buildAnalyticalModel(orgs, reposPerOrg, contribsPerRepo, totalReposPerOrg)

expect(result.totalRepos).toHaveLength(2)
const repoBResult = result.totalRepos.find(r => r.name === 'repo-b')
expect(repoBResult.contributors).toEqual([])
expect(repoBResult.busFactor).toEqual({ factor: 0, risk: 'unknown' })
expect(repoBResult.orgLogin).toBe('org-a')
})

it('populates allRepos only from the scoped reposPerOrg, not from totalReposPerOrg', () => {
const orgs = [{ login: 'org-a' }]
const repoA = makeRepo('repo-a')
const repoB = makeRepo('repo-b')

const reposPerOrg = { 'org-a': [repoA] }
const totalReposPerOrg = { 'org-a': [repoA, repoB] }
const contribsPerRepo = {}

const result = buildAnalyticalModel(orgs, reposPerOrg, contribsPerRepo, totalReposPerOrg)

expect(result.allRepos).toHaveLength(1)
expect(result.allRepos[0].name).toBe('repo-a')
})

it('does NOT include a contributor whose only appearance is in a totalRepos-only repo', () => {
// repo-b has contributor data in contribsPerRepo, but repo-b is only in
// totalReposPerOrg, not reposPerOrg. The contributor map is built solely
// from the reposPerOrg loop, so this contributor must not appear.
const orgs = [{ login: 'org-a' }]
const repoA = makeRepo('repo-a')
const repoB = makeRepo('repo-b')

const reposPerOrg = { 'org-a': [repoA] }
const totalReposPerOrg = { 'org-a': [repoA, repoB] }
const contribsPerRepo = {
'org-a/repo-b': [{ login: 'ghost-contributor', avatar_url: '', contributions: 5 }],
}

const result = buildAnalyticalModel(orgs, reposPerOrg, contribsPerRepo, totalReposPerOrg)

expect(result.contributors.find(c => c.login === 'ghost-contributor')).toBeUndefined()
})

it('deduplicates a contributor appearing across multiple scoped repos, summing contributions', () => {
const orgs = [{ login: 'org-a' }]
const repoA = makeRepo('repo-a')
const repoB = makeRepo('repo-b')

const reposPerOrg = { 'org-a': [repoA, repoB] }
const totalReposPerOrg = { 'org-a': [repoA, repoB] }
const contribsPerRepo = {
'org-a/repo-a': [{ login: 'alice', avatar_url: '', contributions: 10 }],
'org-a/repo-b': [{ login: 'alice', avatar_url: '', contributions: 15 }],
}

const result = buildAnalyticalModel(orgs, reposPerOrg, contribsPerRepo, totalReposPerOrg)

expect(result.contributors).toHaveLength(1)
expect(result.contributors[0].totalContribs).toBe(25)
expect(result.contributors[0].repos).toHaveLength(2)
})

it('marks a contributor as isConnector once they appear in 3+ scoped repos', () => {
const orgs = [{ login: 'org-a' }]
const repos = [makeRepo('r1'), makeRepo('r2'), makeRepo('r3')]

const reposPerOrg = { 'org-a': repos }
const totalReposPerOrg = { 'org-a': repos }
const contribsPerRepo = Object.fromEntries(
repos.map(r => [`org-a/${r.name}`, [{ login: 'carol', avatar_url: '', contributions: 1 }]])
)

const result = buildAnalyticalModel(orgs, reposPerOrg, contribsPerRepo, totalReposPerOrg)

expect(result.contributors[0].isConnector).toBe(true)
})

it('does not mark a contributor as isConnector with fewer than 3 scoped repos', () => {
const orgs = [{ login: 'org-a' }]
const repos = [makeRepo('r1'), makeRepo('r2')]

const reposPerOrg = { 'org-a': repos }
const totalReposPerOrg = { 'org-a': repos }
const contribsPerRepo = Object.fromEntries(
repos.map(r => [`org-a/${r.name}`, [{ login: 'dave', avatar_url: '', contributions: 1 }]])
)

const result = buildAnalyticalModel(orgs, reposPerOrg, contribsPerRepo, totalReposPerOrg)

expect(result.contributors[0].isConnector).toBe(false)
})

it('marks a contributor as isCrossOrg when they appear in 2+ orgs', () => {
const orgs = [{ login: 'org-a' }, { login: 'org-b' }]
const repoA = makeRepo('repo-a')
const repoB = makeRepo('repo-b')

const reposPerOrg = { 'org-a': [repoA], 'org-b': [repoB] }
const totalReposPerOrg = { 'org-a': [repoA], 'org-b': [repoB] }
const contribsPerRepo = {
'org-a/repo-a': [{ login: 'erin', avatar_url: '', contributions: 5 }],
'org-b/repo-b': [{ login: 'erin', avatar_url: '', contributions: 5 }],
}

const result = buildAnalyticalModel(orgs, reposPerOrg, contribsPerRepo, totalReposPerOrg)

expect(result.contributors[0].isCrossOrg).toBe(true)
expect(result.contributors[0].orgs.sort()).toEqual(['org-a', 'org-b'])
})

it('sorts contributors by totalContribs descending', () => {
const orgs = [{ login: 'org-a' }]
const repoA = makeRepo('repo-a')

const reposPerOrg = { 'org-a': [repoA] }
const totalReposPerOrg = { 'org-a': [repoA] }
const contribsPerRepo = {
'org-a/repo-a': [
{ login: 'low', avatar_url: '', contributions: 5 },
{ login: 'high', avatar_url: '', contributions: 50 },
{ login: 'mid', avatar_url: '', contributions: 20 },
],
}

const result = buildAnalyticalModel(orgs, reposPerOrg, contribsPerRepo, totalReposPerOrg)

expect(result.contributors.map(c => c.login)).toEqual(['high', 'mid', 'low'])
})

it('handles an org with zero repos without throwing', () => {
const orgs = [{ login: 'empty-org' }]
const result = buildAnalyticalModel(orgs, { 'empty-org': [] }, {}, { 'empty-org': [] })

expect(result.allRepos).toEqual([])
expect(result.totalRepos).toEqual([])
expect(result.contributors).toEqual([])
})
})
120 changes: 120 additions & 0 deletions src/services/analytics.buildTimeSeries.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
import { describe, it, expect } from 'vitest'
import { buildTimeSeries } from './analytics'

describe('buildTimeSeries', () => {
it('buckets a plain issue by its created_at month', () => {
const issues = [{ created_at: '2026-03-15T00:00:00Z' }]

const result = buildTimeSeries(issues, 'monthly')

expect(result).toHaveLength(1)
expect(result[0].date).toBe('2026-03')
expect(result[0].issues_created).toBe(1)
expect(result[0].prs_created).toBe(0)
})

it('counts a pull_request item as a PR, not an issue', () => {
const issues = [{ created_at: '2026-03-15T00:00:00Z', pull_request: {} }]

const result = buildTimeSeries(issues, 'monthly')

expect(result[0].prs_created).toBe(1)
expect(result[0].issues_created).toBe(0)
})

it('buckets closed_at into the correct month, separate from created_at', () => {
const issues = [{
created_at: '2026-01-01T00:00:00Z',
closed_at: '2026-02-01T00:00:00Z',
}]

const result = buildTimeSeries(issues, 'monthly')

const jan = result.find(r => r.date === '2026-01')
const feb = result.find(r => r.date === '2026-02')

expect(jan.issues_created).toBe(1)
expect(jan.issues_closed).toBe(0)
expect(feb.issues_closed).toBe(1)
expect(feb.issues_created).toBe(0)
})

it('buckets a merged PR into prs_merged using pull_request.merged_at, not closed_at', () => {
const issues = [{
created_at: '2026-01-01T00:00:00Z',
closed_at: '2026-01-05T00:00:00Z',
pull_request: { merged_at: '2026-01-05T00:00:00Z' },
}]

const result = buildTimeSeries(issues, 'monthly')
const jan = result.find(r => r.date === '2026-01')

expect(jan.prs_created).toBe(1)
expect(jan.prs_closed).toBe(1)
expect(jan.prs_merged).toBe(1)
})

it('does not count prs_merged for a closed-but-not-merged PR', () => {
const issues = [{
created_at: '2026-01-01T00:00:00Z',
closed_at: '2026-01-05T00:00:00Z',
pull_request: { merged_at: null },
}]

const result = buildTimeSeries(issues, 'monthly')
const jan = result.find(r => r.date === '2026-01')

expect(jan.prs_closed).toBe(1)
expect(jan.prs_merged).toBe(0)
})

it('produces one bucket per week when granularity is weekly, distinct from monthly', () => {
const issues = [
{ created_at: '2026-01-01T00:00:00Z' },
{ created_at: '2026-01-08T00:00:00Z' },
]

const weekly = buildTimeSeries(issues, 'weekly')

// Two different weeks -> two buckets, and week-format labels
expect(weekly.length).toBeGreaterThanOrEqual(1)
weekly.forEach(bucket => expect(bucket.date).toMatch(/^\d{4}-W\d{2}$/))
})
Comment on lines +71 to +82

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Weak assertion doesn't match the test's stated intent.

The comment says two distinct weeks should produce two buckets, but the assertion only checks length >= 1, which passes even if the function collapsed both dates into a single bucket. Tighten it to actually verify the claimed behavior.

✅ Suggested fix
     const weekly = buildTimeSeries(issues, 'weekly')

-    // Two different weeks -> two buckets, and week-format labels
-    expect(weekly.length).toBeGreaterThanOrEqual(1)
+    // Two different weeks -> two buckets, and week-format labels
+    expect(weekly).toHaveLength(2)
     weekly.forEach(bucket => expect(bucket.date).toMatch(/^\d{4}-W\d{2}$/))

As per path instructions, test files should avoid tautological assertions and have descriptions that clarify the test's purpose — here the assertion should actually validate what the description promises.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
it('produces one bucket per week when granularity is weekly, distinct from monthly', () => {
const issues = [
{ created_at: '2026-01-01T00:00:00Z' },
{ created_at: '2026-01-08T00:00:00Z' },
]
const weekly = buildTimeSeries(issues, 'weekly')
// Two different weeks -> two buckets, and week-format labels
expect(weekly.length).toBeGreaterThanOrEqual(1)
weekly.forEach(bucket => expect(bucket.date).toMatch(/^\d{4}-W\d{2}$/))
})
it('produces one bucket per week when granularity is weekly, distinct from monthly', () => {
const issues = [
{ created_at: '2026-01-01T00:00:00Z' },
{ created_at: '2026-01-08T00:00:00Z' },
]
const weekly = buildTimeSeries(issues, 'weekly')
// Two different weeks -> two buckets, and week-format labels
expect(weekly).toHaveLength(2)
weekly.forEach(bucket => expect(bucket.date).toMatch(/^\d{4}-W\d{2}$/))
})
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/services/analytics.buildTimeSeries.test.js` around lines 71 - 82,
Strengthen the assertions in the weekly case of buildTimeSeries so the two input
dates are verified to produce exactly two buckets, preserving the existing
week-format label validation. Keep the test description aligned with this
distinct-weeks behavior and remove the tautological length check.

Source: Path instructions


it('sorts buckets chronologically', () => {
const issues = [
{ created_at: '2026-03-01T00:00:00Z' },
{ created_at: '2026-01-01T00:00:00Z' },
{ created_at: '2026-02-01T00:00:00Z' },
]

const result = buildTimeSeries(issues, 'monthly')

expect(result.map(r => r.date)).toEqual(['2026-01', '2026-02', '2026-03'])
})

it('returns at most the last 12 buckets when more months are present', () => {
const issues = Array.from({ length: 15 }, (_, i) => ({
created_at: `2025-${String((i % 12) + 1).padStart(2, '0')}-01T00:00:00Z`,
}))
// Construct 15 distinct months across two years to exceed the 12-bucket cap
const spreadIssues = Array.from({ length: 15 }, (_, i) => {
const year = 2025 + Math.floor(i / 12)
const month = (i % 12) + 1
return { created_at: `${year}-${String(month).padStart(2, '0')}-01T00:00:00Z` }
})

const result = buildTimeSeries(spreadIssues, 'monthly')

expect(result.length).toBeLessThanOrEqual(12)
})

it('returns an empty array when given no issues', () => {
expect(buildTimeSeries([], 'monthly')).toEqual([])
})

it('ignores an item with no created_at and no closed_at', () => {
const result = buildTimeSeries([{ created_at: null }], 'monthly')
expect(result).toEqual([])
})
})
Loading
Loading