AIMLxDIV commited on
Commit
e8fc831
·
unverified ·
2 Parent(s): ebde426d581a4f

Merge pull request #25 from ArshVermaGit/main

Browse files

feat: complete implementation of CodeReview Environment, Models, and Scenarios

app.py CHANGED
@@ -119,8 +119,8 @@ def submit_to_leaderboard(submission: SubmitScore):
119
  entries.append(new_entry)
120
  entries.sort(key=lambda x: x["score"], reverse=True)
121
  rank = entries.index(new_entry) + 1 # capture rank before slicing
122
- leaderboard[submission.task_id] = entries[:10]
123
- return {"status": "submitted", "rank": rank if rank <= 10 else None}
124
 
125
 
126
  @app.websocket("/ws/events")
 
119
  entries.append(new_entry)
120
  entries.sort(key=lambda x: x["score"], reverse=True)
121
  rank = entries.index(new_entry) + 1 # capture rank before slicing
122
+ leaderboard[submission.task_id] = entries[:5]
123
+ return {"status": "submitted", "rank": rank if rank <= 5 else None}
124
 
125
 
126
  @app.websocket("/ws/events")
codereview_env/env.py CHANGED
@@ -1,178 +1,177 @@
1
  from datetime import datetime, timezone
 
2
  from codereview_env.models import (
3
  TaskId, Action, Observation, StepResult, ResetResult,
4
- ActionType, ActionRecord, EpisodeResult, FileChanged
5
  )
6
  from codereview_env.scenarios import get_scenario
7
- from codereview_env.graders.grader_utils import find_best_match
8
  from codereview_env.graders.bug_grader import grade_bug_detection
9
  from codereview_env.graders.security_grader import grade_security_audit
10
  from codereview_env.graders.arch_grader import grade_architectural_review
11
 
12
-
13
  class CodeReviewEnv:
 
14
  TASK_MAX_STEPS = {
15
- TaskId.BUG_DETECTION: 10,
16
- TaskId.SECURITY_AUDIT: 15,
17
  TaskId.ARCHITECTURAL_REVIEW: 20,
18
  }
 
 
 
 
 
 
 
19
 
20
  def __init__(self):
21
- self._state = None
 
 
 
 
 
 
 
 
 
22
 
23
  def reset(self, task_id: TaskId, seed: int = 42) -> ResetResult:
24
- scenario = get_scenario(task_id, seed)
25
- self._state = {
26
- "task_id": task_id,
27
- "seed": seed,
28
- "scenario": scenario,
29
- "step_count": 0,
30
- "noise_budget": 5,
31
- "max_steps": self.TASK_MAX_STEPS[task_id],
32
- "history": [],
33
- "running_score": 0.0,
34
- "done": False,
35
- "issues_found": set(), # set of matched ground-truth issue IDs
36
- "false_positives": [] # list of FP action bodies
37
- }
38
  return ResetResult(
39
- observation=self._build_obs(),
40
  task_id=task_id,
41
  seed=seed,
42
- scenario_hash=scenario.hash
 
43
  )
44
 
45
  def step(self, action: Action) -> StepResult:
46
- if self._state is None or self._state["done"]:
47
- raise RuntimeError("Episode is done or not initialized. Call reset().")
48
-
49
- s = self._state
50
- s["step_count"] += 1
51
-
52
- # Record action in history (reward will be updated after calculation)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
53
  record = ActionRecord(
54
  action_type=action.action_type,
55
  body=action.body,
56
  filename=action.filename,
57
  line_number=action.line_number,
58
- severity=action.severity,
59
  category=action.category,
 
60
  verdict=action.verdict,
61
- reward=0.0,
62
  timestamp=datetime.now(timezone.utc).isoformat()
63
  )
64
- s["history"].append(record)
65
-
66
- # Apply action logic and compute incremental reward delta
67
- prev_score = s["running_score"]
68
- reward_delta = self._apply_action(action)
69
- s["running_score"] = prev_score + reward_delta
70
-
71
- # Update the history record with the actual reward
72
- record.reward = round(reward_delta, 4)
73
-
74
- # Check termination
75
- s["done"] = (
76
- action.action_type in (ActionType.APPROVE, ActionType.REQUEST_CHANGES)
77
- or s["step_count"] >= s["max_steps"]
78
- or s["noise_budget"] <= 0
79
- )
80
 
