junaid0600 commited on
Commit
59746b9
Β·
1 Parent(s): 95c7542
Files changed (1) hide show
  1. env/graders.py +69 -68
env/graders.py CHANGED
@@ -4,6 +4,13 @@ from env.tasks import task_manager
4
 
5
  # HELPERS
6
 
 
 
 
 
 
 
 
7
 
8
  def _normalize(text: str) -> str:
9
  """Normalize SQL for comparison β€” lowercase, strip whitespace, collapse spaces."""
@@ -20,10 +27,10 @@ def _safe_get(payload: dict, key: str, default=None):
20
  def _score_explanation(explanation: str) -> float:
21
  """Score explanation quality by length and keyword richness."""
22
  if not explanation or not isinstance(explanation, str):
23
- return 0.001
24
  explanation = explanation.strip()
25
  if len(explanation) < 10:
26
- return 0.001
27
  if len(explanation) < 30:
28
  return 0.05
29
  if len(explanation) < 80:
@@ -38,27 +45,28 @@ def _score_confidence(confidence) -> float:
38
  return 0.05
39
  except (TypeError, ValueError):
40
  pass
41
- return 0.001
42
 
43
  def _query_similarity(submitted: str, expected: str) -> float:
44
  """
45
  Multi-level SQL similarity check.
46
- Returns 0.0 - 1.0 based on how close the submitted query is to expected.
47
  Handles case, whitespace, and keyword-level matching.
48
  """
49
  s = _normalize(submitted)
50
  e = _normalize(expected)
51
 
52
  # Exact match after normalization
 
53
  if s == e:
54
- return 0.999
55
 
56
  # Tokenize and check keyword overlap
57
  s_tokens = set(s.split())
58
  e_tokens = set(e.split())
59
 
60
  if not e_tokens:
61
- return 0.001
62
 
63
  overlap = len(s_tokens & e_tokens) / len(e_tokens)
64
 
@@ -69,7 +77,7 @@ def _query_similarity(submitted: str, expected: str) -> float:
69
 
70
  # Weighted combination
71
  similarity = round((overlap * 0.4) + (critical_score * 0.6), 4)
72
- return max(min(similarity, 0.999), 0.001)
73
 
74
  def _extract_critical_keywords(query: str) -> list[str]:
75
  """Extract SQL keywords that are critical to correctness."""
@@ -91,7 +99,7 @@ def _extract_critical_keywords(query: str) -> list[str]:
91
  def _score_error_type(submitted_type: str, expected_type: str) -> float:
92
  """Score for correctly identifying the error type."""
93
  if not submitted_type:
94
- return 0.001
95
  s = submitted_type.strip().lower()
96
  e = expected_type.strip().lower()
97
  if s == e:
@@ -105,12 +113,12 @@ def _score_error_type(submitted_type: str, expected_type: str) -> float:
105
  for canonical, aliases in related.items():
106
  if e == canonical and any(alias in s for alias in aliases):
107
  return 0.05
108
- return 0.001
109
 
110
  def _score_error_location(submitted_location: str, expected_location: str) -> float:
111
  """Score for correctly identifying WHERE in the query the error is."""
112
  if not submitted_location or not expected_location:
113
- return 0.001
114
  s = submitted_location.strip().lower()
115
  e = expected_location.strip().lower()
116
  if s == e:
@@ -119,7 +127,7 @@ def _score_error_location(submitted_location: str, expected_location: str) -> fl
119
  e_words = set(e.split())
120
  s_words = set(s.split())
121
  overlap = len(e_words & s_words) / len(e_words) if e_words else 0.0
122
- return max(round(overlap * 0.10, 4), 0.001)
123
 
124
 
125
  # GRADERS PER DIFFICULTY
@@ -127,27 +135,25 @@ def _score_error_location(submitted_location: str, expected_location: str) -> fl
127
  def grade_easy(action: Action, ground_truth: dict) -> tuple[float, dict, str]:
128
  """
129
  Easy task grader β€” syntax errors.
130
- Max score: 1.0
131
  Partial credit across: fix correctness, error location, error type, explanation, confidence.
132
  DETERMINISTIC: same input always returns same score.
133
  """
