Initial commit
This commit is contained in:
359
skills/skill/references/ranking-algorithm.md
Normal file
359
skills/skill/references/ranking-algorithm.md
Normal file
@@ -0,0 +1,359 @@
|
||||
# Ranking Algorithm for Claude Skills
|
||||
|
||||
Comprehensive scoring system to rank skills by popularity, freshness, and quality.
|
||||
|
||||
## Core Ranking Formula
|
||||
|
||||
```
|
||||
final_score = (base_stars * freshness_multiplier * quality_bonus)
|
||||
```
|
||||
|
||||
## Component 1: Base Stars
|
||||
|
||||
Direct indicator of community validation and popularity.
|
||||
|
||||
```bash
|
||||
base_stars = repository.stargazers_count
|
||||
|
||||
# Minimum threshold: 10 stars (filter out experiments)
|
||||
if [ $base_stars -lt 10 ]; then
|
||||
skip_result=true
|
||||
fi
|
||||
```
|
||||
|
||||
## Component 2: Freshness Multiplier
|
||||
|
||||
Recent updates indicate active maintenance and modern practices.
|
||||
|
||||
### Calculate Days Since Last Update
|
||||
|
||||
```bash
|
||||
# Get pushed_at timestamp from repository
|
||||
pushed_at="2025-10-28T12:00:00Z"
|
||||
|
||||
# Calculate days old (macOS)
|
||||
days_old=$(( ($(date +%s) - $(date -j -f "%Y-%m-%dT%H:%M:%SZ" "$pushed_at" +%s)) / 86400 ))
|
||||
|
||||
# Calculate days old (Linux)
|
||||
days_old=$(( ($(date +%s) - $(date -d "$pushed_at" +%s)) / 86400 ))
|
||||
```
|
||||
|
||||
### Apply Freshness Multiplier
|
||||
|
||||
```bash
|
||||
if [ $days_old -lt 30 ]; then
|
||||
freshness_multiplier=1.5
|
||||
freshness_badge="🔥"
|
||||
freshness_label="Hot"
|
||||
elif [ $days_old -lt 90 ]; then
|
||||
freshness_multiplier=1.2
|
||||
freshness_badge="📅"
|
||||
freshness_label="Recent"
|
||||
elif [ $days_old -lt 180 ]; then
|
||||
freshness_multiplier=1.0
|
||||
freshness_badge="📆"
|
||||
freshness_label="Active"
|
||||
else
|
||||
freshness_multiplier=0.5
|
||||
freshness_badge="⏰"
|
||||
freshness_label="Older"
|
||||
fi
|
||||
```
|
||||
|
||||
### Freshness Tiers
|
||||
|
||||
| Age Range | Multiplier | Badge | Label | Reasoning |
|
||||
|-----------|------------|-------|-------|-----------|
|
||||
| < 30 days | 1.5x | 🔥 | Hot | Very active, likely works with latest Claude |
|
||||
| 30-90 days | 1.2x | 📅 | Recent | Active maintenance |
|
||||
| 90-180 days | 1.0x | 📆 | Active | Stable, still maintained |
|
||||
| > 180 days | 0.5x | ⏰ | Older | May be outdated or abandoned |
|
||||
|
||||
## Component 3: Quality Bonus (Optional)
|
||||
|
||||
Additional signals of skill quality beyond stars and freshness.
|
||||
|
||||
### Quality Signals
|
||||
|
||||
```bash
|
||||
quality_bonus=1.0 # Start at neutral
|
||||
|
||||
# Has comprehensive description
|
||||
if [ ${#description} -gt 100 ]; then
|
||||
quality_bonus=$(echo "$quality_bonus + 0.1" | bc)
|
||||
fi
|
||||
|
||||
# Has reference files
|
||||
reference_count=$(gh api "repos/$repo/contents" | jq '[.[] | select(.name | test("references|docs|examples"))] | length')
|
||||
if [ $reference_count -gt 0 ]; then
|
||||
quality_bonus=$(echo "$quality_bonus + 0.1" | bc)
|
||||
fi
|
||||
|
||||
# Has dependencies documentation
|
||||
if gh api "repos/$repo/contents" | jq -e '.[] | select(.name == "package.json")' > /dev/null; then
|
||||
quality_bonus=$(echo "$quality_bonus + 0.05" | bc)
|
||||
fi
|
||||
|
||||
# Active issues/PRs indicate engagement
|
||||
open_issues=$(gh api "repos/$repo" | jq '.open_issues_count')
|
||||
if [ $open_issues -gt 0 ] && [ $open_issues -lt 20 ]; then
|
||||
quality_bonus=$(echo "$quality_bonus + 0.05" | bc)
|
||||
fi
|
||||
|
||||
# Has recent commits (beyond just pushed_at)
|
||||
recent_commits=$(gh api "repos/$repo/commits?per_page=10" | jq 'length')
|
||||
if [ $recent_commits -ge 5 ]; then
|
||||
quality_bonus=$(echo "$quality_bonus + 0.1" | bc)
|
||||
fi
|
||||
|
||||
# Cap quality bonus at 1.5x
|
||||
if (( $(echo "$quality_bonus > 1.5" | bc -l) )); then
|
||||
quality_bonus=1.5
|
||||
fi
|
||||
```
|
||||
|
||||
### Quality Tier Examples
|
||||
|
||||
| Quality Bonus | Characteristics |
|
||||
|---------------|-----------------|
|
||||
| 1.0x (baseline) | Basic SKILL.md only |
|
||||
| 1.1x | + Good description |
|
||||
| 1.2x | + Reference files |
|
||||
| 1.3x | + Dependencies documented |
|
||||
| 1.4x | + Active community (issues/PRs) |
|
||||
| 1.5x (max) | All of the above |
|
||||
|
||||
## Complete Scoring Implementation
|
||||
|
||||
### Bash Implementation
|
||||
|
||||
```bash
|
||||
#!/bin/bash
|
||||
|
||||
calculate_score() {
|
||||
local repo=$1
|
||||
local stars=$2
|
||||
local pushed_at=$3
|
||||
|
||||
# Calculate days since last update
|
||||
days_old=$(( ($(date +%s) - $(date -j -f "%Y-%m-%dT%H:%M:%SZ" "$pushed_at" +%s)) / 86400 ))
|
||||
|
||||
# Freshness multiplier
|
||||
if [ $days_old -lt 30 ]; then
|
||||
freshness=1.5
|
||||
badge="🔥"
|
||||
elif [ $days_old -lt 90 ]; then
|
||||
freshness=1.2
|
||||
badge="📅"
|
||||
elif [ $days_old -lt 180 ]; then
|
||||
freshness=1.0
|
||||
badge="📆"
|
||||
else
|
||||
freshness=0.5
|
||||
badge="⏰"
|
||||
fi
|
||||
|
||||
# Calculate final score
|
||||
score=$(echo "$stars * $freshness" | bc)
|
||||
|
||||
echo "$score|$badge|$days_old"
|
||||
}
|
||||
|
||||
# Example usage
|
||||
result=$(calculate_score "lackeyjb/playwright-skill" 612 "2025-10-28T12:00:00Z")
|
||||
score=$(echo "$result" | cut -d'|' -f1)
|
||||
badge=$(echo "$result" | cut -d'|' -f2)
|
||||
days=$(echo "$result" | cut -d'|' -f3)
|
||||
|
||||
echo "Score: $score, Badge: $badge, Days: $days"
|
||||
# Output: Score: 918.0, Badge: 🔥, Days: 2
|
||||
```
|
||||
|
||||
### JQ Implementation (More Portable)
|
||||
|
||||
```bash
|
||||
gh search repos "claude skills" --json name,stargazersCount,pushedAt --limit 20 | \
|
||||
jq -r --arg now "$(date -u +%s)" '.[] |
|
||||
. as $repo |
|
||||
($repo.pushedAt | fromdateiso8601) as $pushed |
|
||||
(($now | tonumber) - $pushed) / 86400 | floor as $days |
|
||||
(if $days < 30 then 1.5 elif $days < 90 then 1.2 elif $days < 180 then 1.0 else 0.5 end) as $multiplier |
|
||||
(if $days < 30 then "🔥" elif $days < 90 then "📅" elif $days < 180 then "📆" else "⏰" end) as $badge |
|
||||
($repo.stargazersCount * $multiplier) as $score |
|
||||
"\($score)|\($repo.name)|\($repo.stargazersCount)|\($badge)|\($days)"
|
||||
' | sort -t'|' -k1 -nr | head -10
|
||||
```
|
||||
|
||||
## Ranking Examples
|
||||
|
||||
### Real-World Scores
|
||||
|
||||
| Skill | Stars | Days Old | Freshness | Multiplier | Final Score | Rank |
|
||||
|-------|-------|----------|-----------|------------|-------------|------|
|
||||
| playwright-skill | 612 | 2 | 🔥 | 1.5x | **918** | #1 |
|
||||
| agent-skill-creator | 96 | 5 | 🔥 | 1.5x | **144** | #2 |
|
||||
| skill-codex | 153 | 9 | 🔥 | 1.5x | **229.5** | Would be #2, but bumped by age |
|
||||
| ios-simulator-skill | 77 | 2 | 🔥 | 1.5x | **115.5** | #3 |
|
||||
| claude-skills-mcp | 85 | 2 | 🔥 | 1.5x | **127.5** | #4 |
|
||||
|
||||
*With hot multiplier, even 77 stars beats 96 stars from 30+ days ago*
|
||||
|
||||
### Score Comparison Scenarios
|
||||
|
||||
**Scenario 1: Fresh skill vs. Popular old skill**
|
||||
- Skill A: 50 stars, 10 days old → 50 × 1.5 = **75 points** 🔥
|
||||
- Skill B: 100 stars, 200 days old → 100 × 0.5 = **50 points** ⏰
|
||||
- **Winner: Skill A** (freshness wins)
|
||||
|
||||
**Scenario 2: Very popular but older skill**
|
||||
- Skill A: 1000 stars, 365 days old → 1000 × 0.5 = **500 points** ⏰
|
||||
- Skill B: 200 stars, 15 days old → 200 × 1.5 = **300 points** 🔥
|
||||
- **Winner: Skill A** (massive popularity overcomes age)
|
||||
|
||||
**Scenario 3: Moderate skill, regularly updated**
|
||||
- Skill A: 50 stars, 85 days old → 50 × 1.2 = **60 points** 📅
|
||||
- Skill B: 60 stars, 95 days old → 60 × 1.0 = **60 points** 📆
|
||||
- **Tie** (freshness threshold matters)
|
||||
|
||||
## Handling Edge Cases
|
||||
|
||||
### Newly Created Skills (< 7 days old)
|
||||
|
||||
```bash
|
||||
# New skills may have inflated scores, add small penalty
|
||||
if [ $days_old -lt 7 ]; then
|
||||
new_skill_penalty=0.9
|
||||
score=$(echo "$score * $new_skill_penalty" | bc)
|
||||
note="⚠️ Very new skill - limited validation"
|
||||
fi
|
||||
```
|
||||
|
||||
### Archived or Deprecated Skills
|
||||
|
||||
```bash
|
||||
# Check if repository is archived
|
||||
is_archived=$(gh api "repos/$repo" | jq -r '.archived')
|
||||
|
||||
if [ "$is_archived" = "true" ]; then
|
||||
score=0 # Exclude from results
|
||||
note="🔒 Archived - no longer maintained"
|
||||
fi
|
||||
```
|
||||
|
||||
### Fork vs. Original
|
||||
|
||||
```bash
|
||||
# Check if repository is a fork
|
||||
is_fork=$(gh api "repos/$repo" | jq -r '.fork')
|
||||
|
||||
if [ "$is_fork" = "true" ]; then
|
||||
# Check if fork has more stars than parent
|
||||
parent_stars=$(gh api "repos/$repo" | jq -r '.parent.stargazers_count')
|
||||
if [ $stars -gt $parent_stars ]; then
|
||||
# Fork is more popular, keep it
|
||||
note="🍴 Popular fork"
|
||||
else
|
||||
# Prefer original
|
||||
score=$(echo "$score * 0.8" | bc)
|
||||
note="🍴 Fork - see original"
|
||||
fi
|
||||
fi
|
||||
```
|
||||
|
||||
## Sorting and Display
|
||||
|
||||
### Final Sort Order
|
||||
|
||||
```bash
|
||||
# Sort by score (descending), then by stars (descending)
|
||||
results | sort -t'|' -k1,1nr -k3,3nr | head -10
|
||||
```
|
||||
|
||||
### Ties
|
||||
|
||||
When scores are identical:
|
||||
1. Sort by stars (higher first)
|
||||
2. If still tied, sort by freshness (newer first)
|
||||
3. If still tied, sort alphabetically
|
||||
|
||||
```bash
|
||||
# Multi-level sort
|
||||
jq -r '.[] | "\(.score)|\(.stars)|\(.days_old)|\(.name)"' | \
|
||||
sort -t'|' -k1,1nr -k2,2nr -k3,3n -k4,4
|
||||
```
|
||||
|
||||
## Performance Optimization
|
||||
|
||||
### Bulk Scoring
|
||||
|
||||
```bash
|
||||
# Score all repos in one pass
|
||||
gh search repos "claude skills" --json name,stargazersCount,pushedAt,fullName --limit 50 | \
|
||||
jq -r --arg now "$(date -u +%s)" '
|
||||
.[] |
|
||||
. as $repo |
|
||||
($repo.pushedAt | fromdateiso8601) as $pushed |
|
||||
(($now | tonumber) - $pushed) / 86400 | floor as $days |
|
||||
(if $days < 30 then 1.5 elif $days < 90 then 1.2 elif $days < 180 then 1.0 else 0.5 end) as $mult |
|
||||
($repo.stargazersCount * $mult) as $score |
|
||||
{
|
||||
name: $repo.name,
|
||||
full_name: $repo.fullName,
|
||||
stars: $repo.stargazersCount,
|
||||
days_old: $days,
|
||||
score: $score
|
||||
}
|
||||
' | jq -s 'sort_by(-.score) | .[0:10]'
|
||||
```
|
||||
|
||||
### Caching Scores
|
||||
|
||||
```bash
|
||||
# Cache scores to avoid recalculation
|
||||
score_cache=".skill-scores.json"
|
||||
|
||||
# Generate or load cache
|
||||
if [ ! -f "$score_cache" ] || [ $(($(date +%s) - $(stat -f %m "$score_cache"))) -gt 3600 ]; then
|
||||
# Regenerate cache (older than 1 hour)
|
||||
calculate_all_scores > "$score_cache"
|
||||
fi
|
||||
|
||||
# Use cached scores
|
||||
cat "$score_cache" | jq '.[] | select(.score > 50)'
|
||||
```
|
||||
|
||||
## Customization
|
||||
|
||||
### User-Defined Weights
|
||||
|
||||
Allow users to adjust ranking preferences:
|
||||
|
||||
```bash
|
||||
# Default weights
|
||||
STAR_WEIGHT=1.0
|
||||
FRESHNESS_WEIGHT=1.0
|
||||
QUALITY_WEIGHT=0.5
|
||||
|
||||
# Calculate weighted score
|
||||
weighted_score=$(echo "$stars * $STAR_WEIGHT + $freshness_score * $FRESHNESS_WEIGHT + $quality_score * $QUALITY_WEIGHT" | bc)
|
||||
```
|
||||
|
||||
### Category-Specific Ranking
|
||||
|
||||
Different categories may value different factors:
|
||||
|
||||
```bash
|
||||
# For automation skills: prioritize quality (working code)
|
||||
if [ "$category" = "automation" ]; then
|
||||
QUALITY_WEIGHT=1.5
|
||||
fi
|
||||
|
||||
# For research skills: prioritize freshness (up-to-date sources)
|
||||
if [ "$category" = "research" ]; then
|
||||
FRESHNESS_WEIGHT=1.5
|
||||
fi
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
**Summary:** The ranking algorithm balances popularity (stars) with recency (freshness), ensuring users see both well-established skills and cutting-edge new capabilities. Quality signals provide additional nuance for edge cases.
|
||||
Reference in New Issue
Block a user