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
47 changes: 34 additions & 13 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,9 @@ A browser extension that shows contributor score cards in pull request sidebars.
[![License](https://img.shields.io/github/license/taiiiyang/oss-stamp)](./LICENSE)
[![Last Commit](https://img.shields.io/github/last-commit/taiiiyang/oss-stamp)](https://github.com/taiiiyang/oss-stamp/commits/main)

[![Chrome Web Store](https://img.shields.io/chrome-web-store/v/dhhibeifipigpbkihogkolbhkcecjgfp)](https://chromewebstore.google.com/detail/oss-stamp/dhhibeifipigpbkihogkolbhkcecjgfp)

<!-- TODO: uncomment when published -->
<!-- [![Chrome Web Store](https://img.shields.io/chrome-web-store/v/EXTENSION_ID)](https://chrome.google.com/webstore/detail/EXTENSION_ID) -->
<!-- [![Firefox Add-ons](https://img.shields.io/amo/v/oss-stamp)](https://addons.mozilla.org/firefox/addon/oss-stamp) -->

</div>
Expand All @@ -34,9 +35,9 @@ A browser extension that shows contributor score cards in pull request sidebars.

See a contributor's tier (S/A/B/C/D) and overall score at a glance, right in the GitHub PR sidebar.

### Multi-dimensional Analysis
### Dual Scoring System

Evaluates five dimensions: contribution count, merge rate, code reviews, tenure, and community presence.
Two independent scores — **Repo Trust Score** (context-specific) and **Profile Score** (global profile) — each rated 0–100 across four dimensions.

### Dark Mode

Expand All @@ -48,20 +49,36 @@ Supports English and 简体中文.

## How Scoring Works

Each contributor is scored across five dimensions, normalized to 0–100:
OSS Stamp computes two independent scores, each 0–100 with four dimensions.

### Repo Trust Score

Measures the contributor's relationship with the **current repository**.

> **Special case:** The repository owner always receives **S / 100**.

| Dimension | Max | Factors |
| ------------------ | --- | ------------------------------------------------------------------------------------- |
| Repo Familiarity | 35 | Merged PRs (0–12), reviews given (0–8), active duration (0–10), contributor flag (+5) |
| Community Standing | 25 | Account age (0–5), followers (0–10), org membership (+10) |
| OSS Influence | 20 | Top repo stars (0–15), total stars (0–5) |
| PR Track Record | 20 | Merge rate bands: <50% → 5, 50–74% → 10, 75–89% → 15, ≥90% → 20 (no PRs → 5) |

| Dimension | What it measures |
| ------------- | -------------------------------- |
| Contributions | Number of merged pull requests |
| Merge Rate | Ratio of merged PRs to total PRs |
| Reviews | Number of code reviews given |
| Tenure | Time since first contribution |
| Presence | Public repos and followers |
### Profile Score

Contributions carry the most weight; the other four dimensions contribute equally.
Measures the contributor's **global GitHub profile**, independent of any specific repo.

| Dimension | Max | Factors |
| ------------------ | --- | ------------------------------------------------------------------------------------------------------ |
| Community Presence | 25 | Account age (0–5), followers via log scale (0–12), follower/following ratio (0–4), has bio (+4) |
| OSS Impact | 25 | Top repo stars via log scale (0–10), total stars via log scale (0–10), total forks via log scale (0–5) |
| Activity | 30 | Yearly contributions via log scale (0–18), public repos via log scale (0–12) |
| Ecosystem | 20 | Org memberships (0–12), language diversity (0–8) |

### Tiers

Both scores share the same tier thresholds:

| Tier | Score |
| ----- | ------ |
| **S** | 90–100 |
Expand All @@ -70,11 +87,15 @@ Contributions carry the most weight; the other four dimensions contribute equall
| **C** | 30–49 |
| **D** | 0–29 |

### Log Scale

Several Profile Score factors use a logarithmic scale (`logScale(value, ref, max)`) to prevent extreme values from dominating. The `ref` parameter is the reference value that maps to ~70% of the maximum points. For example, `logScale(followers, 200, 12)` means 200 followers ≈ 8.4 points out of 12.

## Install

### From Store

> Coming soon — [star this repo](https://github.com/taiiiyang/oss-stamp) to get notified.
- **Chrome**: [Install from Chrome Web Store](https://chromewebstore.google.com/detail/oss-stamp/dhhibeifipigpbkihogkolbhkcecjgfp)

### From Source

Expand Down
47 changes: 34 additions & 13 deletions README.zh-CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,9 @@
[![License](https://img.shields.io/github/license/taiiiyang/oss-stamp)](./LICENSE)
[![Last Commit](https://img.shields.io/github/last-commit/taiiiyang/oss-stamp)](https://github.com/taiiiyang/oss-stamp/commits/main)

[![Chrome Web Store](https://img.shields.io/chrome-web-store/v/dhhibeifipigpbkihogkolbhkcecjgfp)](https://chromewebstore.google.com/detail/oss-stamp/dhhibeifipigpbkihogkolbhkcecjgfp)

<!-- TODO: uncomment when published -->
<!-- [![Chrome Web Store](https://img.shields.io/chrome-web-store/v/EXTENSION_ID)](https://chrome.google.com/webstore/detail/EXTENSION_ID) -->
<!-- [![Firefox Add-ons](https://img.shields.io/amo/v/oss-stamp)](https://addons.mozilla.org/firefox/addon/oss-stamp) -->

</div>
Expand All @@ -34,9 +35,9 @@

在 GitHub PR 侧边栏直接查看贡献者的等级(S/A/B/C/D)和综合评分。

### 多维度分析
### 双评分体系

从五个维度进行评估:贡献数量、合并率、代码审查、参与时长和社区影响力
两套独立评分 — **Repo Trust Score**(基于当前仓库)和 **Profile Score**(基于全局画像)— 均为 0–100 分,各含四个维度

### 深色模式

Expand All @@ -48,20 +49,36 @@

## 评分机制

每位贡献者从五个维度进行评分,每项归一化至 0–100:
OSS Stamp 计算两套独立评分,均为 0–100 分,各含四个维度。

### Repo Trust Score(仓库信任分)

衡量贡献者与**当前仓库**的关系。

> **特殊情况:** 仓库 Owner 直接获得 **S / 100 分**。

| 维度 | 满分 | 评分因子 |
| ------------------ | ---- | ---------------------------------------------------------------------- |
| Repo Familiarity | 35 | 已合并 PR 数 (0–12)、审查次数 (0–8)、活跃时长 (0–10)、贡献者标记 (+5) |
| Community Standing | 25 | 账号年龄 (0–5)、关注者数 (0–10)、组织成员 (+10) |
| OSS Influence | 20 | 最高星标仓库 (0–15)、总星标数 (0–5) |
| PR Track Record | 20 | 合并率分段:<50% → 5, 50–74% → 10, 75–89% → 15, ≥90% → 20(无 PR → 5) |

| 维度 | 衡量内容 |
| -------- | -------------------------- |
| 贡献量 | 已合并的 Pull Request 数量 |
| 合并率 | 已合并 PR 占总 PR 的比例 |
| 代码审查 | 给出的代码审查次数 |
| 参与时长 | 首次贡献距今的时间 |
| 活跃度 | 公开仓库数和关注者数 |
### Profile Score(画像评分)

贡献量占最大权重,其余四个维度权重相同。
衡量贡献者的**全局 GitHub 画像**,与特定仓库无关。

| 维度 | 满分 | 评分因子 |
| ------------------ | ---- | ---------------------------------------------------------------------- |
| Community Presence | 25 | 账号年龄 (0–5)、关注者数/对数 (0–12)、关注者/关注比 (0–4)、有 Bio (+4) |
| OSS Impact | 25 | 最高星标仓库/对数 (0–10)、总星标数/对数 (0–10)、总 Fork 数/对数 (0–5) |
| Activity | 30 | 年度贡献量/对数 (0–18)、公开仓库数/对数 (0–12) |
| Ecosystem | 20 | 组织成员数 (0–12)、语言多样性 (0–8) |

### 等级

两套评分共享相同的等级阈值:

| 等级 | 分数 |
| ----- | ------ |
| **S** | 90–100 |
Expand All @@ -70,11 +87,15 @@
| **C** | 30–49 |
| **D** | 0–29 |

### 对数缩放

Profile Score 中多个因子使用对数缩放函数(`logScale(value, ref, max)`),防止极端值主导评分。`ref` 参数为映射到满分 ~70% 的参考值。例如 `logScale(followers, 200, 12)` 表示 200 个关注者 ≈ 8.4 分(满分 12)。

## 安装

### 从商店安装

> 即将上线 — [Star 本仓库](https://github.com/taiiiyang/oss-stamp)以获取通知。
- **Chrome**:[从 Chrome Web Store 安装](https://chromewebstore.google.com/detail/oss-stamp/dhhibeifipigpbkihogkolbhkcecjgfp)

### 从源码构建

Expand Down
24 changes: 20 additions & 4 deletions src/atoms/contributor-score.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,27 @@
import type { ContributorScore } from '@/lib/scoring'
import type { ProfileScoreResult } from '@/lib/profile-score'
import type { RepoScoreResult } from '@/lib/repo-score'
import { atom } from 'jotai'
import { calculateScore } from '@/lib/scoring'
import { calculateProfileScore } from '@/lib/profile-score'
import { calculateRepoScore } from '@/lib/repo-score'
import { globalContributorAtom } from './contributor-global'
import { repoContributorAtom } from './contributor-repo'
import { currentRepoAtom, prAuthorAtom } from './pr-page'

export const contributorScoreAtom = atom<ContributorScore | null>((get) => {
export const activeTabAtom = atom<'repo' | 'overall'>('repo')

export const repoScoreAtom = atom<RepoScoreResult | null>((get) => {
const { data: repoData } = get(repoContributorAtom)
const { data: globalData } = get(globalContributorAtom)
const repo = get(currentRepoAtom)
const author = get(prAuthorAtom)
if (!repoData || !globalData || !repo || !author)
return null
return calculateRepoScore(repoData, globalData, repo.owner, author)
})

export const profileScoreAtom = atom<ProfileScoreResult | null>((get) => {
const { data: globalData } = get(globalContributorAtom)
if (!globalData)
return null
return calculateScore(globalData)
return calculateProfileScore(globalData)
})
4 changes: 3 additions & 1 deletion src/components/stamp/metric-row.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,9 @@ export function MetricRow({ label, value }: MetricRowProps) {
return (
<div className="flex items-center justify-between py-1 text-sm">
<dt className="text-muted-foreground">{label}</dt>
<dd className="font-medium tabular-nums">{value}</dd>
<dd className="font-medium tabular-nums">
{value}
</dd>
</div>
)
}
5 changes: 3 additions & 2 deletions src/components/stamp/overall-metrics.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ function MetricsSkeleton() {
export function OverallMetrics() {
const [{ data, isPending, isError, error, refetch }]
= useAtom(globalContributorAtom)

if (isPending)
return <MetricsSkeleton />

Expand Down Expand Up @@ -53,12 +54,12 @@ export function OverallMetrics() {
label={i18n.t('globalPrs')}
value={`${data.globalMergedPRs} merged`}
/>
<MetricRow label={i18n.t('commitsYear')} value={data.publicRepos} />
<MetricRow label={i18n.t('followers')} value={data.followers} />
<MetricRow
label={i18n.t('accountAge')}
value={formatAccountAge(data.createdAt)}
/>
<MetricRow label={i18n.t('followers')} value={data.followers} />
<MetricRow label={i18n.t('contributions')} value={data.totalContributions} />
</dl>
)
}
1 change: 1 addition & 0 deletions src/components/stamp/repo-metrics.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ function MetricsSkeleton() {
export function RepoMetrics() {
const [{ data, isPending, isError, error, refetch }]
= useAtom(repoContributorAtom)

if (isPending)
return <MetricsSkeleton />

Expand Down
6 changes: 4 additions & 2 deletions src/components/stamp/score-card.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { i18n } from '#i18n'
import { useAtomValue } from 'jotai'
import { useAtomValue, useSetAtom } from 'jotai'
import { configFieldsAtomMap } from '@/atoms/config'
import { activeTabAtom } from '@/atoms/contributor-score'
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'
import { OverallMetrics } from './overall-metrics'
import { RepoMetrics } from './repo-metrics'
Expand All @@ -9,13 +10,14 @@ import { TokenBanner } from './token-banner'

export function ScoreCard() {
const potToken = useAtomValue(configFieldsAtomMap.potToken)
const setActiveTab = useSetAtom(activeTabAtom)

return (
<div className="bg-card text-card-foreground rounded-lg border border-border p-4 mb-4">
<ScoreHeader />
{potToken
? (
<Tabs defaultValue="repo">
<Tabs defaultValue="repo" onValueChange={v => setActiveTab(v as 'repo' | 'overall')}>
<TabsList className="mt-3">
<TabsTrigger value="repo">{i18n.t('tabThisRepo')}</TabsTrigger>
<TabsTrigger value="overall">{i18n.t('tabOverall')}</TabsTrigger>
Expand Down
15 changes: 10 additions & 5 deletions src/components/stamp/score-header.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { i18n } from '#i18n'
import { useAtomValue } from 'jotai'
import { contributorScoreAtom } from '@/atoms/contributor-score'
import { activeTabAtom, profileScoreAtom, repoScoreAtom } from '@/atoms/contributor-score'
import { Skeleton } from '@/components/ui/skeleton'
import { cn } from '@/lib/utils'

Expand All @@ -27,7 +27,12 @@ function HeaderSkeleton() {
}

export function ScoreHeader() {
const score = useAtomValue(contributorScoreAtom)
const tab = useAtomValue(activeTabAtom)
const repoScore = useAtomValue(repoScoreAtom)
const profileScore = useAtomValue(profileScoreAtom)

const score = tab === 'repo' ? repoScore : profileScore
const label = tab === 'repo' ? i18n.t('repoTrustScore') : i18n.t('profileScore')

if (!score)
return <HeaderSkeleton />
Expand All @@ -36,7 +41,7 @@ export function ScoreHeader() {
<div className="space-y-1">
<div className="flex items-center justify-between">
<span className="text-xs font-semibold tracking-wider text-muted-foreground">
{i18n.t('contributorScore')}
{label}
</span>
<span
className={cn(
Expand All @@ -51,9 +56,9 @@ export function ScoreHeader() {
<div className="flex items-baseline gap-2">
<span
className="text-3xl font-bold tabular-nums"
aria-label={`Score: ${score.overall} out of 100`}
aria-label={`Score: ${score.total} out of 100`}
>
{score.overall}
{score.total}
</span>
<span className="text-sm text-muted-foreground">
{i18n.t('points')}
Expand Down
Loading