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/report.py
ADDED
|
@@ -0,0 +1,363 @@
|
|
|
1
|
+
"""Deterministic text and JSON reports for a change-centred run."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
from collections import Counter
|
|
7
|
+
from dataclasses import asdict
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
from diffgenome.change import ChangeSet
|
|
11
|
+
from diffgenome.graph import BehavioralGraph, GraphEdge, Metrics, Neighborhood
|
|
12
|
+
from diffgenome.model import EvidenceKind
|
|
13
|
+
from diffgenome.probe import ProbeAttempt
|
|
14
|
+
|
|
15
|
+
_GLYPH = {
|
|
16
|
+
EvidenceKind.OBSERVED: "→",
|
|
17
|
+
EvidenceKind.OBSERVED_SAMPLED: "→?",
|
|
18
|
+
EvidenceKind.COMPOSED: "⇢",
|
|
19
|
+
EvidenceKind.INTERNAL_GAP: "→ [gap]",
|
|
20
|
+
EvidenceKind.EXTERNAL_BOUNDARY: "→ [external]",
|
|
21
|
+
EvidenceKind.OS_BOUNDARY: "→ [os]",
|
|
22
|
+
EvidenceKind.UNRESOLVED_BOUNDARY: "→ [unresolved]",
|
|
23
|
+
EvidenceKind.STATIC: "--→",
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _short(symbol: str) -> str:
|
|
28
|
+
return symbol.split(":", 1)[-1]
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _edge_line(d: int, e: GraphEdge) -> str:
|
|
32
|
+
tail = f" tests={len(e.executions)}"
|
|
33
|
+
if e.kind is EvidenceKind.COMPOSED and e.best_join:
|
|
34
|
+
tail += f" join={e.best_join.name.lower()}"
|
|
35
|
+
if e.rules:
|
|
36
|
+
tail += f" rule={'/'.join(sorted(e.rules))}"
|
|
37
|
+
if e.probe_derived:
|
|
38
|
+
tail += " probe-derived"
|
|
39
|
+
return f" d{d} {_short(e.caller)} {_GLYPH[e.kind]} {_short(e.callee)}{tail}"
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def render_neighborhood(nb: Neighborhood, graph: BehavioralGraph, title: str) -> str:
|
|
43
|
+
lines = [f"## {title}", ""]
|
|
44
|
+
seeds = set(nb.seeds)
|
|
45
|
+
up = [(d, e) for d, e in nb.edges.values() if e.callee in seeds or e.caller not in seeds]
|
|
46
|
+
down = [(d, e) for d, e in nb.edges.values()]
|
|
47
|
+
# Partition by direction relative to the seeds using the walk that found them: an
|
|
48
|
+
# edge whose callee is a seed or whose callee leads to a seed is upstream.
|
|
49
|
+
traversable = (EvidenceKind.OBSERVED, EvidenceKind.COMPOSED)
|
|
50
|
+
upstream = sorted(
|
|
51
|
+
((d, e) for d, e in up if e.kind in traversable and _leads_to(e, seeds, graph)),
|
|
52
|
+
key=lambda x: (x[0], x[1].caller, x[1].callee),
|
|
53
|
+
)
|
|
54
|
+
downstream = sorted(
|
|
55
|
+
((d, e) for d, e in down if (d, e) not in upstream),
|
|
56
|
+
key=lambda x: (x[0], x[1].caller, x[1].callee),
|
|
57
|
+
)
|
|
58
|
+
lines.append("### Upstream (what reaches the change)")
|
|
59
|
+
lines.extend(_edge_line(d, e) for d, e in upstream[:60] or [])
|
|
60
|
+
if not upstream:
|
|
61
|
+
lines.append(" (nothing observed reaches the changed symbols)")
|
|
62
|
+
lines.append("")
|
|
63
|
+
lines.append("### Downstream (what continues from the change)")
|
|
64
|
+
lines.extend(_edge_line(d, e) for d, e in downstream[:120])
|
|
65
|
+
lines.append("")
|
|
66
|
+
lines.append(f"### Tests establishing these paths ({len(nb.tests)})")
|
|
67
|
+
lines.extend(f" {t.split('@')[0]}" for t in sorted(nb.tests)[:40])
|
|
68
|
+
if len(nb.tests) > 40:
|
|
69
|
+
lines.append(f" ... {len(nb.tests) - 40} more")
|
|
70
|
+
return "\n".join(lines)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _leads_to(e: GraphEdge, seeds: set[str], graph: BehavioralGraph, depth: int = 4) -> bool:
|
|
74
|
+
frontier: list[str] = [e.callee]
|
|
75
|
+
seen: set[str] = set()
|
|
76
|
+
while frontier and depth:
|
|
77
|
+
nxt: list[str] = []
|
|
78
|
+
for s in frontier:
|
|
79
|
+
if s in seeds:
|
|
80
|
+
return True
|
|
81
|
+
if s in seen:
|
|
82
|
+
continue
|
|
83
|
+
seen.add(s)
|
|
84
|
+
nxt.extend(
|
|
85
|
+
x.callee
|
|
86
|
+
for x in graph.out.get(s, [])
|
|
87
|
+
if x.kind in (EvidenceKind.OBSERVED, EvidenceKind.COMPOSED)
|
|
88
|
+
)
|
|
89
|
+
frontier, depth = nxt, depth - 1
|
|
90
|
+
return any(s in seeds for s in frontier)
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def render_metrics(before: Metrics, after: Metrics | None) -> str:
|
|
94
|
+
rows = [
|
|
95
|
+
("observed edges", "observed"), ("composed edges", "composed"),
|
|
96
|
+
("strong joins (VALUE+)", "strong_joins"), ("weak joins", "weak_joins"),
|
|
97
|
+
("internal gaps", "internal_gaps"), ("unresolved boundaries", "unresolved_boundaries"),
|
|
98
|
+
("external boundaries", "external_boundaries"), ("os boundaries", "os_boundaries"),
|
|
99
|
+
("unsound attempts", "unsound_attempts"), ("probe-derived edges", "probe_derived_edges"),
|
|
100
|
+
("symbols", "symbols"), ("tests", "tests"),
|
|
101
|
+
] # fmt: skip
|
|
102
|
+
out = ["| metric | before | after |", "|---|---|---|"]
|
|
103
|
+
for label, attr in rows:
|
|
104
|
+
b = getattr(before, attr)
|
|
105
|
+
a = getattr(after, attr) if after else "-"
|
|
106
|
+
out.append(f"| {label} | {b} | {a} |")
|
|
107
|
+
b = f"{before.reconstructed}/{before.denominator} = {before.ratio:.3f}"
|
|
108
|
+
a = f"{after.reconstructed}/{after.denominator} = {after.ratio:.3f}" if after else "-"
|
|
109
|
+
out.append(f"| reconstructed / provisional denominator | {b} | {a} |")
|
|
110
|
+
return "\n".join(out)
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def render_report(
|
|
114
|
+
change: ChangeSet,
|
|
115
|
+
graph_before: BehavioralGraph,
|
|
116
|
+
nb_before: Neighborhood,
|
|
117
|
+
attempts: list[ProbeAttempt],
|
|
118
|
+
graph_after: BehavioralGraph | None,
|
|
119
|
+
nb_after: Neighborhood | None,
|
|
120
|
+
notes: list[str],
|
|
121
|
+
) -> str:
|
|
122
|
+
m_before = nb_before.metrics()
|
|
123
|
+
m_after = nb_after.metrics() if nb_after else None
|
|
124
|
+
lines = ["# diffgenome behavioral impact report", ""]
|
|
125
|
+
lines.append(f"Change: {change.description}")
|
|
126
|
+
lines.append(f"Changed symbols ({len(change.symbols)}):")
|
|
127
|
+
for s in change.symbols:
|
|
128
|
+
n = len(graph_before.tests_by_symbol.get(s, ()))
|
|
129
|
+
lines.append(
|
|
130
|
+
f" {_short(s)} executed by {n} test(s)" + ("" if n else " ← no existing execution")
|
|
131
|
+
)
|
|
132
|
+
if change.unmapped_paths:
|
|
133
|
+
lines.append(
|
|
134
|
+
f"Changed files with no mapped code symbol: {', '.join(change.unmapped_paths)}"
|
|
135
|
+
)
|
|
136
|
+
lines.append("")
|
|
137
|
+
lines.append(
|
|
138
|
+
render_neighborhood(nb_before, graph_before, "Behavioral neighborhood (before probes)")
|
|
139
|
+
)
|
|
140
|
+
lines.append("")
|
|
141
|
+
lines.append("## Where knowledge stops (before probes)")
|
|
142
|
+
for kind in (
|
|
143
|
+
EvidenceKind.INTERNAL_GAP,
|
|
144
|
+
EvidenceKind.UNRESOLVED_BOUNDARY,
|
|
145
|
+
EvidenceKind.EXTERNAL_BOUNDARY,
|
|
146
|
+
):
|
|
147
|
+
es = nb_before.edges_of(kind)
|
|
148
|
+
if es:
|
|
149
|
+
lines.append(f"### {kind.value} ({len(es)})")
|
|
150
|
+
lines.extend(_edge_line(d, e) for d, e in es[:30])
|
|
151
|
+
weak = [(d, e) for d, e in nb_before.edges_of(EvidenceKind.COMPOSED) if not e.strong]
|
|
152
|
+
if weak:
|
|
153
|
+
lines.append(f"### weak joins ({len(weak)})")
|
|
154
|
+
lines.extend(_edge_line(d, e) for d, e in weak)
|
|
155
|
+
lines.append("")
|
|
156
|
+
lines.append("## Generated probes")
|
|
157
|
+
if not attempts:
|
|
158
|
+
lines.append(" (none attempted)")
|
|
159
|
+
for a in attempts:
|
|
160
|
+
o = a.objective
|
|
161
|
+
lines.append(
|
|
162
|
+
f"- objective: {o.kind} {_short(o.target)} (from {_short(o.caller)}, d{o.distance})"
|
|
163
|
+
)
|
|
164
|
+
lines.append(
|
|
165
|
+
f" attempt {a.attempt}: {a.verdict}" + (f" model={a.draft.model}" if a.draft else "")
|
|
166
|
+
)
|
|
167
|
+
for r in a.reasons[:8]:
|
|
168
|
+
lines.append(f" - {r}")
|
|
169
|
+
if a.learned:
|
|
170
|
+
lines.append(f" learned {len(a.learned)} observed edge(s):")
|
|
171
|
+
lines.extend(f" + {item}" for item in a.learned[:15])
|
|
172
|
+
lines.append("")
|
|
173
|
+
lines.append("## Before vs after")
|
|
174
|
+
lines.append(render_metrics(m_before, m_after))
|
|
175
|
+
lines.append("")
|
|
176
|
+
lines.append(
|
|
177
|
+
"Denominator is provisional: observed + composed edges + internal gaps + unresolved"
|
|
178
|
+
)
|
|
179
|
+
lines.append(
|
|
180
|
+
"stand-ins inside the neighborhood, i.e. the seams we know about. It excludes behavior"
|
|
181
|
+
)
|
|
182
|
+
lines.append(
|
|
183
|
+
"no execution has come near, and external/OS boundaries, which are terminal by design."
|
|
184
|
+
)
|
|
185
|
+
if nb_after and graph_after:
|
|
186
|
+
lines.append("")
|
|
187
|
+
lines.append("## Remaining after probes")
|
|
188
|
+
for kind in (EvidenceKind.INTERNAL_GAP, EvidenceKind.UNRESOLVED_BOUNDARY):
|
|
189
|
+
es = nb_after.edges_of(kind)
|
|
190
|
+
lines.append(f"### {kind.value} ({len(es)})")
|
|
191
|
+
lines.extend(_edge_line(d, e) for d, e in es[:30])
|
|
192
|
+
weak_after = [(d, e) for d, e in nb_after.edges_of(EvidenceKind.COMPOSED) if not e.strong]
|
|
193
|
+
lines.append(f"### weak joins ({len(weak_after)})")
|
|
194
|
+
lines.extend(_edge_line(d, e) for d, e in weak_after)
|
|
195
|
+
if notes:
|
|
196
|
+
lines.append("")
|
|
197
|
+
lines.append("## Notes")
|
|
198
|
+
lines.extend(f"- {n}" for n in notes)
|
|
199
|
+
return "\n".join(lines) + "\n"
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def report_json(
|
|
203
|
+
change: ChangeSet,
|
|
204
|
+
nb_before: Neighborhood,
|
|
205
|
+
attempts: list[ProbeAttempt],
|
|
206
|
+
nb_after: Neighborhood | None,
|
|
207
|
+
notes: list[str],
|
|
208
|
+
) -> str:
|
|
209
|
+
def edges(nb: Neighborhood) -> list[dict[str, Any]]:
|
|
210
|
+
return [
|
|
211
|
+
{
|
|
212
|
+
"distance": d,
|
|
213
|
+
"caller": e.caller,
|
|
214
|
+
"callee": e.callee,
|
|
215
|
+
"kind": e.kind.value,
|
|
216
|
+
"best_join": e.best_join.name if e.best_join else None,
|
|
217
|
+
"rules": sorted(e.rules),
|
|
218
|
+
"executions": sorted(e.executions),
|
|
219
|
+
"probe_derived": e.probe_derived,
|
|
220
|
+
"evidence": [
|
|
221
|
+
{
|
|
222
|
+
"kind": ev.kind.value,
|
|
223
|
+
"site": asdict(ev.site),
|
|
224
|
+
"fragment": asdict(ev.fragment) if ev.fragment else None,
|
|
225
|
+
"alternates": [asdict(a) for a in ev.alternates],
|
|
226
|
+
"join": ev.join.name if ev.join else None,
|
|
227
|
+
"rule": ev.rule,
|
|
228
|
+
}
|
|
229
|
+
for ev in e.evidence
|
|
230
|
+
],
|
|
231
|
+
}
|
|
232
|
+
for d, e in sorted(nb.edges.values(), key=lambda x: (x[0], x[1].caller, x[1].callee))
|
|
233
|
+
]
|
|
234
|
+
|
|
235
|
+
doc: dict[str, Any] = {
|
|
236
|
+
"change": {
|
|
237
|
+
"description": change.description,
|
|
238
|
+
"symbols": change.symbols,
|
|
239
|
+
"unmapped_paths": change.unmapped_paths,
|
|
240
|
+
},
|
|
241
|
+
"before": {
|
|
242
|
+
"metrics": nb_before.metrics().as_dict(),
|
|
243
|
+
"edges": edges(nb_before),
|
|
244
|
+
"tests": sorted(nb_before.tests),
|
|
245
|
+
},
|
|
246
|
+
"probes": [
|
|
247
|
+
{
|
|
248
|
+
"objective": {
|
|
249
|
+
"kind": a.objective.kind,
|
|
250
|
+
"target": a.objective.target,
|
|
251
|
+
"caller": a.objective.caller,
|
|
252
|
+
"distance": a.objective.distance,
|
|
253
|
+
},
|
|
254
|
+
"attempt": a.attempt,
|
|
255
|
+
"model": a.draft.model if a.draft else None,
|
|
256
|
+
"verdict": a.verdict,
|
|
257
|
+
"reasons": a.reasons,
|
|
258
|
+
"learned": a.learned,
|
|
259
|
+
"executions": [e.id for e in a.executions],
|
|
260
|
+
}
|
|
261
|
+
for a in attempts
|
|
262
|
+
],
|
|
263
|
+
"after": {
|
|
264
|
+
"metrics": nb_after.metrics().as_dict(),
|
|
265
|
+
"edges": edges(nb_after),
|
|
266
|
+
"tests": sorted(nb_after.tests),
|
|
267
|
+
}
|
|
268
|
+
if nb_after
|
|
269
|
+
else None,
|
|
270
|
+
"notes": notes,
|
|
271
|
+
}
|
|
272
|
+
return json.dumps(doc, indent=1) + "\n"
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
# --------------------------------------------------------------------------- map slice
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
def _annotate(e: GraphEdge, graph: BehavioralGraph) -> str:
|
|
279
|
+
bits = [e.kind.value]
|
|
280
|
+
if e.kind is EvidenceKind.COMPOSED and e.best_join:
|
|
281
|
+
bits.append(f"join={e.best_join.name.lower()}")
|
|
282
|
+
alt = sum(len(ev.alternates) for ev in e.evidence)
|
|
283
|
+
if alt:
|
|
284
|
+
bits.append(f"+{alt} same-shape")
|
|
285
|
+
if e.rules:
|
|
286
|
+
bits.append("rule=" + "/".join(sorted(e.rules)))
|
|
287
|
+
outs = graph.outcomes_by_symbol.get(e.callee)
|
|
288
|
+
if outs and e.kind in (EvidenceKind.OBSERVED, EvidenceKind.COMPOSED):
|
|
289
|
+
bits.append("outcomes=" + ",".join(f"{k}:{v}" for k, v in sorted(outs.items())))
|
|
290
|
+
bits.append(f"tests={len(e.executions)}")
|
|
291
|
+
if e.probe_derived:
|
|
292
|
+
bits.append("probe-derived")
|
|
293
|
+
return " [" + " ".join(bits) + "]"
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
def render_map_slice(graph: BehavioralGraph, seeds: list[str], up: int = 3, down: int = 4) -> str:
|
|
297
|
+
"""Human-readable slice of the repo-level graph around the seeds: callers above,
|
|
298
|
+
continuations below, every edge with its evidence. Deterministic. Cycles and repeats
|
|
299
|
+
are cut with `(see above)`."""
|
|
300
|
+
lines: list[str] = []
|
|
301
|
+
seen_down: set[tuple[str, str, EvidenceKind]] = set()
|
|
302
|
+
|
|
303
|
+
def down_tree(sym: str, prefix: str, depth: int, path: frozenset[str]) -> None:
|
|
304
|
+
kids = sorted(graph.out.get(sym, []), key=lambda e: (e.kind.value, e.callee))
|
|
305
|
+
for i, e in enumerate(kids):
|
|
306
|
+
last = i == len(kids) - 1
|
|
307
|
+
branch = "└" if last else "├"
|
|
308
|
+
key = (e.caller, e.callee, e.kind)
|
|
309
|
+
repeat = key in seen_down
|
|
310
|
+
seen_down.add(key)
|
|
311
|
+
label = f"{_GLYPH[e.kind]} {_short(e.callee)}"
|
|
312
|
+
lines.append(
|
|
313
|
+
f"{prefix}{branch}{label}{_annotate(e, graph)}"
|
|
314
|
+
+ (" (see above)" if repeat else "")
|
|
315
|
+
)
|
|
316
|
+
traversable = e.kind in (EvidenceKind.OBSERVED, EvidenceKind.COMPOSED)
|
|
317
|
+
if traversable and not repeat and depth < down and e.callee not in path:
|
|
318
|
+
down_tree(
|
|
319
|
+
e.callee, prefix + (" " if last else "│ "), depth + 1, path | {e.callee}
|
|
320
|
+
)
|
|
321
|
+
|
|
322
|
+
def up_tree(sym: str, prefix: str, depth: int, path: frozenset[str]) -> None:
|
|
323
|
+
parents = sorted(
|
|
324
|
+
(
|
|
325
|
+
e
|
|
326
|
+
for e in graph.inc.get(sym, [])
|
|
327
|
+
if e.kind in (EvidenceKind.OBSERVED, EvidenceKind.COMPOSED)
|
|
328
|
+
),
|
|
329
|
+
key=lambda e: (e.kind.value, e.caller),
|
|
330
|
+
)
|
|
331
|
+
for i, e in enumerate(parents):
|
|
332
|
+
last = i == len(parents) - 1
|
|
333
|
+
branch = "└" if last else "├"
|
|
334
|
+
lines.append(
|
|
335
|
+
f"{prefix}{branch}{_short(e.caller)} {_GLYPH[e.kind]}{_annotate(e, graph)}"
|
|
336
|
+
)
|
|
337
|
+
if depth < up and e.caller not in path:
|
|
338
|
+
up_tree(e.caller, prefix + (" " if last else "│ "), depth + 1, path | {e.caller})
|
|
339
|
+
|
|
340
|
+
for seed in seeds:
|
|
341
|
+
n_tests = len(graph.tests_by_symbol.get(seed, ()))
|
|
342
|
+
outs = graph.outcomes_by_symbol.get(seed, Counter())
|
|
343
|
+
origin = graph.origin(seed).value
|
|
344
|
+
lines.append(
|
|
345
|
+
f"# {_short(seed)} [origin={origin} executed_by={n_tests} outcomes={dict(outs)}]"
|
|
346
|
+
)
|
|
347
|
+
lines.append("## upstream (who reaches it; each line is `caller →/⇢ this`)")
|
|
348
|
+
if graph.inc.get(seed):
|
|
349
|
+
up_tree(seed, " ", 1, frozenset({seed}))
|
|
350
|
+
else:
|
|
351
|
+
lines.append(" (no observed or composed caller)")
|
|
352
|
+
lines.append("## downstream (what continues from it)")
|
|
353
|
+
if graph.out.get(seed):
|
|
354
|
+
down_tree(seed, " ", 1, frozenset({seed}))
|
|
355
|
+
else:
|
|
356
|
+
lines.append(" (no observed continuation)")
|
|
357
|
+
lines.append("")
|
|
358
|
+
legend = (
|
|
359
|
+
"legend: → observed ⇢ composed (join=symbol|arg_shape|value) → [gap] internal gap "
|
|
360
|
+
"→ [unresolved] stand-in not resolved → [external] outside the repository "
|
|
361
|
+
"→ [os] kernel boundary\n"
|
|
362
|
+
)
|
|
363
|
+
return legend + "\n" + "\n".join(lines)
|
diffgenome/resolve.py
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
"""Boundary resolution: classify a stand-in as internal / external / unresolved.
|
|
2
|
+
|
|
3
|
+
Ordered deterministic rules over facts only: the stand-in's claim and the claimed symbol's
|
|
4
|
+
recorded origin. No rule looks at names. The rule that fired is always returned.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from diffgenome.model import (
|
|
10
|
+
BoundaryClass,
|
|
11
|
+
BoundaryResolution,
|
|
12
|
+
Origin,
|
|
13
|
+
SubstitutionNode,
|
|
14
|
+
Symbol,
|
|
15
|
+
SymbolId,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def resolve(sub: SubstitutionNode, symbols: dict[SymbolId, Symbol]) -> BoundaryResolution:
|
|
20
|
+
claim = sub.claimed_target
|
|
21
|
+
if claim is None:
|
|
22
|
+
return BoundaryResolution(BoundaryClass.UNRESOLVED, None, "no-claim")
|
|
23
|
+
symbol = symbols.get(claim)
|
|
24
|
+
origin = symbol.origin if symbol else Origin.UNKNOWN
|
|
25
|
+
if origin is Origin.TEST:
|
|
26
|
+
return BoundaryResolution(BoundaryClass.UNRESOLVED, claim, "claim-in-tests")
|
|
27
|
+
if origin is Origin.EXTERNAL:
|
|
28
|
+
return BoundaryResolution(BoundaryClass.EXTERNAL, claim, "claim-outside-repo")
|
|
29
|
+
if origin is Origin.UNKNOWN:
|
|
30
|
+
return BoundaryResolution(BoundaryClass.UNRESOLVED, claim, "claim-unknown-origin")
|
|
31
|
+
if "()" in sub.path:
|
|
32
|
+
# The claim names the first member; what its *return value* is, nobody observed.
|
|
33
|
+
return BoundaryResolution(BoundaryClass.UNRESOLVED, claim, "claim-return-chain")
|
|
34
|
+
if sub.relation.startswith("static-return-type"):
|
|
35
|
+
# Resolved through a declared return type: static evidence, said so on the edge.
|
|
36
|
+
return BoundaryResolution(BoundaryClass.INTERNAL, claim, "static-return-type")
|
|
37
|
+
return BoundaryResolution(BoundaryClass.INTERNAL, claim, "claim-member")
|
diffgenome/runtime.py
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
"""The runtime seam: what the language-neutral pipeline needs from a target's runtime.
|
|
2
|
+
|
|
3
|
+
A `RuntimeAdapter` prepares a workspace copy for capture, runs a stimulus set under the
|
|
4
|
+
collector and returns protocol `Execution`s, says where a generated probe file goes, and
|
|
5
|
+
describes its conventions to the probe writer. A `SymbolIndex` maps source lines to
|
|
6
|
+
symbols with the collector's naming and extracts source for probe context. Nothing above
|
|
7
|
+
this module knows which runtime it is driving.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
from collections.abc import Sequence
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
from typing import Protocol
|
|
15
|
+
|
|
16
|
+
from diffgenome.model import Execution, Stimulus, SymbolId
|
|
17
|
+
from diffgenome.sandbox import Workspace
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class Definition(Protocol):
|
|
21
|
+
@property
|
|
22
|
+
def symbol(self) -> SymbolId: ...
|
|
23
|
+
@property
|
|
24
|
+
def path(self) -> str: ...
|
|
25
|
+
@property
|
|
26
|
+
def start(self) -> int: ...
|
|
27
|
+
@property
|
|
28
|
+
def end(self) -> int: ...
|
|
29
|
+
@property
|
|
30
|
+
def kind(self) -> str: ...
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class SymbolIndex(Protocol):
|
|
34
|
+
repo_root: Path
|
|
35
|
+
test_roots: list[Path]
|
|
36
|
+
|
|
37
|
+
def symbols_at(self, rel_path: str, lines: set[int]) -> Sequence[Definition]: ...
|
|
38
|
+
def find(self, symbol: SymbolId) -> Definition | None: ...
|
|
39
|
+
def source(self, symbol: SymbolId, context: int = 0) -> str | None: ...
|
|
40
|
+
def enclosing_class(self, symbol: SymbolId) -> Definition | None: ...
|
|
41
|
+
def module_of(self, symbol: SymbolId) -> str | None: ...
|
|
42
|
+
def tests_importing(self, module: str, limit: int = 3) -> list[str]: ...
|
|
43
|
+
def is_test(self, symbol: SymbolId) -> bool:
|
|
44
|
+
"""True when the symbol is defined in test code (test roots, test files, doubles)."""
|
|
45
|
+
...
|
|
46
|
+
|
|
47
|
+
def return_type_of(self, symbol: SymbolId) -> SymbolId | None:
|
|
48
|
+
"""Declared return type of a function as an in-repo class symbol, when the source
|
|
49
|
+
states one unambiguously; None otherwise. A static fact with its own provenance."""
|
|
50
|
+
...
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class RuntimeAdapter(Protocol):
|
|
54
|
+
name: str # e.g. "python/pytest", "node/jest"
|
|
55
|
+
|
|
56
|
+
def prepare(self, ws: Workspace) -> SymbolIndex:
|
|
57
|
+
"""Make the workspace copy capturable (instrument, write config) and return the
|
|
58
|
+
symbol index for the ORIGINAL repository (line numbers of the un-instrumented source)."""
|
|
59
|
+
...
|
|
60
|
+
|
|
61
|
+
def trace(
|
|
62
|
+
self, ws: Workspace, out_dir: Path, stimulus: Stimulus, only: list[str] | None
|
|
63
|
+
) -> tuple[list[Execution], str, str]:
|
|
64
|
+
"""Run the stimuli (all tests, or the given repo-relative paths) under the collector
|
|
65
|
+
inside the sandbox; return executions, stdout, stderr."""
|
|
66
|
+
...
|
|
67
|
+
|
|
68
|
+
def probe_relpath(self, tag: str) -> str:
|
|
69
|
+
"""Repo-relative path where a generated probe file must be written."""
|
|
70
|
+
...
|
|
71
|
+
|
|
72
|
+
def conventions(self, index: SymbolIndex) -> str:
|
|
73
|
+
"""Bounded text for the probe writer: framework, config, fixtures, import style."""
|
|
74
|
+
...
|