134
- # Edge case: null or malformed action
135
  if action is None or action.payload is None:
136
- return 0.001, {"error": "null_action"}, "No action provided."
137
 
138
  payload = action.payload
139
  score = 0.0
140
  breakdown = {}
141
  feedback_parts = []
142
 
143
- action_type = action.action_type.value if hasattr(action.action_type, "value") else str(action.action_type)
144
-
145
  # ── 1. Query fix correctness (0.50) ──────────────────────────
146
  submitted_query = _safe_get(payload, "fixed_query", "") or _safe_get(payload, "optimized_query", "")
147
  expected_query = ground_truth.get("fixed_query", "")
148
  similarity = _query_similarity(submitted_query, expected_query)
149
 
150
- if similarity >= 1.0:
 
151
  fix_score = 0.50
152
  feedback_parts.append("Correct fix applied.")
153
  elif similarity >= 0.75:
@@ -161,15 +167,15 @@ def grade_easy(action: Action, ground_truth: dict) -> tuple[float, dict, str]:
161
  feedback_parts.append("Fix is incorrect or not provided.")
162
 
163
  score += fix_score
164
- breakdown["fix_correctness"] = round(max(min(fix_score, 0.999), 0.001), 4)
165
 
166
  # ── 2. Error location (0.15) ─────────────────────────────────
167
  submitted_location = _safe_get(payload, "error_location", "")
168
  expected_location = ground_truth.get("error_location", "")
169
  loc_score = _score_error_location(str(submitted_location), expected_location)
170
  score += loc_score
171
- breakdown["error_location"] = round(loc_score, 4)
172
- if loc_score > 0:
173
  feedback_parts.append("Correctly identified error location.")
174
 
175
  # ── 3. Error type (0.10) ─────────────────────────────────────
@@ -177,28 +183,25 @@ def grade_easy(action: Action, ground_truth: dict) -> tuple[float, dict, str]:
177
  expected_type = ground_truth.get("error_type", "syntax")
178
  type_score = _score_error_type(str(submitted_type), expected_type)
179
  score += type_score
180
- breakdown["error_type"] = round(type_score, 4)
181
- if type_score > 0:
182
  feedback_parts.append("Correctly identified error type.")
183
 
184
  # ── 4. Explanation quality (0.15) ────────────────────────────
185
  explanation = _safe_get(payload, "explanation", "") or _safe_get(payload, "change_made", "")
186
  expl_score = _score_explanation(str(explanation) if explanation else "")
187
  score += expl_score
188
- breakdown["explanation"] = round(expl_score, 4)
189
- if expl_score > 0:
190
  feedback_parts.append("Explanation provided.")
191
 
192
  # ── 5. Confidence (0.05) ─────────────────────────────────────
193
  confidence = _safe_get(payload, "confidence", None)
194
  conf_score = _score_confidence(confidence)
195
  score += conf_score
196
- breakdown["confidence"] = round(conf_score, 4)
197
 
198
- # ── 6. Hint penalty ──────────────────────────────────────────
199
- # Hint penalty is applied in reward.py, not here
200
-
201
- final_score = round(max(min(score, 0.999), 0.001), 4)
202
  feedback = " ".join(feedback_parts) if feedback_parts else "No valid response provided."
203
  return final_score, breakdown, feedback
204
 
@@ -206,12 +209,12 @@ def grade_easy(action: Action, ground_truth: dict) -> tuple[float, dict, str]:
206
  def grade_medium(action: Action, ground_truth: dict) -> tuple[float, dict, str]:
207
  """
208
  Medium task grader β€” logic errors (wrong JOINs, wrong aggregations, etc).
209
- Max score: 1.0
210
  Higher bar: must correctly identify the logic flaw, not just syntax.
211
  DETERMINISTIC: same input always returns same score.
212
  """
213
  if action is None or action.payload is None:
214
- return 0.001, {"error": "null_action"}, "No action provided."
215
 
216
  payload = action.payload
217
  score = 0.0
