Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .github/workflows/good-egg.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ jobs:
score:
runs-on: ubuntu-latest
steps:
- uses: 2ndSetAI/good-egg@v0
- uses: 2ndSetAI/good-egg@better-egg
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
scoring-model: v2
22 changes: 22 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,27 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [1.0.0] - 2026-02-23

### Added

- **Better Egg (v2) scoring model** -- opt-in combined model that extends the
graph score with merge rate and account age features via logistic regression.
Trained on 5,129 PRs (of 5,417 total, filtered to those with merge rate
data) from 49 repositories.
- `scoring_model` config option (`v1` default, `v2` for Better Egg).
- `v2:` config block with `graph:`, `features:`, and `combined_model:`
sub-sections.
- `--scoring-model` CLI option.
- `scoring-model` GitHub Action input and output.
- `scoring_model` MCP server parameter on scoring tools.
- `GOOD_EGG_SCORING_MODEL` environment variable override.
- Component score breakdown in v2 output (`graph_score`, `merge_rate`,
`log_account_age`).
- `scoring_model` and `component_scores` fields on `TrustScore` model.
- "Better Egg" branding on PR comments when using v2.
- Example workflow for v2: `examples/better-egg-workflow.yml`.

## [0.1.0] - 2026-02-10

### Added
Expand All @@ -22,4 +43,5 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- YAML configuration with environment variable overrides.
- Multiple output formatters: Markdown, CLI table, JSON, and GitHub check-run.

[1.0.0]: https://github.com/2ndSetAI/good-egg/releases/tag/v1.0.0
[0.1.0]: https://github.com/2ndSetAI/good-egg/releases/tag/v0.1.0
14 changes: 14 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,20 @@ Add to Claude Desktop (`claude_desktop_config.json`):

