github-rep 0.1.1__tar.gz → 0.2.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,245 @@
1
+ Metadata-Version: 2.4
2
+ Name: github-rep
3
+ Version: 0.2.0
4
+ Summary: Analyze a GitHub profile and get honest, actionable advice for building real reputation
5
+ Author-email: Basil Alshukaili <basilalshukaili@gmail.com>
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/basilalshukaili/github-rep
8
+ Project-URL: Bug Tracker, https://github.com/basilalshukaili/github-rep/issues
9
+ Keywords: github,open-source,developer-tools,profile,reputation,cli
10
+ Classifier: Development Status :: 3 - Alpha
11
+ Classifier: Environment :: Console
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: License :: OSI Approved :: MIT License
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Programming Language :: Python :: 3.9
16
+ Classifier: Programming Language :: Python :: 3.10
17
+ Classifier: Programming Language :: Python :: 3.11
18
+ Classifier: Programming Language :: Python :: 3.12
19
+ Classifier: Topic :: Software Development :: Version Control :: Git
20
+ Requires-Python: >=3.9
21
+ Description-Content-Type: text/markdown
22
+ License-File: LICENSE
23
+ Requires-Dist: requests>=2.31
24
+ Requires-Dist: rich>=13.0
25
+ Requires-Dist: typer>=0.9
26
+ Requires-Dist: python-dateutil>=2.8
27
+ Provides-Extra: dev
28
+ Requires-Dist: pytest>=7; extra == "dev"
29
+ Requires-Dist: responses>=0.25; extra == "dev"
30
+ Requires-Dist: pytest-cov>=4; extra == "dev"
31
+ Dynamic: license-file
32
+
33
+ # github-rep
34
+
35
+ [![PyPI version](https://img.shields.io/pypi/v/github-rep.svg)](https://pypi.org/project/github-rep/)
36
+ [![Python versions](https://img.shields.io/pypi/pyversions/github-rep.svg)](https://pypi.org/project/github-rep/)
37
+ [![CI](https://github.com/basilalshukaili/github-rep/actions/workflows/ci.yml/badge.svg)](https://github.com/basilalshukaili/github-rep/actions/workflows/ci.yml)
38
+ [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE)
39
+
40
+ **Score any GitHub user's reputation across 11 honest signals and get a prioritized fix list — in under 30 seconds.**
41
+
42
+ ---
43
+
44
+ ## Why this exists
45
+
46
+ GitHub profiles are the de-facto developer resume. Most advice on building GitHub reputation is either
47
+ vague ("just contribute more") or gameable (spam-stars, low-effort PRs). This tool measures the signals
48
+ that actually matter to recruiters, maintainers, and other developers — and tells you exactly what to
49
+ fix first, ordered by impact.
50
+
51
+ ---
52
+
53
+ ## Install
54
+
55
+ ```bash
56
+ pip install github-rep
57
+ ```
58
+
59
+ Requires Python 3.9+. No configuration needed — works unauthenticated (60 req/hr) or with a GitHub
60
+ token for 5000 req/hr.
61
+
62
+ ---
63
+
64
+ ## Quickstart
65
+
66
+ ```bash
67
+ # Analyze any GitHub user
68
+ github-rep analyze-profile torvalds
69
+
70
+ # Use a token to avoid rate limits
71
+ export GITHUB_TOKEN=ghp_yourtoken
72
+ github-rep analyze-profile sindresorhus
73
+
74
+ # Machine-readable JSON output
75
+ github-rep analyze-profile gvanrossum --json
76
+
77
+ # Show all findings, including low-priority ones
78
+ github-rep analyze-profile octocat --verbose
79
+
80
+ # Compare multiple profiles side by side
81
+ github-rep compare torvalds gvanrossum sindresorhus
82
+
83
+ # Check your API rate limit
84
+ github-rep rate-limit
85
+ ```
86
+
87
+ ---
88
+
89
+ ## Sample output
90
+
91
+ ```
92
+ $ github-rep analyze-profile torvalds
93
+
94
+ ╭──────────────────────────────── GitHub Profile ─────────────────────────────╮
95
+ │ @torvalds | Linus Torvalds │
96
+ │ │
97
+ │ Followers: 305,509 | Public repos: 12 | Stars earned: 246,028 │
98
+ ╰──────────────────────────────────────────────────────────────────────────────╯
99
+ Grade: B (78/100)
100
+ Tier: Active developer
101
+
102
+ Score Breakdown
103
+ ┏━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━┳━━━━━━━┓
104
+ ┃ Dimension ┃ Score ┃ Max ┃
105
+ ┡━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━╇━━━━━━━┩
106
+ │ Profile Completeness │ 4 │ 10 │
107
+ │ Readme Quality │ 12 │ 15 │
108
+ │ Star Signal │ 20 │ 20 │
109
+ │ Contribution Streak │ 15 │ 15 │
110
+ │ Repo Diversity │ 7 │ 10 │
111
+ │ Description Quality │ 10 │ 10 │
112
+ │ Topic Tags │ 0 │ 5 │
113
+ │ Fork Ratio │ 5 │ 5 │
114
+ │ Recent Activity │ 5 │ 10 │
115
+ │ Release Cadence │ 0 │ 5 │
116
+ │ Profile Readme │ 0 │ 5 │
117
+ └──────────────────────────┴────────┴───────┘
118
+
119
+ Priority fixes:
120
+ [HIGH] Missing bio
121
+ Your bio is empty. It is the first thing visitors read.
122
+ Fix: Write 1-2 sentences: your focus, what you build, your superpower.
123
+
124
+ What is working:
125
+ [GOOD] Strong star signal (246028 total, top repo: 235147)
126
+ [GOOD] Active recent commits (0d ago)
127
+ [GOOD] All repos have descriptions
128
+ ```
129
+
130
+ Grades: **A** (≥80) · **B** (≥65) · **C** (≥50) · **D** (≥35) · **F**
131
+
132
+ ---
133
+
134
+ ## The 11 scored dimensions
135
+
136
+ | # | Dimension | Max pts | What it measures |
137
+ |---|-----------|--------:|------------------|
138
+ | 1 | Profile Completeness | 10 | Name, bio, avatar, location, website / social link |
139
+ | 2 | README Quality | 15 | Top repo README length, code examples, install instructions |
140
+ | 3 | Star Signal | 20 | Total stars earned across all repos (log-scaled) |
141
+ | 4 | Contribution Streak | 15 | Days since last profile activity |
142
+ | 5 | Repo Diversity | 10 | Number of public repos and language breadth |
143
+ | 6 | Description Quality | 10 | Fraction of repos with meaningful descriptions |
144
+ | 7 | Topic Tags | 5 | Repos tagged with relevant GitHub topics |
145
+ | 8 | Fork Ratio | 5 | Proportion of original work vs. forked repos |
146
+ | 9 | Recent Activity | 10 | Repos with pushes in the last 90 days |
147
+ | 10 | **Release Cadence** | 5 | Published GitHub Releases on top repos |
148
+ | 11 | **Profile README** | 5 | Presence and quality of the username/username profile README |
149
+
150
+ **Total: 100 points.**
151
+
152
+ Dimensions 10 and 11 are new in v0.2.0 and measure signals that indicate a polished,
153
+ production-ready presence — publishing versioned releases and curating a profile page.
154
+
155
+ ---
156
+
157
+ ## All flags
158
+
159
+ ```
160
+ github-rep analyze-profile <username> [OPTIONS]
161
+ ```
162
+
163
+ | Flag | Description |
164
+ |------|-------------|
165
+ | `--json` | Machine-readable JSON output (all scores + findings) |
166
+ | `--verbose` / `-v` | Show all findings including low-priority improvements |
167
+ | `--token` / `-t` | GitHub PAT (also reads `GITHUB_TOKEN` env var) |
168
+ | `--top N` | Number of top repos to deep-analyze (default: 10) |
169
+ | `--help` | Show help and exit |
170
+
171
+ ---
172
+
173
+ ## JSON output
174
+
175
+ ```bash
176
+ github-rep analyze-profile gvanrossum --json
177
+ ```
178
+
179
+ ```json
180
+ {
181
+ "username": "gvanrossum",
182
+ "total": 72,
183
+ "grade": "B",
184
+ "tier": "Active developer",
185
+ "breakdown": {
186
+ "profile_completeness": 8,
187
+ "readme_quality": 12,
188
+ ...
189
+ },
190
+ "findings": [
191
+ {
192
+ "category": "topic_tags",
193
+ "severity": "medium",
194
+ "title": "Most repos have no topic tags",
195
+ "detail": "Missing from GitHub Explore category pages entirely.",
196
+ "fix": "Add topics to your top 3 repos today - GitHub UI, takes 2 minutes."
197
+ }
198
+ ]
199
+ }
200
+ ```
201
+
202
+ ---
203
+
204
+ ## Caching
205
+
206
+ Results are cached for 5 minutes under `~/.cache/github-rep/` to avoid hitting rate limits when
207
+ running the tool multiple times. Delete the cache directory to force a fresh fetch.
208
+
209
+ ---
210
+
211
+ ## FAQ
212
+
213
+ **Do I need a GitHub token?**
214
+ No. Unauthenticated usage gets 60 API requests per hour — enough for a single profile analysis.
215
+ Set `GITHUB_TOKEN` to get 5000 requests per hour and avoid hitting limits when comparing many profiles.
216
+
217
+ **How is the score calculated?**
218
+ Each dimension is scored independently against fixed max points (totalling 100). There is no
219
+ machine learning or relative ranking — the score reflects the absolute presence or absence of
220
+ each signal.
221
+
222
+ **Can I game the score?**
223
+ You could game every individual metric, but you would also genuinely improve your GitHub presence
224
+ in the process. The signals are chosen because they correlate with real reputation value.
225
+
226
+ **Why does a well-known developer score lower than expected?**
227
+ Some high-reputation developers (including Linus Torvalds) score below 80 because they skip
228
+ signals like topic tags, profile READMEs, or GitHub Releases. The tool measures profile
229
+ hygiene signals, not absolute influence.
230
+
231
+ **How often should I re-run this?**
232
+ After each batch of improvements. Treat it like `npm audit` — run it, fix the findings, move on.
233
+
234
+ ---
235
+
236
+ ## Contributing
237
+
238
+ See [CONTRIBUTING.md](CONTRIBUTING.md). Bug reports and improvements welcome — open an issue on
239
+ [github.com/basilalshukaili/github-rep](https://github.com/basilalshukaili/github-rep/issues).
240
+
241
+ ---
242
+
243
+ ## License
244
+
245
+ MIT — see [LICENSE](LICENSE).
@@ -0,0 +1,213 @@
1
+ # github-rep
2
+
3
+ [![PyPI version](https://img.shields.io/pypi/v/github-rep.svg)](https://pypi.org/project/github-rep/)
4
+ [![Python versions](https://img.shields.io/pypi/pyversions/github-rep.svg)](https://pypi.org/project/github-rep/)
5
+ [![CI](https://github.com/basilalshukaili/github-rep/actions/workflows/ci.yml/badge.svg)](https://github.com/basilalshukaili/github-rep/actions/workflows/ci.yml)
6
+ [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE)
7
+
8
+ **Score any GitHub user's reputation across 11 honest signals and get a prioritized fix list — in under 30 seconds.**
9
+
10
+ ---
11
+
12
+ ## Why this exists
13
+
14
+ GitHub profiles are the de-facto developer resume. Most advice on building GitHub reputation is either
15
+ vague ("just contribute more") or gameable (spam-stars, low-effort PRs). This tool measures the signals
16
+ that actually matter to recruiters, maintainers, and other developers — and tells you exactly what to
17
+ fix first, ordered by impact.
18
+
19
+ ---
20
+
21
+ ## Install
22
+
23
+ ```bash
24
+ pip install github-rep
25
+ ```
26
+
27
+ Requires Python 3.9+. No configuration needed — works unauthenticated (60 req/hr) or with a GitHub
28
+ token for 5000 req/hr.
29
+
30
+ ---
31
+
32
+ ## Quickstart
33
+
34
+ ```bash
35
+ # Analyze any GitHub user
36
+ github-rep analyze-profile torvalds
37
+
38
+ # Use a token to avoid rate limits
39
+ export GITHUB_TOKEN=ghp_yourtoken
40
+ github-rep analyze-profile sindresorhus
41
+
42
+ # Machine-readable JSON output
43
+ github-rep analyze-profile gvanrossum --json
44
+
45
+ # Show all findings, including low-priority ones
46
+ github-rep analyze-profile octocat --verbose
47
+
48
+ # Compare multiple profiles side by side
49
+ github-rep compare torvalds gvanrossum sindresorhus
50
+
51
+ # Check your API rate limit
52
+ github-rep rate-limit
53
+ ```
54
+
55
+ ---
56
+
57
+ ## Sample output
58
+
59
+ ```
60
+ $ github-rep analyze-profile torvalds
61
+
62
+ ╭──────────────────────────────── GitHub Profile ─────────────────────────────╮
63
+ │ @torvalds | Linus Torvalds │
64
+ │ │
65
+ │ Followers: 305,509 | Public repos: 12 | Stars earned: 246,028 │
66
+ ╰──────────────────────────────────────────────────────────────────────────────╯
67
+ Grade: B (78/100)
68
+ Tier: Active developer
69
+
70
+ Score Breakdown
71
+ ┏━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━┳━━━━━━━┓
72
+ ┃ Dimension ┃ Score ┃ Max ┃
73
+ ┡━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━╇━━━━━━━┩
74
+ │ Profile Completeness │ 4 │ 10 │
75
+ │ Readme Quality │ 12 │ 15 │
76
+ │ Star Signal │ 20 │ 20 │
77
+ │ Contribution Streak │ 15 │ 15 │
78
+ │ Repo Diversity │ 7 │ 10 │
79
+ │ Description Quality │ 10 │ 10 │
80
+ │ Topic Tags │ 0 │ 5 │
81
+ │ Fork Ratio │ 5 │ 5 │
82
+ │ Recent Activity │ 5 │ 10 │
83
+ │ Release Cadence │ 0 │ 5 │
84
+ │ Profile Readme │ 0 │ 5 │
85
+ └──────────────────────────┴────────┴───────┘
86
+
87
+ Priority fixes:
88
+ [HIGH] Missing bio
89
+ Your bio is empty. It is the first thing visitors read.
90
+ Fix: Write 1-2 sentences: your focus, what you build, your superpower.
91
+
92
+ What is working:
93
+ [GOOD] Strong star signal (246028 total, top repo: 235147)
94
+ [GOOD] Active recent commits (0d ago)
95
+ [GOOD] All repos have descriptions
96
+ ```
97
+
98
+ Grades: **A** (≥80) · **B** (≥65) · **C** (≥50) · **D** (≥35) · **F**
99
+
100
+ ---
101
+
102
+ ## The 11 scored dimensions
103
+
104
+ | # | Dimension | Max pts | What it measures |
105
+ |---|-----------|--------:|------------------|
106
+ | 1 | Profile Completeness | 10 | Name, bio, avatar, location, website / social link |
107
+ | 2 | README Quality | 15 | Top repo README length, code examples, install instructions |
108
+ | 3 | Star Signal | 20 | Total stars earned across all repos (log-scaled) |
109
+ | 4 | Contribution Streak | 15 | Days since last profile activity |
110
+ | 5 | Repo Diversity | 10 | Number of public repos and language breadth |
111
+ | 6 | Description Quality | 10 | Fraction of repos with meaningful descriptions |
112
+ | 7 | Topic Tags | 5 | Repos tagged with relevant GitHub topics |
113
+ | 8 | Fork Ratio | 5 | Proportion of original work vs. forked repos |
114
+ | 9 | Recent Activity | 10 | Repos with pushes in the last 90 days |
115
+ | 10 | **Release Cadence** | 5 | Published GitHub Releases on top repos |
116
+ | 11 | **Profile README** | 5 | Presence and quality of the username/username profile README |
117
+
118
+ **Total: 100 points.**
119
+
120
+ Dimensions 10 and 11 are new in v0.2.0 and measure signals that indicate a polished,
121
+ production-ready presence — publishing versioned releases and curating a profile page.
122
+
123
+ ---
124
+
125
+ ## All flags
126
+
127
+ ```
128
+ github-rep analyze-profile <username> [OPTIONS]
129
+ ```
130
+
131
+ | Flag | Description |
132
+ |------|-------------|
133
+ | `--json` | Machine-readable JSON output (all scores + findings) |
134
+ | `--verbose` / `-v` | Show all findings including low-priority improvements |
135
+ | `--token` / `-t` | GitHub PAT (also reads `GITHUB_TOKEN` env var) |
136
+ | `--top N` | Number of top repos to deep-analyze (default: 10) |
137
+ | `--help` | Show help and exit |
138
+
139
+ ---
140
+
141
+ ## JSON output
142
+
143
+ ```bash
144
+ github-rep analyze-profile gvanrossum --json
145
+ ```
146
+
147
+ ```json
148
+ {
149
+ "username": "gvanrossum",
150
+ "total": 72,
151
+ "grade": "B",
152
+ "tier": "Active developer",
153
+ "breakdown": {
154
+ "profile_completeness": 8,
155
+ "readme_quality": 12,
156
+ ...
157
+ },
158
+ "findings": [
159
+ {
160
+ "category": "topic_tags",
161
+ "severity": "medium",
162
+ "title": "Most repos have no topic tags",
163
+ "detail": "Missing from GitHub Explore category pages entirely.",
164
+ "fix": "Add topics to your top 3 repos today - GitHub UI, takes 2 minutes."
165
+ }
166
+ ]
167
+ }
168
+ ```
169
+
170
+ ---
171
+
172
+ ## Caching
173
+
174
+ Results are cached for 5 minutes under `~/.cache/github-rep/` to avoid hitting rate limits when
175
+ running the tool multiple times. Delete the cache directory to force a fresh fetch.
176
+
177
+ ---
178
+
179
+ ## FAQ
180
+
181
+ **Do I need a GitHub token?**
182
+ No. Unauthenticated usage gets 60 API requests per hour — enough for a single profile analysis.
183
+ Set `GITHUB_TOKEN` to get 5000 requests per hour and avoid hitting limits when comparing many profiles.
184
+
185
+ **How is the score calculated?**
186
+ Each dimension is scored independently against fixed max points (totalling 100). There is no
187
+ machine learning or relative ranking — the score reflects the absolute presence or absence of
188
+ each signal.
189
+
190
+ **Can I game the score?**
191
+ You could game every individual metric, but you would also genuinely improve your GitHub presence
192
+ in the process. The signals are chosen because they correlate with real reputation value.
193
+
194
+ **Why does a well-known developer score lower than expected?**
195
+ Some high-reputation developers (including Linus Torvalds) score below 80 because they skip
196
+ signals like topic tags, profile READMEs, or GitHub Releases. The tool measures profile
197
+ hygiene signals, not absolute influence.
198
+
199
+ **How often should I re-run this?**
200
+ After each batch of improvements. Treat it like `npm audit` — run it, fix the findings, move on.
201
+
202
+ ---
203
+
204
+ ## Contributing
205
+
206
+ See [CONTRIBUTING.md](CONTRIBUTING.md). Bug reports and improvements welcome — open an issue on
207
+ [github.com/basilalshukaili/github-rep](https://github.com/basilalshukaili/github-rep/issues).
208
+
209
+ ---
210
+
211
+ ## License
212
+
213
+ MIT — see [LICENSE](LICENSE).
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "github-rep"
7
- version = "0.1.1"
7
+ version = "0.2.0"
8
8
  description = "Analyze a GitHub profile and get honest, actionable advice for building real reputation"
9
9
  readme = "README.md"
10
10
  license = { text = "MIT" }
@@ -0,0 +1 @@
1
+ __version__ = "0.2.0"
@@ -1,4 +1,4 @@
1
- """Profile analyzer: scores genuine reputation signals across 9 dimensions."""
1
+ """Profile analyzer: scores genuine reputation signals across 11 dimensions."""
2
2
 
3
3
  from __future__ import annotations
4
4
 
@@ -67,7 +67,7 @@ class ProfileScore:
67
67
  return "Just starting"
68
68
 
69
69
 
70
- # ── Helpers ───────────────────────────────────────────────────────────────────
70
+ # -- Helpers -------------------------------------------------------------------
71
71
 
72
72
  def _days_since(dt_str: Optional[str]) -> Optional[int]:
73
73
  if not dt_str:
@@ -113,7 +113,6 @@ def _readme_score(readme_text: Optional[str]) -> Tuple[int, List[Finding]]:
113
113
  else:
114
114
  score += 12
115
115
 
116
- # Code examples
117
116
  if "```" in readme_text or "`" in readme_text:
118
117
  score += 2
119
118
  else:
@@ -124,7 +123,6 @@ def _readme_score(readme_text: Optional[str]) -> Tuple[int, List[Finding]]:
124
123
  "Add a quick-start code block showing the most common use case.",
125
124
  ))
126
125
 
127
- # Installation instructions
128
126
  has_install = any(
129
127
  kw in readme_text.lower()
130
128
  for kw in ["install", "pip install", "npm install", "brew install", "cargo add"]
@@ -142,14 +140,14 @@ def _readme_score(readme_text: Optional[str]) -> Tuple[int, List[Finding]]:
142
140
  return min(score, 15), findings
143
141
 
144
142
 
145
- # ── Main analyzer ─────────────────────────────────────────────────────────────
143
+ # -- Main analyzer -------------------------------------------------------------
146
144
 
147
145
  def analyze(
148
146
  username: str,
149
147
  token: Optional[str] = None,
150
148
  top_n: int = 10,
151
149
  ) -> ProfileScore:
152
- """Fetch GitHub data and compute a ProfileScore across 9 dimensions.
150
+ """Fetch GitHub data and compute a ProfileScore across 11 dimensions.
153
151
 
154
152
  Args:
155
153
  username: GitHub username to analyze.
@@ -168,7 +166,7 @@ def analyze(
168
166
  breakdown: Dict[str, int] = {}
169
167
  findings: List[Finding] = []
170
168
 
171
- # ── 1. Profile completeness (10 pts) ──────────────────────────────────────
169
+ # 1. Profile completeness (10 pts) ----------------------------------------
172
170
  pc = 0
173
171
  if user.get("bio"):
174
172
  pc += 3
@@ -213,7 +211,7 @@ def analyze(
213
211
  ))
214
212
  breakdown["profile_completeness"] = min(pc, 10)
215
213
 
216
- # ── 2. README quality (15 pts) ────────────────────────────────────────────
214
+ # 2. README quality (15 pts) -----------------------------------------------
217
215
  readme_text: Optional[str] = None
218
216
  if top_repos:
219
217
  best = top_repos[0]
@@ -226,7 +224,7 @@ def analyze(
226
224
  breakdown["readme_quality"] = readme_pts
227
225
  findings.extend(readme_findings)
228
226
 
229
- # ── 3. Star signal (20 pts) ───────────────────────────────────────────────
227
+ # 3. Star signal (20 pts) --------------------------------------------------
230
228
  total_stars = sum(r.get("stargazers_count", 0) for r in repos)
231
229
  max_stars = max((r.get("stargazers_count", 0) for r in repos), default=0)
232
230
  if total_stars == 0:
@@ -244,8 +242,7 @@ def analyze(
244
242
  "star_signal", "medium",
245
243
  f"Low star count ({total_stars} total)",
246
244
  "Genuine stars come from genuine visibility.",
247
- "Share in community WHEN you have something useful to say "
248
- "(build log, lesson learned, solved problem).",
245
+ "Share in community when you have something useful to say.",
249
246
  ))
250
247
  elif total_stars < 25:
251
248
  star_pts = 10
@@ -262,7 +259,7 @@ def analyze(
262
259
  ))
263
260
  breakdown["star_signal"] = star_pts
264
261
 
265
- # ── 4. Contribution activity (15 pts) ─────────────────────────────────────
262
+ # 4. Contribution activity (15 pts) ----------------------------------------
266
263
  days = _days_since(user.get("updated_at"))
267
264
  if days is None or days > 180:
268
265
  streak_pts = 0
@@ -291,7 +288,7 @@ def analyze(
291
288
  ))
292
289
  breakdown["contribution_streak"] = streak_pts
293
290
 
294
- # ── 5. Repo diversity (10 pts) ────────────────────────────────────────────
291
+ # 5. Repo diversity (10 pts) -----------------------------------------------
295
292
  n_repos = len(repos)
296
293
  if n_repos == 0:
297
294
  div_pts = 0
@@ -322,7 +319,7 @@ def analyze(
322
319
  ))
323
320
  breakdown["repo_diversity"] = div_pts
324
321
 
325
- # ── 6. Description quality (10 pts) ───────────────────────────────────────
322
+ # 6. Description quality (10 pts) ------------------------------------------
326
323
  repos_missing_desc = [r for r in repos if not r.get("description")]
327
324
  if repos_missing_desc:
328
325
  pct = len(repos_missing_desc) / max(len(repos), 1)
@@ -343,7 +340,7 @@ def analyze(
343
340
  ))
344
341
  breakdown["description_quality"] = desc_pts
345
342
 
346
- # ── 7. Topic tags (5 pts) ─────────────────────────────────────────────────
343
+ # 7. Topic tags (5 pts) ----------------------------------------------------
347
344
  repos_without_topics = [r for r in repos if not r.get("topics")]
348
345
  if not repos_without_topics:
349
346
  topic_pts = 5
@@ -370,7 +367,7 @@ def analyze(
370
367
  ))
371
368
  breakdown["topic_tags"] = topic_pts
372
369
 
373
- # ── 8. Fork ratio (5 pts) ─────────────────────────────────────────────────
370
+ # 8. Fork ratio (5 pts) ----------------------------------------------------
374
371
  all_repos_with_forks = client.get_repos(username, include_forks=True)
375
372
  fork_count = sum(1 for r in all_repos_with_forks if r.get("fork"))
376
373
  total_count = len(all_repos_with_forks)
@@ -389,7 +386,7 @@ def analyze(
389
386
  fork_pts = 5
390
387
  breakdown["fork_ratio"] = fork_pts
391
388
 
392
- # ── 9. Recent activity quality (10 pts) ───────────────────────────────────
389
+ # 9. Recent activity quality (10 pts) --------------------------------------
393
390
  recently_active = [
394
391
  r for r in repos
395
392
  if _days_since(r.get("pushed_at")) is not None
@@ -420,7 +417,82 @@ def analyze(
420
417
  ))
421
418
  breakdown["recent_activity"] = ra_pts
422
419
 
423
- # ── Aggregate ─────────────────────────────────────────────────────────────
420
+ # 10. Release cadence (5 pts) -- NEW ---------------------------------------
421
+ # Published releases signal versioned, production-ready software that
422
+ # users can subscribe to and depend on.
423
+ release_pts = 0
424
+ total_releases = 0
425
+ for repo in top_repos[:5]:
426
+ try:
427
+ releases = client.get(f"/repos/{username}/{repo['name']}/releases",
428
+ params={"per_page": 5})
429
+ total_releases += len(releases)
430
+ except Exception:
431
+ pass
432
+ if total_releases == 0:
433
+ release_pts = 0
434
+ if top_repos:
435
+ findings.append(Finding(
436
+ "release_cadence", "low",
437
+ "No published releases",
438
+ "GitHub Releases make your project feel production-ready and "
439
+ "let users subscribe to new versions.",
440
+ "Tag your first release: git tag v0.1.0 && git push --tags, "
441
+ "then create a GitHub release with changelog notes.",
442
+ ))
443
+ elif total_releases < 3:
444
+ release_pts = 3
445
+ findings.append(Finding(
446
+ "release_cadence", "low",
447
+ f"Only {total_releases} published release(s)",
448
+ "A release cadence signals active maintenance.",
449
+ "Aim for a release whenever you ship a meaningful change.",
450
+ ))
451
+ else:
452
+ release_pts = 5
453
+ findings.append(Finding(
454
+ "release_cadence", "good",
455
+ f"{total_releases} published releases across top repos",
456
+ "Regular releases signal a maintained, production-quality project.",
457
+ ))
458
+ breakdown["release_cadence"] = release_pts
459
+
460
+ # 11. Profile README signal (5 pts) -- NEW ---------------------------------
461
+ # A profile README (special repo username/username) is displayed at the
462
+ # top of the GitHub profile page - the highest-visibility real estate.
463
+ profile_readme_pts = 0
464
+ try:
465
+ raw = client.get(f"/repos/{username}/{username}/readme")
466
+ profile_text = base64.b64decode(raw["content"]).decode("utf-8", errors="replace")
467
+ word_count = len(profile_text.split())
468
+ if word_count >= 100:
469
+ profile_readme_pts = 5
470
+ findings.append(Finding(
471
+ "profile_readme", "good",
472
+ f"Profile README exists and is substantial ({word_count} words)",
473
+ "A profile README is the highest-visibility real estate on GitHub.",
474
+ ))
475
+ else:
476
+ profile_readme_pts = 2
477
+ findings.append(Finding(
478
+ "profile_readme", "low",
479
+ f"Profile README is short ({word_count} words)",
480
+ "You have a profile README but it could work harder for you.",
481
+ "Add your current focus, top project links, and how to contact you.",
482
+ ))
483
+ except Exception:
484
+ profile_readme_pts = 0
485
+ findings.append(Finding(
486
+ "profile_readme", "medium",
487
+ "No profile README",
488
+ f"A profile README (create repo: {username}/{username}) is the first "
489
+ "thing visitors see. Prime real estate for your personal brand.",
490
+ f"Create a repo named exactly '{username}' with a README.md showing "
491
+ "your focus, skills, and top projects.",
492
+ ))
493
+ breakdown["profile_readme"] = profile_readme_pts
494
+
495
+ # Aggregate ----------------------------------------------------------------
424
496
  total = sum(breakdown.values())
425
497
  severity_order = {"critical": 0, "high": 1, "medium": 2, "low": 3, "good": 4}
426
498
  findings.sort(key=lambda f: severity_order.get(f.severity, 5))