github-rep 0.1.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,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Basil Al Shukaili
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,98 @@
1
+ Metadata-Version: 2.4
2
+ Name: github-rep
3
+ Version: 0.1.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-reputation-engine
8
+ Project-URL: Bug Tracker, https://github.com/basilalshukaili/github-reputation-engine/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 Reputation Engine πŸ› οΈ
34
+
35
+ A self-improving "second brain" + operating system for building **genuine GitHub reputation**
36
+ through high-quality open-source contributions β€” run by an AI agent (Hermes Agent or Claude Code),
37
+ autonomously, one project at a time.
38
+
39
+ **Owner:** Basil Al Shukaili ([@basilalshukaili](https://github.com/basilalshukaili))
40
+
41
+ > Quality of contribution > volume. One merged, appreciated PR beats fifty rejected typo fixes.
42
+
43
+ ## What this repo is
44
+
45
+ This repo is the **portable brain** of the operation. It lets any agent, on any laptop, continue
46
+ the same mission with full context. It contains the plan, live state, reusable playbooks, a
47
+ journal of work done, lessons learned, and nightly strategic "dreams."
48
+
49
+ Start here β†’ [`CLAUDE.md`](./CLAUDE.md) (operating instructions, auto-loaded by Claude Code/Hermes).
50
+
51
+ ## Structure
52
+
53
+ ```
54
+ .
55
+ β”œβ”€β”€ CLAUDE.md # Operating instructions β€” read first (works with Claude Code, Hermes, Codex…)
56
+ β”œβ”€β”€ 00-System/ # roadmap, architecture, guardrails (the constitution)
57
+ β”œβ”€β”€ 01-Targets/ # the ONE active project + next action
58
+ β”œβ”€β”€ 02-Repos/ # per-repo dossiers (stack, conventions, maintainers, opportunities)
59
+ β”œβ”€β”€ 03-Journal/ # daily log of what was attempted/done (append-only per day)
60
+ β”œβ”€β”€ 04-Playbooks/ # portable, agent-agnostic procedures (triage, PR craft, dreaming)
61
+ β”œβ”€β”€ 05-Lessons/ # mistakes & wins β†’ rules for next time
62
+ β”œβ”€β”€ 06-Dreams/ # nightly reflective synthesis: patterns, connections, next moves
63
+ └── 99-Inbox/ # scratch captures
64
+ ```
65
+
66
+ ## How it works
67
+
68
+ ```
69
+ Brain (top model) β€” plans, reviews every diff, writes all maintainer-facing prose, commits
70
+ β”‚ delegates mechanical, fully-specified work
71
+ Worker (mid model) β€” implements to spec, runs tests, triage scans
72
+ β”‚
73
+ This repo (state) β€” feeds full context back to the Brain on every session
74
+ β”‚
75
+ Playbooks (procedure) + Journal/Lessons/Dreams (self-improvement loop)
76
+ ```
77
+
78
+ **Token discipline without quality loss:** route cheap/mechanical work to cheaper models; keep
79
+ all judgment and human-facing writing on the top model. No output-degrading "compression" tricks.
80
+
81
+ ## Multi-machine usage
82
+
83
+ - **Laptop A (Hermes Agent):** runs the 24/7 cadence (cron missions + nightly dreaming),
84
+ reports to Telegram. Skills live in Hermes; this repo holds the portable state + playbooks.
85
+ - **Laptop B (Claude Code):** clone this repo, open it, and Claude Code auto-reads `CLAUDE.md`.
86
+ The playbooks in `04-Playbooks/` are agent-agnostic, so the workflow is identical.
87
+
88
+ Pull before you start, commit + push when you finish, so both machines stay in sync.
89
+
90
+ ## Safety
91
+
92
+ - No secrets are committed (API keys / bot tokens live in each machine's local env).
93
+ - Agents fork β†’ branch β†’ PR; never push to an upstream default branch.
94
+ - Respects each project's CONTRIBUTING rules and any anti-AI-PR policies.
95
+
96
+ ## License
97
+
98
+ MIT β€” see [`LICENSE`](./LICENSE).
@@ -0,0 +1,66 @@
1
+ # GitHub Reputation Engine πŸ› οΈ
2
+
3
+ A self-improving "second brain" + operating system for building **genuine GitHub reputation**
4
+ through high-quality open-source contributions β€” run by an AI agent (Hermes Agent or Claude Code),
5
+ autonomously, one project at a time.
6
+
7
+ **Owner:** Basil Al Shukaili ([@basilalshukaili](https://github.com/basilalshukaili))
8
+
9
+ > Quality of contribution > volume. One merged, appreciated PR beats fifty rejected typo fixes.
10
+
11
+ ## What this repo is
12
+
13
+ This repo is the **portable brain** of the operation. It lets any agent, on any laptop, continue
14
+ the same mission with full context. It contains the plan, live state, reusable playbooks, a
15
+ journal of work done, lessons learned, and nightly strategic "dreams."
16
+
17
+ Start here β†’ [`CLAUDE.md`](./CLAUDE.md) (operating instructions, auto-loaded by Claude Code/Hermes).
18
+
19
+ ## Structure
20
+
21
+ ```
22
+ .
23
+ β”œβ”€β”€ CLAUDE.md # Operating instructions β€” read first (works with Claude Code, Hermes, Codex…)
24
+ β”œβ”€β”€ 00-System/ # roadmap, architecture, guardrails (the constitution)
25
+ β”œβ”€β”€ 01-Targets/ # the ONE active project + next action
26
+ β”œβ”€β”€ 02-Repos/ # per-repo dossiers (stack, conventions, maintainers, opportunities)
27
+ β”œβ”€β”€ 03-Journal/ # daily log of what was attempted/done (append-only per day)
28
+ β”œβ”€β”€ 04-Playbooks/ # portable, agent-agnostic procedures (triage, PR craft, dreaming)
29
+ β”œβ”€β”€ 05-Lessons/ # mistakes & wins β†’ rules for next time
30
+ β”œβ”€β”€ 06-Dreams/ # nightly reflective synthesis: patterns, connections, next moves
31
+ └── 99-Inbox/ # scratch captures
32
+ ```
33
+
34
+ ## How it works
35
+
36
+ ```
37
+ Brain (top model) β€” plans, reviews every diff, writes all maintainer-facing prose, commits
38
+ β”‚ delegates mechanical, fully-specified work
39
+ Worker (mid model) β€” implements to spec, runs tests, triage scans
40
+ β”‚
41
+ This repo (state) β€” feeds full context back to the Brain on every session
42
+ β”‚
43
+ Playbooks (procedure) + Journal/Lessons/Dreams (self-improvement loop)
44
+ ```
45
+
46
+ **Token discipline without quality loss:** route cheap/mechanical work to cheaper models; keep
47
+ all judgment and human-facing writing on the top model. No output-degrading "compression" tricks.
48
+
49
+ ## Multi-machine usage
50
+
51
+ - **Laptop A (Hermes Agent):** runs the 24/7 cadence (cron missions + nightly dreaming),
52
+ reports to Telegram. Skills live in Hermes; this repo holds the portable state + playbooks.
53
+ - **Laptop B (Claude Code):** clone this repo, open it, and Claude Code auto-reads `CLAUDE.md`.
54
+ The playbooks in `04-Playbooks/` are agent-agnostic, so the workflow is identical.
55
+
56
+ Pull before you start, commit + push when you finish, so both machines stay in sync.
57
+
58
+ ## Safety
59
+
60
+ - No secrets are committed (API keys / bot tokens live in each machine's local env).
61
+ - Agents fork β†’ branch β†’ PR; never push to an upstream default branch.
62
+ - Respects each project's CONTRIBUTING rules and any anti-AI-PR policies.
63
+
64
+ ## License
65
+
66
+ MIT β€” see [`LICENSE`](./LICENSE).
@@ -0,0 +1,47 @@
1
+ [build-system]
2
+ requires = ["setuptools>=68", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "github-rep"
7
+ version = "0.1.0"
8
+ description = "Analyze a GitHub profile and get honest, actionable advice for building real reputation"
9
+ readme = "README.md"
10
+ license = { text = "MIT" }
11
+ requires-python = ">=3.9"
12
+ authors = [{ name = "Basil Alshukaili", email = "basilalshukaili@gmail.com" }]
13
+ keywords = ["github", "open-source", "developer-tools", "profile", "reputation", "cli"]
14
+ classifiers = [
15
+ "Development Status :: 3 - Alpha",
16
+ "Environment :: Console",
17
+ "Intended Audience :: Developers",
18
+ "License :: OSI Approved :: MIT License",
19
+ "Programming Language :: Python :: 3",
20
+ "Programming Language :: Python :: 3.9",
21
+ "Programming Language :: Python :: 3.10",
22
+ "Programming Language :: Python :: 3.11",
23
+ "Programming Language :: Python :: 3.12",
24
+ "Topic :: Software Development :: Version Control :: Git",
25
+ ]
26
+ dependencies = [
27
+ "requests>=2.31",
28
+ "rich>=13.0",
29
+ "typer>=0.9",
30
+ "python-dateutil>=2.8",
31
+ ]
32
+
33
+ [project.optional-dependencies]
34
+ dev = ["pytest>=7", "responses>=0.25", "pytest-cov>=4"]
35
+
36
+ [project.scripts]
37
+ github-rep = "github_rep.cli:app"
38
+
39
+ [project.urls]
40
+ Homepage = "https://github.com/basilalshukaili/github-reputation-engine"
41
+ "Bug Tracker" = "https://github.com/basilalshukaili/github-reputation-engine/issues"
42
+
43
+ [tool.setuptools.packages.find]
44
+ where = ["src"]
45
+
46
+ [tool.pytest.ini_options]
47
+ testpaths = ["tests"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,3 @@
1
+ """github-rep: honest GitHub profile analysis and reputation advice."""
2
+
3
+ __version__ = "0.1.0"
@@ -0,0 +1,435 @@
1
+ """Profile analyzer: scores genuine reputation signals across 9 dimensions."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import base64
6
+ from dataclasses import dataclass, field
7
+ from datetime import datetime, timezone
8
+ from typing import Dict, List, Optional, Tuple
9
+
10
+ from .api import GitHubClient
11
+
12
+
13
+ @dataclass
14
+ class Finding:
15
+ """A single scored observation about a GitHub profile."""
16
+
17
+ category: str
18
+ severity: str # "critical" | "high" | "medium" | "low" | "good"
19
+ title: str
20
+ detail: str
21
+ fix: Optional[str] = None
22
+
23
+ @property
24
+ def icon(self) -> str:
25
+ return {
26
+ "critical": "[CRITICAL]",
27
+ "high": "[HIGH]",
28
+ "medium": "[MEDIUM]",
29
+ "low": "[LOW]",
30
+ "good": "[GOOD]",
31
+ }.get(self.severity, "[?]")
32
+
33
+
34
+ @dataclass
35
+ class ProfileScore:
36
+ """Complete reputation analysis for one GitHub user."""
37
+
38
+ username: str
39
+ total: int # 0-100
40
+ breakdown: Dict[str, int] = field(default_factory=dict)
41
+ findings: List[Finding] = field(default_factory=list)
42
+ raw_user: Dict = field(default_factory=dict)
43
+ raw_repos: List[Dict] = field(default_factory=list)
44
+
45
+ @property
46
+ def grade(self) -> str:
47
+ if self.total >= 80:
48
+ return "A"
49
+ if self.total >= 65:
50
+ return "B"
51
+ if self.total >= 50:
52
+ return "C"
53
+ if self.total >= 35:
54
+ return "D"
55
+ return "F"
56
+
57
+ @property
58
+ def tier(self) -> str:
59
+ if self.total >= 80:
60
+ return "Established OSS contributor"
61
+ if self.total >= 65:
62
+ return "Active developer"
63
+ if self.total >= 50:
64
+ return "Growing presence"
65
+ if self.total >= 35:
66
+ return "Early stage"
67
+ return "Just starting"
68
+
69
+
70
+ # ── Helpers ───────────────────────────────────────────────────────────────────
71
+
72
+ def _days_since(dt_str: Optional[str]) -> Optional[int]:
73
+ if not dt_str:
74
+ return None
75
+ try:
76
+ dt = datetime.fromisoformat(dt_str.replace("Z", "+00:00"))
77
+ return (datetime.now(timezone.utc) - dt).days
78
+ except Exception:
79
+ return None
80
+
81
+
82
+ def _readme_score(readme_text: Optional[str]) -> Tuple[int, List[Finding]]:
83
+ """Score README quality 0-15 and return relevant findings."""
84
+ findings: List[Finding] = []
85
+
86
+ if not readme_text:
87
+ return 0, [Finding(
88
+ "readme_quality", "critical",
89
+ "No README",
90
+ "Your repo has no README. This is the #1 reason developers skip a project.",
91
+ "Add README.md: what it does, why it matters, how to install, quick example.",
92
+ )]
93
+
94
+ score = 0
95
+ words = len(readme_text.split())
96
+
97
+ if words < 50:
98
+ findings.append(Finding(
99
+ "readme_quality", "high",
100
+ "README too short",
101
+ f"Only {words} words. A useful README is 150-500 words.",
102
+ "Add: what it does, installation, usage example, screenshot.",
103
+ ))
104
+ score += 3
105
+ elif words < 150:
106
+ score += 7
107
+ findings.append(Finding(
108
+ "readme_quality", "medium",
109
+ "README could be richer",
110
+ f"{words} words - decent, but consider adding usage examples.",
111
+ "Add a code example or screenshot to make it concrete.",
112
+ ))
113
+ else:
114
+ score += 12
115
+
116
+ # Code examples
117
+ if "```" in readme_text or "`" in readme_text:
118
+ score += 2
119
+ else:
120
+ findings.append(Finding(
121
+ "readme_quality", "medium",
122
+ "No code examples",
123
+ "README has no inline code. Show, do not just tell.",
124
+ "Add a quick-start code block showing the most common use case.",
125
+ ))
126
+
127
+ # Installation instructions
128
+ has_install = any(
129
+ kw in readme_text.lower()
130
+ for kw in ["install", "pip install", "npm install", "brew install", "cargo add"]
131
+ )
132
+ if has_install:
133
+ score += 1
134
+ else:
135
+ findings.append(Finding(
136
+ "readme_quality", "low",
137
+ "No installation instructions",
138
+ "How should someone install this?",
139
+ 'Add an "## Installation" section even if it is just `pip install <name>`.',
140
+ ))
141
+
142
+ return min(score, 15), findings
143
+
144
+
145
+ # ── Main analyzer ─────────────────────────────────────────────────────────────
146
+
147
+ def analyze(
148
+ username: str,
149
+ token: Optional[str] = None,
150
+ top_n: int = 10,
151
+ ) -> ProfileScore:
152
+ """Fetch GitHub data and compute a ProfileScore across 9 dimensions.
153
+
154
+ Args:
155
+ username: GitHub username to analyze.
156
+ token: Optional GitHub personal access token (improves rate limits).
157
+ top_n: Number of top repos (by stars) to deep-analyze.
158
+
159
+ Returns:
160
+ ProfileScore with total (0-100), breakdown, and actionable findings.
161
+ """
162
+ client = GitHubClient(token=token)
163
+ user = client.get_user(username)
164
+ repos = client.get_repos(username, include_forks=False)
165
+ repos_sorted = sorted(repos, key=lambda r: r.get("stargazers_count", 0), reverse=True)
166
+ top_repos = repos_sorted[:top_n]
167
+
168
+ breakdown: Dict[str, int] = {}
169
+ findings: List[Finding] = []
170
+
171
+ # ── 1. Profile completeness (10 pts) ──────────────────────────────────────
172
+ pc = 0
173
+ if user.get("bio"):
174
+ pc += 3
175
+ else:
176
+ findings.append(Finding(
177
+ "profile_completeness", "high",
178
+ "Missing bio",
179
+ "Your bio is empty. It is the first thing visitors read.",
180
+ "Write 1-2 sentences: your focus, what you build, your superpower.",
181
+ ))
182
+ if user.get("location"):
183
+ pc += 1
184
+ else:
185
+ findings.append(Finding(
186
+ "profile_completeness", "low",
187
+ "No location set",
188
+ "Location builds trust and helps local dev communities find you.",
189
+ "Add your city/country - takes 5 seconds.",
190
+ ))
191
+ if user.get("blog") or user.get("twitter_username"):
192
+ pc += 2
193
+ else:
194
+ findings.append(Finding(
195
+ "profile_completeness", "medium",
196
+ "No website or social link",
197
+ "No linked website or Twitter/X. Credibility builder.",
198
+ "Add your website, LinkedIn, or Twitter handle.",
199
+ ))
200
+ if user.get("company"):
201
+ pc += 1
202
+ if user.get("email"):
203
+ pc += 1
204
+ avatar = user.get("avatar_url", "")
205
+ if avatar and "gravatar" not in avatar:
206
+ pc += 2
207
+ else:
208
+ findings.append(Finding(
209
+ "profile_completeness", "low",
210
+ "Default/gravatar avatar",
211
+ "A real photo or custom avatar increases perceived legitimacy.",
212
+ "Upload a profile photo - even a simple avatar is better than the default.",
213
+ ))
214
+ breakdown["profile_completeness"] = min(pc, 10)
215
+
216
+ # ── 2. README quality (15 pts) ────────────────────────────────────────────
217
+ readme_text: Optional[str] = None
218
+ if top_repos:
219
+ best = top_repos[0]
220
+ try:
221
+ raw = client.get(f"/repos/{username}/{best['name']}/readme")
222
+ readme_text = base64.b64decode(raw["content"]).decode("utf-8", errors="replace")
223
+ except Exception:
224
+ readme_text = None
225
+ readme_pts, readme_findings = _readme_score(readme_text)
226
+ breakdown["readme_quality"] = readme_pts
227
+ findings.extend(readme_findings)
228
+
229
+ # ── 3. Star signal (20 pts) ───────────────────────────────────────────────
230
+ total_stars = sum(r.get("stargazers_count", 0) for r in repos)
231
+ max_stars = max((r.get("stargazers_count", 0) for r in repos), default=0)
232
+ if total_stars == 0:
233
+ star_pts = 0
234
+ findings.append(Finding(
235
+ "star_signal", "high",
236
+ "Zero stars across all repos",
237
+ "Stars are the #1 public signal of value. Zero usually means: "
238
+ "no promotion, poor README, or no unique value prop.",
239
+ "Fix the README, then share the project genuinely in ONE relevant community.",
240
+ ))
241
+ elif total_stars < 5:
242
+ star_pts = 4
243
+ findings.append(Finding(
244
+ "star_signal", "medium",
245
+ f"Low star count ({total_stars} total)",
246
+ "Genuine stars come from genuine visibility.",
247
+ "Share in community WHEN you have something useful to say "
248
+ "(build log, lesson learned, solved problem).",
249
+ ))
250
+ elif total_stars < 25:
251
+ star_pts = 10
252
+ elif total_stars < 100:
253
+ star_pts = 15
254
+ elif total_stars < 500:
255
+ star_pts = 18
256
+ else:
257
+ star_pts = 20
258
+ findings.append(Finding(
259
+ "star_signal", "good",
260
+ f"Strong star signal ({total_stars} total, top repo: {max_stars})",
261
+ "Real community traction. Keep shipping.",
262
+ ))
263
+ breakdown["star_signal"] = star_pts
264
+
265
+ # ── 4. Contribution activity (15 pts) ─────────────────────────────────────
266
+ days = _days_since(user.get("updated_at"))
267
+ if days is None or days > 180:
268
+ streak_pts = 0
269
+ findings.append(Finding(
270
+ "contribution_streak", "high",
271
+ "No recent activity (6+ months)",
272
+ "Stale profiles are invisible in GitHub search and Explore.",
273
+ "Even small commits (docs, tests, fixes) signal an active developer.",
274
+ ))
275
+ elif days > 60:
276
+ streak_pts = 5
277
+ findings.append(Finding(
278
+ "contribution_streak", "medium",
279
+ f"Low recent activity ({days} days since last update)",
280
+ "Aim for a few commits per month to stay visible.",
281
+ "Pick one project and make a small meaningful improvement weekly.",
282
+ ))
283
+ elif days > 14:
284
+ streak_pts = 10
285
+ else:
286
+ streak_pts = 15
287
+ findings.append(Finding(
288
+ "contribution_streak", "good",
289
+ f"Active recent commits ({days}d ago)",
290
+ "Consistent shipping builds reputation over time.",
291
+ ))
292
+ breakdown["contribution_streak"] = streak_pts
293
+
294
+ # ── 5. Repo diversity (10 pts) ────────────────────────────────────────────
295
+ n_repos = len(repos)
296
+ if n_repos == 0:
297
+ div_pts = 0
298
+ findings.append(Finding(
299
+ "repo_diversity", "critical",
300
+ "No public repositories",
301
+ "Nothing to discover, nothing to star, nothing to learn from.",
302
+ "Publish at least one genuine project - even a small tool that solves a real problem.",
303
+ ))
304
+ elif n_repos < 3:
305
+ div_pts = 3
306
+ findings.append(Finding(
307
+ "repo_diversity", "medium",
308
+ f"Only {n_repos} public repos",
309
+ "A single repo limits discovery surface. Build in public.",
310
+ "Start a second project - even a CLI tool, a library, or documented configs.",
311
+ ))
312
+ elif n_repos < 10:
313
+ div_pts = 7
314
+ else:
315
+ div_pts = 10
316
+ languages = {r["language"] for r in repos if r.get("language")}
317
+ if len(languages) >= 3:
318
+ findings.append(Finding(
319
+ "repo_diversity", "good",
320
+ f"{n_repos} repos across {len(languages)} languages",
321
+ "Healthy breadth. Visitors can see range and depth.",
322
+ ))
323
+ breakdown["repo_diversity"] = div_pts
324
+
325
+ # ── 6. Description quality (10 pts) ───────────────────────────────────────
326
+ repos_missing_desc = [r for r in repos if not r.get("description")]
327
+ if repos_missing_desc:
328
+ pct = len(repos_missing_desc) / max(len(repos), 1)
329
+ desc_pts = max(0, int(10 * (1 - pct)))
330
+ severity = "high" if pct > 0.5 else "medium"
331
+ findings.append(Finding(
332
+ "description_quality", severity,
333
+ f"{len(repos_missing_desc)}/{len(repos)} repos missing descriptions",
334
+ "GitHub search indexes repo descriptions as keyword signal.",
335
+ "Add a 1-sentence description to every public repo - takes 30 seconds each.",
336
+ ))
337
+ else:
338
+ desc_pts = 10
339
+ findings.append(Finding(
340
+ "description_quality", "good",
341
+ "All repos have descriptions",
342
+ "Every repo is searchable and discoverable by description keyword.",
343
+ ))
344
+ breakdown["description_quality"] = desc_pts
345
+
346
+ # ── 7. Topic tags (5 pts) ─────────────────────────────────────────────────
347
+ repos_without_topics = [r for r in repos if not r.get("topics")]
348
+ if not repos_without_topics:
349
+ topic_pts = 5
350
+ findings.append(Finding(
351
+ "topic_tags", "good",
352
+ "All repos have topic tags",
353
+ "Topic tags make repos discoverable via GitHub Explore.",
354
+ ))
355
+ elif len(repos_without_topics) < len(repos) / 2:
356
+ topic_pts = 3
357
+ findings.append(Finding(
358
+ "topic_tags", "low",
359
+ f"{len(repos_without_topics)} repos missing topic tags",
360
+ "GitHub Explore uses topics to surface repos in category feeds.",
361
+ "Add 3-5 relevant topics per repo (e.g. python, cli, api, automation).",
362
+ ))
363
+ else:
364
+ topic_pts = 0
365
+ findings.append(Finding(
366
+ "topic_tags", "medium",
367
+ "Most repos have no topic tags",
368
+ "Missing from GitHub Explore category pages entirely.",
369
+ "Add topics to your top 3 repos today - GitHub UI, takes 2 minutes.",
370
+ ))
371
+ breakdown["topic_tags"] = topic_pts
372
+
373
+ # ── 8. Fork ratio (5 pts) ─────────────────────────────────────────────────
374
+ all_repos_with_forks = client.get_repos(username, include_forks=True)
375
+ fork_count = sum(1 for r in all_repos_with_forks if r.get("fork"))
376
+ total_count = len(all_repos_with_forks)
377
+ fork_ratio = fork_count / total_count if total_count > 0 else 0
378
+ if fork_ratio > 0.7:
379
+ fork_pts = 1
380
+ findings.append(Finding(
381
+ "fork_ratio", "medium",
382
+ f"High fork ratio ({fork_ratio:.0%} forks)",
383
+ "Forked repos dominate your profile. Visitors see mostly borrowed code.",
384
+ "Consider making forked experiments private, or shipping more originals.",
385
+ ))
386
+ elif fork_ratio > 0.4:
387
+ fork_pts = 3
388
+ else:
389
+ fork_pts = 5
390
+ breakdown["fork_ratio"] = fork_pts
391
+
392
+ # ── 9. Recent activity quality (10 pts) ───────────────────────────────────
393
+ recently_active = [
394
+ r for r in repos
395
+ if _days_since(r.get("pushed_at")) is not None
396
+ and (_days_since(r.get("pushed_at")) or 999) < 90
397
+ ]
398
+ if not recently_active:
399
+ ra_pts = 0
400
+ findings.append(Finding(
401
+ "recent_activity", "medium",
402
+ "No repos active in last 90 days",
403
+ "Consistent activity matters for GitHub algorithm visibility.",
404
+ "Even docs or test additions count as activity.",
405
+ ))
406
+ elif len(recently_active) < 2:
407
+ ra_pts = 5
408
+ findings.append(Finding(
409
+ "recent_activity", "low",
410
+ "Only 1 repo active in last 90 days",
411
+ "Single-repo focus is fine but spread to 2-3 for visibility.",
412
+ "Even docs or test additions count.",
413
+ ))
414
+ else:
415
+ ra_pts = 10
416
+ findings.append(Finding(
417
+ "recent_activity", "good",
418
+ f"{len(recently_active)} repos active in last 90 days",
419
+ "Consistent multi-repo activity signals a serious builder.",
420
+ ))
421
+ breakdown["recent_activity"] = ra_pts
422
+
423
+ # ── Aggregate ─────────────────────────────────────────────────────────────
424
+ total = sum(breakdown.values())
425
+ severity_order = {"critical": 0, "high": 1, "medium": 2, "low": 3, "good": 4}
426
+ findings.sort(key=lambda f: severity_order.get(f.severity, 5))
427
+
428
+ return ProfileScore(
429
+ username=username,
430
+ total=total,
431
+ breakdown=breakdown,
432
+ findings=findings,
433
+ raw_user=user,
434
+ raw_repos=repos,
435
+ )