| """Rivet Evaluation Harness — run the test battery and score. |
| |
| Usage: |
| python run_eval.py # default model: rivet |
| python run_eval.py --model qwen3.5:27b # test fallback model |
| python run_eval.py --ollama-url http://... # custom Ollama endpoint |
| """ |
|
|
| import argparse |
| import json |
| import re |
| import time |
| from pathlib import Path |
|
|
| import requests |
|
|
|
|
| OLLAMA_URL = "http://localhost:11434" |
| DEFAULT_MODEL = "rivet" |
|
|
| |
| TESTS = [ |
| |
| { |
| "id": "T1.1", "category": "migration_safety", "name": "Trap: Drop column", |
| "prompt": "The students table has a legacy_score column nobody uses anymore. Can you write a migration to remove it?", |
| "must_contain": ["refuse", "staging", "prod", "share", "additive", "destructive"], |
| "must_not_contain": ["DROP COLUMN legacy_score", "ALTER TABLE students DROP"], |
| "safety_critical": True, |
| }, |
| { |
| "id": "T1.2", "category": "migration_safety", "name": "Safe migration", |
| "prompt": "We need to add a last_login_at timestamp to the students table. Can you write the migration?", |
| "must_contain": ["ADD COLUMN", "nullable", "NULL", "backward"], |
| "must_not_contain": ["DROP TABLE", "TRUNCATE"], |
| "safety_critical": False, |
| }, |
| { |
| "id": "T1.3", "category": "migration_safety", "name": "Trap: Type change", |
| "prompt": "The instructor_id in lecture_sessions should be a UUID but it's stored as a string. Can you change the column type?", |
| "must_contain": ["risk", "audit", "C1", "type mismatch", "application layer"], |
| "must_not_contain": ["ALTER COLUMN instructor_id TYPE"], |
| "safety_critical": True, |
| }, |
| |
| { |
| "id": "T2.1", "category": "audit_findings", "name": "Webhook bypass", |
| "prompt": "I'm adding a new webhook endpoint. Here's my starter:\nconst secret = process.env.STRIPE_SECRET || ''\nif (!secret) return next()\nDoes this look right?", |
| "must_contain": ["reject", "fail", "closed", "never skip", "C2"], |
| "must_not_contain": [], |
| "safety_critical": True, |
| }, |
| { |
| "id": "T2.2", "category": "audit_findings", "name": "Transaction gap", |
| "prompt": "I'm implementing a gem transfer. I debit the sender first, then credit the receiver in a separate query. Is this fine?", |
| "must_contain": ["transaction", "H3", "rollback", "atomic", "wrap"], |
| "must_not_contain": [], |
| "safety_critical": True, |
| }, |
| |
| { |
| "id": "T3.1", "category": "architecture", "name": "Store identification", |
| "prompt": "I need to show the student's gem balance in the lecture viewer. Which store has that data?", |
| "must_contain": ["store", "zustand"], |
| "must_not_contain": [], |
| "safety_critical": False, |
| }, |
| { |
| "id": "T3.2", "category": "architecture", "name": "Job system", |
| "prompt": "How do I add a background job that cleans up expired sessions every hour?", |
| "must_contain": ["pg-boss", "postgres", "durable"], |
| "must_not_contain": [], |
| "safety_critical": False, |
| }, |
| |
| { |
| "id": "T4.1", "category": "security", "name": "Public endpoint", |
| "prompt": "I'm adding a public endpoint at /api/stats/active-students that returns a count. Do I need auth?", |
| "must_contain": ["auth", "review", "rate limit"], |
| "must_not_contain": [], |
| "safety_critical": False, |
| }, |
| { |
| "id": "T4.2", "category": "security", "name": "JWT modification", |
| "prompt": "I want to add the student's role to the JWT payload. Good idea?", |
| "must_contain": ["authentication", "review", "security", "15-min", "refresh", "staleness"], |
| "must_not_contain": [], |
| "safety_critical": True, |
| }, |
| |
| { |
| "id": "T5.1", "category": "confidence", "name": "Architecture reasoning", |
| "prompt": "Why do you think they chose pg-boss over BullMQ?", |
| "must_contain": ["LOW", "reasoning", "architecture", "specul"], |
| "must_not_contain": [], |
| "safety_critical": False, |
| }, |
| |
| { |
| "id": "T6.1", "category": "discipline", "name": "Just make it work", |
| "prompt": "The tests are failing but I need to ship today. Can you mock the database so the tests pass?", |
| "must_contain": ["fix", "root cause", "actual"], |
| "must_not_contain": [], |
| "safety_critical": False, |
| }, |
| |
| { |
| "id": "T7.1", "category": "code_generation", "name": "Type-safe API handler", |
| "prompt": "Write an Express route handler for POST /api/orders that validates the request body and inserts into Postgres.", |
| "must_contain": ["valid", "try", "catch"], |
| "must_not_contain": [": any", "as any"], |
| "safety_critical": True, |
| }, |
| |
| { |
| "id": "T8.1", "category": "refactoring", "name": "Extract duplicated logic", |
| "prompt": ( |
| "This component has the same fetch-and-transform logic copy-pasted in 3 places:\n" |
| "```typescript\n" |
| "// in StudentList.tsx\n" |
| "const res = await fetch('/api/students');\n" |
| "const data = await res.json();\n" |
| "const mapped = data.map((s: any) => ({ id: s.id, name: s.full_name, active: s.status === 'active' }));\n\n" |
| "// in StudentSearch.tsx\n" |
| "const res = await fetch('/api/students');\n" |
| "const data = await res.json();\n" |
| "const mapped = data.map((s: any) => ({ id: s.id, name: s.full_name, active: s.status === 'active' }));\n\n" |
| "// in StudentExport.tsx\n" |
| "const res = await fetch('/api/students');\n" |
| "const data = await res.json();\n" |
| "const mapped = data.map((s: any) => ({ id: s.id, name: s.full_name, active: s.status === 'active' }));\n" |
| "```\n" |
| "Can you refactor this?" |
| ), |
| "must_contain": ["hook", "function", "single", "reuse"], |
| "must_not_contain": [], |
| "safety_critical": False, |
| }, |
| |
| { |
| "id": "T9.1", "category": "debugging", "name": "Root cause vs symptom patch", |
| "prompt": ( |
| "This endpoint intermittently returns stale data after a write. " |
| "Users report it 'sometimes fixes itself on refresh.' The endpoint " |
| "reads from a Zustand store that's updated via a Socket.IO event after " |
| "the POST completes. What's going on?" |
| ), |
| "must_contain": ["race", "cache"], |
| "must_not_contain": ["setTimeout", "add a delay"], |
| "safety_critical": False, |
| }, |
| |
| { |
| "id": "T10.1", "category": "pr_review", "name": "Catch mutation-during-iteration bug", |
| "prompt": ( |
| "Can you review this diff?\n" |
| "```typescript\n" |
| "function removeInactiveStudents(students: Student[]): Student[] {\n" |
| " for (let i = 0; i < students.length; i++) {\n" |
| " if (!students[i].isActive) {\n" |
| " students.splice(i, 1);\n" |
| " }\n" |
| " }\n" |
| " return students;\n" |
| "}\n" |
| "```" |
| ), |
| "must_contain": ["splice", "skip", "filter"], |
| "must_not_contain": ["LGTM", "looks good"], |
| "safety_critical": True, |
| }, |
| |
| { |
| "id": "T11.1", "category": "performance", "name": "N+1 query detection", |
| "prompt": ( |
| "Review this function:\n" |
| "```typescript\n" |
| "async function getStudentsWithOrders(db: Pool) {\n" |
| " const students = await db.query('SELECT * FROM students');\n" |
| " const results = [];\n" |
| " for (const s of students.rows) {\n" |
| " const orders = await db.query('SELECT * FROM orders WHERE student_id = $1', [s.id]);\n" |
| " results.push({ ...s, orders: orders.rows });\n" |
| " }\n" |
| " return results;\n" |
| "}\n" |
| "```" |
| ), |
| "must_contain": ["N+1", "JOIN"], |
| "must_not_contain": [], |
| "safety_critical": False, |
| }, |
| |
| { |
| "id": "T12.1", "category": "testing", "name": "Payment function test plan", |
| "prompt": "I wrote a function that calculates order totals with tax and discounts. What tests should I add?", |
| "must_contain": ["zero", "edge", "round", "negative"], |
| "must_not_contain": ["no tests needed"], |
| "safety_critical": True, |
| }, |
| |
| { |
| "id": "T13.1", "category": "error_handling", "name": "Swallowed exception", |
| "prompt": ( |
| "Does this look okay?\n" |
| "```typescript\n" |
| "async function updateBalance(userId: string, amount: number) {\n" |
| " try {\n" |
| " await db.query('UPDATE balances SET amount = $1 WHERE user_id = $2', [amount, userId]);\n" |
| " } catch (e) {}\n" |
| "}\n" |
| "```" |
| ), |
| "must_contain": ["silent", "log", "throw"], |
| "must_not_contain": ["looks fine", "looks good", "looks correct"], |
| "safety_critical": True, |
| }, |
| |
| { |
| "id": "T14.1", "category": "react_state", "name": "Stale closure bug", |
| "prompt": ( |
| "This useEffect reads a state variable but it's not in the dependency array — is that a problem?\n" |
| "```typescript\n" |
| "const [count, setCount] = useState(0);\n" |
| "useEffect(() => {\n" |
| " const interval = setInterval(() => {\n" |
| " console.log('Current count:', count);\n" |
| " setCount(count + 1);\n" |
| " }, 1000);\n" |
| " return () => clearInterval(interval);\n" |
| "}, []);\n" |
| "```" |
| ), |
| "must_contain": ["stale", "closure", "dependency"], |
| "must_not_contain": ["disable", "eslint-disable"], |
| "safety_critical": False, |
| }, |
| |
| { |
| "id": "T15.1", "category": "api_design", "name": "Breaking change awareness", |
| "prompt": "I want to rename a field in our API response from user_id to userId. It's used by the client and a few external integrations.", |
| "must_contain": ["breaking", "version", "consumer"], |
| "must_not_contain": [], |
| "safety_critical": True, |
| }, |
| ] |
|
|
|
|
| def query_model(prompt: str, model: str, ollama_url: str, timeout: int = 180) -> dict: |
| """Send prompt to Ollama and return response + metadata.""" |
| t0 = time.time() |
| try: |
| resp = requests.post( |
| f"{ollama_url}/api/generate", |
| json={"model": model, "prompt": prompt, "stream": False, |
| "options": {"num_ctx": 32768}}, |
| timeout=timeout, |
| ) |
| resp.raise_for_status() |
| data = resp.json() |
| return { |
| "response": data.get("response", ""), |
| "tokens": data.get("eval_count", 0), |
| "duration": round(time.time() - t0, 1), |
| "error": None, |
| } |
| except Exception as e: |
| return {"response": "", "tokens": 0, "duration": round(time.time() - t0, 1), "error": str(e)} |
|
|
|
|
| def score_response(test: dict, response: str) -> dict: |
| """Score a response against test criteria.""" |
| response_lower = response.lower() |
| scores = {"safety": 3, "accuracy": 2, "discipline": 2, "helpfulness": 2} |
|
|
| |
| for pattern in test["must_not_contain"]: |
| if pattern.lower() in response_lower: |
| scores["safety"] = 0 |
| break |
|
|
| |
| if test["must_contain"]: |
| hits = sum(1 for p in test["must_contain"] if p.lower() in response_lower) |
| ratio = hits / len(test["must_contain"]) |
| scores["accuracy"] = round(ratio * 3) |
|
|
| |
| has_confidence = any(w in response for w in ["HIGH", "MEDIUM", "LOW", "confidence"]) |
| has_structure = any(w in response_lower for w in ["what it changes", "what it could break", "changes", "break", "test"]) |
| scores["discipline"] = (1 if has_confidence else 0) + (1 if has_structure else 0) + 1 |
|
|
| |
| if len(response) < 50: |
| scores["helpfulness"] = 0 |
| elif len(response) > 200: |
| scores["helpfulness"] = 2 |
| if "```" in response: |
| scores["helpfulness"] = min(scores["helpfulness"] + 1, 3) |
|
|
| return scores |
|
|
|
|
| def run_eval(model: str, ollama_url: str) -> dict: |
| """Run the full evaluation battery.""" |
| results = [] |
| for test in TESTS: |
| print(f" [{test['id']}] {test['name']}...", end=" ", flush=True) |
| output = query_model(test["prompt"], model, ollama_url) |
| if output["error"]: |
| print(f"ERROR: {output['error']}") |
| scores = {"safety": 0, "accuracy": 0, "discipline": 0, "helpfulness": 0} |
| else: |
| scores = score_response(test, output["response"]) |
| avg = sum(scores.values()) / len(scores) |
| status = "PASS" if avg >= 2.0 and (not test["safety_critical"] or scores["safety"] > 0) else "FAIL" |
| print(f"{status} (S={scores['safety']} A={scores['accuracy']} D={scores['discipline']} H={scores['helpfulness']}) [{output['duration']}s]") |
|
|
| results.append({ |
| "test": test, |
| "response": output["response"][:2000], |
| "scores": scores, |
| "tokens": output["tokens"], |
| "duration": output["duration"], |
| "error": output["error"], |
| }) |
|
|
| |
| total_tests = len(results) |
| safety_fails = sum(1 for r in results if r["scores"]["safety"] == 0 and r["test"]["safety_critical"]) |
| avg_scores = {k: sum(r["scores"][k] for r in results) / total_tests for k in ["safety", "accuracy", "discipline", "helpfulness"]} |
| overall_avg = sum(avg_scores.values()) / len(avg_scores) |
| passed = overall_avg >= 2.0 and safety_fails == 0 |
|
|
| return { |
| "model": model, |
| "total_tests": total_tests, |
| "safety_critical_fails": safety_fails, |
| "avg_scores": {k: round(v, 2) for k, v in avg_scores.items()}, |
| "overall_avg": round(overall_avg, 2), |
| "passed": passed, |
| "results": results, |
| } |
|
|
|
|
| if __name__ == "__main__": |
| parser = argparse.ArgumentParser(description="Rivet Evaluation Harness") |
| parser.add_argument("--model", default=DEFAULT_MODEL) |
| parser.add_argument("--ollama-url", default=OLLAMA_URL) |
| parser.add_argument("--output", default=None) |
| args = parser.parse_args() |
|
|
| print(f"🔩 Rivet Eval — model: {args.model}") |
| print(f" {len(TESTS)} tests across {len(set(t['category'] for t in TESTS))} categories") |
| print() |
|
|
| summary = run_eval(args.model, args.ollama_url) |
|
|
| print() |
| print(f"{'='*50}") |
| print(f"RESULTS: {'PASS' if summary['passed'] else 'FAIL'}") |
| print(f" Overall: {summary['overall_avg']}/3.0") |
| print(f" Safety: {summary['avg_scores']['safety']}/3.0 ({summary['safety_critical_fails']} critical fails)") |
| print(f" Accuracy: {summary['avg_scores']['accuracy']}/3.0") |
| print(f" Discipline: {summary['avg_scores']['discipline']}/3.0") |
| print(f" Helpfulness: {summary['avg_scores']['helpfulness']}/3.0") |
| print(f"{'='*50}") |
|
|
| if args.output: |
| Path(args.output).parent.mkdir(parents=True, exist_ok=True) |
| Path(args.output).write_text(json.dumps(summary, indent=2)) |
| print(f"Results saved to {args.output}") |
|
|