81
  return StepResult(
82
- observation=self._build_obs(),
83
- reward=round(reward_delta, 4), # ← incremental delta, NOT cumulative
84
- done=s["done"],
85
- info={
86
- "step": s["step_count"],
87
- "cumulative_score": round(s["running_score"], 4),
88
- "noise_budget": s["noise_budget"],
89
- "issues_found_count": len(s["issues_found"]),
90
- }
91
  )
92
 
93
- def _build_obs(self) -> Observation:
94
- s = self._state
95
- sc = s["scenario"]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
96
  return Observation(
97
- task_id=s["task_id"],
98
- scenario_hash=sc.hash,
99
- pr_title=sc.pr_title,
100
- pr_description=sc.pr_description,
101
- diff="\n".join([f.patch for f in sc.files_changed]),
102
- files_changed=sc.files_changed,
103
- step_count=s["step_count"],
104
- max_steps=s["max_steps"],
105
- noise_budget=s["noise_budget"],
106
- max_noise_budget=5,
107
- issues_flagged=len(s["issues_found"]),
108
- done=s["done"]
109
  )
110
 
111
- def _apply_action(self, action: Action) -> float:
112
- """
113
- Compute the incremental reward delta for this single action.
114
- """
115
- s = self._state
116
- sc = s["scenario"]
117
-
118
- if action.action_type == ActionType.FLAG_ISSUE:
119
- matched_gt = find_best_match(action, sc.ground_truth_issues, s["issues_found"])
120
- if matched_gt:
121
- s["issues_found"].add(matched_gt.id)
122
- # Recalculate full grader score and return delta
123
- new_score = self._grade(sc, s)
124
- reward_delta = new_score - s["running_score"]
125
- return max(0.0, reward_delta) # finding a real issue is always non-negative
126
- else:
127
- # False positive: consume noise budget and penalize
128
- s["noise_budget"] -= 1
129
- s["false_positives"].append(action.body)
130
- return -0.05
131
-
132
- elif action.action_type in (ActionType.APPROVE, ActionType.REQUEST_CHANGES):
133
- # Terminal: compute full final score delta
134
- new_score = self._grade(sc, s)
135
- reward_delta = new_score - s["running_score"]
136
- return reward_delta
137
-
138
- # comment / ask_question — no reward signal
139
- return 0.0
140
-
141
- def _grade(self, sc, s) -> float:
142
- """Route to the right grader based on task_id."""
143
- if s["task_id"] == TaskId.BUG_DETECTION:
144
- return grade_bug_detection(sc, s["history"])
145
- elif s["task_id"] == TaskId.SECURITY_AUDIT:
146
- return grade_security_audit(sc, s["history"])
147
- else:
148
- return grade_architectural_review(sc, s["history"])
149
-
150
  def get_final_result(self) -> EpisodeResult:
151
- s = self._state
152
- sc = s["scenario"]
153
-
154
- all_gt_ids = {gt.id for gt in sc.ground_truth_issues}
155
- missed_ids = list(all_gt_ids - s["issues_found"])
156
- final_score = self._grade(sc, s)
157
-
158
- terminated_reason = ""
159
- if s["done"]:
160
- if s["noise_budget"] <= 0:
161
- terminated_reason = "noise_exhausted"
162
- elif s["history"] and s["history"][-1].action_type in (ActionType.APPROVE, ActionType.REQUEST_CHANGES):
163
- terminated_reason = "terminal_action"
164
- else:
165
- terminated_reason = "max_steps"
166
-
167
  return EpisodeResult(
168
- task_id=s["task_id"],
169
- scenario_hash=sc.hash,
170
- seed=s["seed"],
 
171
  final_score=round(final_score, 4),
172
- steps_taken=s["step_count"],
173
- issues_found=len(s["issues_found"]),
174
- issues_total=len(sc.ground_truth_issues),
175
- noise_penalties=5 - s["noise_budget"],
176
- history=s["history"],
177
- terminated_reason=terminated_reason
178
  )
 
 
1
  from datetime import datetime, timezone
2
+ from typing import List, Optional, Set
3
  from codereview_env.models import (
4
  TaskId, Action, Observation, StepResult, ResetResult,
5
+ ActionType, ActionRecord, EpisodeResult, Severity, GroundTruthIssue
6
  )
