diffgenome 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (53) hide show
  1. diffgenome/__init__.py +7 -0
  2. diffgenome/__main__.py +240 -0
  3. diffgenome/_collectors/go/dg/dg.go +623 -0
  4. diffgenome/_collectors/go/go.mod +3 -0
  5. diffgenome/_collectors/go/instrument/facts.go +346 -0
  6. diffgenome/_collectors/go/instrument/main.go +484 -0
  7. diffgenome/_collectors/node/instrument.js +289 -0
  8. diffgenome/_collectors/node/jest-setup.js +40 -0
  9. diffgenome/_collectors/node/package-lock.json +35 -0
  10. diffgenome/_collectors/node/package.json +11 -0
  11. diffgenome/_collectors/node/runtime.js +426 -0
  12. diffgenome/ambiguity.py +122 -0
  13. diffgenome/api.py +67 -0
  14. diffgenome/change.py +86 -0
  15. diffgenome/change_artifact.py +310 -0
  16. diffgenome/collect/__init__.py +2 -0
  17. diffgenome/collect/go_test.py +271 -0
  18. diffgenome/collect/node_jest.py +319 -0
  19. diffgenome/collect/py_monitoring.py +985 -0
  20. diffgenome/collect/py_runtime.py +116 -0
  21. diffgenome/collect/py_symbols.py +238 -0
  22. diffgenome/collect/pytest_plugin.py +130 -0
  23. diffgenome/compose.py +469 -0
  24. diffgenome/dependence.py +264 -0
  25. diffgenome/evaluate.py +669 -0
  26. diffgenome/frontends/__init__.py +0 -0
  27. diffgenome/frontends/python_ir.py +335 -0
  28. diffgenome/genome.py +1016 -0
  29. diffgenome/genome_pipeline.py +674 -0
  30. diffgenome/genome_prompt.py +33 -0
  31. diffgenome/genome_state.py +2118 -0
  32. diffgenome/graph.py +426 -0
  33. diffgenome/llm.py +189 -0
  34. diffgenome/model.py +364 -0
  35. diffgenome/mvp.py +398 -0
  36. diffgenome/probe.py +509 -0
  37. diffgenome/projection.py +308 -0
  38. diffgenome/py.typed +0 -0
  39. diffgenome/render.py +118 -0
  40. diffgenome/report.py +363 -0
  41. diffgenome/resolve.py +37 -0
  42. diffgenome/runtime.py +74 -0
  43. diffgenome/runtime_evidence.py +261 -0
  44. diffgenome/sandbox.py +166 -0
  45. diffgenome/serialize.py +96 -0
  46. diffgenome/sites.py +19 -0
  47. diffgenome/static_types.py +69 -0
  48. diffgenome/structure.py +462 -0
  49. diffgenome-0.1.0.dist-info/METADATA +139 -0
  50. diffgenome-0.1.0.dist-info/RECORD +53 -0
  51. diffgenome-0.1.0.dist-info/WHEEL +4 -0
  52. diffgenome-0.1.0.dist-info/entry_points.txt +2 -0
  53. diffgenome-0.1.0.dist-info/licenses/LICENSE +202 -0
