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.
Files changed (60) hide show
  1. align_agent/__init__.py +11 -0
  2. align_agent/_audit/common.py +58 -0
  3. align_agent/_audit/deepswe.py +260 -0
  4. align_agent/_audit/excerpts.py +203 -0
  5. align_agent/_audit/extract.py +798 -0
  6. align_agent/_audit/materialize.py +624 -0
  7. align_agent/_audit/score.py +325 -0
  8. align_agent/_audit/shellparse.py +1141 -0
  9. align_agent/_audit/tarsur.py +153 -0
  10. align_agent/_vendor/src/__init__.py +3 -0
  11. align_agent/_vendor/src/harness/__init__.py +13 -0
  12. align_agent/_vendor/src/harness/arms.py +524 -0
  13. align_agent/_vendor/src/harness/completion.py +104 -0
  14. align_agent/_vendor/src/harness/dependency_notice.py +616 -0
  15. align_agent/_vendor/src/harness/freshness_hook.py +131 -0
  16. align_agent/_vendor/src/harness/lean.py +41 -0
  17. align_agent/_vendor/src/harness/mini_real.py +1040 -0
  18. align_agent/_vendor/src/harness/miniswe_scaffold.py +661 -0
  19. align_agent/_vendor/src/harness/tail_diff.py +292 -0
  20. align_agent/_vendor/src/harness/viewport.py +220 -0
  21. align_agent/_vendor/src/metrics/__init__.py +38 -0
  22. align_agent/_vendor/src/metrics/coherence.py +354 -0
  23. align_agent/_vendor/src/metrics/corruption_oracles.py +139 -0
  24. align_agent/_vendor/src/metrics/replay.py +399 -0
  25. align_agent/_vendor/src/metrics/statistics.py +185 -0
  26. align_agent/_vendor/src/metrics/verification.py +104 -0
  27. align_agent/_vendor/src/parsers/__init__.py +33 -0
  28. align_agent/_vendor/src/parsers/base_parser.py +349 -0
  29. align_agent/_vendor/src/parsers/file_events.py +806 -0
  30. align_agent/_vendor/src/parsers/miniswe_parser.py +96 -0
  31. align_agent/_vendor/src/parsers/openhands_parser.py +414 -0
  32. align_agent/_vendor/src/parsers/sweagent_parser.py +180 -0
  33. align_agent/_vendor/src/parsers/terminus_parser.py +106 -0
  34. align_agent/cli.py +101 -0
  35. align_agent/core.py +161 -0
  36. align_agent/run.py +84 -0
  37. align_agent/sessions.py +985 -0
  38. align_agent/writethrough/__init__.py +4 -0
  39. align_agent/writethrough/_lib/__init__.py +1 -0
  40. align_agent/writethrough/_lib/wt_src/__init__.py +3 -0
  41. align_agent/writethrough/_lib/wt_src/harness/__init__.py +1 -0
  42. align_agent/writethrough/_lib/wt_src/harness/dependency_notice.py +616 -0
  43. align_agent/writethrough/_lib/wt_src/harness/lean.py +41 -0
  44. align_agent/writethrough/_lib/wt_src/harness/tail_diff.py +292 -0
  45. align_agent/writethrough/_lib/wt_src/harness/viewport.py +220 -0
  46. align_agent/writethrough/_lib/wt_src/metrics/__init__.py +38 -0
  47. align_agent/writethrough/_lib/wt_src/metrics/replay.py +399 -0
  48. align_agent/writethrough/_lib/wt_src/parsers/__init__.py +1 -0
  49. align_agent/writethrough/_lib/wt_src/parsers/file_events.py +806 -0
  50. align_agent/writethrough/claude_code.py +81 -0
  51. align_agent/writethrough/engine.py +647 -0
  52. align_agent/writethrough/hooks.py +227 -0
  53. align_agent/writethrough/mcp_server.py +125 -0
  54. align_agent/writethrough/protocol.py +68 -0
  55. align_agent-0.4.3.dist-info/METADATA +224 -0
  56. align_agent-0.4.3.dist-info/RECORD +60 -0
  57. align_agent-0.4.3.dist-info/WHEEL +5 -0
  58. align_agent-0.4.3.dist-info/entry_points.txt +6 -0
  59. align_agent-0.4.3.dist-info/licenses/LICENSE +21 -0
  60. 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()