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/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)