diff --git a/app/page.jsx b/app/page.jsx
index 6b490b2..409cd10 100644
--- a/app/page.jsx
+++ b/app/page.jsx
@@ -447,6 +447,7 @@ export default function Home() {
developers={developers}
onResults={handleSearch}
onReset={handleResetFilter}
+ onSelectDeveloper={handleSelectDev}
onGenerateCard={handleGenerateCard}
onSearchState={handleTourSearchState}
onOpenCardFeature={handleOpenCardFeature}
diff --git a/components/DetailPanel.jsx b/components/DetailPanel.jsx
index e0fd001..4a16982 100644
--- a/components/DetailPanel.jsx
+++ b/components/DetailPanel.jsx
@@ -4,7 +4,7 @@ import React, { useEffect, useRef, useState } from 'react';
import Link from 'next/link';
import { track } from '@vercel/analytics';
import * as d3 from 'd3';
-import { formatNum, formatRelativeTime, isStaleData } from '../lib/format.js';
+import { formatNum, formatRelativeTime, formatUsd, isStaleData } from '../lib/format.js';
import { DIMENSIONS, SCORE_METHODOLOGY } from '../lib/scoring.js';
import { SOCIAL_PREVIEW_VERSION } from '../lib/site.js';
import { classifyAgent } from '../lib/agent-class.js';
@@ -417,35 +417,40 @@ function OssWorthSection({ worth }) {
Public contribution footprint
OSS Worth
-
- {formatNum(worth.totalCredits)} OSC
-
+
+
+ {formatNum(worth.totalCredits)} OSC
+
+
+ {formatUsd(worth.totalDollarValue, true)} estimated
+
+
- Fictional OSS Credits celebrate public contributions. They are not compensation, skill, employability, or financial value.
+ A playful estimate based on public activity, not compensation, skill, employability, or market value. 10 OSC equals $1 in this estimate.
-
-
+
+
How this is calculated
- Inputs are log-normalized against fixed reference caps. Formula {worth.formulaVersion}.
+ GitHub follows GitEstimate: contributions × $0.50 + followers × $0.10 + stars × $0.30. Stack Overflow is calculated separately using answers × $0.50 + reputation × $0.10 + badges × $0.30. Formula {worth.formulaVersion}.
);
}
-function WorthCard({ platform, allocation, worth }) {
+function WorthCard({ platform, worth }) {
const platformClass = platform === 'GitHub' ? 'github' : 'stackoverflow';
return (
{platform}
- {allocation}
+ Independent value
-
- {formatNum(worth.credits)} / {formatNum(worth.maxCredits)} OSC
+
+ {formatUsd(worth.dollarValue, true)} · {formatNum(worth.credits)} OSC
{!worth.available ? (
No linked Stack Overflow profile
@@ -455,7 +460,7 @@ function WorthCard({ platform, allocation, worth }) {
{dimension.label}
- {Math.round(dimension.weight * 100)}% · cap {formatNum(dimension.cap)}
+ {formatUsd(dimension.dollarsPerUnit)} each · {formatUsd(dimension.dollarValue, true)}
{formatNum(dimension.sourceValue)}
diff --git a/components/Leaderboard.jsx b/components/Leaderboard.jsx
index 32b14df..736ac28 100644
--- a/components/Leaderboard.jsx
+++ b/components/Leaderboard.jsx
@@ -1,7 +1,7 @@
'use client';
import React, { useMemo, useRef, useState, useEffect, useCallback } from 'react';
-import { formatNum } from '../lib/format.js';
+import { formatNum, formatUsd } from '../lib/format.js';
import { extractCountry, normalizeCountry, countryKey } from '../lib/country.js';
import { SCORE_METHODOLOGY } from '../lib/scoring.js';
import { compareOssWorth } from '../lib/oss-worth.js';
@@ -262,10 +262,10 @@ export default function Leaderboard({
{dev.soReputation ? ● {formatNum(dev.soReputation)} : null}
- OSC {formatNum(dev.ossWorth?.totalCredits || 0)}
+ OSC {formatNum(dev.ossWorth?.totalCredits || 0)} · {formatUsd(dev.ossWorth?.totalDollarValue || 0, true)}
diff --git a/components/SearchBar.jsx b/components/SearchBar.jsx
index 00bf3e5..082a468 100644
--- a/components/SearchBar.jsx
+++ b/components/SearchBar.jsx
@@ -25,7 +25,7 @@ const SAMPLES_BY_MODE = {
],
};
-export default function SearchBar({ developers, onResults, onReset, onGenerateCard, onSearchState, onOpenCardFeature, onOpenCompareFeature, compareCount = 0 }) {
+export default function SearchBar({ developers, onResults, onReset, onSelectDeveloper, onGenerateCard, onSearchState, onOpenCardFeature, onOpenCompareFeature, compareCount = 0 }) {
const [query, setQuery] = useState('');
const [mode, setMode] = useState('text');
const [topN, setTopN] = useState(50);
@@ -230,18 +230,25 @@ export default function SearchBar({ developers, onResults, onReset, onGenerateCa
{singleResult && (
-
-
-
- {singleResult.name || singleResult.login}
-
-
-
- @{singleResult.login}
- {singleResult.globalRank ? ` · Global #${singleResult.globalRank}` : ''}
+ onSelectDeveloper(singleResult)}
+ aria-label={`Open ${singleResult.name || singleResult.login}'s profile`}
+ >
+
+
+
+ {singleResult.name || singleResult.login}
+
+
+
+ @{singleResult.login}
+ {singleResult.globalRank ? ` · Global #${singleResult.globalRank}` : ''}
+
-
-
onGenerateCard(singleResult)}>
+
+
onGenerateCard(singleResult)}>
diff --git a/docs/prd/oss-worth.md b/docs/prd/oss-worth.md
index 1962c34..553314e 100644
--- a/docs/prd/oss-worth.md
+++ b/docs/prd/oss-worth.md
@@ -9,14 +9,7 @@
DevGlobe will add a playful, transparent OSS Worth measure derived from the public GitHub and Stack Overflow contribution metrics it already indexes. A developer profile will show separate GitHub Worth and Stack Overflow Worth cards, while the leaderboard will show the combined OSS Worth and allow sorting by it.
-OSS Worth is denominated in fictional **OSS Credits (OSC)**. It is a celebration of visible open-source participation, not compensation, employability, skill, economic output, or a financial valuation.
-
-The combined maximum is 1,000,000 OSC, allocated exactly as follows:
-
-- GitHub: up to 600,000 OSC (60%).
-- Stack Overflow: up to 400,000 OSC (40%).
-
-Stack Overflow weight is never redistributed. A profile without linked Stack Overflow data receives zero Stack Overflow credits and clearly shows that the source is unavailable.
+OSS Worth is shown as fictional **OSS Credits (OSC)** and a playful estimated USD value. GitHub and Stack Overflow are valued independently and then added together. A profile without linked Stack Overflow data receives zero Stack Overflow value and clearly shows that the source is unavailable.
## Problem
@@ -27,11 +20,11 @@ The current DevGlobe score cannot be reused directly as worth because it is norm
## Goals
- Produce a deterministic OSS Worth from existing public contribution metrics.
-- Preserve an exact 60% GitHub and 40% Stack Overflow maximum allocation.
+- Value GitHub and Stack Overflow independently with fixed per-unit rates.
- Explain every input and contribution to the result.
- Show GitHub and Stack Overflow as two distinct, equally understandable surfaces.
- Show compact OSS Worth in the leaderboard and support sorting by it.
-- Keep values stable as the DevGlobe dataset grows.
+- Keep values stable and uncapped as the DevGlobe dataset grows.
- Handle absent and partial source data honestly.
- Make the formula independently testable and versioned.
@@ -40,74 +33,47 @@ The current DevGlobe score cannot be reused directly as worth because it is norm
- Estimating salary, consulting rates, employability, seniority, or commercial value.
- Replacing the existing DevGlobe score or changing existing rankings.
- Scraping private activity or data not already available through public APIs.
-- Converting OSC to USD, NGN, BRL, or another real currency.
-- Redistributing missing Stack Overflow allocation to GitHub.
+- Presenting the estimate as compensation, salary, or market value.
- Comparing contribution quality, code correctness, or answer correctness.
## Product principles
1. **Celebratory, not evaluative.** Every surface labels OSC as fictional and avoids language such as net worth, market value, cheap, expensive, or hireable.
2. **Transparent.** Users can inspect source metrics, normalized values, weights, caps, and formula version.
-3. **Stable.** Fixed reference caps replace dataset maxima. A result changes only when source metrics or the formula version changes.
-4. **Source-aware.** Missing Stack Overflow data is unavailable, not zero participation and not a reason to inflate GitHub weight.
-5. **Outlier-resistant.** Log normalization lets early contributions matter without allowing very large accounts to dominate the display.
+3. **Stable.** Fixed per-unit rates mean a result changes only when source metrics or the formula version changes.
+4. **Source-aware.** Missing Stack Overflow data is unavailable, not zero participation and not a reason to change GitHub value.
+5. **Additive.** Every public contribution signal adds value directly; there are no allocation ceilings or normalization caps.
-## Formula v1
+## Formula v2
-### Normalization
+### Credits and dollars
-Every non-negative metric uses capped log normalization:
+Every calculated dollar maps to ten OSS Credits:
```text
-normalize(value, cap) = min(log(1 + max(value, 0)) / log(1 + cap), 1)
+credits = round(estimatedDollars * 10)
```
-Reference caps are product constants, not values calculated from the indexed dataset. Inputs above a cap remain visible in the breakdown but contribute the capped normalized value of 1.
-
### GitHub Worth
-| Dimension | Source field | Within-platform weight | Reference cap |
-|---|---|---:|---:|
-| Repository stars | `totalStars` | 30% | 100,000 |
-| Commit activity | `totalCommits` | 30% | 10,000 |
-| Repository reach | `totalForks + totalWatchers` | 25% | 50,000 |
-| Community reach | `followers` | 10% | 25,000 |
-| Public projects | `publicRepos` | 5% | 100 |
+GitHub follows the direct-value formula published by GitEstimate.
```text
-githubIndex =
- starsNormalized * 0.30 +
- commitsNormalized * 0.30 +
- repoReachNormalized * 0.25 +
- followersNormalized * 0.10 +
- publicReposNormalized * 0.05
-
-githubCredits = round(githubIndex * 600000)
+githubDollars =
+ totalCommits * $0.50 +
+ followers * $0.10 +
+ totalStars * $0.30
```
### Stack Overflow Worth
-Accepted answers are estimated from the aggregate fields DevGlobe currently stores:
-
-```text
-acceptedAnswerEstimate = soAnswers * clamp(soAcceptRate, 0, 100) / 100
-```
-
-| Dimension | Source field | Within-platform weight | Reference cap |
-|---|---|---:|---:|
-| Knowledge reputation | `soReputation` | 55% | 100,000 |
-| Accepted-answer engagement | `acceptedAnswerEstimate` | 35% | 1,000 |
-| Community recognition | `soBadges` | 10% | 100 |
+Stack Overflow uses analogous direct rates and remains separate from GitHub.
```text
-stackoverflowIndex =
- reputationNormalized * 0.55 +
- acceptedAnswersNormalized * 0.35 +
- badgesNormalized * 0.10
-
-stackoverflowCredits = hasStackOverflowData
- ? round(stackoverflowIndex * 400000)
- : 0
+stackoverflowDollars =
+ soAnswers * $0.50 +
+ soReputation * $0.10 +
+ soBadges * $0.30
```
`hasStackOverflowData` is true when `soUserId` is present or at least one Stack Overflow metric is positive. A linked profile with all-zero public metrics is available with zero credits; an unlinked profile is unavailable with zero credits.
@@ -115,10 +81,11 @@ stackoverflowCredits = hasStackOverflowData
### Combined OSS Worth
```text
-ossWorth = githubCredits + stackoverflowCredits
+totalDollars = githubDollars + stackoverflowDollars
+totalCredits = githubCredits + stackoverflowCredits
```
-This construction guarantees a maximum allocation of 600,000 GitHub credits plus 400,000 Stack Overflow credits. No secondary weighting or redistribution is applied.
+There is no maximum, allocation, normalization, or redistribution between platforms.
### Returned model
@@ -126,35 +93,34 @@ The pure calculator returns:
```json
{
- "formulaVersion": "oss-worth-v1",
+ "formulaVersion": "oss-worth-v2",
"totalCredits": 0,
+ "totalDollarValue": 0,
"github": {
"available": true,
"credits": 0,
- "maxCredits": 600000,
- "index": 0,
+ "dollarValue": 0,
"breakdown": []
},
"stackoverflow": {
"available": false,
"credits": 0,
- "maxCredits": 400000,
- "index": 0,
+ "dollarValue": 0,
"breakdown": []
}
}
```
-Each breakdown entry contains the source value, reference cap, normalized value, within-platform weight, and resulting credits. Internal calculations retain full precision; only displayed credits and final serialized credits are rounded.
+Each breakdown entry contains the source value, per-unit dollar rate, resulting dollar value, and credits.
## Data contract
-No new external API calls are required. The v1 inputs already exist in developer documents:
+No new external API calls are required. The v2 inputs already exist in developer documents:
-- GitHub: `totalStars`, `totalCommits`, `totalForks`, `totalWatchers`, `followers`, `publicRepos`.
-- Stack Overflow: `soUserId`, `soReputation`, `soAnswers`, `soAcceptRate`, `soBadges`.
+- GitHub: `totalStars`, `totalCommits`, `followers`.
+- Stack Overflow: `soUserId`, `soReputation`, `soAnswers`, `soBadges`.
-The public developer list currently omits `totalWatchers` and `publicRepos`; its Cosmos projection must include them. The detail projection must include every formula input consistently. Search, cards, MCP, and persisted documents do not need new fields for MVP unless they display OSS Worth.
+The list and detail projections must include every formula input consistently. Search, cards, MCP, and persisted documents do not need new fields unless they display OSS Worth.
Worth should be computed through one shared pure module, proposed as `lib/oss-worth.js`. Callers may enrich a developer response with the returned model, but formula logic must not be duplicated in React components or API routes.
@@ -168,18 +134,18 @@ Add an unframed **OSS Worth** section after the primary contribution statistics
The section contains:
-- A combined headline value such as `428K OSC` and a short fictional-credit disclaimer.
-- A GitHub Worth card showing credits out of 600K, its 60% allocation, and the five source metrics.
-- A Stack Overflow Worth card showing credits out of 400K, its 40% allocation, and the three source metrics.
-- A compact “How this is calculated” disclosure with formula version and reference caps.
+- A combined headline showing OSC and estimated USD with a short playful-estimate disclaimer.
+- A GitHub Worth card showing its independent USD and OSC value and three source metrics.
+- A Stack Overflow Worth card showing its independent USD and OSC value and three source metrics.
+- A compact “How this is calculated” disclosure with formula version and per-unit rates.
The two cards are siblings, never nested. On desktop they use a two-column grid; on narrow screens they stack. GitHub and Stack Overflow retain their recognizable platform accents without making the page a one-color theme.
-When Stack Overflow is unavailable, its card remains visible in an unavailable state with `No linked Stack Overflow profile` and `0 / 400K OSC`. It must not imply poor performance.
+When Stack Overflow is unavailable, its card remains visible in an unavailable state with `No linked Stack Overflow profile` and zero value. It must not imply poor performance.
### Leaderboard
-- Add a compact badge such as `OSC 428K` to each row.
+- Add a compact badge such as `OSC 428K · $42.8K` to each row.
- Add `Worth` to the existing sort menu.
- Sort descending by `totalCredits`, with the existing score and login as deterministic tie-breakers.
- Preserve the row's fixed height and virtualized layout; the new badge must not shift action controls or truncate the developer name.
@@ -195,9 +161,9 @@ When Stack Overflow is unavailable, its card remains visible in an unavailable s
## Integration plan
-1. Add `lib/oss-worth.js` with constants, normalization, platform calculations, and combined calculation.
+1. Add `lib/oss-worth.js` with fixed rates, platform calculations, and combined calculation.
2. Add fixture-driven unit tests in `tests/oss-worth.test.js`.
-3. Include `totalWatchers` and `publicRepos` in the developer list/detail projections.
+3. Include every v2 formula input in the developer list/detail projections.
4. Enrich developers once in the landing-page data pipeline; do not calculate repeatedly during renders or sorting.
5. Add the OSS Worth section and two platform cards to `DetailPanel`.
6. Add the compact value and Worth sort option to `Leaderboard`.
@@ -219,12 +185,11 @@ Do not include raw contribution metrics, calculated credit values, email, or pri
### Unit tests
- All-zero inputs produce zero credits.
-- Values at or above every cap produce exactly 600,000 GitHub credits and 400,000 Stack Overflow credits.
-- Missing Stack Overflow data produces `available: false`, zero SO credits, and no redistribution.
+- GitHub inputs produce the exact GitEstimate direct-value result.
+- Stack Overflow values are calculated separately and added to the total.
- A linked all-zero Stack Overflow profile produces `available: true` and zero SO credits.
-- Increasing any one metric while others remain fixed never decreases credits.
-- Negative, missing, non-finite, and over-100 acceptance-rate inputs are safely clamped.
-- Rounding occurs only at platform-credit output.
+- Doubling any one metric doubles that metric's contribution.
+- Negative, missing, and non-finite inputs are safely clamped.
- Formula version is always returned.
### UI tests
@@ -260,15 +225,15 @@ Do not include raw contribution metrics, calculated credit values, email, or pri
## Risks and mitigations
- **Misread as financial value:** use OSS Credits, display the disclaimer, and prohibit currency conversion in v1.
-- **Metric gaming:** cap and log-normalize inputs; describe the result as playful rather than authoritative.
+- **Metric gaming:** describe the direct estimate as playful rather than authoritative and expose every per-unit rate.
- **Stale source data:** show the existing metrics freshness timestamp near the methodology.
-- **Missing Stack Overflow profiles:** keep the 40% unavailable instead of silently reallocating it.
+- **Missing Stack Overflow profiles:** show the source as unavailable and leave its value at zero.
- **Formula drift:** export constants from one module and include `formulaVersion` in every result.
- **Large payloads:** return the full breakdown only on detail surfaces if list payload size becomes material; leaderboard needs only total credits.
## References
-- [GitHub Worth Calculator](https://github.com/enigma-137/github-worth): playful GitHub-only scoring, transparent breakdown, fixed multiplier, sharing, and an explicit entertainment disclaimer.
+- [GitEstimate](https://github.com/taqui-786/GitEstimate): direct GitHub valuation using contributions, followers, and stars.
- [CommitWorth](https://github.com/andreluizdasilvaa/CommitWorth): direct contribution rates, dashboard metrics, achievements, and generated cards.
The references inform product behavior only. DevGlobe will implement its own formula and code against its existing data model.
diff --git a/lib/format.js b/lib/format.js
index 60682d7..ef410ca 100644
--- a/lib/format.js
+++ b/lib/format.js
@@ -38,4 +38,13 @@ export function isStaleData(isoString, staleDays = STALE_DATA_DAYS) {
if (Number.isNaN(date.getTime())) return true;
const diffDays = (Date.now() - date.getTime()) / (1000 * 60 * 60 * 24);
return diffDays > staleDays;
+}
+
+export function formatUsd(value, compact = false) {
+ return new Intl.NumberFormat('en-US', {
+ style: 'currency',
+ currency: 'USD',
+ maximumFractionDigits: compact ? 1 : 2,
+ notation: compact ? 'compact' : 'standard',
+ }).format(value || 0);
}
\ No newline at end of file
diff --git a/lib/oss-worth.js b/lib/oss-worth.js
index 7042383..071c1e8 100644
--- a/lib/oss-worth.js
+++ b/lib/oss-worth.js
@@ -1,23 +1,19 @@
-export const OSS_WORTH_FORMULA_VERSION = 'oss-worth-v1';
-export const OSS_WORTH_MAX_CREDITS = 1_000_000;
+export const OSS_WORTH_FORMULA_VERSION = 'oss-worth-v2';
+export const OSS_CREDITS_PER_DOLLAR = 10;
export const OSS_WORTH_PLATFORMS = {
github: {
- maxCredits: 600_000,
dimensions: [
- { key: 'stars', label: 'Repository stars', field: 'totalStars', weight: 0.30, cap: 100_000 },
- { key: 'commits', label: 'Commit activity', field: 'totalCommits', weight: 0.30, cap: 10_000 },
- { key: 'repoReach', label: 'Repository reach', weight: 0.25, cap: 50_000 },
- { key: 'followers', label: 'Community reach', field: 'followers', weight: 0.10, cap: 25_000 },
- { key: 'publicRepos', label: 'Public projects', field: 'publicRepos', weight: 0.05, cap: 100 },
+ { key: 'contributions', label: 'Contributions', field: 'totalCommits', dollarsPerUnit: 0.50 },
+ { key: 'followers', label: 'Followers', field: 'followers', dollarsPerUnit: 0.10 },
+ { key: 'stars', label: 'Repository stars', field: 'totalStars', dollarsPerUnit: 0.30 },
],
},
stackoverflow: {
- maxCredits: 400_000,
dimensions: [
- { key: 'reputation', label: 'Knowledge reputation', field: 'soReputation', weight: 0.55, cap: 100_000 },
- { key: 'acceptedAnswers', label: 'Accepted-answer engagement', weight: 0.35, cap: 1_000 },
- { key: 'badges', label: 'Community recognition', field: 'soBadges', weight: 0.10, cap: 100 },
+ { key: 'answers', label: 'Answers', field: 'soAnswers', dollarsPerUnit: 0.50 },
+ { key: 'reputation', label: 'Reputation', field: 'soReputation', dollarsPerUnit: 0.10 },
+ { key: 'badges', label: 'Badges', field: 'soBadges', dollarsPerUnit: 0.30 },
],
},
};
@@ -27,36 +23,28 @@ function nonNegativeNumber(value) {
return Number.isFinite(number) ? Math.max(number, 0) : 0;
}
-function normalize(value, cap) {
- return Math.min(Math.log1p(nonNegativeNumber(value)) / Math.log1p(cap), 1);
-}
-
-function calculatePlatform(dimensions, maxCredits, values, available = true) {
+function calculatePlatform(dimensions, values, available = true) {
const breakdown = dimensions.map(dimension => {
const sourceValue = nonNegativeNumber(values[dimension.key]);
- const normalized = normalize(sourceValue, dimension.cap);
+ const dollarValue = available ? sourceValue * dimension.dollarsPerUnit : 0;
return {
...dimension,
sourceValue,
- normalized,
- credits: normalized * dimension.weight * maxCredits,
+ dollarValue,
+ credits: Math.round(dollarValue * OSS_CREDITS_PER_DOLLAR),
};
});
- const index = available
- ? breakdown.reduce((total, dimension) => total + dimension.normalized * dimension.weight, 0)
- : 0;
+ const dollarValue = breakdown.reduce((total, dimension) => total + dimension.dollarValue, 0);
return {
available,
- credits: Math.round(index * maxCredits),
- maxCredits,
- index,
+ credits: Math.round(dollarValue * OSS_CREDITS_PER_DOLLAR),
+ dollarValue,
breakdown,
};
}
export function calculateOssWorth(developer = {}) {
- const acceptRate = Math.min(nonNegativeNumber(developer.soAcceptRate), 100);
const hasStackOverflowData = Boolean(developer.soUserId) ||
nonNegativeNumber(developer.soReputation) > 0 ||
nonNegativeNumber(developer.soAnswers) > 0 ||
@@ -64,21 +52,17 @@ export function calculateOssWorth(developer = {}) {
const github = calculatePlatform(
OSS_WORTH_PLATFORMS.github.dimensions,
- OSS_WORTH_PLATFORMS.github.maxCredits,
{
- stars: developer.totalStars,
- commits: developer.totalCommits,
- repoReach: nonNegativeNumber(developer.totalForks) + nonNegativeNumber(developer.totalWatchers),
+ contributions: developer.totalCommits,
followers: developer.followers,
- publicRepos: developer.publicRepos,
+ stars: developer.totalStars,
}
);
const stackoverflow = calculatePlatform(
OSS_WORTH_PLATFORMS.stackoverflow.dimensions,
- OSS_WORTH_PLATFORMS.stackoverflow.maxCredits,
{
+ answers: developer.soAnswers,
reputation: developer.soReputation,
- acceptedAnswers: nonNegativeNumber(developer.soAnswers) * acceptRate / 100,
badges: developer.soBadges,
},
hasStackOverflowData
@@ -87,6 +71,7 @@ export function calculateOssWorth(developer = {}) {
return {
formulaVersion: OSS_WORTH_FORMULA_VERSION,
totalCredits: github.credits + stackoverflow.credits,
+ totalDollarValue: github.dollarValue + stackoverflow.dollarValue,
github,
stackoverflow,
};
diff --git a/styles/main.css b/styles/main.css
index 307f103..b44b8f3 100644
--- a/styles/main.css
+++ b/styles/main.css
@@ -2606,17 +2606,33 @@ body {
font-size: 18px;
}
-.oss-worth__heading > strong {
+.oss-worth__totals {
+ display: flex;
+ align-items: flex-end;
+ flex-direction: column;
+ gap: 4px;
+ text-align: right;
+}
+
+.oss-worth__totals > strong {
color: #22d3ee;
font-size: 24px;
line-height: 1;
white-space: nowrap;
}
-.oss-worth__heading > strong small {
+.oss-worth__totals > strong small {
font-size: 11px;
}
+.oss-worth__totals > span {
+ color: var(--text-secondary);
+ font-size: 11px;
+ font-weight: 600;
+ letter-spacing: 0;
+ text-transform: none;
+}
+
.oss-worth__disclaimer {
margin: 9px 0 14px;
color: var(--text-muted);
@@ -4798,7 +4814,7 @@ body {
.search-bar__card-suggestion {
display: grid;
- grid-template-columns: 36px minmax(0, 1fr) auto;
+ grid-template-columns: minmax(0, 1fr) auto;
align-items: center;
gap: 10px;
margin-top: 8px;
@@ -4806,7 +4822,32 @@ body {
border-top: 1px solid var(--border);
}
-.search-bar__card-suggestion > img {
+.search-bar__profile-result {
+ display: grid;
+ grid-template-columns: 36px minmax(0, 1fr);
+ align-items: center;
+ gap: 10px;
+ min-width: 0;
+ padding: 0;
+ border: 0;
+ background: transparent;
+ color: inherit;
+ font: inherit;
+ text-align: left;
+ cursor: pointer;
+}
+
+.search-bar__profile-result:hover .search-bar__card-identity strong {
+ color: #22d3ee;
+}
+
+.search-bar__profile-result:focus-visible {
+ border-radius: 5px;
+ outline: 2px solid #22d3ee;
+ outline-offset: 3px;
+}
+
+.search-bar__profile-result > img {
width: 36px;
height: 36px;
border-radius: 50%;
@@ -4845,7 +4886,7 @@ body {
font-size: 10px;
}
-.search-bar__card-suggestion > button {
+.search-bar__card-action {
display: inline-flex;
align-items: center;
gap: 6px;
@@ -4859,7 +4900,7 @@ body {
transition: background 0.15s, border-color 0.15s;
}
-.search-bar__card-suggestion > button:hover {
+.search-bar__card-action:hover {
border-color: #0891b2;
background: rgba(8, 145, 178, 0.24);
}
diff --git a/tests/oss-worth.test.js b/tests/oss-worth.test.js
index ac33b84..98a8e44 100644
--- a/tests/oss-worth.test.js
+++ b/tests/oss-worth.test.js
@@ -3,8 +3,8 @@ import assert from 'node:assert/strict';
import {
calculateOssWorth,
compareOssWorth,
+ OSS_CREDITS_PER_DOLLAR,
OSS_WORTH_FORMULA_VERSION,
- OSS_WORTH_MAX_CREDITS,
} from '../lib/oss-worth.js';
test('zero inputs produce zero credits with unavailable Stack Overflow', () => {
@@ -12,43 +12,38 @@ test('zero inputs produce zero credits with unavailable Stack Overflow', () => {
assert.equal(worth.formulaVersion, OSS_WORTH_FORMULA_VERSION);
assert.equal(worth.totalCredits, 0);
+ assert.equal(worth.totalDollarValue, 0);
assert.equal(worth.github.available, true);
assert.equal(worth.stackoverflow.available, false);
});
-test('reference caps produce the exact 60/40 maximum allocation', () => {
+test('GitHub uses the GitEstimate direct-value formula', () => {
const worth = calculateOssWorth({
- totalStars: 100_000,
- totalCommits: 10_000,
- totalForks: 25_000,
- totalWatchers: 25_000,
- followers: 25_000,
- publicRepos: 100,
- soUserId: 1,
- soReputation: 100_000,
- soAnswers: 1_000,
- soAcceptRate: 100,
- soBadges: 100,
+ totalCommits: 1_000,
+ followers: 200,
+ totalStars: 300,
});
- assert.equal(worth.github.credits, 600_000);
- assert.equal(worth.stackoverflow.credits, 400_000);
- assert.equal(worth.totalCredits, OSS_WORTH_MAX_CREDITS);
+ assert.equal(worth.github.dollarValue, 610);
+ assert.equal(worth.github.credits, 6_100);
+ assert.equal(worth.totalDollarValue, 610);
});
-test('missing Stack Overflow data is not redistributed to GitHub', () => {
+test('Stack Overflow is valued separately and added to the total', () => {
const worth = calculateOssWorth({
- totalStars: 100_000,
- totalCommits: 10_000,
- totalForks: 50_000,
- followers: 25_000,
- publicRepos: 100,
+ totalCommits: 100,
+ followers: 20,
+ totalStars: 30,
+ soUserId: 1,
+ soAnswers: 40,
+ soReputation: 500,
+ soBadges: 10,
});
- assert.equal(worth.github.credits, 600_000);
- assert.equal(worth.stackoverflow.available, false);
- assert.equal(worth.stackoverflow.credits, 0);
- assert.equal(worth.totalCredits, 600_000);
+ assert.equal(worth.github.dollarValue, 61);
+ assert.equal(worth.stackoverflow.dollarValue, 73);
+ assert.equal(worth.totalDollarValue, 134);
+ assert.equal(worth.totalCredits, 134 * OSS_CREDITS_PER_DOLLAR);
});
test('a linked all-zero Stack Overflow profile is available with zero credits', () => {
@@ -58,13 +53,13 @@ test('a linked all-zero Stack Overflow profile is available with zero credits',
assert.equal(worth.stackoverflow.credits, 0);
});
-test('credits are monotonic and invalid inputs are clamped', () => {
- const baseline = calculateOssWorth({ totalStars: 10, soUserId: 1, soAnswers: 10, soAcceptRate: 50 });
- const increased = calculateOssWorth({ totalStars: 20, soUserId: 1, soAnswers: 10, soAcceptRate: 150 });
- const invalid = calculateOssWorth({ totalStars: -1, totalCommits: Infinity, soUserId: 1, soAnswers: -10, soAcceptRate: NaN });
+test('direct values scale linearly and invalid inputs are clamped', () => {
+ const baseline = calculateOssWorth({ totalStars: 10, soUserId: 1, soAnswers: 10 });
+ const doubled = calculateOssWorth({ totalStars: 20, soUserId: 1, soAnswers: 20 });
+ const invalid = calculateOssWorth({ totalStars: -1, totalCommits: Infinity, soUserId: 1, soAnswers: -10, soReputation: NaN });
- assert.ok(increased.github.credits >= baseline.github.credits);
- assert.ok(increased.stackoverflow.credits >= baseline.stackoverflow.credits);
+ assert.equal(doubled.github.credits, baseline.github.credits * 2);
+ assert.equal(doubled.stackoverflow.credits, baseline.stackoverflow.credits * 2);
assert.equal(invalid.github.credits, 0);
assert.equal(invalid.stackoverflow.credits, 0);
});