every-cli 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.
- every/__init__.py +3 -0
- every/cache.py +65 -0
- every/classify.py +23 -0
- every/cli.py +239 -0
- every/discover.py +96 -0
- every/extract.py +137 -0
- every/judge.py +288 -0
- every/neighborhood.py +45 -0
- every/report.py +63 -0
- every/selftest.py +83 -0
- every/symbols.py +109 -0
- every/units.py +34 -0
- every_cli-0.1.0.dist-info/METADATA +118 -0
- every_cli-0.1.0.dist-info/RECORD +18 -0
- every_cli-0.1.0.dist-info/WHEEL +5 -0
- every_cli-0.1.0.dist-info/entry_points.txt +2 -0
- every_cli-0.1.0.dist-info/licenses/LICENSE +21 -0
- every_cli-0.1.0.dist-info/top_level.txt +1 -0
every/judge.py
ADDED
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
"""The Jev client. Port of reference/sweep.py: embed-per-question batching,
|
|
2
|
+
token-budget packing, bounded concurrency, backoff on overload, split on too-big."""
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
import asyncio
|
|
7
|
+
import json
|
|
8
|
+
import random
|
|
9
|
+
import time
|
|
10
|
+
from dataclasses import dataclass, field
|
|
11
|
+
|
|
12
|
+
import httpx
|
|
13
|
+
|
|
14
|
+
from .units import (OUTPUT_RESERVE_PER_Q, REQUEST_FIXED_TOKENS, estimate_tokens)
|
|
15
|
+
|
|
16
|
+
API_URL = "https://api.typesafe.ai/v1/systemone"
|
|
17
|
+
MODEL = "jev-latest"
|
|
18
|
+
MAX_ATTEMPTS = 6
|
|
19
|
+
|
|
20
|
+
INSTRUCTIONS = (
|
|
21
|
+
"Answer for the single function in `unit` only. Judge from its source, its file "
|
|
22
|
+
"path, and any context included with it. Do not assume anything about code you "
|
|
23
|
+
"cannot see. Question: {question}"
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@dataclass
|
|
28
|
+
class Item:
|
|
29
|
+
id: str
|
|
30
|
+
payload: dict
|
|
31
|
+
est: int = 0
|
|
32
|
+
|
|
33
|
+
def __post_init__(self) -> None:
|
|
34
|
+
if not self.est:
|
|
35
|
+
self.est = estimate_tokens(json.dumps(self.payload))
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@dataclass
|
|
39
|
+
class Stats:
|
|
40
|
+
requests: int = 0
|
|
41
|
+
retries: int = 0
|
|
42
|
+
splits: int = 0
|
|
43
|
+
input_tokens: int = 0
|
|
44
|
+
output_tokens: int = 0
|
|
45
|
+
est_tokens: int = 0
|
|
46
|
+
failed: int = 0
|
|
47
|
+
latencies: list = field(default_factory=list)
|
|
48
|
+
last_error: str = ""
|
|
49
|
+
|
|
50
|
+
def merge(self, other: "Stats") -> "Stats":
|
|
51
|
+
return Stats(
|
|
52
|
+
requests=self.requests + other.requests, retries=self.retries + other.retries,
|
|
53
|
+
splits=self.splits + other.splits, input_tokens=self.input_tokens + other.input_tokens,
|
|
54
|
+
output_tokens=self.output_tokens + other.output_tokens,
|
|
55
|
+
est_tokens=self.est_tokens + other.est_tokens, failed=self.failed + other.failed,
|
|
56
|
+
latencies=self.latencies + other.latencies, last_error=other.last_error or self.last_error,
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class TooBig(Exception):
|
|
61
|
+
pass
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
class Fatal(Exception):
|
|
65
|
+
pass
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
class Unauthorized(Fatal):
|
|
69
|
+
"""401/403 - the API key was rejected; aborts the whole run."""
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def criteria_for(question: str) -> dict:
|
|
73
|
+
return {
|
|
74
|
+
"true": f"Yes. For this function it is true that: {question}",
|
|
75
|
+
"false": f"No. For this function it is not true that: {question}",
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def request_body(question: str, repo_card: dict, batch: list) -> dict:
|
|
80
|
+
return {
|
|
81
|
+
"model": MODEL,
|
|
82
|
+
"state": {"repo": repo_card,
|
|
83
|
+
"note": "Each question below is independent; judge only the unit inside it."},
|
|
84
|
+
"questions": {
|
|
85
|
+
f"q{i}": {
|
|
86
|
+
"type": "noul",
|
|
87
|
+
"instructions": {"question": INSTRUCTIONS.format(question=question), "unit": it.payload},
|
|
88
|
+
"criteria": criteria_for(question),
|
|
89
|
+
}
|
|
90
|
+
for i, it in enumerate(batch)
|
|
91
|
+
},
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def pack(items: list, budget: int, width: int | None = None) -> list:
|
|
96
|
+
batches, cur, tok = [], [], REQUEST_FIXED_TOKENS
|
|
97
|
+
for it in items:
|
|
98
|
+
t = it.est + OUTPUT_RESERVE_PER_Q
|
|
99
|
+
if cur and (tok + t > budget or (width and len(cur) >= width)):
|
|
100
|
+
batches.append(cur)
|
|
101
|
+
cur, tok = [], REQUEST_FIXED_TOKENS
|
|
102
|
+
cur.append(it)
|
|
103
|
+
tok += t
|
|
104
|
+
if cur:
|
|
105
|
+
batches.append(cur)
|
|
106
|
+
return batches
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def _is_too_big(response) -> bool:
|
|
110
|
+
try:
|
|
111
|
+
detail = response.json().get("detail") or {}
|
|
112
|
+
if isinstance(detail, dict) and detail.get("error_type") == "max_tokens_exceeded":
|
|
113
|
+
return True
|
|
114
|
+
except ValueError:
|
|
115
|
+
pass
|
|
116
|
+
return "max_tokens_exceeded" in response.text
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
class Judge:
|
|
120
|
+
def __init__(self, api_key: str, *, api_url: str = API_URL, depth: int = 4,
|
|
121
|
+
budget: int = 45_000, timeout: float = 90.0, transport=None, sleep=None) -> None:
|
|
122
|
+
self.api_key = api_key
|
|
123
|
+
self.api_url = api_url
|
|
124
|
+
self.depth = depth
|
|
125
|
+
self.budget = budget
|
|
126
|
+
self.timeout = timeout
|
|
127
|
+
self.transport = transport
|
|
128
|
+
self._sleep = sleep or asyncio.sleep
|
|
129
|
+
|
|
130
|
+
def _headers(self) -> dict:
|
|
131
|
+
return {"Authorization": f"Bearer {self.api_key}", "Content-Type": "application/json"}
|
|
132
|
+
|
|
133
|
+
async def _send(self, client, question, repo_card, batch, stats: Stats) -> dict:
|
|
134
|
+
body = request_body(question, repo_card, batch)
|
|
135
|
+
for attempt in range(MAX_ATTEMPTS):
|
|
136
|
+
last = attempt == MAX_ATTEMPTS - 1
|
|
137
|
+
t0 = time.perf_counter()
|
|
138
|
+
try:
|
|
139
|
+
r = await client.post(self.api_url, json=body)
|
|
140
|
+
except httpx.HTTPError: # timeouts, transport errors, protocol errors
|
|
141
|
+
stats.retries += 1
|
|
142
|
+
if not last:
|
|
143
|
+
await self._sleep(0.5 * 2 ** attempt + random.random() * 0.3)
|
|
144
|
+
continue
|
|
145
|
+
stats.latencies.append((time.perf_counter() - t0) * 1000)
|
|
146
|
+
stats.requests += 1
|
|
147
|
+
if r.status_code in (401, 403):
|
|
148
|
+
raise Unauthorized(f"HTTP {r.status_code}: API key rejected")
|
|
149
|
+
if r.status_code == 200:
|
|
150
|
+
try:
|
|
151
|
+
data = r.json()
|
|
152
|
+
except ValueError:
|
|
153
|
+
raise Fatal("malformed JSON in 200 response")
|
|
154
|
+
if not isinstance(data, dict):
|
|
155
|
+
raise Fatal("unexpected JSON shape in 200 response")
|
|
156
|
+
usage = data.get("usage") or {}
|
|
157
|
+
stats.input_tokens += int(usage.get("input_tokens", 0) or 0)
|
|
158
|
+
stats.output_tokens += int(usage.get("output_tokens", 0) or 0)
|
|
159
|
+
stats.est_tokens += sum(it.est for it in batch) + REQUEST_FIXED_TOKENS
|
|
160
|
+
answers = data.get("answers") or {}
|
|
161
|
+
out = {}
|
|
162
|
+
for i, it in enumerate(batch):
|
|
163
|
+
a = answers.get(f"q{i}")
|
|
164
|
+
if isinstance(a, dict) and isinstance(a.get("noul"), (int, float)):
|
|
165
|
+
out[it.id] = float(a["noul"])
|
|
166
|
+
return out
|
|
167
|
+
if r.status_code == 400 and _is_too_big(r):
|
|
168
|
+
raise TooBig()
|
|
169
|
+
if r.status_code in (429, 529) or r.status_code >= 500:
|
|
170
|
+
stats.retries += 1
|
|
171
|
+
if not last:
|
|
172
|
+
await self._sleep(0.5 * 2 ** attempt + random.random() * 0.3)
|
|
173
|
+
continue
|
|
174
|
+
raise Fatal(f"HTTP {r.status_code}: {r.text[:200]}")
|
|
175
|
+
raise Fatal("retries exhausted")
|
|
176
|
+
|
|
177
|
+
async def _process(self, client, batch, question, repo_card, stats: Stats, results: dict,
|
|
178
|
+
on_result, queue: asyncio.Queue) -> None:
|
|
179
|
+
"""Judge one batch; split, truncate, or mark failed. Never raises."""
|
|
180
|
+
try:
|
|
181
|
+
got = await self._send(client, question, repo_card, batch, stats)
|
|
182
|
+
except Unauthorized:
|
|
183
|
+
raise
|
|
184
|
+
except TooBig:
|
|
185
|
+
if len(batch) > 1:
|
|
186
|
+
stats.splits += 1
|
|
187
|
+
mid = len(batch) // 2
|
|
188
|
+
queue.put_nowait(batch[:mid])
|
|
189
|
+
queue.put_nowait(batch[mid:])
|
|
190
|
+
return
|
|
191
|
+
it = batch[0]
|
|
192
|
+
if it.payload.get("truncated"):
|
|
193
|
+
stats.failed += 1
|
|
194
|
+
return
|
|
195
|
+
src = it.payload.get("source", "")
|
|
196
|
+
it.payload = {**it.payload, "source": src[: max(1, len(src) // 2)] + "\n[truncated]", "truncated": True}
|
|
197
|
+
it.est = estimate_tokens(json.dumps(it.payload))
|
|
198
|
+
queue.put_nowait([it])
|
|
199
|
+
return
|
|
200
|
+
except Fatal as e:
|
|
201
|
+
stats.failed += len(batch)
|
|
202
|
+
stats.last_error = str(e)
|
|
203
|
+
return
|
|
204
|
+
except Exception as e: # one bad batch must never take the run down
|
|
205
|
+
stats.failed += len(batch)
|
|
206
|
+
stats.last_error = f"{type(e).__name__}: {e}"[:200]
|
|
207
|
+
return
|
|
208
|
+
stats.failed += sum(1 for it in batch if it.id not in got)
|
|
209
|
+
results.update(got)
|
|
210
|
+
if on_result and got:
|
|
211
|
+
try:
|
|
212
|
+
on_result(got)
|
|
213
|
+
except Exception as e: # a side effect must never take the run down
|
|
214
|
+
stats.last_error = f"on_result: {type(e).__name__}: {e}"[:200]
|
|
215
|
+
|
|
216
|
+
async def _run(self, items, question, repo_card, stats: Stats, on_result) -> dict:
|
|
217
|
+
queue: asyncio.Queue = asyncio.Queue()
|
|
218
|
+
for b in pack(items, self.budget):
|
|
219
|
+
queue.put_nowait(b)
|
|
220
|
+
results: dict = {}
|
|
221
|
+
|
|
222
|
+
async def worker(client):
|
|
223
|
+
while True:
|
|
224
|
+
batch = await queue.get() # stays alive until cancelled; splits refill the queue
|
|
225
|
+
try:
|
|
226
|
+
await self._process(client, batch, question, repo_card, stats, results, on_result, queue)
|
|
227
|
+
finally:
|
|
228
|
+
queue.task_done()
|
|
229
|
+
|
|
230
|
+
async with httpx.AsyncClient(headers=self._headers(), timeout=self.timeout,
|
|
231
|
+
transport=self.transport) as client:
|
|
232
|
+
workers = [asyncio.create_task(worker(client)) for _ in range(self.depth)]
|
|
233
|
+
joiner = asyncio.create_task(queue.join())
|
|
234
|
+
done, _ = await asyncio.wait({joiner, *workers}, return_when=asyncio.FIRST_COMPLETED)
|
|
235
|
+
for t in (joiner, *workers):
|
|
236
|
+
t.cancel()
|
|
237
|
+
await asyncio.gather(joiner, *workers, return_exceptions=True)
|
|
238
|
+
first = None
|
|
239
|
+
for t in done:
|
|
240
|
+
if t is not joiner and not t.cancelled() and t.exception() is not None:
|
|
241
|
+
first = first or t.exception()
|
|
242
|
+
if first is not None:
|
|
243
|
+
raise first
|
|
244
|
+
return results
|
|
245
|
+
|
|
246
|
+
def judge(self, items: list, question: str, repo_card: dict, on_result=None) -> tuple:
|
|
247
|
+
stats = Stats()
|
|
248
|
+
results = asyncio.run(self._run(items, question, repo_card, stats, on_result))
|
|
249
|
+
return results, stats
|
|
250
|
+
|
|
251
|
+
def judge_with_reask(self, items: list, question: str, repo_card: dict, *,
|
|
252
|
+
above: float, band: float = 0.10, on_result=None) -> tuple:
|
|
253
|
+
scores, stats = self.judge(items, question, repo_card, on_result)
|
|
254
|
+
near = [it for it in items if it.id in scores and abs(scores[it.id] - above) <= band]
|
|
255
|
+
if not near:
|
|
256
|
+
return scores, stats
|
|
257
|
+
random.Random(7).shuffle(near)
|
|
258
|
+
second, stats2 = self.judge(near, question, repo_card, on_result)
|
|
259
|
+
for uid, s in second.items():
|
|
260
|
+
scores[uid] = (scores[uid] + s) / 2
|
|
261
|
+
return scores, stats.merge(stats2)
|
|
262
|
+
|
|
263
|
+
def choice(self, state: dict, instructions: str, criteria: dict) -> str | None:
|
|
264
|
+
"""One `choice` question. Returns the chosen key, or None on any failure - never raises."""
|
|
265
|
+
body = {"model": MODEL, "state": state,
|
|
266
|
+
"questions": {"c": {"type": "choice", "instructions": instructions, "criteria": criteria}}}
|
|
267
|
+
|
|
268
|
+
async def go():
|
|
269
|
+
try:
|
|
270
|
+
async with httpx.AsyncClient(headers=self._headers(), timeout=self.timeout,
|
|
271
|
+
transport=self.transport) as client:
|
|
272
|
+
r = await client.post(self.api_url, json=body)
|
|
273
|
+
if r.status_code in (401, 403):
|
|
274
|
+
raise Unauthorized(f"HTTP {r.status_code}: API key rejected")
|
|
275
|
+
if r.status_code != 200:
|
|
276
|
+
return None
|
|
277
|
+
data = r.json()
|
|
278
|
+
if not isinstance(data, dict):
|
|
279
|
+
return None
|
|
280
|
+
answer = (data.get("answers") or {}).get("c")
|
|
281
|
+
if not isinstance(answer, dict):
|
|
282
|
+
return None
|
|
283
|
+
chosen = answer.get("choice")
|
|
284
|
+
return chosen if isinstance(chosen, str) else None
|
|
285
|
+
except (httpx.HTTPError, ValueError, TypeError, AttributeError):
|
|
286
|
+
return None
|
|
287
|
+
|
|
288
|
+
return asyncio.run(go())
|
every/neighborhood.py
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
"""Bring the context to the question: per-unit text for stage 1 and stage 2."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from .units import estimate_tokens
|
|
6
|
+
|
|
7
|
+
SIGNATURE_CHARS = 160
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def signature(unit) -> str:
|
|
11
|
+
first = unit.source.splitlines()[0] if unit.source else unit.name
|
|
12
|
+
return first[:SIGNATURE_CHARS]
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def stage1_text(unit, index) -> str:
|
|
16
|
+
parts = [unit.source]
|
|
17
|
+
callees = index.callees_of(unit)
|
|
18
|
+
callers = index.callers_of(unit)
|
|
19
|
+
if callees:
|
|
20
|
+
parts.append("# calls:\n" + "\n".join(f" {c.file}:{c.start_line} {signature(c)}" for c in callees))
|
|
21
|
+
if callers:
|
|
22
|
+
parts.append("# called by:\n" + "\n".join(f" {c.file}:{c.start_line} {signature(c)}" for c in callers))
|
|
23
|
+
return "\n\n".join(parts)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def stage2_text(unit, index, budget_tokens: int = 40_000) -> str:
|
|
27
|
+
parts = [unit.source]
|
|
28
|
+
used = estimate_tokens(unit.source)
|
|
29
|
+
seen = {unit.id}
|
|
30
|
+
frontier = [("calls", c) for c in index.callees_of(unit)] + \
|
|
31
|
+
[("called by", c) for c in index.callers_of(unit)]
|
|
32
|
+
while frontier:
|
|
33
|
+
relation, n = frontier.pop(0)
|
|
34
|
+
if n.id in seen:
|
|
35
|
+
continue
|
|
36
|
+
seen.add(n.id)
|
|
37
|
+
block = f"# {relation}: {n.file}:{n.start_line} {n.name}\n{n.source}"
|
|
38
|
+
cost = estimate_tokens(block)
|
|
39
|
+
if used + cost > budget_tokens:
|
|
40
|
+
break
|
|
41
|
+
parts.append(block)
|
|
42
|
+
used += cost
|
|
43
|
+
frontier += [("calls", c) for c in index.callees_of(n)] + \
|
|
44
|
+
[("called by", c) for c in index.callers_of(n)]
|
|
45
|
+
return "\n\n".join(parts)
|
every/report.py
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
"""Turn scores into a ranked table or JSON."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
|
|
7
|
+
DIM, RESET = "\033[2m", "\033[0m"
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def build_rows(units: list, scores: dict, stage: dict) -> list:
|
|
11
|
+
rows = []
|
|
12
|
+
for u in units:
|
|
13
|
+
if u.id not in scores:
|
|
14
|
+
continue
|
|
15
|
+
rows.append({
|
|
16
|
+
"score": scores[u.id], "file": u.file, "line": u.start_line, "end_line": u.end_line,
|
|
17
|
+
"function": u.name, "language": u.lang, "kind": u.kind, "stage": stage.get(u.id, 1),
|
|
18
|
+
})
|
|
19
|
+
rows.sort(key=lambda r: (-r["score"], r["file"], r["line"]))
|
|
20
|
+
return rows
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def select_rows(rows: list, above: float, top: int) -> list:
|
|
24
|
+
hits = [r for r in rows if r["score"] >= above]
|
|
25
|
+
return hits if len(hits) >= top else rows[:top]
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _header(meta: dict) -> str:
|
|
29
|
+
line = (f"scanned {meta['units']:,} functions in {meta['files']:,} files ... "
|
|
30
|
+
f"{meta['seconds']:.1f}s ${meta['cost_usd']:.2f} class: {meta['class']}")
|
|
31
|
+
if meta.get("coverage") == "partial":
|
|
32
|
+
line += " coverage: partial (neighborhood-level)"
|
|
33
|
+
if meta.get("partial"):
|
|
34
|
+
line += " [interrupted - partial results]"
|
|
35
|
+
return line
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def render_table(rows: list, meta: dict, above: float, top: int, color: bool) -> str:
|
|
39
|
+
out = [_header(meta), ""]
|
|
40
|
+
selected = select_rows(rows, above, top)
|
|
41
|
+
shown = selected[:top]
|
|
42
|
+
width = max((len(f"{r['file']}:{r['line']}") for r in shown), default=10)
|
|
43
|
+
for r in shown:
|
|
44
|
+
loc = f"{r['file']}:{r['line']}"
|
|
45
|
+
line = f" {r['score']:.2f} {loc:<{width}} {r['function']}"
|
|
46
|
+
if r["score"] < above:
|
|
47
|
+
line += f" (below {above:.2f})"
|
|
48
|
+
if color:
|
|
49
|
+
line = DIM + line + RESET
|
|
50
|
+
out.append(line)
|
|
51
|
+
hits = sum(1 for r in rows if r["score"] >= above)
|
|
52
|
+
out.append("")
|
|
53
|
+
footer = f" {hits} hits >= {above:.2f} out of {meta['units']:,} functions"
|
|
54
|
+
if len(selected) > len(shown):
|
|
55
|
+
footer += f" (showing {len(shown)}; use --top to see more)"
|
|
56
|
+
out.append(footer)
|
|
57
|
+
if meta.get("failed_units"):
|
|
58
|
+
out.append(f" {meta['failed_units']} functions could not be judged")
|
|
59
|
+
return "\n".join(out)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def render_json(rows: list, meta: dict) -> str:
|
|
63
|
+
return json.dumps({"results": rows, "meta": meta}, indent=1)
|
every/selftest.py
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
"""A bundled labelled set: ten functions that swallow errors, ten that don't."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from .units import DEFAULT_ABOVE as ABOVE
|
|
6
|
+
from .judge import Item, Judge
|
|
7
|
+
|
|
8
|
+
SELFTEST_QUESTION = ("Does this function catch an exception or receive an error and then ignore it, "
|
|
9
|
+
"without handling, logging, re-raising, or returning it?")
|
|
10
|
+
|
|
11
|
+
POSITIVES = [
|
|
12
|
+
("swallow_pass", "def swallow_pass(path):\n try:\n return open(path).read()\n except Exception:\n pass\n"),
|
|
13
|
+
("swallow_write_failure", "def swallow_write_failure(path, data):\n try:\n with open(path, 'w') as fh:\n fh.write(data)\n except OSError:\n pass\n"),
|
|
14
|
+
("swallow_bare_except", "def swallow_bare_except(client):\n try:\n client.close()\n except:\n pass\n"),
|
|
15
|
+
("swallow_continue", "def swallow_continue(items):\n out = []\n for i in items:\n try:\n out.append(parse(i))\n except Exception:\n continue\n return out\n"),
|
|
16
|
+
("swallow_ellipsis", "def swallow_ellipsis(conn):\n try:\n conn.commit()\n except Exception:\n ...\n"),
|
|
17
|
+
("swallow_comment_only", "def swallow_comment_only(sock):\n try:\n sock.shutdown()\n except OSError:\n # already closed\n pass\n"),
|
|
18
|
+
("swallow_go_style", "def swallow_go_style(cmd):\n result, err = run(cmd)\n return result\n"),
|
|
19
|
+
("swallow_payment_error", "def swallow_payment_error(order):\n try:\n charge(order)\n except PaymentError:\n pass\n order.status = 'paid'\n"),
|
|
20
|
+
("swallow_nested", "def swallow_nested(rows):\n for r in rows:\n try:\n try:\n save(r)\n except KeyError:\n pass\n except Exception:\n pass\n"),
|
|
21
|
+
("swallow_callback_error", "def swallow_callback_error(cb):\n try:\n cb()\n except BaseException:\n return\n"),
|
|
22
|
+
]
|
|
23
|
+
|
|
24
|
+
NEGATIVES = [
|
|
25
|
+
("logs_and_reraises", "def logs_and_reraises(path):\n try:\n return open(path).read()\n except OSError as e:\n log.error('read failed: %s', e)\n raise\n"),
|
|
26
|
+
("returns_error", "def returns_error(x):\n try:\n return int(x), None\n except ValueError as e:\n return None, e\n"),
|
|
27
|
+
("no_try_at_all", "def no_try_at_all(a, b):\n return a + b\n"),
|
|
28
|
+
("wraps_and_raises", "def wraps_and_raises(url):\n try:\n return fetch(url)\n except TimeoutError as e:\n raise ServiceError('upstream timeout') from e\n"),
|
|
29
|
+
("handles_with_fallback_and_log", "def handles_with_fallback_and_log(cfg):\n try:\n return cfg['port']\n except KeyError:\n log.warning('port missing, using default')\n return 8080\n"),
|
|
30
|
+
("finally_only", "def finally_only(conn):\n try:\n conn.execute('BEGIN')\n finally:\n conn.close()\n"),
|
|
31
|
+
("checks_error_value", "def checks_error_value(cmd):\n result, err = run(cmd)\n if err is not None:\n raise RuntimeError(err)\n return result\n"),
|
|
32
|
+
("retries_then_raises", "def retries_then_raises(op):\n for attempt in range(3):\n try:\n return op()\n except IOError:\n if attempt == 2:\n raise\n"),
|
|
33
|
+
("records_metric", "def records_metric(job):\n try:\n job.run()\n except Exception as e:\n metrics.increment('job.failed')\n raise\n"),
|
|
34
|
+
("plain_read_no_error_path", "def plain_read_no_error_path(path):\n with open(path) as fh:\n return fh.read()\n"),
|
|
35
|
+
]
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _items(rows, label):
|
|
39
|
+
# The label lives only in the id (never sent to the model); the payload is what Jev sees.
|
|
40
|
+
return [Item(id=f"{label}:{name}", payload={"file": f"selftest/{name}.py", "name": name,
|
|
41
|
+
"language": "python", "kind": "function", "source": src})
|
|
42
|
+
for name, src in rows]
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _label(item) -> str:
|
|
46
|
+
return item.id.split(":", 1)[0]
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def run_selftest(judge: Judge, out) -> int:
|
|
50
|
+
items = _items(POSITIVES, "pos") + _items(NEGATIVES, "neg")
|
|
51
|
+
scores, stats = judge.judge(items, SELFTEST_QUESTION, {"name": "selftest"})
|
|
52
|
+
pos_items = [i for i in items if _label(i) == "pos"]
|
|
53
|
+
neg_items = [i for i in items if _label(i) == "neg"]
|
|
54
|
+
pos = [scores[i.id] for i in pos_items if i.id in scores]
|
|
55
|
+
neg = [scores[i.id] for i in neg_items if i.id in scores]
|
|
56
|
+
unscored = sum(1 for i in items if i.id not in scores)
|
|
57
|
+
if not pos or not neg:
|
|
58
|
+
print("selftest: no scores returned" + (f" ({stats.last_error})" if stats.last_error else ""), file=out)
|
|
59
|
+
return 2
|
|
60
|
+
recall = sum(s >= ABOVE for s in pos) # unscored positives count as missed
|
|
61
|
+
fp = sum(s >= ABOVE for s in neg)
|
|
62
|
+
auroc = sum((p > n) + 0.5 * (p == n) for p in pos for n in neg) / (len(pos) * len(neg))
|
|
63
|
+
print(f"selftest question: {SELFTEST_QUESTION}", file=out)
|
|
64
|
+
print(f" positives n={len(pos_items)} recall@{ABOVE:.2f} = {recall}/{len(pos_items)} "
|
|
65
|
+
f"min {min(pos):.2f} mean {sum(pos)/len(pos):.2f} max {max(pos):.2f}", file=out)
|
|
66
|
+
print(f" negatives n={len(neg_items)} FP@{ABOVE:.2f} = {fp}/{len(neg_items)} "
|
|
67
|
+
f"min {min(neg):.2f} mean {sum(neg)/len(neg):.2f} max {max(neg):.2f}", file=out)
|
|
68
|
+
print(f" AUROC {auroc:.3f} (over {len(pos)}x{len(neg)} scored pairs) unscored {unscored} "
|
|
69
|
+
f"{stats.requests} requests, {stats.input_tokens:,} tokens", file=out)
|
|
70
|
+
for it in sorted(items, key=lambda i: -scores.get(i.id, -1.0)):
|
|
71
|
+
label, s = _label(it), scores.get(it.id)
|
|
72
|
+
flag = ""
|
|
73
|
+
if label == "pos" and (s is None or s < ABOVE):
|
|
74
|
+
flag = " <-- missed"
|
|
75
|
+
if label == "neg" and s is not None and s >= ABOVE:
|
|
76
|
+
flag = " <-- false positive"
|
|
77
|
+
shown = f"{s:.2f}" if s is not None else " n/a"
|
|
78
|
+
print(f" {label} {shown} {it.payload['name']}{flag}", file=out)
|
|
79
|
+
ok = auroc >= 0.9 and unscored == 0
|
|
80
|
+
if fp > 0:
|
|
81
|
+
print(f" note: {fp} negative(s) scored >= {ABOVE:.2f}; consider a higher --above", file=out)
|
|
82
|
+
print(" PASS" if ok else " FAIL - see rows above", file=out)
|
|
83
|
+
return 0 if ok else 2
|
every/symbols.py
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
"""Call-site extraction and the repo-wide symbol index."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections import defaultdict
|
|
6
|
+
|
|
7
|
+
# Node types that represent a call, per language.
|
|
8
|
+
CALL_NODES = {
|
|
9
|
+
"python": {"call"},
|
|
10
|
+
"javascript": {"call_expression", "new_expression"},
|
|
11
|
+
"typescript": {"call_expression", "new_expression"},
|
|
12
|
+
"tsx": {"call_expression", "new_expression"},
|
|
13
|
+
"go": {"call_expression"},
|
|
14
|
+
"java": {"method_invocation", "object_creation_expression"},
|
|
15
|
+
"rust": {"call_expression"},
|
|
16
|
+
"csharp": {"invocation_expression", "object_creation_expression"},
|
|
17
|
+
"ruby": {"call"},
|
|
18
|
+
"php": {"function_call_expression", "member_call_expression",
|
|
19
|
+
"scoped_call_expression", "object_creation_expression"},
|
|
20
|
+
}
|
|
21
|
+
# Field names that hold the callee expression on a call node, tried in order.
|
|
22
|
+
FUNC_FIELDS = ("function", "name", "method", "constructor", "type")
|
|
23
|
+
# Leaf node types that are identifiers.
|
|
24
|
+
IDENT_TYPES = {"identifier", "property_identifier", "field_identifier", "type_identifier",
|
|
25
|
+
"name", "constant", "simple_identifier"}
|
|
26
|
+
# Fields that point at the "last segment" of a dotted/member expression.
|
|
27
|
+
SUB_FIELDS = ("attribute", "property", "name", "field", "method")
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _text(node, src: bytes) -> str:
|
|
31
|
+
return src[node.start_byte:node.end_byte].decode("utf-8", "replace")
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _last_ident(node, src: bytes) -> str | None:
|
|
35
|
+
"""The identifier a call resolves to: `db.save` -> save, `pkg.Make` -> Make."""
|
|
36
|
+
if node is None:
|
|
37
|
+
return None
|
|
38
|
+
if node.type in IDENT_TYPES and node.child_count == 0:
|
|
39
|
+
return _text(node, src)
|
|
40
|
+
for f in SUB_FIELDS:
|
|
41
|
+
child = node.child_by_field_name(f)
|
|
42
|
+
if child is not None:
|
|
43
|
+
found = _last_ident(child, src)
|
|
44
|
+
if found:
|
|
45
|
+
return found
|
|
46
|
+
found = None
|
|
47
|
+
for child in node.children:
|
|
48
|
+
got = _last_ident(child, src)
|
|
49
|
+
if got:
|
|
50
|
+
found = got
|
|
51
|
+
return found
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def callee_names(node, lang: str, src: bytes) -> list[str]:
|
|
55
|
+
"""Unique callee names inside `node`, in source (preorder) order."""
|
|
56
|
+
call_types = CALL_NODES.get(lang)
|
|
57
|
+
if not call_types:
|
|
58
|
+
return []
|
|
59
|
+
out: list[str] = []
|
|
60
|
+
stack = [node]
|
|
61
|
+
while stack:
|
|
62
|
+
n = stack.pop()
|
|
63
|
+
if n.type in call_types:
|
|
64
|
+
for f in FUNC_FIELDS:
|
|
65
|
+
target = n.child_by_field_name(f)
|
|
66
|
+
if target is not None:
|
|
67
|
+
name = _last_ident(target, src)
|
|
68
|
+
if name and name not in out:
|
|
69
|
+
out.append(name)
|
|
70
|
+
break
|
|
71
|
+
stack.extend(reversed(n.children)) # reversed so the first child is popped first
|
|
72
|
+
return out
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
class SymbolIndex:
|
|
76
|
+
"""name -> definitions, plus reverse call edges, over one run's units."""
|
|
77
|
+
|
|
78
|
+
def __init__(self, units: list) -> None:
|
|
79
|
+
self.units = units
|
|
80
|
+
self.by_name: dict[str, list] = defaultdict(list)
|
|
81
|
+
for u in units:
|
|
82
|
+
if u.kind == "function":
|
|
83
|
+
self.by_name[u.name.split(".")[-1]].append(u)
|
|
84
|
+
self._callers: dict[str, list] = defaultdict(list)
|
|
85
|
+
seen: set = set()
|
|
86
|
+
for u in units:
|
|
87
|
+
if u.kind != "function":
|
|
88
|
+
continue
|
|
89
|
+
for name in u.calls:
|
|
90
|
+
for callee in self.by_name.get(name, []):
|
|
91
|
+
if callee is u or (callee.id, u.id) in seen:
|
|
92
|
+
continue
|
|
93
|
+
seen.add((callee.id, u.id))
|
|
94
|
+
self._callers[callee.id].append(u)
|
|
95
|
+
|
|
96
|
+
def callees_of(self, unit, limit: int = 8) -> list:
|
|
97
|
+
out, seen = [], {unit.id}
|
|
98
|
+
for name in unit.calls:
|
|
99
|
+
for d in self.by_name.get(name, []):
|
|
100
|
+
if d.id in seen:
|
|
101
|
+
continue
|
|
102
|
+
seen.add(d.id)
|
|
103
|
+
out.append(d)
|
|
104
|
+
if len(out) >= limit:
|
|
105
|
+
return out
|
|
106
|
+
return out
|
|
107
|
+
|
|
108
|
+
def callers_of(self, unit, limit: int = 8) -> list:
|
|
109
|
+
return self._callers.get(unit.id, [])[:limit]
|
every/units.py
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
"""Shared types and the token-estimate constants used by every module."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass, field
|
|
6
|
+
|
|
7
|
+
# Measured 2026-09-16 against jev-latest on code+JSON payloads (see reference/).
|
|
8
|
+
CHARS_PER_TOKEN = 2.3
|
|
9
|
+
PER_QUESTION_OVERHEAD = 80 # instructions + criteria + key, per question
|
|
10
|
+
REQUEST_FIXED_TOKENS = 262 # state + envelope, per request
|
|
11
|
+
OUTPUT_RESERVE_PER_Q = 25 # ~18 output tokens per noul answer, rounded up
|
|
12
|
+
USD_PER_INPUT_TOKEN = 0.042 / 1_000_000 # output tokens are free
|
|
13
|
+
DEFAULT_ABOVE = 0.75 # score at/above which a result is a hit. Set from the live
|
|
14
|
+
# selftest on 2026-09-16: positives 0.82-0.98, negatives 0.05-0.29
|
|
15
|
+
# with one borderline at 0.68; 0.75 splits the gap.
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def estimate_tokens(text: str) -> int:
|
|
19
|
+
"""Conservative input-token estimate for one embedded question."""
|
|
20
|
+
return int(len(text) / CHARS_PER_TOKEN) + PER_QUESTION_OVERHEAD
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@dataclass(eq=False)
|
|
24
|
+
class Unit:
|
|
25
|
+
"""One judgeable thing: a function/method, or a file chunk for unsupported languages."""
|
|
26
|
+
id: str # "<file>:<start_line>:<name>", unique within a run
|
|
27
|
+
file: str # path relative to the scanned root, forward slashes
|
|
28
|
+
lang: str
|
|
29
|
+
name: str # "outer.inner" for nested functions; "chunk@<line>" for chunks
|
|
30
|
+
start_line: int # 1-based, inclusive
|
|
31
|
+
end_line: int
|
|
32
|
+
source: str # possibly truncated with a trailing "[truncated]" marker
|
|
33
|
+
kind: str = "function" # "function" | "chunk"
|
|
34
|
+
calls: list = field(default_factory=list) # callee names (last identifier segment)
|