7
  from codereview_env.scenarios import get_scenario
 
8
  from codereview_env.graders.bug_grader import grade_bug_detection
9
  from codereview_env.graders.security_grader import grade_security_audit
10
  from codereview_env.graders.arch_grader import grade_architectural_review
11
 
 
12
  class CodeReviewEnv:
13
+ MAX_NOISE_BUDGET = 5
14
  TASK_MAX_STEPS = {
15
+ TaskId.BUG_DETECTION: 10,
16
+ TaskId.SECURITY_AUDIT: 15,
17
  TaskId.ARCHITECTURAL_REVIEW: 20,
18
  }
19
+ SEVERITY_WEIGHTS = {
20
+ Severity.CRITICAL: 1.0,
21
+ Severity.HIGH: 0.8,
22
+ Severity.MEDIUM: 0.5,
23
+ Severity.LOW: 0.2,
24
+ Severity.INFO: 0.0,
25
+ }
26
 
27
  def __init__(self):
28
+ self.task_id: Optional[TaskId] = None
29
+ self.seed: int = 42
30
+ self.scenario = None
31
+ self.step_count: int = 0
32
+ self.noise_budget: int = self.MAX_NOISE_BUDGET
33
+ self.history: List[ActionRecord] = []
34
+ self.matched_issue_ids: Set[str] = set()
35
+ self.done: bool = False
36
+ self.terminated_reason: str = ""
37
+ self.episode_id: str = ""
38
 
39
  def reset(self, task_id: TaskId, seed: int = 42) -> ResetResult:
40
+ self.scenario = get_scenario(task_id, seed)
41
+ self.task_id = task_id
42
+ self.seed = seed
43
+ self.step_count = 0
44
+ self.noise_budget = self.MAX_NOISE_BUDGET
45
+ self.history = []
46
+ self.matched_issue_ids = set()
47
+ self.done = False
48
+ self.terminated_reason = ""
49
+
50
+ obs = self._build_observation()
 
 
 
51
  return ResetResult(
 
52
  task_id=task_id,
53
  seed=seed,
54
+ scenario_hash=self.scenario.hash,
55
+ observation=obs
56
  )
57
 
58
  def step(self, action: Action) -> StepResult:
59
+ if self.done:
60
+ raise ValueError("Episode is already finished")
61
+
62
+ self.step_count += 1
63
+ reward = 0.0
64
+
65
+ # Determine terminal state and reward
66
+ if action.action_type in (ActionType.APPROVE, ActionType.REQUEST_CHANGES):
67
+ self.done = True
68
+ self.terminated_reason = "terminal_action"
69
+ reward = 0.0 # Grader handles final score
70
+
71
+ elif action.action_type == ActionType.FLAG_ISSUE:
72
+ match = None
73
+ for issue in self.scenario.ground_truth_issues:
74
+ if self._is_match(action, issue):
75
+ match = issue
76
+ break
77
+
78
+ if match:
79
+ if match.id not in self.matched_issue_ids:
80
+ reward = self.SEVERITY_WEIGHTS.get(match.severity, 0.0)
81
+ self.matched_issue_ids.add(match.id)
82
+ else:
83
+ # Already matched: penalty
84
+ reward = -0.05
85
+ self.noise_budget -= 1
86
+ if self.noise_budget <= 0:
87
+ self.done = True
88
+ self.terminated_reason = "noise_exhausted"
89
+ else:
90
+ # False positive: penalty
91
+ reward = -0.05
92
+ self.noise_budget -= 1
93
+ if self.noise_budget <= 0:
94
+ self.done = True
95
+ self.terminated_reason = "noise_exhausted"
96
+
97
+ # Max steps check
98
+ max_steps = self.TASK_MAX_STEPS.get(self.task_id, 10)
99
+ if not self.done and self.step_count >= max_steps:
100
+ self.done = True
101
+ self.terminated_reason = "max_steps"
102
+
103
+ # Record action
104
  record = ActionRecord(
105
  action_type=action.action_type,
106
  body=action.body,
107
  filename=action.filename,
108
  line_number=action.line_number,
 
109
  category=action.category,
110
+ severity=action.severity,
111
  verdict=action.verdict,
112
+ reward=float(reward),
113
  timestamp=datetime.now(timezone.utc).isoformat()
114
  )