@@ -223,7 +226,7 @@ def grade_medium(action: Action, ground_truth: dict) -> tuple[float, dict, str]:
223
  expected_query = ground_truth.get("fixed_query", "")
224
  similarity = _query_similarity(submitted_query, expected_query)
225
 
226
- if similarity >= 1.0:
227
  fix_score = 0.40
228
  feedback_parts.append("Correct fix applied.")
229
  elif similarity >= 0.80:
@@ -240,12 +243,11 @@ def grade_medium(action: Action, ground_truth: dict) -> tuple[float, dict, str]:
240
  feedback_parts.append("Fix is incorrect or missing.")
241
 
242
  score += fix_score
243
- breakdown["fix_correctness"] = round(max(min(fix_score, 0.999), 0.001), 4)
244
 
245
  # ── 2. Identifies the logic flaw (0.20) ──────────────────────
246
  explanation = str(_safe_get(payload, "explanation", "") or _safe_get(payload, "change_made", "") or "")
247
  error_type = ground_truth.get("error_type", "logic")
248
- category = ground_truth.get("category", "")
249
 
250
  logic_keywords = {
251
  "logic": ["join", "left join", "inner join", "having", "where", "group by",
@@ -256,10 +258,10 @@ def grade_medium(action: Action, ground_truth: dict) -> tuple[float, dict, str]:
256
  keywords_to_check = logic_keywords.get(error_type, logic_keywords["logic"])
257
  expl_lower = explanation.lower()
258
  keyword_hits = sum(1 for kw in keywords_to_check if kw in expl_lower)
259
- logic_score = max(min(keyword_hits * 0.05, 0.20), 0.001)
260
  score += logic_score
261
- breakdown["logic_flaw_identification"] = round(logic_score, 4)
262
- if logic_score > 0:
263
  feedback_parts.append("Shows understanding of the logic flaw.")
264
 
265
  # ── 3. Error location (0.15) ─────────────────────────────────
@@ -267,29 +269,29 @@ def grade_medium(action: Action, ground_truth: dict) -> tuple[float, dict, str]:
267
  expected_location = ground_truth.get("error_location", "")
268
  loc_score = _score_error_location(str(submitted_location), expected_location)
269
  score += loc_score
270
- breakdown["error_location"] = round(loc_score, 4)
271
 
272
  # ── 4. Explanation quality (0.15) ────────────────────────────
273
  expl_score = _score_explanation(explanation)
274
  score += expl_score
275
- breakdown["explanation"] = round(expl_score, 4)
276
 
277
  # ── 5. Confidence (0.05) ─────────────────────────────────────
278
  confidence = _safe_get(payload, "confidence", None)
279
  conf_score = _score_confidence(confidence)
280
  score += conf_score
281
- breakdown["confidence"] = round(conf_score, 4)
282
 
283
  # ── 6. Impact analysis bonus (0.05) ──────────────────────────
284
- impact = str(_safe_get(payload, "impact", "") or "")
285
  if len(impact.strip()) > 20:
286
  score += 0.05
287
  breakdown["impact_analysis"] = 0.05
288
  feedback_parts.append("Impact analysis provided.")
289
  else:
290
- breakdown["impact_analysis"] = 0.001
291
 
292
- final_score = round(max(min(score, 0.999), 0.001), 4)
293
  feedback = " ".join(feedback_parts) if feedback_parts else "No valid response provided."
294
  return final_score, breakdown, feedback
295
 
@@ -297,13 +299,19 @@ def grade_medium(action: Action, ground_truth: dict) -> tuple[float, dict, str]:
297
  def grade_hard(action: Action, ground_truth: dict) -> tuple[float, dict, str]:
298
  """
299
  Hard task grader β€” performance issues (N+1, missing index, cartesian, etc).
300
- Max score: 1.0 but frontier models expected ~0.10-0.20.
301
  Extremely strict β€” requires deep understanding of performance concepts.
302
  DETERMINISTIC: same input always returns same score.
303
  """
304
  if action is None or action.payload is None:
305
- return 0.001, {"error": "null_action"}, "No action provided."
306
- rubric = ground_truth.get("scoring_rubric", {})
 
 
 
 
 
 
307
 
308
  # ── 1. Query correctness (0.30) ──────────────────────────────
309
  submitted_query = (
@@ -314,7 +322,7 @@ def grade_hard(action: Action, ground_truth: dict) -> tuple[float, dict, str]:
314
  expected_query = ground_truth.get("fixed_query", "")
315
  similarity = _query_similarity(submitted_query, expected_query)
316
 
317
- if similarity >= 1.0:
318
  fix_score = 0.30
319
  feedback_parts.append("Perfectly optimized query.")
320
  elif similarity >= 0.85:
@@ -331,7 +339,7 @@ def grade_hard(action: Action, ground_truth: dict) -> tuple[float, dict, str]:
331
  feedback_parts.append("Query does not address the performance issue.")
332
 
333
  score += fix_score
334
- breakdown["query_correctness"] = round(fix_score, 4)
335
 
336
  # ── 2. Performance concept identification (0.30) ──────────────
337
  explanation = str(_safe_get(payload, "explanation", "") or _safe_get(payload, "change_made", "") or "")
@@ -348,25 +356,24 @@ def grade_hard(action: Action, ground_truth: dict) -> tuple[float, dict, str]:
348
  "window function": ["window function", "partition by", "row_number", "subquery filter", "where clause window"]
349
  }
350
 
351
- concept_score = 0.001
352
  for concept, keywords in performance_concept_map.items():
353
  if any(concept_part in issue_type for concept_part in concept.split()):
354
  hits = sum(1 for kw in keywords if kw in combined_text)
355
- concept_score = max(min(hits * 0.06, 0.30), 0.001)
356
  break
357
 
358
  score += concept_score
359
- breakdown["performance_concept"] = round(concept_score, 4)
360
- if concept_score > 0:
361
  feedback_parts.append("Demonstrates understanding of the performance issue.")
362
 
363
  # ── 3. Explanation depth (0.15) ───────────────────────────────
364
  expl_score = _score_explanation(explanation)
365
- # Hard tasks require deeper explanations β€” bonus for long explanations
366
  if len(explanation.strip()) > 150:
367
  expl_score = min(expl_score + 0.05, 0.15)
368
  score += expl_score
369
- breakdown["explanation_depth"] = round(expl_score, 4)
370
 
371
  # ── 4. Root cause analysis (0.10) ─────────────────────────────
372
  root_cause = str(_safe_get(payload, "root_cause", "") or "")
@@ -375,7 +382,7 @@ def grade_hard(action: Action, ground_truth: dict) -> tuple[float, dict, str]:
375
  breakdown["root_cause_analysis"] = 0.10
376
  feedback_parts.append("Root cause analysis provided.")
377
  else:
378
- breakdown["root_cause_analysis"] = 0.001
379
 
380
  # ── 5. Expected improvement (0.10) ────────────────────────────
381
  improvement = str(_safe_get(payload, "expected_improvement", "") or "")
@@ -384,17 +391,15 @@ def grade_hard(action: Action, ground_truth: dict) -> tuple[float, dict, str]:
384
  breakdown["expected_improvement"] = 0.10
385
  feedback_parts.append("Performance improvement estimate provided.")
386
  else:
387
- breakdown["expected_improvement"] = 0.001
388
 
389
  # ── 6. Confidence (0.05) ──────────────────────────────────────
390
  confidence = _safe_get(payload, "confidence", None)
391
  conf_score = _score_confidence(confidence)
392
  score += conf_score
393
- breakdown["confidence"] = round(conf_score, 4)
394
 
395
- # Hard cap: frontier model should score ~0.10-0.20
396
- # We do NOT artificially cap β€” the rubric naturally produces low scores
397
- final_score = round(max(min(score, 0.999), 0.001), 4)
398
  feedback = " ".join(feedback_parts) if feedback_parts else "Performance issue not identified."
399
  return final_score, breakdown, feedback
400
 
@@ -409,16 +414,13 @@ def grade(action: Action, task_id: str) -> tuple[float, dict, str]:
409
  Looks up ground truth, dispatches to correct grader by difficulty.
410
  ALWAYS returns (float, dict, str) β€” never crashes.
411
  """
412
- # Edge case: null action
413
  if action is None:
414
- return 0.001, {"error": "null_action"}, "No action provided."
415
 
416
- # Edge case: unknown task
417
  ground_truth = task_manager.get_ground_truth(task_id)
418
  if ground_truth is None:
419
- return 0.001, {"error": "unknown_task"}, f"Task '{task_id}' not found."
420
 
421
- # Dispatch by difficulty
422
  difficulty = ground_truth.get("id", "").split("_")[0]
423
 
424
  try:
@@ -429,7 +431,6 @@ def grade(action: Action, task_id: str) -> tuple[float, dict, str]:
429
  elif difficulty == "hard":
430
  return grade_hard(action, ground_truth)
431
  else:
432
- return 0.001, {"error": "unknown_difficulty"}, f"Unknown difficulty: {difficulty}"
433
  except Exception as e:
434
- # Never crash β€” return 0.0 with error info
435
- return 0.001, {"error": str(e)}, f"Grader error: {str(e)}"
 
4
 
5
  # HELPERS
6
 
7
+ SCORE_MIN = 0.001
8
+ SCORE_MAX = 0.999
9
+
10
+ def _clamp(value: float) -> float:
11
+ """Clamp a score to strictly (0, 1) β€” never 0.0 or 1.0."""
12
+ return round(max(min(float(value), SCORE_MAX), SCORE_MIN), 4)
13
+
14
 
15
  def _normalize(text: str) -> str:
16
  """Normalize SQL for comparison β€” lowercase, strip whitespace, collapse spaces."""
 
27
  def _score_explanation(explanation: str) -> float:
28
  """Score explanation quality by length and keyword richness."""
29
  if not explanation or not isinstance(explanation, str):
30
+ return SCORE_MIN
31
  explanation = explanation.strip()
32
  if len(explanation) < 10:
33
+ return SCORE_MIN
34
  if len(explanation) < 30:
35
  return 0.05
36
  if len(explanation) < 80:
 
45
  return 0.05
46
  except (TypeError, ValueError):
47
  pass
48
+ return SCORE_MIN
49
 
50
  def _query_similarity(submitted: str, expected: str) -> float:
51
  """
52
  Multi-level SQL similarity check.
53
+ Returns SCORE_MIN - SCORE_MAX based on how close the submitted query is to expected.
54
  Handles case, whitespace, and keyword-level matching.
55
  """
56
  s = _normalize(submitted)
57
  e = _normalize(expected)
58
 
59
  # Exact match after normalization
60
+ # NOTE: max similarity is SCORE_MAX (0.999), so threshold must be <= SCORE_MAX
61
  if s == e:
62
+ return SCORE_MAX
63
 
64
  # Tokenize and check keyword overlap
65
  s_tokens = set(s.split())
66
  e_tokens = set(e.split())
67
 
68
  if not e_tokens:
69
+ return SCORE_MIN
70
 
71
  overlap = len(s_tokens & e_tokens) / len(e_tokens)
72
 
 
77
 
78
  # Weighted combination
79
  similarity = round((overlap * 0.4) + (critical_score * 0.6), 4)
80
+ return _clamp(similarity)
81
 
82
  def _extract_critical_keywords(query: str) -> list[str]:
83
  """Extract SQL keywords that are critical to correctness."""
 
99
  def _score_error_type(submitted_type: str, expected_type: str) -> float:
100
  """Score for correctly identifying the error type."""
101
  if not submitted_type:
102
+ return SCORE_MIN
103
  s = submitted_type.strip().lower()
104
  e = expected_type.strip().lower()
105
  if s == e:
 
113
  for canonical, aliases in related.items():
114
  if e == canonical and any(alias in s for alias in aliases):
115
  return 0.05
116
+ return SCORE_MIN
117
 
118
  def _score_error_location(submitted_location: str, expected_location: str) -> float:
119
  """Score for correctly identifying WHERE in the query the error is."""
120
  if not submitted_location or not expected_location:
121
+ return SCORE_MIN
122
  s = submitted_location.strip().lower()
123
  e = expected_location.strip().lower()
124
  if s == e:
 
127
  e_words = set(e.split())
128
  s_words = set(s.split())
129
  overlap = len(e_words & s_words) / len(e_words) if e_words else 0.0
130
+ return _clamp(overlap * 0.10)
131
 
132
 
133
  # GRADERS PER DIFFICULTY
 
135
  def grade_easy(action: Action, ground_truth: dict) -> tuple[float, dict, str]:
136
  """
137
  Easy task grader β€” syntax errors.
138
+ Max score: SCORE_MAX
139
  Partial credit across: fix correctness, error location, error type, explanation, confidence.
140
  DETERMINISTIC: same input always returns same score.
141
  """
 
142
  if action is None or action.payload is None:
143
+ return SCORE_MIN, {"error": "null_action"}, "No action provided."
144
 
145
  payload = action.payload
146
  score = 0.0
147
  breakdown = {}
148
  feedback_parts = []
149
 
 
 
150
  # ── 1. Query fix correctness (0.50) ──────────────────────────
151
  submitted_query = _safe_get(payload, "fixed_query", "") or _safe_get(payload, "optimized_query", "")
152
  expected_query = ground_truth.get("fixed_query", "")
153
  similarity = _query_similarity(submitted_query, expected_query)
154
 
155
+ # Threshold uses SCORE_MAX (0.999) since that is the exact-match ceiling
156
+ if similarity >= SCORE_MAX:
157
  fix_score = 0.50
158
  feedback_parts.append("Correct fix applied.")
159
  elif similarity >= 0.75:
 
167
  feedback_parts.append("Fix is incorrect or not provided.")
168
 
169
  score += fix_score
170
+ breakdown["fix_correctness"] = _clamp(fix_score) if fix_score > 0 else SCORE_MIN
171
 
172
  # ── 2. Error location (0.15) ─────────────────────────────────
173
  submitted_location = _safe_get(payload, "error_location", "")
174
  expected_location = ground_truth.get("error_location", "")
175
  loc_score = _score_error_location(str(submitted_location), expected_location)
176
  score += loc_score
177
+ breakdown["error_location"] = _clamp(loc_score)
178
+ if loc_score > SCORE_MIN:
179
  feedback_parts.append("Correctly identified error location.")
180
 
181
  # ── 3. Error type (0.10) ─────────────────────────────────────
 
183
  expected_type = ground_truth.get("error_type", "syntax")
184
  type_score = _score_error_type(str(submitted_type), expected_type)
185
  score += type_score
186
+ breakdown["error_type"] = _clamp(type_score)
187
+ if type_score > SCORE_MIN:
188
  feedback_parts.append("Correctly identified error type.")
189
 
190
  # ── 4. Explanation quality (0.15) ────────────────────────────
191
  explanation = _safe_get(payload, "explanation", "") or _safe_get(payload, "change_made", "")
192
  expl_score = _score_explanation(str(explanation) if explanation else "")
193
  score += expl_score
194
+ breakdown["explanation"] = _clamp(expl_score)
195
+ if expl_score > SCORE_MIN:
196
  feedback_parts.append("Explanation provided.")
197
 
198
  # ── 5. Confidence (0.05) ─────────────────────────────────────
199
  confidence = _safe_get(payload, "confidence", None)
200
  conf_score = _score_confidence(confidence)
201
  score += conf_score
202
+ breakdown["confidence"] = _clamp(conf_score)
203
 
204
+ final_score = _clamp(score)
 
 
 
205
  feedback = " ".join(feedback_parts) if feedback_parts else "No valid response provided."
206
  return final_score, breakdown, feedback
207
 
 
209
  def grade_medium(action: Action, ground_truth: dict) -> tuple[float, dict, str]:
210
  """
211
  Medium task grader β€” logic errors (wrong JOINs, wrong aggregations, etc).
212
+ Max score: SCORE_MAX
213
  Higher bar: must correctly identify the logic flaw, not just syntax.
214
  DETERMINISTIC: same input always returns same score.
215
  """
216
  if action is None or action.payload is None:
217
+ return SCORE_MIN, {"error": "null_action"}, "No action provided."
218
 
219
  payload = action.payload
220
  score = 0.0
 
226
  expected_query = ground_truth.get("fixed_query", "")
227
  similarity = _query_similarity(submitted_query, expected_query)
228
 
229
+ if similarity >= SCORE_MAX:
230
  fix_score = 0.40
231
  feedback_parts.append("Correct fix applied.")
232
  elif similarity >= 0.80:
 
243
  feedback_parts.append("Fix is incorrect or missing.")
244
 
245
  score += fix_score
246
+ breakdown["fix_correctness"] = _clamp(fix_score) if fix_score > 0 else SCORE_MIN
247
 
248
  # ── 2. Identifies the logic flaw (0.20) ──────────────────────
249
  explanation = str(_safe_get(payload, "explanation", "") or _safe_get(payload, "change_made", "") or "")
250
  error_type = ground_truth.get("error_type", "logic")
 
251
 
252
  logic_keywords = {
253
  "logic": ["join", "left join", "inner join", "having", "where", "group by",
 
258
  keywords_to_check = logic_keywords.get(error_type, logic_keywords["logic"])
259
  expl_lower = explanation.lower()
260
  keyword_hits = sum(1 for kw in keywords_to_check if kw in expl_lower)
261
+ logic_score = _clamp(min(keyword_hits * 0.05, 0.20))
262
  score += logic_score
263
+ breakdown["logic_flaw_identification"] = _clamp(logic_score)
264
+ if logic_score > SCORE_MIN:
265
  feedback_parts.append("Shows understanding of the logic flaw.")
266
 
267
  # ── 3. Error location (0.15) ─────────────────────────────────
 
269
  expected_location = ground_truth.get("error_location", "")
270
  loc_score = _score_error_location(str(submitted_location), expected_location)
271
  score += loc_score
272
+ breakdown["error_location"] = _clamp(loc_score)
273
 
274
  # ── 4. Explanation quality (0.15) ────────────────────────────
275
  expl_score = _score_explanation(explanation)
276
  score += expl_score
277
+ breakdown["explanation"] = _clamp(expl_score)
278
 
279
  # ── 5. Confidence (0.05) ─────────────────────────────────────
280
  confidence = _safe_get(payload, "confidence", None)
281
  conf_score = _score_confidence(confidence)
282
  score += conf_score
283
+ breakdown["confidence"] = _clamp(conf_score)
284
 
285
  # ── 6. Impact analysis bonus (0.05) ──────────────────────────
286
+ impact = str(_safe_get(payload, "impact", "") or "")
287
  if len(impact.strip()) > 20:
288
  score += 0.05
289
  breakdown["impact_analysis"] = 0.05
290
  feedback_parts.append("Impact analysis provided.")
291
  else:
292
+ breakdown["impact_analysis"] = SCORE_MIN
293
 
294
+ final_score = _clamp(score)
295
  feedback = " ".join(feedback_parts) if feedback_parts else "No valid response provided."
296
  return final_score, breakdown, feedback
297
 
 
299
  def grade_hard(action: Action, ground_truth: dict) -> tuple[float, dict, str]:
300
  """
301
  Hard task grader β€” performance issues (N+1, missing index, cartesian, etc).
302
+ Max score: SCORE_MAX but frontier models expected ~0.10-0.20.
303
  Extremely strict β€” requires deep understanding of performance concepts.
304
  DETERMINISTIC: same input always returns same score.
305
  """
306
  if action is None or action.payload is None:
307
+ return SCORE_MIN, {"error": "null_action"}, "No action provided."
308
+
309
+ # ── FIX: initialize all variables before use ──────────────────
310
+ payload = action.payload
311
+ score = 0.0
312
+ breakdown = {}
313
+ feedback_parts = []
314
+ rubric = ground_truth.get("scoring_rubric", {})
315
 
316
  # ── 1. Query correctness (0.30) ──────────────────────────────
317
  submitted_query = (
 
322
  expected_query = ground_truth.get("fixed_query", "")
323
  similarity = _query_similarity(submitted_query, expected_query)
324
 
325
+ if similarity >= SCORE_MAX:
326
  fix_score = 0.30
327
  feedback_parts.append("Perfectly optimized query.")
328
  elif similarity >= 0.85:
 
339
  feedback_parts.append("Query does not address the performance issue.")
340
 
341
  score += fix_score
342
+ breakdown["query_correctness"] = _clamp(fix_score) if fix_score > 0 else SCORE_MIN
343
 
344
  # ── 2. Performance concept identification (0.30) ──────────────
345
  explanation = str(_safe_get(payload, "explanation", "") or _safe_get(payload, "change_made", "") or "")
 
356
  "window function": ["window function", "partition by", "row_number", "subquery filter", "where clause window"]
357
  }
358
 
359
+ concept_score = SCORE_MIN
360
  for concept, keywords in performance_concept_map.items():
361
  if any(concept_part in issue_type for concept_part in concept.split()):
362
  hits = sum(1 for kw in keywords if kw in combined_text)
363
+ concept_score = _clamp(min(hits * 0.06, 0.30))
364
  break
365
 
366
  score += concept_score
367
+ breakdown["performance_concept"] = _clamp(concept_score)
368
+ if concept_score > SCORE_MIN:
369
  feedback_parts.append("Demonstrates understanding of the performance issue.")
370
 
371
  # ── 3. Explanation depth (0.15) ───────────────────────────────
372
  expl_score = _score_explanation(explanation)
 
373
  if len(explanation.strip()) > 150:
374
  expl_score = min(expl_score + 0.05, 0.15)
375
  score += expl_score
376
+ breakdown["explanation_depth"] = _clamp(expl_score)
377
 
378
  # ── 4. Root cause analysis (0.10) ─────────────────────────────
379
  root_cause = str(_safe_get(payload, "root_cause", "") or "")
 
382
  breakdown["root_cause_analysis"] = 0.10
383
  feedback_parts.append("Root cause analysis provided.")
384
  else:
385
+ breakdown["root_cause_analysis"] = SCORE_MIN
386
 
387
  # ── 5. Expected improvement (0.10) ────────────────────────────
388
  improvement = str(_safe_get(payload, "expected_improvement", "") or "")
 
391
  breakdown["expected_improvement"] = 0.10
392
  feedback_parts.append("Performance improvement estimate provided.")
393
  else:
394
+ breakdown["expected_improvement"] = SCORE_MIN
395
 
396
  # ── 6. Confidence (0.05) ──────────────────────────────────────
397
  confidence = _safe_get(payload, "confidence", None)
398
  conf_score = _score_confidence(confidence)
399
  score += conf_score
400
+ breakdown["confidence"] = _clamp(conf_score)
401
 
402
+ final_score = _clamp(score)
 
 
403
  feedback = " ".join(feedback_parts) if feedback_parts else "Performance issue not identified."
404
  return final_score, breakdown, feedback
405
 
 
414
  Looks up ground truth, dispatches to correct grader by difficulty.
415
  ALWAYS returns (float, dict, str) β€” never crashes.
416
  """
 
417
  if action is None:
418
+ return SCORE_MIN, {"error": "null_action"}, "No action provided."
419
 
 
420
  ground_truth = task_manager.get_ground_truth(task_id)
421
  if ground_truth is None:
422
+ return SCORE_MIN, {"error": "unknown_task"}, f"Task '{task_id}' not found."
423
 
 
424
  difficulty = ground_truth.get("id", "").split("_")[0]
425
 
426
  try:
 
431
  elif difficulty == "hard":
432
  return grade_hard(action, ground_truth)
433
  else:
434
+ return SCORE_MIN, {"error": "unknown_difficulty"}, f"Unknown difficulty: {difficulty}"
435
  except Exception as e:
436
+ return SCORE_MIN, {"error": str(e)}, f"Grader error: {str(e)}"