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.
- diffgenome/__init__.py +7 -0
- diffgenome/__main__.py +240 -0
- diffgenome/_collectors/go/dg/dg.go +623 -0
- diffgenome/_collectors/go/go.mod +3 -0
- diffgenome/_collectors/go/instrument/facts.go +346 -0
- diffgenome/_collectors/go/instrument/main.go +484 -0
- diffgenome/_collectors/node/instrument.js +289 -0
- diffgenome/_collectors/node/jest-setup.js +40 -0
- diffgenome/_collectors/node/package-lock.json +35 -0
- diffgenome/_collectors/node/package.json +11 -0
- diffgenome/_collectors/node/runtime.js +426 -0
- diffgenome/ambiguity.py +122 -0
- diffgenome/api.py +67 -0
- diffgenome/change.py +86 -0
- diffgenome/change_artifact.py +310 -0
- diffgenome/collect/__init__.py +2 -0
- diffgenome/collect/go_test.py +271 -0
- diffgenome/collect/node_jest.py +319 -0
- diffgenome/collect/py_monitoring.py +985 -0
- diffgenome/collect/py_runtime.py +116 -0
- diffgenome/collect/py_symbols.py +238 -0
- diffgenome/collect/pytest_plugin.py +130 -0
- diffgenome/compose.py +469 -0
- diffgenome/dependence.py +264 -0
- diffgenome/evaluate.py +669 -0
- diffgenome/frontends/__init__.py +0 -0
- diffgenome/frontends/python_ir.py +335 -0
- diffgenome/genome.py +1016 -0
- diffgenome/genome_pipeline.py +674 -0
- diffgenome/genome_prompt.py +33 -0
- diffgenome/genome_state.py +2118 -0
- diffgenome/graph.py +426 -0
- diffgenome/llm.py +189 -0
- diffgenome/model.py +364 -0
- diffgenome/mvp.py +398 -0
- diffgenome/probe.py +509 -0
- diffgenome/projection.py +308 -0
- diffgenome/py.typed +0 -0
- diffgenome/render.py +118 -0
- diffgenome/report.py +363 -0
- diffgenome/resolve.py +37 -0
- diffgenome/runtime.py +74 -0
- diffgenome/runtime_evidence.py +261 -0
- diffgenome/sandbox.py +166 -0
- diffgenome/serialize.py +96 -0
- diffgenome/sites.py +19 -0
- diffgenome/static_types.py +69 -0
- diffgenome/structure.py +462 -0
- diffgenome-0.1.0.dist-info/METADATA +139 -0
- diffgenome-0.1.0.dist-info/RECORD +53 -0
- diffgenome-0.1.0.dist-info/WHEEL +4 -0
- diffgenome-0.1.0.dist-info/entry_points.txt +2 -0
- 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)
|