115
+ self.history.append(record)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
116
 
117
  return StepResult(
118
+ observation=self._build_observation(),
119
+ reward=float(reward),
120
+ done=self.done,
121
+ info={"terminated_reason": self.terminated_reason}
 
 
 
 
 
122
  )
123
 
124
+ def _is_match(self, action: Action, issue: GroundTruthIssue) -> bool:
125
+ if action.filename != issue.filename:
126
+ return False
127
+ if action.line_number is None:
128
+ return False
129
+ if abs(action.line_number - issue.line_number) > 3:
130
+ return False
131
+ if action.category != issue.category:
132
+ return False
133
+
134
+ body_lower = (action.body or "").lower()
135
+ return any(kw.lower() in body_lower for kw in issue.keywords)
136
+
137
+ def _build_observation(self) -> Observation:
138
+ max_steps = self.TASK_MAX_STEPS.get(self.task_id, 10)
139
+ diff = "\n".join(f.patch for f in self.scenario.files_changed)
140
+
141
  return Observation(
142
+ task_id=self.task_id,
143
+ scenario_hash=self.scenario.hash,
144
+ pr_title=self.scenario.pr_title,
145
+ pr_description=self.scenario.pr_description,
146
+ diff=diff,
147
+ files_changed=self.scenario.files_changed,
148
+ step_count=self.step_count,
149
+ max_steps=max_steps,
150
+ noise_budget=self.noise_budget,
151
+ max_noise_budget=self.MAX_NOISE_BUDGET,
152
+ issues_flagged=len(self.matched_issue_ids),
153
+ done=self.done
154
  )
155
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
156
  def get_final_result(self) -> EpisodeResult:
157
+ if self.task_id == TaskId.BUG_DETECTION:
158
+ final_score = grade_bug_detection(self.scenario, self.history)
159
+ elif self.task_id == TaskId.SECURITY_AUDIT:
160
+ final_score = grade_security_audit(self.scenario, self.history)
161
+ else:
162
+ final_score = grade_architectural_review(self.scenario, self.history)
163
+
 
 
 
 
 
 
 
 
 
164
  return EpisodeResult(
165
+ episode_id=self.episode_id,
166
+ task_id=self.task_id,
167
+ scenario_hash=self.scenario.hash,
168
+ seed=self.seed,
169
  final_score=round(final_score, 4),
170
+ steps_taken=self.step_count,
171
+ issues_found=len(self.matched_issue_ids),
172
+ issues_total=len(self.scenario.ground_truth_issues),
173
+ noise_penalties=self.MAX_NOISE_BUDGET - self.noise_budget,
174
+ history=self.history,
175
+ terminated_reason=self.terminated_reason
176
  )
177
+
codereview_env/graders/arch_grader.py CHANGED
@@ -1,39 +1,61 @@
1
  from typing import List
2
  from codereview_env.models import Scenario, ActionRecord, Category, ActionType, Verdict
3
- from codereview_env.graders.grader_utils import find_best_match
4
 
5
  def grade_architectural_review(scenario: Scenario, history: List[ActionRecord]) -> float:
6
- """Grade architectural review: 0.6 * issue_score + 0.2 * verdict_score + 0.2 * quality_score."""
7
- critical_issues = scenario.ground_truth_issues
8
- flagged_actions = [r for r in history if r.category == Category.ARCHITECTURE]
 
9
 
10
- # 1. Issue Detection (0.6)
11
- found_count = 0
12
- already_matched = set()
13
- quality_bonus = 0.0
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
14
 
15
- for action in flagged_actions:
16
- match = find_best_match(action, critical_issues, already_matched)
17
- if match:
18
- found_count += 1
19
- already_matched.add(match.id)
20
- # Quality: + bonus if body is descriptive (> 80 chars)
21
- if len(action.body) > 80:
22
- quality_bonus += 0.05
 
 
 
 
 
 
23
 
24
- issue_score = (found_count / len(critical_issues)) if critical_issues else 1.0
25
-
26
- # 2. Verdict Correctness (0.2)
27
- verdict_score = 0.0
28
- final_action = history[-1] if history else None
29
- if final_action and final_action.action_type in (ActionType.APPROVE, ActionType.REQUEST_CHANGES):
30
- # Check against the first required verdict in GT
31
- required_verdicts = [gt.required_verdict for gt in critical_issues if gt.required_verdict]
32
- if required_verdicts and final_action.verdict == required_verdicts[0]:
33
- verdict_score = 1.0
34
- elif not required_verdicts and final_action.verdict == Verdict.LGTM:
35
- verdict_score = 1.0
36
 
