diff --git a/Brand/Brand.md b/Brand/Brand.md deleted file mode 100644 index 2c6fcce..0000000 --- a/Brand/Brand.md +++ /dev/null @@ -1,162 +0,0 @@ -# Brand Guidelines - -This document defines the visual identity of the project. Please follow these guidelines when creating new pages, components, or assets to keep the UI consistent. - -## Design Principles - -- Keep the interface clean and minimal. -- Prioritize readability over decoration. -- Support both light and dark themes. -- Use spacing and contrast to create hierarchy instead of excessive colors. -- Avoid introducing new colors or fonts unless there is a strong reason. - ---- - -## Color Palette - -### Core Colors - -| Token | Dark | Light | -| ------ | ----- | ------ | -| Background | `#060606` | `#FFFFFF` | -| Surface | `#141414` | `#F5F5F5` | -| Surface Secondary | `#1A1A1A` | `#ECECEC` | -| Border | `#2A2A2A` | `#D5D5D5` | -| Text | `#FFFFFF` | `#060606` | -| Secondary Text | `#888888` | `#666666` | -| Muted Text | `#444444` | `#999999` | -| Accent | `#F5C518` | `#D4A500` | - -### Semantic Colors - -| Purpose | Color | -| -------- | ----- | -| Success | `#22C55E` | -| Error | `#EF4444` | -| Information | `#3B82F6` | - -Always reference the CSS variables instead of hardcoding colors. - -Example: - -```css -background: var(--surface); -color: var(--text); -border-color: var(--border); -``` - ---- - -## Typography - -Font stack: - -``` --apple-system, -BlinkMacSystemFont, -"Segoe UI", -Roboto, -sans-serif -``` - -### Text Sizes - -| Usage | Size | -| ------ | ---- | -| Body | 14px | -| Line Height | 1.6 | - -Use the default font stack unless there is a project-wide decision to change it. - ---- - -## Border Radius - -Standard radius: - -``` -8px -``` - -Use the `--radius` CSS variable whenever possible. - ---- - -## Theme Support - -Every UI component should work correctly in both themes. - -Prefer using CSS variables: - -```css -var(--bg) -var(--surface) -var(--surface2) -var(--border) -var(--text) -var(--text2) -var(--accent) -``` - -Avoid hardcoded colors. - ---- - -## Links - -Links use the accent color. - -```css -color: var(--accent); -``` - -Links should remain readable in both themes. - ---- - -## Icons & Logos - -- Use SVG whenever possible. -- Icons should inherit `currentColor` unless they intentionally use a fixed brand color. -- The project logo should support both light and dark themes. -- Do not stretch or distort the logo. - ---- - -## Spacing - -Keep spacing consistent across the project. - -Preferred spacing scale: - -``` -4px -8px -12px -16px -24px -32px -48px -64px -``` - ---- - -## Scrollbars - -The project uses a minimal scrollbar. - -- Width: `5px` -- Thumb: `var(--border)` -- Track: `var(--bg)` - ---- - -## Before Opening a UI Pull Request - -- Supports both light and dark themes. -- Uses existing CSS variables. -- Uses the project's typography. -- Maintains consistent spacing. -- Does not introduce unnecessary colors. -- Works across common screen sizes. diff --git a/MAINTAINERS.md b/MAINTAINERS.md deleted file mode 100644 index 4a8918d..0000000 --- a/MAINTAINERS.md +++ /dev/null @@ -1,13 +0,0 @@ -# Maintainers - -This document lists the people responsible for maintaining the project during the Google Summer of Code (GSoC) 2026 development period. - -## Maintainers - -| Name | Role | -|------|------| -| [Bruno](https://github.com/Zahnentferner) | Mentor / Maintainer | -| [Bhavik](https://github.com/bhavik-mangla) | Mentor / Maintainer | -| [Madhav](https://github.com/M4dhav) | Mentor / Maintainer | -| [Rahul](https://github.com/rahul-vyas-dev) | GSoC Maintainer (Student Developer) | -| [Ritik](https://github.com/Ri1tik) | GSoC Maintainer (Student Developer) | diff --git a/privacy-policy.md b/privacy-policy.md new file mode 100644 index 0000000..78a09f8 --- /dev/null +++ b/privacy-policy.md @@ -0,0 +1,179 @@ +# Privacy Policy + +**Last updated: August 2026** + +## Introduction + +At OrgExplorer, we respect your privacy. + +This Privacy Policy explains what data OrgExplorer handles and how — and, just as importantly, what it does **not** do. + +OrgExplorer's architecture is different from many applications because it operates entirely in the browser. It has **no backend server and no user accounts**. + +--- + +## Information We Collect + +OrgExplorer does not have: + +- User accounts +- Signup or waitlist systems +- A backend server operated by us + +Because of this, we do not collect, receive, or store any personal information about you on any system we control. + +### What the App Handles + +#### Data fetched from GitHub + +When you search an organization, publicly available GitHub data is fetched directly from GitHub's API by your browser. + +This may include: + +- Repository information +- Contributor information +- Issues +- Pull requests +- Other publicly available GitHub metadata + +This data never passes through a server operated by OrgExplorer. + +#### Data stored locally on your device + +The following data may be stored locally in your browser: + +- GitHub Personal Access Token (if provided) +- Cached GitHub API responses +- API rate-limit status +- Recent search history +- Application preferences + +This data is stored only in your browser's: + +- `localStorage` +- `IndexedDB` + +OrgExplorer does not transmit this data to any server operated by us. + +When a GitHub Personal Access Token is provided, your browser uses it to authenticate requests directly with GitHub's API. The token is sent only to GitHub for authorized API requests and is never sent to OrgExplorer servers. + +#### Standard Hosting Logs + +OrgExplorer is hosted on GitHub Pages. + +GitHub, as the hosting provider, may collect standard infrastructure logs such as: + +- IP addresses +- Request timestamps +- Other operational metadata + +OrgExplorer maintainers do not have access to these logs. + +For more information, see [GitHub's Privacy Statement](https://docs.github.com/en/site-policy/privacy-policies/github-general-privacy-statement). + +--- + +## How We Use Your Information + +Since OrgExplorer does not collect or receive your personal data, we do not use, analyze, sell, or process your information. + +All locally stored data exists only to make the application function properly, including: + +- Reducing repeated GitHub API requests through caching +- Preserving your preferences +- Allowing you to avoid entering your Personal Access Token repeatedly + +--- + +## Data Sharing and Disclosure + +We do not sell, share, or disclose your data. + +This is because OrgExplorer does not collect your data in the first place. + +The only external communication occurs directly between: +``` +Your Browser +| +| +v +GitHub API (api.github.com) +``` + + +This communication is governed by: + +- [GitHub Terms of Service](https://docs.github.com/en/site-policy/github-terms/github-terms-of-service) +- [GitHub Privacy Statement](https://docs.github.com/en/site-policy/privacy-policies/github-general-privacy-statement) + +--- + +## Data Security + +Because OrgExplorer has no backend server or database, most server-side data security risks do not apply. + +The relevant security considerations involve data stored on your own device. + +### GitHub Personal Access Token + +If you choose to provide a GitHub Personal Access Token: + +- It is stored only in your browser's `localStorage` +- It is sent only to GitHub's API +- It is never logged by OrgExplorer +- It is never transmitted to OrgExplorer servers + +### Token Safety Recommendations + +We recommend: + +- Using a token with the minimum permissions required +- Avoiding unnecessary scopes +- Revoking your token from GitHub settings if you suspect misuse + +Please note that browser `localStorage` is not encrypted. + +--- + +## Your Rights + +Since OrgExplorer does not collect or store personal information on our systems, there is no personal data held by us that you can request to: + +- Access +- Correct +- Delete +- Export + +You maintain complete control over locally stored application data. + +You can remove stored data by: + +- Clearing your browser storage +- Removing your Personal Access Token from the Settings page +- Resetting application data through browser settings + +--- + +## Changes to This Policy + +We may update this Privacy Policy as OrgExplorer evolves. + +Any changes will be reflected on this page with an updated **"Last updated"** date. + +We encourage users to review this policy periodically. + +--- + +## Contact Us + +If you have questions about this Privacy Policy, contact us: + +**Email:** [contact@aossie.org](mailto:contact@aossie.org) + +**Discord:** [Discord](https://discord.gg/hjUhu33uAn) + +Or open an issue on [the project's GitHub repository](https://github.com/AOSSIE-Org/OrgExplorer). + +--- + +*This policy describes OrgExplorer's actual technical behavior as of the date above. It is provided for transparency and is not a substitute for professional legal advice.* diff --git a/src/assests/og-logo.svg b/src/assests/og-logo.svg deleted file mode 100644 index 86165d9..0000000 --- a/src/assests/og-logo.svg +++ /dev/null @@ -1,61 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/src/components/layout/Footer.jsx b/src/components/layout/Footer.jsx index 4b69ed1..ddb2200 100644 --- a/src/components/layout/Footer.jsx +++ b/src/components/layout/Footer.jsx @@ -19,11 +19,11 @@ const footerLinks = [ }, { label: "Terms of Service", - href: "/terms", + href: "https://github.com/AOSSIE-Org/OrgExplorer/blob/main/terms-of-service.md", }, { label: "Privacy Policy", - href: "/privacy", + href: "https://github.com/AOSSIE-Org/OrgExplorer/blob/main/privacy-policy.md", }, { label: "API Status", @@ -110,6 +110,8 @@ export default function Footer() { - - - ); -} - -export default Support; diff --git a/src/services/analytics.buildAnalyticalModel.test.js b/src/services/analytics.buildAnalyticalModel.test.js deleted file mode 100644 index 85cb58a..0000000 --- a/src/services/analytics.buildAnalyticalModel.test.js +++ /dev/null @@ -1,165 +0,0 @@ -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([]) - }) -}) diff --git a/src/services/analytics.buildTimeSeries.test.js b/src/services/analytics.buildTimeSeries.test.js deleted file mode 100644 index c14e599..0000000 --- a/src/services/analytics.buildTimeSeries.test.js +++ /dev/null @@ -1,120 +0,0 @@ -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}$/)) - }) - - 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([]) - }) -}) diff --git a/src/services/analytics.healthMetrics.test.js b/src/services/analytics.healthMetrics.test.js deleted file mode 100644 index 3fdf4f6..0000000 --- a/src/services/analytics.healthMetrics.test.js +++ /dev/null @@ -1,116 +0,0 @@ -import { describe, it, expect } from 'vitest' -import { - computeHealthScore, - computeActivityClassification, - computeBusFactor, -} from './analytics' - -function daysAgoISO(days) { - return new Date(Date.now() - days * 86_400_000).toISOString() -} - -describe('computeHealthScore', () => { - it('scores a freshly pushed, issue-free repo with 2 contributors at 76', () => { - // activity = 100 - 0 = 100 - // issueHealth = 100 - (0 / (0+10)) * 100 = 100 - // diversity = min(100, 2 * 10) = 20 - // score = round(100*0.4 + 100*0.3 + 20*0.3) = round(40 + 30 + 6) = 76 - const repo = { pushed_at: new Date().toISOString(), open_issues_count: 0 } - - expect(computeHealthScore(repo, 2)).toBe(76) - }) - - it('lowers the score as open issue count grows relative to the +10 baseline', () => { - const freshRepo = pushed => ({ pushed_at: pushed, open_issues_count: 0 }) - - const noIssues = computeHealthScore({ ...freshRepo(new Date().toISOString()), open_issues_count: 0 }, 0) - const manyIssues = computeHealthScore({ ...freshRepo(new Date().toISOString()), open_issues_count: 90 }, 0) - - expect(manyIssues).toBeLessThan(noIssues) - }) - - it('caps diversity contribution at 100 once contributor count reaches 10', () => { - const repo = { pushed_at: new Date().toISOString(), open_issues_count: 0 } - - const at10 = computeHealthScore(repo, 10) - const at50 = computeHealthScore(repo, 50) - - // diversity is min(100, count*10) for both -> identical score - expect(at10).toBe(at50) - }) - - it('lowers activity score as days since last push increases', () => { - const recentRepo = { pushed_at: daysAgoISO(5), open_issues_count: 0 } - const staleRepo = { pushed_at: daysAgoISO(95), open_issues_count: 0 } - - expect(computeHealthScore(recentRepo, 5)).toBeGreaterThan( - computeHealthScore(staleRepo, 5) - ) - }) - - it('never returns a negative score for a very old, issue-heavy repo', () => { - const repo = { pushed_at: daysAgoISO(1000), open_issues_count: 500 } - - expect(computeHealthScore(repo, 0)).toBeGreaterThanOrEqual(0) - }) -}) - -describe('computeActivityClassification', () => { - it('classifies a repo pushed within 30 days as Thriving', () => { - expect(computeActivityClassification({ pushed_at: daysAgoISO(10) })).toBe('Thriving') - }) - - it('classifies a repo pushed within 31-90 days as Active', () => { - expect(computeActivityClassification({ pushed_at: daysAgoISO(60) })).toBe('Active') - }) - - it('classifies a repo pushed within 91-180 days as Dormant', () => { - expect(computeActivityClassification({ pushed_at: daysAgoISO(120) })).toBe('Dormant') - }) - - it('classifies a repo pushed more than 180 days ago as Hibernating', () => { - expect(computeActivityClassification({ pushed_at: daysAgoISO(200) })).toBe('Hibernating') - }) - - it('treats the exact 30-day boundary as Thriving (inclusive)', () => { - expect(computeActivityClassification({ pushed_at: daysAgoISO(30) })).toBe('Thriving') - }) -}) - -describe('computeBusFactor', () => { - it('returns unknown/0 for an empty contributor list', () => { - expect(computeBusFactor([])).toEqual({ factor: 0, risk: 'unknown' }) - }) - - it('returns unknown/0 when total contributions sum to zero', () => { - const contributors = [{ contributions: 0 }, { contributions: 0 }] - expect(computeBusFactor(contributors)).toEqual({ factor: 0, risk: 'unknown' }) - }) - - it('flags critical risk when one contributor alone exceeds 50% of contributions', () => { - const contributors = [{ contributions: 60 }, { contributions: 40 }] - // cum after contributor 1: 60/100 = 0.6 > 0.5 -> factor 1 - expect(computeBusFactor(contributors)).toEqual({ factor: 1, risk: 'critical' }) - }) - - it('flags high risk when it takes exactly 2 contributors to exceed 50%', () => { - const contributors = [{ contributions: 30 }, { contributions: 30 }, { contributions: 40 }] - // cum after 1: 30/100=0.3 (not >0.5); cum after 2: 60/100=0.6>0.5 -> factor 2 - expect(computeBusFactor(contributors)).toEqual({ factor: 2, risk: 'high' }) - }) - - it('flags healthy risk when it takes 3+ contributors to exceed 50%', () => { - const contributors = [ - { contributions: 20 }, { contributions: 20 }, { contributions: 20 }, { contributions: 40 }, - ] - // cum: 20/100=.2, 40/100=.4, 60/100=.6>0.5 -> factor 3 - expect(computeBusFactor(contributors)).toEqual({ factor: 3, risk: 'healthy' }) - }) - - it('respects contributor order — cumulative sum is positional, not sorted', () => { - // Smallest contributor listed first; still accumulates in list order - const contributors = [{ contributions: 10 }, { contributions: 90 }] - // cum after 1: 10/100=.1; cum after 2: 100/100=1.0>0.5 -> factor 2 - expect(computeBusFactor(contributors)).toEqual({ factor: 2, risk: 'high' }) - }) -}) diff --git a/src/test/setup.js b/src/test/setup.js deleted file mode 100644 index 4110b52..0000000 --- a/src/test/setup.js +++ /dev/null @@ -1,7 +0,0 @@ -import "@testing-library/jest-dom/vitest"; -import { afterEach } from "vitest"; -import { cleanup } from "@testing-library/react"; - -afterEach(() => { - cleanup(); -}); \ No newline at end of file diff --git a/src/utils/formatNumber.js b/src/utils/formatNumber.js deleted file mode 100644 index 987f3c5..0000000 --- a/src/utils/formatNumber.js +++ /dev/null @@ -1,19 +0,0 @@ -export function formatNumber(num) { - if (num >= 1_000_000_000) { - return `${(num / 1_000_000_000).toFixed(1)}B`; - } - - if (num >= 1_000_000) { - const millions = (num / 1_000_000).toFixed(1); - if (millions === '1000.0') return `${(num / 1_000_000_000).toFixed(1)}B`; - return `${millions}M`; - } - - if (num >= 1_000) { - const thousands = (num / 1_000).toFixed(1); - if (thousands === '1000.0') return `${(num / 1_000_000).toFixed(1)}M`; - return `${thousands}K`; - } - - return num.toString(); -} \ No newline at end of file diff --git a/src/utils/formatNumber.test.js b/src/utils/formatNumber.test.js deleted file mode 100644 index 91e74b2..0000000 --- a/src/utils/formatNumber.test.js +++ /dev/null @@ -1,21 +0,0 @@ -import { describe, it, expect } from "vitest"; -import { formatNumber } from "./formatNumber"; - -describe("formatNumber", () => { - it("return a number in a form of a string if it is lesser than 1000", () => { - expect(formatNumber(500)).toBe("500"); - }); - it("formats numbers in the thousands as K", () => { - expect(formatNumber(999_949)).toBe("999.9K") - }); - it("formats numbers in the millions as M", () => { - expect(formatNumber(999_950)).toBe("1.0M"); - }); - it("formats numbers in the billions as B", () => { - expect(formatNumber(999_950_000)).toBe("1.0B"); - }); - - it("handles exactly 1000", () => { - expect(formatNumber(1000)).toBe("1.0K"); - }); -}) \ No newline at end of file diff --git a/terms-of-service.md b/terms-of-service.md new file mode 100644 index 0000000..62f8767 --- /dev/null +++ b/terms-of-service.md @@ -0,0 +1,202 @@ +# Terms of Service + +**Last updated: August 2026** + +## 1. Introduction + +Welcome to OrgExplorer, an open-source, browser-only tool for analyzing publicly available GitHub organization data — repositories, contributors, governance signals, and activity trends. + +This Terms of Service Agreement ("Agreement") governs your use of OrgExplorer, including its software, documentation, and hosted deployment (collectively, the "Project"). + +By accessing or using the Project, you agree to be bound by these terms. If you do not agree, do not use the Project. + +--- + +## 2. Grant of License + +OrgExplorer is open-source software. Its source code, license terms, and contribution guidelines are available at [the project's GitHub repository](https://github.com/AOSSIE-Org/OrgExplorer). + +Your rights to use, modify, and redistribute the underlying code are governed by that repository's license. This Agreement additionally governs your use of the *deployed, hosted instance* of the Project available at (https://orgexplorer.aossie.org/). + +### Permitted Uses + +You may use OrgExplorer for: + +- Analyzing publicly available GitHub organization data +- Research and evaluation purposes +- Governance auditing +- General exploration and learning + +### Restrictions + +You agree not to use the Project in a manner that: + +- Violates applicable laws +- Violates GitHub's Terms of Service +- Attempts to bypass GitHub API rate limits through unauthorized methods +- Misuses Personal Access Tokens or other authentication mechanisms + +--- + +## 3. Intellectual Property Rights + +All right, title, and interest in OrgExplorer's own source code remains with its maintainers and contributors, subject to the open-source license available in the repository. + +Data displayed within the Project, including: + +- Repository names +- Contributor information +- Issue and pull request content +- Other GitHub metadata + +is publicly available data retrieved from GitHub and remains the property of its respective owners, subject to GitHub's own terms. + +### Third-Party Components + +The Project uses third-party open-source libraries, including React, D3.js, and others. + +Your use of these components is subject to their respective license terms, which are available in the repository. + +--- + +## 4. Disclaimer of Warranties + +The Project is provided **"as is"** without warranty of any kind, either expressed or implied, including but not limited to warranties of: + +- Merchantability +- Fitness for a particular purpose +- Non-infringement + +### No Guarantee + +Neither the maintainers nor contributors guarantee that the Project will: + +- Be error-free +- Always remain secure +- Be available without interruption +- Produce perfectly accurate analytics + +All analytics, scores, and metrics shown, including health scores, bus factor analysis, activity classification, and other derived insights, are computed estimates based on publicly available GitHub data at the time of analysis. + +These metrics are provided for informational purposes only. + +### Risk Acknowledgment + +You assume all risks associated with: + +- Using the Project +- Relying on generated metrics or analysis +- Using your own GitHub Personal Access Token with the application + +--- + +## 5. Limitation of Liability + +To the maximum extent permitted by applicable law, OrgExplorer maintainers, contributors, and affiliates shall not be liable for any: + +- Indirect damages +- Incidental damages +- Special damages +- Consequential damages +- Punitive damages + +arising from your use of, or inability to use, the Project. + +This includes loss of locally cached data, as OrgExplorer does not store user data on any system operated by the maintainers. + +--- + + +## 6. Third-Party Services and Integrations + +OrgExplorer integrates with GitHub's public REST API. + +Your use of this integration is subject to: + +- [GitHub Terms of Service](https://docs.github.com/en/site-policy/github-terms/github-terms-of-service) +- [GitHub REST API Rate Limit Policies](https://docs.github.com/en/rest/using-the-rest-api/rate-limits-for-the-rest-api) + +### No Endorsement + +Reference to GitHub does not imply endorsement by OrgExplorer, nor does GitHub endorse OrgExplorer. + +### User Responsibility + +You are responsible for: + +- Following GitHub's terms +- Proper usage of Personal Access Tokens +- Ensuring your API usage complies with GitHub policies + +OrgExplorer is not responsible for GitHub service availability, API changes, or restrictions imposed by GitHub. + +--- + +## 7. Data Privacy and Use + +OrgExplorer has **no backend server and no user accounts**. + +The Project does not collect, store, or process your personal data on any system operated by us. + +Data fetched from GitHub is retrieved directly by your browser. + +Data stored locally for performance purposes, including: + +- Repository data +- Contributor data +- Personal Access Tokens +- Recent searches + +is stored only within your browser's local storage or browser databases. + +For complete details, see our [Privacy Policy](https://github.com/AOSSIE-Org/OrgExplorer/blob/main/privacy-policy.md). + +### Compliance + +You remain responsible for your use of any data retrieved through the Project, including compliance with GitHub's policies regarding API data usage. + +--- + +## 8. Modification and Termination + +### Amendments + +We may update this Agreement from time to time. + +Continued use of the Project after changes are posted constitutes acceptance of the updated terms. + +### Termination + +Since OrgExplorer does not maintain user accounts, termination applies only to: + +- Your decision to stop using the application +- Clearing locally stored application data + +We reserve the right to modify or discontinue the hosted deployment at any time. + +The underlying open-source code remains available according to its license. + +--- + +## 9. Governing Law and Dispute Resolution + +This Agreement is governed by applicable law in the jurisdiction where the maintaining organization, AOSSIE, primarily operates. + +In case of disputes, the parties agree to first attempt resolution through informal communication before pursuing other remedies. + +--- + +## 10. Contact Information + +If you have questions regarding these Terms of Service, contact us: + +**Email:** [contact@aossie.org](mailto:contact@aossie.org) + +**Discord:** [Discord](https://discord.gg/hjUhu33uAn) + + +Or open an issue on [the project's GitHub repository](https://github.com/AOSSIE-Org/OrgExplorer). + +--- + +*These Terms are provided for transparency around an open-source project and are not a substitute for professional legal advice.* \ No newline at end of file