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
8 changes: 7 additions & 1 deletion src/context/AppContext.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,12 @@ export function AppProvider({ children }) {
);

setTotalRepo(total);
const totalReposPerOrg = Object.fromEntries(
Object.entries(reposPerOrg).map(([org, repos]) => [
org,
[...repos], // copy each array
])
);

setLoadMsg('Fetching contributor data for top repositories...')
const contribsPerRepo = {}
Expand All @@ -100,7 +106,7 @@ export function AppProvider({ children }) {
}

setLoadMsg('Building analytical data model...')
setModel(buildAnalyticalModel(validOrgs, reposPerOrg, contribsPerRepo))
setModel(buildAnalyticalModel(validOrgs, reposPerOrg, contribsPerRepo, totalReposPerOrg))


// Save to recent searches
Expand Down
12 changes: 6 additions & 6 deletions src/pages/OverviewPage.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,18 +30,18 @@ export default function OverviewPage() {

if (!model) return null

const { allRepos } = model
const { totalRepos } = model
const isMulti = orgs.length > 1
const totalStars = allRepos.reduce((s, r) => s + r.stargazers_count, 0)
const totalForks = allRepos.reduce((s, r) => s + r.forks_count, 0)
const activeRepos = allRepos.filter(r => r.activityClassification === 'Thriving' || r.activityClassification === 'Active').length
const totalStars = totalRepos.reduce((s, r) => s + r.stargazers_count, 0)
const totalForks = totalRepos.reduce((s, r) => s + r.forks_count, 0)
const activeRepos = totalRepos.filter(r => r.activityClassification === 'Thriving' || r.activityClassification === 'Active').length

const langMap = {}
allRepos.forEach(r => { if (r.language) langMap[r.language] = (langMap[r.language] || 0) + 1 })
totalRepos.forEach(r => { if (r.language) langMap[r.language] = (langMap[r.language] || 0) + 1 })
const langs = Object.entries(langMap).sort((a, b) => b[1] - a[1]).slice(0, 7)
const langTotal = langs.reduce((s, [, c]) => s + c, 0)

const topRepos = [...allRepos].sort((a, b) => b.healthScore - a.healthScore).slice(0, 5)
const topRepos = [...totalRepos].sort((a, b) => b.healthScore - a.healthScore).slice(0, 5)

const NavCard = ({ to, label, sub }) => (
<div
Expand Down
2 changes: 1 addition & 1 deletion src/pages/RepositoriesPage.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ export default function RepositoriesPage() {
}, [])

const navigate = useNavigate()
const allRepos = model?.allRepos ?? []
const allRepos = model?.totalRepos ?? []

const langs = useMemo(() =>
['All Languages', ...new Set(allRepos.map(r => r.language).filter(Boolean))].slice(0, 10),
Expand Down
22 changes: 15 additions & 7 deletions src/services/analytics.js
Original file line number Diff line number Diff line change
Expand Up @@ -37,20 +37,28 @@ export function computeBusFactor(contributors = []) {
// Unified Analytical Data Model
// Merges multiple orgs into one normalized graph:
// Organization → Repositories → Contributors → Issues/PRs
export function buildAnalyticalModel(orgs, reposPerOrg, contribsPerRepo) {
export function buildAnalyticalModel(orgs, reposPerOrg, contribsPerRepo, totalReposPerOrg) {
const allRepos = []
const contributorMap = {}
const totalRepos = [];

orgs.forEach(org => {
const repos = reposPerOrg[org.login] || []
const total = totalReposPerOrg[org.login] || [];

total.forEach(repo => {
const key = `${org.login}/${repo.name}`
const contribs = contribsPerRepo[key] || []
const health = computeHealthScore(repo, contribs.length)
const activityClassification = computeActivityClassification(repo)
const bf = computeBusFactor(contribs)
totalRepos.push({ ...repo, orgLogin: org.login, contributors: contribs, healthScore: health, activityClassification: activityClassification, busFactor: bf })
})
Comment on lines +40 to +56

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

Health score/bus factor are computed with incomplete contributor data for most of totalRepos.

total (from totalReposPerOrg) contains all repos per org, but contribsPerRepo is only ever populated for the reduced "top" list built in AppContext.jsx's explore() (getTopRepositories(...,10) for non-PAT users, or the full list only when a PAT is present). For every repo in total that isn't in that top subset, contribs resolves to [], so:

  • computeHealthScore's contributor-diversity term (30% weight) is always 0, capping the score near ~70 regardless of actual contributor activity.
  • computeBusFactor([]) always returns { factor: 0, risk: 'unknown' }.

This directly affects OverviewPage's health-sorted topRepos and RepositoriesPage's default health-sorted table (both driven by model.totalRepos), which is exactly the “all repositories” feature this PR is meant to introduce.

💡 Possible directions
  • Fetch contributor counts for all repos (may be rate-limit prohibitive without a PAT — note the 60 req/hr GitHub default shown in the product).
  • Or reweight computeHealthScore to redistribute the diversity weight when contributor data is unavailable, rather than silently defaulting to 0.
  • At minimum, mark repos lacking contributor data so downstream UI can distinguish "no data" from "poor diversity".
🤖 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.js` around lines 40 - 56, The `buildAnalyticalModel`
flow is computing `healthScore` and `busFactor` for `totalRepos` using
incomplete `contribsPerRepo` data from `AppContext.jsx`’s `explore()` subset, so
repos outside that subset get treated as having zero contributors. Fix this by
either ensuring contributor data is available for every repo in `totalRepos` (or
fetching it where needed) or by changing `computeHealthScore`/`computeBusFactor`
handling in `analytics.js` so missing contributor data is distinguished from
truly empty data and does not silently default to zero. Keep the logic aligned
with `buildAnalyticalModel`, `computeHealthScore`, and `computeBusFactor` so
downstream `OverviewPage` and `RepositoriesPage` sorting reflects actual data
availability.


repos.forEach(repo => {
const key = `${org.login}/${repo.name}`
const key = `${org.login}/${repo.name}`
const contribs = contribsPerRepo[key] || []
const health = computeHealthScore(repo, contribs.length)
const activityClassification = computeActivityClassification(repo)
const bf = computeBusFactor(contribs)
allRepos.push({ ...repo, orgLogin: org.login, contributors: contribs, healthScore: health, activityClassification: activityClassification, busFactor: bf })
allRepos.push({ ...repo, orgLogin: org.login });

// Build contributor map — deduplicated by login across orgs
contribs.forEach(c => {
Expand Down Expand Up @@ -87,7 +95,7 @@ export function buildAnalyticalModel(orgs, reposPerOrg, contribsPerRepo) {
})).sort((a, b) => b.totalContribs - a.totalContribs)

// Graph is constructed here and persisted through cache layers
return { allRepos, contributors }
return { allRepos, contributors, totalRepos }
}

// Time-Series Bucketing
Expand Down
Loading