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
2 changes: 1 addition & 1 deletion .github/workflows/devlens-live-integration.yml
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ jobs:
assert set(report["scores"]) == dimensions
assert all(0 <= int(v) <= 100 for v in report["scores"].values())
assert os.environ["BADGE"].startswith("https://img.shields.io/badge/")
assert report["action_version"] == "2.0.9"
assert report["action_version"] == "2.1.0"

print(f"Live DevLens integration passed: {score}/100")

Expand Down
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@ All notable changes to DevLens, newest first.
## v2.0.9 — 2026-09-30

- Corrected Issue Response scoring to exclude pull requests from issue counts.
- Changed Commit Activity to count commits through GitHub search without materializing the full commit history.
- Changed Documentation detection to avoid recursive-tree truncation.
- Redefined PR Velocity as merged-PR throughput over the same 90-day window as Commit Activity.
- Added deterministic regression fixtures for issue-response scoring.
- Updated live integration expectations for `v2.0.9`.

Expand Down
4 changes: 2 additions & 2 deletions docs/AI_PROJECT_CONTEXT.md
Original file line number Diff line number Diff line change
Expand Up @@ -310,7 +310,7 @@ As of 2026-09-30:

- `v2.0.0` exists as the original Marketplace release tag.
- `v2.0.1` was previously published successfully.
- `v2.0.9` is the latest verified release and was created and published by the automatic critical-change release workflow.
- `v2.1.0` is the latest verified release and was created and published by the automatic critical-change release workflow.
- The release preflight executed the real Action successfully and validated the reported Action version against the release version.
- The floating `v2` tag was updated successfully to `v2.0.9`.
- The automatic release workflow detects critical production paths on pushes to `main`, derives the release version from `scripts/devlens.py`, runs static validation and live Action preflight, updates `v2`, and publishes the GitHub Release.
Expand Down Expand Up @@ -559,7 +559,7 @@ When uncertain, inspect current repository files and current GitHub Actions/rele
**Implemented**
- Hardened `.github/workflows/release.yml` so static and live preflight run before creation of a new versioned release tag.
- Added immutable versioned-tag validation.
- Bumped the machine-readable Action version to `2.0.9` for the Issue Response scoring correction.
- Bumped the machine-readable Action version to `2.0.9` for the scoring evidence/API-efficiency corrections.
- Updated live integration validation to require `2.0.8`.
- Merged PR #24 and triggered the automatic critical-change release.

Expand Down
60 changes: 37 additions & 23 deletions scripts/devlens.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,15 +45,15 @@ def score_readme():

def score_activity():
try:
since = now - timedelta(days=90)
commits = list(repo.get_commits(since=since))
n = len(commits)
since = (now - timedelta(days=90)).strftime("%Y-%m-%d")
n = g.search_commits(query=f"repo:{REPO_NAME} committer:>={since}").totalCount
if n >= 30: return 100
if n >= 15: return 75
if n >= 5: return 50
if n >= 1: return 25
return 0
except: return 0
except Exception:
return 0

def score_freshness():
d = days_since(repo.pushed_at)
Expand All @@ -64,14 +64,29 @@ def score_freshness():
return 10

def score_docs():
s = 0
key_files = ["LICENSE", "CONTRIBUTING.md", "CHANGELOG.md", "CODE_OF_CONDUCT.md", "SECURITY.md", "docs/"]
score = 0
try:
paths = [c.path for c in repo.get_git_tree("HEAD", recursive=True).tree]
for f in key_files:
if any(c.startswith(f.rstrip("/")) for c in paths): s += 17
return min(s, 100)
except: return 0
for path in ["LICENSE", "LICENSE.md", "LICENSE.txt"]:
try:
repo.get_contents(path)
score += 17
break
except Exception:
pass
for path in ["CONTRIBUTING.md", "CHANGELOG.md", "CODE_OF_CONDUCT.md", "SECURITY.md"]:
try:
repo.get_contents(path)
score += 17
except Exception:
pass
try:
repo.get_contents("docs")
score += 17
except Exception:
pass
return min(score, 100)
except Exception:
return 0

def score_ci():
try:
Expand Down Expand Up @@ -106,18 +121,17 @@ def score_community():

weights = {"readme":0.20,"activity":0.20,"freshness":0.10,"docs":0.10,"ci":0.10,"issues":0.10,"community":0.05,"pr_velocity":0.10,"security":0.05}
def score_pr_velocity():
"""Measure merged-PR throughput over the same 90-day window used for activity."""
try:
pulls = list(repo.get_pulls(state="closed", sort="updated", direction="desc"))[:30]
merged = [pr for pr in pulls if pr.merged_at][:20]
durations = [(pr.merged_at - pr.created_at).total_seconds() / 86400 for pr in merged if pr.created_at and pr.merged_at]
if not durations: return 50
avg_days = sum(durations) / len(durations)
if avg_days <= 1: return 100
if avg_days <= 3: return 85
if avg_days <= 7: return 70
if avg_days <= 14: return 55
if avg_days <= 30: return 35
return 15
since = (now - timedelta(days=90)).strftime("%Y-%m-%d")
merged = g.search_issues(
query=f"repo:{REPO_NAME} is:pr is:merged merged:>={since}"
).totalCount
if merged >= 30: return 100
if merged >= 15: return 75
if merged >= 5: return 50
if merged >= 1: return 25
return 0
except Exception:
return 50

Expand Down Expand Up @@ -172,7 +186,7 @@ def dim_bar(score):
badge_url = (f"https://img.shields.io/badge/DevLens%20Health-{health}%2F100"
f"-{badge_color(health)}?style={BADGE_STYLE}&logo=github")

report = {"repo":REPO_NAME,"score_model":"action-v2-9d","action_version":"2.0.9","health_score":health,"scores":scores,
report = {"repo":REPO_NAME,"score_model":"action-v2-9d","action_version":"2.1.0","health_score":health,"scores":scores,
"badge_url":badge_url,"generated_at":now.isoformat()}

print(json.dumps(report, indent=2))
Expand Down
21 changes: 21 additions & 0 deletions tests/test_scoring_evidence.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
from pathlib import Path
import ast

source = Path("scripts/devlens.py").read_text()
tree = ast.parse(source)
wanted = {"score_activity", "score_docs", "score_pr_velocity"}
functions = {n.name: n for n in tree.body if isinstance(n, ast.FunctionDef) and n.name in wanted}
assert set(functions) == wanted

# Verify the production functions use bounded thresholds and no bulk commit/PR materialization.
src = source
assert "list(repo.get_commits" not in src
assert "list(repo.get_pulls" not in src
assert 'search_commits(query=' in src
assert 'is:pr is:merged' in src
assert 'repo.get_git_tree("HEAD", recursive=True)' not in src

# Preserve the documented 0-100 score contract.
assert "return 100" in ast.unparse(functions["score_activity"])
assert "return 100" in ast.unparse(functions["score_pr_velocity"])
print("Scoring evidence/API-efficiency fixtures passed.")
Loading