swegen 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- swegen/__init__.py +14 -0
- swegen/analyze/__init__.py +24 -0
- swegen/analyze/classifier.py +637 -0
- swegen/analyze/classify_prompt.txt +241 -0
- swegen/analyze/models.py +253 -0
- swegen/analyze/run.py +656 -0
- swegen/analyze/verdict_prompt.txt +126 -0
- swegen/cli.py +411 -0
- swegen/config.py +142 -0
- swegen/create/__init__.py +22 -0
- swegen/create/claude_code_runner.py +988 -0
- swegen/create/claude_code_utils.py +95 -0
- swegen/create/create.py +706 -0
- swegen/create/diff_utils.py +142 -0
- swegen/create/orchestrator.py +368 -0
- swegen/create/pr_fetcher.py +187 -0
- swegen/create/repo_cache.py +175 -0
- swegen/create/task_instruction.py +363 -0
- swegen/create/task_reference.py +130 -0
- swegen/create/task_skeleton.py +266 -0
- swegen/create/utils.py +350 -0
- swegen/farm/__init__.py +13 -0
- swegen/farm/farm_hand.py +342 -0
- swegen/farm/fetcher.py +341 -0
- swegen/farm/state.py +231 -0
- swegen/farm/stream_farm.py +430 -0
- swegen/tools/__init__.py +16 -0
- swegen/tools/harbor_runner.py +191 -0
- swegen/tools/validate.py +523 -0
- swegen/tools/validate_utils.py +142 -0
- swegen-0.1.0.dist-info/METADATA +292 -0
- swegen-0.1.0.dist-info/RECORD +35 -0
- swegen-0.1.0.dist-info/WHEEL +4 -0
- swegen-0.1.0.dist-info/entry_points.txt +3 -0
- swegen-0.1.0.dist-info/licenses/LICENSE +201 -0
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
from harbor.models.task.config import (
|
|
7
|
+
AgentConfig,
|
|
8
|
+
EnvironmentConfig,
|
|
9
|
+
TaskConfig,
|
|
10
|
+
VerifierConfig,
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
from .utils import strip_tests_prefix
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@dataclass
|
|
17
|
+
class SkeletonParams:
|
|
18
|
+
"""Parameters for skeleton generation (all deterministic from git)."""
|
|
19
|
+
|
|
20
|
+
repo_url: str
|
|
21
|
+
head_sha: str
|
|
22
|
+
base_sha: str
|
|
23
|
+
pr_number: int
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def generate_dockerfile(params: SkeletonParams) -> str:
|
|
27
|
+
"""
|
|
28
|
+
Generate a minimal, language-agnostic Dockerfile skeleton.
|
|
29
|
+
|
|
30
|
+
The skeleton contains:
|
|
31
|
+
- Deterministic parts filled in (git clone, SHAs, bug.patch application)
|
|
32
|
+
- TODO comments for Claude Code to fill in (runtime, deps, build)
|
|
33
|
+
|
|
34
|
+
Claude Code will analyze the repo and fill in:
|
|
35
|
+
- Language runtime installation
|
|
36
|
+
- Package manager setup
|
|
37
|
+
- Dependency installation
|
|
38
|
+
- Build steps (if needed)
|
|
39
|
+
- Post-patch rebuild (if needed)
|
|
40
|
+
|
|
41
|
+
Git clone strategy:
|
|
42
|
+
- Simple + robust: clone, then fetch the exact commit SHA.
|
|
43
|
+
- NOTE: `head_sha` currently comes from the PR's HEAD branch tip (GitHub API).
|
|
44
|
+
- If the PR was squash-merged/rebased, that commit may not be on any normal branch.
|
|
45
|
+
- In that case, fetching `refs/pull/<n>/head` is a robust fallback without fetching ALL PR refs.
|
|
46
|
+
"""
|
|
47
|
+
return f"""FROM ubuntu:24.04
|
|
48
|
+
|
|
49
|
+
# Base system packages (common to all languages)
|
|
50
|
+
RUN apt-get update && apt-get install -y \\
|
|
51
|
+
git \\
|
|
52
|
+
curl \\
|
|
53
|
+
ca-certificates \\
|
|
54
|
+
patch \\
|
|
55
|
+
build-essential \\
|
|
56
|
+
&& rm -rf /var/lib/apt/lists/*
|
|
57
|
+
|
|
58
|
+
# TODO: Install language runtime
|
|
59
|
+
# Analyze the repo to determine what's needed. Examples:
|
|
60
|
+
# Python: apt-get install python3 python3-pip python3-venv python3-dev
|
|
61
|
+
# Node.js: curl -fsSL https://deb.nodesource.com/setup_20.x | bash - && apt-get install -y nodejs
|
|
62
|
+
# Go: Download from golang.org/dl or use apt
|
|
63
|
+
# Rust: curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y
|
|
64
|
+
# Ruby: apt-get install ruby ruby-dev
|
|
65
|
+
# Java: apt-get install openjdk-17-jdk
|
|
66
|
+
# Check .nvmrc, .python-version, .ruby-version, go.mod, rust-toolchain.toml, etc.
|
|
67
|
+
|
|
68
|
+
# TODO: Install additional system packages if needed
|
|
69
|
+
# Check CI config (.github/workflows/*.yml) for hints about required packages
|
|
70
|
+
# Examples: python3-dev, libssl-dev, pkg-config, cmake, etc.
|
|
71
|
+
|
|
72
|
+
# TODO: Set up package manager if needed
|
|
73
|
+
# For Python: PREFER uv (much faster than pip)
|
|
74
|
+
# curl -LsSf https://astral.sh/uv/install.sh | sh && mv /root/.local/bin/uv /usr/local/bin/uv
|
|
75
|
+
# For Node.js: corepack enable (for yarn/pnpm) or npm is built-in
|
|
76
|
+
# For Ruby: gem install bundler
|
|
77
|
+
|
|
78
|
+
WORKDIR /app
|
|
79
|
+
|
|
80
|
+
# Clone repo at HEAD commit (with fix applied)
|
|
81
|
+
RUN git clone {params.repo_url} src && \\
|
|
82
|
+
cd src && \\
|
|
83
|
+
(git fetch --depth 1 origin {params.head_sha} || git fetch --depth 1 origin "+refs/pull/{params.pr_number}/head:refs/remotes/origin/pr/{params.pr_number}") && \\
|
|
84
|
+
git checkout --detach FETCH_HEAD && \\
|
|
85
|
+
git submodule update --init --recursive
|
|
86
|
+
|
|
87
|
+
WORKDIR /app/src
|
|
88
|
+
|
|
89
|
+
# TODO: Set environment variables if needed
|
|
90
|
+
# Check CI config and README for required env vars
|
|
91
|
+
# Examples: CI=true, NODE_ENV=test, CARGO_TERM_COLOR=never
|
|
92
|
+
|
|
93
|
+
# TODO: Install dependencies
|
|
94
|
+
# For Python: PREFER uv (much faster). Create venv and install:
|
|
95
|
+
# uv venv /opt/venv
|
|
96
|
+
# uv pip install --python /opt/venv/bin/python -e ".[dev,test]"
|
|
97
|
+
# # Or: uv pip install --python /opt/venv/bin/python -r requirements.txt
|
|
98
|
+
# # Then add to PATH: ENV PATH="/opt/venv/bin:${{PATH}}"
|
|
99
|
+
# For Node.js: npm ci, yarn install --frozen-lockfile, pnpm install --frozen-lockfile
|
|
100
|
+
# For Go: go mod download
|
|
101
|
+
# For Rust: cargo fetch
|
|
102
|
+
# For Ruby: bundle install
|
|
103
|
+
# For Java: mvn dependency:resolve or gradle dependencies
|
|
104
|
+
|
|
105
|
+
# TODO: Build if needed (check if it's a compiled language or has build step)
|
|
106
|
+
# Examples:
|
|
107
|
+
# TypeScript: npm run build, tsc
|
|
108
|
+
# Rust: cargo build
|
|
109
|
+
# Go: go build ./...
|
|
110
|
+
# Java: mvn compile, gradle build
|
|
111
|
+
|
|
112
|
+
# If install/build steps touched tracked files, reset them so bug.patch applies cleanly,
|
|
113
|
+
RUN git reset --hard
|
|
114
|
+
|
|
115
|
+
# Apply bug.patch to revert to buggy state (BASE)
|
|
116
|
+
COPY bug.patch /tmp/bug.patch
|
|
117
|
+
RUN patch -p1 < /tmp/bug.patch && rm /tmp/bug.patch
|
|
118
|
+
|
|
119
|
+
# TODO: Rebuild after applying bug.patch if needed
|
|
120
|
+
# For compiled languages (TypeScript, Rust, Go, Java), you MUST rebuild after patching
|
|
121
|
+
|
|
122
|
+
RUN rm -rf /app/src/.git
|
|
123
|
+
|
|
124
|
+
WORKDIR /app/src
|
|
125
|
+
"""
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def generate_test_sh(
|
|
129
|
+
test_files: list[str],
|
|
130
|
+
) -> str:
|
|
131
|
+
"""
|
|
132
|
+
Generate a minimal, language-agnostic test.sh skeleton.
|
|
133
|
+
|
|
134
|
+
The skeleton contains:
|
|
135
|
+
- Test file copy commands (deterministic)
|
|
136
|
+
- TODO for Claude Code to fill in the actual test command
|
|
137
|
+
|
|
138
|
+
Claude Code will analyze the repo and fill in:
|
|
139
|
+
- Test framework detection
|
|
140
|
+
- Correct test command with specific file paths
|
|
141
|
+
"""
|
|
142
|
+
# Build copy commands for test files
|
|
143
|
+
if test_files:
|
|
144
|
+
copy_lines = []
|
|
145
|
+
for tf in test_files:
|
|
146
|
+
# Handle common test directory prefixes
|
|
147
|
+
source_path = strip_tests_prefix(tf)
|
|
148
|
+
target_dir = str(Path(tf).parent)
|
|
149
|
+
copy_lines.append(f'mkdir -p "{target_dir}"')
|
|
150
|
+
copy_lines.append(f'cp "/tests/{source_path}" "{tf}"')
|
|
151
|
+
copy_commands = "\n".join(copy_lines)
|
|
152
|
+
|
|
153
|
+
# Build example test file list for comments
|
|
154
|
+
test_files_example = " ".join([f'"{tf}"' for tf in test_files[:5]])
|
|
155
|
+
if len(test_files) > 5:
|
|
156
|
+
test_files_example += f" # ... and {len(test_files) - 5} more"
|
|
157
|
+
else:
|
|
158
|
+
copy_commands = "# No test files to copy"
|
|
159
|
+
test_files_example = ""
|
|
160
|
+
|
|
161
|
+
return f"""#!/bin/bash
|
|
162
|
+
|
|
163
|
+
cd /app/src
|
|
164
|
+
|
|
165
|
+
# TODO: Set environment variables if needed for tests
|
|
166
|
+
# Examples: CI=true, NODE_ENV=test, RUST_BACKTRACE=1
|
|
167
|
+
|
|
168
|
+
# Copy HEAD test files from /tests (overwrites BASE state)
|
|
169
|
+
{copy_commands}
|
|
170
|
+
|
|
171
|
+
# CRITICAL: Run ONLY the specific test files from the PR, NOT the entire test suite!
|
|
172
|
+
# The test files to run are: {test_files_example if test_files_example else "(see list above)"}
|
|
173
|
+
#
|
|
174
|
+
# TODO: Fill in the actual test command to run ONLY these specific files
|
|
175
|
+
#
|
|
176
|
+
# DO NOT run the entire test suite - it's too slow and may have unrelated failures!
|
|
177
|
+
#
|
|
178
|
+
# Examples for different languages/frameworks:
|
|
179
|
+
#
|
|
180
|
+
# Python (pytest with uv):
|
|
181
|
+
# # If using uv venv at /opt/venv:
|
|
182
|
+
# source /opt/venv/bin/activate
|
|
183
|
+
# uv pip install -e . --no-deps 2>/dev/null || true # Reinstall to pick up changes
|
|
184
|
+
# pytest -xvs path/to/test_file.py
|
|
185
|
+
# # Or without venv activation:
|
|
186
|
+
# /opt/venv/bin/pytest -xvs path/to/test_file.py
|
|
187
|
+
#
|
|
188
|
+
# JavaScript/TypeScript (IMPORTANT: disable coverage thresholds when running subset!):
|
|
189
|
+
# npx jest path/to/test.js path/to/test2.js --coverage=false
|
|
190
|
+
# npx vitest run path/to/test.ts --coverage.enabled=false
|
|
191
|
+
# npx mocha path/to/test.js path/to/test2.js
|
|
192
|
+
# npx borp path/to/test.js --no-check-coverage # Used by fastify, pino, etc.
|
|
193
|
+
# npx tap path/to/test.js --no-check-coverage # Node TAP framework
|
|
194
|
+
# npx ava path/to/test.js # AVA framework
|
|
195
|
+
#
|
|
196
|
+
# CRITICAL for JS/TS: DO NOT use "npm test" or "npm run test" without args!
|
|
197
|
+
# These run the ENTIRE suite. Pass specific files via the test runner directly.
|
|
198
|
+
# If you must use npm: npm run test -- path/to/test.js (note the -- separator)
|
|
199
|
+
#
|
|
200
|
+
# Go:
|
|
201
|
+
# go test -v ./path/to/package/...
|
|
202
|
+
# go test -v -run TestSpecificName ./...
|
|
203
|
+
#
|
|
204
|
+
# Rust:
|
|
205
|
+
# cargo test --test test_name -- --nocapture
|
|
206
|
+
# cargo test specific_test_name -- --nocapture
|
|
207
|
+
#
|
|
208
|
+
# Ruby (RSpec/Minitest):
|
|
209
|
+
# bundle exec rspec path/to/spec.rb
|
|
210
|
+
# bundle exec ruby -Itest path/to/test.rb
|
|
211
|
+
#
|
|
212
|
+
# Java (JUnit/Maven/Gradle):
|
|
213
|
+
# mvn test -Dtest=TestClassName
|
|
214
|
+
# gradle test --tests TestClassName
|
|
215
|
+
|
|
216
|
+
# TODO: Replace this placeholder with actual test command running ONLY the specific test files above
|
|
217
|
+
echo "ERROR: Test command not filled in! Must run specific test files, not entire suite." >&2
|
|
218
|
+
false
|
|
219
|
+
test_status=$?
|
|
220
|
+
|
|
221
|
+
if [ $test_status -eq 0 ]; then
|
|
222
|
+
echo 1 > /logs/verifier/reward.txt
|
|
223
|
+
else
|
|
224
|
+
echo 0 > /logs/verifier/reward.txt
|
|
225
|
+
fi
|
|
226
|
+
exit "$test_status"
|
|
227
|
+
"""
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
def generate_solve_sh() -> str:
|
|
231
|
+
"""Generate solution/solve.sh script (same for all tasks)."""
|
|
232
|
+
return """#!/bin/bash
|
|
233
|
+
|
|
234
|
+
set -euo pipefail
|
|
235
|
+
cd /app/src
|
|
236
|
+
|
|
237
|
+
patch -p1 < /solution/fix.patch
|
|
238
|
+
"""
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
def generate_instruction_md(instruction_data: dict) -> str:
|
|
242
|
+
"""Generate instruction.md file for Harbor format."""
|
|
243
|
+
return instruction_data["instruction"]
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
def generate_task_toml(instruction_data: dict) -> str:
|
|
247
|
+
"""Generate task.toml config file for Harbor format.
|
|
248
|
+
|
|
249
|
+
Uses Harbor's TaskConfig for proper serialization and validation.
|
|
250
|
+
"""
|
|
251
|
+
config = TaskConfig(
|
|
252
|
+
metadata={
|
|
253
|
+
"difficulty": instruction_data.get("difficulty", "medium"),
|
|
254
|
+
"category": instruction_data.get("category", "bugfix"),
|
|
255
|
+
"tags": instruction_data.get("tags", []),
|
|
256
|
+
},
|
|
257
|
+
verifier=VerifierConfig(timeout_sec=600.0),
|
|
258
|
+
agent=AgentConfig(timeout_sec=600.0),
|
|
259
|
+
environment=EnvironmentConfig(
|
|
260
|
+
build_timeout_sec=600.0,
|
|
261
|
+
cpus=1,
|
|
262
|
+
memory_mb=2048,
|
|
263
|
+
storage_mb=10240,
|
|
264
|
+
),
|
|
265
|
+
)
|
|
266
|
+
return config.model_dump_toml()
|
swegen/create/utils.py
ADDED
|
@@ -0,0 +1,350 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
from pydantic import BaseModel, Field
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class CombinedPRTaskEvaluation(BaseModel):
|
|
9
|
+
"""Combined evaluation and task generation for a PR.
|
|
10
|
+
|
|
11
|
+
First evaluates if PR is substantial, then generates task details if it is.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
is_substantial: bool = Field(
|
|
15
|
+
..., description="Whether the PR is substantial enough to generate a task"
|
|
16
|
+
)
|
|
17
|
+
reason: str = Field(..., description="Brief explanation of why the PR is or isn't substantial")
|
|
18
|
+
instruction: str | None = Field(
|
|
19
|
+
None,
|
|
20
|
+
description="Concise bug report describing problem, reproduction, expected behavior. No bullet lists or verbose sections.",
|
|
21
|
+
)
|
|
22
|
+
difficulty: str = Field("medium", description="Task difficulty: easy, medium, or hard")
|
|
23
|
+
category: str = Field("bugfix", description="Task category, typically 'bugfix' or 'feature'")
|
|
24
|
+
tags: list[str] = Field(
|
|
25
|
+
default_factory=list,
|
|
26
|
+
description="Exactly 3 tags: [language, tier, framework/category]. Example: ['python', 'backend', 'fastapi']",
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def strip_tests_prefix(path: str) -> str:
|
|
31
|
+
"""Strip leading test directory prefix if present.
|
|
32
|
+
|
|
33
|
+
Handles common patterns across languages:
|
|
34
|
+
- tests/, test/, __tests__/ (Python, JS/TS)
|
|
35
|
+
- spec/ (Ruby)
|
|
36
|
+
- src/test/ (Java/Kotlin)
|
|
37
|
+
|
|
38
|
+
Args:
|
|
39
|
+
path: File path that may start with a test directory prefix
|
|
40
|
+
|
|
41
|
+
Returns:
|
|
42
|
+
Path with test directory prefix removed if present
|
|
43
|
+
"""
|
|
44
|
+
p = Path(path)
|
|
45
|
+
parts = p.parts
|
|
46
|
+
|
|
47
|
+
if not parts:
|
|
48
|
+
return path
|
|
49
|
+
|
|
50
|
+
first = parts[0].lower()
|
|
51
|
+
|
|
52
|
+
# Python, JS/TS, Ruby
|
|
53
|
+
if first in ("tests", "test", "__tests__", "spec"):
|
|
54
|
+
return str(Path(*parts[1:]))
|
|
55
|
+
|
|
56
|
+
# Java/Kotlin: src/test/java/... or src/test/kotlin/...
|
|
57
|
+
if len(parts) >= 2 and parts[0].lower() == "src" and parts[1].lower() == "test":
|
|
58
|
+
return str(Path(*parts[2:]))
|
|
59
|
+
|
|
60
|
+
return path
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def is_test_file(filename: str) -> bool:
|
|
64
|
+
"""Check if a filename represents a test file or test-related resource.
|
|
65
|
+
|
|
66
|
+
Supports all languages: Python, JS/TS, Go, Rust, Ruby, Java, C/C++, PHP, C#.
|
|
67
|
+
|
|
68
|
+
Args:
|
|
69
|
+
filename: File path (repo-relative)
|
|
70
|
+
|
|
71
|
+
Returns:
|
|
72
|
+
True if the file is a test file or test resource (fixtures, data, etc.)
|
|
73
|
+
"""
|
|
74
|
+
if not filename:
|
|
75
|
+
return False
|
|
76
|
+
|
|
77
|
+
name_lower = filename.lower()
|
|
78
|
+
base_name = filename.split("/")[-1].lower()
|
|
79
|
+
|
|
80
|
+
# Check if file is under a test directory (common across languages)
|
|
81
|
+
in_test_dir = (
|
|
82
|
+
# Python/generic
|
|
83
|
+
name_lower.startswith("tests/")
|
|
84
|
+
or "/tests/" in name_lower
|
|
85
|
+
or name_lower.startswith("test/")
|
|
86
|
+
or "/test/" in name_lower
|
|
87
|
+
# JS/TS
|
|
88
|
+
or name_lower.startswith("__tests__/")
|
|
89
|
+
or "/__tests__/" in name_lower
|
|
90
|
+
# Ruby
|
|
91
|
+
or name_lower.startswith("spec/")
|
|
92
|
+
or "/spec/" in name_lower
|
|
93
|
+
# Java/Kotlin (Maven/Gradle convention)
|
|
94
|
+
or "/src/test/" in name_lower
|
|
95
|
+
or name_lower.startswith("src/test/")
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
# Python patterns
|
|
99
|
+
is_python_test = (
|
|
100
|
+
base_name.startswith("test_") and name_lower.endswith(".py")
|
|
101
|
+
) or base_name.endswith("_test.py")
|
|
102
|
+
|
|
103
|
+
# JavaScript/TypeScript patterns
|
|
104
|
+
is_js_ts_test = (
|
|
105
|
+
base_name.endswith(".test.js")
|
|
106
|
+
or base_name.endswith(".test.ts")
|
|
107
|
+
or base_name.endswith(".test.jsx")
|
|
108
|
+
or base_name.endswith(".test.tsx")
|
|
109
|
+
or base_name.endswith(".test.mjs")
|
|
110
|
+
or base_name.endswith(".test.cjs")
|
|
111
|
+
or base_name.endswith(".spec.js")
|
|
112
|
+
or base_name.endswith(".spec.ts")
|
|
113
|
+
or base_name.endswith(".spec.jsx")
|
|
114
|
+
or base_name.endswith(".spec.tsx")
|
|
115
|
+
or base_name.endswith(".spec.mjs")
|
|
116
|
+
or base_name.endswith(".spec.cjs")
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
# Go patterns
|
|
120
|
+
is_go_test = base_name.endswith("_test.go")
|
|
121
|
+
|
|
122
|
+
# Rust patterns
|
|
123
|
+
is_rust_test = base_name.endswith("_test.rs") or base_name == "tests.rs"
|
|
124
|
+
|
|
125
|
+
# Ruby patterns
|
|
126
|
+
is_ruby_test = (
|
|
127
|
+
base_name.endswith("_spec.rb")
|
|
128
|
+
or base_name.endswith("_test.rb")
|
|
129
|
+
or base_name.startswith("test_")
|
|
130
|
+
and name_lower.endswith(".rb")
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
# Java/Kotlin patterns
|
|
134
|
+
is_java_test = (
|
|
135
|
+
base_name.endswith("test.java")
|
|
136
|
+
or base_name.endswith("tests.java")
|
|
137
|
+
or base_name.endswith("test.kt")
|
|
138
|
+
or base_name.endswith("tests.kt")
|
|
139
|
+
or base_name.startswith("test")
|
|
140
|
+
and (name_lower.endswith(".java") or name_lower.endswith(".kt"))
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
# C/C++ patterns
|
|
144
|
+
is_cpp_test = (
|
|
145
|
+
base_name.endswith("_test.cpp")
|
|
146
|
+
or base_name.endswith("_test.cc")
|
|
147
|
+
or base_name.endswith("_test.c")
|
|
148
|
+
or base_name.startswith("test_")
|
|
149
|
+
and name_lower.endswith((".cpp", ".cc", ".c"))
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
# PHP patterns
|
|
153
|
+
is_php_test = (
|
|
154
|
+
base_name.endswith("test.php")
|
|
155
|
+
or base_name.startswith("test")
|
|
156
|
+
and name_lower.endswith(".php")
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
# C# patterns
|
|
160
|
+
is_csharp_test = base_name.endswith("tests.cs") or base_name.endswith("test.cs")
|
|
161
|
+
|
|
162
|
+
return (
|
|
163
|
+
in_test_dir
|
|
164
|
+
or is_python_test
|
|
165
|
+
or is_js_ts_test
|
|
166
|
+
or is_go_test
|
|
167
|
+
or is_rust_test
|
|
168
|
+
or is_ruby_test
|
|
169
|
+
or is_java_test
|
|
170
|
+
or is_cpp_test
|
|
171
|
+
or is_php_test
|
|
172
|
+
or is_csharp_test
|
|
173
|
+
)
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def identify_test_files(files: list[dict]) -> list[str]:
|
|
177
|
+
"""Identify test files from a list of changed files.
|
|
178
|
+
|
|
179
|
+
Supports all languages: Python, JS/TS, Go, Rust, Ruby, Java, C/C++, PHP, C#.
|
|
180
|
+
|
|
181
|
+
Args:
|
|
182
|
+
files: List of file dicts with 'filename' key (from GitHub API)
|
|
183
|
+
|
|
184
|
+
Returns:
|
|
185
|
+
List of test file paths (repo-relative)
|
|
186
|
+
"""
|
|
187
|
+
test_files = []
|
|
188
|
+
|
|
189
|
+
for f in files:
|
|
190
|
+
filename = f.get("filename", "")
|
|
191
|
+
if is_test_file(filename):
|
|
192
|
+
test_files.append(filename)
|
|
193
|
+
|
|
194
|
+
return test_files
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
def _is_relevant_source(path: str) -> bool:
|
|
198
|
+
"""Check if a file path is relevant for the fix (not tests, CI, or build artifacts).
|
|
199
|
+
|
|
200
|
+
NOTE: We include docs, examples, and other non-test files to keep fix.patch
|
|
201
|
+
consistent with bug.patch. This prevents issues where bug.patch reverts docs
|
|
202
|
+
but fix.patch doesn't re-apply them, causing inconsistencies.
|
|
203
|
+
|
|
204
|
+
Supports all languages: Python, JS/TS, Go, Rust, Ruby, Java, C/C++, PHP, C#.
|
|
205
|
+
|
|
206
|
+
Args:
|
|
207
|
+
path: File path to check
|
|
208
|
+
|
|
209
|
+
Returns:
|
|
210
|
+
True if the file should be included in fix.patch
|
|
211
|
+
"""
|
|
212
|
+
pl = path.lower()
|
|
213
|
+
base = path.split("/")[-1].lower()
|
|
214
|
+
|
|
215
|
+
# === Common exclusions (all languages) ===
|
|
216
|
+
|
|
217
|
+
# Exclude test directories
|
|
218
|
+
if pl.startswith("tests/") or "/tests/" in pl:
|
|
219
|
+
return False
|
|
220
|
+
if pl.startswith("test/") or "/test/" in pl:
|
|
221
|
+
return False
|
|
222
|
+
if pl.startswith("__tests__/") or "/__tests__/" in pl:
|
|
223
|
+
return False
|
|
224
|
+
if pl.startswith("spec/") or "/spec/" in pl: # Ruby
|
|
225
|
+
return False
|
|
226
|
+
if "/src/test/" in pl or pl.startswith("src/test/"): # Java/Kotlin
|
|
227
|
+
return False
|
|
228
|
+
|
|
229
|
+
# Exclude CI and meta (these shouldn't be in fix.patch)
|
|
230
|
+
if pl.startswith(".github/") or "/.github/" in pl:
|
|
231
|
+
return False
|
|
232
|
+
if pl.startswith(".gitlab/") or "/.gitlab/" in pl:
|
|
233
|
+
return False
|
|
234
|
+
if pl.startswith(".circleci/") or "/.circleci/" in pl:
|
|
235
|
+
return False
|
|
236
|
+
|
|
237
|
+
# Exclude build outputs and dependency directories (should never be in a PR)
|
|
238
|
+
build_dirs = [
|
|
239
|
+
"node_modules/",
|
|
240
|
+
"dist/",
|
|
241
|
+
"build/",
|
|
242
|
+
".next/",
|
|
243
|
+
"__pycache__/",
|
|
244
|
+
".tox/",
|
|
245
|
+
".pytest_cache/",
|
|
246
|
+
"*.egg-info/",
|
|
247
|
+
"target/",
|
|
248
|
+
"vendor/",
|
|
249
|
+
"bin/",
|
|
250
|
+
"obj/",
|
|
251
|
+
"out/",
|
|
252
|
+
]
|
|
253
|
+
for bd in build_dirs:
|
|
254
|
+
if bd in pl or pl.startswith(bd.rstrip("/")):
|
|
255
|
+
return False
|
|
256
|
+
|
|
257
|
+
# Exclude test files by naming convention (comprehensive, language-agnostic)
|
|
258
|
+
|
|
259
|
+
# Python
|
|
260
|
+
if base.startswith("test_") and base.endswith(".py"):
|
|
261
|
+
return False
|
|
262
|
+
if base.endswith("_test.py"):
|
|
263
|
+
return False
|
|
264
|
+
|
|
265
|
+
# JavaScript/TypeScript
|
|
266
|
+
if base.endswith((".test.js", ".test.ts", ".test.jsx", ".test.tsx", ".test.mjs", ".test.cjs")):
|
|
267
|
+
return False
|
|
268
|
+
if base.endswith((".spec.js", ".spec.ts", ".spec.jsx", ".spec.tsx", ".spec.mjs", ".spec.cjs")):
|
|
269
|
+
return False
|
|
270
|
+
|
|
271
|
+
# Go
|
|
272
|
+
if base.endswith("_test.go"):
|
|
273
|
+
return False
|
|
274
|
+
|
|
275
|
+
# Rust
|
|
276
|
+
if base.endswith("_test.rs") or base == "tests.rs":
|
|
277
|
+
return False
|
|
278
|
+
|
|
279
|
+
# Ruby
|
|
280
|
+
if base.endswith("_spec.rb") or base.endswith("_test.rb"):
|
|
281
|
+
return False
|
|
282
|
+
if base.startswith("test_") and base.endswith(".rb"):
|
|
283
|
+
return False
|
|
284
|
+
|
|
285
|
+
# Java/Kotlin
|
|
286
|
+
if base.endswith(("test.java", "tests.java", "test.kt", "tests.kt")):
|
|
287
|
+
return False
|
|
288
|
+
|
|
289
|
+
# C/C++
|
|
290
|
+
if base.endswith(("_test.cpp", "_test.cc", "_test.c")):
|
|
291
|
+
return False
|
|
292
|
+
if base.startswith("test_") and base.endswith((".cpp", ".cc", ".c")):
|
|
293
|
+
return False
|
|
294
|
+
|
|
295
|
+
# PHP
|
|
296
|
+
if base.endswith("test.php"):
|
|
297
|
+
return False
|
|
298
|
+
|
|
299
|
+
# C#
|
|
300
|
+
if base.endswith(("tests.cs", "test.cs")):
|
|
301
|
+
return False
|
|
302
|
+
|
|
303
|
+
# Include everything else (source code, docs, examples, type definitions, etc.)
|
|
304
|
+
# This ensures fix.patch is comprehensive and consistent with bug.patch
|
|
305
|
+
return True
|
|
306
|
+
|
|
307
|
+
|
|
308
|
+
def check_multi_file_requirement(
|
|
309
|
+
files: list[dict], min_files: int = 3, max_files: int = 10
|
|
310
|
+
) -> tuple[bool, str, int]:
|
|
311
|
+
"""Check if PR modifies sufficient source files for a good task.
|
|
312
|
+
|
|
313
|
+
Harbor tasks should require changes to 3+ source files (tests don't count).
|
|
314
|
+
Single-file and two-file changes are too easy - agents can pattern-match.
|
|
315
|
+
Large refactors (10+ files) are too complex and often not single bug fixes.
|
|
316
|
+
|
|
317
|
+
Args:
|
|
318
|
+
files: List of file dicts with 'filename' key (from GitHub API)
|
|
319
|
+
min_files: Minimum number of source files required (default: 3)
|
|
320
|
+
max_files: Maximum number of source files allowed (default: 10)
|
|
321
|
+
|
|
322
|
+
Returns:
|
|
323
|
+
Tuple of (passes, reason, source_count) where:
|
|
324
|
+
- passes: True if source files are within [min_files, max_files] range
|
|
325
|
+
- reason: Explanation if failed
|
|
326
|
+
- source_count: Number of source files found
|
|
327
|
+
"""
|
|
328
|
+
source_files = []
|
|
329
|
+
for f in files:
|
|
330
|
+
filename = f.get("filename", "")
|
|
331
|
+
if _is_relevant_source(filename):
|
|
332
|
+
source_files.append(filename)
|
|
333
|
+
|
|
334
|
+
count = len(source_files)
|
|
335
|
+
|
|
336
|
+
if count < min_files:
|
|
337
|
+
return (
|
|
338
|
+
False,
|
|
339
|
+
f"Only {count} source file{'s' if count != 1 else ''} modified (need {min_files}+, tests excluded)",
|
|
340
|
+
count,
|
|
341
|
+
)
|
|
342
|
+
|
|
343
|
+
if count > max_files:
|
|
344
|
+
return (
|
|
345
|
+
False,
|
|
346
|
+
f"Too many source files modified ({count}, max {max_files}) - likely a large refactor (tests excluded)",
|
|
347
|
+
count,
|
|
348
|
+
)
|
|
349
|
+
|
|
350
|
+
return True, "", count
|
swegen/farm/__init__.py
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
from .farm_hand import PRCandidate, TaskResult
|
|
2
|
+
from .fetcher import StreamingPRFetcher, load_skip_list
|
|
3
|
+
from .state import StreamState
|
|
4
|
+
from .stream_farm import StreamFarmer
|
|
5
|
+
|
|
6
|
+
__all__ = [
|
|
7
|
+
"StreamFarmer",
|
|
8
|
+
"StreamState",
|
|
9
|
+
"StreamingPRFetcher",
|
|
10
|
+
"PRCandidate",
|
|
11
|
+
"TaskResult",
|
|
12
|
+
"load_skip_list",
|
|
13
|
+
]
|