align-agent 0.4.3__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.
- align_agent/__init__.py +11 -0
- align_agent/_audit/common.py +58 -0
- align_agent/_audit/deepswe.py +260 -0
- align_agent/_audit/excerpts.py +203 -0
- align_agent/_audit/extract.py +798 -0
- align_agent/_audit/materialize.py +624 -0
- align_agent/_audit/score.py +325 -0
- align_agent/_audit/shellparse.py +1141 -0
- align_agent/_audit/tarsur.py +153 -0
- align_agent/_vendor/src/__init__.py +3 -0
- align_agent/_vendor/src/harness/__init__.py +13 -0
- align_agent/_vendor/src/harness/arms.py +524 -0
- align_agent/_vendor/src/harness/completion.py +104 -0
- align_agent/_vendor/src/harness/dependency_notice.py +616 -0
- align_agent/_vendor/src/harness/freshness_hook.py +131 -0
- align_agent/_vendor/src/harness/lean.py +41 -0
- align_agent/_vendor/src/harness/mini_real.py +1040 -0
- align_agent/_vendor/src/harness/miniswe_scaffold.py +661 -0
- align_agent/_vendor/src/harness/tail_diff.py +292 -0
- align_agent/_vendor/src/harness/viewport.py +220 -0
- align_agent/_vendor/src/metrics/__init__.py +38 -0
- align_agent/_vendor/src/metrics/coherence.py +354 -0
- align_agent/_vendor/src/metrics/corruption_oracles.py +139 -0
- align_agent/_vendor/src/metrics/replay.py +399 -0
- align_agent/_vendor/src/metrics/statistics.py +185 -0
- align_agent/_vendor/src/metrics/verification.py +104 -0
- align_agent/_vendor/src/parsers/__init__.py +33 -0
- align_agent/_vendor/src/parsers/base_parser.py +349 -0
- align_agent/_vendor/src/parsers/file_events.py +806 -0
- align_agent/_vendor/src/parsers/miniswe_parser.py +96 -0
- align_agent/_vendor/src/parsers/openhands_parser.py +414 -0
- align_agent/_vendor/src/parsers/sweagent_parser.py +180 -0
- align_agent/_vendor/src/parsers/terminus_parser.py +106 -0
- align_agent/cli.py +101 -0
- align_agent/core.py +161 -0
- align_agent/run.py +84 -0
- align_agent/sessions.py +985 -0
- align_agent/writethrough/__init__.py +4 -0
- align_agent/writethrough/_lib/__init__.py +1 -0
- align_agent/writethrough/_lib/wt_src/__init__.py +3 -0
- align_agent/writethrough/_lib/wt_src/harness/__init__.py +1 -0
- align_agent/writethrough/_lib/wt_src/harness/dependency_notice.py +616 -0
- align_agent/writethrough/_lib/wt_src/harness/lean.py +41 -0
- align_agent/writethrough/_lib/wt_src/harness/tail_diff.py +292 -0
- align_agent/writethrough/_lib/wt_src/harness/viewport.py +220 -0
- align_agent/writethrough/_lib/wt_src/metrics/__init__.py +38 -0
- align_agent/writethrough/_lib/wt_src/metrics/replay.py +399 -0
- align_agent/writethrough/_lib/wt_src/parsers/__init__.py +1 -0
- align_agent/writethrough/_lib/wt_src/parsers/file_events.py +806 -0
- align_agent/writethrough/claude_code.py +81 -0
- align_agent/writethrough/engine.py +647 -0
- align_agent/writethrough/hooks.py +227 -0
- align_agent/writethrough/mcp_server.py +125 -0
- align_agent/writethrough/protocol.py +68 -0
- align_agent-0.4.3.dist-info/METADATA +224 -0
- align_agent-0.4.3.dist-info/RECORD +60 -0
- align_agent-0.4.3.dist-info/WHEEL +5 -0
- align_agent-0.4.3.dist-info/entry_points.txt +6 -0
- align_agent-0.4.3.dist-info/licenses/LICENSE +21 -0
- align_agent-0.4.3.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,624 @@
|
|
|
1
|
+
"""Automated materialization classifier for structural hazards (refined protocol). Usage: python3 scripts/audit/materialize.py
|
|
2
|
+
|
|
3
|
+
For each hazard E (an anchored edit composed against a view that is no longer
|
|
4
|
+
current) on file f, with W = the writes to f between the agent's last view of
|
|
5
|
+
f and E, it rebuilds what can be known of:
|
|
6
|
+
V the text the agent last saw of f (all views of f's last-seen version)
|
|
7
|
+
B the believed state: V plus the agent's own later writes as commanded
|
|
8
|
+
(minus any earlier write that E simply re-issues)
|
|
9
|
+
R the real state: B unless a write in W failed or did not do what was
|
|
10
|
+
commanded (no exact oracle exists: OpenHands' logged old_content is
|
|
11
|
+
the old_str argument, not the file)
|
|
12
|
+
and assigns the first class that fires:
|
|
13
|
+
1 SPURIOUS no write in W changed f (static no-op or failed)
|
|
14
|
+
2 FAILED-UNRELATED E did nothing for reasons unrelated to content, or its
|
|
15
|
+
anchor was not in any text of f the agent had seen or written
|
|
16
|
+
2b FAILED-OWN-WRITE E's content anchor was removed by a write in W exactly as
|
|
17
|
+
the agent commanded it: E fails on B and on R alike
|
|
18
|
+
(named bucket; not materialized under the definition)
|
|
19
|
+
3 MATERIALIZED R and B give different results in E's region:
|
|
20
|
+
M1 loud failure, M2 silent no-op, M3 drift/double application
|
|
21
|
+
4 BENIGN-FORWARDED overlap or drift exists but E is right against R
|
|
22
|
+
(anchors on W's new text, adjusted or runtime-resolved lines)
|
|
23
|
+
5 BENIGN-DISJOINT E's anchor shares no text with W's changes; no drift
|
|
24
|
+
6 INDETERMINATE anchor or W not extractable, or the static prediction is
|
|
25
|
+
contradicted by what the trajectory later shows
|
|
26
|
+
Also classifies intra-unit repeated edits (a later edit in the same action after
|
|
27
|
+
an earlier write to the same file) and computes the region-level sensitivity
|
|
28
|
+
for all opportunities. Writes work/materialization.jsonl and
|
|
29
|
+
work/materialization_summary.json.
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
import json
|
|
33
|
+
import os
|
|
34
|
+
import re
|
|
35
|
+
import sys
|
|
36
|
+
from collections import Counter, defaultdict
|
|
37
|
+
|
|
38
|
+
HERE = os.path.dirname(os.path.abspath(__file__))
|
|
39
|
+
sys.path.insert(0, HERE)
|
|
40
|
+
from common import WORK, dump # noqa: E402
|
|
41
|
+
from excerpts import items as all_items, load_trajs # noqa: E402
|
|
42
|
+
from score import output_views # noqa: E402
|
|
43
|
+
|
|
44
|
+
NUM_LINE = re.compile(r"^\s*(\d+)\t(.*)$")
|
|
45
|
+
NUM_COLON = re.compile(r"^(\d+)[:-](.*)$")
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
# ---------------------------------------------------------------- text helpers
|
|
49
|
+
|
|
50
|
+
def norm(s):
|
|
51
|
+
return re.sub(r"[ \t]+", " ", (s or "")).strip()
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def literal_key(pat, bre=True):
|
|
55
|
+
"""Longest literal fragment of a regex pattern (for searching text)."""
|
|
56
|
+
if not pat:
|
|
57
|
+
return ""
|
|
58
|
+
frags, cur, i = [], [], 0
|
|
59
|
+
while i < len(pat):
|
|
60
|
+
c = pat[i]
|
|
61
|
+
if c == "\\" and i + 1 < len(pat):
|
|
62
|
+
n = pat[i + 1]
|
|
63
|
+
if n in "nt":
|
|
64
|
+
cur.append("\n" if n == "n" else "\t")
|
|
65
|
+
elif n in "sSwWdDbB<>`'":
|
|
66
|
+
frags.append("".join(cur))
|
|
67
|
+
cur = []
|
|
68
|
+
elif bre and n in "(){}|+?":
|
|
69
|
+
frags.append("".join(cur))
|
|
70
|
+
cur = []
|
|
71
|
+
else:
|
|
72
|
+
cur.append(n)
|
|
73
|
+
i += 2
|
|
74
|
+
continue
|
|
75
|
+
if c in ".[^$" or (not bre and c in "()|+?{}"):
|
|
76
|
+
if c == "[":
|
|
77
|
+
j = pat.find("]", i + 2)
|
|
78
|
+
i = j + 1 if j > 0 else len(pat)
|
|
79
|
+
else:
|
|
80
|
+
i += 1
|
|
81
|
+
frags.append("".join(cur))
|
|
82
|
+
cur = []
|
|
83
|
+
continue
|
|
84
|
+
if c in "*" or (not bre and c in "+?"):
|
|
85
|
+
if cur:
|
|
86
|
+
cur.pop()
|
|
87
|
+
frags.append("".join(cur))
|
|
88
|
+
cur = []
|
|
89
|
+
i += 1
|
|
90
|
+
continue
|
|
91
|
+
cur.append(c)
|
|
92
|
+
i += 1
|
|
93
|
+
frags.append("".join(cur))
|
|
94
|
+
frags = [f for f in frags if len(f.strip()) >= 3]
|
|
95
|
+
return max(frags, key=len) if frags else ""
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def shown_lines(step, path):
|
|
99
|
+
"""{line number: text} the step's output showed for path, when numbered."""
|
|
100
|
+
obs = step.get("obs") or ""
|
|
101
|
+
out = {}
|
|
102
|
+
base = path.rsplit("/", 1)[-1]
|
|
103
|
+
for ln in obs.split("\n"):
|
|
104
|
+
m = NUM_LINE.match(ln)
|
|
105
|
+
if m:
|
|
106
|
+
out[int(m.group(1))] = m.group(2)
|
|
107
|
+
continue
|
|
108
|
+
m = re.match(r"^(\S+?):(\d+):(.*)$", ln)
|
|
109
|
+
if m and m.group(1).endswith(base):
|
|
110
|
+
out[int(m.group(2))] = m.group(3)
|
|
111
|
+
continue
|
|
112
|
+
return out
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def view_text(t, step_i):
|
|
116
|
+
s = t["steps"][step_i]
|
|
117
|
+
obs = s.get("obs") or ""
|
|
118
|
+
if not obs and t["agent"] == "Terminus2":
|
|
119
|
+
same = [x for x in t["steps"] if x.get("episode") == s.get("episode") and x.get("obs")]
|
|
120
|
+
obs = same[-1]["obs"] if same else ""
|
|
121
|
+
return obs
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def strip_numbers(text):
|
|
125
|
+
"""Text with cat -n / nl / grep -n prefixes removed, line by line."""
|
|
126
|
+
out = []
|
|
127
|
+
for ln in (text or "").split("\n"):
|
|
128
|
+
m = NUM_LINE.match(ln)
|
|
129
|
+
if m:
|
|
130
|
+
out.append(m.group(2))
|
|
131
|
+
continue
|
|
132
|
+
m = re.match(r"^(?:\S+?:)?\d+[:-](.*)$", ln)
|
|
133
|
+
out.append(m.group(1) if m else ln)
|
|
134
|
+
return "\n".join(out)
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def contains(hay, needle):
|
|
138
|
+
if not needle or not hay:
|
|
139
|
+
return False
|
|
140
|
+
if needle in hay:
|
|
141
|
+
return True
|
|
142
|
+
return norm(needle) in norm(hay) if len(norm(needle)) >= 4 else False
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def lines_of(s):
|
|
146
|
+
return [x.strip() for x in (s or "").split("\n") if len(x.strip()) >= 4]
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def overlaps(a, b):
|
|
150
|
+
"""Two texts share a line (or one contains the other)."""
|
|
151
|
+
if not a or not b:
|
|
152
|
+
return False
|
|
153
|
+
if contains(a, b) or contains(b, a):
|
|
154
|
+
return True
|
|
155
|
+
la, lb = set(lines_of(a)), set(lines_of(b))
|
|
156
|
+
return bool(la & lb)
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
_COMMENT = re.compile(r"^(//|#|/\*|\*|--|;|<!--)")
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def substantive_lines(text):
|
|
163
|
+
"""Lines that identify a block of code: 12+ characters after folding
|
|
164
|
+
whitespace and sed's escapes, with a word character, and not a comment.
|
|
165
|
+
Braces, `pub(crate)` or a shared doc-comment line do not make two blocks
|
|
166
|
+
the same."""
|
|
167
|
+
out = set()
|
|
168
|
+
for ln in (text or "").split("\n"):
|
|
169
|
+
x = norm(re.sub(r"\\(.)", r"\1", ln.replace("\\t", " ").replace("\\n", " ")))
|
|
170
|
+
if len(x) >= 12 and re.search(r"\w", x) and not _COMMENT.match(x):
|
|
171
|
+
out.add(x)
|
|
172
|
+
return out
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def removed_later(j, wops, added):
|
|
176
|
+
"""Whether a write after wops[j] took back what wops[j] added: a restore of
|
|
177
|
+
the file, or a write that removes that text."""
|
|
178
|
+
for _, op, dw, w in wops[j + 1:]:
|
|
179
|
+
if w[4] is False:
|
|
180
|
+
continue
|
|
181
|
+
if op.get("kind") in ("restore", "restore*"):
|
|
182
|
+
return True
|
|
183
|
+
if any(overlaps(r, a) for r in dw["removed_only"] for a in added):
|
|
184
|
+
return True
|
|
185
|
+
return False
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def sed_cmd(op):
|
|
189
|
+
"""(command, pattern) of a single-command sed edit, else (None, None)."""
|
|
190
|
+
a = op.get("anchor") or {}
|
|
191
|
+
cmds = a.get("cmds") or []
|
|
192
|
+
if a.get("type") != "sed" or len(cmds) != 1:
|
|
193
|
+
return None, None
|
|
194
|
+
return cmds[0].get("cmd"), cmds[0].get("pat")
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
# ---------------------------------------------------------------- anchors and write effects
|
|
198
|
+
|
|
199
|
+
def dynamic_address(raw_scripts):
|
|
200
|
+
return any(re.match(r"^\s*\$\{?[A-Za-z_]", s or "") or re.search(r"\$\{?[A-Za-z_]\w*\}?[aicd]", s or "")
|
|
201
|
+
for s in raw_scripts or [])
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
def _describe(op):
|
|
205
|
+
"""Effects of a write op as commanded: removed texts, added texts, line
|
|
206
|
+
changes [(line or None, delta)], anchors for E: content keys and line numbers."""
|
|
207
|
+
a = op.get("anchor") or {}
|
|
208
|
+
t = a.get("type")
|
|
209
|
+
d = {"removed": [], "added": [], "lines": [], "keys": [], "line_anchors": [], "opaque": False,
|
|
210
|
+
"static_noop": False, "dynamic": False, "exact": False}
|
|
211
|
+
kind = op.get("kind")
|
|
212
|
+
if kind in ("append",):
|
|
213
|
+
d["added"].append(op.get("text") or "")
|
|
214
|
+
return d
|
|
215
|
+
if kind in ("replace", "rewrite", "overwrite_unseen", "restore", "undo", "delete", "restore*"):
|
|
216
|
+
d["opaque"] = True
|
|
217
|
+
return d
|
|
218
|
+
if kind == "overwrite":
|
|
219
|
+
d["opaque"] = True
|
|
220
|
+
return d
|
|
221
|
+
if t == "str_replace":
|
|
222
|
+
old, new = a.get("old") or "", a.get("new") or ""
|
|
223
|
+
d.update(removed=[old], added=[new], keys=[old], exact=True)
|
|
224
|
+
d["lines"].append((None, new.count("\n") - old.count("\n")))
|
|
225
|
+
d["static_noop"] = old == new
|
|
226
|
+
elif t == "insert":
|
|
227
|
+
new = a.get("new") or ""
|
|
228
|
+
try:
|
|
229
|
+
ln = int(a.get("line"))
|
|
230
|
+
except (TypeError, ValueError):
|
|
231
|
+
ln = None
|
|
232
|
+
d["added"].append(new)
|
|
233
|
+
d["lines"].append((ln, new.count("\n") + 1))
|
|
234
|
+
if ln is not None:
|
|
235
|
+
d["line_anchors"].append(ln)
|
|
236
|
+
elif t == "lines": # SWE-agent 0.x edit N:M
|
|
237
|
+
new = a.get("new") or ""
|
|
238
|
+
st, en = a.get("start"), a.get("end")
|
|
239
|
+
d["added"].append(new)
|
|
240
|
+
d["line_anchors"] += [st, en]
|
|
241
|
+
d["lines"].append((st, (new.count("\n") + 1) - (en - st + 1)))
|
|
242
|
+
elif t == "py":
|
|
243
|
+
for r in a.get("replaces") or []:
|
|
244
|
+
d["removed"].append(r["old"])
|
|
245
|
+
d["added"].append(r["new"])
|
|
246
|
+
d["keys"].append(r["old"])
|
|
247
|
+
d["lines"].append((None, r["new"].count("\n") - r["old"].count("\n")))
|
|
248
|
+
if r["old"] == r["new"]:
|
|
249
|
+
d["static_noop"] = True
|
|
250
|
+
for r in a.get("regex") or []:
|
|
251
|
+
k = literal_key(r.get("pat") or "", bre=False)
|
|
252
|
+
if k:
|
|
253
|
+
d["removed"].append(k)
|
|
254
|
+
d["keys"].append(k)
|
|
255
|
+
if r.get("rep"):
|
|
256
|
+
d["added"].append(re.sub(r"\\\d|\\g<\w+>", "", r["rep"]))
|
|
257
|
+
d["lines"].append((None, None))
|
|
258
|
+
for f in a.get("finds") or []:
|
|
259
|
+
d["keys"].append(f)
|
|
260
|
+
if a.get("line_ops"):
|
|
261
|
+
d["lines"].append((None, None))
|
|
262
|
+
d["opaque"] = not (a.get("replaces") or a.get("regex") or a.get("finds"))
|
|
263
|
+
d["exact"] = bool(a.get("replaces"))
|
|
264
|
+
elif t in ("sed", "perl"):
|
|
265
|
+
raw = a.get("raw") or [a.get("script") or ""]
|
|
266
|
+
if dynamic_address(raw):
|
|
267
|
+
d["dynamic"] = True
|
|
268
|
+
cmds = a.get("cmds") or [{"cmd": "s", "addr": [], "pat": x.get("pat"), "rep": x.get("rep")} for x in a.get("subs") or []]
|
|
269
|
+
bre = not a.get("E") and t == "sed"
|
|
270
|
+
for c in cmds:
|
|
271
|
+
addr = c.get("addr") or []
|
|
272
|
+
nums = [x["line"] for x in addr if isinstance(x.get("line"), int)]
|
|
273
|
+
res = [x["re"] for x in addr if x.get("re")]
|
|
274
|
+
for r_ in res:
|
|
275
|
+
k = literal_key(r_, bre)
|
|
276
|
+
if k:
|
|
277
|
+
d["keys"].append(k)
|
|
278
|
+
if nums and not d["dynamic"]:
|
|
279
|
+
d["line_anchors"] += nums
|
|
280
|
+
cmd = c.get("cmd")
|
|
281
|
+
if cmd == "s":
|
|
282
|
+
pat, rep = c.get("pat") or "", c.get("rep") or ""
|
|
283
|
+
k = literal_key(pat, bre)
|
|
284
|
+
if k:
|
|
285
|
+
d["removed"].append(k)
|
|
286
|
+
d["keys"].append(k)
|
|
287
|
+
rep_lit = re.sub(r"\\[0-9&]|&", "", rep).replace("\\n", "\n").replace("\\/", "/")
|
|
288
|
+
d["added"].append(rep_lit)
|
|
289
|
+
nl_in = "\\n" in pat
|
|
290
|
+
if nl_in and not a.get("z") and not any(x.get("cmd") in ("N", "D", "P") for x in cmds) and t == "sed":
|
|
291
|
+
d["static_noop"] = True # a newline cannot occur inside sed's pattern space
|
|
292
|
+
if pat and rep and pat == rep:
|
|
293
|
+
d["static_noop"] = True
|
|
294
|
+
delta = rep_lit.count("\n") - pat.count("\\n")
|
|
295
|
+
d["lines"].append((nums[0] if nums else None, delta if not (nl_in and d["static_noop"]) else 0))
|
|
296
|
+
elif cmd in ("a", "i"):
|
|
297
|
+
d["added"].append(c.get("text") or "")
|
|
298
|
+
pos = (nums[0] + (1 if cmd == "a" else 0)) if nums else None
|
|
299
|
+
d["lines"].append((pos, (c.get("text") or "").count("\n") + 1))
|
|
300
|
+
elif cmd == "c":
|
|
301
|
+
d["added"].append(c.get("text") or "")
|
|
302
|
+
if len(nums) == 2:
|
|
303
|
+
d["lines"].append((nums[0], (c.get("text") or "").count("\n") + 1 - (nums[1] - nums[0] + 1)))
|
|
304
|
+
else:
|
|
305
|
+
d["lines"].append((nums[0] if nums else None, None if res else 0))
|
|
306
|
+
elif cmd == "d":
|
|
307
|
+
rel = [x["rel"] for x in addr if isinstance(x.get("rel"), int)]
|
|
308
|
+
if rel:
|
|
309
|
+
d["lines"].append((nums[0] if nums else None, -(rel[0] + 1)))
|
|
310
|
+
elif len(nums) == 2:
|
|
311
|
+
d["lines"].append((nums[0], -(nums[1] - nums[0] + 1)))
|
|
312
|
+
elif len(nums) == 1:
|
|
313
|
+
d["lines"].append((nums[0], -1))
|
|
314
|
+
else:
|
|
315
|
+
d["lines"].append((None, None))
|
|
316
|
+
for r_ in res:
|
|
317
|
+
d["removed"].append(literal_key(r_, bre))
|
|
318
|
+
elif cmd in ("r", "R"):
|
|
319
|
+
# inserts a file the command names: its length is not known here
|
|
320
|
+
d["lines"].append(((nums[0] + 1) if nums else None, None))
|
|
321
|
+
elif cmd in ("?", "y", "{", "}", "p", "q"):
|
|
322
|
+
if cmd == "?":
|
|
323
|
+
d["opaque"] = True
|
|
324
|
+
elif t == "patch":
|
|
325
|
+
for h in a.get("hunks") or []:
|
|
326
|
+
old = "\n".join(ln[1:] for ln in h["lines"] if ln[:1] in (" ", "-"))
|
|
327
|
+
new = "\n".join(ln[1:] for ln in h["lines"] if ln[:1] in (" ", "+"))
|
|
328
|
+
rem = "\n".join(ln[1:] for ln in h["lines"] if ln[:1] == "-")
|
|
329
|
+
add = "\n".join(ln[1:] for ln in h["lines"] if ln[:1] == "+")
|
|
330
|
+
if rem:
|
|
331
|
+
d["removed"].append(rem)
|
|
332
|
+
if add:
|
|
333
|
+
d["added"].append(add)
|
|
334
|
+
d["keys"].append(old)
|
|
335
|
+
d["lines"].append((h.get("old_start"), new.count("\n") - old.count("\n")))
|
|
336
|
+
d["exact"] = True
|
|
337
|
+
elif t in ("ed", "awk"):
|
|
338
|
+
d["opaque"] = True
|
|
339
|
+
else:
|
|
340
|
+
d["opaque"] = True
|
|
341
|
+
d["removed"] = [x for x in d["removed"] if x and x.strip()]
|
|
342
|
+
d["added"] = [x for x in d["added"] if x is not None]
|
|
343
|
+
d["keys"] = [x for x in d["keys"] if x and len(x.strip()) >= 3]
|
|
344
|
+
# lines that a write only removes / only adds (context shared by the old
|
|
345
|
+
# and new text is neither)
|
|
346
|
+
rem_l = set(lines_of("\n".join(d["removed"])))
|
|
347
|
+
add_l = set(lines_of("\n".join(d["added"])))
|
|
348
|
+
d["removed_only"] = [x for x in d["removed"] if len(lines_of(x)) <= 1] + ["\n".join(sorted(rem_l - add_l))]
|
|
349
|
+
d["added_only"] = [x for x in d["added"] if len(lines_of(x)) <= 1] + ["\n".join(sorted(add_l - rem_l))]
|
|
350
|
+
d["removed_only"] = [x for x in d["removed_only"] if x.strip()]
|
|
351
|
+
d["added_only"] = [x for x in d["added_only"] if x.strip()]
|
|
352
|
+
return d
|
|
353
|
+
|
|
354
|
+
|
|
355
|
+
_LIT = re.compile(r'"""(.*?)"""|\'\'\'(.*?)\'\'\'', re.S)
|
|
356
|
+
|
|
357
|
+
|
|
358
|
+
def describe(op, action=None):
|
|
359
|
+
"""Effects of a write op; for Python scripts, the long string literals in
|
|
360
|
+
the script are also taken as text the write may add (their exact use is
|
|
361
|
+
not modelled)."""
|
|
362
|
+
d = _describe(op)
|
|
363
|
+
d.setdefault("removed_only", [x for x in d["removed"] if x and x.strip()])
|
|
364
|
+
d.setdefault("added_only", [x for x in d["added"] if x and x.strip()])
|
|
365
|
+
if op.get("mech") == "python" and action:
|
|
366
|
+
lits = [a or b for a, b in _LIT.findall(action)]
|
|
367
|
+
lits = [x.encode("utf-8", "replace").decode("unicode_escape", "replace") if "\\n" in x else x for x in lits]
|
|
368
|
+
d["script_literals"] = [x for x in lits if len(x.strip()) >= 20]
|
|
369
|
+
if d["opaque"] or op.get("kind") in ("rewrite", "overwrite_unseen"):
|
|
370
|
+
d["added_only"] = d["added_only"] + d["script_literals"]
|
|
371
|
+
return d
|
|
372
|
+
|
|
373
|
+
|
|
374
|
+
def later_evidence(t, item, de, horizon=10):
|
|
375
|
+
"""What later views of the file show about E: 'applied' when E's new text
|
|
376
|
+
appears, 'not_applied' when the anchor text is still shown and the new
|
|
377
|
+
text is not, else 'unknown'. Stops at the next write to the file."""
|
|
378
|
+
steps = t["steps"]
|
|
379
|
+
new_texts = [x for x in de.get("added_only") or [] if len(x.strip()) >= 6]
|
|
380
|
+
key = max(de["keys"], key=len) if de["keys"] else ""
|
|
381
|
+
s_e = steps[item["step"]]
|
|
382
|
+
e = s_e["ops"][item["op"]]
|
|
383
|
+
if e.get("mech") in ("editor",) and e.get("landed") is True:
|
|
384
|
+
return "applied (editor confirmed)"
|
|
385
|
+
for s in steps[item["step"] + 1:item["step"] + 1 + horizon]:
|
|
386
|
+
wrote = any(o["op"] == "W" and o["path"] == item["path"] and o.get("landed") is not False for o in s["ops"])
|
|
387
|
+
viewed = any(o["op"] == "R" and o["path"] == item["path"] and o.get("landed") is not False for o in s["ops"])
|
|
388
|
+
if viewed:
|
|
389
|
+
txt = strip_numbers(view_text(t, s["i"]))
|
|
390
|
+
if any(contains(txt, n) for n in new_texts):
|
|
391
|
+
return "applied (later view shows the new text)"
|
|
392
|
+
if key and contains(txt, key) and new_texts and not any(contains(txt, n) for n in new_texts):
|
|
393
|
+
return "not_applied (later view still shows the anchor)"
|
|
394
|
+
if wrote:
|
|
395
|
+
break
|
|
396
|
+
fd = t.get("final_diff") or ""
|
|
397
|
+
if fd and new_texts:
|
|
398
|
+
if any(n.strip() in fd for n in new_texts):
|
|
399
|
+
return "applied (final diff has the new text)"
|
|
400
|
+
return "unknown"
|
|
401
|
+
|
|
402
|
+
|
|
403
|
+
# ---------------------------------------------------------------- classification
|
|
404
|
+
|
|
405
|
+
def same_command(a, b):
|
|
406
|
+
return norm(a) == norm(b) and len(norm(a)) > 0
|
|
407
|
+
|
|
408
|
+
|
|
409
|
+
def classify(item, t):
|
|
410
|
+
steps = t["steps"]
|
|
411
|
+
s_e = steps[item["step"]]
|
|
412
|
+
e = s_e["ops"][item["op"]]
|
|
413
|
+
de = describe(e, s_e.get("action"))
|
|
414
|
+
W = [w for w in (item.get("W") or item.get("prior_same_unit_writes") or [])]
|
|
415
|
+
wops = []
|
|
416
|
+
for w in W:
|
|
417
|
+
s_w = steps[w[0]]
|
|
418
|
+
op = s_w["ops"][w[1]] if 0 <= w[1] < len(s_w["ops"]) else {"kind": "restore*", "mech": "git"}
|
|
419
|
+
wops.append((s_w, op, describe(op, s_w.get("action")), w))
|
|
420
|
+
lv = item.get("last_view")
|
|
421
|
+
V_text, V_lines = "", {}
|
|
422
|
+
if lv:
|
|
423
|
+
# all views of the last-seen version
|
|
424
|
+
vsteps = [lv[0]]
|
|
425
|
+
V_text = strip_numbers(view_text(t, lv[0]))
|
|
426
|
+
V_lines = shown_lines(steps[lv[0]], item["path"])
|
|
427
|
+
K_text = V_text
|
|
428
|
+
paths = {item["path"]}
|
|
429
|
+
for s in steps[:item["step"]]:
|
|
430
|
+
for o in s["ops"]:
|
|
431
|
+
if o["op"] == "W" and o["path"] == item["path"] and o.get("src"):
|
|
432
|
+
paths.add(o["src"]) # content copied or moved in from another file
|
|
433
|
+
for s in steps[:item["step"]]:
|
|
434
|
+
for o in s["ops"]:
|
|
435
|
+
if o["op"] == "R" and o["path"] in paths and o.get("landed") is not False:
|
|
436
|
+
K_text += "\n" + strip_numbers(view_text(t, s["i"]))
|
|
437
|
+
if o["op"] == "W" and o["path"] in paths and o.get("kind") in ("overwrite", "append") and o.get("text"):
|
|
438
|
+
K_text += "\n" + o["text"]
|
|
439
|
+
ev = {"e_mech": e.get("mech"), "e_landed": e.get("landed"), "e_ev": e.get("ev"),
|
|
440
|
+
"n_W": len(W), "W_kinds": [op.get("kind") + ":" + str(op.get("mech")) for _, op, _, _ in wops],
|
|
441
|
+
"W_landed": [w[4] for w in W], "keys": [k[:80] for k in de["keys"][:3]],
|
|
442
|
+
"line_anchors": de["line_anchors"][:4]}
|
|
443
|
+
# --- 1 SPURIOUS: no write in W made a real change
|
|
444
|
+
real_change = []
|
|
445
|
+
for s_w, op, dw, w in wops:
|
|
446
|
+
if w[4] is False:
|
|
447
|
+
continue
|
|
448
|
+
orc = op.get("oracle") or {}
|
|
449
|
+
if orc.get("old_content") is not None and orc.get("new_content") is not None and orc["old_content"] == orc["new_content"]:
|
|
450
|
+
continue
|
|
451
|
+
if dw["static_noop"]:
|
|
452
|
+
continue
|
|
453
|
+
if op.get("kind") == "restore*" or (op.get("kind") == "restore" and op.get("mech") == "git"):
|
|
454
|
+
real_change.append((s_w, op, dw, w))
|
|
455
|
+
continue
|
|
456
|
+
# a substitution whose pattern the agent had seen nowhere, followed by
|
|
457
|
+
# a view that still shows the pattern: it did not match
|
|
458
|
+
real_change.append((s_w, op, dw, w))
|
|
459
|
+
if not real_change:
|
|
460
|
+
return "SPURIOUS", "no write after the last view changed the file (static no-op or failed)", ev
|
|
461
|
+
# (No OpenHands oracle: in the event streams extras.old_content is the
|
|
462
|
+
# edit's old_str argument, not the file content; see oh_oracle_check.py.)
|
|
463
|
+
# --- 2 FAILED-UNRELATED: E failed for non-content reasons
|
|
464
|
+
e_ev = (e.get("ev") or "")
|
|
465
|
+
anc = e.get("anchor") or {}
|
|
466
|
+
if anc.get("type") == "str_replace" and e.get("landed") is False and anc.get("old"):
|
|
467
|
+
raw = anc["old"]
|
|
468
|
+
if raw != raw.strip("\n") and not contains(K_text, raw) and contains(K_text, raw.strip("\n")):
|
|
469
|
+
return "FAILED-UNRELATED", "editor rejected an old_str whose only mismatch is leading/trailing newlines", ev
|
|
470
|
+
if e.get("landed") is False and not re.search(r"did not appear verbatim|No replacement|patch failed|FAILED|Multiple occurrences", e_ev + " " + (s_e.get("obs") or "")[:2000]):
|
|
471
|
+
return "FAILED-UNRELATED", f"edit failed for a reason unrelated to content ({e_ev[:60]})", ev
|
|
472
|
+
# --- E cannot change anything (s/X/X/)
|
|
473
|
+
if de["static_noop"]:
|
|
474
|
+
return "FAILED-UNRELATED", "the edit replaces text with itself", ev
|
|
475
|
+
# --- double application: E re-issues an earlier write that landed
|
|
476
|
+
e_inserts = any(c.get("cmd") in ("a", "i") for c in ((e.get("anchor") or {}).get("cmds") or [])) or \
|
|
477
|
+
(e.get("anchor") or {}).get("type") in ("insert",) or e.get("kind") == "append"
|
|
478
|
+
for s_w, op, dw, w in real_change:
|
|
479
|
+
same = same_command(json.dumps(op.get("anchor"), sort_keys=True), json.dumps(e.get("anchor"), sort_keys=True)) \
|
|
480
|
+
and op.get("mech") == e.get("mech")
|
|
481
|
+
if same and removed_later(wops.index((s_w, op, dw, w)), wops, dw["added_only"]):
|
|
482
|
+
continue # restored or removed before E re-issued it
|
|
483
|
+
if same:
|
|
484
|
+
ev["reissue_of_step"] = s_w["i"]
|
|
485
|
+
if e_inserts:
|
|
486
|
+
return "MATERIALIZED-M3", f"re-issues the insertion made at step {s_w['i']}, which had landed: double application", ev
|
|
487
|
+
return "BENIGN-FORWARDED", f"re-issues the substitution made at step {s_w['i']}; on the real file it is a no-op with the same final text", ev
|
|
488
|
+
# --- double application by a different command: E adds text an unseen
|
|
489
|
+
# write already added, without replacing that text
|
|
490
|
+
# The duplicate must be the same block (most of E's substantive lines) put
|
|
491
|
+
# at the same place (E's anchor is W's anchor, or text W wrote), and still
|
|
492
|
+
# in the file when E ran.
|
|
493
|
+
e_sub = substantive_lines("\n".join(de["added_only"]))
|
|
494
|
+
for s_w, op, dw, w in real_change:
|
|
495
|
+
w_new = [x for x in dw["added_only"] if len(norm(x)) >= 12]
|
|
496
|
+
w_sub = substantive_lines("\n".join(dw["added_only"]))
|
|
497
|
+
same_block = bool(e_sub) and len(e_sub & w_sub) >= 0.8 * len(e_sub)
|
|
498
|
+
w_text = "\n".join(dw["added"] + dw["added_only"])
|
|
499
|
+
same_place = any(overlaps(a, b) for a in de["keys"] for b in dw["keys"]) or \
|
|
500
|
+
any(contains(w_text, a) for a in de["keys"]) or (dw["opaque"] and not dw["keys"])
|
|
501
|
+
replaces_it = any(overlaps(r, y) for r in de["removed_only"] for y in w_new)
|
|
502
|
+
if same_block and same_place and e_inserts and not replaces_it and e.get("landed") is not False \
|
|
503
|
+
and not removed_later(wops.index((s_w, op, dw, w)), wops, dw["added_only"]):
|
|
504
|
+
ev["duplicate_of_step"] = s_w["i"]
|
|
505
|
+
return "MATERIALIZED-M3", f"adds text the unseen write at step {s_w['i']} had already added: double application", ev
|
|
506
|
+
# --- the unseen write restored or copied known content (cp from a
|
|
507
|
+
# backup the agent made, git checkout) and E was composed for it
|
|
508
|
+
last = real_change[-1]
|
|
509
|
+
if last[1].get("kind") in ("replace", "restore", "restore*") and (last[1].get("src") or last[1].get("kind") != "replace"):
|
|
510
|
+
same_unit = last[3][6] == item.get("unit")
|
|
511
|
+
if same_unit or e.get("landed") is True or (de["keys"] and contains(K_text, max(de["keys"], key=len))):
|
|
512
|
+
return "BENIGN-FORWARDED", ("composed together with the restore in the same action" if same_unit
|
|
513
|
+
else "edit applies to the restored content the agent knew"), ev
|
|
514
|
+
# --- dynamic (runtime-resolved) anchors
|
|
515
|
+
if de["dynamic"]:
|
|
516
|
+
return "BENIGN-FORWARDED", "line address computed at run time from the current file", ev
|
|
517
|
+
# --- line drift. A write that left the file exactly as the agent last saw
|
|
518
|
+
# it (a git checkout back to a viewed committed version, a revert) resets
|
|
519
|
+
# the line numbering: only writes after the last such one can shift it.
|
|
520
|
+
drift_writes = real_change
|
|
521
|
+
lv_cid = lv[1] if lv else None
|
|
522
|
+
back = [j for j, (_, _, _, w) in enumerate(real_change) if w[3] == lv_cid]
|
|
523
|
+
if back:
|
|
524
|
+
drift_writes = real_change[back[-1] + 1:]
|
|
525
|
+
if de["line_anchors"] and not de["keys"]:
|
|
526
|
+
n = min(x for x in de["line_anchors"] if isinstance(x, int))
|
|
527
|
+
e_cmd, e_pat = sed_cmd(e)
|
|
528
|
+
if n == 1 and (e_cmd == "i" or (e_cmd == "s" and e_pat == "^")):
|
|
529
|
+
return "BENIGN-FORWARDED", "inserts at the top of the file, which no change above can move", ev
|
|
530
|
+
deltas, unknown, above = 0, False, []
|
|
531
|
+
for _, op, dw, _ in drift_writes:
|
|
532
|
+
if dw["opaque"]:
|
|
533
|
+
unknown = True
|
|
534
|
+
for pos, dl in dw["lines"]:
|
|
535
|
+
if dl is None:
|
|
536
|
+
if pos is None or pos <= n:
|
|
537
|
+
unknown = True
|
|
538
|
+
elif dl != 0:
|
|
539
|
+
if pos is None:
|
|
540
|
+
unknown = True
|
|
541
|
+
elif dl < 0 and pos == n and e_cmd == "i":
|
|
542
|
+
continue # E inserts where a block was deleted: a replacement
|
|
543
|
+
elif pos <= n:
|
|
544
|
+
deltas += dl
|
|
545
|
+
above.append((pos, dl, sed_cmd(op)[0]))
|
|
546
|
+
ev["line_delta_above"] = deltas
|
|
547
|
+
# The agent's own run of line inserts, each placed right after the
|
|
548
|
+
# previous one (133a, 134a, 135a): it accounted for every shift.
|
|
549
|
+
e_pos = de["lines"][0][0] if de["lines"] else None
|
|
550
|
+
if deltas and not unknown and e_cmd in ("a", "i") and e_pos is not None and \
|
|
551
|
+
all(c in ("a", "i") and dl > 0 for _, dl, c in above) and \
|
|
552
|
+
all(above[k][0] == above[k - 1][0] + above[k - 1][1] for k in range(1, len(above))) and \
|
|
553
|
+
e_pos == above[-1][0] + above[-1][1]:
|
|
554
|
+
return "BENIGN-FORWARDED", "continues the agent's own run of line inserts, each placed after the last", ev
|
|
555
|
+
if deltas != 0 and not unknown:
|
|
556
|
+
return "MATERIALIZED-M3", f"line anchor {n} after a {deltas:+d}-line change above it (V numbering)", ev
|
|
557
|
+
if unknown:
|
|
558
|
+
return "INDETERMINATE", "line anchor; an unseen write changed line counts at an unknown position", ev
|
|
559
|
+
return "BENIGN-DISJOINT", "line anchor; no line-count change above it", ev
|
|
560
|
+
if not de["keys"]:
|
|
561
|
+
return "INDETERMINATE", "edit anchor not extractable", ev
|
|
562
|
+
a = max(de["keys"], key=len)
|
|
563
|
+
removed_hit = [(s_w, dw) for s_w, _, dw, _ in real_change for r in dw["removed_only"] if overlaps(a, r)]
|
|
564
|
+
added_hit = [(s_w, dw) for s_w, _, dw, _ in real_change for x in dw["added_only"] if x and overlaps(a, x)]
|
|
565
|
+
ev["later"] = later_evidence(t, item, de)
|
|
566
|
+
opaque = any(dw["opaque"] for _, _, dw, _ in real_change)
|
|
567
|
+
in_K = contains(K_text, a) or bool(added_hit)
|
|
568
|
+
ev["anchor_in_known_text"] = in_K
|
|
569
|
+
loud = e.get("landed") is False
|
|
570
|
+
# line anchors mixed with content (e.g. sed 'Ns/foo/bar/'): drift check with content
|
|
571
|
+
if de["line_anchors"]:
|
|
572
|
+
n = min(x for x in de["line_anchors"] if isinstance(x, int))
|
|
573
|
+
deltas = sum(dl for _, _, dw, _ in drift_writes for pos, dl in dw["lines"] if dl and pos is not None and pos <= n)
|
|
574
|
+
if deltas:
|
|
575
|
+
vline = V_lines.get(n)
|
|
576
|
+
if vline is not None and a in vline:
|
|
577
|
+
return "MATERIALIZED-M3", f"line {n} holds the anchor only in the agent's last view; {deltas:+d} lines above moved it", ev
|
|
578
|
+
return "BENIGN-FORWARDED", f"line {n} with a {deltas:+d} shift above; anchor not on the old line (adjusted)", ev
|
|
579
|
+
if added_hit and not removed_hit:
|
|
580
|
+
return "BENIGN-FORWARDED", f"anchor includes text the agent itself wrote at step {added_hit[0][0]['i']}", ev
|
|
581
|
+
if removed_hit and not added_hit:
|
|
582
|
+
if ev["later"].startswith("applied"):
|
|
583
|
+
return "INDETERMINATE", "anchor overlaps text an unseen write removed, yet the edit's new text appears later", ev
|
|
584
|
+
if loud:
|
|
585
|
+
return "FAILED-OWN-WRITE", f"anchor removed by the agent's own write at step {removed_hit[0][0]['i']}; the edit was rejected", ev
|
|
586
|
+
return "FAILED-OWN-WRITE", f"anchor removed by the agent's own write at step {removed_hit[0][0]['i']}; the edit silently matched nothing", ev
|
|
587
|
+
if removed_hit and added_hit:
|
|
588
|
+
return "INDETERMINATE", "anchor overlaps both removed and added text of unseen writes", ev
|
|
589
|
+
if loud and re.search(r"did not appear verbatim|No replacement|patch failed|FAILED", e_ev + (s_e.get("obs") or "")[:2000]):
|
|
590
|
+
if opaque:
|
|
591
|
+
return "INDETERMINATE", "edit rejected after an unseen whole-file write", ev
|
|
592
|
+
if not in_K:
|
|
593
|
+
return "FAILED-UNRELATED", "edit rejected; its anchor was not in any text the agent had seen", ev
|
|
594
|
+
return "INDETERMINATE", "edit rejected although the anchor was seen and no unseen write touched it", ev
|
|
595
|
+
if opaque:
|
|
596
|
+
return "INDETERMINATE", "an unseen whole-file write (copy, script rewrite, restore) with unknown content" + \
|
|
597
|
+
("; the edit's new text appears later" if ev["later"].startswith("applied") else ""), ev
|
|
598
|
+
if ev["later"].startswith("not_applied"):
|
|
599
|
+
return "INDETERMINATE", "no overlap with the unseen writes found, but a later view shows the edit did not apply", ev
|
|
600
|
+
return "BENIGN-DISJOINT", "anchor shares no text with the unseen writes", ev
|
|
601
|
+
|
|
602
|
+
|
|
603
|
+
def main():
|
|
604
|
+
its = all_items()
|
|
605
|
+
trajs = load_trajs({it["traj_id"] for it in its})
|
|
606
|
+
out = open(os.path.join(WORK, "materialization.jsonl"), "w", encoding="utf-8")
|
|
607
|
+
counts = defaultdict(Counter)
|
|
608
|
+
for it in its:
|
|
609
|
+
t = trajs[it["traj_id"]]
|
|
610
|
+
cls, why, ev = classify(it, t)
|
|
611
|
+
rec = {k: it[k] for k in ("id", "traj_id", "agent", "model", "step", "op", "path", "kind_of_item", "bench")}
|
|
612
|
+
rec.update(cls=cls, why=why, evidence=ev)
|
|
613
|
+
out.write(json.dumps(rec, default=str) + "\n")
|
|
614
|
+
counts[(it["kind_of_item"], it["agent"])][cls] += 1
|
|
615
|
+
counts[(it["kind_of_item"], "ALL")][cls] += 1
|
|
616
|
+
out.close()
|
|
617
|
+
summary = {f"{k[0]}|{k[1]}": dict(v) for k, v in counts.items()}
|
|
618
|
+
for k, v in sorted(summary.items()):
|
|
619
|
+
print(k, dict(sorted(v.items())))
|
|
620
|
+
dump(summary, "materialization_summary.json")
|
|
621
|
+
|
|
622
|
+
|
|
623
|
+
if __name__ == "__main__":
|
|
624
|
+
main()
|