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.
- github_rep-0.1.0/LICENSE +21 -0
- github_rep-0.1.0/PKG-INFO +98 -0
- github_rep-0.1.0/README.md +66 -0
- github_rep-0.1.0/pyproject.toml +47 -0
- github_rep-0.1.0/setup.cfg +4 -0
- github_rep-0.1.0/src/github_rep/__init__.py +3 -0
- github_rep-0.1.0/src/github_rep/analyzer.py +435 -0
- github_rep-0.1.0/src/github_rep/api.py +100 -0
- github_rep-0.1.0/src/github_rep/cli.py +260 -0
- github_rep-0.1.0/src/github_rep.egg-info/PKG-INFO +98 -0
- github_rep-0.1.0/src/github_rep.egg-info/SOURCES.txt +14 -0
- github_rep-0.1.0/src/github_rep.egg-info/dependency_links.txt +1 -0
- github_rep-0.1.0/src/github_rep.egg-info/entry_points.txt +2 -0
- github_rep-0.1.0/src/github_rep.egg-info/requires.txt +9 -0
- github_rep-0.1.0/src/github_rep.egg-info/top_level.txt +1 -0
- github_rep-0.1.0/tests/test_analyzer.py +221 -0
github_rep-0.1.0/LICENSE
ADDED
|
@@ -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,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
|
+
)
|