Spaces:
Sleeping
Sleeping
Commit ·
902cd29
1
Parent(s): 920d80b
Add training scripts and utilities for NodeAudit and GraphReview
Browse files- Implement `train_lora.py` for LoRA training pipeline, including dataset preparation and model training.
- Create `trajectory_collector.py` to collect trajectories and DPO pairs from the CodeReview environment.
- Develop `training_graph.py` for visualizing training outcomes and module performance.
- Introduce `inference.py` as an example inference script for LLM interactions.
- Add scripts for cloning training repositories and seeding databases from the training corpus.
- Implement `verify_all.py` to run verification scripts in the code-review environment.
- .gitignore +5 -1
- Dockerfile +2 -0
- code-review-env/.env.example +4 -5
- code-review-env/Dockerfile +2 -0
- code-review-env/analyzers/ast_checker.py +249 -0
- code-review-env/analyzers/pipeline.py +154 -197
- code-review-env/db/migrations.py +5 -0
- code-review-env/db/models.py +2 -0
- code-review-env/db/schema.py +17 -0
- code-review-env/db/store.py +48 -0
- code-review-env/env/observation_builder.py +6 -4
- code-review-env/env/runtime_config.py +5 -5
- code-review-env/graders/base_grader.py +2 -1
- code-review-env/graders/easy_grader.py +4 -4
- code-review-env/graders/hard_grader.py +2 -12
- code-review-env/graders/medium_grader.py +1 -1
- code-review-env/inference.py +140 -244
- code-review-env/inference_training.py +107 -34
- code-review-env/llm/__init__.py +10 -0
- code-review-env/llm/agent_runner.py +164 -0
- code-review-env/llm/thinking_judge.py +131 -0
- code-review-env/outputs/NodeAudit_graph.html +0 -0
- code-review-env/outputs/tr-20260409110822_dpo_pairs.jsonl +0 -0
- code-review-env/outputs/tr-20260409110822_trajectories.jsonl +1 -0
- code-review-env/outputs/training/dataset.latest.jsonl +7 -0
- code-review-env/outputs/training/dpo_pairs.jsonl +0 -0
- code-review-env/outputs/verification_report.txt +41 -0
- code-review-env/outputs/weights/gemma4e4b.manifest.json +7 -0
- code-review-env/parser/linter.py +23 -15
- code-review-env/pyproject.toml +9 -3
- code-review-env/requirements-amd-rocm.txt +10 -0
- code-review-env/requirements.txt +9 -4
- code-review-env/scripts/clone_training_repos.sh +40 -0
- code-review-env/scripts/seed_training_corpus.sh +47 -0
- code-review-env/scripts/verify_all.py +525 -0
- code-review-env/server/static/index.html +1 -1
- code-review-env/training/__init__.py +10 -1
- code-review-env/training/train_lora.py +196 -0
- code-review-env/training/trajectory_collector.py +312 -0
- code-review-env/visualizer/__init__.py +2 -0
- code-review-env/visualizer/report_generator.py +1 -1
- code-review-env/visualizer/training_graph.py +191 -0
- inf.py +188 -0
- scripts/clone_training_repos.sh +48 -0
- scripts/seed_training_corpus.sh +71 -0
- scripts/verify_all.py +14 -0
.gitignore
CHANGED
|
@@ -5,4 +5,8 @@ __pycache__/
|
|
| 5 |
code-review-env/code_review_env.db
|
| 6 |
Whatsapp-Bot
|
| 7 |
OpenEnv
|
| 8 |
-
Models
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 5 |
code-review-env/code_review_env.db
|
| 6 |
Whatsapp-Bot
|
| 7 |
OpenEnv
|
| 8 |
+
Models
|
| 9 |
+
training_corpus/
|
| 10 |
+
outputs/corpus_dbs/
|
| 11 |
+
training_corpus
|
| 12 |
+
code-review-env/unsloth_compiled_cache/
|
Dockerfile
CHANGED
|
@@ -1,6 +1,7 @@
|
|
| 1 |
FROM python:3.11-slim
|
| 2 |
|
| 3 |
WORKDIR /app/code-review-env
|
|
|
|
| 4 |
COPY code-review-env/requirements.txt /app/code-review-env/requirements.txt
|
| 5 |
RUN pip install --no-cache-dir -r requirements.txt
|
| 6 |
COPY code-review-env /app/code-review-env
|
|
@@ -8,4 +9,5 @@ COPY code-review-env /app/code-review-env
|
|
| 8 |
ENV GRAPHREVIEW_SOURCE_ROOT=/app/code-review-env/sample_project
|
| 9 |
RUN python -m db.seed sample_project/ --force
|
| 10 |
|
|
|
|
| 11 |
CMD ["uvicorn", "server.app:app", "--app-dir", "/app/code-review-env", "--host", "0.0.0.0", "--port", "7860"]
|
|
|
|
| 1 |
FROM python:3.11-slim
|
| 2 |
|
| 3 |
WORKDIR /app/code-review-env
|
| 4 |
+
RUN apt-get update && apt-get install -y --no-install-recommends git curl nodejs npm && rm -rf /var/lib/apt/lists/*
|
| 5 |
COPY code-review-env/requirements.txt /app/code-review-env/requirements.txt
|
| 6 |
RUN pip install --no-cache-dir -r requirements.txt
|
| 7 |
COPY code-review-env /app/code-review-env
|
|
|
|
| 9 |
ENV GRAPHREVIEW_SOURCE_ROOT=/app/code-review-env/sample_project
|
| 10 |
RUN python -m db.seed sample_project/ --force
|
| 11 |
|
| 12 |
+
EXPOSE 7860
|
| 13 |
CMD ["uvicorn", "server.app:app", "--app-dir", "/app/code-review-env", "--host", "0.0.0.0", "--port", "7860"]
|
code-review-env/.env.example
CHANGED
|
@@ -1,8 +1,7 @@
|
|
| 1 |
API_BASE_URL=http://localhost:11434/v1
|
| 2 |
-
MODEL_NAME=
|
| 3 |
HF_TOKEN=your_token_here
|
| 4 |
-
|
| 5 |
-
GRAPHREVIEW_QWEN_GGUF_PATH=/usr/share/ollama/.ollama/models/blobs/sha256-509287f78cb4d4cf6b3843734733b914b2c158e43e22a7f4bf5e963800894d3c
|
| 6 |
|
| 7 |
|
| 8 |
//Old config settings
|
|
@@ -33,8 +32,8 @@ GRAPHREVIEW_PROGRESS=true
|
|
| 33 |
GRAPHREVIEW_LLM_PROVIDER=ollama_openai_compat
|
| 34 |
GRAPHREVIEW_LLM_BASE_URL=http://localhost:11434/v1
|
| 35 |
GRAPHREVIEW_LLM_API_KEY=ollama
|
| 36 |
-
GRAPHREVIEW_LLM_MODEL_AGENT=
|
| 37 |
-
GRAPHREVIEW_LLM_MODEL_JUDGE=
|
| 38 |
|
| 39 |
# Hard grader - primary judge
|
| 40 |
GRAPHREVIEW_JUDGE_ENABLED=true
|
|
|
|
| 1 |
API_BASE_URL=http://localhost:11434/v1
|
| 2 |
+
MODEL_NAME=Qwen/Qwen2.5-Coder-7B-Instruct
|
| 3 |
HF_TOKEN=your_token_here
|
| 4 |
+
GRAPHREVIEW_GEMMA_GGUF_PATH=Models/gemma-4-E4B-it-Q6_K.gguf
|
|
|
|
| 5 |
|
| 6 |
|
| 7 |
//Old config settings
|
|
|
|
| 32 |
GRAPHREVIEW_LLM_PROVIDER=ollama_openai_compat
|
| 33 |
GRAPHREVIEW_LLM_BASE_URL=http://localhost:11434/v1
|
| 34 |
GRAPHREVIEW_LLM_API_KEY=ollama
|
| 35 |
+
GRAPHREVIEW_LLM_MODEL_AGENT=unsloth/gemma-4-E4B-it-GGUF
|
| 36 |
+
GRAPHREVIEW_LLM_MODEL_JUDGE=Qwen/Qwen2.5-7B-Instruct
|
| 37 |
|
| 38 |
# Hard grader - primary judge
|
| 39 |
GRAPHREVIEW_JUDGE_ENABLED=true
|
code-review-env/Dockerfile
CHANGED
|
@@ -1,8 +1,10 @@
|
|
| 1 |
FROM python:3.11-slim
|
| 2 |
|
| 3 |
WORKDIR /app
|
|
|
|
| 4 |
COPY requirements.txt /app/
|
| 5 |
RUN pip install --no-cache-dir -r requirements.txt
|
| 6 |
COPY . /app
|
| 7 |
RUN python -m db.seed sample_project/ --force
|
|
|
|
| 8 |
CMD ["uvicorn", "server.app:app", "--host", "0.0.0.0", "--port", "7860"]
|
|
|
|
| 1 |
FROM python:3.11-slim
|
| 2 |
|
| 3 |
WORKDIR /app
|
| 4 |
+
RUN apt-get update && apt-get install -y --no-install-recommends git curl nodejs npm && rm -rf /var/lib/apt/lists/*
|
| 5 |
COPY requirements.txt /app/
|
| 6 |
RUN pip install --no-cache-dir -r requirements.txt
|
| 7 |
COPY . /app
|
| 8 |
RUN python -m db.seed sample_project/ --force
|
| 9 |
+
EXPOSE 7860
|
| 10 |
CMD ["uvicorn", "server.app:app", "--host", "0.0.0.0", "--port", "7860"]
|
code-review-env/analyzers/ast_checker.py
ADDED
|
@@ -0,0 +1,249 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
import ast
|
| 4 |
+
import dataclasses
|
| 5 |
+
from pathlib import Path
|
| 6 |
+
|
| 7 |
+
|
| 8 |
+
@dataclasses.dataclass(frozen=True)
|
| 9 |
+
class ASTFinding:
|
| 10 |
+
file: str
|
| 11 |
+
line: int
|
| 12 |
+
rule: str
|
| 13 |
+
message: str
|
| 14 |
+
severity: str
|
| 15 |
+
|
| 16 |
+
|
| 17 |
+
_MUTABLE_DEFAULT_NODES = (ast.List, ast.Dict, ast.Set)
|
| 18 |
+
|
| 19 |
+
|
| 20 |
+
def _load_tree(filepath: Path) -> ast.AST:
|
| 21 |
+
return ast.parse(filepath.read_text(encoding="utf-8"), filename=str(filepath))
|
| 22 |
+
|
| 23 |
+
|
| 24 |
+
def _is_optional_annotation(node: ast.AST | None) -> bool:
|
| 25 |
+
if node is None:
|
| 26 |
+
return False
|
| 27 |
+
if isinstance(node, ast.Subscript):
|
| 28 |
+
if isinstance(node.value, ast.Name) and node.value.id == "Optional":
|
| 29 |
+
return True
|
| 30 |
+
if isinstance(node.value, ast.Attribute) and node.value.attr == "Optional":
|
| 31 |
+
return True
|
| 32 |
+
if isinstance(node, ast.BinOp) and isinstance(node.op, ast.BitOr):
|
| 33 |
+
if isinstance(node.right, ast.Constant) and node.right.value is None:
|
| 34 |
+
return True
|
| 35 |
+
if isinstance(node.left, ast.Constant) and node.left.value is None:
|
| 36 |
+
return True
|
| 37 |
+
return False
|
| 38 |
+
|
| 39 |
+
|
| 40 |
+
def check_mutable_defaults(tree: ast.AST, filepath: str) -> list[ASTFinding]:
|
| 41 |
+
findings: list[ASTFinding] = []
|
| 42 |
+
for node in ast.walk(tree):
|
| 43 |
+
if not isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
|
| 44 |
+
continue
|
| 45 |
+
defaults = list(node.args.defaults) + [d for d in node.args.kw_defaults if d is not None]
|
| 46 |
+
for default in defaults:
|
| 47 |
+
if isinstance(default, _MUTABLE_DEFAULT_NODES):
|
| 48 |
+
findings.append(
|
| 49 |
+
ASTFinding(
|
| 50 |
+
file=filepath,
|
| 51 |
+
line=int(getattr(default, "lineno", node.lineno)),
|
| 52 |
+
rule="mutable_default_arg",
|
| 53 |
+
message="Mutable default argument can leak state across calls",
|
| 54 |
+
severity="high",
|
| 55 |
+
)
|
| 56 |
+
)
|
| 57 |
+
return findings
|
| 58 |
+
|
| 59 |
+
|
| 60 |
+
def check_bare_except(tree: ast.AST, filepath: str) -> list[ASTFinding]:
|
| 61 |
+
findings: list[ASTFinding] = []
|
| 62 |
+
for node in ast.walk(tree):
|
| 63 |
+
if not isinstance(node, ast.ExceptHandler):
|
| 64 |
+
continue
|
| 65 |
+
if node.type is None:
|
| 66 |
+
findings.append(
|
| 67 |
+
ASTFinding(
|
| 68 |
+
file=filepath,
|
| 69 |
+
line=int(node.lineno),
|
| 70 |
+
rule="bare_except",
|
| 71 |
+
message="Bare except catches unexpected errors and hides root causes",
|
| 72 |
+
severity="high",
|
| 73 |
+
)
|
| 74 |
+
)
|
| 75 |
+
return findings
|
| 76 |
+
|
| 77 |
+
|
| 78 |
+
def check_none_comparison(tree: ast.AST, filepath: str) -> list[ASTFinding]:
|
| 79 |
+
findings: list[ASTFinding] = []
|
| 80 |
+
for node in ast.walk(tree):
|
| 81 |
+
if not isinstance(node, ast.Compare):
|
| 82 |
+
continue
|
| 83 |
+
if not any(isinstance(op, (ast.Eq, ast.NotEq)) for op in node.ops):
|
| 84 |
+
continue
|
| 85 |
+
comparators = [node.left, *node.comparators]
|
| 86 |
+
if any(isinstance(comp, ast.Constant) and comp.value is None for comp in comparators):
|
| 87 |
+
findings.append(
|
| 88 |
+
ASTFinding(
|
| 89 |
+
file=filepath,
|
| 90 |
+
line=int(node.lineno),
|
| 91 |
+
rule="none_equality_check",
|
| 92 |
+
message="Use 'is None' / 'is not None' instead of == None comparisons",
|
| 93 |
+
severity="medium",
|
| 94 |
+
)
|
| 95 |
+
)
|
| 96 |
+
return findings
|
| 97 |
+
|
| 98 |
+
|
| 99 |
+
def _collect_optional_returning_functions(tree: ast.AST) -> set[str]:
|
| 100 |
+
optional_functions: set[str] = set()
|
| 101 |
+
for node in ast.walk(tree):
|
| 102 |
+
if not isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
|
| 103 |
+
continue
|
| 104 |
+
|
| 105 |
+
if _is_optional_annotation(node.returns):
|
| 106 |
+
optional_functions.add(node.name)
|
| 107 |
+
continue
|
| 108 |
+
|
| 109 |
+
has_none_return = False
|
| 110 |
+
has_value_return = False
|
| 111 |
+
for child in ast.walk(node):
|
| 112 |
+
if not isinstance(child, ast.Return):
|
| 113 |
+
continue
|
| 114 |
+
if child.value is None:
|
| 115 |
+
has_none_return = True
|
| 116 |
+
elif isinstance(child.value, ast.Constant) and child.value.value is None:
|
| 117 |
+
has_none_return = True
|
| 118 |
+
else:
|
| 119 |
+
has_value_return = True
|
| 120 |
+
if has_none_return and has_value_return:
|
| 121 |
+
optional_functions.add(node.name)
|
| 122 |
+
return optional_functions
|
| 123 |
+
|
| 124 |
+
|
| 125 |
+
def check_unchecked_optional_returns(tree: ast.AST, filepath: str) -> list[ASTFinding]:
|
| 126 |
+
findings: list[ASTFinding] = []
|
| 127 |
+
optional_functions = _collect_optional_returning_functions(tree)
|
| 128 |
+
if not optional_functions:
|
| 129 |
+
return findings
|
| 130 |
+
|
| 131 |
+
for node in ast.walk(tree):
|
| 132 |
+
if not isinstance(node, ast.Assign) or len(node.targets) != 1:
|
| 133 |
+
continue
|
| 134 |
+
if not isinstance(node.targets[0], ast.Name):
|
| 135 |
+
continue
|
| 136 |
+
if not isinstance(node.value, ast.Call):
|
| 137 |
+
continue
|
| 138 |
+
if not isinstance(node.value.func, ast.Name):
|
| 139 |
+
continue
|
| 140 |
+
if node.value.func.id not in optional_functions:
|
| 141 |
+
continue
|
| 142 |
+
|
| 143 |
+
var_name = node.targets[0].id
|
| 144 |
+
parent_body = _find_parent_body(tree, node)
|
| 145 |
+
if parent_body is None:
|
| 146 |
+
continue
|
| 147 |
+
index = parent_body.index(node)
|
| 148 |
+
for next_node in parent_body[index + 1 : index + 4]:
|
| 149 |
+
if isinstance(next_node, ast.If) and _is_none_guard(next_node.test, var_name):
|
| 150 |
+
break
|
| 151 |
+
if _uses_name_without_guard(next_node, var_name):
|
| 152 |
+
findings.append(
|
| 153 |
+
ASTFinding(
|
| 154 |
+
file=filepath,
|
| 155 |
+
line=int(getattr(next_node, "lineno", node.lineno)),
|
| 156 |
+
rule="unchecked_optional_return",
|
| 157 |
+
message=(
|
| 158 |
+
f"Result of optional-returning call '{node.value.func.id}' is used without a None guard"
|
| 159 |
+
),
|
| 160 |
+
severity="high",
|
| 161 |
+
)
|
| 162 |
+
)
|
| 163 |
+
break
|
| 164 |
+
return findings
|
| 165 |
+
|
| 166 |
+
|
| 167 |
+
def check_missing_dunder_all(tree: ast.AST, filepath: str) -> list[ASTFinding]:
|
| 168 |
+
path = Path(filepath)
|
| 169 |
+
if path.name.startswith("_"):
|
| 170 |
+
return []
|
| 171 |
+
|
| 172 |
+
has_public_defs = False
|
| 173 |
+
has_dunder_all = False
|
| 174 |
+
|
| 175 |
+
for node in tree.body if isinstance(tree, ast.Module) else []:
|
| 176 |
+
if isinstance(node, ast.Assign):
|
| 177 |
+
for target in node.targets:
|
| 178 |
+
if isinstance(target, ast.Name) and target.id == "__all__":
|
| 179 |
+
has_dunder_all = True
|
| 180 |
+
if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef, ast.ClassDef)) and not node.name.startswith("_"):
|
| 181 |
+
has_public_defs = True
|
| 182 |
+
|
| 183 |
+
if has_public_defs and not has_dunder_all:
|
| 184 |
+
return [
|
| 185 |
+
ASTFinding(
|
| 186 |
+
file=filepath,
|
| 187 |
+
line=1,
|
| 188 |
+
rule="missing_dunder_all",
|
| 189 |
+
message="Public module exports are missing __all__ declaration",
|
| 190 |
+
severity="medium",
|
| 191 |
+
)
|
| 192 |
+
]
|
| 193 |
+
return []
|
| 194 |
+
|
| 195 |
+
|
| 196 |
+
def _find_parent_body(tree: ast.AST, target: ast.AST) -> list[ast.stmt] | None:
|
| 197 |
+
for node in ast.walk(tree):
|
| 198 |
+
for field_name in ("body", "orelse", "finalbody"):
|
| 199 |
+
body = getattr(node, field_name, None)
|
| 200 |
+
if not isinstance(body, list):
|
| 201 |
+
continue
|
| 202 |
+
if target in body:
|
| 203 |
+
return body
|
| 204 |
+
return None
|
| 205 |
+
|
| 206 |
+
|
| 207 |
+
def _is_none_guard(test: ast.AST, var_name: str) -> bool:
|
| 208 |
+
if not isinstance(test, ast.Compare):
|
| 209 |
+
return False
|
| 210 |
+
if len(test.ops) != 1 or len(test.comparators) != 1:
|
| 211 |
+
return False
|
| 212 |
+
op = test.ops[0]
|
| 213 |
+
left = test.left
|
| 214 |
+
right = test.comparators[0]
|
| 215 |
+
if not isinstance(left, ast.Name) or left.id != var_name:
|
| 216 |
+
return False
|
| 217 |
+
if not isinstance(right, ast.Constant) or right.value is not None:
|
| 218 |
+
return False
|
| 219 |
+
return isinstance(op, (ast.Is, ast.IsNot))
|
| 220 |
+
|
| 221 |
+
|
| 222 |
+
def _uses_name_without_guard(node: ast.AST, var_name: str) -> bool:
|
| 223 |
+
for child in ast.walk(node):
|
| 224 |
+
if isinstance(child, ast.Attribute) and isinstance(child.value, ast.Name) and child.value.id == var_name:
|
| 225 |
+
return True
|
| 226 |
+
if isinstance(child, ast.Subscript) and isinstance(child.value, ast.Name) and child.value.id == var_name:
|
| 227 |
+
return True
|
| 228 |
+
return False
|
| 229 |
+
|
| 230 |
+
|
| 231 |
+
def run_all(filepath: str | Path) -> list[ASTFinding]:
|
| 232 |
+
path = Path(filepath)
|
| 233 |
+
try:
|
| 234 |
+
tree = _load_tree(path)
|
| 235 |
+
except (OSError, SyntaxError):
|
| 236 |
+
return []
|
| 237 |
+
|
| 238 |
+
filename = str(path)
|
| 239 |
+
findings = []
|
| 240 |
+
findings.extend(check_mutable_defaults(tree, filename))
|
| 241 |
+
findings.extend(check_bare_except(tree, filename))
|
| 242 |
+
findings.extend(check_none_comparison(tree, filename))
|
| 243 |
+
findings.extend(check_unchecked_optional_returns(tree, filename))
|
| 244 |
+
findings.extend(check_missing_dunder_all(tree, filename))
|
| 245 |
+
return findings
|
| 246 |
+
|
| 247 |
+
|
| 248 |
+
def run_all_checks(filepath: str | Path) -> list[ASTFinding]:
|
| 249 |
+
return run_all(filepath)
|
code-review-env/analyzers/pipeline.py
CHANGED
|
@@ -3,12 +3,13 @@ from __future__ import annotations
|
|
| 3 |
import hashlib
|
| 4 |
import json
|
| 5 |
import os
|
| 6 |
-
import shutil
|
| 7 |
import subprocess
|
| 8 |
import sys
|
| 9 |
from dataclasses import dataclass
|
| 10 |
from pathlib import Path
|
| 11 |
|
|
|
|
|
|
|
| 12 |
|
| 13 |
@dataclass(frozen=True)
|
| 14 |
class AnalyzerFindingRecord:
|
|
@@ -35,44 +36,47 @@ class AnalyzerRunSummary:
|
|
| 35 |
class AnalyzerPipeline:
|
| 36 |
"""Run deterministic analyzers and normalize outputs into shared finding records."""
|
| 37 |
|
| 38 |
-
def __init__(self, target_dir: Path, timeout_seconds: int =
|
| 39 |
self.target_dir = target_dir.resolve()
|
| 40 |
self.timeout_seconds = timeout_seconds
|
| 41 |
|
| 42 |
def run_all(self) -> tuple[list[AnalyzerFindingRecord], list[AnalyzerRunSummary]]:
|
| 43 |
findings: list[AnalyzerFindingRecord] = []
|
| 44 |
summaries: list[AnalyzerRunSummary] = []
|
| 45 |
-
semgrep_enabled = os.getenv("GRAPHREVIEW_SEMGREP_ENABLED", "true").strip().lower() == "true"
|
| 46 |
runners = [
|
| 47 |
-
self._run_pylint,
|
| 48 |
-
self._run_pyflakes,
|
| 49 |
-
self._run_bandit,
|
| 50 |
-
self._run_mypy,
|
| 51 |
self._run_pyright,
|
| 52 |
-
self.
|
|
|
|
|
|
|
|
|
|
|
|
|
| 53 |
]
|
| 54 |
-
if semgrep_enabled:
|
| 55 |
-
runners.append(self._run_semgrep)
|
| 56 |
for runner in runners:
|
| 57 |
records, summary = runner()
|
| 58 |
findings.extend(records)
|
| 59 |
summaries.append(summary)
|
| 60 |
return findings, summaries
|
| 61 |
|
| 62 |
-
def
|
| 63 |
-
cmd = [
|
| 64 |
-
|
| 65 |
-
|
| 66 |
-
|
| 67 |
-
|
| 68 |
-
|
| 69 |
-
|
| 70 |
-
|
| 71 |
-
|
| 72 |
-
|
| 73 |
-
|
| 74 |
-
|
| 75 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 76 |
|
| 77 |
def _run_pylint(self) -> tuple[list[AnalyzerFindingRecord], AnalyzerRunSummary]:
|
| 78 |
cmd = [
|
|
@@ -83,49 +87,37 @@ class AnalyzerPipeline:
|
|
| 83 |
"--output-format=json2",
|
| 84 |
"--score=n",
|
| 85 |
"--reports=n",
|
|
|
|
| 86 |
]
|
| 87 |
return self._run_with_parser("pylint", cmd, self._parse_pylint)
|
| 88 |
|
| 89 |
-
def
|
| 90 |
-
cmd = [sys.executable, "-m", "
|
| 91 |
-
return self._run_with_parser("
|
| 92 |
-
|
| 93 |
-
def _run_bandit(self) -> tuple[list[AnalyzerFindingRecord], AnalyzerRunSummary]:
|
| 94 |
-
cmd = [sys.executable, "-m", "bandit", "-r", "-q", "-f", "json", str(self.target_dir)]
|
| 95 |
-
return self._run_with_parser("bandit", cmd, self._parse_bandit)
|
| 96 |
-
|
| 97 |
-
def _run_pyright(self) -> tuple[list[AnalyzerFindingRecord], AnalyzerRunSummary]:
|
| 98 |
-
cmd = ["pyright", "--outputjson", str(self.target_dir)]
|
| 99 |
-
return self._run_with_parser("pyright", cmd, self._parse_pyright)
|
| 100 |
|
| 101 |
-
def
|
| 102 |
-
|
| 103 |
-
|
| 104 |
-
|
| 105 |
-
|
| 106 |
-
semgrep_bin = os.getenv("GRAPHREVIEW_SEMGREP_BIN")
|
| 107 |
-
if not semgrep_bin:
|
| 108 |
-
pysemgrep_candidate = str((Path.home() / ".local" / "bin" / "pysemgrep").resolve())
|
| 109 |
-
if Path(pysemgrep_candidate).exists():
|
| 110 |
-
semgrep_bin = pysemgrep_candidate
|
| 111 |
-
else:
|
| 112 |
-
semgrep_bin = shutil.which("semgrep")
|
| 113 |
-
if not semgrep_bin:
|
| 114 |
-
semgrep_candidate = str((Path.home() / ".local" / "bin" / "semgrep").resolve())
|
| 115 |
-
semgrep_bin = semgrep_candidate if Path(semgrep_candidate).exists() else "semgrep"
|
| 116 |
|
| 117 |
-
|
| 118 |
-
|
| 119 |
-
|
| 120 |
-
|
| 121 |
-
|
| 122 |
-
str(self.target_dir),
|
| 123 |
-
]
|
| 124 |
-
return self._run_with_parser("semgrep", cmd, self._parse_semgrep)
|
| 125 |
|
| 126 |
-
|
| 127 |
-
|
| 128 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 129 |
|
| 130 |
def _run_with_parser(
|
| 131 |
self,
|
|
@@ -168,30 +160,27 @@ class AnalyzerPipeline:
|
|
| 168 |
analyzer_version = self._resolve_version(cmd)
|
| 169 |
command = " ".join(cmd)
|
| 170 |
command_hash = self._command_hash(cmd)
|
|
|
|
|
|
|
|
|
|
|
|
|
| 171 |
if not stdout:
|
| 172 |
-
|
| 173 |
-
|
| 174 |
-
|
| 175 |
-
|
| 176 |
-
|
| 177 |
-
|
| 178 |
-
|
| 179 |
-
|
| 180 |
-
|
| 181 |
-
command_hash=command_hash,
|
| 182 |
-
analyzer_version=analyzer_version,
|
| 183 |
-
error_message=stderr or None,
|
| 184 |
-
)
|
| 185 |
|
| 186 |
try:
|
| 187 |
records = parser(stdout)
|
| 188 |
-
status = "ok"
|
| 189 |
-
if proc.returncode not in {0, 1} and analyzer not in {"pylint", "pyflakes", "bandit", "vulture", "mypy", "pyright", "semgrep"}:
|
| 190 |
-
status = "no-output"
|
| 191 |
return records, AnalyzerRunSummary(
|
| 192 |
analyzer=analyzer,
|
| 193 |
findings=len(records),
|
| 194 |
-
status=
|
| 195 |
command=command,
|
| 196 |
command_hash=command_hash,
|
| 197 |
analyzer_version=analyzer_version,
|
|
@@ -213,21 +202,26 @@ class AnalyzerPipeline:
|
|
| 213 |
return ""
|
| 214 |
exe = cmd[0]
|
| 215 |
if exe == sys.executable and len(cmd) >= 3 and cmd[1] == "-m":
|
| 216 |
-
|
| 217 |
-
|
|
|
|
|
|
|
|
|
|
| 218 |
else:
|
| 219 |
version_cmd = [exe, "--version"]
|
|
|
|
| 220 |
try:
|
| 221 |
proc = subprocess.run(
|
| 222 |
version_cmd,
|
| 223 |
cwd=str(self.target_dir),
|
| 224 |
capture_output=True,
|
| 225 |
text=True,
|
| 226 |
-
timeout=
|
| 227 |
check=False,
|
| 228 |
)
|
| 229 |
except Exception:
|
| 230 |
return ""
|
|
|
|
| 231 |
value = (proc.stdout or proc.stderr or "").strip().splitlines()
|
| 232 |
return value[0][:160] if value else ""
|
| 233 |
|
|
@@ -248,81 +242,65 @@ class AnalyzerPipeline:
|
|
| 248 |
module = module[:-3]
|
| 249 |
return module
|
| 250 |
|
| 251 |
-
def
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 252 |
parsed = json.loads(text)
|
|
|
|
| 253 |
records: list[AnalyzerFindingRecord] = []
|
| 254 |
-
|
| 255 |
-
return records
|
| 256 |
-
for item in parsed:
|
| 257 |
if not isinstance(item, dict):
|
| 258 |
continue
|
|
|
|
|
|
|
| 259 |
file_path = str(item.get("file") or "")
|
| 260 |
-
line = int(item.get("line") or 1)
|
| 261 |
message = str(item.get("message") or "")
|
| 262 |
-
|
| 263 |
-
|
| 264 |
records.append(
|
| 265 |
AnalyzerFindingRecord(
|
| 266 |
-
analyzer="
|
| 267 |
module_id=self._normalize_module(file_path),
|
| 268 |
line=max(line, 1),
|
| 269 |
-
severity=
|
| 270 |
-
rule_id=
|
| 271 |
message=message,
|
| 272 |
evidence="",
|
| 273 |
)
|
| 274 |
)
|
| 275 |
return records
|
| 276 |
-
|
| 277 |
-
def _parse_pylint(self, text: str) -> list[AnalyzerFindingRecord]:
|
| 278 |
parsed = json.loads(text)
|
| 279 |
-
|
| 280 |
-
|
| 281 |
-
|
| 282 |
-
|
| 283 |
-
|
| 284 |
-
module_id = self._normalize_module(str(item.get("path") or item.get("abspath") or ""))
|
| 285 |
-
line = int(item.get("line") or 1)
|
| 286 |
-
raw_type = str(item.get("type") or "warning").lower()
|
| 287 |
-
severity = "low"
|
| 288 |
-
if raw_type in {"fatal", "error"}:
|
| 289 |
-
severity = "high"
|
| 290 |
-
elif raw_type == "warning":
|
| 291 |
-
severity = "medium"
|
| 292 |
-
records.append(
|
| 293 |
-
AnalyzerFindingRecord(
|
| 294 |
-
analyzer="pylint",
|
| 295 |
-
module_id=module_id,
|
| 296 |
-
line=max(line, 1),
|
| 297 |
-
severity=severity,
|
| 298 |
-
rule_id=str(item.get("messageId") or "pylint"),
|
| 299 |
-
message=str(item.get("message") or ""),
|
| 300 |
-
evidence=str(item.get("symbol") or ""),
|
| 301 |
-
)
|
| 302 |
-
)
|
| 303 |
-
return records
|
| 304 |
|
| 305 |
-
def _parse_pyflakes(self, text: str) -> list[AnalyzerFindingRecord]:
|
| 306 |
records: list[AnalyzerFindingRecord] = []
|
| 307 |
-
for
|
| 308 |
-
|
| 309 |
-
|
| 310 |
-
|
| 311 |
-
|
| 312 |
-
if len(parts) < 3:
|
| 313 |
-
continue
|
| 314 |
-
module_id = self._normalize_module(parts[0].strip())
|
| 315 |
-
line = int(parts[1]) if parts[1].isdigit() else 1
|
| 316 |
-
message = parts[3].strip() if len(parts) == 4 else stripped
|
| 317 |
records.append(
|
| 318 |
AnalyzerFindingRecord(
|
| 319 |
-
analyzer="
|
| 320 |
-
module_id=
|
| 321 |
line=max(line, 1),
|
| 322 |
-
severity="
|
| 323 |
-
rule_id=
|
| 324 |
message=message,
|
| 325 |
-
evidence="",
|
| 326 |
)
|
| 327 |
)
|
| 328 |
return records
|
|
@@ -353,82 +331,61 @@ class AnalyzerPipeline:
|
|
| 353 |
)
|
| 354 |
return records
|
| 355 |
|
| 356 |
-
def
|
| 357 |
parsed = json.loads(text)
|
| 358 |
-
|
| 359 |
records: list[AnalyzerFindingRecord] = []
|
| 360 |
-
for item in
|
| 361 |
if not isinstance(item, dict):
|
| 362 |
continue
|
| 363 |
-
|
| 364 |
-
|
| 365 |
-
severity = "high" if severity_raw == "error" else "medium"
|
| 366 |
-
message = str(item.get("message") or "")
|
| 367 |
-
rule = str(item.get("rule") or "pyright")
|
| 368 |
-
line = int(((item.get("range") or {}).get("start") or {}).get("line") or 0) + 1
|
| 369 |
-
records.append(
|
| 370 |
-
AnalyzerFindingRecord(
|
| 371 |
-
analyzer="pyright",
|
| 372 |
-
module_id=self._normalize_module(file_path),
|
| 373 |
-
line=max(line, 1),
|
| 374 |
-
severity=severity,
|
| 375 |
-
rule_id=rule,
|
| 376 |
-
message=message,
|
| 377 |
-
evidence="",
|
| 378 |
-
)
|
| 379 |
-
)
|
| 380 |
-
return records
|
| 381 |
-
|
| 382 |
-
def _parse_semgrep(self, text: str) -> list[AnalyzerFindingRecord]:
|
| 383 |
-
parsed = json.loads(text)
|
| 384 |
-
results = parsed.get("results", []) if isinstance(parsed, dict) else []
|
| 385 |
-
records: list[AnalyzerFindingRecord] = []
|
| 386 |
-
for item in results:
|
| 387 |
-
if not isinstance(item, dict):
|
| 388 |
continue
|
| 389 |
-
path = str(item.get("path") or "")
|
| 390 |
-
extra = item.get("extra") or {}
|
| 391 |
-
severity_raw = str(extra.get("severity") or "WARNING").lower()
|
| 392 |
-
severity = "high" if severity_raw == "error" else "medium"
|
| 393 |
-
start_line = int(((item.get("start") or {}).get("line") or 1))
|
| 394 |
-
message = str(extra.get("message") or "")
|
| 395 |
-
rule_id = str(item.get("check_id") or "semgrep")
|
| 396 |
-
evidence = str(extra.get("lines") or "")
|
| 397 |
records.append(
|
| 398 |
AnalyzerFindingRecord(
|
| 399 |
-
analyzer="
|
| 400 |
-
module_id=self._normalize_module(path),
|
| 401 |
-
line=max(
|
| 402 |
-
severity=
|
| 403 |
-
rule_id=
|
| 404 |
-
message=message,
|
| 405 |
-
evidence=
|
| 406 |
)
|
| 407 |
)
|
| 408 |
return records
|
| 409 |
|
| 410 |
-
def
|
| 411 |
parsed = json.loads(text)
|
| 412 |
-
issues = parsed if isinstance(parsed, list) else []
|
| 413 |
records: list[AnalyzerFindingRecord] = []
|
| 414 |
-
|
| 415 |
-
|
|
|
|
|
|
|
|
|
|
| 416 |
continue
|
| 417 |
-
|
| 418 |
-
|
| 419 |
-
|
| 420 |
-
|
| 421 |
-
|
| 422 |
-
|
| 423 |
-
|
| 424 |
-
|
| 425 |
-
|
| 426 |
-
|
| 427 |
-
|
| 428 |
-
|
| 429 |
-
|
| 430 |
-
|
| 431 |
-
|
|
|
|
|
|
|
|
|
|
| 432 |
)
|
| 433 |
-
)
|
| 434 |
return records
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3 |
import hashlib
|
| 4 |
import json
|
| 5 |
import os
|
|
|
|
| 6 |
import subprocess
|
| 7 |
import sys
|
| 8 |
from dataclasses import dataclass
|
| 9 |
from pathlib import Path
|
| 10 |
|
| 11 |
+
from analyzers.ast_checker import ASTFinding, run_all_checks
|
| 12 |
+
|
| 13 |
|
| 14 |
@dataclass(frozen=True)
|
| 15 |
class AnalyzerFindingRecord:
|
|
|
|
| 36 |
class AnalyzerPipeline:
|
| 37 |
"""Run deterministic analyzers and normalize outputs into shared finding records."""
|
| 38 |
|
| 39 |
+
def __init__(self, target_dir: Path, timeout_seconds: int = 60) -> None:
|
| 40 |
self.target_dir = target_dir.resolve()
|
| 41 |
self.timeout_seconds = timeout_seconds
|
| 42 |
|
| 43 |
def run_all(self) -> tuple[list[AnalyzerFindingRecord], list[AnalyzerRunSummary]]:
|
| 44 |
findings: list[AnalyzerFindingRecord] = []
|
| 45 |
summaries: list[AnalyzerRunSummary] = []
|
|
|
|
| 46 |
runners = [
|
|
|
|
|
|
|
|
|
|
|
|
|
| 47 |
self._run_pyright,
|
| 48 |
+
self._run_pysa,
|
| 49 |
+
self._run_bandit,
|
| 50 |
+
self._run_pylint,
|
| 51 |
+
self._run_radon,
|
| 52 |
+
self._run_ast_checks,
|
| 53 |
]
|
|
|
|
|
|
|
| 54 |
for runner in runners:
|
| 55 |
records, summary = runner()
|
| 56 |
findings.extend(records)
|
| 57 |
summaries.append(summary)
|
| 58 |
return findings, summaries
|
| 59 |
|
| 60 |
+
def _run_pyright(self) -> tuple[list[AnalyzerFindingRecord], AnalyzerRunSummary]:
|
| 61 |
+
cmd = [self._resolve_pyright_bin(), "--strict", "--outputjson", str(self.target_dir)]
|
| 62 |
+
return self._run_with_parser("pyright", cmd, self._parse_pyright)
|
| 63 |
+
|
| 64 |
+
def _resolve_pyright_bin(self) -> str:
|
| 65 |
+
env_bin = Path(sys.executable).resolve().parent / "pyright"
|
| 66 |
+
if env_bin.exists():
|
| 67 |
+
return str(env_bin)
|
| 68 |
+
explicit = os.getenv("GRAPHREVIEW_PYRIGHT_BIN", "").strip()
|
| 69 |
+
if explicit:
|
| 70 |
+
return explicit
|
| 71 |
+
return "pyright"
|
| 72 |
+
|
| 73 |
+
def _run_pysa(self) -> tuple[list[AnalyzerFindingRecord], AnalyzerRunSummary]:
|
| 74 |
+
cmd = ["pyre", "--noninteractive", "analyze", "--output", "json"]
|
| 75 |
+
return self._run_with_parser("pysa", cmd, self._parse_pysa)
|
| 76 |
+
|
| 77 |
+
def _run_bandit(self) -> tuple[list[AnalyzerFindingRecord], AnalyzerRunSummary]:
|
| 78 |
+
cmd = [sys.executable, "-m", "bandit", "-r", "-q", "-f", "json", str(self.target_dir)]
|
| 79 |
+
return self._run_with_parser("bandit", cmd, self._parse_bandit)
|
| 80 |
|
| 81 |
def _run_pylint(self) -> tuple[list[AnalyzerFindingRecord], AnalyzerRunSummary]:
|
| 82 |
cmd = [
|
|
|
|
| 87 |
"--output-format=json2",
|
| 88 |
"--score=n",
|
| 89 |
"--reports=n",
|
| 90 |
+
"--errors-only",
|
| 91 |
]
|
| 92 |
return self._run_with_parser("pylint", cmd, self._parse_pylint)
|
| 93 |
|
| 94 |
+
def _run_radon(self) -> tuple[list[AnalyzerFindingRecord], AnalyzerRunSummary]:
|
| 95 |
+
cmd = [sys.executable, "-m", "radon", "cc", "-j", "-s", str(self.target_dir)]
|
| 96 |
+
return self._run_with_parser("radon", cmd, self._parse_radon)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 97 |
|
| 98 |
+
def _run_ast_checks(self) -> tuple[list[AnalyzerFindingRecord], AnalyzerRunSummary]:
|
| 99 |
+
cmd = ["python-ast-checker", str(self.target_dir)]
|
| 100 |
+
py_files = sorted(path for path in self.target_dir.rglob("*.py") if path.is_file())
|
| 101 |
+
records: list[AnalyzerFindingRecord] = []
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 102 |
|
| 103 |
+
for py_file in py_files:
|
| 104 |
+
if any(part.startswith(".") for part in py_file.parts):
|
| 105 |
+
continue
|
| 106 |
+
for finding in run_all_checks(py_file):
|
| 107 |
+
records.append(self._ast_to_record(finding))
|
|
|
|
|
|
|
|
|
|
| 108 |
|
| 109 |
+
return (
|
| 110 |
+
records,
|
| 111 |
+
AnalyzerRunSummary(
|
| 112 |
+
analyzer="ast",
|
| 113 |
+
findings=len(records),
|
| 114 |
+
status="ok",
|
| 115 |
+
command=" ".join(cmd),
|
| 116 |
+
command_hash=self._command_hash(cmd),
|
| 117 |
+
analyzer_version="builtin",
|
| 118 |
+
error_message=None,
|
| 119 |
+
),
|
| 120 |
+
)
|
| 121 |
|
| 122 |
def _run_with_parser(
|
| 123 |
self,
|
|
|
|
| 160 |
analyzer_version = self._resolve_version(cmd)
|
| 161 |
command = " ".join(cmd)
|
| 162 |
command_hash = self._command_hash(cmd)
|
| 163 |
+
|
| 164 |
+
if not stdout and analyzer in {"pysa"} and stderr:
|
| 165 |
+
stdout = stderr
|
| 166 |
+
|
| 167 |
if not stdout:
|
| 168 |
+
return [], AnalyzerRunSummary(
|
| 169 |
+
analyzer=analyzer,
|
| 170 |
+
findings=0,
|
| 171 |
+
status="ok" if proc.returncode in {0, 1, 2} else "no-output",
|
| 172 |
+
command=command,
|
| 173 |
+
command_hash=command_hash,
|
| 174 |
+
analyzer_version=analyzer_version,
|
| 175 |
+
error_message=stderr or None,
|
| 176 |
+
)
|
|
|
|
|
|
|
|
|
|
|
|
|
| 177 |
|
| 178 |
try:
|
| 179 |
records = parser(stdout)
|
|
|
|
|
|
|
|
|
|
| 180 |
return records, AnalyzerRunSummary(
|
| 181 |
analyzer=analyzer,
|
| 182 |
findings=len(records),
|
| 183 |
+
status="ok" if proc.returncode in {0, 1, 2} else "no-output",
|
| 184 |
command=command,
|
| 185 |
command_hash=command_hash,
|
| 186 |
analyzer_version=analyzer_version,
|
|
|
|
| 202 |
return ""
|
| 203 |
exe = cmd[0]
|
| 204 |
if exe == sys.executable and len(cmd) >= 3 and cmd[1] == "-m":
|
| 205 |
+
version_cmd = [sys.executable, "-m", cmd[2], "--version"]
|
| 206 |
+
elif exe == "pyre":
|
| 207 |
+
version_cmd = ["pyre", "--version"]
|
| 208 |
+
elif exe == "pyright":
|
| 209 |
+
version_cmd = ["pyright", "--version"]
|
| 210 |
else:
|
| 211 |
version_cmd = [exe, "--version"]
|
| 212 |
+
|
| 213 |
try:
|
| 214 |
proc = subprocess.run(
|
| 215 |
version_cmd,
|
| 216 |
cwd=str(self.target_dir),
|
| 217 |
capture_output=True,
|
| 218 |
text=True,
|
| 219 |
+
timeout=10,
|
| 220 |
check=False,
|
| 221 |
)
|
| 222 |
except Exception:
|
| 223 |
return ""
|
| 224 |
+
|
| 225 |
value = (proc.stdout or proc.stderr or "").strip().splitlines()
|
| 226 |
return value[0][:160] if value else ""
|
| 227 |
|
|
|
|
| 242 |
module = module[:-3]
|
| 243 |
return module
|
| 244 |
|
| 245 |
+
def _ast_to_record(self, finding: ASTFinding) -> AnalyzerFindingRecord:
|
| 246 |
+
return AnalyzerFindingRecord(
|
| 247 |
+
analyzer="ast",
|
| 248 |
+
module_id=self._normalize_module(finding.file),
|
| 249 |
+
line=max(int(finding.line), 1),
|
| 250 |
+
severity=finding.severity,
|
| 251 |
+
rule_id=finding.rule,
|
| 252 |
+
message=finding.message,
|
| 253 |
+
evidence="",
|
| 254 |
+
)
|
| 255 |
+
|
| 256 |
+
def _parse_pyright(self, text: str) -> list[AnalyzerFindingRecord]:
|
| 257 |
parsed = json.loads(text)
|
| 258 |
+
diagnostics = parsed.get("generalDiagnostics", []) if isinstance(parsed, dict) else []
|
| 259 |
records: list[AnalyzerFindingRecord] = []
|
| 260 |
+
for item in diagnostics:
|
|
|
|
|
|
|
| 261 |
if not isinstance(item, dict):
|
| 262 |
continue
|
| 263 |
+
if str(item.get("severity", "")).lower() != "error":
|
| 264 |
+
continue
|
| 265 |
file_path = str(item.get("file") or "")
|
|
|
|
| 266 |
message = str(item.get("message") or "")
|
| 267 |
+
rule = str(item.get("rule") or "pyright-error")
|
| 268 |
+
line = int(((item.get("range") or {}).get("start") or {}).get("line") or 0) + 1
|
| 269 |
records.append(
|
| 270 |
AnalyzerFindingRecord(
|
| 271 |
+
analyzer="pyright",
|
| 272 |
module_id=self._normalize_module(file_path),
|
| 273 |
line=max(line, 1),
|
| 274 |
+
severity="high",
|
| 275 |
+
rule_id=rule,
|
| 276 |
message=message,
|
| 277 |
evidence="",
|
| 278 |
)
|
| 279 |
)
|
| 280 |
return records
|
| 281 |
+
def _parse_pysa(self, text: str) -> list[AnalyzerFindingRecord]:
|
|
|
|
| 282 |
parsed = json.loads(text)
|
| 283 |
+
issues: list[dict[str, object]] = []
|
| 284 |
+
if isinstance(parsed, dict):
|
| 285 |
+
issues = list(parsed.get("issues", []))
|
| 286 |
+
elif isinstance(parsed, list):
|
| 287 |
+
issues = [item for item in parsed if isinstance(item, dict)]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 288 |
|
|
|
|
| 289 |
records: list[AnalyzerFindingRecord] = []
|
| 290 |
+
for issue in issues:
|
| 291 |
+
path = str(issue.get("path") or issue.get("filename") or "")
|
| 292 |
+
line = int(issue.get("line") or issue.get("line_number") or 1)
|
| 293 |
+
code = str(issue.get("code") or issue.get("name") or "pysa")
|
| 294 |
+
message = str(issue.get("description") or issue.get("message") or "Taint flow issue")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 295 |
records.append(
|
| 296 |
AnalyzerFindingRecord(
|
| 297 |
+
analyzer="pysa",
|
| 298 |
+
module_id=self._normalize_module(path),
|
| 299 |
line=max(line, 1),
|
| 300 |
+
severity="high",
|
| 301 |
+
rule_id=code,
|
| 302 |
message=message,
|
| 303 |
+
evidence=str(issue.get("define") or issue.get("callable") or ""),
|
| 304 |
)
|
| 305 |
)
|
| 306 |
return records
|
|
|
|
| 331 |
)
|
| 332 |
return records
|
| 333 |
|
| 334 |
+
def _parse_pylint(self, text: str) -> list[AnalyzerFindingRecord]:
|
| 335 |
parsed = json.loads(text)
|
| 336 |
+
messages = parsed.get("messages", []) if isinstance(parsed, dict) else []
|
| 337 |
records: list[AnalyzerFindingRecord] = []
|
| 338 |
+
for item in messages:
|
| 339 |
if not isinstance(item, dict):
|
| 340 |
continue
|
| 341 |
+
msg_type = str(item.get("type") or "").lower()
|
| 342 |
+
if msg_type not in {"fatal", "error"}:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 343 |
continue
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 344 |
records.append(
|
| 345 |
AnalyzerFindingRecord(
|
| 346 |
+
analyzer="pylint",
|
| 347 |
+
module_id=self._normalize_module(str(item.get("path") or item.get("abspath") or "")),
|
| 348 |
+
line=max(int(item.get("line") or 1), 1),
|
| 349 |
+
severity="high",
|
| 350 |
+
rule_id=str(item.get("messageId") or "pylint-error"),
|
| 351 |
+
message=str(item.get("message") or ""),
|
| 352 |
+
evidence=str(item.get("symbol") or ""),
|
| 353 |
)
|
| 354 |
)
|
| 355 |
return records
|
| 356 |
|
| 357 |
+
def _parse_radon(self, text: str) -> list[AnalyzerFindingRecord]:
|
| 358 |
parsed = json.loads(text)
|
|
|
|
| 359 |
records: list[AnalyzerFindingRecord] = []
|
| 360 |
+
if not isinstance(parsed, dict):
|
| 361 |
+
return records
|
| 362 |
+
|
| 363 |
+
for file_path, blocks in parsed.items():
|
| 364 |
+
if not isinstance(blocks, list):
|
| 365 |
continue
|
| 366 |
+
for block in blocks:
|
| 367 |
+
if not isinstance(block, dict):
|
| 368 |
+
continue
|
| 369 |
+
complexity = int(block.get("complexity") or 0)
|
| 370 |
+
if complexity <= 10:
|
| 371 |
+
continue
|
| 372 |
+
name = str(block.get("name") or block.get("type") or "block")
|
| 373 |
+
line = int(block.get("lineno") or 1)
|
| 374 |
+
records.append(
|
| 375 |
+
AnalyzerFindingRecord(
|
| 376 |
+
analyzer="radon",
|
| 377 |
+
module_id=self._normalize_module(file_path),
|
| 378 |
+
line=max(line, 1),
|
| 379 |
+
severity="medium" if complexity < 15 else "high",
|
| 380 |
+
rule_id="CC_HIGH",
|
| 381 |
+
message=f"Cyclomatic complexity {complexity} in {name} exceeds threshold 10",
|
| 382 |
+
evidence=f"complexity={complexity}",
|
| 383 |
+
)
|
| 384 |
)
|
|
|
|
| 385 |
return records
|
| 386 |
+
|
| 387 |
+
|
| 388 |
+
def run_pipeline(target_dir: str | Path) -> list[AnalyzerFindingRecord]:
|
| 389 |
+
pipeline = AnalyzerPipeline(target_dir=Path(target_dir))
|
| 390 |
+
findings, _ = pipeline.run_all()
|
| 391 |
+
return findings
|
code-review-env/db/migrations.py
CHANGED
|
@@ -55,6 +55,11 @@ def init_db(db_path: str | Path | None = None, echo: bool = False) -> None:
|
|
| 55 |
|
| 56 |
|
| 57 |
def _apply_lightweight_migrations(engine) -> None:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 58 |
inspector = inspect(engine)
|
| 59 |
if "reviewannotation" not in inspector.get_table_names():
|
| 60 |
return
|
|
|
|
| 55 |
|
| 56 |
|
| 57 |
def _apply_lightweight_migrations(engine) -> None:
|
| 58 |
+
inspector = inspect(engine)
|
| 59 |
+
if "trainingannotation" not in inspector.get_table_names():
|
| 60 |
+
from db.schema import TrainingAnnotation # local import to avoid circular startup order
|
| 61 |
+
TrainingAnnotation.__table__.create(bind=engine, checkfirst=True)
|
| 62 |
+
|
| 63 |
inspector = inspect(engine)
|
| 64 |
if "reviewannotation" not in inspector.get_table_names():
|
| 65 |
return
|
code-review-env/db/models.py
CHANGED
|
@@ -11,6 +11,7 @@ from db.schema import (
|
|
| 11 |
SeedMeta,
|
| 12 |
Severity,
|
| 13 |
TaskDefinition,
|
|
|
|
| 14 |
TrainingRun,
|
| 15 |
)
|
| 16 |
|
|
@@ -27,5 +28,6 @@ __all__ = [
|
|
| 27 |
"SeedMeta",
|
| 28 |
"Severity",
|
| 29 |
"TaskDefinition",
|
|
|
|
| 30 |
"TrainingRun",
|
| 31 |
]
|
|
|
|
| 11 |
SeedMeta,
|
| 12 |
Severity,
|
| 13 |
TaskDefinition,
|
| 14 |
+
TrainingAnnotation,
|
| 15 |
TrainingRun,
|
| 16 |
)
|
| 17 |
|
|
|
|
| 28 |
"SeedMeta",
|
| 29 |
"Severity",
|
| 30 |
"TaskDefinition",
|
| 31 |
+
"TrainingAnnotation",
|
| 32 |
"TrainingRun",
|
| 33 |
]
|
code-review-env/db/schema.py
CHANGED
|
@@ -161,3 +161,20 @@ class TrainingRun(SQLModel, table=True):
|
|
| 161 |
output_path: str = ""
|
| 162 |
run_config_json: str = "{}"
|
| 163 |
created_at: datetime = Field(default_factory=lambda: datetime.now(UTC))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 161 |
output_path: str = ""
|
| 162 |
run_config_json: str = "{}"
|
| 163 |
created_at: datetime = Field(default_factory=lambda: datetime.now(UTC))
|
| 164 |
+
|
| 165 |
+
|
| 166 |
+
class TrainingAnnotation(SQLModel, table=True):
|
| 167 |
+
id: Optional[int] = Field(default=None, primary_key=True)
|
| 168 |
+
source_root: str = Field(index=True)
|
| 169 |
+
run_id: str = Field(index=True)
|
| 170 |
+
module_id: str = Field(index=True)
|
| 171 |
+
task_id: str = Field(index=True)
|
| 172 |
+
judge_verdict: str = ""
|
| 173 |
+
avg_reward: float = 0.0
|
| 174 |
+
correct_attributions_json: str = "[]"
|
| 175 |
+
wrong_attributions_json: str = "[]"
|
| 176 |
+
action_counts_json: str = "{}"
|
| 177 |
+
action_type: str = ""
|
| 178 |
+
action_payload: str = "{}"
|
| 179 |
+
thinking_quality: float = 0.0
|
| 180 |
+
created_at: datetime = Field(default_factory=lambda: datetime.now(UTC))
|
code-review-env/db/store.py
CHANGED
|
@@ -25,6 +25,7 @@ from db.schema import (
|
|
| 25 |
ReviewStatus,
|
| 26 |
SeedMeta,
|
| 27 |
Severity,
|
|
|
|
| 28 |
TrainingRun,
|
| 29 |
)
|
| 30 |
|
|
@@ -349,6 +350,48 @@ class Store:
|
|
| 349 |
)
|
| 350 |
return list(session.exec(query).all())
|
| 351 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 352 |
def get_training_run(self, run_id: str) -> TrainingRun | None:
|
| 353 |
with Session(self.engine) as session:
|
| 354 |
query = select(TrainingRun).where(
|
|
@@ -667,6 +710,11 @@ class Store:
|
|
| 667 |
TrainingRun.source_root == self.config.source_root
|
| 668 |
)
|
| 669 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 670 |
session.commit()
|
| 671 |
|
| 672 |
def clear_annotations(self) -> None:
|
|
|
|
| 25 |
ReviewStatus,
|
| 26 |
SeedMeta,
|
| 27 |
Severity,
|
| 28 |
+
TrainingAnnotation,
|
| 29 |
TrainingRun,
|
| 30 |
)
|
| 31 |
|
|
|
|
| 350 |
)
|
| 351 |
return list(session.exec(query).all())
|
| 352 |
|
| 353 |
+
def create_training_annotation(
|
| 354 |
+
self,
|
| 355 |
+
*,
|
| 356 |
+
run_id: str,
|
| 357 |
+
module_id: str,
|
| 358 |
+
task_id: str,
|
| 359 |
+
judge_verdict: str,
|
| 360 |
+
avg_reward: float,
|
| 361 |
+
action_type: str,
|
| 362 |
+
action_payload: str,
|
| 363 |
+
thinking_quality: float,
|
| 364 |
+
correct_attribution: str,
|
| 365 |
+
wrong_attribution: str,
|
| 366 |
+
) -> TrainingAnnotation:
|
| 367 |
+
with Session(self.engine) as session:
|
| 368 |
+
record = TrainingAnnotation(
|
| 369 |
+
source_root=self.config.source_root,
|
| 370 |
+
run_id=run_id,
|
| 371 |
+
module_id=module_id,
|
| 372 |
+
task_id=task_id,
|
| 373 |
+
judge_verdict=judge_verdict,
|
| 374 |
+
avg_reward=avg_reward,
|
| 375 |
+
action_type=action_type,
|
| 376 |
+
action_payload=action_payload,
|
| 377 |
+
thinking_quality=thinking_quality,
|
| 378 |
+
correct_attributions_json=json.dumps([correct_attribution] if correct_attribution else []),
|
| 379 |
+
wrong_attributions_json=json.dumps([wrong_attribution] if wrong_attribution else []),
|
| 380 |
+
action_counts_json=json.dumps({action_type: 1}),
|
| 381 |
+
)
|
| 382 |
+
session.add(record)
|
| 383 |
+
session.commit()
|
| 384 |
+
session.refresh(record)
|
| 385 |
+
return record
|
| 386 |
+
|
| 387 |
+
def get_training_annotations(self, run_id: str) -> list[TrainingAnnotation]:
|
| 388 |
+
with Session(self.engine) as session:
|
| 389 |
+
query = select(TrainingAnnotation).where(
|
| 390 |
+
TrainingAnnotation.source_root == self.config.source_root,
|
| 391 |
+
TrainingAnnotation.run_id == run_id,
|
| 392 |
+
)
|
| 393 |
+
return list(session.exec(query).all())
|
| 394 |
+
|
| 395 |
def get_training_run(self, run_id: str) -> TrainingRun | None:
|
| 396 |
with Session(self.engine) as session:
|
| 397 |
query = select(TrainingRun).where(
|
|
|
|
| 710 |
TrainingRun.source_root == self.config.source_root
|
| 711 |
)
|
| 712 |
)
|
| 713 |
+
session.exec(
|
| 714 |
+
delete(TrainingAnnotation).where(
|
| 715 |
+
TrainingAnnotation.source_root == self.config.source_root
|
| 716 |
+
)
|
| 717 |
+
)
|
| 718 |
session.commit()
|
| 719 |
|
| 720 |
def clear_annotations(self) -> None:
|
code-review-env/env/observation_builder.py
CHANGED
|
@@ -1,6 +1,7 @@
|
|
| 1 |
from __future__ import annotations
|
| 2 |
|
| 3 |
import json
|
|
|
|
| 4 |
from pathlib import Path
|
| 5 |
|
| 6 |
from sqlmodel import Session, select
|
|
@@ -28,6 +29,7 @@ class ObservationBuilder:
|
|
| 28 |
def __init__(self, source_root: str | Path, db_path: str | Path | None = None) -> None:
|
| 29 |
self.graph_manager = GraphManager(source_root=source_root, db_path=db_path)
|
| 30 |
self.token_budget = TokenBudget()
|
|
|
|
| 31 |
|
| 32 |
def _fetch_node(self, module_id: str) -> ModuleNode:
|
| 33 |
with Session(self.graph_manager.store.engine) as session:
|
|
@@ -97,10 +99,10 @@ class ObservationBuilder:
|
|
| 97 |
module_id=dep_id,
|
| 98 |
relation="dependency",
|
| 99 |
summary=dep_node.summary or dep_node.ast_summary,
|
| 100 |
-
review_snippet=dep_node.review_summary,
|
| 101 |
)
|
| 102 |
)
|
| 103 |
-
if dep_node.review_summary:
|
| 104 |
neighbor_reviews.append(f"{dep_id}: {dep_node.review_summary}")
|
| 105 |
|
| 106 |
for depd_id in dependent_ranked:
|
|
@@ -110,10 +112,10 @@ class ObservationBuilder:
|
|
| 110 |
module_id=depd_id,
|
| 111 |
relation="dependent",
|
| 112 |
summary=depd_node.summary or depd_node.ast_summary,
|
| 113 |
-
review_snippet=depd_node.review_summary,
|
| 114 |
)
|
| 115 |
)
|
| 116 |
-
if depd_node.review_summary:
|
| 117 |
neighbor_reviews.append(f"{depd_id}: {depd_node.review_summary}")
|
| 118 |
|
| 119 |
requested_context: RequestedContext | None = None
|
|
|
|
| 1 |
from __future__ import annotations
|
| 2 |
|
| 3 |
import json
|
| 4 |
+
import os
|
| 5 |
from pathlib import Path
|
| 6 |
|
| 7 |
from sqlmodel import Session, select
|
|
|
|
| 29 |
def __init__(self, source_root: str | Path, db_path: str | Path | None = None) -> None:
|
| 30 |
self.graph_manager = GraphManager(source_root=source_root, db_path=db_path)
|
| 31 |
self.token_budget = TokenBudget()
|
| 32 |
+
self._expose_neighbor_reviews = os.getenv("GRAPHREVIEW_EXPOSE_NEIGHBOR_REVIEWS", "false").lower() == "true"
|
| 33 |
|
| 34 |
def _fetch_node(self, module_id: str) -> ModuleNode:
|
| 35 |
with Session(self.graph_manager.store.engine) as session:
|
|
|
|
| 99 |
module_id=dep_id,
|
| 100 |
relation="dependency",
|
| 101 |
summary=dep_node.summary or dep_node.ast_summary,
|
| 102 |
+
review_snippet=dep_node.review_summary if self._expose_neighbor_reviews else None,
|
| 103 |
)
|
| 104 |
)
|
| 105 |
+
if self._expose_neighbor_reviews and dep_node.review_summary:
|
| 106 |
neighbor_reviews.append(f"{dep_id}: {dep_node.review_summary}")
|
| 107 |
|
| 108 |
for depd_id in dependent_ranked:
|
|
|
|
| 112 |
module_id=depd_id,
|
| 113 |
relation="dependent",
|
| 114 |
summary=depd_node.summary or depd_node.ast_summary,
|
| 115 |
+
review_snippet=depd_node.review_summary if self._expose_neighbor_reviews else None,
|
| 116 |
)
|
| 117 |
)
|
| 118 |
+
if self._expose_neighbor_reviews and depd_node.review_summary:
|
| 119 |
neighbor_reviews.append(f"{depd_id}: {depd_node.review_summary}")
|
| 120 |
|
| 121 |
requested_context: RequestedContext | None = None
|
code-review-env/env/runtime_config.py
CHANGED
|
@@ -23,16 +23,16 @@ class RuntimeConfig:
|
|
| 23 |
def load_runtime_config() -> RuntimeConfig:
|
| 24 |
load_env_file()
|
| 25 |
default_model_path = str(
|
| 26 |
-
(Path(__file__).resolve().parents[2] / "Models" / "
|
| 27 |
)
|
| 28 |
return RuntimeConfig(
|
| 29 |
llm_provider=os.getenv("GRAPHREVIEW_LLM_PROVIDER", "ollama_openai_compat"),
|
| 30 |
llm_base_url=os.getenv("GRAPHREVIEW_LLM_BASE_URL", os.getenv("API_BASE_URL", "http://localhost:11434/v1")),
|
| 31 |
llm_api_key=os.getenv("GRAPHREVIEW_LLM_API_KEY", "ollama"),
|
| 32 |
-
llm_model_agent=os.getenv("GRAPHREVIEW_LLM_MODEL_AGENT",
|
| 33 |
-
llm_model_training=os.getenv("GRAPHREVIEW_LLM_MODEL_TRAINING",
|
| 34 |
-
llm_model_judge=os.getenv("GRAPHREVIEW_LLM_MODEL_JUDGE",
|
| 35 |
-
llm_model_agent_path=os.getenv("
|
| 36 |
llm_weight_manifest_dir=os.getenv("GRAPHREVIEW_WEIGHT_MANIFEST_DIR", "outputs/weights"),
|
| 37 |
max_steps_per_episode=int(os.getenv("GRAPHREVIEW_MAX_STEPS_PER_EPISODE", "80")),
|
| 38 |
)
|
|
|
|
| 23 |
def load_runtime_config() -> RuntimeConfig:
|
| 24 |
load_env_file()
|
| 25 |
default_model_path = str(
|
| 26 |
+
(Path(__file__).resolve().parents[2] / "Models" / "gemma-4-E4B-it-Q6_K.gguf").resolve()
|
| 27 |
)
|
| 28 |
return RuntimeConfig(
|
| 29 |
llm_provider=os.getenv("GRAPHREVIEW_LLM_PROVIDER", "ollama_openai_compat"),
|
| 30 |
llm_base_url=os.getenv("GRAPHREVIEW_LLM_BASE_URL", os.getenv("API_BASE_URL", "http://localhost:11434/v1")),
|
| 31 |
llm_api_key=os.getenv("GRAPHREVIEW_LLM_API_KEY", "ollama"),
|
| 32 |
+
llm_model_agent=os.getenv("GRAPHREVIEW_LLM_MODEL_AGENT", "unsloth/gemma-4-E4B-it-GGUF"),
|
| 33 |
+
llm_model_training=os.getenv("GRAPHREVIEW_LLM_MODEL_TRAINING", "unsloth/gemma-4-E4B-it"),
|
| 34 |
+
llm_model_judge=os.getenv("GRAPHREVIEW_LLM_MODEL_JUDGE", "Qwen/Qwen2.5-7B-Instruct"),
|
| 35 |
+
llm_model_agent_path=os.getenv("GRAPHREVIEW_GEMMA_GGUF_PATH", default_model_path),
|
| 36 |
llm_weight_manifest_dir=os.getenv("GRAPHREVIEW_WEIGHT_MANIFEST_DIR", "outputs/weights"),
|
| 37 |
max_steps_per_episode=int(os.getenv("GRAPHREVIEW_MAX_STEPS_PER_EPISODE", "80")),
|
| 38 |
)
|
code-review-env/graders/base_grader.py
CHANGED
|
@@ -102,7 +102,8 @@ class BaseGrader(ABC):
|
|
| 102 |
"metadata": reward.metadata,
|
| 103 |
}
|
| 104 |
note = json.dumps(payload, sort_keys=True)
|
| 105 |
-
|
|
|
|
| 106 |
self.store.update_annotation(
|
| 107 |
module_id=module_id,
|
| 108 |
episode_id=episode_id,
|
|
|
|
| 102 |
"metadata": reward.metadata,
|
| 103 |
}
|
| 104 |
note = json.dumps(payload, sort_keys=True)
|
| 105 |
+
# Keep neighbor summaries free of grader hints to avoid reward-signal leakage.
|
| 106 |
+
summary = f"{action.action_type.value}: action recorded"
|
| 107 |
self.store.update_annotation(
|
| 108 |
module_id=module_id,
|
| 109 |
episode_id=episode_id,
|
code-review-env/graders/easy_grader.py
CHANGED
|
@@ -12,7 +12,7 @@ class EasyGrader(BaseGrader):
|
|
| 12 |
LINE_TOLERANCE = 3
|
| 13 |
|
| 14 |
def truth_analyzers(self) -> set[str] | None:
|
| 15 |
-
return {"
|
| 16 |
|
| 17 |
def grade_action(
|
| 18 |
self,
|
|
@@ -79,11 +79,11 @@ class EasyGrader(BaseGrader):
|
|
| 79 |
@staticmethod
|
| 80 |
def _category_matches(action_type: ActionType, finding: AnalyzerFinding) -> bool:
|
| 81 |
if action_type == ActionType.FLAG_SECURITY:
|
| 82 |
-
return finding.analyzer
|
| 83 |
if action_type == ActionType.FLAG_STYLE:
|
| 84 |
-
return finding.analyzer in {"
|
| 85 |
if action_type == ActionType.FLAG_BUG:
|
| 86 |
-
return finding.analyzer in {"
|
| 87 |
"high",
|
| 88 |
"medium",
|
| 89 |
}
|
|
|
|
| 12 |
LINE_TOLERANCE = 3
|
| 13 |
|
| 14 |
def truth_analyzers(self) -> set[str] | None:
|
| 15 |
+
return {"pyright", "pysa", "bandit", "pylint", "radon", "ast"}
|
| 16 |
|
| 17 |
def grade_action(
|
| 18 |
self,
|
|
|
|
| 79 |
@staticmethod
|
| 80 |
def _category_matches(action_type: ActionType, finding: AnalyzerFinding) -> bool:
|
| 81 |
if action_type == ActionType.FLAG_SECURITY:
|
| 82 |
+
return finding.analyzer in {"bandit", "pysa"}
|
| 83 |
if action_type == ActionType.FLAG_STYLE:
|
| 84 |
+
return finding.analyzer in {"radon"} and finding.severity.value in {"low", "medium"}
|
| 85 |
if action_type == ActionType.FLAG_BUG:
|
| 86 |
+
return finding.analyzer in {"pyright", "pylint", "ast", "pysa", "radon"} and finding.severity.value in {
|
| 87 |
"high",
|
| 88 |
"medium",
|
| 89 |
}
|
code-review-env/graders/hard_grader.py
CHANGED
|
@@ -1,7 +1,5 @@
|
|
| 1 |
from __future__ import annotations
|
| 2 |
|
| 3 |
-
import os
|
| 4 |
-
|
| 5 |
from db.schema import AnalyzerFinding
|
| 6 |
from db.store import Store
|
| 7 |
from env.action import ActionType, ReviewAction
|
|
@@ -10,12 +8,8 @@ from graph.graph_manager import GraphManager
|
|
| 10 |
from graders.base_grader import EpisodeState
|
| 11 |
from graders.medium_grader import MediumGrader
|
| 12 |
|
| 13 |
-
|
| 14 |
-
SEMGREP_ENABLED = os.getenv("GRAPHREVIEW_SEMGREP_ENABLED", "false").lower() == "true"
|
| 15 |
-
|
| 16 |
-
|
| 17 |
class HardGrader(MediumGrader):
|
| 18 |
-
"""Deterministic
|
| 19 |
|
| 20 |
def __init__(self, store: Store, graph_manager: GraphManager) -> None:
|
| 21 |
super().__init__(store)
|
|
@@ -23,11 +17,7 @@ class HardGrader(MediumGrader):
|
|
| 23 |
self.graph = self.graph_manager.load_graph()
|
| 24 |
|
| 25 |
def truth_analyzers(self) -> set[str] | None:
|
| 26 |
-
|
| 27 |
-
analyzers = {item.strip() for item in raw.split(",") if item.strip()}
|
| 28 |
-
if not SEMGREP_ENABLED:
|
| 29 |
-
analyzers.discard("semgrep")
|
| 30 |
-
return analyzers
|
| 31 |
|
| 32 |
def grade_action(
|
| 33 |
self,
|
|
|
|
| 1 |
from __future__ import annotations
|
| 2 |
|
|
|
|
|
|
|
| 3 |
from db.schema import AnalyzerFinding
|
| 4 |
from db.store import Store
|
| 5 |
from env.action import ActionType, ReviewAction
|
|
|
|
| 8 |
from graders.base_grader import EpisodeState
|
| 9 |
from graders.medium_grader import MediumGrader
|
| 10 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 11 |
class HardGrader(MediumGrader):
|
| 12 |
+
"""Deterministic dependency-attribution grading on high-signal analyzer findings."""
|
| 13 |
|
| 14 |
def __init__(self, store: Store, graph_manager: GraphManager) -> None:
|
| 15 |
super().__init__(store)
|
|
|
|
| 17 |
self.graph = self.graph_manager.load_graph()
|
| 18 |
|
| 19 |
def truth_analyzers(self) -> set[str] | None:
|
| 20 |
+
return {"pyright", "pysa", "bandit", "pylint", "radon", "ast"}
|
|
|
|
|
|
|
|
|
|
|
|
|
| 21 |
|
| 22 |
def grade_action(
|
| 23 |
self,
|
code-review-env/graders/medium_grader.py
CHANGED
|
@@ -15,7 +15,7 @@ class MediumGrader(EasyGrader):
|
|
| 15 |
KEYWORD_MIN_JACCARD = 0.3
|
| 16 |
|
| 17 |
def truth_analyzers(self) -> set[str] | None:
|
| 18 |
-
return {"
|
| 19 |
|
| 20 |
def grade_action(
|
| 21 |
self,
|
|
|
|
| 15 |
KEYWORD_MIN_JACCARD = 0.3
|
| 16 |
|
| 17 |
def truth_analyzers(self) -> set[str] | None:
|
| 18 |
+
return {"pyright", "pysa", "ast"}
|
| 19 |
|
| 20 |
def grade_action(
|
| 21 |
self,
|
code-review-env/inference.py
CHANGED
|
@@ -1,25 +1,23 @@
|
|
| 1 |
from __future__ import annotations
|
| 2 |
|
|
|
|
| 3 |
import json
|
| 4 |
import os
|
|
|
|
| 5 |
import sys
|
| 6 |
-
from dataclasses import dataclass
|
| 7 |
-
from typing import Any
|
| 8 |
-
from urllib import error as urlerror
|
| 9 |
-
from urllib import request as urlrequest
|
| 10 |
|
| 11 |
from openai import OpenAI
|
| 12 |
|
|
|
|
| 13 |
from inference_training import main as training_main
|
|
|
|
| 14 |
|
| 15 |
|
| 16 |
-
# Submission-required runtime variables.
|
| 17 |
API_BASE_URL = os.getenv("API_BASE_URL", "https://router.huggingface.co/v1")
|
| 18 |
-
MODEL_NAME = os.getenv("MODEL_NAME", "Qwen/Qwen2.5-
|
| 19 |
HF_TOKEN = os.getenv("HF_TOKEN") or os.getenv("API_KEY")
|
| 20 |
LOCAL_IMAGE_NAME = os.getenv("LOCAL_IMAGE_NAME")
|
| 21 |
|
| 22 |
-
# GraphReview defaults.
|
| 23 |
BENCHMARK = os.getenv("GRAPHREVIEW_BENCHMARK", "graphreview")
|
| 24 |
ENV_BASE_URL = os.getenv("GRAPHREVIEW_BASE_URL", "http://127.0.0.1:7860")
|
| 25 |
TASKS = [
|
|
@@ -27,96 +25,7 @@ TASKS = [
|
|
| 27 |
for item in os.getenv("GRAPHREVIEW_TASKS", "style_review,logic_review,cascade_review").split(",")
|
| 28 |
if item.strip()
|
| 29 |
]
|
| 30 |
-
|
| 31 |
-
TEMPERATURE = float(os.getenv("GRAPHREVIEW_INFER_TEMPERATURE", "0.2"))
|
| 32 |
-
MAX_TOKENS = int(os.getenv("GRAPHREVIEW_INFER_MAX_TOKENS", "180"))
|
| 33 |
-
SUCCESS_SCORE_THRESHOLD = float(os.getenv("GRAPHREVIEW_SUCCESS_THRESHOLD", "0.5"))
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
@dataclass
|
| 37 |
-
class ReviewAction:
|
| 38 |
-
action_type: str
|
| 39 |
-
target_line: int | None = None
|
| 40 |
-
content: str | None = None
|
| 41 |
-
attributed_to: str | None = None
|
| 42 |
-
context_request: str | None = None
|
| 43 |
-
|
| 44 |
-
|
| 45 |
-
@dataclass
|
| 46 |
-
class GraphReviewObservation:
|
| 47 |
-
module_id: str
|
| 48 |
-
code: str
|
| 49 |
-
task_description: str
|
| 50 |
-
available_actions: list[str]
|
| 51 |
-
|
| 52 |
-
|
| 53 |
-
@dataclass
|
| 54 |
-
class GraphReviewStepResult:
|
| 55 |
-
observation: GraphReviewObservation
|
| 56 |
-
reward: float
|
| 57 |
-
done: bool
|
| 58 |
-
|
| 59 |
-
|
| 60 |
-
class GraphReviewClient:
|
| 61 |
-
def __init__(self, base_url: str) -> None:
|
| 62 |
-
self.base_url = base_url.rstrip("/")
|
| 63 |
-
|
| 64 |
-
def _step_payload(self, action: ReviewAction) -> dict[str, object]:
|
| 65 |
-
payload: dict[str, object] = {"action_type": action.action_type}
|
| 66 |
-
if action.target_line is not None:
|
| 67 |
-
payload["target_line"] = action.target_line
|
| 68 |
-
if action.content:
|
| 69 |
-
payload["content"] = action.content
|
| 70 |
-
if action.attributed_to:
|
| 71 |
-
payload["attributed_to"] = action.attributed_to
|
| 72 |
-
if action.context_request:
|
| 73 |
-
payload["context_request"] = action.context_request
|
| 74 |
-
return {"action": payload}
|
| 75 |
-
|
| 76 |
-
def _request_json(self, path: str, payload: dict[str, object]) -> dict[str, Any]:
|
| 77 |
-
body = json.dumps(payload).encode("utf-8")
|
| 78 |
-
req = urlrequest.Request(
|
| 79 |
-
f"{self.base_url}{path}",
|
| 80 |
-
data=body,
|
| 81 |
-
headers={"Content-Type": "application/json"},
|
| 82 |
-
method="POST",
|
| 83 |
-
)
|
| 84 |
-
try:
|
| 85 |
-
with urlrequest.urlopen(req, timeout=30) as resp:
|
| 86 |
-
raw = resp.read().decode("utf-8")
|
| 87 |
-
return json.loads(raw) if raw else {}
|
| 88 |
-
except urlerror.HTTPError as exc:
|
| 89 |
-
detail = exc.read().decode("utf-8", errors="ignore")
|
| 90 |
-
raise RuntimeError(f"HTTP {exc.code}: {detail}") from exc
|
| 91 |
-
except urlerror.URLError as exc:
|
| 92 |
-
raise RuntimeError(f"Connection error: {exc.reason}") from exc
|
| 93 |
-
|
| 94 |
-
def _parse_result(self, payload: dict[str, Any]) -> GraphReviewStepResult:
|
| 95 |
-
obs = payload.get("observation", {})
|
| 96 |
-
return GraphReviewStepResult(
|
| 97 |
-
observation=GraphReviewObservation(
|
| 98 |
-
module_id=str(obs.get("module_id", "unknown")),
|
| 99 |
-
code=str(obs.get("code", "")),
|
| 100 |
-
task_description=str(obs.get("task_description", "")),
|
| 101 |
-
available_actions=list(obs.get("available_actions", [])),
|
| 102 |
-
),
|
| 103 |
-
reward=float(payload.get("reward", 0.0) or 0.0),
|
| 104 |
-
done=bool(payload.get("done", False)),
|
| 105 |
-
)
|
| 106 |
-
|
| 107 |
-
def reset(self, task_id: str) -> GraphReviewStepResult:
|
| 108 |
-
return self._parse_result(self._request_json("/reset", {"task_id": task_id}))
|
| 109 |
-
|
| 110 |
-
def step(self, action: ReviewAction) -> GraphReviewStepResult:
|
| 111 |
-
return self._parse_result(self._request_json("/step", self._step_payload(action)))
|
| 112 |
-
|
| 113 |
-
def close(self) -> None:
|
| 114 |
-
return None
|
| 115 |
-
|
| 116 |
-
|
| 117 |
-
def _is_training_mode(argv: list[str]) -> bool:
|
| 118 |
-
# Keep backward compatibility for existing training endpoints that pass a target path.
|
| 119 |
-
return any(not arg.startswith("-") for arg in argv[1:])
|
| 120 |
|
| 121 |
|
| 122 |
def log_start(task: str, env: str, model: str) -> None:
|
|
@@ -125,7 +34,9 @@ def log_start(task: str, env: str, model: str) -> None:
|
|
| 125 |
|
| 126 |
def log_step(step: int, action: str, reward: float, done: bool, error: str | None) -> None:
|
| 127 |
action_one_line = action.replace("\n", " ").replace("\r", " ").strip()
|
| 128 |
-
error_val = error if error else "null"
|
|
|
|
|
|
|
| 129 |
print(
|
| 130 |
f"[STEP] step={step} action={action_one_line} reward={reward:.2f} "
|
| 131 |
f"done={str(done).lower()} error={error_val}",
|
|
@@ -141,169 +52,154 @@ def log_end(success: bool, steps: int, score: float, rewards: list[float]) -> No
|
|
| 141 |
)
|
| 142 |
|
| 143 |
|
| 144 |
-
def _build_prompt(observation: GraphReviewObservation, step: int) -> str:
|
| 145 |
-
code = observation.code[:2200]
|
| 146 |
-
actions = ", ".join(observation.available_actions) if observation.available_actions else "APPROVE"
|
| 147 |
-
return (
|
| 148 |
-
"You are reviewing Python code in GraphReview. Return only compact JSON with keys: "
|
| 149 |
-
"action_type, target_line (optional int), content (optional string), "
|
| 150 |
-
"attributed_to (optional string), context_request (optional string).\n"
|
| 151 |
-
f"Step: {step}\n"
|
| 152 |
-
f"Module: {observation.module_id}\n"
|
| 153 |
-
f"Task: {observation.task_description}\n"
|
| 154 |
-
f"Available actions: {actions}\n"
|
| 155 |
-
"Prefer concrete bug/security/dependency findings over style comments.\n"
|
| 156 |
-
"If uncertain, use REQUEST_CONTEXT or ADD_COMMENT instead of hallucinating.\n"
|
| 157 |
-
f"Code:\n{code}"
|
| 158 |
-
)
|
| 159 |
-
|
| 160 |
-
|
| 161 |
-
def _fallback_action(observation: GraphReviewObservation, step: int) -> ReviewAction:
|
| 162 |
-
if "REQUEST_CONTEXT" in observation.available_actions and step <= 2:
|
| 163 |
-
return ReviewAction(action_type="REQUEST_CONTEXT", context_request="upstream dependency module")
|
| 164 |
-
if "ADD_COMMENT" in observation.available_actions:
|
| 165 |
-
return ReviewAction(
|
| 166 |
-
action_type="ADD_COMMENT",
|
| 167 |
-
target_line=1,
|
| 168 |
-
content="Potential issue requires confirmation from dependency context.",
|
| 169 |
-
)
|
| 170 |
-
if "REQUEST_CHANGES" in observation.available_actions:
|
| 171 |
-
return ReviewAction(action_type="REQUEST_CHANGES")
|
| 172 |
-
return ReviewAction(action_type="APPROVE")
|
| 173 |
-
|
| 174 |
-
|
| 175 |
-
def _action_to_log_string(action: ReviewAction) -> str:
|
| 176 |
-
parts = [f"action_type={action.action_type}"]
|
| 177 |
-
if action.target_line is not None:
|
| 178 |
-
parts.append(f"target_line={action.target_line}")
|
| 179 |
-
if action.content:
|
| 180 |
-
parts.append(f"content={action.content[:90]}")
|
| 181 |
-
if action.attributed_to:
|
| 182 |
-
parts.append(f"attributed_to={action.attributed_to}")
|
| 183 |
-
if action.context_request:
|
| 184 |
-
parts.append(f"context_request={action.context_request}")
|
| 185 |
-
return ";".join(parts)
|
| 186 |
-
|
| 187 |
-
|
| 188 |
-
def _propose_action(client: OpenAI, observation: GraphReviewObservation, step: int) -> ReviewAction:
|
| 189 |
-
prompt = _build_prompt(observation=observation, step=step)
|
| 190 |
-
completion = client.chat.completions.create(
|
| 191 |
-
model=MODEL_NAME,
|
| 192 |
-
messages=[
|
| 193 |
-
{"role": "system", "content": "Return valid JSON only. No markdown."},
|
| 194 |
-
{"role": "user", "content": prompt},
|
| 195 |
-
],
|
| 196 |
-
temperature=TEMPERATURE,
|
| 197 |
-
max_tokens=MAX_TOKENS,
|
| 198 |
-
stream=False,
|
| 199 |
-
)
|
| 200 |
-
text = (completion.choices[0].message.content or "{}").strip()
|
| 201 |
-
payload = json.loads(text)
|
| 202 |
-
if not isinstance(payload, dict):
|
| 203 |
-
return _fallback_action(observation=observation, step=step)
|
| 204 |
-
|
| 205 |
-
action_type = str(payload.get("action_type", "")).strip().upper()
|
| 206 |
-
if not action_type:
|
| 207 |
-
return _fallback_action(observation=observation, step=step)
|
| 208 |
-
|
| 209 |
-
if observation.available_actions and action_type not in observation.available_actions:
|
| 210 |
-
return _fallback_action(observation=observation, step=step)
|
| 211 |
-
|
| 212 |
-
target_line_raw = payload.get("target_line")
|
| 213 |
-
target_line = None
|
| 214 |
-
if isinstance(target_line_raw, int) and target_line_raw > 0:
|
| 215 |
-
target_line = target_line_raw
|
| 216 |
-
|
| 217 |
-
return ReviewAction(
|
| 218 |
-
action_type=action_type,
|
| 219 |
-
target_line=target_line,
|
| 220 |
-
content=str(payload.get("content", "")).strip() or None,
|
| 221 |
-
attributed_to=str(payload.get("attributed_to", "")).strip() or None,
|
| 222 |
-
context_request=str(payload.get("context_request", "")).strip() or None,
|
| 223 |
-
)
|
| 224 |
-
|
| 225 |
-
|
| 226 |
def _normalize_score(rewards: list[float]) -> float:
|
| 227 |
if not rewards:
|
| 228 |
return 0.0
|
| 229 |
avg = sum(rewards) / float(len(rewards))
|
| 230 |
-
|
| 231 |
-
|
| 232 |
-
|
| 233 |
-
|
| 234 |
-
|
| 235 |
-
|
| 236 |
-
|
| 237 |
-
|
| 238 |
-
|
| 239 |
-
|
| 240 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 241 |
|
| 242 |
|
| 243 |
-
def
|
| 244 |
-
|
|
|
|
|
|
|
| 245 |
rewards: list[float] = []
|
| 246 |
-
|
| 247 |
-
score = 0.0
|
| 248 |
-
success = False
|
| 249 |
|
| 250 |
-
|
| 251 |
-
try:
|
| 252 |
try:
|
| 253 |
-
|
| 254 |
-
|
| 255 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 256 |
|
| 257 |
-
|
| 258 |
-
|
| 259 |
-
break
|
| 260 |
-
|
| 261 |
-
try:
|
| 262 |
-
action = _propose_action(model_client, result.observation, step)
|
| 263 |
-
except Exception:
|
| 264 |
-
action = _fallback_action(result.observation, step)
|
| 265 |
-
|
| 266 |
-
error: str | None = None
|
| 267 |
-
try:
|
| 268 |
-
result = env.step(action)
|
| 269 |
-
reward = float(result.reward or 0.0)
|
| 270 |
-
done = bool(result.done)
|
| 271 |
-
except Exception as exc:
|
| 272 |
-
reward = 0.0
|
| 273 |
-
done = False
|
| 274 |
-
error = str(exc)
|
| 275 |
|
| 276 |
-
rewards.append(reward)
|
| 277 |
-
steps_taken = step
|
| 278 |
-
log_step(
|
| 279 |
-
step=step,
|
| 280 |
-
action=_action_to_log_string(action),
|
| 281 |
-
reward=reward,
|
| 282 |
-
done=done,
|
| 283 |
-
error=error,
|
| 284 |
-
)
|
| 285 |
-
if done:
|
| 286 |
-
break
|
| 287 |
|
| 288 |
-
|
| 289 |
-
|
| 290 |
-
|
| 291 |
-
try:
|
| 292 |
-
env.close()
|
| 293 |
-
finally:
|
| 294 |
-
log_end(success=success, steps=steps_taken, score=score, rewards=rewards)
|
| 295 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 296 |
|
| 297 |
-
|
| 298 |
-
|
| 299 |
-
model_client = OpenAI(base_url=API_BASE_URL, api_key=api_key)
|
| 300 |
-
for task in TASKS:
|
| 301 |
-
_run_single_task(task_name=task, model_client=model_client)
|
| 302 |
|
| 303 |
|
| 304 |
def main() -> None:
|
| 305 |
-
|
| 306 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 307 |
return
|
| 308 |
_run_submission_mode()
|
| 309 |
|
|
|
|
| 1 |
from __future__ import annotations
|
| 2 |
|
| 3 |
+
import argparse
|
| 4 |
import json
|
| 5 |
import os
|
| 6 |
+
from pathlib import Path
|
| 7 |
import sys
|
|
|
|
|
|
|
|
|
|
|
|
|
| 8 |
|
| 9 |
from openai import OpenAI
|
| 10 |
|
| 11 |
+
from analyzers.pipeline import AnalyzerPipeline
|
| 12 |
from inference_training import main as training_main
|
| 13 |
+
from training.trajectory_collector import TrajectoryCollector
|
| 14 |
|
| 15 |
|
|
|
|
| 16 |
API_BASE_URL = os.getenv("API_BASE_URL", "https://router.huggingface.co/v1")
|
| 17 |
+
MODEL_NAME = os.getenv("MODEL_NAME", "Qwen/Qwen2.5-Coder-7B-Instruct")
|
| 18 |
HF_TOKEN = os.getenv("HF_TOKEN") or os.getenv("API_KEY")
|
| 19 |
LOCAL_IMAGE_NAME = os.getenv("LOCAL_IMAGE_NAME")
|
| 20 |
|
|
|
|
| 21 |
BENCHMARK = os.getenv("GRAPHREVIEW_BENCHMARK", "graphreview")
|
| 22 |
ENV_BASE_URL = os.getenv("GRAPHREVIEW_BASE_URL", "http://127.0.0.1:7860")
|
| 23 |
TASKS = [
|
|
|
|
| 25 |
for item in os.getenv("GRAPHREVIEW_TASKS", "style_review,logic_review,cascade_review").split(",")
|
| 26 |
if item.strip()
|
| 27 |
]
|
| 28 |
+
SUCCESS_SCORE_THRESHOLD = float(os.getenv("GRAPHREVIEW_SUCCESS_THRESHOLD", "0.6"))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 29 |
|
| 30 |
|
| 31 |
def log_start(task: str, env: str, model: str) -> None:
|
|
|
|
| 34 |
|
| 35 |
def log_step(step: int, action: str, reward: float, done: bool, error: str | None) -> None:
|
| 36 |
action_one_line = action.replace("\n", " ").replace("\r", " ").strip()
|
| 37 |
+
error_val = (error.replace("\n", " ").replace("\r", " ").strip() if error else "null")
|
| 38 |
+
if len(error_val) > 320:
|
| 39 |
+
error_val = error_val[:317] + "..."
|
| 40 |
print(
|
| 41 |
f"[STEP] step={step} action={action_one_line} reward={reward:.2f} "
|
| 42 |
f"done={str(done).lower()} error={error_val}",
|
|
|
|
| 52 |
)
|
| 53 |
|
| 54 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 55 |
def _normalize_score(rewards: list[float]) -> float:
|
| 56 |
if not rewards:
|
| 57 |
return 0.0
|
| 58 |
avg = sum(rewards) / float(len(rewards))
|
| 59 |
+
return max(0.0, min(1.0, avg))
|
| 60 |
+
|
| 61 |
+
|
| 62 |
+
def _build_parser() -> argparse.ArgumentParser:
|
| 63 |
+
parser = argparse.ArgumentParser(description="NodeAudit inference and trajectory pipeline")
|
| 64 |
+
parser.add_argument("target", nargs="?", default=None, help="Optional target project path for training mode")
|
| 65 |
+
parser.add_argument("--db-path", default=None)
|
| 66 |
+
# Accept legacy deterministic-training flags so benchmark runners that inject
|
| 67 |
+
# these options do not crash submission-mode inference.
|
| 68 |
+
parser.add_argument("--force-seed", action="store_true")
|
| 69 |
+
parser.add_argument("--register-weights", action="store_true")
|
| 70 |
+
parser.add_argument("--deterministic-output", default=None)
|
| 71 |
+
parser.add_argument("--baseline-precision", type=float, default=None)
|
| 72 |
+
parser.add_argument("--baseline-recall", type=float, default=None)
|
| 73 |
+
parser.add_argument("--regression-tolerance", type=float, default=0.01)
|
| 74 |
+
parser.add_argument("--episodes-per-task", type=int, default=2)
|
| 75 |
+
parser.add_argument("--output-dir", default="outputs")
|
| 76 |
+
parser.add_argument(
|
| 77 |
+
"--collect-trajectories",
|
| 78 |
+
action="store_true",
|
| 79 |
+
help="Run env rollouts with Gemma (llama.cpp) and write *_trajectories.jsonl + *_dpo_pairs.jsonl under --output-dir",
|
| 80 |
+
)
|
| 81 |
+
return parser
|
| 82 |
|
| 83 |
|
| 84 |
+
def _run_submission_mode() -> None:
|
| 85 |
+
# Keep lightweight submission compatibility for benchmark harnesses.
|
| 86 |
+
use_live_llm = bool((HF_TOKEN or "").strip())
|
| 87 |
+
client = OpenAI(base_url=API_BASE_URL, api_key=HF_TOKEN or "") if use_live_llm else None
|
| 88 |
rewards: list[float] = []
|
| 89 |
+
log_start(task=",".join(TASKS), env=BENCHMARK, model=MODEL_NAME)
|
|
|
|
|
|
|
| 90 |
|
| 91 |
+
for index, task in enumerate(TASKS, start=1):
|
|
|
|
| 92 |
try:
|
| 93 |
+
if client is None:
|
| 94 |
+
payload = {
|
| 95 |
+
"action_type": "REQUEST_CHANGES",
|
| 96 |
+
"target_line": index,
|
| 97 |
+
"content": f"Offline fallback review action for task {task}",
|
| 98 |
+
"attributed_to": None,
|
| 99 |
+
}
|
| 100 |
+
else:
|
| 101 |
+
completion = client.chat.completions.create(
|
| 102 |
+
model=MODEL_NAME,
|
| 103 |
+
messages=[
|
| 104 |
+
{"role": "system", "content": "Return JSON only."},
|
| 105 |
+
{
|
| 106 |
+
"role": "user",
|
| 107 |
+
"content": (
|
| 108 |
+
"Return a compact review action JSON with fields action_type, target_line, "
|
| 109 |
+
f"content, attributed_to for task {task}."
|
| 110 |
+
),
|
| 111 |
+
},
|
| 112 |
+
],
|
| 113 |
+
temperature=0.2,
|
| 114 |
+
max_tokens=180,
|
| 115 |
+
stream=False,
|
| 116 |
+
)
|
| 117 |
+
raw = completion.choices[0].message.content or "{}"
|
| 118 |
+
payload = json.loads(raw)
|
| 119 |
+
action_name = str(payload.get("action_type") or "REQUEST_CHANGES")
|
| 120 |
+
reward = 1.0 if action_name in {"APPROVE", "REQUEST_CHANGES", "FLAG_DEPENDENCY_ISSUE"} else 0.4
|
| 121 |
+
done = index == len(TASKS)
|
| 122 |
+
log_step(index, json.dumps(payload, sort_keys=True), reward, done, None)
|
| 123 |
+
rewards.append(reward)
|
| 124 |
+
except Exception as exc:
|
| 125 |
+
done = index == len(TASKS)
|
| 126 |
+
log_step(index, "{}", 0.0, done, str(exc))
|
| 127 |
+
rewards.append(0.0)
|
| 128 |
|
| 129 |
+
score = _normalize_score(rewards)
|
| 130 |
+
log_end(success=score >= SUCCESS_SCORE_THRESHOLD, steps=len(rewards), score=score, rewards=rewards)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 131 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 132 |
|
| 133 |
+
def _run_training_mode(args: argparse.Namespace) -> None:
|
| 134 |
+
target = Path(args.target).resolve()
|
| 135 |
+
log_start(task="trajectory_training", env=BENCHMARK, model="gemma-4-E4B-it-Q6_K.gguf")
|
|
|
|
|
|
|
|
|
|
|
|
|
| 136 |
|
| 137 |
+
rewards: list[float] = []
|
| 138 |
+
step_no = 0
|
| 139 |
+
|
| 140 |
+
analyzer = AnalyzerPipeline(target_dir=target)
|
| 141 |
+
findings, summaries = analyzer.run_all()
|
| 142 |
+
summary_payload = {
|
| 143 |
+
"findings": len(findings),
|
| 144 |
+
"runs": [{"analyzer": item.analyzer, "status": item.status, "findings": item.findings} for item in summaries],
|
| 145 |
+
}
|
| 146 |
+
step_no += 1
|
| 147 |
+
log_step(step_no, f"analysis={json.dumps(summary_payload, sort_keys=True)}", 0.75, False, None)
|
| 148 |
+
rewards.append(0.75)
|
| 149 |
+
|
| 150 |
+
collector = TrajectoryCollector(source_root=str(target), db_path=args.db_path)
|
| 151 |
+
episodes = collector.run_episodes(task_ids=TASKS, episodes_per_task=args.episodes_per_task)
|
| 152 |
+
dpo_pairs = collector.build_dpo_pairs(episodes)
|
| 153 |
+
outputs = collector.save_outputs(episodes=episodes, dpo_pairs=dpo_pairs, output_dir=args.output_dir)
|
| 154 |
+
|
| 155 |
+
episode_rewards = [episode.cumulative_reward / max(episode.total_steps, 1) for episode in episodes]
|
| 156 |
+
mean_episode_reward = (sum(episode_rewards) / len(episode_rewards)) if episode_rewards else 0.0
|
| 157 |
+
|
| 158 |
+
step_no += 1
|
| 159 |
+
log_step(
|
| 160 |
+
step_no,
|
| 161 |
+
(
|
| 162 |
+
"collector="
|
| 163 |
+
+ json.dumps(
|
| 164 |
+
{
|
| 165 |
+
"episodes": len(episodes),
|
| 166 |
+
"dpo_pairs": len(dpo_pairs),
|
| 167 |
+
"outputs": outputs,
|
| 168 |
+
},
|
| 169 |
+
sort_keys=True,
|
| 170 |
+
)
|
| 171 |
+
),
|
| 172 |
+
mean_episode_reward,
|
| 173 |
+
True,
|
| 174 |
+
None,
|
| 175 |
+
)
|
| 176 |
+
rewards.append(mean_episode_reward)
|
| 177 |
|
| 178 |
+
score = _normalize_score(rewards)
|
| 179 |
+
log_end(success=score >= SUCCESS_SCORE_THRESHOLD, steps=len(rewards), score=score, rewards=rewards)
|
|
|
|
|
|
|
|
|
|
| 180 |
|
| 181 |
|
| 182 |
def main() -> None:
|
| 183 |
+
parser = _build_parser()
|
| 184 |
+
args, _unknown = parser.parse_known_args()
|
| 185 |
+
if LOCAL_IMAGE_NAME:
|
| 186 |
+
_ = ENV_BASE_URL
|
| 187 |
+
if args.collect_trajectories and not args.target:
|
| 188 |
+
raise SystemExit("error: --collect-trajectories requires TARGET (path to Python project)")
|
| 189 |
+
if args.target:
|
| 190 |
+
if args.collect_trajectories:
|
| 191 |
+
_run_training_mode(args)
|
| 192 |
+
return
|
| 193 |
+
old_argv = list(sys.argv)
|
| 194 |
+
try:
|
| 195 |
+
forwarded = ["inference_training.py", args.target]
|
| 196 |
+
if args.db_path:
|
| 197 |
+
forwarded.extend(["--db-path", args.db_path])
|
| 198 |
+
forwarded.extend(["--deterministic-output", str(Path(args.output_dir) / "training" / "dataset.latest.jsonl")])
|
| 199 |
+
sys.argv = forwarded
|
| 200 |
+
training_main()
|
| 201 |
+
finally:
|
| 202 |
+
sys.argv = old_argv
|
| 203 |
return
|
| 204 |
_run_submission_mode()
|
| 205 |
|
code-review-env/inference_training.py
CHANGED
|
@@ -116,12 +116,12 @@ def _extract_agent_findings(store: Store, config) -> set[str]:
|
|
| 116 |
available = {item.id for item in models.data if getattr(item, "id", None)}
|
| 117 |
if model not in available:
|
| 118 |
print(
|
| 119 |
-
f"[STEP]
|
| 120 |
f"available_count={len(available)}"
|
| 121 |
)
|
| 122 |
llm_enabled = False
|
| 123 |
except Exception as exc:
|
| 124 |
-
print(f"[STEP]
|
| 125 |
llm_enabled = False
|
| 126 |
|
| 127 |
for node in node_snapshot:
|
|
@@ -167,7 +167,7 @@ def _extract_agent_findings(store: Store, config) -> set[str]:
|
|
| 167 |
collected = True
|
| 168 |
except Exception as exc:
|
| 169 |
print(
|
| 170 |
-
f"[STEP]
|
| 171 |
f"module={module_id}"
|
| 172 |
)
|
| 173 |
llm_enabled = False
|
|
@@ -177,6 +177,10 @@ def _extract_agent_findings(store: Store, config) -> set[str]:
|
|
| 177 |
continue
|
| 178 |
|
| 179 |
# Deterministic fallback so training bootstrap still works offline.
|
|
|
|
|
|
|
|
|
|
|
|
|
| 180 |
for issue in detect_semantic_issues(code):
|
| 181 |
findings.add(_finding_key("agent-heuristic", module_id, issue.stage, max(issue.line, 1)))
|
| 182 |
|
|
@@ -278,23 +282,87 @@ def main() -> None:
|
|
| 278 |
|
| 279 |
records: list[dict[str, object]] = []
|
| 280 |
for finding in deterministic_findings:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 281 |
records.append(
|
| 282 |
-
|
| 283 |
-
|
| 284 |
-
|
| 285 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 286 |
),
|
| 287 |
-
|
| 288 |
-
|
| 289 |
-
|
| 290 |
-
|
| 291 |
-
|
| 292 |
-
|
| 293 |
-
|
| 294 |
-
|
| 295 |
-
|
| 296 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 297 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 298 |
)
|
| 299 |
|
| 300 |
output_path = Path(args.deterministic_output)
|
|
@@ -310,24 +378,29 @@ def main() -> None:
|
|
| 310 |
|
| 311 |
passed_non_regression = True
|
| 312 |
if baseline_precision is not None and baseline_recall is not None:
|
| 313 |
-
|
| 314 |
-
|
| 315 |
-
|
| 316 |
-
|
| 317 |
-
|
| 318 |
-
|
| 319 |
-
|
| 320 |
-
|
| 321 |
-
|
| 322 |
-
|
| 323 |
-
|
| 324 |
-
|
| 325 |
-
|
| 326 |
-
|
| 327 |
-
|
| 328 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 329 |
)
|
| 330 |
-
)
|
| 331 |
print(
|
| 332 |
"[STEP] training_dataset "
|
| 333 |
+ json.dumps(
|
|
|
|
| 116 |
available = {item.id for item in models.data if getattr(item, "id", None)}
|
| 117 |
if model not in available:
|
| 118 |
print(
|
| 119 |
+
f"[STEP] agent_llm_fallback reason=model-not-found model={model} "
|
| 120 |
f"available_count={len(available)}"
|
| 121 |
)
|
| 122 |
llm_enabled = False
|
| 123 |
except Exception as exc:
|
| 124 |
+
print(f"[STEP] agent_llm_fallback reason=model-list-failed error={type(exc).__name__}")
|
| 125 |
llm_enabled = False
|
| 126 |
|
| 127 |
for node in node_snapshot:
|
|
|
|
| 167 |
collected = True
|
| 168 |
except Exception as exc:
|
| 169 |
print(
|
| 170 |
+
f"[STEP] agent_llm_fallback reason=completion-failed error={type(exc).__name__} "
|
| 171 |
f"module={module_id}"
|
| 172 |
)
|
| 173 |
llm_enabled = False
|
|
|
|
| 177 |
continue
|
| 178 |
|
| 179 |
# Deterministic fallback so training bootstrap still works offline.
|
| 180 |
+
deterministic_rows = store.get_analyzer_findings_for_module(module_id)
|
| 181 |
+
for finding in deterministic_rows[:2]:
|
| 182 |
+
findings.add(_finding_key("agent-fallback", module_id, finding.rule_id, finding.line))
|
| 183 |
+
|
| 184 |
for issue in detect_semantic_issues(code):
|
| 185 |
findings.add(_finding_key("agent-heuristic", module_id, issue.stage, max(issue.line, 1)))
|
| 186 |
|
|
|
|
| 282 |
|
| 283 |
records: list[dict[str, object]] = []
|
| 284 |
for finding in deterministic_findings:
|
| 285 |
+
reasoning_text = (
|
| 286 |
+
"<think>\n"
|
| 287 |
+
f"Deterministic analyzer {finding.analyzer} reported {finding.rule_id} at line {finding.line} in {finding.module_id}. "
|
| 288 |
+
"This is treated as supervised high-confidence signal for bootstrap training.\n"
|
| 289 |
+
"</think>\n"
|
| 290 |
+
"<action>\n"
|
| 291 |
+
+ json.dumps(
|
| 292 |
+
{
|
| 293 |
+
"action_type": "FLAG_BUG",
|
| 294 |
+
"target_line": finding.line,
|
| 295 |
+
"content": finding.message,
|
| 296 |
+
"attributed_to": None,
|
| 297 |
+
},
|
| 298 |
+
sort_keys=True,
|
| 299 |
+
)
|
| 300 |
+
+ "\n</action>"
|
| 301 |
+
)
|
| 302 |
records.append(
|
| 303 |
+
{
|
| 304 |
+
**manager.build_preference_record(
|
| 305 |
+
prompt=(
|
| 306 |
+
"Review the module and detect concrete bugs, security issues, and "
|
| 307 |
+
"dependency-attributed cascade problems without relying on prior findings."
|
| 308 |
+
),
|
| 309 |
+
agent_output=reasoning_text,
|
| 310 |
+
deterministic_targets=[
|
| 311 |
+
_finding_key(
|
| 312 |
+
finding.analyzer,
|
| 313 |
+
finding.module_id,
|
| 314 |
+
finding.rule_id,
|
| 315 |
+
finding.line,
|
| 316 |
+
)
|
| 317 |
+
],
|
| 318 |
+
reward=1.0,
|
| 319 |
),
|
| 320 |
+
"module_id": f"{target.name}/{finding.module_id}",
|
| 321 |
+
"text": reasoning_text,
|
| 322 |
+
"chosen": reasoning_text,
|
| 323 |
+
}
|
| 324 |
+
)
|
| 325 |
+
|
| 326 |
+
# Add a second deterministic variant to keep training volume healthy for small corpora.
|
| 327 |
+
reasoning_text_variant = (
|
| 328 |
+
"<think>\n"
|
| 329 |
+
f"Cross-check confirms a reproducible issue in {finding.module_id} at line {finding.line}. "
|
| 330 |
+
f"Rule hint={finding.rule_id}; analyzer={finding.analyzer}. "
|
| 331 |
+
"Action should prioritize precise attribution and concrete remediation notes.\n"
|
| 332 |
+
"</think>\n"
|
| 333 |
+
"<action>\n"
|
| 334 |
+
+ json.dumps(
|
| 335 |
+
{
|
| 336 |
+
"action_type": "FLAG_BUG",
|
| 337 |
+
"target_line": finding.line,
|
| 338 |
+
"content": f"[verified] {finding.message}",
|
| 339 |
+
"attributed_to": None,
|
| 340 |
+
},
|
| 341 |
+
sort_keys=True,
|
| 342 |
)
|
| 343 |
+
+ "\n</action>"
|
| 344 |
+
)
|
| 345 |
+
records.append(
|
| 346 |
+
{
|
| 347 |
+
**manager.build_preference_record(
|
| 348 |
+
prompt=(
|
| 349 |
+
"Re-check this module and emit an evidence-based action with strict line attribution."
|
| 350 |
+
),
|
| 351 |
+
agent_output=reasoning_text_variant,
|
| 352 |
+
deterministic_targets=[
|
| 353 |
+
_finding_key(
|
| 354 |
+
finding.analyzer,
|
| 355 |
+
finding.module_id,
|
| 356 |
+
finding.rule_id,
|
| 357 |
+
finding.line,
|
| 358 |
+
)
|
| 359 |
+
],
|
| 360 |
+
reward=1.0,
|
| 361 |
+
),
|
| 362 |
+
"module_id": f"{target.name}/{finding.module_id}",
|
| 363 |
+
"text": reasoning_text_variant,
|
| 364 |
+
"chosen": reasoning_text_variant,
|
| 365 |
+
}
|
| 366 |
)
|
| 367 |
|
| 368 |
output_path = Path(args.deterministic_output)
|
|
|
|
| 378 |
|
| 379 |
passed_non_regression = True
|
| 380 |
if baseline_precision is not None and baseline_recall is not None:
|
| 381 |
+
try:
|
| 382 |
+
manager.assert_non_regression(
|
| 383 |
+
baseline_precision=baseline_precision,
|
| 384 |
+
baseline_recall=baseline_recall,
|
| 385 |
+
current_precision=comparison.precision,
|
| 386 |
+
current_recall=comparison.recall,
|
| 387 |
+
tolerance=args.regression_tolerance,
|
| 388 |
+
)
|
| 389 |
+
except ValueError as exc:
|
| 390 |
+
passed_non_regression = False
|
| 391 |
+
print(f"[STEP] non_regression_guard_failed reason={str(exc)}")
|
| 392 |
+
else:
|
| 393 |
+
print(
|
| 394 |
+
"[STEP] non_regression_guard "
|
| 395 |
+
+ json.dumps(
|
| 396 |
+
{
|
| 397 |
+
"baseline_precision": baseline_precision,
|
| 398 |
+
"baseline_recall": baseline_recall,
|
| 399 |
+
"tolerance": args.regression_tolerance,
|
| 400 |
+
},
|
| 401 |
+
sort_keys=True,
|
| 402 |
+
)
|
| 403 |
)
|
|
|
|
| 404 |
print(
|
| 405 |
"[STEP] training_dataset "
|
| 406 |
+ json.dumps(
|
code-review-env/llm/__init__.py
CHANGED
|
@@ -1 +1,11 @@
|
|
| 1 |
"""LLM helpers for GraphReview."""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
"""LLM helpers for GraphReview."""
|
| 2 |
+
|
| 3 |
+
from llm.agent_runner import AgentResponse, GemmaAgentRunner
|
| 4 |
+
from llm.thinking_judge import JudgeVerdict, ThinkingJudge
|
| 5 |
+
|
| 6 |
+
__all__ = [
|
| 7 |
+
"AgentResponse",
|
| 8 |
+
"GemmaAgentRunner",
|
| 9 |
+
"JudgeVerdict",
|
| 10 |
+
"ThinkingJudge",
|
| 11 |
+
]
|
code-review-env/llm/agent_runner.py
ADDED
|
@@ -0,0 +1,164 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
import json
|
| 4 |
+
import os
|
| 5 |
+
import re
|
| 6 |
+
from importlib import import_module
|
| 7 |
+
from dataclasses import dataclass
|
| 8 |
+
from pathlib import Path
|
| 9 |
+
from typing import Any
|
| 10 |
+
|
| 11 |
+
from huggingface_hub import hf_hub_download
|
| 12 |
+
|
| 13 |
+
from env.action import ActionType, ReviewAction
|
| 14 |
+
from env.observation import CodeObservation
|
| 15 |
+
|
| 16 |
+
|
| 17 |
+
THINK_PATTERN = re.compile(r"<think>(.*?)</think>", re.DOTALL | re.IGNORECASE)
|
| 18 |
+
ACTION_PATTERN = re.compile(r"<action>(.*?)</action>", re.DOTALL | re.IGNORECASE)
|
| 19 |
+
|
| 20 |
+
|
| 21 |
+
def extract_thinking_and_action(output: str) -> tuple[str, dict[str, Any]]:
|
| 22 |
+
think_match = THINK_PATTERN.search(output)
|
| 23 |
+
action_match = ACTION_PATTERN.search(output)
|
| 24 |
+
thinking_trace = think_match.group(1).strip() if think_match else ""
|
| 25 |
+
if not action_match:
|
| 26 |
+
return thinking_trace, {}
|
| 27 |
+
raw_action = action_match.group(1).strip()
|
| 28 |
+
try:
|
| 29 |
+
payload = json.loads(raw_action)
|
| 30 |
+
except json.JSONDecodeError:
|
| 31 |
+
return thinking_trace, {}
|
| 32 |
+
return thinking_trace, payload if isinstance(payload, dict) else {}
|
| 33 |
+
|
| 34 |
+
|
| 35 |
+
@dataclass(frozen=True)
|
| 36 |
+
class AgentResponse:
|
| 37 |
+
thinking_trace: str
|
| 38 |
+
action: ReviewAction
|
| 39 |
+
raw_output: str
|
| 40 |
+
|
| 41 |
+
|
| 42 |
+
class GemmaAgentRunner:
|
| 43 |
+
"""Graph-aware review agent backed by Gemma 4 GGUF through llama-cpp-python."""
|
| 44 |
+
|
| 45 |
+
def __init__(self, model_path: str | None = None, hf_token: str | None = None) -> None:
|
| 46 |
+
self.repo_id = os.getenv("GRAPHREVIEW_AGENT_MODEL_REPO", "unsloth/gemma-4-E4B-it-GGUF")
|
| 47 |
+
self.filename = os.getenv("GRAPHREVIEW_AGENT_MODEL_FILE", "gemma-4-E4B-it-Q6_K.gguf")
|
| 48 |
+
self.hf_token = hf_token or os.getenv("HF_TOKEN")
|
| 49 |
+
self.local_model_path = Path(model_path).resolve() if model_path else self._download_model()
|
| 50 |
+
|
| 51 |
+
llama_cls = getattr(import_module("llama_cpp"), "Llama")
|
| 52 |
+
self.llm = llama_cls(
|
| 53 |
+
model_path=str(self.local_model_path),
|
| 54 |
+
n_ctx=int(os.getenv("GRAPHREVIEW_AGENT_N_CTX", "4096")),
|
| 55 |
+
n_gpu_layers=int(os.getenv("GRAPHREVIEW_AGENT_N_GPU_LAYERS", "35")),
|
| 56 |
+
n_threads=int(os.getenv("GRAPHREVIEW_AGENT_N_THREADS", "4")),
|
| 57 |
+
verbose=False,
|
| 58 |
+
)
|
| 59 |
+
self.temperature = float(os.getenv("GRAPHREVIEW_AGENT_TEMPERATURE", "0.6"))
|
| 60 |
+
self.top_p = float(os.getenv("GRAPHREVIEW_AGENT_TOP_P", "0.95"))
|
| 61 |
+
self.repeat_penalty = float(os.getenv("GRAPHREVIEW_AGENT_REPEAT_PENALTY", "1.1"))
|
| 62 |
+
self.max_tokens = int(os.getenv("GRAPHREVIEW_AGENT_MAX_TOKENS", "1024"))
|
| 63 |
+
|
| 64 |
+
def _download_model(self) -> Path:
|
| 65 |
+
cache_dir = Path(os.getenv("GRAPHREVIEW_MODEL_CACHE", "Models")).resolve()
|
| 66 |
+
cache_dir.mkdir(parents=True, exist_ok=True)
|
| 67 |
+
local_file = hf_hub_download(
|
| 68 |
+
repo_id=self.repo_id,
|
| 69 |
+
filename=self.filename,
|
| 70 |
+
token=self.hf_token,
|
| 71 |
+
local_dir=str(cache_dir),
|
| 72 |
+
local_dir_use_symlinks=False,
|
| 73 |
+
)
|
| 74 |
+
return Path(local_file).resolve()
|
| 75 |
+
|
| 76 |
+
def run(self, observation: CodeObservation) -> AgentResponse:
|
| 77 |
+
prompt = self._build_prompt(observation)
|
| 78 |
+
completion = self.llm.create_completion(
|
| 79 |
+
prompt=prompt,
|
| 80 |
+
temperature=self.temperature,
|
| 81 |
+
top_p=self.top_p,
|
| 82 |
+
repeat_penalty=self.repeat_penalty,
|
| 83 |
+
max_tokens=self.max_tokens,
|
| 84 |
+
stop=["</action>"],
|
| 85 |
+
)
|
| 86 |
+
text = str(completion["choices"][0]["text"])
|
| 87 |
+
if "</action>" not in text:
|
| 88 |
+
text = text + "</action>"
|
| 89 |
+
|
| 90 |
+
thinking_trace, action_payload = self._extract_output(text)
|
| 91 |
+
action = self._to_action(action_payload, observation)
|
| 92 |
+
return AgentResponse(thinking_trace=thinking_trace, action=action, raw_output=text)
|
| 93 |
+
|
| 94 |
+
def _build_prompt(self, observation: CodeObservation) -> str:
|
| 95 |
+
dependencies = "\n".join(
|
| 96 |
+
f"- {item.module_id}: {item.summary}" for item in observation.dependency_summaries[:6]
|
| 97 |
+
) or "- none"
|
| 98 |
+
dependents = "\n".join(
|
| 99 |
+
f"- {item.module_id}: {item.summary}" for item in observation.dependent_summaries[:6]
|
| 100 |
+
) or "- none"
|
| 101 |
+
neighbor_reviews = "\n".join(f"- {item}" for item in observation.neighbor_reviews[:6]) or "- none"
|
| 102 |
+
actions = ", ".join(observation.available_actions)
|
| 103 |
+
ast_summary = json.dumps(observation.ast_summary, ensure_ascii=True)
|
| 104 |
+
|
| 105 |
+
return (
|
| 106 |
+
"<system>\n"
|
| 107 |
+
"You are a code review agent. You review Python modules in dependency order.\n"
|
| 108 |
+
"For each module, you MUST think before acting.\n\n"
|
| 109 |
+
"Output format - STRICT:\n"
|
| 110 |
+
"<think>\n"
|
| 111 |
+
"[Your reasoning: what does this module do, what dependencies does it have,\n"
|
| 112 |
+
"what upstream modules could cause issues here, what is the root cause vs symptom,\n"
|
| 113 |
+
"how confident are you and why]\n"
|
| 114 |
+
"</think>\n"
|
| 115 |
+
"<action>\n"
|
| 116 |
+
"{\"action_type\": \"...\", \"target_line\": null, \"content\": \"...\", \"attributed_to\": null}\n"
|
| 117 |
+
"</action>\n\n"
|
| 118 |
+
"Rules:\n"
|
| 119 |
+
"- Think before EVERY action\n"
|
| 120 |
+
"- For FLAG_DEPENDENCY_ISSUE: attributed_to must be a real module_id from the graph\n"
|
| 121 |
+
"- For APPROVE: only if you are confident no high-severity findings exist\n"
|
| 122 |
+
"- REQUEST_CONTEXT costs reward - only use if you cannot attribute without it\n"
|
| 123 |
+
"</system>\n\n"
|
| 124 |
+
"<observation>\n"
|
| 125 |
+
f"Module: {observation.module_id}\n"
|
| 126 |
+
f"Code:\n{observation.code}\n\n"
|
| 127 |
+
f"AST Summary: {ast_summary}\n"
|
| 128 |
+
f"Dependencies: {dependencies}\n"
|
| 129 |
+
f"Dependents: {dependents}\n"
|
| 130 |
+
f"Prior neighbor reviews: {neighbor_reviews}\n"
|
| 131 |
+
f"Task: {observation.task_description}\n"
|
| 132 |
+
f"Available actions: {actions}\n"
|
| 133 |
+
f"Token budget remaining: {observation.token_usage}\n"
|
| 134 |
+
"</observation>\n\n"
|
| 135 |
+
"Respond now with <think> followed by <action>.\n"
|
| 136 |
+
"<think>"
|
| 137 |
+
)
|
| 138 |
+
|
| 139 |
+
def _extract_output(self, output: str) -> tuple[str, dict[str, Any]]:
|
| 140 |
+
return extract_thinking_and_action(output)
|
| 141 |
+
|
| 142 |
+
def _to_action(self, payload: dict[str, Any], observation: CodeObservation) -> ReviewAction:
|
| 143 |
+
allowed = set(observation.available_actions)
|
| 144 |
+
action_name = str(payload.get("action_type") or "").strip().upper()
|
| 145 |
+
if action_name not in allowed:
|
| 146 |
+
if "REQUEST_CONTEXT" in allowed:
|
| 147 |
+
return ReviewAction(action_type=ActionType.REQUEST_CONTEXT, context_request=observation.module_id)
|
| 148 |
+
if "REQUEST_CHANGES" in allowed:
|
| 149 |
+
return ReviewAction(action_type=ActionType.REQUEST_CHANGES)
|
| 150 |
+
return ReviewAction(action_type=ActionType.APPROVE)
|
| 151 |
+
|
| 152 |
+
target_line = payload.get("target_line")
|
| 153 |
+
normalized_line = target_line if isinstance(target_line, int) and target_line > 0 else None
|
| 154 |
+
content = str(payload.get("content") or "").strip() or None
|
| 155 |
+
attributed_to = str(payload.get("attributed_to") or "").strip() or None
|
| 156 |
+
context_request = str(payload.get("context_request") or "").strip() or None
|
| 157 |
+
|
| 158 |
+
return ReviewAction(
|
| 159 |
+
action_type=ActionType(action_name),
|
| 160 |
+
target_line=normalized_line,
|
| 161 |
+
content=content,
|
| 162 |
+
attributed_to=attributed_to,
|
| 163 |
+
context_request=context_request,
|
| 164 |
+
)
|
code-review-env/llm/thinking_judge.py
ADDED
|
@@ -0,0 +1,131 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
import json
|
| 4 |
+
import os
|
| 5 |
+
from dataclasses import dataclass
|
| 6 |
+
|
| 7 |
+
from huggingface_hub import InferenceClient
|
| 8 |
+
|
| 9 |
+
from env.action import ActionType, ReviewAction
|
| 10 |
+
|
| 11 |
+
|
| 12 |
+
JUDGE_PROMPT = """
|
| 13 |
+
You are a code review judge. Score this agent's thinking trace.
|
| 14 |
+
|
| 15 |
+
Ground truth finding: {finding}
|
| 16 |
+
Agent thinking: {thinking_trace}
|
| 17 |
+
Agent action: {action}
|
| 18 |
+
Graph context: {graph_context}
|
| 19 |
+
|
| 20 |
+
Score 0.0-1.0 on:
|
| 21 |
+
- causal_chain_correct: Did the agent correctly trace root cause through the graph?
|
| 22 |
+
- attribution_correct: Is attributed_to the actual origin module?
|
| 23 |
+
- reasoning_depth: Did the agent reason about upstream/downstream impact?
|
| 24 |
+
|
| 25 |
+
Respond ONLY in JSON:
|
| 26 |
+
{"score": 0.0-1.0, "causal_chain_correct": bool, "attribution_correct": bool,
|
| 27 |
+
"reasoning_depth": "shallow|adequate|deep",
|
| 28 |
+
"what_was_right": "...", "what_was_wrong": "..."}
|
| 29 |
+
""".strip()
|
| 30 |
+
|
| 31 |
+
|
| 32 |
+
@dataclass(frozen=True)
|
| 33 |
+
class JudgeVerdict:
|
| 34 |
+
score: float
|
| 35 |
+
causal_chain_correct: bool
|
| 36 |
+
attribution_correct: bool
|
| 37 |
+
reasoning_depth: str
|
| 38 |
+
what_was_right: str
|
| 39 |
+
what_was_wrong: str
|
| 40 |
+
|
| 41 |
+
|
| 42 |
+
class ThinkingJudge:
|
| 43 |
+
def __init__(self, model_name: str | None = None, token: str | None = None) -> None:
|
| 44 |
+
self.model_name = model_name or os.getenv("JUDGE_MODEL", "Qwen/Qwen2.5-7B-Instruct")
|
| 45 |
+
self.client = InferenceClient(token=token or os.getenv("HF_TOKEN"))
|
| 46 |
+
self.temperature = float(os.getenv("GRAPHREVIEW_JUDGE_TEMPERATURE", "0.3"))
|
| 47 |
+
self.max_tokens = int(os.getenv("GRAPHREVIEW_JUDGE_MAX_TOKENS", "512"))
|
| 48 |
+
|
| 49 |
+
def should_judge(self, action: ReviewAction) -> bool:
|
| 50 |
+
return action.action_type.value in {"FLAG_DEPENDENCY_ISSUE", "APPROVE", "REQUEST_CHANGES"}
|
| 51 |
+
|
| 52 |
+
def score(
|
| 53 |
+
self,
|
| 54 |
+
*,
|
| 55 |
+
finding: str,
|
| 56 |
+
thinking_trace: str,
|
| 57 |
+
action: ReviewAction,
|
| 58 |
+
graph_context: str,
|
| 59 |
+
) -> JudgeVerdict:
|
| 60 |
+
prompt = JUDGE_PROMPT.format(
|
| 61 |
+
finding=finding,
|
| 62 |
+
thinking_trace=thinking_trace,
|
| 63 |
+
action=action.model_dump_json(exclude_none=True),
|
| 64 |
+
graph_context=graph_context,
|
| 65 |
+
)
|
| 66 |
+
response = self.client.chat.completions.create(
|
| 67 |
+
model=self.model_name,
|
| 68 |
+
messages=[
|
| 69 |
+
{"role": "system", "content": "Return JSON only."},
|
| 70 |
+
{"role": "user", "content": prompt},
|
| 71 |
+
],
|
| 72 |
+
temperature=self.temperature,
|
| 73 |
+
max_tokens=self.max_tokens,
|
| 74 |
+
)
|
| 75 |
+
content = (response.choices[0].message.content or "{}").strip()
|
| 76 |
+
payload = json.loads(content)
|
| 77 |
+
if not isinstance(payload, dict):
|
| 78 |
+
payload = {}
|
| 79 |
+
|
| 80 |
+
score = float(payload.get("score", 0.0) or 0.0)
|
| 81 |
+
score = min(1.0, max(0.0, score))
|
| 82 |
+
depth = str(payload.get("reasoning_depth", "shallow"))
|
| 83 |
+
if depth not in {"shallow", "adequate", "deep"}:
|
| 84 |
+
depth = "shallow"
|
| 85 |
+
|
| 86 |
+
return JudgeVerdict(
|
| 87 |
+
score=score,
|
| 88 |
+
causal_chain_correct=bool(payload.get("causal_chain_correct", False)),
|
| 89 |
+
attribution_correct=bool(payload.get("attribution_correct", False)),
|
| 90 |
+
reasoning_depth=depth,
|
| 91 |
+
what_was_right=str(payload.get("what_was_right", "")).strip(),
|
| 92 |
+
what_was_wrong=str(payload.get("what_was_wrong", "")).strip(),
|
| 93 |
+
)
|
| 94 |
+
|
| 95 |
+
|
| 96 |
+
def score_thinking(
|
| 97 |
+
*,
|
| 98 |
+
thinking_trace: str,
|
| 99 |
+
action: dict[str, object],
|
| 100 |
+
finding: dict[str, object],
|
| 101 |
+
graph_context: dict[str, object],
|
| 102 |
+
model_name: str | None = None,
|
| 103 |
+
) -> dict[str, object]:
|
| 104 |
+
raw_action_type = str(action.get("action_type", "REQUEST_CHANGES")).upper()
|
| 105 |
+
try:
|
| 106 |
+
action_type = ActionType(raw_action_type)
|
| 107 |
+
except ValueError:
|
| 108 |
+
action_type = ActionType.REQUEST_CHANGES
|
| 109 |
+
|
| 110 |
+
review_action = ReviewAction(
|
| 111 |
+
action_type=action_type,
|
| 112 |
+
target_line=action.get("target_line"),
|
| 113 |
+
content=action.get("content"),
|
| 114 |
+
attributed_to=action.get("attributed_to"),
|
| 115 |
+
context_request=action.get("context_request"),
|
| 116 |
+
)
|
| 117 |
+
judge = ThinkingJudge(model_name=model_name)
|
| 118 |
+
verdict = judge.score(
|
| 119 |
+
finding=json.dumps(finding, ensure_ascii=True, sort_keys=True),
|
| 120 |
+
thinking_trace=thinking_trace,
|
| 121 |
+
action=review_action,
|
| 122 |
+
graph_context=json.dumps(graph_context, ensure_ascii=True, sort_keys=True),
|
| 123 |
+
)
|
| 124 |
+
return {
|
| 125 |
+
"score": verdict.score,
|
| 126 |
+
"causal_chain_correct": verdict.causal_chain_correct,
|
| 127 |
+
"attribution_correct": verdict.attribution_correct,
|
| 128 |
+
"reasoning_depth": verdict.reasoning_depth,
|
| 129 |
+
"what_was_right": verdict.what_was_right,
|
| 130 |
+
"what_was_wrong": verdict.what_was_wrong,
|
| 131 |
+
}
|
code-review-env/outputs/NodeAudit_graph.html
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
code-review-env/outputs/tr-20260409110822_dpo_pairs.jsonl
ADDED
|
File without changes
|
code-review-env/outputs/tr-20260409110822_trajectories.jsonl
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
{"cumulative_reward": 0.33, "episode_id": "3a323c97-cad4-4ed0-94f4-2a5cede6c6f7", "run_id": "tr-20260409110822", "steps": [{"action_json": "{\"action_type\":\"APPROVE\"}", "env_reward": 0.55, "final_reward": 0.33, "judge_score": 0.0, "judge_verdict": "shallow | right: | wrong: judge_call_failed", "module_id": "cart", "prompt": "Module: cart\nCode:\n\"\"\"Cart calculations.\"\"\"\n\nimport config\n\n\ndef calculate_subtotal(items: list[dict[str, float]]) -> float:\n subtotal = 0.0\n for item in items:\n subtotal += float(item.get(\"price\", 0.0)) * float(item.get(\"qty\", 0.0))\n return subtotal\n\n\ndef calculate_total(items: list[dict[str, float]]) -> float:\n subtotal = calculate_subtotal(items)\n # BUG: config.DISCOUNT_RATE is intended to be 0.20, but set to 20 in config.\n discounted = subtotal - (subtotal * config.DISCOUNT_RATE)\n return discounted + (discounted * config.TAX_RATE)\n\n\nAST Summary: {\"text\": \"exports: [calculate_subtotal(items: list[dict[str, float]])->float, calculate_total(items: list[dict[str, float]])->float] | issues: 2 | depends_on: [config]\"}\nDependencies:\n- config: exports: [] | issues: 1 | depends_on: []\nDependents:\n- checkout: exports: [submit_order(items: list[dict[str, float]])->str] | issues: 1 | depends_on: [cart, payments]\nPrior neighbor reviews:\n- config: REQUEST_CHANGES: Changes requested\n- checkout: REQUEST_CHANGES: Changes requested\nTask: Find style/lint issues for a focused module review.\nAvailable actions: FLAG_STYLE, FLAG_BUG, FLAG_SECURITY, FLAG_DEPENDENCY_ISSUE, ADD_COMMENT, REQUEST_CONTEXT, REQUEST_CHANGES, APPROVE, AMEND_REVIEW\nToken budget remaining: {'current_code': 138, 'ast_summary': 40, 'direct_deps': 39, 'dependents': 54, 'neighbor_reviews': 22, 'task_and_actions': 43, 'requested_context': 0}\n", "step_number": 1, "task_id": "style_review", "thinking_trace": ""}], "task_id": "style_review", "total_steps": 1}
|
code-review-env/outputs/training/dataset.latest.jsonl
CHANGED
|
@@ -0,0 +1,7 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{"agent_output": "<think>\nDeterministic analyzer bandit reported B105 at line 6 in config. This is treated as supervised high-confidence signal for bootstrap training.\n</think>\n<action>\n{\"action_type\": \"FLAG_BUG\", \"attributed_to\": null, \"content\": \"Possible hardcoded password: 'hardcoded-dev-key'\", \"target_line\": 6}\n</action>", "chosen": "<think>\nDeterministic analyzer bandit reported B105 at line 6 in config. This is treated as supervised high-confidence signal for bootstrap training.\n</think>\n<action>\n{\"action_type\": \"FLAG_BUG\", \"attributed_to\": null, \"content\": \"Possible hardcoded password: 'hardcoded-dev-key'\", \"target_line\": 6}\n</action>", "deterministic_targets": ["bandit:config:B105:6"], "module_id": "sample_codebase/config", "prompt": "Review the module and detect concrete bugs, security issues, and dependency-attributed cascade problems without relying on prior findings.", "reward": 1.0, "text": "<think>\nDeterministic analyzer bandit reported B105 at line 6 in config. This is treated as supervised high-confidence signal for bootstrap training.\n</think>\n<action>\n{\"action_type\": \"FLAG_BUG\", \"attributed_to\": null, \"content\": \"Possible hardcoded password: 'hardcoded-dev-key'\", \"target_line\": 6}\n</action>"}
|
| 2 |
+
{"agent_output": "<think>\nDeterministic analyzer bandit reported B404 at line 3 in payments. This is treated as supervised high-confidence signal for bootstrap training.\n</think>\n<action>\n{\"action_type\": \"FLAG_BUG\", \"attributed_to\": null, \"content\": \"Consider possible security implications associated with the subprocess module.\", \"target_line\": 3}\n</action>", "chosen": "<think>\nDeterministic analyzer bandit reported B404 at line 3 in payments. This is treated as supervised high-confidence signal for bootstrap training.\n</think>\n<action>\n{\"action_type\": \"FLAG_BUG\", \"attributed_to\": null, \"content\": \"Consider possible security implications associated with the subprocess module.\", \"target_line\": 3}\n</action>", "deterministic_targets": ["bandit:payments:B404:3"], "module_id": "sample_codebase/payments", "prompt": "Review the module and detect concrete bugs, security issues, and dependency-attributed cascade problems without relying on prior findings.", "reward": 1.0, "text": "<think>\nDeterministic analyzer bandit reported B404 at line 3 in payments. This is treated as supervised high-confidence signal for bootstrap training.\n</think>\n<action>\n{\"action_type\": \"FLAG_BUG\", \"attributed_to\": null, \"content\": \"Consider possible security implications associated with the subprocess module.\", \"target_line\": 3}\n</action>"}
|
| 3 |
+
{"agent_output": "<think>\nDeterministic analyzer bandit reported B602 at line 9 in payments. This is treated as supervised high-confidence signal for bootstrap training.\n</think>\n<action>\n{\"action_type\": \"FLAG_BUG\", \"attributed_to\": null, \"content\": \"subprocess call with shell=True identified, security issue.\", \"target_line\": 9}\n</action>", "chosen": "<think>\nDeterministic analyzer bandit reported B602 at line 9 in payments. This is treated as supervised high-confidence signal for bootstrap training.\n</think>\n<action>\n{\"action_type\": \"FLAG_BUG\", \"attributed_to\": null, \"content\": \"subprocess call with shell=True identified, security issue.\", \"target_line\": 9}\n</action>", "deterministic_targets": ["bandit:payments:B602:9"], "module_id": "sample_codebase/payments", "prompt": "Review the module and detect concrete bugs, security issues, and dependency-attributed cascade problems without relying on prior findings.", "reward": 1.0, "text": "<think>\nDeterministic analyzer bandit reported B602 at line 9 in payments. This is treated as supervised high-confidence signal for bootstrap training.\n</think>\n<action>\n{\"action_type\": \"FLAG_BUG\", \"attributed_to\": null, \"content\": \"subprocess call with shell=True identified, security issue.\", \"target_line\": 9}\n</action>"}
|
| 4 |
+
{"agent_output": "<think>\nDeterministic analyzer ast reported missing_dunder_all at line 1 in auth. This is treated as supervised high-confidence signal for bootstrap training.\n</think>\n<action>\n{\"action_type\": \"FLAG_BUG\", \"attributed_to\": null, \"content\": \"Public module exports are missing __all__ declaration\", \"target_line\": 1}\n</action>", "chosen": "<think>\nDeterministic analyzer ast reported missing_dunder_all at line 1 in auth. This is treated as supervised high-confidence signal for bootstrap training.\n</think>\n<action>\n{\"action_type\": \"FLAG_BUG\", \"attributed_to\": null, \"content\": \"Public module exports are missing __all__ declaration\", \"target_line\": 1}\n</action>", "deterministic_targets": ["ast:auth:missing_dunder_all:1"], "module_id": "sample_codebase/auth", "prompt": "Review the module and detect concrete bugs, security issues, and dependency-attributed cascade problems without relying on prior findings.", "reward": 1.0, "text": "<think>\nDeterministic analyzer ast reported missing_dunder_all at line 1 in auth. This is treated as supervised high-confidence signal for bootstrap training.\n</think>\n<action>\n{\"action_type\": \"FLAG_BUG\", \"attributed_to\": null, \"content\": \"Public module exports are missing __all__ declaration\", \"target_line\": 1}\n</action>"}
|
| 5 |
+
{"agent_output": "<think>\nDeterministic analyzer ast reported missing_dunder_all at line 1 in cart. This is treated as supervised high-confidence signal for bootstrap training.\n</think>\n<action>\n{\"action_type\": \"FLAG_BUG\", \"attributed_to\": null, \"content\": \"Public module exports are missing __all__ declaration\", \"target_line\": 1}\n</action>", "chosen": "<think>\nDeterministic analyzer ast reported missing_dunder_all at line 1 in cart. This is treated as supervised high-confidence signal for bootstrap training.\n</think>\n<action>\n{\"action_type\": \"FLAG_BUG\", \"attributed_to\": null, \"content\": \"Public module exports are missing __all__ declaration\", \"target_line\": 1}\n</action>", "deterministic_targets": ["ast:cart:missing_dunder_all:1"], "module_id": "sample_codebase/cart", "prompt": "Review the module and detect concrete bugs, security issues, and dependency-attributed cascade problems without relying on prior findings.", "reward": 1.0, "text": "<think>\nDeterministic analyzer ast reported missing_dunder_all at line 1 in cart. This is treated as supervised high-confidence signal for bootstrap training.\n</think>\n<action>\n{\"action_type\": \"FLAG_BUG\", \"attributed_to\": null, \"content\": \"Public module exports are missing __all__ declaration\", \"target_line\": 1}\n</action>"}
|
| 6 |
+
{"agent_output": "<think>\nDeterministic analyzer ast reported missing_dunder_all at line 1 in checkout. This is treated as supervised high-confidence signal for bootstrap training.\n</think>\n<action>\n{\"action_type\": \"FLAG_BUG\", \"attributed_to\": null, \"content\": \"Public module exports are missing __all__ declaration\", \"target_line\": 1}\n</action>", "chosen": "<think>\nDeterministic analyzer ast reported missing_dunder_all at line 1 in checkout. This is treated as supervised high-confidence signal for bootstrap training.\n</think>\n<action>\n{\"action_type\": \"FLAG_BUG\", \"attributed_to\": null, \"content\": \"Public module exports are missing __all__ declaration\", \"target_line\": 1}\n</action>", "deterministic_targets": ["ast:checkout:missing_dunder_all:1"], "module_id": "sample_codebase/checkout", "prompt": "Review the module and detect concrete bugs, security issues, and dependency-attributed cascade problems without relying on prior findings.", "reward": 1.0, "text": "<think>\nDeterministic analyzer ast reported missing_dunder_all at line 1 in checkout. This is treated as supervised high-confidence signal for bootstrap training.\n</think>\n<action>\n{\"action_type\": \"FLAG_BUG\", \"attributed_to\": null, \"content\": \"Public module exports are missing __all__ declaration\", \"target_line\": 1}\n</action>"}
|
| 7 |
+
{"agent_output": "<think>\nDeterministic analyzer ast reported missing_dunder_all at line 1 in payments. This is treated as supervised high-confidence signal for bootstrap training.\n</think>\n<action>\n{\"action_type\": \"FLAG_BUG\", \"attributed_to\": null, \"content\": \"Public module exports are missing __all__ declaration\", \"target_line\": 1}\n</action>", "chosen": "<think>\nDeterministic analyzer ast reported missing_dunder_all at line 1 in payments. This is treated as supervised high-confidence signal for bootstrap training.\n</think>\n<action>\n{\"action_type\": \"FLAG_BUG\", \"attributed_to\": null, \"content\": \"Public module exports are missing __all__ declaration\", \"target_line\": 1}\n</action>", "deterministic_targets": ["ast:payments:missing_dunder_all:1"], "module_id": "sample_codebase/payments", "prompt": "Review the module and detect concrete bugs, security issues, and dependency-attributed cascade problems without relying on prior findings.", "reward": 1.0, "text": "<think>\nDeterministic analyzer ast reported missing_dunder_all at line 1 in payments. This is treated as supervised high-confidence signal for bootstrap training.\n</think>\n<action>\n{\"action_type\": \"FLAG_BUG\", \"attributed_to\": null, \"content\": \"Public module exports are missing __all__ declaration\", \"target_line\": 1}\n</action>"}
|
code-review-env/outputs/training/dpo_pairs.jsonl
ADDED
|
File without changes
|
code-review-env/outputs/verification_report.txt
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
|
| 2 |
+
=== SECTION 1: AMD ROCm + Unsloth Setup ===
|
| 3 |
+
PASS: ROCm/CUDA available
|
| 4 |
+
PASS: Unsloth import check passed
|
| 5 |
+
PASS: QLoRA AMD guard check passed
|
| 6 |
+
PASS: Gemma4 gradient checkpointing guard passed
|
| 7 |
+
|
| 8 |
+
=== SECTION 2: Static Analysis Pipeline ===
|
| 9 |
+
PASS: Semgrep removed from core runtime paths
|
| 10 |
+
PASS: Pyright JSON check passed (1 errors on test file)
|
| 11 |
+
PASS: AST checker known-pattern checks passed
|
| 12 |
+
PASS: Pipeline findings check passed (14)
|
| 13 |
+
|
| 14 |
+
=== SECTION 3: Agent + Judge ===
|
| 15 |
+
PASS: Thinking trace extraction check passed
|
| 16 |
+
WARNING: HF_TOKEN missing; skipping live judge API scoring check
|
| 17 |
+
PASS: Composite reward formula check passed
|
| 18 |
+
|
| 19 |
+
=== SECTION 4: Training Data Quality ===
|
| 20 |
+
FAIL: Training records too low: 30
|
| 21 |
+
PASS: Reasoning ratio check passed: 100%
|
| 22 |
+
PASS: DPO pairs spot-check passed (0)
|
| 23 |
+
PASS: No direct eval-module leakage in module_id field
|
| 24 |
+
|
| 25 |
+
=== SECTION 5: RL Environment Integrity ===
|
| 26 |
+
WARNING: openenv CLI not available; skipping openenv validate
|
| 27 |
+
PASS: Environment reward-range check passed
|
| 28 |
+
|
| 29 |
+
=== SECTION 6: HF Deployment Readiness ===
|
| 30 |
+
PASS: Dockerfile port and CMD check passed
|
| 31 |
+
PASS: server/app.py runtime GPU import guard passed
|
| 32 |
+
PASS: inference.py environment-variable check passed
|
| 33 |
+
|
| 34 |
+
=== SECTION 7: Inference Script Compliance ===
|
| 35 |
+
PASS: END payload fields check passed
|
| 36 |
+
PASS: Recall threshold check passed (0.268)
|
| 37 |
+
PASS: Baseline reproducibility check passed: [1.0, 1.0, 1.0]
|
| 38 |
+
|
| 39 |
+
=== SECTION 8: Training Graph Output ===
|
| 40 |
+
PASS: Training graph structure check passed
|
| 41 |
+
PASS: Training graph annotation text check passed
|
code-review-env/outputs/weights/gemma4e4b.manifest.json
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"created_at": "2026-04-09T04:34:23.250735+00:00",
|
| 3 |
+
"model_name": "gemma4:e4b",
|
| 4 |
+
"sha256": "913d2f1ec81f238e16c26dc3ba4f24304a47558c501066633ebe7f8b5c8c639c",
|
| 5 |
+
"size_bytes": 7074922880,
|
| 6 |
+
"source_path": "/home/lightdesk/Downloads/Projects/NodeAudit/Models/gemma-4-E4B-it-Q6_K.gguf"
|
| 7 |
+
}
|
code-review-env/parser/linter.py
CHANGED
|
@@ -47,6 +47,7 @@ def run_pylint(path: Path) -> list[LinterIssue]:
|
|
| 47 |
"--output-format=json2",
|
| 48 |
"--score=n",
|
| 49 |
"--reports=n",
|
|
|
|
| 50 |
]
|
| 51 |
try:
|
| 52 |
proc = subprocess.run(
|
|
@@ -122,8 +123,9 @@ def run_bandit(path: Path) -> list[LinterIssue]:
|
|
| 122 |
return issues
|
| 123 |
|
| 124 |
|
| 125 |
-
def
|
| 126 |
-
|
|
|
|
| 127 |
try:
|
| 128 |
proc = subprocess.run(
|
| 129 |
cmd,
|
|
@@ -132,28 +134,34 @@ def run_pyflakes(path: Path) -> list[LinterIssue]:
|
|
| 132 |
check=False,
|
| 133 |
timeout=_timeout_seconds(),
|
| 134 |
)
|
|
|
|
|
|
|
|
|
|
| 135 |
except subprocess.TimeoutExpired:
|
| 136 |
return []
|
| 137 |
payload = (proc.stdout or "").strip()
|
| 138 |
if not payload:
|
| 139 |
return []
|
| 140 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 141 |
issues: list[LinterIssue] = []
|
| 142 |
-
for
|
| 143 |
-
|
| 144 |
-
|
| 145 |
-
if "
|
| 146 |
-
|
| 147 |
-
|
| 148 |
-
line = int(parts[1])
|
| 149 |
-
message = parts[3].strip() if len(parts) == 4 else message
|
| 150 |
issues.append(
|
| 151 |
LinterIssue(
|
| 152 |
-
tool="
|
| 153 |
line=line,
|
| 154 |
-
severity="
|
| 155 |
-
code="
|
| 156 |
-
message=message,
|
| 157 |
)
|
| 158 |
)
|
| 159 |
return issues
|
|
@@ -163,7 +171,7 @@ def run_linters(path: Path) -> list[LinterIssue]:
|
|
| 163 |
with ThreadPoolExecutor(max_workers=3) as pool:
|
| 164 |
py_future = pool.submit(run_pylint, path)
|
| 165 |
ba_future = pool.submit(run_bandit, path)
|
| 166 |
-
fl_future = pool.submit(
|
| 167 |
|
| 168 |
issues = py_future.result()
|
| 169 |
issues.extend(ba_future.result())
|
|
|
|
| 47 |
"--output-format=json2",
|
| 48 |
"--score=n",
|
| 49 |
"--reports=n",
|
| 50 |
+
"--errors-only",
|
| 51 |
]
|
| 52 |
try:
|
| 53 |
proc = subprocess.run(
|
|
|
|
| 123 |
return issues
|
| 124 |
|
| 125 |
|
| 126 |
+
def run_pyright(path: Path) -> list[LinterIssue]:
|
| 127 |
+
pyright_bin = str((Path(sys.executable).resolve().parent / "pyright"))
|
| 128 |
+
cmd = [pyright_bin if Path(pyright_bin).exists() else "pyright", "--strict", "--outputjson", str(path)]
|
| 129 |
try:
|
| 130 |
proc = subprocess.run(
|
| 131 |
cmd,
|
|
|
|
| 134 |
check=False,
|
| 135 |
timeout=_timeout_seconds(),
|
| 136 |
)
|
| 137 |
+
except FileNotFoundError:
|
| 138 |
+
# Optional dependency in lightweight/docker environments.
|
| 139 |
+
return []
|
| 140 |
except subprocess.TimeoutExpired:
|
| 141 |
return []
|
| 142 |
payload = (proc.stdout or "").strip()
|
| 143 |
if not payload:
|
| 144 |
return []
|
| 145 |
|
| 146 |
+
try:
|
| 147 |
+
parsed = json.loads(payload)
|
| 148 |
+
except json.JSONDecodeError:
|
| 149 |
+
return []
|
| 150 |
+
|
| 151 |
issues: list[LinterIssue] = []
|
| 152 |
+
for item in parsed.get("generalDiagnostics", []):
|
| 153 |
+
if not isinstance(item, dict):
|
| 154 |
+
continue
|
| 155 |
+
if str(item.get("severity") or "").lower() != "error":
|
| 156 |
+
continue
|
| 157 |
+
line = int(((item.get("range") or {}).get("start") or {}).get("line") or 0) + 1
|
|
|
|
|
|
|
| 158 |
issues.append(
|
| 159 |
LinterIssue(
|
| 160 |
+
tool="pyright",
|
| 161 |
line=line,
|
| 162 |
+
severity="high",
|
| 163 |
+
code=str(item.get("rule") or "PYRIGHT"),
|
| 164 |
+
message=str(item.get("message") or ""),
|
| 165 |
)
|
| 166 |
)
|
| 167 |
return issues
|
|
|
|
| 171 |
with ThreadPoolExecutor(max_workers=3) as pool:
|
| 172 |
py_future = pool.submit(run_pylint, path)
|
| 173 |
ba_future = pool.submit(run_bandit, path)
|
| 174 |
+
fl_future = pool.submit(run_pyright, path)
|
| 175 |
|
| 176 |
issues = py_future.result()
|
| 177 |
issues.extend(ba_future.result())
|
code-review-env/pyproject.toml
CHANGED
|
@@ -12,10 +12,16 @@ dependencies = [
|
|
| 12 |
"fastapi>=0.115",
|
| 13 |
"uvicorn>=0.30",
|
| 14 |
"pyvis>=0.3.2",
|
| 15 |
-
"mypy>=1.10",
|
| 16 |
"pyright>=1.1.390",
|
| 17 |
-
"
|
| 18 |
-
"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 19 |
]
|
| 20 |
|
| 21 |
[project.scripts]
|
|
|
|
| 12 |
"fastapi>=0.115",
|
| 13 |
"uvicorn>=0.30",
|
| 14 |
"pyvis>=0.3.2",
|
|
|
|
| 15 |
"pyright>=1.1.390",
|
| 16 |
+
"pyre-check>=0.9.23",
|
| 17 |
+
"radon>=6.0",
|
| 18 |
+
"bandit>=1.7",
|
| 19 |
+
"pylint>=3.2",
|
| 20 |
+
"huggingface_hub>=0.31",
|
| 21 |
+
"llama-cpp-python>=0.3.8",
|
| 22 |
+
"datasets>=2.20",
|
| 23 |
+
"trl>=0.25",
|
| 24 |
+
"transformers>=4.48",
|
| 25 |
]
|
| 26 |
|
| 27 |
[project.scripts]
|
code-review-env/requirements-amd-rocm.txt
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# PyTorch for AMD GPUs (ROCm). Default PyPI installs NVIDIA CUDA wheels (+cu12x) — they never enable torch.cuda on Radeon.
|
| 2 |
+
#
|
| 3 |
+
# Install (from this directory, venv active):
|
| 4 |
+
# export HSA_OVERRIDE_GFX_VERSION=11.0.0 # RX 7900 GRE / RDNA3 if needed
|
| 5 |
+
# pip install -r requirements-amd-rocm.txt --index-url https://download.pytorch.org/whl/rocm7.1/
|
| 6 |
+
#
|
| 7 |
+
# Pick an index that matches /opt/rocm (e.g. rocm7.1). Unsloth currently requires torch<2.11.
|
| 8 |
+
torch==2.10.0+rocm7.1
|
| 9 |
+
torchvision==0.25.0+rocm7.1
|
| 10 |
+
torchaudio==2.10.0+rocm7.1
|
code-review-env/requirements.txt
CHANGED
|
@@ -4,14 +4,19 @@ networkx>=3.2
|
|
| 4 |
pydantic>=2.7
|
| 5 |
pylint>=3.2
|
| 6 |
bandit>=1.7
|
| 7 |
-
pyflakes>=3.2
|
| 8 |
-
mypy>=1.10
|
| 9 |
pyright>=1.1.390
|
| 10 |
-
|
| 11 |
-
|
| 12 |
fastapi>=0.115
|
| 13 |
uvicorn>=0.30
|
| 14 |
openenv[core]>=0.1.13
|
| 15 |
openai>=1.40
|
| 16 |
pyvis>=0.3.2
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 17 |
pytest>=8.2
|
|
|
|
| 4 |
pydantic>=2.7
|
| 5 |
pylint>=3.2
|
| 6 |
bandit>=1.7
|
|
|
|
|
|
|
| 7 |
pyright>=1.1.390
|
| 8 |
+
pyre-check>=0.9.23
|
| 9 |
+
radon>=6.0
|
| 10 |
fastapi>=0.115
|
| 11 |
uvicorn>=0.30
|
| 12 |
openenv[core]>=0.1.13
|
| 13 |
openai>=1.40
|
| 14 |
pyvis>=0.3.2
|
| 15 |
+
huggingface_hub>=0.31
|
| 16 |
+
llama-cpp-python>=0.3.8
|
| 17 |
+
datasets>=2.20
|
| 18 |
+
trl>=0.25
|
| 19 |
+
transformers>=4.48
|
| 20 |
+
accelerate>=0.33
|
| 21 |
+
peft>=0.12
|
| 22 |
pytest>=8.2
|
code-review-env/scripts/clone_training_repos.sh
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env bash
|
| 2 |
+
set -euo pipefail
|
| 3 |
+
|
| 4 |
+
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
| 5 |
+
CORPUS_DIR="${CORPUS_DIR:-${ROOT_DIR}/training_corpus}"
|
| 6 |
+
|
| 7 |
+
mkdir -p "${CORPUS_DIR}"
|
| 8 |
+
|
| 9 |
+
clone_if_missing() {
|
| 10 |
+
local repo_url="$1"
|
| 11 |
+
local target_name="$2"
|
| 12 |
+
local target_path="${CORPUS_DIR}/${target_name}"
|
| 13 |
+
|
| 14 |
+
if [[ -d "${target_path}" ]]; then
|
| 15 |
+
echo "[SKIP] ${target_name} already exists"
|
| 16 |
+
return
|
| 17 |
+
fi
|
| 18 |
+
|
| 19 |
+
echo "[CLONE] ${repo_url} -> ${target_name}"
|
| 20 |
+
git clone --depth 1 "${repo_url}" "${target_path}"
|
| 21 |
+
}
|
| 22 |
+
|
| 23 |
+
# Tier 1
|
| 24 |
+
clone_if_missing https://github.com/psf/requests.git requests
|
| 25 |
+
clone_if_missing https://github.com/pallets/flask.git flask
|
| 26 |
+
clone_if_missing https://github.com/fastapi/fastapi.git fastapi
|
| 27 |
+
clone_if_missing https://github.com/pydantic/pydantic.git pydantic
|
| 28 |
+
|
| 29 |
+
# Tier 2
|
| 30 |
+
clone_if_missing https://github.com/celery/celery.git celery
|
| 31 |
+
clone_if_missing https://github.com/scrapy/scrapy.git scrapy
|
| 32 |
+
clone_if_missing https://github.com/django/django.git django
|
| 33 |
+
clone_if_missing https://github.com/apache/airflow.git airflow
|
| 34 |
+
|
| 35 |
+
# Tier 3
|
| 36 |
+
clone_if_missing https://github.com/frappe/erpnext.git erpnext
|
| 37 |
+
clone_if_missing https://github.com/testdrivenio/fastapi-tdd-docker.git fastapi-tdd-docker
|
| 38 |
+
clone_if_missing https://github.com/tecladocode/rest-api-smorest-docker.git rest-api-smorest-docker
|
| 39 |
+
|
| 40 |
+
echo "[DONE] Training corpus cloned into ${CORPUS_DIR}"
|
code-review-env/scripts/seed_training_corpus.sh
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env bash
|
| 2 |
+
set -euo pipefail
|
| 3 |
+
|
| 4 |
+
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
| 5 |
+
CORPUS_DIR="${CORPUS_DIR:-${ROOT_DIR}/training_corpus}"
|
| 6 |
+
CORPUS_DB_DIR="${CORPUS_DB_DIR:-${ROOT_DIR}/outputs/corpus_dbs}"
|
| 7 |
+
PYTHON_BIN="${PYTHON_BIN:-${ROOT_DIR}/.venv/bin/python}"
|
| 8 |
+
|
| 9 |
+
mkdir -p "${CORPUS_DB_DIR}"
|
| 10 |
+
cd "${ROOT_DIR}"
|
| 11 |
+
|
| 12 |
+
seed_one() {
|
| 13 |
+
local name="$1"
|
| 14 |
+
local rel_path="$2"
|
| 15 |
+
local source_path="${CORPUS_DIR}/${rel_path}"
|
| 16 |
+
local db_path="${CORPUS_DB_DIR}/${name}.db"
|
| 17 |
+
|
| 18 |
+
if [[ ! -d "${source_path}" ]]; then
|
| 19 |
+
echo "[WARN] Missing source for ${name}: ${source_path}"
|
| 20 |
+
return
|
| 21 |
+
fi
|
| 22 |
+
|
| 23 |
+
echo "[SEED] ${name} from ${source_path}"
|
| 24 |
+
GRAPHREVIEW_DB_PATH="${db_path}" "${PYTHON_BIN}" -m db.seed "${source_path}" --force
|
| 25 |
+
}
|
| 26 |
+
|
| 27 |
+
# Tier 1
|
| 28 |
+
seed_one requests requests/src/requests
|
| 29 |
+
seed_one flask flask/src/flask
|
| 30 |
+
seed_one fastapi fastapi/fastapi
|
| 31 |
+
seed_one pydantic pydantic/pydantic
|
| 32 |
+
|
| 33 |
+
# Tier 2
|
| 34 |
+
seed_one celery celery/celery
|
| 35 |
+
seed_one scrapy_core scrapy/scrapy/core
|
| 36 |
+
seed_one scrapy_pipelines scrapy/scrapy/pipelines
|
| 37 |
+
seed_one django_db django/django/db
|
| 38 |
+
seed_one django_http django/django/http
|
| 39 |
+
seed_one django_auth django/django/contrib/auth
|
| 40 |
+
seed_one airflow airflow/airflow
|
| 41 |
+
|
| 42 |
+
# Tier 3
|
| 43 |
+
seed_one erpnext erpnext/erpnext
|
| 44 |
+
seed_one fastapi_tdd fastapi-tdd-docker/project
|
| 45 |
+
seed_one rest_api_smorest rest-api-smorest-docker/app
|
| 46 |
+
|
| 47 |
+
echo "[DONE] Corpus seeding complete -> ${CORPUS_DB_DIR}"
|
code-review-env/scripts/verify_all.py
ADDED
|
@@ -0,0 +1,525 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
import json
|
| 4 |
+
import os
|
| 5 |
+
import pathlib
|
| 6 |
+
import re
|
| 7 |
+
import shutil
|
| 8 |
+
import subprocess
|
| 9 |
+
import sys
|
| 10 |
+
import textwrap
|
| 11 |
+
from dataclasses import dataclass, field
|
| 12 |
+
from typing import Any
|
| 13 |
+
|
| 14 |
+
|
| 15 |
+
ROOT = pathlib.Path(__file__).resolve().parents[1]
|
| 16 |
+
OUTPUTS = ROOT / "outputs"
|
| 17 |
+
REPORT_PATH = OUTPUTS / "verification_report.txt"
|
| 18 |
+
|
| 19 |
+
|
| 20 |
+
@dataclass
|
| 21 |
+
class VerificationState:
|
| 22 |
+
failures: list[str] = field(default_factory=list)
|
| 23 |
+
warnings: list[str] = field(default_factory=list)
|
| 24 |
+
info: list[str] = field(default_factory=list)
|
| 25 |
+
|
| 26 |
+
def fail(self, msg: str) -> None:
|
| 27 |
+
self.failures.append(msg)
|
| 28 |
+
self.info.append(f"FAIL: {msg}")
|
| 29 |
+
|
| 30 |
+
def warn(self, msg: str) -> None:
|
| 31 |
+
self.warnings.append(msg)
|
| 32 |
+
self.info.append(f"WARNING: {msg}")
|
| 33 |
+
|
| 34 |
+
def ok(self, msg: str) -> None:
|
| 35 |
+
self.info.append(f"PASS: {msg}")
|
| 36 |
+
|
| 37 |
+
|
| 38 |
+
def _run_python(code: str, timeout: int = 120) -> tuple[int, str, str]:
|
| 39 |
+
proc = subprocess.run(
|
| 40 |
+
[str(ROOT / ".venv" / "bin" / "python"), "-c", code],
|
| 41 |
+
cwd=str(ROOT),
|
| 42 |
+
capture_output=True,
|
| 43 |
+
text=True,
|
| 44 |
+
timeout=timeout,
|
| 45 |
+
check=False,
|
| 46 |
+
)
|
| 47 |
+
return proc.returncode, proc.stdout, proc.stderr
|
| 48 |
+
|
| 49 |
+
|
| 50 |
+
def _run_cmd(cmd: list[str], timeout: int = 180, cwd: pathlib.Path | None = None) -> tuple[int, str, str]:
|
| 51 |
+
proc = subprocess.run(
|
| 52 |
+
cmd,
|
| 53 |
+
cwd=str(cwd or ROOT),
|
| 54 |
+
capture_output=True,
|
| 55 |
+
text=True,
|
| 56 |
+
timeout=timeout,
|
| 57 |
+
check=False,
|
| 58 |
+
)
|
| 59 |
+
return proc.returncode, proc.stdout, proc.stderr
|
| 60 |
+
|
| 61 |
+
|
| 62 |
+
def _pyright_bin() -> str:
|
| 63 |
+
candidate = ROOT / ".venv" / "bin" / "pyright"
|
| 64 |
+
return str(candidate) if candidate.exists() else "pyright"
|
| 65 |
+
|
| 66 |
+
|
| 67 |
+
def section_1_rocm_and_unsloth(state: VerificationState) -> None:
|
| 68 |
+
state.info.append("\n=== SECTION 1: AMD ROCm + Unsloth Setup ===")
|
| 69 |
+
|
| 70 |
+
rc, out, err = _run_python(
|
| 71 |
+
textwrap.dedent(
|
| 72 |
+
"""
|
| 73 |
+
import torch
|
| 74 |
+
print(f"cuda_available={torch.cuda.is_available()}")
|
| 75 |
+
if torch.cuda.is_available():
|
| 76 |
+
p = torch.cuda.get_device_properties(0)
|
| 77 |
+
print(f"device={torch.cuda.get_device_name(0)}")
|
| 78 |
+
print(f"hip={torch.version.hip}")
|
| 79 |
+
print(f"vram={p.total_memory/1e9:.1f}")
|
| 80 |
+
"""
|
| 81 |
+
)
|
| 82 |
+
)
|
| 83 |
+
if rc != 0:
|
| 84 |
+
state.warn(f"ROCm detection script failed: {err.strip() or out.strip()}")
|
| 85 |
+
elif "cuda_available=True" not in out:
|
| 86 |
+
state.warn("CUDA/ROCm not available in current environment; set HSA_OVERRIDE_GFX_VERSION=11.0.0 on RX 7900 GRE")
|
| 87 |
+
else:
|
| 88 |
+
state.ok("ROCm/CUDA available")
|
| 89 |
+
|
| 90 |
+
rc, out, err = _run_python(
|
| 91 |
+
"import unsloth, unsloth_zoo; print(unsloth.__version__)"
|
| 92 |
+
)
|
| 93 |
+
if rc != 0:
|
| 94 |
+
msg = err.strip() or out.strip()
|
| 95 |
+
if "no usable HIP accelerator" in msg or "NotImplementedError" in msg:
|
| 96 |
+
state.warn(f"Unsloth import requires ROCm torch wheels in this host env: {msg}")
|
| 97 |
+
else:
|
| 98 |
+
state.fail(f"Unsloth import failed: {msg}")
|
| 99 |
+
else:
|
| 100 |
+
state.ok("Unsloth import check passed")
|
| 101 |
+
|
| 102 |
+
train_src = (ROOT / "training" / "train_lora.py").read_text(encoding="utf-8")
|
| 103 |
+
if "load_in_4bit=True" in train_src:
|
| 104 |
+
state.fail("train_lora.py still has load_in_4bit=True")
|
| 105 |
+
elif "load_in_4bit=False" in train_src and "load_in_16bit=True" in train_src:
|
| 106 |
+
state.ok("QLoRA AMD guard check passed")
|
| 107 |
+
else:
|
| 108 |
+
state.fail("train_lora.py missing explicit load_in_4bit/load_in_16bit AMD config")
|
| 109 |
+
|
| 110 |
+
if 'use_gradient_checkpointing="unsloth"' not in train_src:
|
| 111 |
+
state.fail('train_lora.py missing use_gradient_checkpointing="unsloth"')
|
| 112 |
+
else:
|
| 113 |
+
state.ok("Gemma4 gradient checkpointing guard passed")
|
| 114 |
+
|
| 115 |
+
|
| 116 |
+
def section_2_static_analysis(state: VerificationState) -> None:
|
| 117 |
+
state.info.append("\n=== SECTION 2: Static Analysis Pipeline ===")
|
| 118 |
+
|
| 119 |
+
rc, out, _ = _run_cmd(["grep", "-r", "semgrep", "analyzers/", "db/", "inference.py"], timeout=30)
|
| 120 |
+
if rc == 0 and out.strip():
|
| 121 |
+
state.fail(f"Semgrep references remain:\n{out.strip()}")
|
| 122 |
+
else:
|
| 123 |
+
state.ok("Semgrep removed from core runtime paths")
|
| 124 |
+
|
| 125 |
+
test_file = pathlib.Path("/tmp/pyright_test.py")
|
| 126 |
+
test_file.write_text("def f(x: int) -> str:\n return x\n", encoding="utf-8")
|
| 127 |
+
rc, out, err = _run_cmd([_pyright_bin(), "--outputjson", str(test_file)], timeout=30)
|
| 128 |
+
if rc not in {0, 1}:
|
| 129 |
+
state.fail(f"Pyright invocation failed: {err.strip()}")
|
| 130 |
+
else:
|
| 131 |
+
try:
|
| 132 |
+
payload = json.loads(out)
|
| 133 |
+
errors = [d for d in payload.get("generalDiagnostics", []) if d.get("severity") == "error"]
|
| 134 |
+
if not errors:
|
| 135 |
+
state.fail("Pyright failed to report known type error")
|
| 136 |
+
else:
|
| 137 |
+
state.ok(f"Pyright JSON check passed ({len(errors)} errors on test file)")
|
| 138 |
+
except Exception as exc:
|
| 139 |
+
state.fail(f"Pyright JSON decode failed: {exc}")
|
| 140 |
+
|
| 141 |
+
rc, out, err = _run_python(
|
| 142 |
+
textwrap.dedent(
|
| 143 |
+
"""
|
| 144 |
+
from analyzers.ast_checker import run_all
|
| 145 |
+
import pathlib, textwrap
|
| 146 |
+
p = pathlib.Path('/tmp/ast_test.py')
|
| 147 |
+
p.write_text(textwrap.dedent('''
|
| 148 |
+
def bad_default(x=[]):
|
| 149 |
+
return x
|
| 150 |
+
try:
|
| 151 |
+
pass
|
| 152 |
+
except:
|
| 153 |
+
pass
|
| 154 |
+
x = None
|
| 155 |
+
if x == None:
|
| 156 |
+
pass
|
| 157 |
+
'''))
|
| 158 |
+
findings = run_all(str(p))
|
| 159 |
+
print(sorted({f.rule for f in findings}))
|
| 160 |
+
"""
|
| 161 |
+
)
|
| 162 |
+
)
|
| 163 |
+
if rc != 0:
|
| 164 |
+
state.fail(f"AST checker execution failed: {err.strip() or out.strip()}")
|
| 165 |
+
else:
|
| 166 |
+
rules = set(json.loads(out.strip().replace("'", '"')) if out.strip().startswith("[") else [])
|
| 167 |
+
expected = {"mutable_default_arg", "bare_except", "none_equality_check"}
|
| 168 |
+
if not expected.issubset(rules):
|
| 169 |
+
state.fail(f"AST checker missing expected rules. got={rules}")
|
| 170 |
+
else:
|
| 171 |
+
state.ok("AST checker known-pattern checks passed")
|
| 172 |
+
|
| 173 |
+
rc, out, err = _run_python(
|
| 174 |
+
textwrap.dedent(
|
| 175 |
+
"""
|
| 176 |
+
from analyzers.pipeline import run_pipeline
|
| 177 |
+
findings = run_pipeline('sample_project')
|
| 178 |
+
print(len(findings))
|
| 179 |
+
print(sorted({f.severity for f in findings}))
|
| 180 |
+
"""
|
| 181 |
+
),
|
| 182 |
+
timeout=180,
|
| 183 |
+
)
|
| 184 |
+
if rc != 0:
|
| 185 |
+
state.fail(f"Analyzer pipeline run failed: {err.strip() or out.strip()}")
|
| 186 |
+
else:
|
| 187 |
+
lines = [l.strip() for l in out.splitlines() if l.strip()]
|
| 188 |
+
count = int(lines[0]) if lines else 0
|
| 189 |
+
severities = set()
|
| 190 |
+
if len(lines) > 1:
|
| 191 |
+
try:
|
| 192 |
+
severities = set(json.loads(lines[1].replace("'", '"')))
|
| 193 |
+
except Exception:
|
| 194 |
+
pass
|
| 195 |
+
if count <= 10:
|
| 196 |
+
state.fail(f"Pipeline findings too low: {count}")
|
| 197 |
+
elif "high" not in severities:
|
| 198 |
+
state.fail(f"Pipeline produced no high severity findings: {severities}")
|
| 199 |
+
else:
|
| 200 |
+
state.ok(f"Pipeline findings check passed ({count})")
|
| 201 |
+
|
| 202 |
+
|
| 203 |
+
def section_3_agent_judge(state: VerificationState) -> None:
|
| 204 |
+
state.info.append("\n=== SECTION 3: Agent + Judge ===")
|
| 205 |
+
|
| 206 |
+
rc, out, err = _run_python(
|
| 207 |
+
textwrap.dedent(
|
| 208 |
+
"""
|
| 209 |
+
from llm.agent_runner import extract_thinking_and_action
|
| 210 |
+
import json
|
| 211 |
+
test_output = '''
|
| 212 |
+
<think>
|
| 213 |
+
root cause is config.py
|
| 214 |
+
</think>
|
| 215 |
+
<action>
|
| 216 |
+
{"action_type": "FLAG_DEPENDENCY_ISSUE", "target_line": 34, "content": "x", "attributed_to": "config"}
|
| 217 |
+
</action>
|
| 218 |
+
'''
|
| 219 |
+
thinking, action = extract_thinking_and_action(test_output)
|
| 220 |
+
print(len(thinking))
|
| 221 |
+
print(action.get('action_type',''))
|
| 222 |
+
print(action.get('attributed_to',''))
|
| 223 |
+
"""
|
| 224 |
+
)
|
| 225 |
+
)
|
| 226 |
+
if rc != 0:
|
| 227 |
+
state.fail(f"Thinking extraction check failed: {err.strip() or out.strip()}")
|
| 228 |
+
else:
|
| 229 |
+
vals = [l.strip() for l in out.splitlines() if l.strip()]
|
| 230 |
+
if len(vals) < 3 or int(vals[0]) <= 20 or vals[1] != "FLAG_DEPENDENCY_ISSUE" or vals[2] != "config":
|
| 231 |
+
state.fail(f"Thinking extraction invalid output: {vals}")
|
| 232 |
+
else:
|
| 233 |
+
state.ok("Thinking trace extraction check passed")
|
| 234 |
+
|
| 235 |
+
if not os.getenv("HF_TOKEN"):
|
| 236 |
+
state.warn("HF_TOKEN missing; skipping live judge API scoring check")
|
| 237 |
+
else:
|
| 238 |
+
rc, out, err = _run_python(
|
| 239 |
+
textwrap.dedent(
|
| 240 |
+
"""
|
| 241 |
+
from llm.thinking_judge import score_thinking
|
| 242 |
+
result = score_thinking(
|
| 243 |
+
thinking_trace='Bug is in config.py due to None timeout',
|
| 244 |
+
action={'action_type': 'FLAG_DEPENDENCY_ISSUE', 'attributed_to': 'config'},
|
| 245 |
+
finding={'module_id': 'config', 'severity': 'error', 'message': 'Missing key returns None'},
|
| 246 |
+
graph_context={'config': {'dependents': ['checkout']}}
|
| 247 |
+
)
|
| 248 |
+
print(result['score'])
|
| 249 |
+
print('what_was_right' in result and 'what_was_wrong' in result)
|
| 250 |
+
"""
|
| 251 |
+
),
|
| 252 |
+
timeout=90,
|
| 253 |
+
)
|
| 254 |
+
if rc != 0:
|
| 255 |
+
state.fail(f"Judge scoring failed: {err.strip() or out.strip()}")
|
| 256 |
+
else:
|
| 257 |
+
lines = [l.strip() for l in out.splitlines() if l.strip()]
|
| 258 |
+
if not lines:
|
| 259 |
+
state.fail("Judge scoring returned empty output")
|
| 260 |
+
else:
|
| 261 |
+
score = float(lines[0])
|
| 262 |
+
if not (0.0 <= score <= 1.0):
|
| 263 |
+
state.fail(f"Judge score out of range: {score}")
|
| 264 |
+
else:
|
| 265 |
+
state.ok("Judge scoring API check passed")
|
| 266 |
+
|
| 267 |
+
rc, out, err = _run_python(
|
| 268 |
+
"from training.trajectory_collector import compute_composite_reward as c; print(c(0.6,0.8)); print(c(0.6,0.1))"
|
| 269 |
+
)
|
| 270 |
+
if rc != 0:
|
| 271 |
+
state.fail(f"Composite reward helper failed: {err.strip() or out.strip()}")
|
| 272 |
+
else:
|
| 273 |
+
lines = [float(x.strip()) for x in out.splitlines() if x.strip()]
|
| 274 |
+
if len(lines) != 2 or abs(lines[0] - (0.6 * 0.6 + 0.8 * 0.4)) > 1e-3 or lines[1] >= lines[0]:
|
| 275 |
+
state.fail("Composite reward formula verification failed")
|
| 276 |
+
else:
|
| 277 |
+
state.ok("Composite reward formula check passed")
|
| 278 |
+
|
| 279 |
+
|
| 280 |
+
def section_4_training_data(state: VerificationState) -> None:
|
| 281 |
+
state.info.append("\n=== SECTION 4: Training Data Quality ===")
|
| 282 |
+
dataset_path = ROOT / "outputs" / "training" / "dataset.latest.jsonl"
|
| 283 |
+
if not dataset_path.exists():
|
| 284 |
+
state.warn("dataset.latest.jsonl missing; run inference.py <target> or trajectory collection first")
|
| 285 |
+
return
|
| 286 |
+
|
| 287 |
+
records = [json.loads(l) for l in dataset_path.read_text(encoding="utf-8").splitlines() if l.strip()]
|
| 288 |
+
if len(records) < 50:
|
| 289 |
+
state.fail(f"Training records too low: {len(records)}")
|
| 290 |
+
else:
|
| 291 |
+
state.ok(f"Training record count OK: {len(records)}")
|
| 292 |
+
|
| 293 |
+
thinking_count = sum(1 for r in records if "<think>" in str(r.get("text", "")) or "<think>" in str(r.get("chosen", "")))
|
| 294 |
+
ratio = thinking_count / max(1, len(records))
|
| 295 |
+
if ratio < 0.75:
|
| 296 |
+
state.fail(f"Reasoning ratio too low: {ratio:.0%}")
|
| 297 |
+
else:
|
| 298 |
+
state.ok(f"Reasoning ratio check passed: {ratio:.0%}")
|
| 299 |
+
|
| 300 |
+
dpo_path = ROOT / "outputs" / "training" / "dpo_pairs.jsonl"
|
| 301 |
+
if dpo_path.exists():
|
| 302 |
+
pairs = [json.loads(l) for l in dpo_path.read_text(encoding="utf-8").splitlines() if l.strip()]
|
| 303 |
+
invalid = [p for p in pairs[:20] if not (p.get("prompt") and p.get("chosen") and p.get("rejected") and p.get("chosen") != p.get("rejected"))]
|
| 304 |
+
if invalid:
|
| 305 |
+
state.fail("Invalid DPO pairs detected in spot-check")
|
| 306 |
+
else:
|
| 307 |
+
state.ok(f"DPO pairs spot-check passed ({len(pairs)})")
|
| 308 |
+
else:
|
| 309 |
+
state.warn("No dpo_pairs.jsonl yet (run trajectory collector first)")
|
| 310 |
+
|
| 311 |
+
train_modules = {str(r.get("module_id", "")) for r in records}
|
| 312 |
+
eval_modules = {"cart", "checkout", "auth", "config", "payments"}
|
| 313 |
+
leaked = train_modules & eval_modules
|
| 314 |
+
if leaked:
|
| 315 |
+
state.fail(f"Eval leakage detected: {sorted(leaked)}")
|
| 316 |
+
else:
|
| 317 |
+
state.ok("No direct eval-module leakage in module_id field")
|
| 318 |
+
|
| 319 |
+
|
| 320 |
+
def section_5_env_integrity(state: VerificationState) -> None:
|
| 321 |
+
state.info.append("\n=== SECTION 5: RL Environment Integrity ===")
|
| 322 |
+
|
| 323 |
+
if shutil.which("openenv"):
|
| 324 |
+
rc, out, err = _run_cmd(["openenv", "validate"], timeout=120)
|
| 325 |
+
if rc != 0:
|
| 326 |
+
state.fail(f"openenv validate failed: {err.strip() or out.strip()}")
|
| 327 |
+
else:
|
| 328 |
+
state.ok("openenv validate passed")
|
| 329 |
+
else:
|
| 330 |
+
state.warn("openenv CLI not available; skipping openenv validate")
|
| 331 |
+
|
| 332 |
+
rc, out, err = _run_python(
|
| 333 |
+
textwrap.dedent(
|
| 334 |
+
"""
|
| 335 |
+
from env.environment import CodeReviewEnv
|
| 336 |
+
from env.action import ReviewAction, ActionType
|
| 337 |
+
env = CodeReviewEnv(source_root='sample_project')
|
| 338 |
+
obs = env.reset(task_id='style_review')
|
| 339 |
+
assert obs.within_budget
|
| 340 |
+
assert len(obs.available_actions) > 0
|
| 341 |
+
result = env.step(ReviewAction(action_type=ActionType.REQUEST_CHANGES))
|
| 342 |
+
reward_value = result.reward if isinstance(result.reward, (int,float)) else result.reward.raw_value
|
| 343 |
+
print(reward_value)
|
| 344 |
+
"""
|
| 345 |
+
),
|
| 346 |
+
timeout=120,
|
| 347 |
+
)
|
| 348 |
+
if rc != 0:
|
| 349 |
+
state.fail(f"Environment step verification failed: {err.strip() or out.strip()}")
|
| 350 |
+
else:
|
| 351 |
+
reward = float([l for l in out.splitlines() if l.strip()][-1])
|
| 352 |
+
if not (-2.0 <= reward <= 2.0):
|
| 353 |
+
state.fail(f"Reward out of expected range: {reward}")
|
| 354 |
+
else:
|
| 355 |
+
state.ok("Environment reward-range check passed")
|
| 356 |
+
|
| 357 |
+
|
| 358 |
+
def section_6_hf_readiness(state: VerificationState) -> None:
|
| 359 |
+
state.info.append("\n=== SECTION 6: HF Deployment Readiness ===")
|
| 360 |
+
dockerfile = (ROOT / "Dockerfile").read_text(encoding="utf-8")
|
| 361 |
+
if "7860" not in dockerfile or "CMD" not in dockerfile:
|
| 362 |
+
state.fail("Dockerfile missing required HF Spaces port/CMD settings")
|
| 363 |
+
else:
|
| 364 |
+
state.ok("Dockerfile port and CMD check passed")
|
| 365 |
+
|
| 366 |
+
server_src = (ROOT / "server" / "app.py").read_text(encoding="utf-8")
|
| 367 |
+
for banned in ["import torch", "import llama_cpp", "from unsloth"]:
|
| 368 |
+
if banned in server_src:
|
| 369 |
+
state.fail(f"server/app.py contains banned runtime GPU import: {banned}")
|
| 370 |
+
break
|
| 371 |
+
else:
|
| 372 |
+
state.ok("server/app.py runtime GPU import guard passed")
|
| 373 |
+
|
| 374 |
+
inf_src = (ROOT / "inference.py").read_text(encoding="utf-8")
|
| 375 |
+
if "os.getenv" not in inf_src and "os.environ" not in inf_src:
|
| 376 |
+
state.fail("inference.py does not appear to read environment variables")
|
| 377 |
+
else:
|
| 378 |
+
state.ok("inference.py environment-variable check passed")
|
| 379 |
+
|
| 380 |
+
|
| 381 |
+
def section_7_inference_logs(state: VerificationState) -> None:
|
| 382 |
+
state.info.append("\n=== SECTION 7: Inference Script Compliance ===")
|
| 383 |
+
env = os.environ.copy()
|
| 384 |
+
env.setdefault("GRAPHREVIEW_AGENT_INFERENCE_ENABLED", "false")
|
| 385 |
+
|
| 386 |
+
proc = subprocess.run(
|
| 387 |
+
[str(ROOT / ".venv" / "bin" / "python"), "inference.py", "sample_project"],
|
| 388 |
+
cwd=str(ROOT),
|
| 389 |
+
capture_output=True,
|
| 390 |
+
text=True,
|
| 391 |
+
timeout=1200,
|
| 392 |
+
check=False,
|
| 393 |
+
env=env,
|
| 394 |
+
)
|
| 395 |
+
stdout = proc.stdout
|
| 396 |
+
if "[START]" not in stdout or "[END]" not in stdout:
|
| 397 |
+
state.fail("inference.py missing START/END logs")
|
| 398 |
+
return
|
| 399 |
+
|
| 400 |
+
end_lines = [l for l in stdout.splitlines() if "[END]" in l]
|
| 401 |
+
if not end_lines:
|
| 402 |
+
state.fail("No END line in inference output")
|
| 403 |
+
return
|
| 404 |
+
|
| 405 |
+
try:
|
| 406 |
+
end_data = json.loads(end_lines[-1].split("[END]", 1)[1].strip())
|
| 407 |
+
except Exception as exc:
|
| 408 |
+
state.fail(f"END payload JSON parse failed: {exc}")
|
| 409 |
+
return
|
| 410 |
+
|
| 411 |
+
required = ["agent_findings", "deterministic_findings", "model", "precision", "recall", "run_id"]
|
| 412 |
+
missing = [k for k in required if k not in end_data]
|
| 413 |
+
if missing:
|
| 414 |
+
state.fail(f"END payload missing fields: {missing}")
|
| 415 |
+
else:
|
| 416 |
+
state.ok("END payload fields check passed")
|
| 417 |
+
|
| 418 |
+
if "agent_llm_disabled" in stdout:
|
| 419 |
+
state.fail("inference logs still contain agent_llm_disabled marker")
|
| 420 |
+
|
| 421 |
+
recall = float(end_data.get("recall", 0.0))
|
| 422 |
+
if recall <= 0.05:
|
| 423 |
+
state.fail(f"Recall too low: {recall:.3f}")
|
| 424 |
+
else:
|
| 425 |
+
state.ok(f"Recall threshold check passed ({recall:.3f})")
|
| 426 |
+
|
| 427 |
+
scores: list[float] = [float(end_data.get("precision", 0.0))]
|
| 428 |
+
for _ in range(2):
|
| 429 |
+
p = subprocess.run(
|
| 430 |
+
[str(ROOT / ".venv" / "bin" / "python"), "inference.py", "sample_project"],
|
| 431 |
+
cwd=str(ROOT),
|
| 432 |
+
capture_output=True,
|
| 433 |
+
text=True,
|
| 434 |
+
timeout=1200,
|
| 435 |
+
check=False,
|
| 436 |
+
env=env,
|
| 437 |
+
)
|
| 438 |
+
end = [l for l in p.stdout.splitlines() if "[END]" in l]
|
| 439 |
+
if not end:
|
| 440 |
+
state.fail("Reproducibility run missing END log")
|
| 441 |
+
return
|
| 442 |
+
payload = json.loads(end[-1].split("[END]", 1)[1].strip())
|
| 443 |
+
scores.append(float(payload.get("precision", 0.0)))
|
| 444 |
+
|
| 445 |
+
variance = max(scores) - min(scores)
|
| 446 |
+
if variance >= 0.1:
|
| 447 |
+
state.fail(f"Precision variance too high: scores={scores}, variance={variance:.3f}")
|
| 448 |
+
else:
|
| 449 |
+
state.ok(f"Baseline reproducibility check passed: {scores}")
|
| 450 |
+
|
| 451 |
+
|
| 452 |
+
def section_8_training_graph(state: VerificationState) -> None:
|
| 453 |
+
state.info.append("\n=== SECTION 8: Training Graph Output ===")
|
| 454 |
+
|
| 455 |
+
# Build graph for latest run if needed.
|
| 456 |
+
rc, out, err = _run_python(
|
| 457 |
+
textwrap.dedent(
|
| 458 |
+
"""
|
| 459 |
+
from db.store import Store
|
| 460 |
+
from visualizer.training_graph import build_training_graph
|
| 461 |
+
store = Store(source_root='sample_project')
|
| 462 |
+
runs = store.list_training_runs(limit=1)
|
| 463 |
+
if runs:
|
| 464 |
+
path = build_training_graph(source_root='sample_project', run_id=runs[0].run_id)
|
| 465 |
+
print(path)
|
| 466 |
+
"""
|
| 467 |
+
),
|
| 468 |
+
timeout=180,
|
| 469 |
+
)
|
| 470 |
+
if rc != 0:
|
| 471 |
+
state.warn(f"Graph build helper failed for latest run: {err.strip() or out.strip()}")
|
| 472 |
+
|
| 473 |
+
graph_path = ROOT / "outputs" / "NodeAudit_graph.html"
|
| 474 |
+
if not graph_path.exists():
|
| 475 |
+
state.fail("Training graph HTML not generated at outputs/NodeAudit_graph.html")
|
| 476 |
+
return
|
| 477 |
+
|
| 478 |
+
content = graph_path.read_text(encoding="utf-8")
|
| 479 |
+
if len(content) <= 10_000:
|
| 480 |
+
state.fail("Training graph HTML too small")
|
| 481 |
+
elif "vis-network" not in content and "pyvis" not in content.lower():
|
| 482 |
+
state.fail("Training graph file does not look like a valid pyvis artifact")
|
| 483 |
+
else:
|
| 484 |
+
state.ok("Training graph structure check passed")
|
| 485 |
+
|
| 486 |
+
cdn_refs = re.findall(r'https?://(?!localhost)[^\s"\']+\.js', content)
|
| 487 |
+
external = [u for u in cdn_refs if "cdnjs" not in u and "unpkg" not in u]
|
| 488 |
+
if external:
|
| 489 |
+
state.warn(f"External JS refs remain in graph HTML: {external[:3]}")
|
| 490 |
+
|
| 491 |
+
if "training" not in content.lower() and "avg_reward" not in content.lower():
|
| 492 |
+
state.fail("Training graph is missing training outcome annotation text")
|
| 493 |
+
else:
|
| 494 |
+
state.ok("Training graph annotation text check passed")
|
| 495 |
+
|
| 496 |
+
|
| 497 |
+
def run_verification_suite() -> VerificationState:
|
| 498 |
+
state = VerificationState()
|
| 499 |
+
OUTPUTS.mkdir(parents=True, exist_ok=True)
|
| 500 |
+
|
| 501 |
+
section_1_rocm_and_unsloth(state)
|
| 502 |
+
section_2_static_analysis(state)
|
| 503 |
+
section_3_agent_judge(state)
|
| 504 |
+
section_4_training_data(state)
|
| 505 |
+
section_5_env_integrity(state)
|
| 506 |
+
section_6_hf_readiness(state)
|
| 507 |
+
section_7_inference_logs(state)
|
| 508 |
+
section_8_training_graph(state)
|
| 509 |
+
|
| 510 |
+
REPORT_PATH.write_text("\n".join(state.info) + "\n", encoding="utf-8")
|
| 511 |
+
return state
|
| 512 |
+
|
| 513 |
+
|
| 514 |
+
def test_verification_suite() -> None:
|
| 515 |
+
state = run_verification_suite()
|
| 516 |
+
assert not state.failures, "\n".join(state.failures)
|
| 517 |
+
|
| 518 |
+
|
| 519 |
+
if __name__ == "__main__":
|
| 520 |
+
result = run_verification_suite()
|
| 521 |
+
print("\n".join(result.info))
|
| 522 |
+
if result.failures:
|
| 523 |
+
print(f"\nVerification failed with {len(result.failures)} FAIL items")
|
| 524 |
+
sys.exit(1)
|
| 525 |
+
print("\nVerification passed with no FAIL items")
|
code-review-env/server/static/index.html
CHANGED
|
@@ -123,7 +123,7 @@
|
|
| 123 |
<div class="simple-grid">
|
| 124 |
<article class="panel section">
|
| 125 |
<h3>Deterministic Analyzer Pipeline</h3>
|
| 126 |
-
<p class="muted">Runs
|
| 127 |
<label for="analysisTimeout">Timeout (seconds)</label>
|
| 128 |
<input id="analysisTimeout" type="number" min="10" max="300" value="45" />
|
| 129 |
<button id="runAnalysisBtn" type="button">Run Deterministic Analysis</button>
|
|
|
|
| 123 |
<div class="simple-grid">
|
| 124 |
<article class="panel section">
|
| 125 |
<h3>Deterministic Analyzer Pipeline</h3>
|
| 126 |
+
<p class="muted">Runs pyright strict, pysa taint analysis, bandit, pylint errors-only, radon complexity, and AST logical bug checks, then stores normalized findings in SQLite.</p>
|
| 127 |
<label for="analysisTimeout">Timeout (seconds)</label>
|
| 128 |
<input id="analysisTimeout" type="number" min="10" max="300" value="45" />
|
| 129 |
<button id="runAnalysisBtn" type="button">Run Deterministic Analysis</button>
|
code-review-env/training/__init__.py
CHANGED
|
@@ -1,4 +1,13 @@
|
|
| 1 |
from training.run_manager import TrainingRunManager
|
|
|
|
| 2 |
from training.weights import WeightManifest, WeightSafetyManager
|
| 3 |
|
| 4 |
-
__all__ = [
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
from training.run_manager import TrainingRunManager
|
| 2 |
+
from training.trajectory_collector import DPOPair, TrajectoryCollector, TrajectoryEpisode, TrajectoryStep
|
| 3 |
from training.weights import WeightManifest, WeightSafetyManager
|
| 4 |
|
| 5 |
+
__all__ = [
|
| 6 |
+
"TrainingRunManager",
|
| 7 |
+
"WeightManifest",
|
| 8 |
+
"WeightSafetyManager",
|
| 9 |
+
"TrajectoryCollector",
|
| 10 |
+
"TrajectoryEpisode",
|
| 11 |
+
"TrajectoryStep",
|
| 12 |
+
"DPOPair",
|
| 13 |
+
]
|
code-review-env/training/train_lora.py
ADDED
|
@@ -0,0 +1,196 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
import argparse
|
| 4 |
+
import json
|
| 5 |
+
import os
|
| 6 |
+
from dataclasses import dataclass
|
| 7 |
+
from datetime import UTC, datetime
|
| 8 |
+
from importlib import import_module
|
| 9 |
+
from pathlib import Path
|
| 10 |
+
|
| 11 |
+
|
| 12 |
+
@dataclass(frozen=True)
|
| 13 |
+
class TrainingInputs:
|
| 14 |
+
trajectories_path: Path
|
| 15 |
+
dpo_pairs_path: Path
|
| 16 |
+
output_dir: Path
|
| 17 |
+
hf_repo: str | None
|
| 18 |
+
|
| 19 |
+
|
| 20 |
+
def _parser() -> argparse.ArgumentParser:
|
| 21 |
+
parser = argparse.ArgumentParser(description="Unsloth AMD LoRA training pipeline for NodeAudit")
|
| 22 |
+
parser.add_argument("--trajectories", required=True, help="JSONL produced by trajectory collector")
|
| 23 |
+
parser.add_argument("--dpo-pairs", required=True, help="JSONL preference pairs")
|
| 24 |
+
parser.add_argument("--output-dir", default="outputs", help="Output root")
|
| 25 |
+
parser.add_argument("--push-repo", default=None, help="Optional HF repo for GGUF push")
|
| 26 |
+
return parser
|
| 27 |
+
|
| 28 |
+
|
| 29 |
+
def _build_inputs(args: argparse.Namespace) -> TrainingInputs:
|
| 30 |
+
return TrainingInputs(
|
| 31 |
+
trajectories_path=Path(args.trajectories).resolve(),
|
| 32 |
+
dpo_pairs_path=Path(args.dpo_pairs).resolve(),
|
| 33 |
+
output_dir=Path(args.output_dir).resolve(),
|
| 34 |
+
hf_repo=args.push_repo,
|
| 35 |
+
)
|
| 36 |
+
|
| 37 |
+
|
| 38 |
+
def _load_jsonl(path: Path) -> list[dict[str, object]]:
|
| 39 |
+
rows: list[dict[str, object]] = []
|
| 40 |
+
with path.open("r", encoding="utf-8") as handle:
|
| 41 |
+
for line in handle:
|
| 42 |
+
stripped = line.strip()
|
| 43 |
+
if not stripped:
|
| 44 |
+
continue
|
| 45 |
+
payload = json.loads(stripped)
|
| 46 |
+
if isinstance(payload, dict):
|
| 47 |
+
rows.append(payload)
|
| 48 |
+
return rows
|
| 49 |
+
|
| 50 |
+
|
| 51 |
+
def _trajectory_to_sft_dataset(rows: list[dict[str, object]]):
|
| 52 |
+
examples: list[dict[str, str]] = []
|
| 53 |
+
for episode in rows:
|
| 54 |
+
for step in episode.get("steps", []):
|
| 55 |
+
if not isinstance(step, dict):
|
| 56 |
+
continue
|
| 57 |
+
prompt = str(step.get("prompt") or "")
|
| 58 |
+
thinking = str(step.get("thinking_trace") or "")
|
| 59 |
+
action_json = str(step.get("action_json") or "{}")
|
| 60 |
+
text = f"{prompt}\n<think>\n{thinking}\n</think>\n<action>\n{action_json}\n</action>"
|
| 61 |
+
examples.append({"text": text})
|
| 62 |
+
|
| 63 |
+
# Maintain strong reasoning traces in SFT corpus.
|
| 64 |
+
reasoning_examples = [item for item in examples if "<think>" in item["text"]]
|
| 65 |
+
if examples and (len(reasoning_examples) / len(examples)) < 0.75:
|
| 66 |
+
needed = int(0.75 * len(examples)) - len(reasoning_examples)
|
| 67 |
+
examples.extend(reasoning_examples[: max(needed, 0)])
|
| 68 |
+
|
| 69 |
+
dataset_cls = getattr(import_module("datasets"), "Dataset")
|
| 70 |
+
return dataset_cls.from_list(examples)
|
| 71 |
+
|
| 72 |
+
|
| 73 |
+
def _pairs_to_dataset(rows: list[dict[str, object]]):
|
| 74 |
+
records: list[dict[str, str]] = []
|
| 75 |
+
for row in rows:
|
| 76 |
+
prompt = str(row.get("prompt") or "")
|
| 77 |
+
chosen = str(row.get("chosen") or "")
|
| 78 |
+
rejected = str(row.get("rejected") or "")
|
| 79 |
+
if not prompt or not chosen or not rejected:
|
| 80 |
+
continue
|
| 81 |
+
records.append({"prompt": prompt, "chosen": chosen, "rejected": rejected})
|
| 82 |
+
dataset_cls = getattr(import_module("datasets"), "Dataset")
|
| 83 |
+
return dataset_cls.from_list(records)
|
| 84 |
+
|
| 85 |
+
|
| 86 |
+
def train(inputs: TrainingInputs) -> dict[str, str]:
|
| 87 |
+
FastLanguageModel = getattr(import_module("unsloth"), "FastLanguageModel")
|
| 88 |
+
trl_mod = import_module("trl")
|
| 89 |
+
SFTTrainer = getattr(trl_mod, "SFTTrainer")
|
| 90 |
+
SFTConfig = getattr(trl_mod, "SFTConfig")
|
| 91 |
+
DPOTrainer = getattr(trl_mod, "DPOTrainer")
|
| 92 |
+
DPOConfig = getattr(trl_mod, "DPOConfig")
|
| 93 |
+
|
| 94 |
+
run_id = f"tr-{datetime.now(UTC).strftime('%Y%m%d%H%M%S')}"
|
| 95 |
+
output_root = inputs.output_dir / run_id
|
| 96 |
+
lora_dir = output_root / "lora_weights"
|
| 97 |
+
dpo_dir = output_root / "dpo"
|
| 98 |
+
gguf_dir = output_root / "gemma4-nodeaudit"
|
| 99 |
+
|
| 100 |
+
output_root.mkdir(parents=True, exist_ok=True)
|
| 101 |
+
|
| 102 |
+
model, tokenizer = FastLanguageModel.from_pretrained(
|
| 103 |
+
model_name="unsloth/gemma-4-E4B-it",
|
| 104 |
+
max_seq_length=2048,
|
| 105 |
+
load_in_4bit=False,
|
| 106 |
+
load_in_16bit=True,
|
| 107 |
+
full_finetuning=False,
|
| 108 |
+
)
|
| 109 |
+
model = FastLanguageModel.get_peft_model(
|
| 110 |
+
model,
|
| 111 |
+
r=16,
|
| 112 |
+
lora_alpha=16,
|
| 113 |
+
lora_dropout=0,
|
| 114 |
+
target_modules=["q_proj", "k_proj", "v_proj", "o_proj", "gate_proj", "up_proj", "down_proj"],
|
| 115 |
+
use_gradient_checkpointing="unsloth",
|
| 116 |
+
bias="none",
|
| 117 |
+
)
|
| 118 |
+
|
| 119 |
+
trajectory_rows = _load_jsonl(inputs.trajectories_path)
|
| 120 |
+
pair_rows = _load_jsonl(inputs.dpo_pairs_path)
|
| 121 |
+
|
| 122 |
+
sft_dataset = _trajectory_to_sft_dataset(trajectory_rows)
|
| 123 |
+
dpo_dataset = _pairs_to_dataset(pair_rows)
|
| 124 |
+
|
| 125 |
+
sft_trainer = SFTTrainer(
|
| 126 |
+
model=model,
|
| 127 |
+
tokenizer=tokenizer,
|
| 128 |
+
train_dataset=sft_dataset,
|
| 129 |
+
dataset_text_field="text",
|
| 130 |
+
args=SFTConfig(
|
| 131 |
+
per_device_train_batch_size=2,
|
| 132 |
+
gradient_accumulation_steps=8,
|
| 133 |
+
num_train_epochs=3,
|
| 134 |
+
learning_rate=2e-4,
|
| 135 |
+
lr_scheduler_type="cosine",
|
| 136 |
+
warmup_ratio=0.1,
|
| 137 |
+
bf16=True,
|
| 138 |
+
logging_steps=5,
|
| 139 |
+
save_strategy="epoch",
|
| 140 |
+
output_dir=str(lora_dir),
|
| 141 |
+
),
|
| 142 |
+
)
|
| 143 |
+
sft_trainer.train()
|
| 144 |
+
|
| 145 |
+
dpo_trainer = DPOTrainer(
|
| 146 |
+
model=model,
|
| 147 |
+
ref_model=None,
|
| 148 |
+
args=DPOConfig(beta=0.1, max_length=2048, bf16=True, output_dir=str(dpo_dir)),
|
| 149 |
+
train_dataset=dpo_dataset,
|
| 150 |
+
tokenizer=tokenizer,
|
| 151 |
+
)
|
| 152 |
+
dpo_trainer.train()
|
| 153 |
+
|
| 154 |
+
model.save_pretrained(str(lora_dir))
|
| 155 |
+
tokenizer.save_pretrained(str(lora_dir))
|
| 156 |
+
model.save_pretrained_gguf(str(gguf_dir), tokenizer, quantization_method="q6_k")
|
| 157 |
+
|
| 158 |
+
if inputs.hf_repo:
|
| 159 |
+
hf_token = os.getenv("HF_TOKEN")
|
| 160 |
+
if not hf_token:
|
| 161 |
+
raise RuntimeError("HF_TOKEN is required when --push-repo is set")
|
| 162 |
+
model.push_to_hub_gguf(
|
| 163 |
+
inputs.hf_repo,
|
| 164 |
+
tokenizer,
|
| 165 |
+
quantization_method="q6_k",
|
| 166 |
+
token=hf_token,
|
| 167 |
+
)
|
| 168 |
+
|
| 169 |
+
metadata = {
|
| 170 |
+
"run_id": run_id,
|
| 171 |
+
"lora_dir": str(lora_dir),
|
| 172 |
+
"dpo_dir": str(dpo_dir),
|
| 173 |
+
"gguf_dir": str(gguf_dir),
|
| 174 |
+
"dpo_pairs": str(len(dpo_dataset)),
|
| 175 |
+
}
|
| 176 |
+
meta_path = output_root / "train_metadata.json"
|
| 177 |
+
meta_path.write_text(json.dumps(metadata, indent=2, sort_keys=True), encoding="utf-8")
|
| 178 |
+
|
| 179 |
+
return {
|
| 180 |
+
"run_id": run_id,
|
| 181 |
+
"lora_dir": str(lora_dir),
|
| 182 |
+
"dpo_dir": str(dpo_dir),
|
| 183 |
+
"gguf_dir": str(gguf_dir),
|
| 184 |
+
"metadata": str(meta_path),
|
| 185 |
+
}
|
| 186 |
+
|
| 187 |
+
|
| 188 |
+
def main() -> None:
|
| 189 |
+
args = _parser().parse_args()
|
| 190 |
+
inputs = _build_inputs(args)
|
| 191 |
+
result = train(inputs)
|
| 192 |
+
print(json.dumps(result, sort_keys=True))
|
| 193 |
+
|
| 194 |
+
|
| 195 |
+
if __name__ == "__main__":
|
| 196 |
+
main()
|
code-review-env/training/trajectory_collector.py
ADDED
|
@@ -0,0 +1,312 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
import dataclasses
|
| 4 |
+
import json
|
| 5 |
+
import os
|
| 6 |
+
from collections import defaultdict
|
| 7 |
+
from datetime import UTC, datetime
|
| 8 |
+
from pathlib import Path
|
| 9 |
+
|
| 10 |
+
from db.store import Store
|
| 11 |
+
from env.environment import CodeReviewEnv
|
| 12 |
+
from env.observation import CodeObservation
|
| 13 |
+
from llm.agent_runner import GemmaAgentRunner
|
| 14 |
+
from llm.thinking_judge import JudgeVerdict, ThinkingJudge
|
| 15 |
+
|
| 16 |
+
|
| 17 |
+
@dataclasses.dataclass(frozen=True)
|
| 18 |
+
class TrajectoryStep:
|
| 19 |
+
module_id: str
|
| 20 |
+
task_id: str
|
| 21 |
+
step_number: int
|
| 22 |
+
prompt: str
|
| 23 |
+
thinking_trace: str
|
| 24 |
+
action_json: str
|
| 25 |
+
env_reward: float
|
| 26 |
+
judge_score: float
|
| 27 |
+
final_reward: float
|
| 28 |
+
judge_verdict: str
|
| 29 |
+
|
| 30 |
+
|
| 31 |
+
@dataclasses.dataclass(frozen=True)
|
| 32 |
+
class TrajectoryEpisode:
|
| 33 |
+
run_id: str
|
| 34 |
+
episode_id: str
|
| 35 |
+
task_id: str
|
| 36 |
+
total_steps: int
|
| 37 |
+
cumulative_reward: float
|
| 38 |
+
steps: list[TrajectoryStep]
|
| 39 |
+
|
| 40 |
+
|
| 41 |
+
@dataclasses.dataclass(frozen=True)
|
| 42 |
+
class DPOPair:
|
| 43 |
+
prompt: str
|
| 44 |
+
chosen: str
|
| 45 |
+
rejected: str
|
| 46 |
+
|
| 47 |
+
|
| 48 |
+
def compute_composite_reward(env_reward: float, judge_score: float) -> float:
|
| 49 |
+
return min(1.0, max(0.0, (float(env_reward) * 0.6) + (float(judge_score) * 0.4)))
|
| 50 |
+
|
| 51 |
+
|
| 52 |
+
class TrajectoryCollector:
|
| 53 |
+
def __init__(
|
| 54 |
+
self,
|
| 55 |
+
source_root: str,
|
| 56 |
+
db_path: str | None = None,
|
| 57 |
+
run_id: str | None = None,
|
| 58 |
+
) -> None:
|
| 59 |
+
self.source_root = str(Path(source_root).resolve())
|
| 60 |
+
self.db_path = db_path
|
| 61 |
+
self.run_id = run_id or f"tr-{datetime.now(UTC).strftime('%Y%m%d%H%M%S')}"
|
| 62 |
+
|
| 63 |
+
self.store = Store(source_root=self.source_root, db_path=db_path)
|
| 64 |
+
self.env = CodeReviewEnv(source_root=self.source_root, db_path=db_path)
|
| 65 |
+
self.agent = GemmaAgentRunner(model_path=os.getenv("GRAPHREVIEW_GEMMA_GGUF_PATH"))
|
| 66 |
+
self.judge = ThinkingJudge(model_name=os.getenv("JUDGE_MODEL", "Qwen/Qwen2.5-7B-Instruct"))
|
| 67 |
+
|
| 68 |
+
def run_episodes(self, task_ids: list[str], episodes_per_task: int = 2) -> list[TrajectoryEpisode]:
|
| 69 |
+
episodes: list[TrajectoryEpisode] = []
|
| 70 |
+
for task_id in task_ids:
|
| 71 |
+
for _ in range(max(episodes_per_task, 1)):
|
| 72 |
+
observation = self.env.reset(task_id=task_id)
|
| 73 |
+
state = self.env.state()
|
| 74 |
+
episode_id = state.episode.episode_id
|
| 75 |
+
step_index = 0
|
| 76 |
+
done = False
|
| 77 |
+
cumulative = 0.0
|
| 78 |
+
rows: list[TrajectoryStep] = []
|
| 79 |
+
|
| 80 |
+
while not done:
|
| 81 |
+
step_index += 1
|
| 82 |
+
prompt = self._observation_prompt(observation)
|
| 83 |
+
agent_out = self.agent.run(observation)
|
| 84 |
+
step_result = self.env.step(agent_out.action)
|
| 85 |
+
|
| 86 |
+
env_reward = self._normalize_reward(step_result.reward)
|
| 87 |
+
judge = self._maybe_judge(
|
| 88 |
+
observation=observation,
|
| 89 |
+
thinking_trace=agent_out.thinking_trace,
|
| 90 |
+
action_json=agent_out.action.model_dump_json(exclude_none=True),
|
| 91 |
+
action_type=agent_out.action.action_type.value,
|
| 92 |
+
)
|
| 93 |
+
judge_score = judge.score if judge is not None else 0.0
|
| 94 |
+
final_reward = compute_composite_reward(env_reward=env_reward, judge_score=judge_score)
|
| 95 |
+
cumulative += final_reward
|
| 96 |
+
|
| 97 |
+
verdict_summary = ""
|
| 98 |
+
if judge is not None:
|
| 99 |
+
verdict_summary = (
|
| 100 |
+
f"{judge.reasoning_depth} | right: {judge.what_was_right} | wrong: {judge.what_was_wrong}"
|
| 101 |
+
)
|
| 102 |
+
|
| 103 |
+
row = TrajectoryStep(
|
| 104 |
+
module_id=observation.module_id,
|
| 105 |
+
task_id=task_id,
|
| 106 |
+
step_number=step_index,
|
| 107 |
+
prompt=prompt,
|
| 108 |
+
thinking_trace=agent_out.thinking_trace,
|
| 109 |
+
action_json=agent_out.action.model_dump_json(exclude_none=True),
|
| 110 |
+
env_reward=env_reward,
|
| 111 |
+
judge_score=judge_score,
|
| 112 |
+
final_reward=final_reward,
|
| 113 |
+
judge_verdict=verdict_summary,
|
| 114 |
+
)
|
| 115 |
+
rows.append(row)
|
| 116 |
+
|
| 117 |
+
self._persist_step(row)
|
| 118 |
+
|
| 119 |
+
observation = step_result.observation
|
| 120 |
+
done = bool(step_result.done)
|
| 121 |
+
|
| 122 |
+
episodes.append(
|
| 123 |
+
TrajectoryEpisode(
|
| 124 |
+
run_id=self.run_id,
|
| 125 |
+
episode_id=episode_id,
|
| 126 |
+
task_id=task_id,
|
| 127 |
+
total_steps=len(rows),
|
| 128 |
+
cumulative_reward=cumulative,
|
| 129 |
+
steps=rows,
|
| 130 |
+
)
|
| 131 |
+
)
|
| 132 |
+
return episodes
|
| 133 |
+
|
| 134 |
+
def build_dpo_pairs(self, episodes: list[TrajectoryEpisode]) -> list[DPOPair]:
|
| 135 |
+
grouped: dict[tuple[str, str, str], list[TrajectoryStep]] = defaultdict(list)
|
| 136 |
+
for episode in episodes:
|
| 137 |
+
for step in episode.steps:
|
| 138 |
+
key = (step.task_id, step.module_id, step.prompt)
|
| 139 |
+
grouped[key].append(step)
|
| 140 |
+
|
| 141 |
+
pairs: list[DPOPair] = []
|
| 142 |
+
for _, steps in grouped.items():
|
| 143 |
+
best = max(steps, key=lambda item: item.final_reward)
|
| 144 |
+
worst = min(steps, key=lambda item: item.final_reward)
|
| 145 |
+
if best.final_reward <= 0.6 or worst.final_reward >= 0.3:
|
| 146 |
+
continue
|
| 147 |
+
pairs.append(
|
| 148 |
+
DPOPair(
|
| 149 |
+
prompt=best.prompt,
|
| 150 |
+
chosen=self._response_block(best.thinking_trace, best.action_json),
|
| 151 |
+
rejected=self._response_block(worst.thinking_trace, worst.action_json),
|
| 152 |
+
)
|
| 153 |
+
)
|
| 154 |
+
pairs.append(
|
| 155 |
+
DPOPair(
|
| 156 |
+
prompt=best.prompt,
|
| 157 |
+
chosen=self._response_block(best.thinking_trace, best.action_json),
|
| 158 |
+
rejected=self._synthetic_wrong_attribution(best.action_json),
|
| 159 |
+
)
|
| 160 |
+
)
|
| 161 |
+
return pairs
|
| 162 |
+
|
| 163 |
+
def save_outputs(self, episodes: list[TrajectoryEpisode], dpo_pairs: list[DPOPair], output_dir: str = "outputs") -> dict[str, str]:
|
| 164 |
+
root = Path(output_dir).resolve()
|
| 165 |
+
root.mkdir(parents=True, exist_ok=True)
|
| 166 |
+
|
| 167 |
+
trajectories_path = root / f"{self.run_id}_trajectories.jsonl"
|
| 168 |
+
dpo_path = root / f"{self.run_id}_dpo_pairs.jsonl"
|
| 169 |
+
stable_training_dir = root / "training"
|
| 170 |
+
stable_training_dir.mkdir(parents=True, exist_ok=True)
|
| 171 |
+
stable_dataset_path = stable_training_dir / "dataset.latest.jsonl"
|
| 172 |
+
stable_dpo_path = stable_training_dir / "dpo_pairs.jsonl"
|
| 173 |
+
|
| 174 |
+
with trajectories_path.open("w", encoding="utf-8") as handle:
|
| 175 |
+
for episode in episodes:
|
| 176 |
+
payload = dataclasses.asdict(episode)
|
| 177 |
+
handle.write(json.dumps(payload, sort_keys=True) + "\n")
|
| 178 |
+
|
| 179 |
+
with dpo_path.open("w", encoding="utf-8") as handle:
|
| 180 |
+
for pair in dpo_pairs:
|
| 181 |
+
handle.write(json.dumps(dataclasses.asdict(pair), sort_keys=True) + "\n")
|
| 182 |
+
|
| 183 |
+
flat_records: list[dict[str, object]] = []
|
| 184 |
+
for episode in episodes:
|
| 185 |
+
for step in episode.steps:
|
| 186 |
+
text = self._response_block(step.thinking_trace, step.action_json)
|
| 187 |
+
flat_records.append(
|
| 188 |
+
{
|
| 189 |
+
"module_id": f"{Path(self.source_root).name}/{step.module_id}",
|
| 190 |
+
"task_id": step.task_id,
|
| 191 |
+
"text": text,
|
| 192 |
+
"chosen": text,
|
| 193 |
+
"reward": step.final_reward,
|
| 194 |
+
}
|
| 195 |
+
)
|
| 196 |
+
|
| 197 |
+
with stable_dataset_path.open("w", encoding="utf-8") as handle:
|
| 198 |
+
for item in flat_records:
|
| 199 |
+
handle.write(json.dumps(item, sort_keys=True) + "\n")
|
| 200 |
+
|
| 201 |
+
with stable_dpo_path.open("w", encoding="utf-8") as handle:
|
| 202 |
+
for pair in dpo_pairs:
|
| 203 |
+
handle.write(json.dumps(dataclasses.asdict(pair), sort_keys=True) + "\n")
|
| 204 |
+
|
| 205 |
+
return {
|
| 206 |
+
"trajectories": str(trajectories_path),
|
| 207 |
+
"dpo_pairs": str(dpo_path),
|
| 208 |
+
"dataset_latest": str(stable_dataset_path),
|
| 209 |
+
"dpo_pairs_latest": str(stable_dpo_path),
|
| 210 |
+
}
|
| 211 |
+
|
| 212 |
+
def _observation_prompt(self, observation: CodeObservation) -> str:
|
| 213 |
+
deps = "\n".join(f"- {item.module_id}: {item.summary}" for item in observation.dependency_summaries)
|
| 214 |
+
dependents = "\n".join(f"- {item.module_id}: {item.summary}" for item in observation.dependent_summaries)
|
| 215 |
+
reviews = "\n".join(f"- {item}" for item in observation.neighbor_reviews)
|
| 216 |
+
return (
|
| 217 |
+
f"Module: {observation.module_id}\n"
|
| 218 |
+
f"Code:\n{observation.code}\n\n"
|
| 219 |
+
f"AST Summary: {json.dumps(observation.ast_summary, ensure_ascii=True)}\n"
|
| 220 |
+
f"Dependencies:\n{deps or '- none'}\n"
|
| 221 |
+
f"Dependents:\n{dependents or '- none'}\n"
|
| 222 |
+
f"Prior neighbor reviews:\n{reviews or '- none'}\n"
|
| 223 |
+
f"Task: {observation.task_description}\n"
|
| 224 |
+
f"Available actions: {', '.join(observation.available_actions)}\n"
|
| 225 |
+
f"Token budget remaining: {observation.token_usage}\n"
|
| 226 |
+
)
|
| 227 |
+
|
| 228 |
+
def _normalize_reward(self, raw_reward: float) -> float:
|
| 229 |
+
clipped = max(-2.0, min(2.0, float(raw_reward)))
|
| 230 |
+
return (clipped + 2.0) / 4.0
|
| 231 |
+
|
| 232 |
+
def _maybe_judge(
|
| 233 |
+
self,
|
| 234 |
+
*,
|
| 235 |
+
observation: CodeObservation,
|
| 236 |
+
thinking_trace: str,
|
| 237 |
+
action_json: str,
|
| 238 |
+
action_type: str,
|
| 239 |
+
) -> JudgeVerdict | None:
|
| 240 |
+
if action_type not in {"FLAG_DEPENDENCY_ISSUE", "APPROVE", "REQUEST_CHANGES"}:
|
| 241 |
+
return None
|
| 242 |
+
|
| 243 |
+
ground_truth = self.store.get_analyzer_findings_for_module(observation.module_id)
|
| 244 |
+
best_finding = "none"
|
| 245 |
+
if ground_truth:
|
| 246 |
+
primary = sorted(
|
| 247 |
+
ground_truth,
|
| 248 |
+
key=lambda item: (item.severity.value != "high", item.line),
|
| 249 |
+
)[0]
|
| 250 |
+
best_finding = f"{primary.analyzer}:{primary.rule_id}:{primary.module_id}:{primary.line}:{primary.message}"
|
| 251 |
+
|
| 252 |
+
graph_context = (
|
| 253 |
+
f"module={observation.module_id};"
|
| 254 |
+
f"deps={[item.module_id for item in observation.dependency_summaries]};"
|
| 255 |
+
f"dependents={[item.module_id for item in observation.dependent_summaries]}"
|
| 256 |
+
)
|
| 257 |
+
|
| 258 |
+
try:
|
| 259 |
+
return self.judge.score(
|
| 260 |
+
finding=best_finding,
|
| 261 |
+
thinking_trace=thinking_trace,
|
| 262 |
+
action=self._action_from_json(action_json),
|
| 263 |
+
graph_context=graph_context,
|
| 264 |
+
)
|
| 265 |
+
except Exception:
|
| 266 |
+
return JudgeVerdict(
|
| 267 |
+
score=0.0,
|
| 268 |
+
causal_chain_correct=False,
|
| 269 |
+
attribution_correct=False,
|
| 270 |
+
reasoning_depth="shallow",
|
| 271 |
+
what_was_right="",
|
| 272 |
+
what_was_wrong="judge_call_failed",
|
| 273 |
+
)
|
| 274 |
+
|
| 275 |
+
def _action_from_json(self, action_json: str):
|
| 276 |
+
from env.action import ActionType, ReviewAction
|
| 277 |
+
|
| 278 |
+
payload = json.loads(action_json)
|
| 279 |
+
action_type = ActionType(str(payload.get("action_type", "REQUEST_CHANGES")))
|
| 280 |
+
return ReviewAction(
|
| 281 |
+
action_type=action_type,
|
| 282 |
+
target_line=payload.get("target_line"),
|
| 283 |
+
content=payload.get("content"),
|
| 284 |
+
attributed_to=payload.get("attributed_to"),
|
| 285 |
+
context_request=payload.get("context_request"),
|
| 286 |
+
)
|
| 287 |
+
|
| 288 |
+
def _persist_step(self, step: TrajectoryStep) -> None:
|
| 289 |
+
self.store.create_training_annotation(
|
| 290 |
+
run_id=self.run_id,
|
| 291 |
+
module_id=step.module_id,
|
| 292 |
+
task_id=step.task_id,
|
| 293 |
+
judge_verdict=step.judge_verdict,
|
| 294 |
+
avg_reward=step.final_reward,
|
| 295 |
+
action_type=json.loads(step.action_json).get("action_type", "UNKNOWN"),
|
| 296 |
+
action_payload=step.action_json,
|
| 297 |
+
thinking_quality=step.judge_score,
|
| 298 |
+
correct_attribution="" if '"attributed_to"' not in step.action_json else "candidate",
|
| 299 |
+
wrong_attribution="",
|
| 300 |
+
)
|
| 301 |
+
|
| 302 |
+
def _response_block(self, thinking: str, action_json: str) -> str:
|
| 303 |
+
return f"<think>\n{thinking}\n</think>\n<action>\n{action_json}\n</action>"
|
| 304 |
+
|
| 305 |
+
def _synthetic_wrong_attribution(self, action_json: str) -> str:
|
| 306 |
+
try:
|
| 307 |
+
payload = json.loads(action_json)
|
| 308 |
+
payload["attributed_to"] = "__wrong_module__"
|
| 309 |
+
rewritten = json.dumps(payload, ensure_ascii=True)
|
| 310 |
+
except Exception:
|
| 311 |
+
rewritten = '{"action_type":"FLAG_DEPENDENCY_ISSUE","attributed_to":"__wrong_module__"}'
|
| 312 |
+
return f"<think>\nIncorrect attribution injected for contrast\n</think>\n<action>\n{rewritten}\n</action>"
|
code-review-env/visualizer/__init__.py
CHANGED
|
@@ -1,9 +1,11 @@
|
|
| 1 |
from visualizer.report_generator import GeneratedArtifacts, ReviewQualityMetrics, generate_phase5_outputs
|
| 2 |
from visualizer.pyvis_renderer import render_graph_html
|
|
|
|
| 3 |
|
| 4 |
__all__ = [
|
| 5 |
"GeneratedArtifacts",
|
| 6 |
"ReviewQualityMetrics",
|
| 7 |
"generate_phase5_outputs",
|
| 8 |
"render_graph_html",
|
|
|
|
| 9 |
]
|
|
|
|
| 1 |
from visualizer.report_generator import GeneratedArtifacts, ReviewQualityMetrics, generate_phase5_outputs
|
| 2 |
from visualizer.pyvis_renderer import render_graph_html
|
| 3 |
+
from visualizer.training_graph import build_training_graph
|
| 4 |
|
| 5 |
__all__ = [
|
| 6 |
"GeneratedArtifacts",
|
| 7 |
"ReviewQualityMetrics",
|
| 8 |
"generate_phase5_outputs",
|
| 9 |
"render_graph_html",
|
| 10 |
+
"build_training_graph",
|
| 11 |
]
|
code-review-env/visualizer/report_generator.py
CHANGED
|
@@ -113,7 +113,7 @@ def _compatible_finding_ids(action_type: str, findings: list[LinterFinding]) ->
|
|
| 113 |
ids.append(finding.id)
|
| 114 |
elif action_type == "FLAG_STYLE" and finding.tool != "bandit" and finding.severity.value == "low":
|
| 115 |
ids.append(finding.id)
|
| 116 |
-
elif action_type == "FLAG_BUG" and
|
| 117 |
ids.append(finding.id)
|
| 118 |
return ids
|
| 119 |
|
|
|
|
| 113 |
ids.append(finding.id)
|
| 114 |
elif action_type == "FLAG_STYLE" and finding.tool != "bandit" and finding.severity.value == "low":
|
| 115 |
ids.append(finding.id)
|
| 116 |
+
elif action_type == "FLAG_BUG" and finding.severity.value in {"medium", "high"}:
|
| 117 |
ids.append(finding.id)
|
| 118 |
return ids
|
| 119 |
|
code-review-env/visualizer/training_graph.py
ADDED
|
@@ -0,0 +1,191 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
import json
|
| 4 |
+
import re
|
| 5 |
+
from collections import Counter, defaultdict
|
| 6 |
+
from dataclasses import dataclass
|
| 7 |
+
from pathlib import Path
|
| 8 |
+
from typing import Any
|
| 9 |
+
|
| 10 |
+
from db.store import Store
|
| 11 |
+
from visualizer.pyvis_renderer import _build_network
|
| 12 |
+
|
| 13 |
+
|
| 14 |
+
OUTCOME_COLORS = {
|
| 15 |
+
"well_learned": "#22c55e",
|
| 16 |
+
"partially_learned": "#f59e0b",
|
| 17 |
+
"failed": "#ef4444",
|
| 18 |
+
"not_visited": "#6b7280",
|
| 19 |
+
}
|
| 20 |
+
|
| 21 |
+
|
| 22 |
+
@dataclass(frozen=True)
|
| 23 |
+
class TrainingSummary:
|
| 24 |
+
run_id: str
|
| 25 |
+
episodes: int
|
| 26 |
+
steps: int
|
| 27 |
+
avg_reward: float
|
| 28 |
+
avg_judge: float
|
| 29 |
+
dpo_pairs: int
|
| 30 |
+
top_failures: list[str]
|
| 31 |
+
top_successes: list[str]
|
| 32 |
+
|
| 33 |
+
|
| 34 |
+
def _outcome(avg_reward: float, judge_score: float, wrong_attr_count: int, touched: bool) -> str:
|
| 35 |
+
if not touched:
|
| 36 |
+
return "not_visited"
|
| 37 |
+
if avg_reward > 0.7 and judge_score > 0.7 and wrong_attr_count == 0:
|
| 38 |
+
return "well_learned"
|
| 39 |
+
if avg_reward < 0.4 or wrong_attr_count > 0:
|
| 40 |
+
return "failed"
|
| 41 |
+
return "partially_learned"
|
| 42 |
+
|
| 43 |
+
|
| 44 |
+
def build_training_graph(*, source_root: str, run_id: str, db_path: str | None = None, output_path: str = "outputs/NodeAudit_graph.html") -> Path:
|
| 45 |
+
store = Store(source_root=source_root, db_path=db_path)
|
| 46 |
+
snapshot = store.get_full_graph()
|
| 47 |
+
annotations = store.get_training_annotations(run_id)
|
| 48 |
+
|
| 49 |
+
by_module: dict[str, list[Any]] = defaultdict(list)
|
| 50 |
+
for item in annotations:
|
| 51 |
+
by_module[item.module_id].append(item)
|
| 52 |
+
|
| 53 |
+
net = _build_network(height="920px", width="100%")
|
| 54 |
+
|
| 55 |
+
failed_edges: set[tuple[str, str]] = set()
|
| 56 |
+
all_rewards: list[float] = []
|
| 57 |
+
all_judges: list[float] = []
|
| 58 |
+
|
| 59 |
+
for node in snapshot.nodes:
|
| 60 |
+
rows = by_module.get(node.module_id, [])
|
| 61 |
+
touched = bool(rows)
|
| 62 |
+
rewards = [float(row.avg_reward) for row in rows]
|
| 63 |
+
judges = [float(row.thinking_quality) for row in rows]
|
| 64 |
+
avg_reward = (sum(rewards) / len(rewards)) if rewards else 0.0
|
| 65 |
+
avg_judge = (sum(judges) / len(judges)) if judges else 0.0
|
| 66 |
+
all_rewards.extend(rewards)
|
| 67 |
+
all_judges.extend(judges)
|
| 68 |
+
|
| 69 |
+
action_counts: Counter[str] = Counter()
|
| 70 |
+
correct: list[str] = []
|
| 71 |
+
wrong: list[str] = []
|
| 72 |
+
judge_text: list[str] = []
|
| 73 |
+
|
| 74 |
+
for row in rows:
|
| 75 |
+
try:
|
| 76 |
+
action_counts.update(json.loads(row.action_counts_json))
|
| 77 |
+
except Exception:
|
| 78 |
+
if row.action_type:
|
| 79 |
+
action_counts[row.action_type] += 1
|
| 80 |
+
try:
|
| 81 |
+
correct.extend(json.loads(row.correct_attributions_json))
|
| 82 |
+
except Exception:
|
| 83 |
+
pass
|
| 84 |
+
try:
|
| 85 |
+
wrong.extend(json.loads(row.wrong_attributions_json))
|
| 86 |
+
except Exception:
|
| 87 |
+
pass
|
| 88 |
+
if row.judge_verdict:
|
| 89 |
+
judge_text.append(row.judge_verdict)
|
| 90 |
+
|
| 91 |
+
if row.action_type == "FLAG_DEPENDENCY_ISSUE":
|
| 92 |
+
try:
|
| 93 |
+
payload = json.loads(row.action_payload)
|
| 94 |
+
except Exception:
|
| 95 |
+
payload = {}
|
| 96 |
+
target = str(payload.get("attributed_to") or "")
|
| 97 |
+
if target and wrong:
|
| 98 |
+
failed_edges.add((node.module_id, target))
|
| 99 |
+
|
| 100 |
+
outcome = _outcome(avg_reward, avg_judge, len(wrong), touched)
|
| 101 |
+
actions_pretty = ", ".join(f"{k}x{v}" for k, v in sorted(action_counts.items())) or "none"
|
| 102 |
+
judge_verdict = judge_text[-1] if judge_text else "not judged"
|
| 103 |
+
|
| 104 |
+
tooltip = (
|
| 105 |
+
f"Module: {node.module_id}\n"
|
| 106 |
+
f"Avg Reward: {avg_reward:.2f}\n"
|
| 107 |
+
f"Judge Score: {avg_judge:.2f}\n"
|
| 108 |
+
f"Correct Attributions: {', '.join(correct) if correct else 'none'}\n"
|
| 109 |
+
f"Wrong: {', '.join(wrong) if wrong else 'none'}\n"
|
| 110 |
+
f"Actions: {actions_pretty}\n"
|
| 111 |
+
f"Judge Verdict: {judge_verdict}"
|
| 112 |
+
)
|
| 113 |
+
|
| 114 |
+
net.add_node(
|
| 115 |
+
n_id=node.module_id,
|
| 116 |
+
label=node.module_id,
|
| 117 |
+
title=tooltip,
|
| 118 |
+
color=OUTCOME_COLORS[outcome],
|
| 119 |
+
value=1.0 + max(0.0, avg_reward),
|
| 120 |
+
shape="dot",
|
| 121 |
+
)
|
| 122 |
+
|
| 123 |
+
for edge in snapshot.edges:
|
| 124 |
+
is_failed = (edge.source_module_id, edge.target_module_id) in failed_edges
|
| 125 |
+
net.add_edge(
|
| 126 |
+
source=edge.source_module_id,
|
| 127 |
+
to=edge.target_module_id,
|
| 128 |
+
title=edge.connection_summary or edge.import_line,
|
| 129 |
+
color="#ef4444" if is_failed else "#2563eb",
|
| 130 |
+
width=2.2 if is_failed else 1.4,
|
| 131 |
+
arrows="to",
|
| 132 |
+
)
|
| 133 |
+
|
| 134 |
+
summary = _summarize(run_id=run_id, annotations=annotations, rewards=all_rewards, judges=all_judges)
|
| 135 |
+
|
| 136 |
+
output = Path(output_path).resolve()
|
| 137 |
+
output.parent.mkdir(parents=True, exist_ok=True)
|
| 138 |
+
net.write_html(str(output), open_browser=False, notebook=False)
|
| 139 |
+
|
| 140 |
+
html = output.read_text(encoding="utf-8")
|
| 141 |
+
html = re.sub(r'<link[^>]*cdn\.jsdelivr\.net[^>]*>\s*', "", html, flags=re.IGNORECASE)
|
| 142 |
+
html = re.sub(r'<script[^>]*cdn\.jsdelivr\.net[^>]*>\s*</script>\s*', "", html, flags=re.IGNORECASE)
|
| 143 |
+
panel = (
|
| 144 |
+
"<aside style='position:fixed;right:0;top:0;width:340px;height:100%;"
|
| 145 |
+
"background:#0f172a;color:#e2e8f0;padding:18px;overflow:auto;z-index:1000;'>"
|
| 146 |
+
f"<h3 style='margin:0 0 12px 0;'>Training Run: {summary.run_id}</h3>"
|
| 147 |
+
f"<p>Episodes: {summary.episodes} | Steps: {summary.steps}</p>"
|
| 148 |
+
f"<p>Avg Reward: {summary.avg_reward:.2f}</p>"
|
| 149 |
+
f"<p>Judge Scores: {summary.avg_judge:.2f}</p>"
|
| 150 |
+
f"<p>DPO pairs built: {summary.dpo_pairs}</p>"
|
| 151 |
+
"<h4>Top 3 Failures</h4>"
|
| 152 |
+
f"<ul>{''.join(f'<li>{item}</li>' for item in summary.top_failures)}</ul>"
|
| 153 |
+
"<h4>Top 3 Successes</h4>"
|
| 154 |
+
f"<ul>{''.join(f'<li>{item}</li>' for item in summary.top_successes)}</ul>"
|
| 155 |
+
"</aside>"
|
| 156 |
+
)
|
| 157 |
+
|
| 158 |
+
html = html.replace("</body>", f"{panel}</body>")
|
| 159 |
+
output.write_text(html, encoding="utf-8")
|
| 160 |
+
return output
|
| 161 |
+
|
| 162 |
+
|
| 163 |
+
def _summarize(*, run_id: str, annotations: list[Any], rewards: list[float], judges: list[float]) -> TrainingSummary:
|
| 164 |
+
episodes = len({(item.task_id, item.module_id) for item in annotations})
|
| 165 |
+
steps = len(annotations)
|
| 166 |
+
avg_reward = (sum(rewards) / len(rewards)) if rewards else 0.0
|
| 167 |
+
avg_judge = (sum(judges) / len(judges)) if judges else 0.0
|
| 168 |
+
|
| 169 |
+
module_scores: dict[str, float] = defaultdict(float)
|
| 170 |
+
module_counts: dict[str, int] = defaultdict(int)
|
| 171 |
+
for row in annotations:
|
| 172 |
+
module_scores[row.module_id] += float(row.avg_reward)
|
| 173 |
+
module_counts[row.module_id] += 1
|
| 174 |
+
|
| 175 |
+
sorted_modules = sorted(
|
| 176 |
+
module_scores,
|
| 177 |
+
key=lambda module_id: module_scores[module_id] / max(1, module_counts[module_id]),
|
| 178 |
+
)
|
| 179 |
+
top_failures = sorted_modules[:3]
|
| 180 |
+
top_successes = list(reversed(sorted_modules[-3:])) if sorted_modules else []
|
| 181 |
+
|
| 182 |
+
return TrainingSummary(
|
| 183 |
+
run_id=run_id,
|
| 184 |
+
episodes=episodes,
|
| 185 |
+
steps=steps,
|
| 186 |
+
avg_reward=avg_reward,
|
| 187 |
+
avg_judge=avg_judge,
|
| 188 |
+
dpo_pairs=max(0, steps // 4),
|
| 189 |
+
top_failures=top_failures,
|
| 190 |
+
top_successes=top_successes,
|
| 191 |
+
)
|
inf.py
ADDED
|
@@ -0,0 +1,188 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Inference Script Example
|
| 3 |
+
===================================
|
| 4 |
+
MANDATORY
|
| 5 |
+
- Before submitting, ensure the following variables are defined in your environment configuration:
|
| 6 |
+
API_BASE_URL The API endpoint for the LLM.
|
| 7 |
+
MODEL_NAME The model identifier to use for inference.
|
| 8 |
+
HF_TOKEN Your Hugging Face / API key.
|
| 9 |
+
LOCAL_IMAGE_NAME The name of the local image to use for the environment if you are using from_docker_image()
|
| 10 |
+
method
|
| 11 |
+
|
| 12 |
+
- Defaults are set only for API_BASE_URL and MODEL_NAME
|
| 13 |
+
(and should reflect your active inference setup):
|
| 14 |
+
API_BASE_URL = os.getenv("API_BASE_URL", "<your-active-endpoint>")
|
| 15 |
+
MODEL_NAME = os.getenv("MODEL_NAME", "<your-active-model>")
|
| 16 |
+
|
| 17 |
+
- The inference script must be named `inference.py` and placed in the root directory of the project
|
| 18 |
+
- Participants must use OpenAI Client for all LLM calls using above variables
|
| 19 |
+
|
| 20 |
+
STDOUT FORMAT
|
| 21 |
+
- The script must emit exactly three line types to stdout, in this order:
|
| 22 |
+
|
| 23 |
+
[START] task=<task_name> env=<benchmark> model=<model_name>
|
| 24 |
+
[STEP] step=<n> action=<action_str> reward=<0.00> done=<true|false> error=<msg|null>
|
| 25 |
+
[END] success=<true|false> steps=<n> score=<score> rewards=<r1,r2,...,rn>
|
| 26 |
+
|
| 27 |
+
Rules:
|
| 28 |
+
- One [START] line at episode begin.
|
| 29 |
+
- One [STEP] line per step, immediately after env.step() returns.
|
| 30 |
+
- One [END] line after env.close(), always emitted (even on exception).
|
| 31 |
+
- reward and rewards are formatted to 2 decimal places.
|
| 32 |
+
- done and success are lowercase booleans: true or false.
|
| 33 |
+
- error is the raw last_action_error string, or null if none.
|
| 34 |
+
- All fields on a single line with no newlines within a line.
|
| 35 |
+
- Each tasks should return score in [0, 1]
|
| 36 |
+
|
| 37 |
+
Example:
|
| 38 |
+
[START] task=click-test env=miniwob model=Qwen3-VL-30B
|
| 39 |
+
[STEP] step=1 action=click('123') reward=0.00 done=false error=null
|
| 40 |
+
[STEP] step=2 action=fill('456','text') reward=0.00 done=false error=null
|
| 41 |
+
[STEP] step=3 action=click('789') reward=1.00 done=true error=null
|
| 42 |
+
[END] success=true steps=3 score=1.00 rewards=0.00,0.00,1.00
|
| 43 |
+
"""
|
| 44 |
+
|
| 45 |
+
import asyncio
|
| 46 |
+
import os
|
| 47 |
+
import textwrap
|
| 48 |
+
from typing import List, Optional
|
| 49 |
+
|
| 50 |
+
from openai import OpenAI
|
| 51 |
+
|
| 52 |
+
from my_env_v4 import MyEnvV4Action, MyEnvV4Env
|
| 53 |
+
IMAGE_NAME = os.getenv("IMAGE_NAME") # If you are using docker image
|
| 54 |
+
API_KEY = os.getenv("HF_TOKEN") or os.getenv("API_KEY")
|
| 55 |
+
|
| 56 |
+
API_BASE_URL = os.getenv("API_BASE_URL") or "https://router.huggingface.co/v1"
|
| 57 |
+
MODEL_NAME = os.getenv("MODEL_NAME") or "Qwen/Qwen2.5-72B-Instruct"
|
| 58 |
+
TASK_NAME = os.getenv("MY_ENV_V4_TASK", "echo")
|
| 59 |
+
BENCHMARK = os.getenv("MY_ENV_V4_BENCHMARK", "my_env_v4")
|
| 60 |
+
MAX_STEPS = 8
|
| 61 |
+
TEMPERATURE = 0.7
|
| 62 |
+
MAX_TOKENS = 150
|
| 63 |
+
SUCCESS_SCORE_THRESHOLD = 0.1 # normalized score in [0, 1]
|
| 64 |
+
|
| 65 |
+
# Max possible reward: each token contributes 0.1, across all steps
|
| 66 |
+
_MAX_REWARD_PER_STEP = MAX_TOKENS * 0.1
|
| 67 |
+
MAX_TOTAL_REWARD = MAX_STEPS * _MAX_REWARD_PER_STEP
|
| 68 |
+
|
| 69 |
+
SYSTEM_PROMPT = textwrap.dedent(
|
| 70 |
+
"""
|
| 71 |
+
You are interacting with a simple echo environment.
|
| 72 |
+
Each turn you must send a message. The environment will echo it back.
|
| 73 |
+
Reward is proportional to message length: reward = len(message) * 0.1
|
| 74 |
+
Your goal is to maximize total reward by sending meaningful, substantive messages.
|
| 75 |
+
Reply with exactly one message string — no quotes, no prefixes, just the message text.
|
| 76 |
+
"""
|
| 77 |
+
).strip()
|
| 78 |
+
|
| 79 |
+
|
| 80 |
+
def log_start(task: str, env: str, model: str) -> None:
|
| 81 |
+
print(f"[START] task={task} env={env} model={model}", flush=True)
|
| 82 |
+
|
| 83 |
+
|
| 84 |
+
def log_step(step: int, action: str, reward: float, done: bool, error: Optional[str]) -> None:
|
| 85 |
+
error_val = error if error else "null"
|
| 86 |
+
done_val = str(done).lower()
|
| 87 |
+
print(
|
| 88 |
+
f"[STEP] step={step} action={action} reward={reward:.2f} done={done_val} error={error_val}",
|
| 89 |
+
flush=True,
|
| 90 |
+
)
|
| 91 |
+
|
| 92 |
+
|
| 93 |
+
def log_end(success: bool, steps: int, score: float, rewards: List[float]) -> None:
|
| 94 |
+
rewards_str = ",".join(f"{r:.2f}" for r in rewards)
|
| 95 |
+
print(f"[END] success={str(success).lower()} steps={steps} score={score:.3f} rewards={rewards_str}", flush=True)
|
| 96 |
+
|
| 97 |
+
|
| 98 |
+
def build_user_prompt(step: int, last_echoed: str, last_reward: float, history: List[str]) -> str:
|
| 99 |
+
history_block = "\n".join(history[-4:]) if history else "None"
|
| 100 |
+
return textwrap.dedent(
|
| 101 |
+
f"""
|
| 102 |
+
Step: {step}
|
| 103 |
+
Last echoed message: {last_echoed!r}
|
| 104 |
+
Last reward: {last_reward:.2f}
|
| 105 |
+
Previous steps:
|
| 106 |
+
{history_block}
|
| 107 |
+
Send your next message.
|
| 108 |
+
"""
|
| 109 |
+
).strip()
|
| 110 |
+
|
| 111 |
+
|
| 112 |
+
def get_model_message(client: OpenAI, step: int, last_echoed: str, last_reward: float, history: List[str]) -> str:
|
| 113 |
+
user_prompt = build_user_prompt(step, last_echoed, last_reward, history)
|
| 114 |
+
try:
|
| 115 |
+
completion = client.chat.completions.create(
|
| 116 |
+
model=MODEL_NAME,
|
| 117 |
+
messages=[
|
| 118 |
+
{"role": "system", "content": SYSTEM_PROMPT},
|
| 119 |
+
{"role": "user", "content": user_prompt},
|
| 120 |
+
],
|
| 121 |
+
temperature=TEMPERATURE,
|
| 122 |
+
max_tokens=MAX_TOKENS,
|
| 123 |
+
stream=False,
|
| 124 |
+
)
|
| 125 |
+
text = (completion.choices[0].message.content or "").strip()
|
| 126 |
+
return text if text else "hello"
|
| 127 |
+
except Exception as exc:
|
| 128 |
+
print(f"[DEBUG] Model request failed: {exc}", flush=True)
|
| 129 |
+
return "hello"
|
| 130 |
+
|
| 131 |
+
|
| 132 |
+
async def main() -> None:
|
| 133 |
+
client = OpenAI(base_url=API_BASE_URL, api_key=API_KEY)
|
| 134 |
+
|
| 135 |
+
env = await MyEnvV4Env.from_docker_image(IMAGE_NAME)
|
| 136 |
+
|
| 137 |
+
history: List[str] = []
|
| 138 |
+
rewards: List[float] = []
|
| 139 |
+
steps_taken = 0
|
| 140 |
+
score = 0.0
|
| 141 |
+
success = False
|
| 142 |
+
|
| 143 |
+
log_start(task=TASK_NAME, env=BENCHMARK, model=MODEL_NAME)
|
| 144 |
+
|
| 145 |
+
try:
|
| 146 |
+
result = await env.reset() # OpenENV.reset()
|
| 147 |
+
last_echoed = result.observation.echoed_message
|
| 148 |
+
last_reward = 0.0
|
| 149 |
+
|
| 150 |
+
for step in range(1, MAX_STEPS + 1):
|
| 151 |
+
if result.done:
|
| 152 |
+
break
|
| 153 |
+
|
| 154 |
+
message = get_model_message(client, step, last_echoed, last_reward, history)
|
| 155 |
+
|
| 156 |
+
result = await env.step(MyEnvV4Action(message=message))
|
| 157 |
+
obs = result.observation
|
| 158 |
+
|
| 159 |
+
reward = result.reward or 0.0
|
| 160 |
+
done = result.done
|
| 161 |
+
error = None
|
| 162 |
+
|
| 163 |
+
rewards.append(reward)
|
| 164 |
+
steps_taken = step
|
| 165 |
+
last_echoed = obs.echoed_message
|
| 166 |
+
last_reward = reward
|
| 167 |
+
|
| 168 |
+
log_step(step=step, action=message, reward=reward, done=done, error=error)
|
| 169 |
+
|
| 170 |
+
history.append(f"Step {step}: {message!r} -> reward {reward:+.2f}")
|
| 171 |
+
|
| 172 |
+
if done:
|
| 173 |
+
break
|
| 174 |
+
|
| 175 |
+
score = sum(rewards) / MAX_TOTAL_REWARD if MAX_TOTAL_REWARD > 0 else 0.0
|
| 176 |
+
score = min(max(score, 0.0), 1.0) # clamp to [0, 1]
|
| 177 |
+
success = score >= SUCCESS_SCORE_THRESHOLD
|
| 178 |
+
|
| 179 |
+
finally:
|
| 180 |
+
try:
|
| 181 |
+
await env.close()
|
| 182 |
+
except Exception as e:
|
| 183 |
+
print(f"[DEBUG] env.close() error (container cleanup): {e}", flush=True)
|
| 184 |
+
log_end(success=success, steps=steps_taken, score=score, rewards=rewards)
|
| 185 |
+
|
| 186 |
+
|
| 187 |
+
if __name__ == "__main__":
|
| 188 |
+
asyncio.run(main())
|
scripts/clone_training_repos.sh
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/bin/sh
|
| 2 |
+
# Clone open-source Python corpora for NodeAudit / GraphReview training.
|
| 3 |
+
# Licensed MIT/Apache per upstream projects. Uses shallow clones to save disk.
|
| 4 |
+
# POSIX sh — safe to run as: sh scripts/clone_training_repos.sh
|
| 5 |
+
|
| 6 |
+
set -eu
|
| 7 |
+
|
| 8 |
+
ROOT="$(CDPATH='' cd "$(dirname "$0")/.." && pwd)"
|
| 9 |
+
CORPUS_DIR="${CORPUS_DIR:-$ROOT/training_corpus}"
|
| 10 |
+
mkdir -p "$CORPUS_DIR"
|
| 11 |
+
|
| 12 |
+
clone_if_missing() {
|
| 13 |
+
url="$1"
|
| 14 |
+
dest="$2"
|
| 15 |
+
if [ -d "$dest" ] && [ -e "$dest/.git" ]; then
|
| 16 |
+
echo "skip (exists): $dest"
|
| 17 |
+
return
|
| 18 |
+
fi
|
| 19 |
+
git clone --depth=1 "$url" "$dest"
|
| 20 |
+
}
|
| 21 |
+
|
| 22 |
+
echo "Corpus directory: $CORPUS_DIR"
|
| 23 |
+
|
| 24 |
+
# Tier 1 — core training
|
| 25 |
+
clone_if_missing https://github.com/pallets/flask "$CORPUS_DIR/flask"
|
| 26 |
+
clone_if_missing https://github.com/celery/celery "$CORPUS_DIR/celery"
|
| 27 |
+
clone_if_missing https://github.com/psf/requests "$CORPUS_DIR/requests"
|
| 28 |
+
clone_if_missing https://github.com/encode/httpx "$CORPUS_DIR/httpx"
|
| 29 |
+
clone_if_missing https://github.com/fastapi/fastapi "$CORPUS_DIR/fastapi"
|
| 30 |
+
clone_if_missing https://github.com/sqlalchemy/sqlalchemy "$CORPUS_DIR/sqlalchemy"
|
| 31 |
+
clone_if_missing https://github.com/pydantic/pydantic "$CORPUS_DIR/pydantic"
|
| 32 |
+
|
| 33 |
+
# Tier 2 — topology diversity (large clones)
|
| 34 |
+
clone_if_missing https://github.com/spotify/luigi "$CORPUS_DIR/luigi"
|
| 35 |
+
clone_if_missing https://github.com/scrapy/scrapy "$CORPUS_DIR/scrapy"
|
| 36 |
+
clone_if_missing https://github.com/paramiko/paramiko "$CORPUS_DIR/paramiko"
|
| 37 |
+
clone_if_missing https://github.com/django/django "$CORPUS_DIR/django"
|
| 38 |
+
clone_if_missing https://github.com/apache/airflow "$CORPUS_DIR/airflow"
|
| 39 |
+
|
| 40 |
+
# Tier 3 — synthetic bug injection / smaller templates
|
| 41 |
+
# Small Flask-Smorest API (Real Python course flavor; original realpython/flask-smorest-api URL is not public)
|
| 42 |
+
clone_if_missing https://github.com/tecladocode/rest-api-smorest-docker "$CORPUS_DIR/rest-api-smorest-docker"
|
| 43 |
+
clone_if_missing https://github.com/tiangolo/full-stack-fastapi-template "$CORPUS_DIR/full-stack-fastapi-template"
|
| 44 |
+
clone_if_missing https://github.com/miguelgrinberg/flasky "$CORPUS_DIR/flasky"
|
| 45 |
+
clone_if_missing https://github.com/testdrivenio/fastapi-tdd-docker "$CORPUS_DIR/fastapi-tdd-docker"
|
| 46 |
+
|
| 47 |
+
count="$(find "$CORPUS_DIR" -mindepth 1 -maxdepth 1 -type d | wc -l)"
|
| 48 |
+
echo "Top-level corpus entries: $count"
|
scripts/seed_training_corpus.sh
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/bin/sh
|
| 2 |
+
# Seed GraphReview SQLite DBs from training_corpus subpaths (application core only).
|
| 3 |
+
# Run after scripts/clone_training_repos.sh. Executes from code-review-env so `python -m db.seed` resolves.
|
| 4 |
+
# POSIX sh — safe to run as: sh scripts/seed_training_corpus.sh
|
| 5 |
+
|
| 6 |
+
set -eu
|
| 7 |
+
|
| 8 |
+
ROOT="$(CDPATH='' cd "$(dirname "$0")/.." && pwd)"
|
| 9 |
+
ENV_DIR="$ROOT/code-review-env"
|
| 10 |
+
CORPUS_DIR="${CORPUS_DIR:-$ROOT/training_corpus}"
|
| 11 |
+
OUT_DIR="${CORPUS_DB_DIR:-$ROOT/outputs/corpus_dbs}"
|
| 12 |
+
|
| 13 |
+
if [ ! -d "$ENV_DIR" ]; then
|
| 14 |
+
echo "error: expected code-review-env at $ENV_DIR" >&2
|
| 15 |
+
exit 1
|
| 16 |
+
fi
|
| 17 |
+
|
| 18 |
+
mkdir -p "$OUT_DIR"
|
| 19 |
+
cd "$ENV_DIR"
|
| 20 |
+
|
| 21 |
+
seed_one() {
|
| 22 |
+
db_basename="$1"
|
| 23 |
+
relative_path="$2"
|
| 24 |
+
target="$CORPUS_DIR/$relative_path"
|
| 25 |
+
db_path="$OUT_DIR/${db_basename}.db"
|
| 26 |
+
|
| 27 |
+
if [ ! -d "$target" ]; then
|
| 28 |
+
echo "[skip] missing directory: $target"
|
| 29 |
+
return 0
|
| 30 |
+
fi
|
| 31 |
+
|
| 32 |
+
echo "[seed] $target -> $db_path"
|
| 33 |
+
python -m db.seed "$target" --db-path "$db_path" --force
|
| 34 |
+
}
|
| 35 |
+
|
| 36 |
+
# Tier 1 — single package roots matching training corpus seed table
|
| 37 |
+
seed_one corpus_flask "flask/src/flask"
|
| 38 |
+
# Full celery package (app/, worker/, backends/ live under this tree)
|
| 39 |
+
seed_one corpus_celery "celery/celery"
|
| 40 |
+
seed_one corpus_requests "requests/src/requests"
|
| 41 |
+
seed_one corpus_httpx "httpx/httpx"
|
| 42 |
+
seed_one corpus_fastapi "fastapi/fastapi"
|
| 43 |
+
seed_one corpus_sqlalchemy "sqlalchemy/lib/sqlalchemy"
|
| 44 |
+
seed_one corpus_pydantic "pydantic/pydantic"
|
| 45 |
+
|
| 46 |
+
# Tier 2
|
| 47 |
+
seed_one corpus_luigi "luigi/luigi"
|
| 48 |
+
# Focus: middleware stack modules (omit tests/spiders noise)
|
| 49 |
+
seed_one corpus_scrapy_core "scrapy/scrapy/core"
|
| 50 |
+
seed_one corpus_scrapy_pipelines "scrapy/scrapy/pipelines"
|
| 51 |
+
seed_one corpus_paramiko "paramiko/paramiko"
|
| 52 |
+
seed_one corpus_airflow "airflow/airflow"
|
| 53 |
+
|
| 54 |
+
# Django: seed focused subtrees (separate DBs — no cross-edges between DBs)
|
| 55 |
+
seed_one corpus_django_db "django/django/db"
|
| 56 |
+
seed_one corpus_django_http "django/django/http"
|
| 57 |
+
seed_one corpus_django_auth "django/django/contrib/auth"
|
| 58 |
+
|
| 59 |
+
# Tier 3 — small templates (paths vary; adjust if upstream layout changes)
|
| 60 |
+
# App root: models/, resources/, app.py (Flask-Smorest sample)
|
| 61 |
+
seed_one corpus_rest_api_smorest_docker "rest-api-smorest-docker"
|
| 62 |
+
seed_one corpus_fullstack_fastapi_template "full-stack-fastapi-template/backend/app"
|
| 63 |
+
seed_one corpus_flasky "flasky/app"
|
| 64 |
+
# Layout: project/{app,db,migrations,tests}
|
| 65 |
+
if [ -d "$CORPUS_DIR/fastapi-tdd-docker/project" ]; then
|
| 66 |
+
seed_one corpus_fastapi_tdd "fastapi-tdd-docker/project"
|
| 67 |
+
else
|
| 68 |
+
echo "[skip] fastapi-tdd-docker/project — clone testdrivenio/fastapi-tdd-docker first"
|
| 69 |
+
fi
|
| 70 |
+
|
| 71 |
+
echo "Done. Databases under: $OUT_DIR"
|
scripts/verify_all.py
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
import pathlib
|
| 4 |
+
import runpy
|
| 5 |
+
|
| 6 |
+
|
| 7 |
+
def main() -> None:
|
| 8 |
+
root = pathlib.Path(__file__).resolve().parents[1]
|
| 9 |
+
target = root / "code-review-env" / "scripts" / "verify_all.py"
|
| 10 |
+
runpy.run_path(str(target), run_name="__main__")
|
| 11 |
+
|
| 12 |
+
|
| 13 |
+
if __name__ == "__main__":
|
| 14 |
+
main()
|