See [docs/mcp-server.md](https://github.com/2ndSetAI/good-egg/blob/main/docs/mcp-server.md) for tool reference.

## Scoring Models

Good Egg supports two scoring models:

| Model | Name | Description |
|-------|------|-------------|
| `v1` | Good Egg (default) | Graph-based scoring from contribution history |
| `v2` | Better Egg | Graph score + merge rate + account age via logistic regression |

To use v2, set `scoring_model: v2` in your `.good-egg.yml`, pass
`--scoring-model v2` on the CLI, or set `scoring-model: v2` in the action
input. See [Methodology](https://github.com/2ndSetAI/good-egg/blob/main/docs/methodology.md#better-egg-v2) for how the
v2 model works.

## How It Works

Good Egg builds a weighted contribution graph from a user's merged PRs and
Expand Down
7 changes: 7 additions & 0 deletions action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,9 @@ inputs:
description: 'Fail the action if trust level is LOW'
required: false
default: 'false'
scoring-model:
description: 'Scoring model to use (v1 or v2)'
required: false

outputs:
score:
Expand All @@ -33,6 +36,9 @@ outputs:
user:
description: 'GitHub username that was scored'
value: ${{ steps.score.outputs.user }}
scoring-model:
description: 'Scoring model that was used (v1 or v2)'
value: ${{ steps.score.outputs.scoring-model }}

runs:
using: 'composite'
Expand Down Expand Up @@ -62,6 +68,7 @@ runs:
INPUT_COMMENT: ${{ inputs.comment }}
INPUT_CHECK-RUN: ${{ inputs.check-run }}
INPUT_FAIL-ON-LOW: ${{ inputs.fail-on-low }}
INPUT_SCORING_MODEL: ${{ inputs.scoring-model }}
run: |
cd ${{ github.action_path }}
uv run python -m good_egg.action
Expand Down
65 changes: 63 additions & 2 deletions docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,26 @@ Configuration values are resolved in this order (highest priority first):
3. **YAML config file**
4. **Built-in defaults**

## Scoring Model

Good Egg supports two scoring models. Set the model at the top level of the
config file:

```yaml
scoring_model: v1 # default -- graph-based scoring only
scoring_model: v2 # Better Egg -- graph + external features via logistic regression
```

When using v2, PR comments are branded "Better Egg" instead of "Good Egg".
See [methodology.md](methodology.md#better-egg-v2) for how the v2 model
works.

## Full YAML Schema

```yaml
# Scoring model selection: v1 (default) or v2
scoring_model: v1

# Graph-based scoring algorithm parameters
graph_scoring:
alpha: 0.85 # Damping factor (0-1)
Expand Down Expand Up @@ -82,10 +99,53 @@ language_normalization:
Go: 2.30
Rust: 2.63
# ... see examples/.good-egg.yml for the full list

# v2 (Better Egg) scoring model parameters
# Only used when scoring_model is set to v2.
v2:
graph:
half_life_days: 180 # Recency decay half-life for v2 graph
max_age_days: 730 # Ignore PRs older than this
archived_penalty: 0.5 # Penalty multiplier for archived repos
fork_penalty: 0.3 # Penalty multiplier for forked repos
features:
merge_rate: true # Include merge rate feature
account_age: true # Include account age feature
combined_model:
intercept: -0.8094 # Logistic regression intercept
graph_score_weight: 1.9138 # Weight for graph score
merge_rate_weight: -0.7783 # Weight for merge rate
account_age_weight: 0.1493 # Weight for log(account_age_days + 1)
```

## Config Sections

### scoring_model

Selects the scoring model. Set to `v1` (default) for graph-only scoring or
`v2` for the Better Egg combined model. When set to `v2`, the parameters
under the `v2:` block are used and the graph construction is simplified
(no self-contribution penalty, no language normalization in repo quality, no
diversity/volume adjustment). Language match personalization weighting
(`same_language_weight`) is retained in v2.

### v2 (Better Egg)

Configuration for the Better Egg (v2) scoring model. This section is only
used when `scoring_model` is set to `v2`.

- **`v2.graph`** -- Graph construction parameters for the simplified v2 graph.
Supports `half_life_days`, `max_age_days`, `archived_penalty`, and
`fork_penalty`. Note that v2 shares graph algorithm parameters (`alpha`,
`max_iterations`, `tolerance`) with the top-level `graph_scoring` config.
- **`v2.features`** -- Toggle external features on or off. `merge_rate`
(merged/(merged+closed)) and `account_age` (log-transformed days) are both
enabled by default.
- **`v2.combined_model`** -- Logistic regression coefficients. The final
score is `sigmoid(intercept + graph_score_weight * graph_score +
merge_rate_weight * merge_rate + account_age_weight *
log(account_age_days + 1))`.

### graph_scoring

Controls the graph-based scoring algorithm. The `alpha` parameter is the
Expand Down Expand Up @@ -143,6 +203,7 @@ The following environment variables override individual config values:
| `GOOD_EGG_HIGH_TRUST` | `thresholds.high_trust` | float |
| `GOOD_EGG_MEDIUM_TRUST` | `thresholds.medium_trust` | float |
| `GOOD_EGG_HALF_LIFE_DAYS` | `recency.half_life_days` | int |
| `GOOD_EGG_SCORING_MODEL` | `scoring_model` | str (`v1` or `v2`) |

## Programmatic Configuration

Expand All @@ -167,5 +228,5 @@ config = load_config(".good-egg.yml")

The `GoodEggConfig` class is composed of the following sub-configs:
`GraphScoringConfig`, `EdgeWeightConfig`, `RecencyConfig`,
`ThresholdConfig`, `CacheTTLConfig`, `LanguageNormalization`, and
`FetchConfig`.
`ThresholdConfig`, `CacheTTLConfig`, `LanguageNormalization`,
`FetchConfig`, and (for v2) `V2Config`.
26 changes: 26 additions & 0 deletions docs/github-action.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ This posts a trust score comment on each pull request:
| `comment` | No | `true` | Post a PR comment with the trust score |
| `check-run` | No | `false` | Create a check run with the trust score |
| `fail-on-low` | No | `false` | Fail the action if trust level is LOW |
| `scoring-model` | No | `v1` | Scoring model: `v1` (Good Egg) or `v2` (Better Egg) |

## Outputs

Expand All @@ -45,6 +46,7 @@ This posts a trust score comment on each pull request:
| `score` | Normalized trust score (0.0 - 1.0) |
| `trust-level` | Trust level: HIGH, MEDIUM, LOW, UNKNOWN, or BOT |
| `user` | GitHub username that was scored |
| `scoring-model` | Scoring model used: `v1` (Good Egg) or `v2` (Better Egg) |

## Custom Configuration

Expand Down Expand Up @@ -151,6 +153,28 @@ PRs and repository metadata. To stay within rate limits:
- The built-in cache (SQLite-backed) avoids refetching data that has not
changed. Cache TTLs are configurable.

## Using Better Egg (v2)

To use the v2 scoring model, set the `scoring-model` input:

```yaml
jobs:
score:
runs-on: ubuntu-latest
steps:
- uses: 2ndSetAI/good-egg@v0
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
scoring-model: v2
```

When using v2, PR comments are branded "Better Egg" and include a component
score breakdown showing graph score, merge rate, and account age
contributions. The `scoring-model` output reflects which model was used.

You can also set `scoring-model` via the `GOOD_EGG_SCORING_MODEL` environment
variable, but the input takes precedence.

## Example Workflows

See the [examples/](../examples/) directory for complete workflow files:
Expand All @@ -159,3 +183,5 @@ See the [examples/](../examples/) directory for complete workflow files:
that posts a PR comment
- [strict-workflow.yml](../examples/strict-workflow.yml) -- comment, check
run, and fail-on-low
- [better-egg-workflow.yml](../examples/better-egg-workflow.yml) -- v2
scoring model with component breakdown
40 changes: 40 additions & 0 deletions docs/library.md
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,44 @@ result = await score_pr_author(
)
```

### v2 (Better Egg) Configuration

To use the v2 scoring model, set `scoring_model` on the config:

```python
from good_egg import GoodEggConfig, score_pr_author

config = GoodEggConfig(
scoring_model="v2",
v2={
"graph": {"half_life_days": 180, "max_age_days": 730},
"features": {"merge_rate": True, "account_age": True},
"combined_model": {
"intercept": -0.8094,
"graph_score_weight": 1.9138,
"merge_rate_weight": -0.7783,
"account_age_weight": 0.1493,
},
},
)

result = await score_pr_author(
login="octocat",
repo_owner="octocat",
repo_name="Hello-World",
config=config,
)

# v2 results include component scores
if result.component_scores:
print(f"Graph score: {result.component_scores['graph_score']:.3f}")
print(f"Merge rate: {result.component_scores['merge_rate']:.3f}")
print(f"Log account age: {result.component_scores['log_account_age']:.3f}")
print(f"Normalized score: {result.normalized_score:.3f}")

print(f"Scoring model: {result.scoring_model}")
```

You can also load configuration from a YAML file:

```python
Expand Down Expand Up @@ -120,6 +158,8 @@ the following fields:
| `top_contributions` | `list[ContributionSummary]` | Top repositories contributed to |
| `language_match` | `bool` | Whether the user's top language matches the context repo |
| `flags` | `dict[str, bool]` | Flags (is_bot, is_new_account, etc.) |
| `scoring_model` | `str` | Scoring model used: `v1` or `v2` |
| `component_scores` | `dict[str, float] \| None` | Component breakdown (v2 only): `graph_score`, `merge_rate`, `log_account_age` |
| `scoring_metadata` | `dict[str, Any]` | Internal scoring details |

`TrustScore` is a Pydantic model, so you can serialize it:
Expand Down
63 changes: 60 additions & 3 deletions docs/mcp-server.md
Original file line number Diff line number Diff line change
Expand Up @@ -82,11 +82,14 @@ Returns the full trust score as JSON, including all fields from the
|------|------|----------|-------------|
| `username` | `string` | Yes | GitHub username to score |
| `repo` | `string` | Yes | Target repository in `owner/repo` format |
| `scoring_model` | `string` | No | Scoring model: `v1` (Good Egg, default) or `v2` (Better Egg) |

**Returns:** Full `TrustScore` JSON with all fields (user_login,
context_repo, raw_score, normalized_score, trust_level, percentile,
account_age_days, total_merged_prs, unique_repos_contributed,
top_contributions, language_match, flags, scoring_metadata).
top_contributions, language_match, flags, scoring_model, component_scores,
scoring_metadata). When `scoring_model` is `v2`, the response includes
`component_scores` with graph_score, merge_rate, and log_account_age.

### check_pr_author

Expand All @@ -98,8 +101,9 @@ Returns a compact summary suitable for quick checks.
|------|------|----------|-------------|
| `username` | `string` | Yes | GitHub username to check |
| `repo` | `string` | Yes | Target repository in `owner/repo` format |
| `scoring_model` | `string` | No | Scoring model: `v1` (Good Egg, default) or `v2` (Better Egg) |

**Returns:**
**Returns (v1):**

```json
{
Expand All @@ -110,6 +114,23 @@ Returns a compact summary suitable for quick checks.
}
```

**Returns (v2):**

```json
{
"user_login": "octocat",
"trust_level": "HIGH",
"normalized_score": 0.82,
"total_merged_prs": 47,
"scoring_model": "v2",
"component_scores": {
"graph_score": 0.78,
"merge_rate": 0.91,
"log_account_age": 3.45
}
}
```

### get_trust_details

Returns an expanded breakdown with contributions, flags, and metadata.
Expand All @@ -120,8 +141,38 @@ Returns an expanded breakdown with contributions, flags, and metadata.
|------|------|----------|-------------|
| `username` | `string` | Yes | GitHub username to analyse |
| `repo` | `string` | Yes | Target repository in `owner/repo` format |
| `scoring_model` | `string` | No | Scoring model: `v1` (Good Egg, default) or `v2` (Better Egg) |

**Returns:**
**Returns (v1):**

```json
{
"user_login": "octocat",
"context_repo": "octocat/Hello-World",
"trust_level": "HIGH",
"normalized_score": 0.82,
"raw_score": 0.0045,
"account_age_days": 3650,
"total_merged_prs": 47,
"unique_repos_contributed": 12,
"language_match": true,
"top_contributions": [
{
"repo_name": "octocat/Hello-World",
"pr_count": 15,
"language": "Python",
"stars": 1200
}
],
"flags": {
"is_bot": false,
"is_new_account": false
},
"scoring_metadata": {}
}
```

**Returns (v2):**

```json
{
Expand All @@ -146,6 +197,12 @@ Returns an expanded breakdown with contributions, flags, and metadata.
"is_bot": false,
"is_new_account": false
},
"scoring_model": "v2",
"component_scores": {
"graph_score": 0.78,
"merge_rate": 0.91,
"log_account_age": 3.45
},
"scoring_metadata": {}
}
```
Expand Down
Loading