@@ -0,0 +1,319 @@
1
+ """Node/TypeScript runtime adapter: source instrumentation + Jest.
2
+
3
+ Everything Node-specific lives here and in diffgenome/_collectors/node. The workspace copy is
4
+ instrumented in place by `instrument.js` (our tool, our pinned TypeScript; no target code
5
+ runs), a workspace-only Jest config loads `jest-setup.js`, and the instrumented tests run
6
+ inside the sandbox with loopback allowed (test servers bind localhost; that is not egress).
7
+ The symbol index comes from the instrumenter's definition list, so diff lines map to the
8
+ same identities the collector emits.
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import json
14
+ import os
15
+ import shutil
16
+ import subprocess
17
+ from dataclasses import dataclass
18
+ from pathlib import Path
19
+
20
+ from diffgenome.model import Execution, Stimulus, SymbolId
21
+ from diffgenome.probe import copy_probe_traces
22
+ from diffgenome.runtime import SymbolIndex
23
+ from diffgenome.sandbox import Workspace
24
+ from diffgenome.serialize import execution_from_json
25
+
26
+ TOOLS = Path(__file__).resolve().parent.parent / "_collectors" / "node"
27
+
28
+
29
+ def _node_path(ws_repo: Path) -> str:
30
+ """Where the instrumenter finds `typescript`: an explicit DIFFGENOME_NODE_PATH, the
31
+ target's own node_modules (TypeScript projects carry it), then a node_modules next to the
32
+ collector (a development checkout). No dependency is bundled with the Python package."""
33
+ parts = [
34
+ os.environ.get("DIFFGENOME_NODE_PATH", ""),
35
+ str(ws_repo / "node_modules"),
36
+ str(TOOLS / "node_modules"),
37
+ ]
38
+ return os.pathsep.join(p for p in parts if p)
39
+
40
+
41
+ @dataclass(frozen=True)
42
+ class Definition:
43
+ symbol: SymbolId
44
+ path: str
45
+ start: int
46
+ end: int
47
+ kind: str
48
+ is_async: bool = False
49
+
50
+
51
+ class NodeSymbolIndex:
52
+ """Definitions as the instrumenter indexed them, against the ORIGINAL sources."""
53
+
54
+ def __init__(
55
+ self, repo_root: Path, source_roots: list[Path], test_roots: list[Path], index_file: Path
56
+ ) -> None:
57
+ self.repo_root = repo_root.resolve()
58
+ self.source_roots = [p.resolve() for p in source_roots]
59
+ self.test_roots = [p.resolve() for p in test_roots]
60
+ doc = json.loads(index_file.read_text())
61
+ self._defs: list[Definition] = [
62
+ Definition(
63
+ d["symbol"], d["path"], d["start"], d["end"], d["kind"], d.get("is_async", False)
64
+ )
65
+ for d in doc["definitions"]
66
+ ]
67
+ self._by_symbol = {d.symbol: d for d in self._defs}
68
+ self._by_path: dict[str, list[Definition]] = {}
69
+ for d in self._defs:
70
+ self._by_path.setdefault(d.path, []).append(d)
71
+
72
+ def definitions(self, rel_path: str) -> list[Definition]:
73
+ return self._by_path.get(rel_path, [])
74
+
75
+ def symbols_at(self, rel_path: str, lines: set[int]) -> list[Definition]:
76
+ hits: dict[SymbolId, Definition] = {}
77
+ for line in lines:
78
+ covering = [d for d in self.definitions(rel_path) if d.start <= line <= d.end]
79
+ funcs = [d for d in covering if d.kind == "function"]
80
+ pick = (
81
+ max(funcs, key=lambda d: d.start)
82
+ if funcs
83
+ else (max(covering, key=lambda d: d.start) if covering else None)
84
+ )
85
+ if pick:
86
+ hits[pick.symbol] = pick
87
+ return sorted(hits.values(), key=lambda d: (d.path, d.start))
88
+
89
+ def find(self, symbol: SymbolId) -> Definition | None:
90
+ return self._by_symbol.get(symbol)
91
+
92
+ def source(self, symbol: SymbolId, context: int = 0) -> str | None:
93
+ d = self.find(symbol)
94
+ if d is None:
95
+ return None
96
+ lines = (self.repo_root / d.path).read_text(encoding="utf-8").splitlines()
97
+ lo, hi = max(0, d.start - 1 - context), min(len(lines), d.end + context)
98
+ return "\n".join(f"{i + 1:5d} {lines[i]}" for i in range(lo, hi))
99
+
100
+ def enclosing_class(self, symbol: SymbolId) -> Definition | None:
101
+ d = self.find(symbol)
102
+ if d is None:
103
+ return None
104
+ classes = [
105
+ c for c in self.definitions(d.path)
106
+ if c.kind == "class" and c.start <= d.start and c.end >= d.end and c.symbol != d.symbol
107
+ ] # fmt: skip
108
+ return max(classes, key=lambda c: c.start) if classes else None
109
+
110
+ def is_test(self, symbol: SymbolId) -> bool:
111
+ d = self.find(symbol)
112
+ if d is None:
113
+ return False
114
+ path = self.repo_root / d.path
115
+ return any(path.is_relative_to(r) for r in self.test_roots)
116
+
117
+ def return_type_of(self, symbol: SymbolId) -> SymbolId | None:
118
+ return None # not extracted for this runtime yet
119
+
120
+ def module_of(self, symbol: SymbolId) -> str | None:
121
+ d = self.find(symbol)
122
+ if d is None:
123
+ return None
124
+ return d.path.rsplit(".", 1)[0] # e.g. src/api/services/PetService
125
+
126
+ def tests_importing(self, module: str, limit: int = 3) -> list[str]:
127
+ needle = module.split("/")[-1] # PetService
128
+ hits: list[str] = []
129
+ for root in self.test_roots:
130
+ for file in sorted(root.rglob("*.test.ts")) + sorted(root.rglob("*.spec.ts")):
131
+ try:
132
+ text = file.read_text(encoding="utf-8")
133
+ except (OSError, UnicodeDecodeError):
134
+ continue
135
+ if f"/{needle}'" in text or f'/{needle}"' in text:
136
+ hits.append(str(file.resolve().relative_to(self.repo_root)))
137
+ if len(hits) >= limit:
138
+ return hits
139
+ return hits
140
+
141
+
142
+ class NodeJestRuntime:
143
+ name = "node/jest"
144
+
145
+ def __init__(
146
+ self, repo: Path, source_root: str, test_root: str, tests: str, node: Path | None = None
147
+ ) -> None:
148
+ self.repo = repo.resolve()
149
+ self.source_root = source_root
150
+ self.test_root = test_root
151
+ self.tests = tests
152
+ which = shutil.which("node")
153
+ found = node or (Path(which) if which else None)
154
+ if found is None:
155
+ raise RuntimeError("node not found on PATH")
156
+ self.node = found.resolve()
157
+ self.index_file: Path | None = None
158
+
159
+ # ------------------------------------------------------------------ preparation
160
+
161
+ def prepare(self, ws: Workspace) -> SymbolIndex:
162
+ # Dependencies are the target's environment (like a venv): linked read-only, never copied.
163
+ nm = self.repo / "node_modules"
164
+ if nm.is_dir() and not (ws.repo / "node_modules").exists():
165
+ os.symlink(nm, ws.repo / "node_modules")
166
+ index_file = ws.root / "symbol-index.json"
167
+ result = subprocess.run(
168
+ [
169
+ str(self.node),
170
+ str(TOOLS / "instrument.js"),
171
+ "--root",
172
+ str(ws.repo),
173
+ "--src",
174
+ self.source_root,
175
+ "--tests",
176
+ self.test_root,
177
+ "--runtime",
178
+ str(TOOLS / "runtime.js"),
179
+ "--index",
180
+ str(index_file),
181
+ ],
182
+ env={**os.environ, "NODE_PATH": _node_path(ws.repo)},
183
+ capture_output=True,
184
+ text=True,
185
+ check=True,
186
+ )
187
+ (ws.root / "instrument.log").write_text(result.stdout + result.stderr)
188
+ self.index_file = index_file
189
+ self._write_jest_config(ws)
190
+ # the analyzed revision's copy, not the checkout (which may be at another revision)
191
+ return NodeSymbolIndex(
192
+ ws.repo, [ws.repo / self.source_root], [ws.repo / self.test_root], index_file
193
+ )
194
+
195
+ def _jest_major(self) -> int:
196
+ try:
197
+ pkg = json.loads((self.repo / "node_modules" / "jest" / "package.json").read_text())
198
+ return int(str(pkg["version"]).split(".")[0])
199
+ except (OSError, KeyError, ValueError):
200
+ return 29
201
+
202
+ def _write_jest_config(self, ws: Workspace) -> None:
203
+ setup = str(TOOLS / "jest-setup.js")
204
+ major = self._jest_major()
205
+ base_expr = (
206
+ 'require("./package.json").jest || {}'
207
+ if (ws.repo / "package.json").is_file()
208
+ and "jest" in json.loads((ws.repo / "package.json").read_text())
209
+ else ('require("./jest.config.js")' if (ws.repo / "jest.config.js").is_file() else "{}")
210
+ )
211
+ if major < 24:
212
+ # The original setup file is chained by jest-setup.js via DIFFGENOME_CHAIN_SETUP.
213
+ hook = f" setupTestFrameworkScriptFile: {json.dumps(setup)},\n"
214
+ else:
215
+ hook = (
216
+ f" setupFilesAfterEnv: [{json.dumps(setup)}]"
217
+ ".concat(base.setupFilesAfterEnv || []),\n"
218
+ )
219
+ # optional: a pure-JS bcrypt when the target's native binding cannot build here
220
+ shim = next(
221
+ (
222
+ p
223
+ for p in (
224
+ ws.repo / "node_modules" / "bcryptjs",
225
+ TOOLS / "node_modules" / "bcryptjs",
226
+ )
227
+ if p.is_dir()
228
+ ),
229
+ TOOLS / "node_modules" / "bcryptjs",
230
+ )
231
+ mapper = ""
232
+ if shim.is_dir():
233
+ # bcrypt's native binding does not build on this Node; the pure-JS port is an
234
+ # external substitution and is recorded as such in the run notes.
235
+ mapper = (
236
+ " moduleNameMapper: Object.assign({}, base.moduleNameMapper || {}, "
237
+ f'{{ "^bcrypt$": {json.dumps(str(shim))} }}),\n'
238
+ )
239
+ (ws.repo / "diffgenome.jest.config.js").write_text(
240
+ f"const base = {base_expr};\n"
241
+ "module.exports = Object.assign({}, base, {\n"
242
+ " rootDir: __dirname,\n"
243
+ f"{hook}{mapper}"
244
+ "});\n"
245
+ )
246
+ self._chain_setup = None
247
+ if major < 24:
248
+ base = (
249
+ json.loads((ws.repo / "package.json").read_text()).get("jest", {})
250
+ if (ws.repo / "package.json").is_file()
251
+ else {}
252
+ )
253
+ original = base.get("setupTestFrameworkScriptFile")
254
+ if original:
255
+ self._chain_setup = str(ws.repo / original.replace("<rootDir>/", ""))
256
+
257
+ # ------------------------------------------------------------------ execution
258
+
259
+ def trace(
260
+ self, ws: Workspace, out_dir: Path, stimulus: Stimulus, only: list[str] | None
261
+ ) -> tuple[list[Execution], str, str]:
262
+ tag = "existing" if stimulus is Stimulus.EXISTING_TEST else "probe"
263
+ traces_ws = ws.root / f"traces-{tag}-{len(list(ws.root.glob('traces-*')))}"
264
+ traces_ws.mkdir(parents=True, exist_ok=True)
265
+ env = {
266
+ "DIFFGENOME_OUT": str(traces_ws),
267
+ "DIFFGENOME_RUNTIME": str(TOOLS / "runtime.js"),
268
+ "DIFFGENOME_STIMULUS": stimulus.value,
269
+ "NODE_OPTIONS": "--max-old-space-size=2048",
270
+ "CI": "1",
271
+ }
272
+ if getattr(self, "_chain_setup", None):
273
+ env["DIFFGENOME_CHAIN_SETUP"] = self._chain_setup or ""
274
+ argv = [
275
+ str(self.node), str(ws.repo / "node_modules" / "jest" / "bin" / "jest.js"),
276
+ "--config", "diffgenome.jest.config.js", "--runInBand", "--forceExit",
277
+ *(only if only else [self.tests]),
278
+ ] # fmt: skip
279
+ result = ws.run(argv, env=env, timeout=1800, allow_loopback=True)
280
+ out_dir.mkdir(parents=True, exist_ok=True)
281
+ copy_probe_traces(traces_ws, out_dir)
282
+ executions = [execution_from_json(f.read_text()) for f in sorted(out_dir.glob("*.json"))]
283
+ return executions, result.stdout, result.stderr
284
+
285
+ def probe_relpath(self, tag: str) -> str:
286
+ return f"{self.test_root}/diffgenome_probe_{tag}.test.ts"
287
+
288
+ def conventions(self, index: SymbolIndex) -> str:
289
+ major = self._jest_major()
290
+ rel = os.path.relpath(self.repo / self.source_root, self.repo / self.test_root)
291
+ parts = [
292
+ f"Runtime: Node.js {self._node_version()}, TypeScript, Jest {major} (TypeScript is "
293
+ f"compiled by the repository's own preprocessor, no type checking). The probe is a "
294
+ f"Jest test file named *.test.ts placed directly in {self.test_root}/, so imports of "
295
+ f"the source use '{rel}/...' (for example '{rel}/lib/logger/Logger'). Use ES imports "
296
+ f"at the top of the file like the existing tests do."
297
+ ]
298
+ if major < 24:
299
+ parts.append(
300
+ f"Jest {major} API caveats: jest.isolateModules, setupFilesAfterEnv and "
301
+ "expect().resolves/rejects-with-toMatchObject on older matchers may not exist; "
302
+ "use jest.mock(path, factory) at top level, jest.fn(), jest.resetModules() + "
303
+ "require(), and async test bodies."
304
+ )
305
+ pkg = self.repo / "package.json"
306
+ if pkg.is_file():
307
+ doc = json.loads(pkg.read_text())
308
+ jest_cfg = doc.get("jest")
309
+ if jest_cfg:
310
+ parts.append("jest config (package.json):\n" + json.dumps(jest_cfg, indent=1)[:800])
311
+ return "\n\n".join(parts)
312
+
313
+ def _node_version(self) -> str:
314
+ try:
315
+ return subprocess.run(
316
+ [str(self.node), "--version"], capture_output=True, text=True, timeout=10
317
+ ).stdout.strip()
318
+ except (OSError, subprocess.TimeoutExpired):
319
+ return "?"