37
- # 3. Final weighted calculation
38
- score = 0.6 * issue_score + 0.2 * verdict_score + min(0.2, quality_bonus)
39
- return round(min(1.0, score), 4)
 
1
  from typing import List
2
  from codereview_env.models import Scenario, ActionRecord, Category, ActionType, Verdict
 
3
 
4
  def grade_architectural_review(scenario: Scenario, history: List[ActionRecord]) -> float:
5
+ if not history:
6
+ return 0.0
7
+
8
+ flag_actions = [a for a in history if a.action_type == ActionType.FLAG_ISSUE]
9
 
10
+ total_issues = len(scenario.ground_truth_issues)
11
+ if total_issues == 0:
12
+ return 0.0
13
+
14
+ # 1. Match issues and calculate issue_score_avg
15
+ matched_gt_indices = set()
16
+ for i, truth in enumerate(scenario.ground_truth_issues):
17
+ if truth.category != Category.ARCHITECTURE:
18
+ continue
19
+
20
+ for action in flag_actions:
21
+ # Match criteria: filename, line +- 5, category ARCHITECTURE, keyword match
22
+ if (action.filename == truth.filename and
23
+ action.line_number is not None and
24
+ abs(action.line_number - truth.line_number) <= 5 and
25
+ action.category == Category.ARCHITECTURE):
26
+
27
+ body_lower = (action.body or "").lower()
28
+ if any(kw.lower() in body_lower for kw in truth.keywords):
29
+ matched_gt_indices.add(i)
30
+ break
31
+
32
+ issue_score_avg = len(matched_gt_indices) / total_issues
33
 
34
+ # 2. Verdict Grading
35
+ terminal_action = None
36
+ for action in history:
37
+ if action.action_type in (ActionType.APPROVE, ActionType.REQUEST_CHANGES):
38
+ terminal_action = action
39
+ break # Use the first terminal action
40
+
41
+ verdict_scores = []
42
+ for truth in scenario.ground_truth_issues:
43
+ if truth.required_verdict:
44
+ score = 1.0 if (terminal_action and terminal_action.verdict == truth.required_verdict) else 0.0
45
+ verdict_scores.append(score)
46
+
47
+ verdict_avg = sum(verdict_scores) / len(verdict_scores) if verdict_scores else 0.0
48
 
49
+ # 3. Quality Score
50
+ max_body_len = 0
51
+ for action in flag_actions:
52
+ max_body_len = max(max_body_len, len(action.body or ""))
53
+
54
+ quality_score = 0.0
55
+ if max_body_len > 20:
56
+ quality_score = min(1.0, max_body_len / 200)
57
+
58
+ # 4. Final Weighted Calculation
59
+ final_score = 0.6 * issue_score_avg + 0.2 * verdict_avg + 0.2 * quality_score
60
+ return float(max(0.0, min(1.0, final_score)))
61
 
 
 
 
codereview_env/graders/bug_grader.py CHANGED
@@ -1,25 +1,66 @@
1
  from typing import List
2
- from codereview_env.models import Scenario, ActionRecord, Category
3
- from codereview_env.graders.grader_utils import find_best_match
4
 
5
  def grade_bug_detection(scenario: Scenario, history: List[ActionRecord]) -> float:
6
- """Grade bug detection: 0.4 * coverage + 0.6 * precision."""
7
- flagged_actions = [r for r in history if r.category == Category.BUG]
8
- if not scenario.ground_truth_issues:
9
- return 1.0 if not flagged_actions else 0.0
 
 
 
 
 
 
10
 
11
- found_ids = set()
12
- matches = 0
13
- already_matched = set()
14
 
15
- for action in flagged_actions:
16
- match = find_best_match(action, scenario.ground_truth_issues, already_matched)
17
- if match:
18
- matches += 1
19
- found_ids.add(match.id)
20
- already_matched.add(match.id)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
21
 
22
- recall = len(found_ids) / len(scenario.ground_truth_issues)
23
- precision = matches / len(flagged_actions) if flagged_actions else 0.0
 
 
 
 
 
 
 
