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
diffgenome/mvp.py ADDED
@@ -0,0 +1,398 @@
1
+ """One workflow: trace a repository's tests, build the map, centre on a change, probe the
2
+ gaps with generated unit probes under confinement, re-compose, report.
3
+
4
+ python -m diffgenome mvp --runtime python --repo <r> --python .venv/bin/python \\
5
+ --test-root tests --diff <rev> --writer openai --out out/run
6
+ python -m diffgenome mvp --runtime node --repo <r> --source-root src --test-root test/unit \\
7
+ --diff <rev> --writer openai --out out/run
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import argparse
13
+ import json
14
+ import os
15
+ import platform
16
+ import shlex
17
+ import sys
18
+ import time
19
+ from pathlib import Path
20
+ from typing import Any
21
+
22
+ from diffgenome.ambiguity import render_ambiguity
23
+ from diffgenome.change import ChangeSet, changes_from_diff, changes_from_symbols, git_diff
24
+ from diffgenome.change_artifact import build_change_artifact
25
+ from diffgenome.change_artifact import dumps as dump_artifact
26
+ from diffgenome.compose import build_corpus
27
+ from diffgenome.graph import build_graph
28
+ from diffgenome.llm import OpenAIProbeWriter, ProbeWriter, RecordedProbeWriter
29
+ from diffgenome.model import CallNode, Execution, Origin, Stimulus
30
+ from diffgenome.probe import ProbeAttempt, ProbeRunner, run_probe_loop
31
+ from diffgenome.projection import case_metrics, render_behavior_map, slice_json
32
+ from diffgenome.report import render_map_slice, render_report, report_json
33
+ from diffgenome.runtime import RuntimeAdapter, SymbolIndex
34
+ from diffgenome.runtime_evidence import build_runtime_evidence
35
+ from diffgenome.sandbox import Workspace, host_supports_confinement
36
+ from diffgenome.serialize import execution_from_json
37
+ from diffgenome.static_types import apply_static_return_types
38
+
39
+
40
+ def mechanics_frontier(executions: list[Execution], seeds: list[str]) -> set[str]:
41
+ """In-repo symbols observed as DIRECT callees of a changed symbol (or of a function
42
+ nested in one). The change's behavior often lives one call away, in code the diff does
43
+ not touch (Baserow #6069: the importer whose ordering is the phenotype)."""
44
+ seed_set = set(seeds)
45
+
46
+ def is_seed(sym: str) -> bool:
47
+ return sym in seed_set or any(sym.startswith(s + ".<locals>.") for s in seed_set)
48
+
49
+ out: set[str] = set()
50
+ for ex in executions:
51
+ for n in ex.nodes:
52
+ if not isinstance(n, CallNode) or n.parent is None:
53
+ continue
54
+ parent = ex.nodes[n.parent]
55
+ if isinstance(parent, CallNode) and is_seed(parent.symbol):
56
+ out.add(n.symbol)
57
+ return out
58
+
59
+
60
+ def write_mechanics(
61
+ out: Path,
62
+ ws: Workspace,
63
+ repo: Path,
64
+ runtime_name: str,
65
+ seeds: list[str],
66
+ index: SymbolIndex,
67
+ executions: list[Execution] | None = None,
68
+ ) -> None:
69
+ """Deterministic intra-procedural facts (decision sites, local def-use, control
70
+ requirements, field stores) from the ORIGINAL source. Go: the instrumenter's IR of every
71
+ repo function. Python: lowered here, for the files of the changed symbols and for the
72
+ files of in-repo functions they were observed calling directly (the frontier)."""
73
+ from diffgenome.dependence import analyze_all
74
+
75
+ functions: list[dict[str, Any]] = []
76
+ file_roles: dict[str, str] = {}
77
+ if runtime_name.startswith("go"):
78
+ ir = ws.root / "mechanics-ir.json"
79
+ if ir.is_file():
80
+ functions = json.loads(ir.read_text()).get("functions") or []
81
+ elif runtime_name.startswith("python"):
82
+ from diffgenome.frontends.python_ir import lower_functions
83
+
84
+ owner: dict[str, str] = {}
85
+ for s in seeds:
86
+ if (d := index.find(s)) is not None:
87
+ owner.setdefault(d.path, s)
88
+ file_roles[d.path] = "changed"
89
+ for s in sorted(mechanics_frontier(executions or [], seeds)):
90
+ d = index.find(s)
91
+ if d is None or index.is_test(s) or d.path in owner:
92
+ continue
93
+ owner[d.path] = s
94
+ file_roles[d.path] = "frontier"
95
+ for rel in sorted(owner):
96
+ module = index.module_of(owner[rel]) or ""
97
+ try:
98
+ # the analyzed revision's source (the workspace copy is not instrumented for
99
+ # Python; the checkout may be at another revision under --rev)
100
+ functions += lower_functions(
101
+ (ws.repo / rel).read_text(encoding="utf-8"), rel, module
102
+ )
103
+ except (OSError, SyntaxError):
104
+ continue
105
+ if functions:
106
+ (out / "mechanics.json").write_text(
107
+ json.dumps(
108
+ {
109
+ "format": "diffgenome-mechanics/0",
110
+ "files": file_roles,
111
+ "functions": analyze_all(functions),
112
+ },
113
+ indent=1,
114
+ )
115
+ + "\n"
116
+ )
117
+
118
+
119
+ def _write_status(out: Path, status: str, reason: str) -> None:
120
+ """When no diffgenome-change.json can be produced, say why in a file an integrator in
121
+ another job can read (diffgenome-status/1), instead of leaving only a missing file."""
122
+ (out / "diffgenome-status.json").write_text(
123
+ json.dumps({"format": "diffgenome-status/1", "status": status, "reason": reason}) + "\n"
124
+ )
125
+
126
+
127
+ def _log(msg: str) -> None:
128
+ print(f"[diffgenome {time.strftime('%H:%M:%S')}] {msg}", file=sys.stderr, flush=True)
129
+
130
+
131
+ def make_runtime(args: argparse.Namespace, repo: Path, pytest_args: list[str]) -> RuntimeAdapter:
132
+ tests = args.tests or args.test_root
133
+ if args.runtime == "python":
134
+ from diffgenome.collect.py_runtime import PytestRuntime
135
+
136
+ if not args.python:
137
+ raise SystemExit("--python is required for --runtime python")
138
+ # Never resolve symlinks: a venv's python is a symlink to the base interpreter, and
139
+ # the venv is selected by the *unresolved* executable path.
140
+ python = Path(
141
+ os.path.abspath(args.python if args.python.is_absolute() else repo / args.python)
142
+ )
143
+ test_env = dict(kv.split("=", 1) for kv in args.test_env)
144
+ return PytestRuntime(
145
+ repo, python, args.source_root, args.test_root, tests, pytest_args,
146
+ pythonpath=args.pythonpath, test_env=test_env, allow_loopback=args.allow_loopback,
147
+ ) # fmt: skip
148
+ if args.runtime == "node":
149
+ from diffgenome.collect.node_jest import NodeJestRuntime
150
+
151
+ return NodeJestRuntime(repo, args.source_root, args.test_root, tests)
152
+ if args.runtime == "go":
153
+ from diffgenome.collect.go_test import GoTestRuntime
154
+
155
+ return GoTestRuntime(repo, args.source_root, args.test_root, tests, args.mock_dir)
156
+ raise SystemExit(f"unknown runtime {args.runtime}")
157
+
158
+
159
+ def main(argv: list[str] | None = None) -> int:
160
+ ap = argparse.ArgumentParser(prog="diffgenome mvp")
161
+ ap.add_argument("--runtime", default="python", choices=["python", "node", "go"])
162
+ ap.add_argument(
163
+ "--mock-dir",
164
+ action="append",
165
+ default=[],
166
+ help="go: generated mock package dirs (test origin)",
167
+ )
168
+ ap.add_argument("--repo", required=True, type=Path)
169
+ ap.add_argument("--python", type=Path, help="target interpreter (python runtime)")
170
+ ap.add_argument("--source-root", default=".")
171
+ ap.add_argument("--test-root", required=True)
172
+ ap.add_argument("--tests", default=None, help="test target path (default: test root)")
173
+ ap.add_argument("--pytest-arg", action="append", default=[])
174
+ ap.add_argument(
175
+ "--pythonpath", action="append", default=[],
176
+ help="python: repo-relative import root, prepended inside the workspace (repeatable)",
177
+ ) # fmt: skip
178
+ ap.add_argument(
179
+ "--test-env", action="append", default=[],
180
+ help="python: KEY=VALUE for the target tests; non-secret test values (repeatable)",
181
+ ) # fmt: skip
182
+ ap.add_argument(
183
+ "--allow-loopback", action="store_true",
184
+ help="python: allow localhost sockets for local test services; other network denied",
185
+ ) # fmt: skip
186
+ ap.add_argument("--diff", help="git revspec: base..head, or a commit (vs its parent)")
187
+ ap.add_argument(
188
+ "--rev", help="analyze this git revision (workspace from `git archive`), not the checkout"
189
+ )
190
+ ap.add_argument("--symbol", action="append", default=[], help="explicit changed symbol")
191
+ ap.add_argument("--traces", type=Path, help="reuse existing-test traces from this directory")
192
+ ap.add_argument("--out", required=True, type=Path)
193
+ ap.add_argument("--workspaces", type=Path, default=None)
194
+ ap.add_argument("--writer", default="openai", help="openai | none | recorded:<dir>")
195
+ ap.add_argument("--model", default=None)
196
+ ap.add_argument("--probes", type=int, default=3)
197
+ ap.add_argument("--attempts", type=int, default=2)
198
+ ap.add_argument("--up", type=int, default=3)
199
+ ap.add_argument("--down", type=int, default=4)
200
+ ap.add_argument(
201
+ "--no-state", action="store_true", help="ignore state facts at seams (VALUE-only baseline)"
202
+ )
203
+ ap.add_argument(
204
+ "--probe-max-distance",
205
+ type=int,
206
+ default=None,
207
+ help="only probe gaps within this many hops of a changed symbol (integrated mode: 1)",
208
+ )
209
+ args = ap.parse_args(argv)
210
+
211
+ repo = args.repo.resolve()
212
+ out = args.out.resolve()
213
+ out.mkdir(parents=True, exist_ok=True)
214
+ if not host_supports_confinement():
215
+ # Invariant: target code never runs unconfined. Say so in one line an integrator
216
+ # can show verbatim, instead of failing later with a traceback.
217
+ reason = (
218
+ f"refusing to run: no supported OS sandbox on this host ({platform.system()}); "
219
+ "target tests are never executed unconfined"
220
+ )
221
+ _log(reason)
222
+ _write_status(out, "refused", reason)
223
+ return 3
224
+ pytest_args = [a for chunk in args.pytest_arg for a in shlex.split(chunk)]
225
+ runtime = make_runtime(args, repo, pytest_args)
226
+ notes: list[str] = []
227
+
228
+ ws = Workspace.create(repo, args.workspaces or out / "workspaces", rev=args.rev)
229
+ _log(f"workspace {ws.root} (copy of {repo}; original never written) runtime={runtime.name}")
230
+ try:
231
+ index: SymbolIndex = runtime.prepare(ws)
232
+ # 1. existing executions
233
+ if args.traces:
234
+ executions = [
235
+ execution_from_json(f.read_text()) for f in sorted(args.traces.glob("*.json"))
236
+ ]
237
+ _log(f"reusing {len(executions)} existing-test traces from {args.traces}")
238
+ else:
239
+ executions, stdout, stderr = runtime.trace(
240
+ ws, out / "traces-existing", Stimulus.EXISTING_TEST, None
241
+ )
242
+ (out / "existing-tests.log").write_text(stdout + "\n" + stderr)
243
+ tail = (stdout.strip() or stderr.strip()).splitlines()[-1:]
244
+ _log(f"existing tests: {tail} -> {len(executions)} executions")
245
+ if not executions and not args.traces:
246
+ _log("no executions captured; see existing-tests.log")
247
+ _write_status(
248
+ out, "failed", "no test executions captured (test command failed or found no tests)"
249
+ )
250
+ return 2
251
+ if not executions:
252
+ notes.append("cold start: no existing executions; every changed symbol is uncovered")
253
+ executions, n_static = apply_static_return_types(executions, index)
254
+ if n_static:
255
+ notes.append(
256
+ f"{n_static} factory().member stand-ins resolved through declared return "
257
+ "types (rule static-return-type)"
258
+ )
259
+ # 2. map
260
+ corpus = build_corpus(executions)
261
+ graph = build_graph(corpus, use_state=not args.no_state)
262
+ _log(f"map: {len(graph.edges)} edges, {len(graph.symbols)} symbols, {len(graph.gaps)} gaps")
263
+ # 3. change
264
+ change: ChangeSet
265
+ if args.diff:
266
+ change = changes_from_diff(git_diff(repo, args.diff), index, f"git diff {args.diff}")
267
+ elif args.symbol:
268
+ change = changes_from_symbols(args.symbol)
269
+ else:
270
+ ap.error("one of --diff or --symbol is required")
271
+ seeds = [
272
+ s
273
+ for s in change.symbols
274
+ if (graph.origin(s) is Origin.REPO or s not in graph.symbols)
275
+ and not _under_test_root(s, index)
276
+ ]
277
+ never_run = [s for s in seeds if s not in graph.tests_by_symbol]
278
+ if never_run:
279
+ notes.append(
280
+ f"{len(never_run)} changed symbol(s) have no existing execution at all: "
281
+ + ", ".join(s.split(":")[-1] for s in never_run[:8])
282
+ )
283
+ _log(f"change: {len(change.symbols)} symbols, {len(seeds)} in-repo seeds")
284
+ # 4-5. neighborhood
285
+ nb_before = graph.neighborhood(seeds, up=args.up, down=args.down)
286
+ _log(f"neighborhood: {nb_before.metrics().as_dict()}")
287
+ # 6-11. probes
288
+ attempts: list[ProbeAttempt] = []
289
+ corpus_after = corpus
290
+ writer: ProbeWriter | None = None
291
+ if args.writer == "openai":
292
+ try:
293
+ writer = OpenAIProbeWriter(model=args.model)
294
+ except RuntimeError as exc:
295
+ notes.append(f"no probe writer: {exc}")
296
+ _log(f"probes skipped: {exc}")
297
+ elif args.writer.startswith("recorded:"):
298
+ writer = RecordedProbeWriter(Path(args.writer.split(":", 1)[1]))
299
+ if writer is not None and args.probes > 0:
300
+ runner = ProbeRunner(ws, runtime)
301
+ skipped: list[str] = []
302
+ attempts, corpus_after = run_probe_loop(
303
+ graph, nb_before, index, writer, runner, out, args.probes, args.attempts,
304
+ args.up, args.down, skipped, max_distance=args.probe_max_distance,
305
+ ) # fmt: skip
306
+ notes.extend(f"not probeable: {s}" for s in skipped)
307
+ accepted = sum(a.verdict == "accepted" for a in attempts)
308
+ _log(f"probes: {accepted} accepted of {len(attempts)} attempts")
309
+ graph_after = build_graph(corpus_after, use_state=not args.no_state) if attempts else None
310
+ nb_after = (
311
+ graph_after.neighborhood(seeds, up=args.up, down=args.down) if graph_after else None
312
+ )
313
+ # 12. materialize the graph and the report
314
+ (out / "graph-before.json").write_text(json.dumps(graph.to_json(), indent=1) + "\n")
315
+ final_graph = graph_after or graph
316
+ (out / "graph.json").write_text(json.dumps(final_graph.to_json(), indent=1) + "\n")
317
+ evidence_slice = render_map_slice(final_graph, seeds, up=args.up, down=args.down)
318
+ (out / "map-evidence.md").write_text(evidence_slice)
319
+ declared = {s for s in seeds if (d := index.find(s)) is not None and d.kind == "class"}
320
+ behavior_map = render_behavior_map(
321
+ final_graph, seeds, up=args.up, down=args.down, declarations=declared
322
+ )
323
+ (out / "ambiguity.md").write_text(render_ambiguity(final_graph))
324
+ (out / "map.md").write_text(behavior_map)
325
+ (out / "slice.json").write_text(
326
+ json.dumps(slice_json(final_graph, seeds, args.up, args.down), indent=1) + "\n"
327
+ )
328
+ metrics = case_metrics(
329
+ final_graph, seeds, args.up, args.down, attempts, graph if graph_after else None
330
+ )
331
+ (out / "metrics.json").write_text(json.dumps(metrics, indent=1) + "\n")
332
+ text = render_report(change, graph, nb_before, attempts, graph_after, nb_after, notes)
333
+ text += "\n## Behavioral map around the changed symbols\n\n```\n" + behavior_map + "```\n"
334
+ text += (
335
+ "\nThe full repo-level graph with every edge's provenance is `graph.json` next to this "
336
+ "report (`graph-before.json` is the state before probes). Query it with "
337
+ "`python -m diffgenome inspect --graph graph.json --symbol <id or suffix>`.\n"
338
+ )
339
+ (out / "report.md").write_text(text)
340
+ (out / "report.json").write_text(report_json(change, nb_before, attempts, nb_after, notes))
341
+ # 13. the integration artifact: bounded, change-centred, no internal objects
342
+ artifact = build_change_artifact(
343
+ final_graph,
344
+ seeds,
345
+ up=args.up,
346
+ down=args.down,
347
+ change_spec=change.description,
348
+ runtime=runtime.name,
349
+ repo=str(repo),
350
+ revision=args.rev,
351
+ budget={
352
+ "writer": args.writer,
353
+ "probes_requested": args.probes if writer is not None else 0,
354
+ "attempts_per_probe": args.attempts,
355
+ "llm_calls": sum(1 for a in attempts if a.draft is not None)
356
+ if args.writer == "openai"
357
+ else 0,
358
+ "existing_traces_reused": bool(args.traces),
359
+ "probe_max_distance": args.probe_max_distance,
360
+ },
361
+ probes=[
362
+ {
363
+ "tag": f"{i}",
364
+ "objective": a.objective.kind,
365
+ "target": a.objective.target,
366
+ "attempt": a.attempt,
367
+ "verdict": a.verdict,
368
+ "reasons": list(a.reasons)[:3],
369
+ }
370
+ for i, a in enumerate(attempts)
371
+ ],
372
+ notes=notes,
373
+ graph_before=graph if graph_after else None,
374
+ )
375
+ write_mechanics(out, ws, repo, runtime.name, seeds, index, executions)
376
+ # the runtime-evidence contract (diffgenome-runtime/1): observed facts only
377
+ mech_path = out / "mechanics.json"
378
+ mech_fns = json.loads(mech_path.read_text())["functions"] if mech_path.is_file() else []
379
+ artifact["runtime"] = build_runtime_evidence(
380
+ executions,
381
+ change,
382
+ index,
383
+ mech_fns,
384
+ artifact.get("boundaries"),
385
+ test_scope=" ".join([args.tests or args.test_root, *pytest_args]).strip()[:500],
386
+ )
387
+ (out / "diffgenome-change.json").write_text(dump_artifact(artifact))
388
+ (out / "behavioral-map.md").write_text(behavior_map)
389
+ print(text)
390
+ _log(f"report: {out / 'report.md'} graph: {out / 'graph.json'} map: {out / 'map.md'}")
391
+ finally:
392
+ ws.destroy()
393
+ return 0
394
+
395
+
396
+ def _under_test_root(symbol: str, index: SymbolIndex) -> bool:
397
+ """True if the symbol is test code, as the runtime's index defines test code."""
398
+ return index.is_test(symbol)