code-jig 0.12.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.
- code_jig-0.12.0.dist-info/METADATA +154 -0
- code_jig-0.12.0.dist-info/RECORD +52 -0
- code_jig-0.12.0.dist-info/WHEEL +4 -0
- code_jig-0.12.0.dist-info/entry_points.txt +2 -0
- code_jig-0.12.0.dist-info/licenses/LICENSE +202 -0
- codejig/__init__.py +1 -0
- codejig/__main__.py +106 -0
- codejig/_core/VERSION +2 -0
- codejig/_core/bench/requirements.toml +4 -0
- codejig/_core/core/AGENTS.core.md +9 -0
- codejig/_core/core/agents/explorer.toml +24 -0
- codejig/_core/core/agents/judge.toml +19 -0
- codejig/_core/core/agents/reviewer.toml +10 -0
- codejig/_core/core/tiers.toml +4 -0
- codejig/_core/skills/bug-hunt/SKILL.md +131 -0
- codejig/_core/skills/code-analysis/SKILL.md +72 -0
- codejig/_core/skills/codegen/SKILL.md +46 -0
- codejig/_core/skills/cross-review/SKILL.md +32 -0
- codejig/_core/skills/grilling/SKILL.md +60 -0
- codejig/_core/skills/hld-trace/SKILL.md +69 -0
- codejig/_core/skills/improve/SKILL.md +38 -0
- codejig/_core/skills/redacao/SKILL.md +114 -0
- codejig/_core/skills/redacao/checar.py +75 -0
- codejig/_core/skills/redacao/reference.md +118 -0
- codejig/_core/skills/regression-tests/SKILL.md +53 -0
- codejig/_core/skills/remove-ai-attribution/SKILL.md +33 -0
- codejig/_core/skills/skill-validate/SKILL.md +58 -0
- codejig/_core/skills/web-consensus/SKILL.md +55 -0
- codejig/analyze.py +499 -0
- codejig/atomicwrite.py +49 -0
- codejig/collect.py +311 -0
- codejig/doctor.py +370 -0
- codejig/evidence.py +418 -0
- codejig/feedback.py +165 -0
- codejig/gitgate.py +278 -0
- codejig/hooks.py +64 -0
- codejig/humanize.py +206 -0
- codejig/install.py +381 -0
- codejig/managed.py +64 -0
- codejig/migrate.py +515 -0
- codejig/overlay.py +43 -0
- codejig/paths.py +40 -0
- codejig/probe.py +136 -0
- codejig/review.py +73 -0
- codejig/scrub.py +255 -0
- codejig/skillcheck.py +400 -0
- codejig/skilllint.py +99 -0
- codejig/sources.py +248 -0
- codejig/tiers.py +63 -0
- codejig/tokens.py +118 -0
- codejig/trace.py +137 -0
- codejig/watermark.py +147 -0
codejig/sources.py
ADDED
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
import contextlib
|
|
2
|
+
import errno
|
|
3
|
+
import glob
|
|
4
|
+
import http.client
|
|
5
|
+
import ipaddress
|
|
6
|
+
import json
|
|
7
|
+
import os
|
|
8
|
+
import re
|
|
9
|
+
import socket
|
|
10
|
+
import sys
|
|
11
|
+
import urllib.error
|
|
12
|
+
import urllib.request
|
|
13
|
+
from concurrent.futures import ThreadPoolExecutor
|
|
14
|
+
from dataclasses import dataclass
|
|
15
|
+
from html.parser import HTMLParser
|
|
16
|
+
|
|
17
|
+
MAX_BYTES = 3_000_000
|
|
18
|
+
TIMEOUT = 20
|
|
19
|
+
USER_AGENT = "Mozilla/5.0 (compatible; code-jig check-sources)"
|
|
20
|
+
BLOCKED = {401, 403, 429, 451}
|
|
21
|
+
SKIP_TAGS = {"script", "style", "noscript", "template"}
|
|
22
|
+
TEXT_TYPES = ("text/", "application/xhtml", "application/xml", "application/json")
|
|
23
|
+
FAILING = {"dead", "quote-missing", "bad-url", "no-quote", "malformed"}
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@dataclass
|
|
27
|
+
class Claim:
|
|
28
|
+
where: str
|
|
29
|
+
claim: str = ""
|
|
30
|
+
url: str = ""
|
|
31
|
+
quote: str = ""
|
|
32
|
+
verdict: str = ""
|
|
33
|
+
detail: str = ""
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class _Text(HTMLParser):
|
|
37
|
+
def __init__(self):
|
|
38
|
+
super().__init__(convert_charrefs=True)
|
|
39
|
+
self.parts, self.skip = [], 0
|
|
40
|
+
|
|
41
|
+
def handle_starttag(self, tag, _attrs):
|
|
42
|
+
self.skip += tag in SKIP_TAGS
|
|
43
|
+
self.parts.append(" ")
|
|
44
|
+
|
|
45
|
+
def handle_endtag(self, tag):
|
|
46
|
+
self.skip -= tag in SKIP_TAGS and self.skip > 0
|
|
47
|
+
self.parts.append(" ")
|
|
48
|
+
|
|
49
|
+
def handle_data(self, data):
|
|
50
|
+
if not self.skip:
|
|
51
|
+
self.parts.append(data)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def normalize(text: str) -> str:
|
|
55
|
+
return " ".join(re.sub(r"[\W_]+", " ", text.casefold()).split())
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def page_text(body: bytes, ctype: str) -> str:
|
|
59
|
+
charset = re.search(r"charset=([\w-]+)", ctype)
|
|
60
|
+
text = body.decode(charset.group(1) if charset else "utf-8", errors="replace")
|
|
61
|
+
if "html" in ctype or "xml" in ctype:
|
|
62
|
+
parser = _Text()
|
|
63
|
+
try:
|
|
64
|
+
parser.feed(text)
|
|
65
|
+
except AssertionError:
|
|
66
|
+
pass
|
|
67
|
+
text = "".join(parser.parts)
|
|
68
|
+
return normalize(text)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def is_public_ip(ip: str) -> bool:
|
|
72
|
+
return ipaddress.ip_address(ip.split("%")[0]).is_global
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def is_public(host: str) -> bool | None:
|
|
76
|
+
"""True when every address the host resolves to is public, False when any is not,
|
|
77
|
+
None when it does not resolve here."""
|
|
78
|
+
try:
|
|
79
|
+
infos = socket.getaddrinfo(host, None, proto=socket.IPPROTO_TCP)
|
|
80
|
+
except (OSError, UnicodeError):
|
|
81
|
+
return None
|
|
82
|
+
return all(is_public_ip(info[4][0]) for info in infos)
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def _target(url: str) -> tuple[str, str] | None:
|
|
86
|
+
"""(scheme, host) exactly as urllib and http.client will parse them for the connection."""
|
|
87
|
+
try:
|
|
88
|
+
req = urllib.request.Request(url)
|
|
89
|
+
return req.type.lower(), http.client.HTTPConnection(req.host).host
|
|
90
|
+
except (ValueError, http.client.InvalidURL):
|
|
91
|
+
return None
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def refused(url: str) -> str | None:
|
|
95
|
+
"""Why `url` must not be requested, or None. Through a proxy, a host that does not resolve here
|
|
96
|
+
is refused: the proxy may resolve internal names this machine cannot."""
|
|
97
|
+
target = _target(url)
|
|
98
|
+
if target is None or target[0] not in ("http", "https") or not target[1]:
|
|
99
|
+
return "only http and https are checked"
|
|
100
|
+
public = is_public(target[1])
|
|
101
|
+
if public is False:
|
|
102
|
+
return f"{target[1]} is a private or local address"
|
|
103
|
+
if public is None and proxied(url):
|
|
104
|
+
return f"cannot confirm {target[1]} is public: it does not resolve here and a proxy would fetch it"
|
|
105
|
+
return None
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def proxied(url: str) -> bool:
|
|
109
|
+
target = _target(url)
|
|
110
|
+
proxies = urllib.request.getproxies()
|
|
111
|
+
return bool(target) and target[0] in proxies and not urllib.request.proxy_bypass(target[1])
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def _check_peer(conn: http.client.HTTPConnection) -> None:
|
|
115
|
+
if conn._tunnel_host:
|
|
116
|
+
return
|
|
117
|
+
ip = conn.sock.getpeername()[0]
|
|
118
|
+
if not is_public_ip(ip):
|
|
119
|
+
conn.close()
|
|
120
|
+
raise OSError(f"refused: {ip} is a private or local address")
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
class _HTTPConnection(http.client.HTTPConnection):
|
|
124
|
+
def connect(self):
|
|
125
|
+
super().connect()
|
|
126
|
+
_check_peer(self)
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
class _HTTPSConnection(http.client.HTTPSConnection):
|
|
130
|
+
def connect(self):
|
|
131
|
+
super().connect()
|
|
132
|
+
_check_peer(self)
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
class _HTTPHandler(urllib.request.HTTPHandler):
|
|
136
|
+
def http_open(self, req):
|
|
137
|
+
return self.do_open(_HTTPConnection, req)
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
class _HTTPSHandler(urllib.request.HTTPSHandler):
|
|
141
|
+
def https_open(self, req):
|
|
142
|
+
return self.do_open(_HTTPSConnection, req, context=self._context)
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
class _Redirects(urllib.request.HTTPRedirectHandler):
|
|
146
|
+
def redirect_request(self, req, fp, code, msg, headers, newurl):
|
|
147
|
+
reason = refused(newurl)
|
|
148
|
+
if reason:
|
|
149
|
+
raise urllib.error.URLError(f"redirect refused: {reason}")
|
|
150
|
+
return super().redirect_request(req, fp, code, msg, headers, newurl)
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
DIRECT = urllib.request.build_opener(urllib.request.ProxyHandler({}), _HTTPHandler, _HTTPSHandler, _Redirects)
|
|
154
|
+
VIA_PROXY = urllib.request.build_opener(_Redirects)
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def fetch(url: str) -> tuple[str, str]:
|
|
158
|
+
"""(verdict, page text or detail) for one URL. Only public http and https addresses are requested,
|
|
159
|
+
redirects included; without a proxy, the address the socket actually connected to is checked too."""
|
|
160
|
+
reason = refused(url)
|
|
161
|
+
if reason:
|
|
162
|
+
return "bad-url", reason
|
|
163
|
+
req = urllib.request.Request(url, headers={"User-Agent": USER_AGENT, "Accept": "text/html,text/plain;q=0.9,*/*;q=0.5"})
|
|
164
|
+
try:
|
|
165
|
+
with (VIA_PROXY if proxied(url) else DIRECT).open(req, timeout=TIMEOUT) as r:
|
|
166
|
+
ctype = r.headers.get("Content-Type", "")
|
|
167
|
+
if not ctype.startswith(TEXT_TYPES):
|
|
168
|
+
return "unreadable", ctype or "unknown type"
|
|
169
|
+
return "ok", page_text(r.read(MAX_BYTES), ctype)
|
|
170
|
+
except urllib.error.HTTPError as e:
|
|
171
|
+
e.close()
|
|
172
|
+
return ("blocked" if e.code in BLOCKED else "dead"), f"HTTP {e.code}"
|
|
173
|
+
except (urllib.error.URLError, OSError, ValueError) as e:
|
|
174
|
+
return "dead", str(getattr(e, "reason", e))
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def expand(patterns: list[str]) -> list[str]:
|
|
178
|
+
"""File names with shell patterns expanded here, so PowerShell and cmd behave like a POSIX shell."""
|
|
179
|
+
out = []
|
|
180
|
+
for p in patterns:
|
|
181
|
+
hits = (sorted(glob.glob(p)) or ([p] if os.path.exists(p) else [])) if glob.has_magic(p) else [p]
|
|
182
|
+
if not hits:
|
|
183
|
+
raise FileNotFoundError(errno.ENOENT, os.strerror(errno.ENOENT), p)
|
|
184
|
+
out += hits
|
|
185
|
+
return out
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def _pre_verdict(c: Claim) -> None:
|
|
189
|
+
if not c.url:
|
|
190
|
+
c.verdict, c.detail = "malformed", "no url"
|
|
191
|
+
elif not c.url.lower().startswith(("http://", "https://")):
|
|
192
|
+
c.verdict, c.detail = "bad-url", "only http and https are checked"
|
|
193
|
+
elif not normalize(c.quote):
|
|
194
|
+
c.verdict, c.detail = "no-quote", "a claim needs an exact quote with words in it"
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
def _parse_line(where: str, line: str) -> Claim:
|
|
198
|
+
c = Claim(where)
|
|
199
|
+
try:
|
|
200
|
+
row = json.loads(line)
|
|
201
|
+
c.claim, c.url, c.quote = (str(row.get(k) or "").strip() for k in ("claim", "url", "quote"))
|
|
202
|
+
except (ValueError, AttributeError):
|
|
203
|
+
c.verdict, c.detail = "malformed", "not a JSON object"
|
|
204
|
+
return c
|
|
205
|
+
_pre_verdict(c)
|
|
206
|
+
return c
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
def load(files: list[str]) -> list[Claim]:
|
|
210
|
+
claims = []
|
|
211
|
+
for name in expand(files):
|
|
212
|
+
with contextlib.nullcontext(sys.stdin) if name == "-" else open(name, encoding="utf-8-sig") as f:
|
|
213
|
+
claims += [_parse_line(f"{name}:{n}", line) for n, line in enumerate(f, 1) if line.strip()]
|
|
214
|
+
return claims
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
def check(claims: list[Claim]) -> list[Claim]:
|
|
218
|
+
todo = [c for c in claims if not c.verdict]
|
|
219
|
+
urls = list(dict.fromkeys(c.url for c in todo))
|
|
220
|
+
with ThreadPoolExecutor(max_workers=8) as pool:
|
|
221
|
+
pages = dict(zip(urls, pool.map(fetch, urls), strict=True))
|
|
222
|
+
for c in todo:
|
|
223
|
+
verdict, text = pages[c.url]
|
|
224
|
+
if verdict != "ok":
|
|
225
|
+
c.verdict, c.detail = verdict, text
|
|
226
|
+
elif normalize(c.quote) in text:
|
|
227
|
+
c.verdict = "ok"
|
|
228
|
+
else:
|
|
229
|
+
c.verdict, c.detail = "quote-missing", "the page does not contain the quote"
|
|
230
|
+
return claims
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
def cli(args) -> int:
|
|
234
|
+
if os.environ.get("CODEX_SANDBOX_NETWORK_DISABLED") == "1":
|
|
235
|
+
print("code-jig: network is disabled in this sandbox; run check-sources with escalation", file=sys.stderr)
|
|
236
|
+
return 2
|
|
237
|
+
try:
|
|
238
|
+
claims = check(load(args.files))
|
|
239
|
+
except OSError as e:
|
|
240
|
+
print(f"code-jig: {e}", file=sys.stderr)
|
|
241
|
+
return 2
|
|
242
|
+
counts: dict[str, int] = {}
|
|
243
|
+
for c in claims:
|
|
244
|
+
counts[c.verdict] = counts.get(c.verdict, 0) + 1
|
|
245
|
+
note = f" ({c.detail})" if c.detail else ""
|
|
246
|
+
print(f"{c.verdict:13} {c.where} {c.claim[:70]} {c.url}{note}")
|
|
247
|
+
print(f"{len(claims)} claims: " + ", ".join(f"{n} {v}" for v, n in sorted(counts.items(), key=lambda kv: kv[0] != "ok")))
|
|
248
|
+
return 1 if FAILING & counts.keys() else 0
|
codejig/tiers.py
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import tomllib
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
@dataclass(frozen=True)
|
|
8
|
+
class Tier:
|
|
9
|
+
name: str
|
|
10
|
+
model: str
|
|
11
|
+
effort: str
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def read_toml(path: Path) -> dict:
|
|
15
|
+
try:
|
|
16
|
+
return tomllib.loads(path.read_text(encoding="utf-8-sig"))
|
|
17
|
+
except FileNotFoundError:
|
|
18
|
+
raise ValueError(f"{path}: not found") from None
|
|
19
|
+
except (tomllib.TOMLDecodeError, UnicodeDecodeError) as e:
|
|
20
|
+
raise ValueError(f"{path}: {e}") from e
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _strings(path: Path, data: dict) -> dict:
|
|
24
|
+
bad = [k for k, v in data.items() if not isinstance(v, str) or not v.strip()]
|
|
25
|
+
if bad:
|
|
26
|
+
raise ValueError(f"{path}: {', '.join(bad)} must be a non-empty string")
|
|
27
|
+
return data
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def load(core: Path, overlay_root: Path) -> list[Tier]:
|
|
31
|
+
efforts_path = core / "core" / "tiers.toml"
|
|
32
|
+
models_path = overlay_root / "profiles.toml"
|
|
33
|
+
efforts = _strings(efforts_path, read_toml(efforts_path))
|
|
34
|
+
models = _strings(models_path, read_toml(models_path))
|
|
35
|
+
missing = [t for t in efforts if t not in models]
|
|
36
|
+
if missing:
|
|
37
|
+
raise ValueError(f"{models_path}: no model for tier {', '.join(missing)}")
|
|
38
|
+
unknown = [t for t in models if t not in efforts]
|
|
39
|
+
if unknown:
|
|
40
|
+
raise ValueError(f"{models_path}: unknown tier {', '.join(unknown)}; tiers are {', '.join(efforts)}")
|
|
41
|
+
return [Tier(name, models[name], effort) for name, effort in efforts.items()]
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def extra_policy(overlay_root: Path) -> str:
|
|
45
|
+
path = overlay_root / "guardian-policy.md"
|
|
46
|
+
return path.read_text(encoding="utf-8-sig").strip() if path.is_file() else ""
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def toml_str(s: str) -> str:
|
|
50
|
+
return json.dumps(s, ensure_ascii=False).replace("\x7f", "\\u007f")
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def profile_body(tier: Tier, policy: str, compact: int | None = None) -> str:
|
|
54
|
+
lines = [
|
|
55
|
+
f"model = {toml_str(tier.model)}",
|
|
56
|
+
f"model_reasoning_effort = {toml_str(tier.effort)}",
|
|
57
|
+
'approvals_reviewer = "auto_review"',
|
|
58
|
+
]
|
|
59
|
+
if compact is not None:
|
|
60
|
+
lines.append(f"model_auto_compact_token_limit = {compact}")
|
|
61
|
+
if policy:
|
|
62
|
+
lines += ["", "[auto_review]", f"extra_policy = {toml_str(policy)}"]
|
|
63
|
+
return "\n".join(lines) + "\n"
|
codejig/tokens.py
ADDED
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import sqlite3
|
|
2
|
+
import sys
|
|
3
|
+
from collections import Counter, defaultdict
|
|
4
|
+
from datetime import UTC, datetime, timedelta
|
|
5
|
+
|
|
6
|
+
from codejig import collect
|
|
7
|
+
|
|
8
|
+
TWO_WORD = {"git", "npm", "pnpm", "yarn", "cargo", "go", "uv", "dotnet", "docker", "kubectl", "gh"}
|
|
9
|
+
POLL = ("sleep", "wait")
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def prefix(cmd: str) -> str:
|
|
13
|
+
words = [w for w in cmd.split() if not ("=" in w and not w.startswith("-") and w.split("=", 1)[0].isidentifier())]
|
|
14
|
+
if not words:
|
|
15
|
+
return ""
|
|
16
|
+
first = words[0].replace("\\", "/").rsplit("/", 1)[-1]
|
|
17
|
+
if first in TWO_WORD and len(words) > 1 and not words[1].startswith("-"):
|
|
18
|
+
return f"{first} {words[1]}"
|
|
19
|
+
return first
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _tally(rows: list, comps: Counter) -> tuple[dict, dict, dict, set]:
|
|
23
|
+
by_kind: dict[str, int] = defaultdict(int)
|
|
24
|
+
models: dict[str, list[int]] = defaultdict(lambda: [0, 0])
|
|
25
|
+
sessions: dict[str, dict] = {}
|
|
26
|
+
reviews = set()
|
|
27
|
+
for thread, kind, parent, turn, model, inp, cached, out in rows:
|
|
28
|
+
by_kind[kind] += inp
|
|
29
|
+
models[model or "(unknown)"][0] += inp
|
|
30
|
+
models[model or "(unknown)"][1] += out
|
|
31
|
+
owner = parent if kind != "main" and parent else thread
|
|
32
|
+
s = sessions.setdefault(owner, {"id": owner, "turns": 0, "input": 0, "cached": 0, "guardian_input": 0,
|
|
33
|
+
"subagent_input": 0, "compactions": comps.get(owner, 0), "polling": 0})
|
|
34
|
+
if kind == "guardian":
|
|
35
|
+
s["guardian_input"] += inp
|
|
36
|
+
reviews.add((thread, turn))
|
|
37
|
+
elif kind == "subagent":
|
|
38
|
+
s["subagent_input"] += inp
|
|
39
|
+
else:
|
|
40
|
+
s["input"] += inp
|
|
41
|
+
s["cached"] += cached
|
|
42
|
+
s["turns"] += 1
|
|
43
|
+
return by_kind, models, sessions, reviews
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _count_polling(calls: list, sessions: dict) -> None:
|
|
47
|
+
for thread, cmd, _ in calls:
|
|
48
|
+
if thread in sessions and cmd.lstrip().split(" ", 1)[0] in POLL:
|
|
49
|
+
sessions[thread]["polling"] += 1
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def summary(con: sqlite3.Connection, since: str, top: int = 10, long_turns: int = 300) -> dict:
|
|
53
|
+
rows = con.execute("SELECT thread_id, kind, parent_id, turn_id, model, input, cached, output FROM turn_usage WHERE ts >= ?", (since,)).fetchall()
|
|
54
|
+
calls = con.execute("SELECT thread_id, cmd, escalated FROM calls WHERE ts >= ?", (since,)).fetchall()
|
|
55
|
+
comps = Counter(t for (t,) in con.execute("SELECT thread_id FROM compactions WHERE ts >= ?", (since,)))
|
|
56
|
+
by_kind, models, sessions, reviews = _tally(rows, comps)
|
|
57
|
+
_count_polling(calls, sessions)
|
|
58
|
+
total = sum(by_kind.values())
|
|
59
|
+
guardian = by_kind.get("guardian", 0)
|
|
60
|
+
ranked = sorted(sessions.values(), key=lambda s: s["input"] + s["guardian_input"] + s["subagent_input"], reverse=True)
|
|
61
|
+
return {
|
|
62
|
+
"input": total,
|
|
63
|
+
"cached": sum(r[6] for r in rows),
|
|
64
|
+
"output": sum(r[7] for r in rows),
|
|
65
|
+
"by_kind": dict(by_kind),
|
|
66
|
+
"guardian_share": guardian / total if total else 0.0,
|
|
67
|
+
"reviews": len(reviews),
|
|
68
|
+
"input_per_review": guardian / len(reviews) if reviews else 0,
|
|
69
|
+
"escalations": Counter(prefix(c) for _, c, e in calls if e).most_common(),
|
|
70
|
+
"sessions": ranked[:top],
|
|
71
|
+
"over_long": sum(1 for s in sessions.values() if s["turns"] > long_turns),
|
|
72
|
+
"models": sorted(((m, v[0], v[1]) for m, v in models.items()), key=lambda r: r[1], reverse=True),
|
|
73
|
+
"quarantined": con.execute("SELECT COUNT(*) FROM quarantine").fetchone()[0],
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def _safe(text: str) -> str:
|
|
78
|
+
return "".join(ch if ch.isprintable() else f"\\x{ord(ch):02x}" for ch in text)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def _pct(a: int, b: int) -> str:
|
|
82
|
+
return f"{100 * a / b:.1f}%" if b else "-"
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def render(s: dict, floor: float) -> list[str]:
|
|
86
|
+
out = [
|
|
87
|
+
f"Input {s['input']:,} cached {_pct(s['cached'], s['input'])} output {s['output']:,}",
|
|
88
|
+
f"Guardian share of input: {100 * s['guardian_share']:.1f}% reviews {s['reviews']} input per review {s['input_per_review']:,.0f}",
|
|
89
|
+
f"Sessions over 300 model requests: {s['over_long']}",
|
|
90
|
+
"",
|
|
91
|
+
"Top sessions (own input, cache hit, model requests, compactions, polling calls, Guardian input, subagent input):",
|
|
92
|
+
]
|
|
93
|
+
for x in s["sessions"]:
|
|
94
|
+
hit = 100 * x["cached"] / x["input"] if x["input"] else 100.0
|
|
95
|
+
flag = " LOW" if x["input"] and hit < floor else ""
|
|
96
|
+
out.append(f" {_safe(x['id'])} {x['input']:,} {hit:.1f}%{flag} {x['turns']} {x['compactions']} {x['polling']} {x['guardian_input']:,} {x['subagent_input']:,}")
|
|
97
|
+
out += ["", "Escalations sent to Guardian, by command:"]
|
|
98
|
+
out += [f" {_safe(p) or '(empty)'} {n}" for p, n in s["escalations"]] or [" none"]
|
|
99
|
+
out += ["", "By model (input, output):"]
|
|
100
|
+
out += [f" {_safe(m)} {i:,} {o:,}" for m, i, o in s["models"]] or [" none"]
|
|
101
|
+
if s["quarantined"]:
|
|
102
|
+
out += ["", f"warning: {s['quarantined']} rollout lines quarantined; see the quarantine table"]
|
|
103
|
+
return out
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def cli(args) -> int:
|
|
107
|
+
path = collect.db_path()
|
|
108
|
+
if not path.is_file():
|
|
109
|
+
print(f"code-jig: {path} not found; run code-jig collect", file=sys.stderr)
|
|
110
|
+
return 2
|
|
111
|
+
con = collect.connect(path)
|
|
112
|
+
try:
|
|
113
|
+
since = (datetime.now(UTC) - timedelta(days=args.days)).strftime("%Y-%m-%dT%H:%M:%SZ")
|
|
114
|
+
for line in render(summary(con, since, args.top), args.cache_floor):
|
|
115
|
+
print(line)
|
|
116
|
+
finally:
|
|
117
|
+
con.close()
|
|
118
|
+
return 0
|
codejig/trace.py
ADDED
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import codecs
|
|
4
|
+
import json
|
|
5
|
+
import re
|
|
6
|
+
import sys
|
|
7
|
+
from dataclasses import dataclass
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
from codejig.analyze import printable, source_files, utf8_stdout
|
|
11
|
+
|
|
12
|
+
TEST_PATH = re.compile(
|
|
13
|
+
r"(^|/)(tests?|__tests__|specs?)/"
|
|
14
|
+
r"|\.(test|spec)\.[cm]?[jt]sx?$"
|
|
15
|
+
r"|(^|/)test_[^/]*\.py$"
|
|
16
|
+
r"|_test\.py$"
|
|
17
|
+
r"|Tests?\.cs$"
|
|
18
|
+
r"|(^|/)[^/]*\.Tests?/"
|
|
19
|
+
)
|
|
20
|
+
ID_CHARS = r"[A-Za-z0-9_]+(?:[.-][A-Za-z0-9_]+)*"
|
|
21
|
+
MAX_BYTES = 2_000_000
|
|
22
|
+
LINE_ID = re.compile(rf"^\s*(?:[-*+]\s+|\d+[.)]\s+)?#?(?:REQ-)?({ID_CHARS})", re.IGNORECASE)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@dataclass(frozen=True)
|
|
26
|
+
class Result:
|
|
27
|
+
covered: dict[str, list[str]]
|
|
28
|
+
missing: list[str]
|
|
29
|
+
unknown: dict[str, list[str]]
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _norm(value) -> str:
|
|
33
|
+
text = str(value).strip().lstrip("#")
|
|
34
|
+
return text[4:] if text.lower().startswith("req-") else text
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _json_ids(data) -> list:
|
|
38
|
+
if isinstance(data, dict):
|
|
39
|
+
data = data.get("workItems") or data.get("value") or ([data] if "id" in data else [])
|
|
40
|
+
if isinstance(data, (int, str)) and not isinstance(data, bool):
|
|
41
|
+
return [data]
|
|
42
|
+
if not isinstance(data, list):
|
|
43
|
+
return []
|
|
44
|
+
return [item.get("id") if isinstance(item, dict) else item for item in data if not isinstance(item, (list, bool))]
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def read_ids(text: str) -> list[str]:
|
|
48
|
+
text = text.lstrip("\ufeff")
|
|
49
|
+
try:
|
|
50
|
+
raw = _json_ids(json.loads(text))
|
|
51
|
+
except ValueError:
|
|
52
|
+
if text.lstrip().startswith(("[", "{")):
|
|
53
|
+
raise ValueError("looks like JSON but does not parse") from None
|
|
54
|
+
raw = [m.group(1) for m in map(LINE_ID.match, text.splitlines()) if m]
|
|
55
|
+
ids = [_norm(v) for v in raw if v is not None and _norm(v)]
|
|
56
|
+
return list(dict.fromkeys(ids))
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _regex(pattern: str, id_part: str) -> re.Pattern:
|
|
60
|
+
if "{id}" not in pattern:
|
|
61
|
+
raise ValueError("pattern must contain {id}")
|
|
62
|
+
head, _, tail = pattern.partition("{id}")
|
|
63
|
+
return re.compile(rf"(?<![\w-]){re.escape(head)}{id_part}{re.escape(tail)}(?!\w|[.-]\w)", re.IGNORECASE)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def trace(root: Path, ids: list[str], pattern: str = "REQ-{id}") -> Result:
|
|
67
|
+
wanted = {i.lower(): i for i in ids}
|
|
68
|
+
finder = _regex(pattern, f"({ID_CHARS})")
|
|
69
|
+
covered: dict[str, list[str]] = {}
|
|
70
|
+
unknown: dict[str, list[str]] = {}
|
|
71
|
+
for name in source_files(root):
|
|
72
|
+
if not TEST_PATH.search(name):
|
|
73
|
+
continue
|
|
74
|
+
f = root / name
|
|
75
|
+
if f.stat().st_size > MAX_BYTES:
|
|
76
|
+
continue
|
|
77
|
+
text = f.read_text(encoding="utf-8", errors="replace")
|
|
78
|
+
for found in dict.fromkeys(m.group(1) for m in finder.finditer(text)):
|
|
79
|
+
target = covered if found.lower() in wanted else unknown
|
|
80
|
+
key = wanted.get(found.lower(), found)
|
|
81
|
+
if name not in target.setdefault(key, []):
|
|
82
|
+
target[key].append(name)
|
|
83
|
+
ordered = {i: covered[i] for i in ids if i in covered}
|
|
84
|
+
return Result(ordered, [i for i in ids if i not in covered], dict(sorted(unknown.items())))
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def _files(names: list[str], limit: int = 3) -> str:
|
|
88
|
+
extra = f" and {len(names) - limit} more" if len(names) > limit else ""
|
|
89
|
+
return printable(", ".join(names[:limit]) + extra)
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def render(result: Result) -> list[str]:
|
|
93
|
+
total = len(result.covered) + len(result.missing)
|
|
94
|
+
out = [f"trace: {len(result.covered)} of {total} requirements have tests"]
|
|
95
|
+
out += [f" {printable(i)} {_files(names)}" for i, names in result.covered.items()]
|
|
96
|
+
if result.missing:
|
|
97
|
+
out.append(f"missing: {printable(', '.join(result.missing))}")
|
|
98
|
+
out += [f"unknown in tests: {printable(i)} ({_files(names)})" for i, names in result.unknown.items()]
|
|
99
|
+
return out
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def decode(data: bytes) -> str:
|
|
103
|
+
if data.startswith((codecs.BOM_UTF16_LE, codecs.BOM_UTF16_BE)):
|
|
104
|
+
return data.decode("utf-16")
|
|
105
|
+
return data.decode("utf-8-sig")
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def _read(name: str) -> str:
|
|
109
|
+
if name != "-":
|
|
110
|
+
return decode(Path(name).read_bytes())
|
|
111
|
+
return decode(sys.stdin.buffer.read()) if hasattr(sys.stdin, "buffer") else sys.stdin.read()
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def cli(args) -> int:
|
|
115
|
+
utf8_stdout()
|
|
116
|
+
try:
|
|
117
|
+
ids = read_ids(_read(args.ids))
|
|
118
|
+
except OSError as e:
|
|
119
|
+
print(f"code-jig: {args.ids}: {e.strerror or e}", file=sys.stderr)
|
|
120
|
+
return 2
|
|
121
|
+
except ValueError as e:
|
|
122
|
+
print(f"code-jig: {args.ids}: {e}", file=sys.stderr)
|
|
123
|
+
return 2
|
|
124
|
+
if not ids:
|
|
125
|
+
print(f"code-jig: no requirement IDs in {args.ids}", file=sys.stderr)
|
|
126
|
+
return 2
|
|
127
|
+
try:
|
|
128
|
+
result = trace(Path(args.path).resolve(), ids, args.pattern)
|
|
129
|
+
except (ValueError, re.error) as e:
|
|
130
|
+
print(f"code-jig: --pattern: {e}", file=sys.stderr)
|
|
131
|
+
return 2
|
|
132
|
+
if args.json:
|
|
133
|
+
print(json.dumps({"covered": result.covered, "missing": result.missing, "unknown": result.unknown}, ensure_ascii=False, indent=1))
|
|
134
|
+
else:
|
|
135
|
+
for line in render(result):
|
|
136
|
+
print(line)
|
|
137
|
+
return 1 if result.missing or result.unknown else 0
|