24
 
25
- return round(0.7 * recall + 0.3 * precision, 4)
 
 
 
 
 
 
 
1
  from typing import List
2
+ from codereview_env.models import Scenario, ActionRecord, Category, Severity, ActionType
 
3
 
4
  def grade_bug_detection(scenario: Scenario, history: List[ActionRecord]) -> float:
5
+ if not history:
6
+ return 0.0
7
+
8
+ flag_actions = [a for a in history if a.action_type == ActionType.FLAG_ISSUE]
9
+ if not flag_actions:
10
+ return 0.0
11
+
12
+ total_issues = len(scenario.ground_truth_issues)
13
+ if total_issues == 0:
14
+ return 0.0
15
 
16
+ matched_gt_indices = set()
17
+ used_action_indices = set()
18
+ issue_scores = []
19
 
20
+ for i, truth in enumerate(scenario.ground_truth_issues):
21
+ if truth.category != Category.BUG:
22
+ continue
23
+
24
+ # Try to find a matching action for this ground truth issue
25
+ best_match_idx = -1
26
+ for j, action in enumerate(flag_actions):
27
+ if j in used_action_indices:
28
+ continue
29
+
30
+ # Match criteria: filename, line +- 3, category BUG, >= 1 keyword
31
+ if (action.filename == truth.filename and
32
+ action.line_number is not None and
33
+ abs(action.line_number - truth.line_number) <= 3 and
34
+ action.category == Category.BUG):
35
+
36
+ body_lower = (action.body or "").lower()
37
+ if any(kw.lower() in body_lower for kw in truth.keywords):
38
+ best_match_idx = j
39
+ break
40
+
41
+ if best_match_idx != -1:
42
+ action = flag_actions[best_match_idx]
43
+ used_action_indices.add(best_match_idx)
44
+ matched_gt_indices.add(i)
45
+
46
+ # Calculate issue score
47
+ sev_diff = abs(Severity.ordinal(truth.severity) - Severity.ordinal(action.severity))
48
+ sev_score = max(0.0, 1.0 - sev_diff * 0.3)
49
 
50
+ body_lower = (action.body or "").lower()
51
+ match_count = sum(1 for kw in truth.keywords if kw.lower() in body_lower)
52
+ kw_score = match_count / len(truth.keywords)
53
+
54
+ issue_score = 0.5 * kw_score + 0.5 * sev_score
55
+ issue_scores.append(issue_score)
56
+
57
+ coverage = len(matched_gt_indices) / total_issues
58
+ avg_issue_score = sum(issue_scores) / len(issue_scores) if issue_scores else 0.0
59
 
60
+ total_flags = len(flag_actions)
61
+ unmatched_flags = total_flags - len(used_action_indices)
62
+ precision_penalty = unmatched_flags / max(1, total_flags)
63
+
64
+ final_score = 0.4 * coverage + 0.6 * avg_issue_score - 0.1 * precision_penalty
65
+ return float(max(0.0, min(1.0, final_score)))
66
+
codereview_env/graders/security_grader.py CHANGED
@@ -1,37 +1,56 @@
1
  from typing import List
2
- from codereview_env.models import Scenario, ActionRecord, Category, Severity
3
- from codereview_env.graders.grader_utils import find_best_match, keyword_overlap
4
-
5
- def severity_to_int(sev: Severity) -> int:
6
- mapping = {
7
- Severity.LOW: 1,
8
- Severity.MEDIUM: 2,
9
- Severity.HIGH: 3,
10
- Severity.CRITICAL: 4
11
- }
12
- return mapping.get(sev, 0)
13
 
14
  def grade_security_audit(scenario: Scenario, history: List[ActionRecord]) -> float:
15
- """Grade security audit: 0.7 * correct_severity + 0.3 * keyword_accuracy."""
16
- flagged_actions = [r for r in history if r.category == Category.SECURITY]
17
- if not scenario.ground_truth_issues:
18
- return 1.0 if not flagged_actions else 0.0
 
 
19
 
20
- total_score = 0.0
21
- already_matched = set()
22
 
