local-coding-agent 1.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.
Files changed (101) hide show
  1. local_coding_agent/__init__.py +314 -0
  2. local_coding_agent/__main__.py +5 -0
  3. local_coding_agent/acp_server/__init__.py +38 -0
  4. local_coding_agent/acp_server/_codec.py +95 -0
  5. local_coding_agent/acp_server/_protocol.py +13 -0
  6. local_coding_agent/acp_server/_server.py +663 -0
  7. local_coding_agent/acp_server/_session.py +24 -0
  8. local_coding_agent/acp_server/_tools.py +126 -0
  9. local_coding_agent/ast_compactor.py +130 -0
  10. local_coding_agent/atomizer.py +77 -0
  11. local_coding_agent/benchmark/__init__.py +57 -0
  12. local_coding_agent/benchmark/_cases.py +307 -0
  13. local_coding_agent/benchmark/_judge.py +175 -0
  14. local_coding_agent/benchmark/_oracles.py +157 -0
  15. local_coding_agent/benchmark/_runner.py +259 -0
  16. local_coding_agent/benchmark/_stats.py +22 -0
  17. local_coding_agent/benchmark/_summary.py +143 -0
  18. local_coding_agent/benchmark_oracle_worker.py +169 -0
  19. local_coding_agent/calibration.py +154 -0
  20. local_coding_agent/capability.py +450 -0
  21. local_coding_agent/chat_compaction.py +276 -0
  22. local_coding_agent/cli/__init__.py +12 -0
  23. local_coding_agent/cli/_dispatch.py +377 -0
  24. local_coding_agent/cli/_handlers.py +320 -0
  25. local_coding_agent/cli/_handlers2.py +725 -0
  26. local_coding_agent/cli/_input.py +61 -0
  27. local_coding_agent/cli/_main.py +159 -0
  28. local_coding_agent/cli/_parser.py +327 -0
  29. local_coding_agent/context_manager.py +155 -0
  30. local_coding_agent/controller/__init__.py +18 -0
  31. local_coding_agent/controller/_constants.py +108 -0
  32. local_coding_agent/controller/_controller.py +1008 -0
  33. local_coding_agent/controller/_post_apply.py +216 -0
  34. local_coding_agent/delegator.py +180 -0
  35. local_coding_agent/desktop/__init__.py +8 -0
  36. local_coding_agent/desktop/app.py +100 -0
  37. local_coding_agent/desktop/assets/lucide.min.js +12 -0
  38. local_coding_agent/desktop/assets/tailwind.css +1 -0
  39. local_coding_agent/desktop/client_js.py +1271 -0
  40. local_coding_agent/desktop/components.py +555 -0
  41. local_coding_agent/desktop/server/__init__.py +37 -0
  42. local_coding_agent/desktop/server/_handlers.py +2178 -0
  43. local_coding_agent/desktop/server/_models.py +204 -0
  44. local_coding_agent/desktop/server/_server.py +347 -0
  45. local_coding_agent/desktop/server/_telemetry.py +87 -0
  46. local_coding_agent/desktop/styles.py +76 -0
  47. local_coding_agent/desktop/ui.py +53 -0
  48. local_coding_agent/doctor.py +413 -0
  49. local_coding_agent/hooks/__init__.py +19 -0
  50. local_coding_agent/hooks/_adapters.py +458 -0
  51. local_coding_agent/hooks/_bridge.py +346 -0
  52. local_coding_agent/hooks/_decision.py +26 -0
  53. local_coding_agent/lsp/__init__.py +60 -0
  54. local_coding_agent/lsp/_client.py +322 -0
  55. local_coding_agent/lsp/_fallback.py +297 -0
  56. local_coding_agent/lsp/_manager.py +173 -0
  57. local_coding_agent/lsp/_mock.py +200 -0
  58. local_coding_agent/lsp/_types.py +398 -0
  59. local_coding_agent/mcp_config.py +398 -0
  60. local_coding_agent/mcp_server.py +377 -0
  61. local_coding_agent/memory.py +140 -0
  62. local_coding_agent/mode_router.py +200 -0
  63. local_coding_agent/model_scanner.py +525 -0
  64. local_coding_agent/monitor.py +635 -0
  65. local_coding_agent/observation_policy.py +184 -0
  66. local_coding_agent/ollama_adapter.py +736 -0
  67. local_coding_agent/plan_mode.py +653 -0
  68. local_coding_agent/prescriptions.py +278 -0
  69. local_coding_agent/profiles.py +239 -0
  70. local_coding_agent/repository_tools.py +782 -0
  71. local_coding_agent/ripgrep.py +326 -0
  72. local_coding_agent/semantic_linter.py +122 -0
  73. local_coding_agent/service.py +405 -0
  74. local_coding_agent/session_events.py +684 -0
  75. local_coding_agent/session_query.py +578 -0
  76. local_coding_agent/skill_config.py +426 -0
  77. local_coding_agent/smoke.py +268 -0
  78. local_coding_agent/speculative_racing.py +85 -0
  79. local_coding_agent/spill.py +258 -0
  80. local_coding_agent/stats.py +255 -0
  81. local_coding_agent/stdio.py +132 -0
  82. local_coding_agent/subagent.py +516 -0
  83. local_coding_agent/task.py +90 -0
  84. local_coding_agent/task_store.py +142 -0
  85. local_coding_agent/tasks.py +240 -0
  86. local_coding_agent/terminal/__init__.py +35 -0
  87. local_coding_agent/terminal/_manager.py +156 -0
  88. local_coding_agent/terminal/_process.py +148 -0
  89. local_coding_agent/terminal/_session.py +452 -0
  90. local_coding_agent/terminal/_tools.py +233 -0
  91. local_coding_agent/terminal/_types.py +60 -0
  92. local_coding_agent/tool_call_parser.py +178 -0
  93. local_coding_agent/validators.py +563 -0
  94. local_coding_agent/vram_fit.py +240 -0
  95. local_coding_agent/worker_pool.py +516 -0
  96. local_coding_agent-1.1.0.dist-info/METADATA +202 -0
  97. local_coding_agent-1.1.0.dist-info/RECORD +101 -0
  98. local_coding_agent-1.1.0.dist-info/WHEEL +5 -0
  99. local_coding_agent-1.1.0.dist-info/entry_points.txt +3 -0
  100. local_coding_agent-1.1.0.dist-info/licenses/LICENSE +21 -0
  101. local_coding_agent-1.1.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,307 @@
