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,292 @@
1
+ """The tail-diff coherence mechanism, decoupled from any one agent scaffold.
2
+
3
+ Unlike full-file JIT Tail Refresh (Arm 7), which dumps the entire file (2k-4.2k
4
+ lines, $O(N)$ tokens) and rewrites earlier turns in the history (annihilating
5
+ the KV-cache prefix), this mechanism:
6
+ 1. Is strictly append-only: past conversation turns are never modified,
7
+ preserving 100% of the prefix KV-cache in local engines (vLLM, SGLang) and APIs.
8
+ 2. Computes the net unified diff or line-numbered diff hunk between what the
9
+ agent last saw (held view) and the current live file on disk ($O(\\Delta)$ tokens).
10
+ 3. Naturally captures multiple disjoint hunks across large files (solving the
11
+ single-window limitation of Arm 9 Write-Through Viewport, which misses edits
12
+ outside a +/- 25 line window).
13
+ 4. Handles sequential conflicting overwrites by collapsing intermediate mutations
14
+ into the exact net diff from the agent's held view to live disk.
15
+ """
16
+
17
+ import copy
18
+ import difflib
19
+ import os
20
+ from typing import Any, Dict, List, Optional, Protocol, Tuple
21
+
22
+ TAIL_DIFF_CONTEXT = 3
23
+ TAIL_DIFF_MAX_FILES = 3
24
+ TAIL_DIFF_MAX_LINE = 200
25
+ TAIL_DIFF_MAX_CHARS = 10_000
26
+
27
+
28
+ class _BindingsSource(Protocol):
29
+ bindings: List[object] # each binding needs .file_path and .file_content
30
+
31
+
32
+ def _is_binary(path: str) -> bool:
33
+ try:
34
+ with open(path, "rb") as f:
35
+ return b"\0" in f.read(8192)
36
+ except OSError:
37
+ return True
38
+
39
+
40
+ def _clip(line: str) -> str:
41
+ body = line.rstrip("\n")
42
+ if len(body) <= TAIL_DIFF_MAX_LINE:
43
+ return line
44
+ tail = "\n" if line.endswith("\n") else ""
45
+ return f"{body[:TAIL_DIFF_MAX_LINE]} ...[{len(body) - TAIL_DIFF_MAX_LINE} more characters]{tail}"
46
+
47
+
48
+ def format_line_numbered_diff(
49
+ held_content: str,
50
+ disk_content: str,
51
+ filename: str,
52
+ context: int = TAIL_DIFF_CONTEXT,
53
+ step_seen: Optional[int] = None,
54
+ current_step: Optional[int] = None,
55
+ ) -> str:
56
+ """Format a diff between held content and live disk with explicit live line numbers."""
57
+ held_lines = held_content.splitlines()
58
+ disk_lines = disk_content.splitlines()
59
+ matcher = difflib.SequenceMatcher(None, held_lines, disk_lines)
60
+
61
+ step_info = f" (held at Step {step_seen})" if step_seen is not None else ""
62
+ curr_info = f" (Step {current_step})" if current_step is not None else ""
63
+
64
+ blocks: List[str] = []
65
+ for tag, alo, ahi, blo, bhi in matcher.get_opcodes():
66
+ if tag == "equal":
67
+ continue
68
+
69
+ c_alo = max(0, alo - context)
70
+ c_ahi = min(len(held_lines), ahi + context)
71
+ c_blo = max(0, blo - context)
72
+ c_bhi = min(len(disk_lines), bhi + context)
73
+
74
+ hunk_header = (
75
+ f"@@ Held Lines {c_alo + 1}-{c_ahi}{step_info} "
76
+ f"-> Live Disk Lines {c_blo + 1}-{c_bhi}{curr_info} @@"
77
+ )
78
+ lines_out = [hunk_header]
79
+
80
+ # Context lines before change (using current live disk line numbers)
81
+ for lno in range(c_blo, blo):
82
+ lines_out.append(f"{lno + 1:4d} {_clip(disk_lines[lno])}")
83
+
84
+ # Removed lines from held view
85
+ for lno in range(alo, ahi):
86
+ lines_out.append(f" - {_clip(held_lines[lno])}")
87
+
88
+ # Added / modified lines on disk with current line numbers
89
+ for lno in range(blo, bhi):
90
+ lines_out.append(f"{lno + 1:4d} + {_clip(disk_lines[lno])}")
91
+
92
+ # Context lines after change
93
+ for lno in range(bhi, c_bhi):
94
+ lines_out.append(f"{lno + 1:4d} {_clip(disk_lines[lno])}")
95
+
96
+ blocks.append("\n".join(lines_out))
97
+
98
+ if not blocks:
99
+ return ""
100
+
101
+ return (
102
+ f"[TAIL DIFF: File '{filename}' mutated on disk vs your held view]:\n"
103
+ + "\n\n".join(blocks)
104
+ )
105
+
106
+
107
+ def tail_diff_blocks(
108
+ workspace_dir: str,
109
+ files: List[str],
110
+ freshness_hook: Optional[_BindingsSource] = None,
111
+ before_contents: Optional[Dict[str, str]] = None,
112
+ context_lines: int = TAIL_DIFF_CONTEXT,
113
+ line_numbered: bool = True,
114
+ current_step: Optional[int] = None,
115
+ ) -> Tuple[str, List[str]]:
116
+ """Compute tail-diff blocks for the specified files against held views.
117
+
118
+ The held view is the latest binding from a step before `current_step`: a
119
+ read in the step that wrote the file was registered with the file as it
120
+ stood after the write, and diffing against it would show nothing. With no
121
+ earlier view, the content before this step is the baseline.
122
+
123
+ Returns the formatted diff text and the list of files successfully shown.
124
+ """
125
+ if not files:
126
+ return "", []
127
+
128
+ bindings = getattr(freshness_hook, "bindings", None) or []
129
+ from .viewport import _ignored # generated output is not file state (harness v12)
130
+ ignored = _ignored(workspace_dir, list(files))
131
+ files = [f for f in files if f not in ignored]
132
+ known = set(before_contents or ()) | {getattr(b, "file_path", None) for b in bindings}
133
+ blocks: List[str] = []
134
+ shown: List[str] = []
135
+ used_chars = 0
136
+
137
+ for rel_path in sorted(files, key=lambda p: (p not in known, p)):
138
+ abs_path = os.path.join(workspace_dir, rel_path) if not os.path.isabs(rel_path) else rel_path
139
+ if not os.path.isfile(abs_path) or _is_binary(abs_path):
140
+ continue
141
+ if len(shown) >= TAIL_DIFF_MAX_FILES:
142
+ break
143
+
144
+ try:
145
+ with open(abs_path, "r", encoding="utf-8", errors="replace") as f:
146
+ disk_content = f.read()
147
+ except OSError:
148
+ continue
149
+
150
+ # Locate held content and step ID
151
+ held_content = None
152
+ step_seen = None
153
+ for b in reversed(bindings):
154
+ if getattr(b, "file_path", None) != rel_path:
155
+ continue
156
+ if current_step is not None and getattr(b, "step_id", None) is not None and b.step_id >= current_step:
157
+ continue # this step's own read: the file after the write
158
+ held_content = getattr(b, "file_content", None) or getattr(b, "raw_content", None)
159
+ step_seen = getattr(b, "step_id", None)
160
+ break
161
+
162
+ if held_content is None and before_contents and rel_path in before_contents:
163
+ held_content = before_contents[rel_path]
164
+
165
+ if held_content is None:
166
+ # No prior view exists: agent never read this file
167
+ continue
168
+
169
+ if held_content == disk_content:
170
+ # File matches held view: no staleness
171
+ continue
172
+
173
+ if line_numbered:
174
+ diff_text = format_line_numbered_diff(
175
+ held_content, disk_content, rel_path, context=context_lines, step_seen=step_seen
176
+ )
177
+ else:
178
+ diff_lines = list(difflib.unified_diff(
179
+ held_content.splitlines(keepends=True),
180
+ disk_content.splitlines(keepends=True),
181
+ fromfile=f"{rel_path} (held at Step {step_seen})",
182
+ tofile=f"{rel_path} (current on disk)",
183
+ n=context_lines,
184
+ ))
185
+ diff_text = f"[TAIL DIFF: File '{rel_path}']\n" + "".join(_clip(l) for l in diff_lines)
186
+
187
+ if not diff_text:
188
+ continue
189
+
190
+ if used_chars + len(diff_text) > TAIL_DIFF_MAX_CHARS:
191
+ diff_text = diff_text[: TAIL_DIFF_MAX_CHARS - used_chars] + "\n...[truncated]"
192
+
193
+ blocks.append(diff_text)
194
+ shown.append(rel_path)
195
+ used_chars += len(diff_text)
196
+ if used_chars >= TAIL_DIFF_MAX_CHARS:
197
+ break
198
+
199
+ if not blocks:
200
+ return "", []
201
+
202
+ return "\n\n" + "\n\n".join(blocks), shown
203
+
204
+
205
+ def apply_tail_diff(
206
+ messages: List[Dict[str, Any]],
207
+ bindings: List[object],
208
+ workspace_root: str = ".",
209
+ line_numbered: bool = True,
210
+ ) -> List[Dict[str, Any]]:
211
+ """Transform context by appending net diffs of stale files to prompt tail.
212
+
213
+ Crucially:
214
+ - messages[:-1] is NEVER touched, guaranteeing 100% prefix KV-cache reuse.
215
+ - Only files that are currently stale are diffed against the agent's held view.
216
+ - The diff is appended to the tail message.
217
+ """
218
+ transformed = copy.deepcopy(messages)
219
+ if not transformed:
220
+ return transformed
221
+
222
+ # Find distinct stale files
223
+ latest_by_file: Dict[str, object] = {}
224
+ for b in bindings:
225
+ if getattr(b, "is_invalid", False):
226
+ latest_by_file[getattr(b, "file_path", "")] = b
227
+
228
+ if not latest_by_file:
229
+ return transformed
230
+
231
+ diff_blocks: List[str] = []
232
+ for f_path, binding in latest_by_file.items():
233
+ abs_p = os.path.join(workspace_root, f_path) if not os.path.isabs(f_path) else f_path
234
+ if os.path.exists(abs_p) and os.path.isfile(abs_p):
235
+ try:
236
+ with open(abs_p, "r", encoding="utf-8", errors="replace") as f:
237
+ disk_content = f.read()
238
+ held_content = getattr(binding, "file_content", None) or getattr(binding, "raw_content", None)
239
+ if held_content is None or held_content == disk_content:
240
+ continue
241
+
242
+ step_seen = getattr(binding, "step_id", None)
243
+ inval_step = getattr(binding, "invalidation_step", None)
244
+
245
+ if line_numbered:
246
+ diff_block = format_line_numbered_diff(
247
+ held_content=held_content,
248
+ disk_content=disk_content,
249
+ filename=f_path,
250
+ context=TAIL_DIFF_CONTEXT,
251
+ step_seen=step_seen,
252
+ current_step=inval_step,
253
+ )
254
+ else:
255
+ diff_lines = list(difflib.unified_diff(
256
+ held_content.splitlines(keepends=True),
257
+ disk_content.splitlines(keepends=True),
258
+ fromfile=f"{f_path} (held at Step {step_seen})",
259
+ tofile=f"{f_path} (current on disk at Step {inval_step})",
260
+ n=TAIL_DIFF_CONTEXT,
261
+ ))
262
+ diff_block = f"[TAIL DIFF: File '{f_path}']\n" + "".join(_clip(l) for l in diff_lines)
263
+
264
+ if diff_block:
265
+ diff_blocks.append(diff_block)
266
+ except Exception:
267
+ pass
268
+
269
+ if diff_blocks:
270
+ forwarding_text = "\n\n" + "\n\n".join(diff_blocks)
271
+ if transformed[-1].get("role") in ["user", "tool"]:
272
+ transformed[-1]["content"] += forwarding_text
273
+ else:
274
+ transformed.append({"role": "user", "content": forwarding_text.strip()})
275
+
276
+ return transformed
277
+
278
+
279
+ def line_number_note(path, base, new, limit=4):
280
+ """Every stretch of the model's last whole-file view of `path` that has moved since, with its
281
+ offset. Counted from that one view, so offsets from several writes add up instead of each being
282
+ stated against the previous block (which, in a 60-run test, led agents to apply a shift twice or
283
+ miss one; see docs/results/shift_test_anthropic_claude-sonnet-5.md)."""
284
+ moved = [(i1 + 1, i2, j1 + 1, j2, j1 - i1) for tag, i1, i2, j1, j2 in
285
+ difflib.SequenceMatcher(None, base.splitlines(), new.splitlines(), autojunk=False).get_opcodes()
286
+ if tag == "equal" and j1 != i1]
287
+ if not moved:
288
+ return ""
289
+ parts = [f"lines {a}-{b} are now {c}-{d} ({o:+d})" for a, b, c, d, o in moved[:limit]]
290
+ if len(moved) > limit:
291
+ parts.append(f"and {len(moved) - limit} more moved stretches")
292
+ return f"[LINE NUMBERS: since your last full view of '{path}': " + "; ".join(parts) + "]"
@@ -0,0 +1,220 @@
1
+ """The write-through viewport fix, decoupled from any one agent scaffold.
2
+
3
+ This is the entire mechanism behind Arm 9 (write-through viewport): after a
4
+ step writes a file, compute a small line-numbered window centered on the
5
+ edited region and hand it back as text to append to that step's tool output.
6
+ Unlike retroactive prompt-rewriting arms, this never touches earlier turns --
7
+ the caller appends the returned text to the *current* step's own output,
8
+ before it enters the message history, so the transcript stays append-only
9
+ and prefix caches stay valid.
10
+
11
+ Nothing here is specific to mini-SWE-agent or to any particular action
12
+ syntax. The three inputs are generic: a workspace directory, the relative
13
+ paths a step wrote (however the caller detected that -- a filesystem
14
+ snapshot diff is the recommended method: `os.walk` the workspace before and
15
+ after the step, compare (mtime_ns, size) per file, and diff the two maps.
16
+ Command-text parsing cannot keep up with the real range of ways a step can
17
+ write a file: sed -i, an editor tool, a heredoc, a Python script that opens
18
+ the file itself), and the file's content immediately before the step, so the
19
+ edited region can be located.
20
+
21
+ Minimal wiring pattern for a new scaffold:
22
+
23
+ before = snapshot_workspace(workspace_dir)
24
+ before_contents = {p: read(p) for p in referenced_paths}
25
+ ... run the step ...
26
+ after = snapshot_workspace(workspace_dir)
27
+ writes = sorted(p for p, meta in after.items() if before.get(p) != meta)
28
+ if writes:
29
+ step_output += viewport_echo(workspace_dir, writes, freshness_hook, before_contents)
30
+
31
+ `freshness_hook` is optional: pass `None` (or any object with an empty
32
+ `.bindings` list) if the caller doesn't already track per-file observation
33
+ history. Its only use here is as a fallback source for a file's prior
34
+ content when the caller didn't capture `before_contents` for a path that
35
+ turns out to have been written -- with no fallback available, the viewport
36
+ simply centers on the file's midpoint instead of the edited region.
37
+ """
38
+
39
+ import os
40
+ import subprocess
41
+ from typing import Dict, List, Optional, Protocol, Tuple
42
+
43
+ VIEWPORT_HALF = 25 # lines above and below the edit centre
44
+ # Bounds (harness v11). The viewport is appended after the observation template
45
+ # has folded the command's output, so nothing else limits it. Without them,
46
+ # `npm test` writing coverage files (one 2.85M-character line) and
47
+ # `git checkout <rev> -- src/` each produced a single observation of millions
48
+ # of characters. The total matches the template's 10,000-character fold.
49
+ VIEWPORT_MAX_FILES = 3
50
+ VIEWPORT_MAX_LINE = 200
51
+ VIEWPORT_MAX_CHARS = 10_000
52
+
53
+
54
+ def snapshot_workspace(workspace_dir: str, skip_dirs: Optional[set] = None) -> Dict[str, Tuple[int, int]]:
55
+ """Map every workspace file to (mtime_ns, size).
56
+
57
+ Used to detect writes by diffing before/after a step. Inferring writes
58
+ from command text cannot keep up with the shell surface agents actually
59
+ use (sed -i, python scripts that open(...,'w'), patch, cp, tee, editors),
60
+ and missing a write means a stale binding is never invalidated, which
61
+ silently zeroes out the measurement.
62
+ """
63
+ skip = skip_dirs or {".git", "__pycache__", "node_modules", ".egg-info"}
64
+ snap: Dict[str, Tuple[int, int]] = {}
65
+ for root, dirs, files in os.walk(workspace_dir):
66
+ dirs[:] = [d for d in dirs if d not in skip]
67
+ for name in files:
68
+ full = os.path.join(root, name)
69
+ try:
70
+ st = os.stat(full)
71
+ except OSError:
72
+ continue
73
+ snap[os.path.relpath(full, workspace_dir)] = (st.st_mtime_ns, st.st_size)
74
+ return snap
75
+
76
+
77
+ def diff_snapshots(before: Dict[str, Tuple[int, int]], after: Dict[str, Tuple[int, int]]) -> List[str]:
78
+ """Files created or modified between two snapshots."""
79
+ return sorted(p for p, meta in after.items() if before.get(p) != meta)
80
+
81
+
82
+ class _BindingsSource(Protocol):
83
+ bindings: List[object] # each binding needs .file_path and .file_content
84
+
85
+
86
+ def _is_binary(path: str) -> bool:
87
+ try:
88
+ with open(path, "rb") as f:
89
+ return b"\0" in f.read(8192)
90
+ except OSError:
91
+ return True
92
+
93
+
94
+ def _ignored(workspace_dir: str, paths: List[str]) -> set:
95
+ """The paths git ignores in this workspace: generated output (coverage,
96
+ build artefacts, dependencies), not file state the agent acts on. Empty
97
+ when the workspace is not a git repository."""
98
+ if not paths:
99
+ return set()
100
+ try:
101
+ res = subprocess.run(["git", "-C", workspace_dir, "check-ignore", "--stdin"], input="\n".join(paths),
102
+ capture_output=True, text=True, timeout=30)
103
+ except (OSError, subprocess.SubprocessError):
104
+ return set()
105
+ return set(res.stdout.split("\n")) if res.returncode in (0, 1) else set()
106
+
107
+
108
+ def _clip(line: str) -> str:
109
+ body = line.rstrip("\n")
110
+ if len(body) <= VIEWPORT_MAX_LINE:
111
+ return line
112
+ tail = "\n" if line.endswith("\n") else ""
113
+ return f"{body[:VIEWPORT_MAX_LINE]} ...[{len(body) - VIEWPORT_MAX_LINE} more characters]{tail}"
114
+
115
+
116
+ def viewport_blocks(
117
+ workspace_dir: str,
118
+ writes: List[str],
119
+ freshness_hook: Optional[_BindingsSource] = None,
120
+ before_contents: Optional[Dict[str, str]] = None,
121
+ ) -> Tuple[str, List[str]]:
122
+ """The viewport text for a step's writes, and the files it shows.
123
+
124
+ A line-numbered window onto each just-written file, centered on the edit.
125
+ `writes` are workspace-relative paths a step wrote (see module docstring
126
+ for how to detect these). `before_contents` maps a subset of those paths
127
+ to their content immediately before the step, used to locate the edited
128
+ region; a path without an entry there falls back to the most recent
129
+ binding in `freshness_hook.bindings` for that path, then to the file's
130
+ midpoint if neither is available.
131
+
132
+ At most VIEWPORT_MAX_FILES files are shown, lines are clipped to
133
+ VIEWPORT_MAX_LINE characters and the whole to VIEWPORT_MAX_CHARS; files
134
+ the agent had read come first, and the rest are named in a closing line.
135
+ Binary files are skipped. Only the returned files may be registered as
136
+ seen: a written file the bounds left out was not shown to the agent.
137
+ """
138
+ if not writes:
139
+ return "", []
140
+
141
+ bindings = getattr(freshness_hook, "bindings", None) or []
142
+ ignored = _ignored(workspace_dir, list(writes))
143
+ writes = [w for w in writes if w not in ignored]
144
+ known = set(before_contents or ()) | {getattr(b, "file_path", None) for b in bindings}
145
+ blocks: List[str] = []
146
+ shown: List[str] = []
147
+ left_out: List[str] = []
148
+ used = 0
149
+ for rel_path in sorted(writes, key=lambda p: (p not in known, p)):
150
+ abs_path = os.path.join(workspace_dir, rel_path)
151
+ if not os.path.isfile(abs_path) or _is_binary(abs_path):
152
+ continue
153
+ if len(shown) >= VIEWPORT_MAX_FILES:
154
+ left_out.append(rel_path)
155
+ continue
156
+ try:
157
+ with open(abs_path, "r", encoding="utf-8", errors="replace") as f:
158
+ lines = f.readlines()
159
+ except OSError:
160
+ continue
161
+
162
+ total = len(lines)
163
+ # Find the centre of the edit region by diffing against the
164
+ # before-snapshot content. Fall back to the file midpoint.
165
+ centre = total // 2
166
+ old_content = None
167
+ if before_contents and rel_path in before_contents:
168
+ old_content = before_contents[rel_path]
169
+ else:
170
+ for b in reversed(bindings):
171
+ if getattr(b, "file_path", None) == rel_path and getattr(b, "file_content", None):
172
+ old_content = b.file_content
173
+ break
174
+
175
+ if old_content is not None:
176
+ old_lines = old_content.splitlines(keepends=True)
177
+ first_diff = None
178
+ for i, (a, b) in enumerate(zip(old_lines, lines)):
179
+ if a != b:
180
+ first_diff = i
181
+ break
182
+ if first_diff is None and len(old_lines) != total:
183
+ first_diff = min(len(old_lines), total)
184
+ if first_diff is not None:
185
+ centre = first_diff
186
+
187
+ half = VIEWPORT_HALF
188
+ if total <= half * 2:
189
+ start, end = 0, total
190
+ else:
191
+ start = max(0, centre - half)
192
+ end = min(total, start + half * 2)
193
+ if end - start < half * 2:
194
+ start = max(0, end - half * 2)
195
+
196
+ viewport = "".join(f"{i}\t{_clip(line)}" for i, line in enumerate(lines[start:end], start=start + 1))
197
+ block = f"[VIEWPORT (lines {start + 1}-{end} of {total}): {rel_path}]\n{viewport}"
198
+ if used + len(block) > VIEWPORT_MAX_CHARS:
199
+ left_out.append(rel_path)
200
+ continue
201
+ blocks.append(block)
202
+ shown.append(rel_path)
203
+ used += len(block)
204
+
205
+ if left_out:
206
+ names = ", ".join(left_out[:10]) + (", ..." if len(left_out) > 10 else "")
207
+ blocks.append(f"[{len(left_out)} more written files not shown: {names}]")
208
+ if not blocks:
209
+ return "", []
210
+ return "\n\n" + "\n\n".join(blocks), shown
211
+
212
+
213
+ def viewport_echo(
214
+ workspace_dir: str,
215
+ writes: List[str],
216
+ freshness_hook: Optional[_BindingsSource] = None,
217
+ before_contents: Optional[Dict[str, str]] = None,
218
+ ) -> str:
219
+ """The text of `viewport_blocks` alone."""
220
+ return viewport_blocks(workspace_dir, writes, freshness_hook, before_contents)[0]
@@ -0,0 +1,38 @@
1
+ """Metrics package for staleness replay and statistical analysis.
2
+
3
+ The statistics helpers pull in statsmodels, scipy and numpy. Importing them
4
+ here made every consumer of this package pay that cost, including the
5
+ experiment runner, which needs nothing but the corruption oracles: on one
6
+ machine a scipy/numpy version mismatch in the active interpreter turned
7
+ `from wt_src.metrics.corruption_oracles import ...` into an import error, which
8
+ has nothing to do with what the caller asked for. They are resolved on first
9
+ attribute access instead.
10
+
11
+ The paper's numbers come from src/metrics/replay.py (one stale-edit
12
+ definition for both research questions) via scripts/run_phase1_analysis.py
13
+ and scripts/recompute_metrics.py.
14
+ """
15
+
16
+ from typing import Any
17
+
18
+ _LAZY = {
19
+ "compute_windowed_odds_ratios": ".statistics",
20
+ "fit_length_controlled_logistic_regression": ".statistics",
21
+ "fit_stale_edit_association": ".statistics",
22
+ }
23
+
24
+
25
+ def __getattr__(name: str) -> Any:
26
+ module_name = _LAZY.get(name)
27
+ if module_name is None:
28
+ raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
29
+ from importlib import import_module
30
+
31
+ return getattr(import_module(module_name, __name__), name)
32
+
33
+
34
+ __all__ = [
35
+ "compute_windowed_odds_ratios",
36
+ "fit_length_controlled_logistic_regression",
37
+ "fit_stale_edit_association",
38
+ ]