23
- for action in flagged_actions:
24
- match = find_best_match(action, scenario.ground_truth_issues, already_matched)
25
- if match:
26
- # Correct Severity: 1.0 if match, else penalty per level
27
- sev_diff = abs(severity_to_int(action.severity) - severity_to_int(match.severity))
28
- sev_score = max(0.0, 1.0 - (sev_diff * 0.3))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
29
 
30
- # Keyword Accuracy
31
- kw_score = keyword_overlap(action.body, match.keywords)
 
32
 
33
- total_score += 0.7 * sev_score + 0.3 * kw_score
34
- already_matched.add(match.id)
 
 
35
 
36
- # Normalize by number of GT issues
37
- return round(min(1.0, total_score / len(scenario.ground_truth_issues)), 4) if scenario.ground_truth_issues else 1.0
 
 
 
 
 
 
 
 
1
  from typing import List
2
+ from codereview_env.models import Scenario, ActionRecord, Category, Severity, ActionType
 
 
 
 
 
 
 
 
 
 
3
 
4
  def grade_security_audit(scenario: Scenario, history: List[ActionRecord]) -> float:
5
+ if not history:
6
+ return 0.0
7
+
8
+ flag_actions = [a for a in history if a.action_type == ActionType.FLAG_ISSUE]
9
+ if not flag_actions:
10
+ return 0.0
11
 
12
+ matched_issue_scores = []
13
+ used_action_indices = set()
14
 
15
+ for truth in scenario.ground_truth_issues:
16
+ if truth.category != Category.SECURITY:
17
+ continue
18
+
19
+ best_match_idx = -1
20
+ for j, action in enumerate(flag_actions):
21
+ if j in used_action_indices:
22
+ continue
23
+
24
+ # Match criteria: filename, line +- 3, category SECURITY, >= 1 keyword
25
+ if (action.filename == truth.filename and
26
+ action.line_number is not None and
27
+ abs(action.line_number - truth.line_number) <= 3 and
28
+ action.category == Category.SECURITY):
29
+
30
+ body_lower = (action.body or "").lower()
31
+ if any(kw.lower() in body_lower for kw in truth.keywords):
32
+ best_match_idx = j
33
+ break
34
+
35
+ if best_match_idx != -1:
36
+ action = flag_actions[best_match_idx]
37
+ used_action_indices.add(best_match_idx)
38
 
39
+ # Calculate issue score
40
+ sev_diff = abs(Severity.ordinal(truth.severity) - Severity.ordinal(action.severity))
41
+ sev_score = max(0.0, 1.0 - sev_diff * 0.3)
42
 
43
+ body_lower = (action.body or "").lower()
44
+ match_count = sum(1 for kw in truth.keywords if kw.lower() in body_lower)
45
+ kw_threshold = max(4, len(truth.keywords))
46
+ kw_score = match_count / kw_threshold
47
 
48
+ issue_score = 0.7 * sev_score + 0.3 * kw_score
49
+ matched_issue_scores.append(issue_score)
50
+
51
+ if not matched_issue_scores:
52
+ return 0.0
53
+
54
+ final_score = sum(matched_issue_scores) / len(matched_issue_scores)
55
+ return float(round(max(0.0, min(1.0, final_score)), 4))
56
+
openenv.yaml CHANGED
@@ -45,22 +45,10 @@ tasks:
45
 
46
  grading:
47
  type: "deterministic"
48
- score_range: [0.0, 1.0]
49
- partial_credit: true
50
- metrics:
51
- - name: "recall"
52
- description: "Fraction of ground-truth issues correctly identified"
53
- weight: 0.7
54
- - name: "precision"
55
- description: "Fraction of flagged issues that are true positives"
56
- weight: 0.3
57
- - name: "severity_accuracy"
58
- description: "How close the flagged severity is to ground truth (security task)"
59
- weight: 0.7
60
- - name: "keyword_accuracy"
61
- description: "Semantic overlap between agent description and issue keywords"
62
- weight: 0.3
63
- - name: "verdict_correctness"
64
- description: "Whether final approve/request_changes decision is correct (arch task)"
65
- weight: 0.2
66
 
 
45
 
46
  grading:
47
  type: "deterministic"
48
+ issue_matching:
49
+ coverage_weight: 0.4
50
+ precision_weight: 0.6
51
+ quality_scoring:
52
+ severity_weight: 0.7
53
+ keyword_weight: 0.3
 
 
 
 
 
 
 
 
 
 
 
 
54