get_score
Calculates a project's failure-prevention score from A+ to F, showing debugging hours saved, tokens prevented, and dollars protected to answer questions about progress or value.
Instructions
Get the project's failure-prevention score.
Returns an A+→F grade with concrete ROI numbers: debugging hours
saved, tokens prevented, dollars protected. Use when the user asks
about progress or value.Input Schema
| Name | Required | Description | Default |
|---|---|---|---|
No arguments | |||
Output Schema
| Name | Required | Description | Default |
|---|---|---|---|
| result | Yes |
Implementation Reference
- src/projectmem/mcp_server.py:288-315 (handler)The MCP tool handler for 'get_score' — reads events from events.jsonl, calls calculate_score(), and returns a formatted string with grade, score, and ROI metrics.
@mcp.tool() @safe_tool def get_score() -> str: """Get the project's failure-prevention score. Returns an A+→F grade with concrete ROI numbers: debugging hours saved, tokens prevented, dollars protected. Use when the user asks about progress or value.""" import json from projectmem.commands.score import calculate_score from projectmem.storage import events_path raw = [] path = events_path() for line in path.read_text(encoding="utf-8").splitlines(): if line.strip(): raw.append(json.loads(line)) result = calculate_score(raw) c = result["components"] v = result["value"] return ( f"projectmem Prevention Score: {result['grade']} ({result['score']}/100)\n" f" Failed approaches on record: {c['failed_approaches']}\n" f" Decisions documented: {c['decisions_documented']}\n" f" Fixes with context: {c['fixes_with_context']}\n" f" Debugging hours saved: ~{v['debugging_hours_saved']}h\n" f" Tokens saved: {v['tokens_saved']:,}\n" f" Estimated USD saved: ${v['usd_saved']:.2f}" ) - src/projectmem/mcp_server.py:290-315 (schema)Signature: no arguments, returns a string.
def get_score() -> str: """Get the project's failure-prevention score. Returns an A+→F grade with concrete ROI numbers: debugging hours saved, tokens prevented, dollars protected. Use when the user asks about progress or value.""" import json from projectmem.commands.score import calculate_score from projectmem.storage import events_path raw = [] path = events_path() for line in path.read_text(encoding="utf-8").splitlines(): if line.strip(): raw.append(json.loads(line)) result = calculate_score(raw) c = result["components"] v = result["value"] return ( f"projectmem Prevention Score: {result['grade']} ({result['score']}/100)\n" f" Failed approaches on record: {c['failed_approaches']}\n" f" Decisions documented: {c['decisions_documented']}\n" f" Fixes with context: {c['fixes_with_context']}\n" f" Debugging hours saved: ~{v['debugging_hours_saved']}h\n" f" Tokens saved: {v['tokens_saved']:,}\n" f" Estimated USD saved: ${v['usd_saved']:.2f}" ) - src/projectmem/mcp_server.py:288-288 (registration)Registered as an MCP tool via the @mcp.tool() decorator on the FastMCP instance.
@mcp.tool() - Core scoring engine — calculates component metrics, score breakdown (0-100), letter grade, and estimated value (hours saved, tokens saved, USD saved).
def calculate_score(events: list[dict[str, Any]], since_days: int | None = None) -> dict[str, Any]: """Calculate the prevention score from raw event dicts.""" now = datetime.now(timezone.utc) # Filter by time window if specified filtered = events if since_days is not None: cutoff = now - timedelta(days=since_days) filtered = [] for e in events: ts = e.get("timestamp", "") try: event_time = datetime.fromisoformat(ts.replace("Z", "+00:00")) if event_time >= cutoff: filtered.append(e) except (ValueError, AttributeError): filtered.append(e) # include if timestamp can't be parsed # ── Component metrics ── failed_approaches = 0 successful_attempts = 0 fixes_with_context = 0 decisions_documented = 0 notes_count = 0 auto_captured_count = 0 manual_count = 0 files_seen: set[str] = set() files_with_failures: set[str] = set() files_with_gotchas: set[str] = set() # files with any associated event file_event_counts: dict[str, int] = defaultdict(int) for e in filtered: etype = e.get("type", "") outcome = e.get("outcome") is_auto = e.get("auto_captured", False) if is_auto: auto_captured_count += 1 else: manual_count += 1 # Track files event_files = list(e.get("files", [])) loc = e.get("location", "") if loc and ":" in loc: event_files.append(loc.split(":")[0]) for f in event_files: files_seen.add(f) file_event_counts[f] += 1 files_with_gotchas.add(f) if etype == "attempt": if outcome == "failed": failed_approaches += 1 for f in event_files: files_with_failures.add(f) elif outcome == "worked": successful_attempts += 1 elif etype == "fix": fixes_with_context += 1 elif etype == "decision": decisions_documented += 1 elif etype == "note": notes_count += 1 # ── Derived metrics ── debugging_hours_saved = round( (failed_approaches * HOURS_PER_FAILED_APPROACH) + (fixes_with_context * HOURS_PER_FIX_WITH_CONTEXT) + (decisions_documented * HOURS_PER_DECISION), 1, ) # Count high-churn files (4+ events) high_churn_files = sum(1 for c in file_event_counts.values() if c >= 4) debugging_hours_saved += round(high_churn_files * HOURS_PER_CHURN_FLAG, 1) tokens_saved = ( (failed_approaches * TOKENS_PER_FAILED_APPROACH) + (fixes_with_context * TOKENS_PER_CONTEXT_REBUILD) + (decisions_documented * TOKENS_PER_DECISION) + (auto_captured_count * TOKENS_PER_CONTEXT_REBUILD) # auto events = context that'd be rebuilt ) usd_saved = round((tokens_saved / 1_000_000) * USD_PER_MILLION_TOKENS, 2) # ── Score calculation ── # Score components (0-20 each, max 100) failed_score = min(failed_approaches * 2, 20) decision_score = min(decisions_documented * 1.5, 20) fix_score = min(fixes_with_context * 2, 20) gotcha_score = min(len(files_with_gotchas) * 0.5, 20) coverage_score = min( (notes_count * 0.5) + (auto_captured_count * 0.3) + (successful_attempts * 1), 20, ) total_score = min( int(failed_score + decision_score + fix_score + gotcha_score + coverage_score), 100, ) # Letter grade if total_score >= 90: grade = "A+" elif total_score >= 80: grade = "A" elif total_score >= 60: grade = "B" elif total_score >= 40: grade = "C" elif total_score >= 20: grade = "D" else: grade = "F" return { "score": total_score, "grade": grade, "components": { "failed_approaches": failed_approaches, "successful_attempts": successful_attempts, "decisions_documented": decisions_documented, "fixes_with_context": fixes_with_context, "notes_count": notes_count, "files_with_gotchas": len(files_with_gotchas), "high_churn_files": high_churn_files, }, "capture": { "auto_captured": auto_captured_count, "manual": manual_count, "total": len(filtered), "auto_rate": round(auto_captured_count / max(len(filtered), 1) * 100, 1), }, "value": { "debugging_hours_saved": debugging_hours_saved, "tokens_saved": tokens_saved, "usd_saved": usd_saved, }, "score_breakdown": { "failed_knowledge": round(failed_score, 1), "decisions": round(decision_score, 1), "fixes": round(fix_score, 1), "file_coverage": round(gotcha_score, 1), "general_coverage": round(coverage_score, 1), }, }