1
+ """Benchmark case model and the fixed default case set."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass
6
+ from pathlib import Path
7
+ from typing import Any, Callable, Mapping
8
+
9
+ from ..task import TaskEnvelope
10
+
11
+ from ._oracles import (
12
+ _unique_oracle,
13
+ _limit_oracle,
14
+ _utf8_oracle,
15
+ _no_mutation_oracle,
16
+ _count_positives_oracle,
17
+ _max_value_oracle,
18
+ _abs_sum_oracle,
19
+ _reverse_str_oracle,
20
+ _filter_evens_oracle,
21
+ _count_words_oracle,
22
+ _dict_default_oracle,
23
+ _strip_text_oracle,
24
+ _join_words_oracle,
25
+ _last_element_oracle,
26
+ _sorted_copy_oracle,
27
+ _replace_dash_oracle,
28
+ _starts_with_oracle,
29
+ _dot_product_oracle,
30
+ _min_value_oracle,
31
+ _title_case_oracle,
32
+ )
33
+
34
+
35
+ @dataclass(frozen=True)
36
+ class BenchmarkCase:
37
+ id: str
38
+ task: TaskEnvelope
39
+ fixture: Mapping[str, str]
40
+ expected_files: Mapping[str, str]
41
+ oracle: Callable[[Path], tuple[bool, str]] | None = None
42
+
43
+ def __post_init__(self) -> None:
44
+ if not self.id.strip():
45
+ raise ValueError("benchmark case id must be non-empty")
46
+ fixture_paths = set(self.fixture)
47
+ if fixture_paths != set(self.task.files):
48
+ raise ValueError("benchmark fixture paths must match task allowlist")
49
+ if not self.expected_files:
50
+ raise ValueError("benchmark case must have expected files")
51
+ if not set(self.expected_files).issubset(fixture_paths):
52
+ raise ValueError("expected files must be part of the fixture")
53
+
54
+
55
+ def _edit_case(
56
+ case_id: str,
57
+ goal: str,
58
+ file_name: str,
59
+ buggy: str,
60
+ expected: str,
61
+ context: str,
62
+ acceptance: str,
63
+ oracle: Callable[[Path], tuple[bool, str]],
64
+ ) -> BenchmarkCase:
65
+ return BenchmarkCase(
66
+ id=case_id,
67
+ task=TaskEnvelope(
68
+ id=case_id,
69
+ goal=goal,
70
+ files=(f"src/{file_name}.py",),
71
+ context=context,
72
+ constraints=("сохранить имя функции", "не добавлять зависимости"),
73
+ acceptance=(acceptance,),
74
+ ),
75
+ fixture={f"src/{file_name}.py": buggy},
76
+ expected_files={f"src/{file_name}.py": expected},
77
+ oracle=oracle,
78
+ )
79
+
80
+
81
+ def default_cases() -> tuple[BenchmarkCase, ...]:
82
+ """Return a small fixed task set with deterministic external oracles."""
83
+
84
+ return (
85
+ BenchmarkCase(
86
+ id="unique-preserve-order",
87
+ task=TaskEnvelope(
88
+ id="unique-preserve-order",
89
+ goal="убрать сортировку из unique и сохранить порядок первого появления",
90
+ files=("src/unique.py",),
91
+ context="Функция должна удалить повторы, но не менять порядок входных значений.",
92
+ constraints=("не менять публичную сигнатуру", "не добавлять зависимости"),
93
+ acceptance=("повторы удаляются", "порядок первого появления сохраняется"),
94
+ ),
95
+ fixture={"src/unique.py": "def unique(values):\n return sorted(set(values))\n"},
96
+ expected_files={"src/unique.py": "def unique(values):\n return list(dict.fromkeys(values))\n"},
97
+ oracle=_unique_oracle,
98
+ ),
99
+ BenchmarkCase(
100
+ id="limit-inclusive",
101
+ task=TaskEnvelope(
102
+ id="limit-inclusive",
103
+ goal="исправить off-by-one и вернуть ровно limit элементов",
104
+ files=("src/window.py",),
105
+ context="При положительном limit срез должен включать элемент с индексом limit-1.",
106
+ constraints=("изменить только выражение среза",),
107
+ acceptance=("limit=3 возвращает первые три элемента",),
108
+ ),
109
+ fixture={"src/window.py": "def take(values, limit):\n return values[: limit - 1]\n"},
110
+ expected_files={"src/window.py": "def take(values, limit):\n return values[:limit]\n"},
111
+ oracle=_limit_oracle,
112
+ ),
113
+ BenchmarkCase(
114
+ id="utf8-json",
115
+ task=TaskEnvelope(
116
+ id="utf8-json",
117
+ goal="сохранить русские символы при сериализации JSON",
118
+ files=("src/encoding.py",),
119
+ context="JSON должен оставаться валидным и не превращать Unicode-символы в escape-последовательности.",
120
+ constraints=("не менять имя функции", "использовать только стандартную библиотеку"),
121
+ acceptance=("ensure_ascii отключён",),
122
+ ),
123
+ fixture={
124
+ "src/encoding.py": "import json\n\ndef encode(value):\n return json.dumps(value)\n"
125
+ },
126
+ expected_files={
127
+ "src/encoding.py": "import json\n\ndef encode(value):\n return json.dumps(value, ensure_ascii=False)\n"
128
+ },
129
+ oracle=_utf8_oracle,
130
+ ),
131
+ BenchmarkCase(
132
+ id="avoid-input-mutation",
133
+ task=TaskEnvelope(
134
+ id="avoid-input-mutation",
135
+ goal="добавить flag без изменения входного списка",
136
+ files=("src/flags.py",),
137
+ context="Вызов не должен мутировать values: исходный список должен остаться прежним.",
138
+ constraints=("сохранить имя функции", "не добавлять зависимости"),
139
+ acceptance=("возвращается новый список с flag в конце",),
140
+ ),
141
+ fixture={"src/flags.py": "def append_flag(values, flag):\n values.append(flag)\n return values\n"},
142
+ expected_files={
143
+ "src/flags.py": "def append_flag(values, flag):\n return [*values, flag]\n"
144
+ },
145
+ oracle=_no_mutation_oracle,
146
+ ),
147
+ _edit_case(
148
+ "count-positives",
149
+ "посчитать количество положительных элементов",
150
+ "count",
151
+ "def count_positives(values):\n return len(values)\n",
152
+ "def count_positives(values):\n return sum(1 for value in values if value > 0)\n",
153
+ "Считать нужно только элементы строго больше нуля.",
154
+ "счётчик учитывает только положительные элементы",
155
+ _count_positives_oracle,
156
+ ),
157
+ _edit_case(
158
+ "max-value",
159
+ "вернуть максимальное значение из списка",
160
+ "maxval",
161
+ "def max_value(values):\n return values[0]\n",
162
+ "def max_value(values):\n return max(values)\n",
163
+ "Функция должна возвращать наибольший элемент входного списка.",
164
+ "для [3,7,2,5] возвращается 7",
165
+ _max_value_oracle,
166
+ ),
167
+ _edit_case(
168
+ "abs-sum",
169
+ "вернуть сумму модулей элементов",
170
+ "abssum",
171
+ "def abs_sum(values):\n return sum(values)\n",
172
+ "def abs_sum(values):\n return sum(abs(value) for value in values)\n",
173
+ "Отрицательные числа должны входить в сумму как положительные.",
174
+ "для [-1,2,-3] возвращается 6",
175
+ _abs_sum_oracle,
176
+ ),
177
+ _edit_case(
178
+ "reverse-str",
179
+ "развернуть строку в обратном порядке",
180
+ "reverse",
181
+ "def reverse_str(value):\n return value\n",
182
+ "def reverse_str(value):\n return value[::-1]\n",
183
+ "Результат — та же строка в обратном порядке символов.",
184
+ "для 'мир' возвращается 'рим'",
185
+ _reverse_str_oracle,
186
+ ),
187
+ _edit_case(
188
+ "filter-evens",
189
+ "оставить только чётные элементы",
190
+ "evens",
191
+ "def evens(values):\n return values\n",
192
+ "def evens(values):\n return [value for value in values if value % 2 == 0]\n",
193
+ "Нужно вернуть новый список только с чётными числами.",
194
+ "для [1,2,3,4,6] возвращается [2,4,6]",
195
+ _filter_evens_oracle,
196
+ ),
197
+ _edit_case(
198
+ "count-words",
199
+ "посчитать слова в строке",
200
+ "words",
201
+ "def count_words(text):\n return len(text)\n",
202
+ "def count_words(text):\n return len(text.split())\n",
203
+ "Слова разделены пробелами; считать именно слова, а не символы.",
204
+ "для 'один два три' возвращается 3",
205
+ _count_words_oracle,
206
+ ),
207
+ _edit_case(
208
+ "dict-default",
209
+ "вернуть значение ключа или ноль",
210
+ "defval",
211
+ "def get_or_zero(mapping, key):\n return mapping[key]\n",
212
+ "def get_or_zero(mapping, key):\n return mapping.get(key, 0)\n",
213
+ "Отсутствующий ключ не должен выбрасывать ошибку.",
214
+ "для отсутствующего ключа возвращается 0",
215
+ _dict_default_oracle,
216
+ ),
217
+ _edit_case(
218
+ "strip-text",
219
+ "убрать пробелы по краям строки",
220
+ "strip",
221
+ "def normalize(text):\n return text.replace(' ', '')\n",
222
+ "def normalize(text):\n return text.strip()\n",
223
+ "Убирать пробелы нужно только по краям, а не внутри.",
224
+ "для ' hi ' возвращается 'hi'",
225
+ _strip_text_oracle,
226
+ ),
227
+ _edit_case(
228
+ "join-words",
229
+ "склеить слова через пробел",
230
+ "join",
231
+ "def join_words(words):\n return words\n",
232
+ "def join_words(words):\n return ' '.join(words)\n",
233
+ "Результат — одна строка из слов, разделённых одиночным пробелом.",
234
+ "для ['a','b','c'] возвращается 'a b c'",
235
+ _join_words_oracle,
236
+ ),
237
+ _edit_case(
238
+ "last-element",
239
+ "вернуть последний элемент списка",
240
+ "last",
241
+ "def last(values):\n return values[0]\n",
242
+ "def last(values):\n return values[-1]\n",
243
+ "Нужен именно последний, а не первый элемент.",
244
+ "для [1,2,3] возвращается 3",
245
+ _last_element_oracle,
246
+ ),
247
+ _edit_case(
248
+ "sorted-copy",
249
+ "вернуть отсортированную копию без изменения входа",
250
+ "sortcopy",
251
+ "def sorted_copy(values):\n values.sort()\n return values\n",
252
+ "def sorted_copy(values):\n return sorted(values)\n",
253
+ "Исходный список не должен изменяться.",
254
+ "вход [3,1,2] остаётся неизменным, результат [1,2,3]",
255
+ _sorted_copy_oracle,
256
+ ),
257
+ _edit_case(
258
+ "replace-dash",
259
+ "заменить дефисы на подчёркивания",
260
+ "replace",
261
+ "def replace_dash(text):\n return text\n",
262
+ "def replace_dash(text):\n return text.replace('-', '_')\n",
263
+ "Все символы '-' должны стать '_'.",
264
+ "для 'a-b-c' возвращается 'a_b_c'",
265
+ _replace_dash_oracle,
266
+ ),
267
+ _edit_case(
268
+ "starts-with",
269
+ "проверить начало строки",
270
+ "starts",
271
+ "def starts_with(text, prefix):\n return prefix in text\n",
272
+ "def starts_with(text, prefix):\n return text.startswith(prefix)\n",
273
+ "Совпадение должно быть именно в начале строки.",
274
+ "для 'hello','he' — True; для 'hello','xy' — False",
275
+ _starts_with_oracle,
276
+ ),
277
+ _edit_case(
278
+ "dot-product",
279
+ "посчитать скалярное произведение",
280
+ "dot",
281
+ "def dot(left, right):\n return sum(left) * sum(right)\n",
282
+ "def dot(left, right):\n return sum(a * b for a, b in zip(left, right))\n",
283
+ "Суммировать нужно попарные произведения элементов.",
284
+ "для [1,2,3] и [4,5,6] возвращается 32",
285
+ _dot_product_oracle,
286
+ ),
287
+ _edit_case(
288
+ "min-value",
289
+ "вернуть минимальное значение из списка",
290
+ "minval",
291
+ "def min_value(values):\n return values[0]\n",
292
+ "def min_value(values):\n return min(values)\n",
293
+ "Функция должна возвращать наименьший элемент входного списка.",
294
+ "для [3,7,2,5] возвращается 2",
295
+ _min_value_oracle,
296
+ ),
297
+ _edit_case(
298
+ "title-case",
299
+ "перевести каждое слово в регистр заголовка",
300
+ "title",
301
+ "def title_case(text):\n return text.upper()\n",
302
+ "def title_case(text):\n return text.title()\n",
303
+ "Первая буква каждого слова — заглавная, остальные — строчные.",
304
+ "для 'hello world' возвращается 'Hello World'",
305
+ _title_case_oracle,
306
+ ),
307
+ )
@@ -0,0 +1,175 @@
1
+ """Patch judgement: validate, git-apply, and run external oracles on a proposal."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import inspect
7
+ import os
8
+ import shutil
9
+ import subprocess
10
+ import sys
11
+ import textwrap
12
+ from pathlib import Path
13
+ from typing import Any, Callable, Mapping
14
+
15
+ from ..validators import validate_candidate
16
+
17
+ from ._cases import BenchmarkCase
18
+
19
+
20
+ def _judge_patch(
21
+ result: Mapping[str, Any],
22
+ case: BenchmarkCase,
23
+ workspace: Path,
24
+ *,
25
+ fallback_patch: str,
26
+ fallback_edits: list[dict[str, Any]] | None = None,
27
+ ) -> tuple[bool, bool, str, str]:
28
+ patch_source = "accepted_result" if result.get("status") == "accepted" else "tool_proposal"
29
+ patch = result.get("patch") if patch_source == "accepted_result" else fallback_patch
30
+ if (not isinstance(patch, str) or not patch.strip()) and fallback_patch:
31
+ patch_source = "tool_proposal"
32
+ patch = fallback_patch
33
+ edits = None
34
+ if not isinstance(patch, str) or not patch.strip():
35
+ if fallback_edits:
36
+ patch_source = "tool_proposal"
37
+ edits = fallback_edits
38
+ has_patch = isinstance(patch, str) and bool(patch.strip())
39
+ if not has_patch and not edits:
40
+ return False, False, "candidate did not contain a patch", "none"
41
+ validation = _validate_patch_for_case(patch, case, edits=edits, workspace=workspace)
42
+ if not validation.valid:
43
+ return False, False, "; ".join(validation.issues), patch_source
44
+ resolved = validation.resolved_patch or patch
45
+ if shutil.which("git") is None:
46
+ return False, False, "git executable is unavailable for isolated patch application", patch_source
47
+ apply_check = _git_apply(workspace, resolved, check=True)
48
+ if apply_check.returncode != 0:
49
+ return False, False, _process_error(apply_check), patch_source
50
+ applied = _git_apply(workspace, resolved, check=False)
51
+ if applied.returncode != 0:
52
+ return False, False, _process_error(applied), patch_source
53
+ try:
54
+ if case.oracle is not None:
55
+ correct, oracle_error = _run_oracle_in_restricted_process(case.oracle, workspace)
56
+ else:
57
+ correct, oracle_error = _exact_file_oracle(case, workspace)
58
+ except Exception as error: # external oracle must turn malformed proposals into a score
59
+ correct, oracle_error = False, f"external oracle error: {error}"
60
+ if not correct:
61
+ return True, False, oracle_error, patch_source
62
+ if set(validation.changed_files) != set(case.expected_files):
63
+ return True, False, "external oracle changed-file set mismatch", patch_source
64
+ return True, True, "", patch_source
65
+
66
+
67
+ def _validate_patch_for_case(patch: str, case: BenchmarkCase, *, edits=None, workspace=None):
68
+ candidate = {
69
+ "status": "candidate",
70
+ "summary": "benchmark proposal",
71
+ "patch": patch,
72
+ "checks": [],
73
+ "risks": [],
74
+ }
75
+ if edits is not None:
76
+ candidate["edits"] = edits
77
+ return validate_candidate(candidate, case.task, workspace_root=workspace)
78
+
79
+
80
+ def _exact_file_oracle(case: BenchmarkCase, workspace: Path) -> tuple[bool, str]:
81
+ for raw_path, expected in case.expected_files.items():
82
+ actual = (workspace / raw_path).read_text(encoding="utf-8")
83
+ if actual != expected:
84
+ return False, f"external oracle mismatch: {raw_path}"
85
+ return True, ""
86
+
87
+
88
+ def _git_apply(workspace: Path, patch: str, *, check: bool) -> subprocess.CompletedProcess[bytes]:
89
+ command = ["git", "apply", "--whitespace=nowarn"]
90
+ if check:
91
+ command.append("--check")
92
+ command.append("-")
93
+ return subprocess.run(
94
+ command,
95
+ cwd=workspace,
96
+ input=patch.encode("utf-8"),
97
+ stdout=subprocess.PIPE,
98
+ stderr=subprocess.PIPE,
99
+ check=False,
100
+ timeout=10,
101
+ )
102
+
103
+
104
+ def _process_error(process: subprocess.CompletedProcess[bytes]) -> str:
105
+ detail = process.stderr.decode("utf-8", errors="replace").strip()
106
+ return detail or f"git apply exited with code {process.returncode}"
107
+
108
+
109
+ def _run_oracle_in_restricted_process(
110
+ oracle: Callable[[Path], tuple[bool, str]], workspace: Path
111
+ ) -> tuple[bool, str]:
112
+ """Run model-controlled fixture code outside the controller process."""
113
+
114
+ try:
115
+ oracle_source = textwrap.dedent(inspect.getsource(oracle))
116
+ except (OSError, TypeError) as error:
117
+ return False, f"external oracle source is unavailable: {error}"
118
+ payload = {
119
+ "workspace": str(workspace),
120
+ "oracle_name": getattr(oracle, "__name__", ""),
121
+ "oracle_source": oracle_source,
122
+ }
123
+ # the worker module lives one directory up from this package
124
+ worker = Path(__file__).resolve().parents[1] / "benchmark_oracle_worker.py"
125
+ try:
126
+ completed = subprocess.run(
127
+ [sys.executable, "-I", "-u", str(worker)],
128
+ cwd=workspace,
129
+ env=_benchmark_worker_environment(workspace),
130
+ input=(json.dumps(payload, ensure_ascii=False) + "\n").encode("utf-8"),
131
+ stdout=subprocess.PIPE,
132
+ stderr=subprocess.PIPE,
133
+ timeout=10,
134
+ check=False,
135
+ **_benchmark_process_options(),
136
+ )
137
+ except (OSError, subprocess.TimeoutExpired) as error:
138
+ return False, f"external oracle process failed: {error}"
139
+ if completed.returncode != 0:
140
+ detail = completed.stderr.decode("utf-8", errors="replace").strip()
141
+ return False, detail[:2000] or f"external oracle exited with code {completed.returncode}"
142
+ try:
143
+ result = json.loads(completed.stdout.decode("utf-8"))
144
+ except (UnicodeDecodeError, json.JSONDecodeError) as error:
145
+ return False, f"external oracle returned invalid JSON: {error}"
146
+ if not isinstance(result, Mapping) or result.get("ok") is not True:
147
+ detail = result.get("error") if isinstance(result, Mapping) else None
148
+ return False, f"external oracle error: {detail or 'unknown worker error'}"
149
+ correct = result.get("correct")
150
+ detail = result.get("detail", "")
151
+ if not isinstance(correct, bool) or not isinstance(detail, str):
152
+ return False, "external oracle returned an invalid result shape"
153
+ return correct, detail
154
+
155
+
156
+ def _benchmark_worker_environment(workspace: Path) -> dict[str, str]:
157
+ python_dir = str(Path(sys.executable).resolve().parent)
158
+ environment = {
159
+ "PATH": python_dir,
160
+ "PYTHONIOENCODING": "utf-8",
161
+ "PYTHONUTF8": "1",
162
+ "TEMP": str(workspace),
163
+ "TMP": str(workspace),
164
+ }
165
+ for key in ("SystemRoot", "WINDIR"):
166
+ value = os.environ.get(key)
167
+ if value:
168
+ environment[key] = value
169
+ return environment
170
+
171
+
172
+ def _benchmark_process_options() -> dict[str, Any]:
173
+ if os.name == "nt":
174
+ return {"creationflags": getattr(subprocess, "CREATE_NEW_PROCESS_GROUP", 0)}
175
+ return {"start_new_session": True}
@@ -0,0 +1,157 @@
1
+ """External correctness oracles for benchmark cases.
2
+
3
+ Note: oracle bodies reference ``_load_function`` as a module-level global.
4
+ That symbol is injected at call time by ``benchmark_oracle_worker.py``, which
5
+ executes the oracle source in a restricted child process; it is never resolved
6
+ in this package.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ from pathlib import Path
12
+ from typing import Callable
13
+
14
+
15
+ def _unique_oracle(workspace: Path) -> tuple[bool, str]:
16
+ unique = _load_function(workspace / "src/unique.py", "unique")
17
+ if unique([3, 1, 2, 1, 3]) != [3, 1, 2]:
18
+ return False, "external oracle mismatch: unique does not preserve order"
19
+ return True, ""
20
+
21
+
22
+ def _limit_oracle(workspace: Path) -> tuple[bool, str]:
23
+ take = _load_function(workspace / "src/window.py", "take")
24
+ if take([0, 1, 2, 3], 3) != [0, 1, 2]:
25
+ return False, "external oracle mismatch: limit is not inclusive"
26
+ return True, ""
27
+
28
+
29
+ def _utf8_oracle(workspace: Path) -> tuple[bool, str]:
30
+ encode = _load_function(workspace / "src/encoding.py", "encode")
31
+ encoded = encode({"message": "мир"})
32
+ if not isinstance(encoded, str) or "\\u" in encoded or '"мир"' not in encoded:
33
+ return False, "external oracle mismatch: Unicode was escaped"
34
+ return True, ""
35
+
36
+
37
+ def _no_mutation_oracle(workspace: Path) -> tuple[bool, str]:
38
+ append_flag = _load_function(workspace / "src/flags.py", "append_flag")
39
+ values = ["a"]
40
+ returned = append_flag(values, "b")
41
+ if values != ["a"] or returned != ["a", "b"] or returned is values:
42
+ return False, "external oracle mismatch: input list was mutated"
43
+ return True, ""
44
+
45
+
46
+ def _count_positives_oracle(workspace: Path) -> tuple[bool, str]:
47
+ fn = _load_function(workspace / "src/count.py", "count_positives")
48
+ if fn([-1, 2, -3, 4, 0]) != 2:
49
+ return False, "external oracle mismatch: count_positives"
50
+ return True, ""
51
+
52
+
53
+ def _max_value_oracle(workspace: Path) -> tuple[bool, str]:
54
+ fn = _load_function(workspace / "src/maxval.py", "max_value")
55
+ if fn([3, 7, 2, 5]) != 7:
56
+ return False, "external oracle mismatch: max_value"
57
+ return True, ""
58
+
59
+
60
+ def _abs_sum_oracle(workspace: Path) -> tuple[bool, str]:
61
+ fn = _load_function(workspace / "src/abssum.py", "abs_sum")
62
+ if fn([-1, 2, -3]) != 6:
63
+ return False, "external oracle mismatch: abs_sum"
64
+ return True, ""
65
+
66
+
67
+ def _reverse_str_oracle(workspace: Path) -> tuple[bool, str]:
68
+ fn = _load_function(workspace / "src/reverse.py", "reverse_str")
69
+ if fn("мир") != "рим":
70
+ return False, "external oracle mismatch: reverse_str"
71
+ return True, ""
72
+
73
+
74
+ def _filter_evens_oracle(workspace: Path) -> tuple[bool, str]:
75
+ fn = _load_function(workspace / "src/evens.py", "evens")
76
+ if fn([1, 2, 3, 4, 6]) != [2, 4, 6]:
77
+ return False, "external oracle mismatch: evens"
78
+ return True, ""
79
+
80
+
81
+ def _count_words_oracle(workspace: Path) -> tuple[bool, str]:
82
+ fn = _load_function(workspace / "src/words.py", "count_words")
83
+ if fn("один два три") != 3:
84
+ return False, "external oracle mismatch: count_words"
85
+ return True, ""
86
+
87
+
88
+ def _dict_default_oracle(workspace: Path) -> tuple[bool, str]:
89
+ fn = _load_function(workspace / "src/defval.py", "get_or_zero")
90
+ if fn({"a": 1}, "b") != 0 or fn({"a": 5}, "a") != 5:
91
+ return False, "external oracle mismatch: get_or_zero"
92
+ return True, ""
93
+
94
+
95
+ def _strip_text_oracle(workspace: Path) -> tuple[bool, str]:
96
+ fn = _load_function(workspace / "src/strip.py", "normalize")
97
+ if fn(" hi ") != "hi":
98
+ return False, "external oracle mismatch: normalize"
99
+ return True, ""
100
+
101
+
102
+ def _join_words_oracle(workspace: Path) -> tuple[bool, str]:
103
+ fn = _load_function(workspace / "src/join.py", "join_words")
104
+ if fn(["a", "b", "c"]) != "a b c":
105
+ return False, "external oracle mismatch: join_words"
106
+ return True, ""
107
+
108
+
109
+ def _last_element_oracle(workspace: Path) -> tuple[bool, str]:
110
+ fn = _load_function(workspace / "src/last.py", "last")
111
+ if fn([1, 2, 3]) != 3:
112
+ return False, "external oracle mismatch: last"
113
+ return True, ""
114
+
115
+
116
+ def _sorted_copy_oracle(workspace: Path) -> tuple[bool, str]:
117
+ fn = _load_function(workspace / "src/sortcopy.py", "sorted_copy")
118
+ values = [3, 1, 2]
119
+ returned = fn(values)
120
+ if returned != [1, 2, 3] or values != [3, 1, 2]:
121
+ return False, "external oracle mismatch: sorted_copy"
122
+ return True, ""
123
+
124
+
125
+ def _replace_dash_oracle(workspace: Path) -> tuple[bool, str]:
126
+ fn = _load_function(workspace / "src/replace.py", "replace_dash")
127
+ if fn("a-b-c") != "a_b_c":
128
+ return False, "external oracle mismatch: replace_dash"
129
+ return True, ""
130
+
131
+
132
+ def _starts_with_oracle(workspace: Path) -> tuple[bool, str]:
133
+ fn = _load_function(workspace / "src/starts.py", "starts_with")
134
+ if not fn("hello", "he") or fn("hello", "xy"):
135
+ return False, "external oracle mismatch: starts_with"
136
+ return True, ""
137
+
138
+
139
+ def _dot_product_oracle(workspace: Path) -> tuple[bool, str]:
140
+ fn = _load_function(workspace / "src/dot.py", "dot")
141
+ if fn([1, 2, 3], [4, 5, 6]) != 32:
142
+ return False, "external oracle mismatch: dot"
143
+ return True, ""
144
+
145
+
146
+ def _min_value_oracle(workspace: Path) -> tuple[bool, str]:
147
+ fn = _load_function(workspace / "src/minval.py", "min_value")
148
+ if fn([3, 7, 2, 5]) != 2:
149
+ return False, "external oracle mismatch: min_value"
150
+ return True, ""
151
+
152
+
153
+ def _title_case_oracle(workspace: Path) -> tuple[bool, str]:
154
+ fn = _load_function(workspace / "src/title.py", "title_case")
155
+ if fn("hello world") != "Hello World":
156
+ return False, "external oracle mismatch: title_case"
157
+ return True, ""