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,308 @@
1
+ """Behavioral projection of the evidence graph: production behavior with evidence attached.
2
+
3
+ `graph.json` stays canonical. This module derives the human/product view: one behavioral
4
+ edge per production caller→callee pair (observed and composed evidence merged), tests and
5
+ probes as evidence annotations rather than branches, boundaries as leaves, and
6
+ declarations (symbols with no in-repo executable body) shown as what they are, not as
7
+ gaps. It also produces the change-centred JSON slice and the per-case metrics table.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ from collections import Counter, defaultdict
13
+ from dataclasses import dataclass, field
14
+ from typing import Any
15
+
16
+ from diffgenome.graph import BehavioralGraph
17
+ from diffgenome.model import EvidenceKind, JoinStrength, Origin, Stimulus, SymbolId
18
+
19
+ _BOUNDARY = {
20
+ EvidenceKind.EXTERNAL_BOUNDARY: "external",
21
+ EvidenceKind.OS_BOUNDARY: "os",
22
+ EvidenceKind.UNRESOLVED_BOUNDARY: "unresolved",
23
+ EvidenceKind.INTERNAL_GAP: "gap",
24
+ }
25
+
26
+
27
+ def _short(symbol: str) -> str:
28
+ return symbol.split(":", 1)[-1]
29
+
30
+
31
+ @dataclass
32
+ class BehaviorEdge:
33
+ """One production caller→callee relationship with all its evidence merged."""
34
+
35
+ caller: SymbolId
36
+ callee: SymbolId
37
+ boundary: (
38
+ str | None
39
+ ) # None for production continuations; else external/os/unresolved/gap/declaration
40
+ observed_executions: set[str] = field(default_factory=set)
41
+ composed: Counter[str] = field(default_factory=Counter) # grade -> distinct fragment shapes
42
+ alternates: int = 0
43
+ tests: set[str] = field(default_factory=set)
44
+ probes: set[str] = field(default_factory=set)
45
+ rules: set[str] = field(default_factory=set)
46
+ ambiguous: bool = False # more than one materially different continuation at one seam
47
+
48
+ @property
49
+ def glyph(self) -> str:
50
+ if self.boundary:
51
+ return {"external": "→ [external]", "os": "→ [os]", "unresolved": "→ [unresolved]",
52
+ "gap": "→ [gap]", "declaration": "→ [declaration]"}[self.boundary] # fmt: skip
53
+ return "→" if self.observed_executions else "⇢"
54
+
55
+ @property
56
+ def best_grade(self) -> str | None:
57
+ if not self.composed:
58
+ return None
59
+ return max(self.composed, key=lambda g: JoinStrength[g].value)
60
+
61
+ def evidence_line(self) -> str:
62
+ bits = []
63
+ if self.observed_executions:
64
+ bits.append(f"observed {len(self.observed_executions)} exec")
65
+ for grade in sorted(self.composed, key=lambda g: JoinStrength[g].value, reverse=True):
66
+ bits.append(f"composed {grade} x{self.composed[grade]}")
67
+ if self.alternates:
68
+ bits.append(f"+{self.alternates} same-shape")
69
+ if self.ambiguous:
70
+ bits.append("AMBIGUOUS")
71
+ if self.rules:
72
+ bits.append("rule=" + "/".join(sorted(self.rules)))
73
+ bits.append(f"tests {len(self.tests)}")
74
+ if self.probes:
75
+ bits.append(f"probes {len(self.probes)}")
76
+ return " · ".join(bits)
77
+
78
+
79
+ def _is_probe(execution_id: str, graph: BehavioralGraph) -> bool:
80
+ if graph.corpus is not None and execution_id in graph.corpus.executions:
81
+ return graph.corpus.executions[execution_id].stimulus is Stimulus.GENERATED_PROBE
82
+ return "diffgenome_probe" in execution_id
83
+
84
+
85
+ def _symbol_kind(graph: BehavioralGraph, symbol: SymbolId) -> str:
86
+ s = graph.symbols.get(symbol)
87
+ return getattr(s, "kind", "callable") if s else "callable"
88
+
89
+
90
+ def project(graph: BehavioralGraph) -> dict[tuple[SymbolId, SymbolId], BehaviorEdge]:
91
+ """Merge the evidence graph into behavioral edges between production symbols (and
92
+ boundary leaves). Test-origin callers are dropped as nodes and kept as evidence."""
93
+ out: dict[tuple[SymbolId, SymbolId], BehaviorEdge] = {}
94
+ for e in graph.edges.values():
95
+ if graph.origin(e.caller) is Origin.TEST:
96
+ continue # tests are evidence on the callee's incoming edges, not behavior
97
+ boundary = _BOUNDARY.get(e.kind)
98
+ if e.kind is EvidenceKind.INTERNAL_GAP and _symbol_kind(graph, e.callee) == "declaration":
99
+ boundary = "declaration"
100
+ key = (e.caller, e.callee)
101
+ be = out.get(key)
102
+ if be is None:
103
+ be = out[key] = BehaviorEdge(e.caller, e.callee, boundary)
104
+ elif be.boundary and not boundary:
105
+ be.boundary = None # a real continuation exists; the boundary was one seed's view
106
+ if e.kind is EvidenceKind.OBSERVED:
107
+ be.observed_executions.update(e.executions)
108
+ if e.kind is EvidenceKind.COMPOSED:
109
+ per_site: dict[Any, int] = defaultdict(int)
110
+ for ev in e.evidence:
111
+ if ev.join:
112
+ be.composed[ev.join.name] += 1
113
+ be.alternates += len(ev.alternates)
114
+ per_site[ev.site] += 1
115
+ if any(n > 1 for n in per_site.values()):
116
+ be.ambiguous = True
117
+ be.rules.update(e.rules)
118
+ for x in e.executions:
119
+ (be.probes if _is_probe(x, graph) else be.tests).add(x)
120
+ # incoming evidence from tests: attach to the callee's production edges as tests
121
+ return out
122
+
123
+
124
+ def _tests_reaching(graph: BehavioralGraph, symbol: SymbolId) -> set[str]:
125
+ return set(graph.tests_by_symbol.get(symbol, ()))
126
+
127
+
128
+ def render_behavior_map(
129
+ graph: BehavioralGraph,
130
+ seeds: list[SymbolId],
131
+ up: int = 3,
132
+ down: int = 4,
133
+ declarations: set[SymbolId] | None = None,
134
+ ) -> str:
135
+ edges = project(graph)
136
+ out_by: dict[SymbolId, list[BehaviorEdge]] = defaultdict(list)
137
+ inc_by: dict[SymbolId, list[BehaviorEdge]] = defaultdict(list)
138
+ for be in edges.values():
139
+ out_by[be.caller].append(be)
140
+ inc_by[be.callee].append(be)
141
+ lines: list[str] = [
142
+ "legend: → observed continuation ⇢ composed continuation (grade) "
143
+ "[external] outside the repo [unresolved] stand-in without a resolution "
144
+ "[gap] internal continuation nobody has executed "
145
+ "[declaration] construction/declaration with no in-repo executable body",
146
+ "",
147
+ ]
148
+ seen: set[tuple[str, str]] = set()
149
+
150
+ def entry_evidence(sym: SymbolId) -> str:
151
+ tests = sorted(t.split("@")[0] for t in _tests_reaching(graph, sym))
152
+ probes = [t for t in tests if "diffgenome_probe" in t]
153
+ tests = [t for t in tests if "diffgenome_probe" not in t]
154
+ parts = []
155
+ if tests:
156
+ parts.append(
157
+ f"tests {len(tests)}: " + ", ".join(tests[:3]) + (" …" if len(tests) > 3 else "")
158
+ )
159
+ if probes:
160
+ parts.append(f"probes {len(probes)}")
161
+ return "; ".join(parts) if parts else "no execution"
162
+
163
+ def down_tree(sym: SymbolId, prefix: str, depth: int, path: frozenset[str]) -> None:
164
+ kids = sorted(out_by.get(sym, []), key=lambda b: (b.boundary or "", b.callee))
165
+ for i, be in enumerate(kids):
166
+ last = i == len(kids) - 1
167
+ branch = "└" if last else "├"
168
+ cont = prefix + (" " if last else "│ ")
169
+ repeat = (be.caller, be.callee) in seen
170
+ seen.add((be.caller, be.callee))
171
+ grade = f" ({be.best_grade})" if not be.observed_executions and be.best_grade else ""
172
+ lines.append(
173
+ f"{prefix}{branch}{be.glyph} {_short(be.callee)}{grade}"
174
+ + (" (see above)" if repeat else "")
175
+ )
176
+ if not repeat:
177
+ lines.append(f"{cont} evidence: {be.evidence_line()}")
178
+ if be.boundary is None and not repeat and depth < down and be.callee not in path:
179
+ down_tree(be.callee, cont, depth + 1, path | {be.callee})
180
+
181
+ def up_tree(sym: SymbolId, prefix: str, depth: int, path: frozenset[str]) -> None:
182
+ parents = sorted(
183
+ (b for b in inc_by.get(sym, []) if b.boundary is None), key=lambda b: b.caller
184
+ )
185
+ for i, be in enumerate(parents):
186
+ last = i == len(parents) - 1
187
+ branch = "└" if last else "├"
188
+ cont = prefix + (" " if last else "│ ")
189
+ is_entry = not any(b.boundary is None for b in inc_by.get(be.caller, []))
190
+ tag = " [entrypoint]" if is_entry else ""
191
+ grade = f" ({be.best_grade})" if not be.observed_executions and be.best_grade else ""
192
+ lines.append(f"{prefix}{branch}{_short(be.caller)}{tag} {be.glyph}{grade} this")
193
+ lines.append(f"{cont} evidence: {be.evidence_line()}")
194
+ if is_entry:
195
+ lines.append(f"{cont} reached by: {entry_evidence(be.caller)}")
196
+ if depth < up and be.caller not in path:
197
+ up_tree(be.caller, cont, depth + 1, path | {be.caller})
198
+
199
+ for seed in seeds:
200
+ if declarations and seed in declarations:
201
+ lines.append(f"# {_short(seed)} [changed declaration: no executable body of its own]")
202
+ lines.append("")
203
+ continue
204
+ outs = graph.outcomes_by_symbol.get(seed, Counter())
205
+ outcome_text = dict(outs) or "never executed"
206
+ reached = entry_evidence(seed)
207
+ lines.append(
208
+ f"# {_short(seed)} [changed; outcomes={outcome_text}; reached by: {reached}]"
209
+ )
210
+ lines.append("## reaches the change")
211
+ if any(b.boundary is None for b in inc_by.get(seed, [])):
212
+ up_tree(seed, " ", 1, frozenset({seed}))
213
+ else:
214
+ lines.append(
215
+ " (no production caller observed or composed; only tests/probes call it directly)"
216
+ )
217
+ lines.append("## continues from the change")
218
+ if out_by.get(seed):
219
+ down_tree(seed, " ", 1, frozenset({seed}))
220
+ else:
221
+ lines.append(" (no continuation observed)")
222
+ lines.append("")
223
+ return "\n".join(lines) + "\n"
224
+
225
+
226
+ def slice_json(graph: BehavioralGraph, seeds: list[SymbolId], up: int, down: int) -> dict[str, Any]:
227
+ nb = graph.neighborhood(seeds, up=up, down=down)
228
+ projected = project(graph)
229
+ keys = {(e.caller, e.callee) for _, e in nb.edges.values()}
230
+ return {
231
+ "seeds": seeds,
232
+ "behavioral_edges": [
233
+ {
234
+ "caller": be.caller,
235
+ "callee": be.callee,
236
+ "kind": "observed"
237
+ if be.observed_executions
238
+ else ("composed" if be.composed else be.boundary),
239
+ "boundary": be.boundary,
240
+ "best_join": be.best_grade,
241
+ "composed_shapes_by_grade": dict(be.composed),
242
+ "same_shape_alternates": be.alternates,
243
+ "ambiguous": be.ambiguous,
244
+ "rules": sorted(be.rules),
245
+ "tests": sorted(t.split("@")[0] for t in be.tests),
246
+ "probes": sorted(t.split("@")[0] for t in be.probes),
247
+ }
248
+ for (c, k), be in sorted(projected.items())
249
+ if (c, k) in keys and graph.origin(c) is not Origin.TEST
250
+ ],
251
+ "metrics": nb.metrics().as_dict(),
252
+ }
253
+
254
+
255
+ def case_metrics(
256
+ graph: BehavioralGraph,
257
+ seeds: list[SymbolId],
258
+ up: int,
259
+ down: int,
260
+ attempts: list[Any],
261
+ before: BehavioralGraph | None = None,
262
+ ) -> dict[str, Any]:
263
+ """The compact table every case study reports. `ratio` is structural coverage only."""
264
+ nb = graph.neighborhood(seeds, up=up, down=down)
265
+ projected = project(graph)
266
+ keys = {(e.caller, e.callee) for _, e in nb.edges.values()}
267
+ bes = [
268
+ be
269
+ for (c, k), be in projected.items()
270
+ if (c, k) in keys and graph.origin(c) is not Origin.TEST
271
+ ]
272
+ joins: Counter[str] = Counter()
273
+ for be in bes:
274
+ if be.best_grade and not be.observed_executions:
275
+ joins[be.best_grade] += 1
276
+ sites = {ev.site for _, e in nb.edges.values() for ev in e.evidence}
277
+ rejected = sum(1 for a in graph.attempts if not a.accepted and a.site in sites)
278
+ m = nb.metrics()
279
+
280
+ def counts(g: BehavioralGraph) -> dict[str, int]:
281
+ n = g.neighborhood(seeds, up=up, down=down).metrics()
282
+ return {"strong_joins": n.strong_joins, "weak_joins": n.weak_joins, "gaps": n.internal_gaps,
283
+ "observed": n.observed, "composed": n.composed} # fmt: skip
284
+
285
+ return {
286
+ "changed_symbols": len(seeds),
287
+ "behavioral_symbols_in_neighborhood": len(
288
+ {s for be in bes for s in (be.caller, be.callee) if graph.origin(s) is Origin.REPO}
289
+ ),
290
+ "observed_edges": sum(1 for be in bes if be.observed_executions and be.boundary is None),
291
+ "composed_edges": sum(1 for be in bes if not be.observed_executions and be.composed),
292
+ "joins": {g: joins.get(g, 0) for g in ("SYMBOL", "ARG_SHAPE", "VALUE", "STATE")},
293
+ "rejected_joins": rejected,
294
+ "ambiguous_joins": sum(1 for be in bes if be.ambiguous),
295
+ "internal_gaps": sum(1 for be in bes if be.boundary == "gap"),
296
+ "declarations_no_in_repo_body": sum(1 for be in bes if be.boundary == "declaration"),
297
+ "unresolved_boundaries": sum(1 for be in bes if be.boundary == "unresolved"),
298
+ "external_boundaries": sum(1 for be in bes if be.boundary == "external"),
299
+ "existing_tests_contributing": len({t for be in bes for t in be.tests}),
300
+ "probe_derived_edges": sum(1 for be in bes if be.probes),
301
+ "llm_calls": len(attempts),
302
+ "accepted_probes": sum(1 for a in attempts if a.verdict == "accepted"),
303
+ "rejected_probes": sum(1 for a in attempts if a.verdict != "accepted"),
304
+ "before_after": {"before": counts(before), "after": counts(graph)}
305
+ if before is not None
306
+ else None,
307
+ "structural_coverage_ratio_NOT_correctness": round(m.ratio, 3),
308
+ }
diffgenome/py.typed ADDED
File without changes
diffgenome/render.py ADDED
@@ -0,0 +1,118 @@
1
+ """Deterministic text rendering of a single `Execution` as a tree.
2
+
3
+ Glyphs: `→` observed call, `⊗` stand-in (with mechanism, claim and relation), `⚡` OS event,
4
+ `{tN}` a call on thread N. No composition happens here; see the composer for `⇢`.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from collections import defaultdict
10
+
11
+ from diffgenome.compose import Branch, Composition
12
+ from diffgenome.model import (
13
+ ArgShapes,
14
+ CallNode,
15
+ Edge,
16
+ EvidenceKind,
17
+ Execution,
18
+ Node,
19
+ OsEventNode,
20
+ SubstitutionNode,
21
+ )
22
+
23
+
24
+ def fmt_outcome(outcome: str) -> str:
25
+ return "" if outcome == "returned" else f" !{outcome}"
26
+
27
+
28
+ def fmt_args(args: ArgShapes) -> str:
29
+ return f" ({', '.join(f'{n}={s}' for n, s, _ in args)})" if args else ""
30
+
31
+
32
+ def _label(node: Node) -> str:
33
+ if isinstance(node, CallNode):
34
+ thread = f" {{t{node.thread}}}" if node.thread else ""
35
+ return f"→ {node.symbol}{thread}{fmt_args(node.args)}{fmt_outcome(node.outcome)}"
36
+ if isinstance(node, SubstitutionNode):
37
+ path = ".".join(node.path) or "<call>"
38
+ claim = f" claims={node.claimed_target}" if node.claimed_target else " claims=∅"
39
+ args = fmt_args(node.args)
40
+ return (
41
+ f"⊗ {node.mechanism.value} .{path} substitute={node.substitute}"
42
+ f"{claim} relation={node.relation}{args}{fmt_outcome(node.outcome)}"
43
+ )
44
+ if isinstance(node, OsEventNode):
45
+ return f"⚡ {node.kind.value} {node.target} → {node.outcome}"
46
+ raise TypeError(node)
47
+
48
+
49
+ def render_execution(execution: Execution) -> str:
50
+ children: dict[int, list[Node]] = defaultdict(list)
51
+ for node in execution.nodes:
52
+ if node.parent is not None:
53
+ children[node.parent].append(node)
54
+ collectors = ", ".join(f"{c.name}: {c.fidelity.value}" for c in execution.collectors)
55
+ lines = [f"{execution.stimulus_ref} [{collectors}] {execution.outcome}"]
56
+
57
+ def walk(node_id: int, prefix: str) -> None:
58
+ kids = children[node_id]
59
+ for i, kid in enumerate(kids):
60
+ last = i == len(kids) - 1
61
+ lines.append(f"{prefix}{'└' if last else '├'}{_label(kid)}")
62
+ walk(kid.id, prefix + (" " if last else "│ "))
63
+
64
+ walk(0, "")
65
+ return "\n".join(lines) + "\n"
66
+
67
+
68
+ _GLYPH = {
69
+ EvidenceKind.OBSERVED: "→ ",
70
+ EvidenceKind.OBSERVED_SAMPLED: "→?",
71
+ EvidenceKind.COMPOSED: "⇢ ",
72
+ EvidenceKind.STATIC: "--→",
73
+ EvidenceKind.INTERNAL_GAP: "→ [gap]",
74
+ EvidenceKind.EXTERNAL_BOUNDARY: "→ [external]",
75
+ EvidenceKind.OS_BOUNDARY: "→ [os]",
76
+ EvidenceKind.UNRESOLVED_BOUNDARY: "→ [unresolved]",
77
+ }
78
+
79
+
80
+ def render_composition(composition: Composition) -> str:
81
+ """Composed tree. Every non-observed edge prints its rule; composed edges print their
82
+ join grade and the execution the continuation was borrowed from."""
83
+ lines = [f"{composition.seed} seed={composition.root}"]
84
+
85
+ def label(edge: Edge) -> str:
86
+ ev = edge.evidence
87
+ tail = ""
88
+ if ev.rule:
89
+ tail += f" rule={ev.rule}"
90
+ if ev.kind is EvidenceKind.COMPOSED and ev.fragment and ev.join:
91
+ tail += f" join={ev.join.name.lower()} fragment={ev.fragment.execution}"
92
+ if ev.alternates:
93
+ tail += f" +{len(ev.alternates)} same-shape"
94
+ if ev.probe_derived:
95
+ tail += " probe-derived"
96
+ return f"{_GLYPH[ev.kind]} {edge.callee}{tail}"
97
+
98
+ def walk(branches: list[Branch], prefix: str) -> None:
99
+ for i, b in enumerate(branches):
100
+ last = i == len(branches) - 1
101
+ lines.append(f"{prefix}{'└' if last else '├'}{label(b.edge)}")
102
+ walk(b.children, prefix + (" " if last else "│ "))
103
+
104
+ walk(composition.branches, "")
105
+ if composition.gaps:
106
+ lines.append("gaps:")
107
+ lines.extend(
108
+ f" {g.kind}: {g.target} at {g.site.execution}#{g.site.node}" for g in composition.gaps
109
+ )
110
+ rejected = [a for a in composition.attempts if not a.accepted]
111
+ if rejected:
112
+ lines.append("rejected joins:")
113
+ lines.extend(
114
+ f" {a.target} {a.grade.name.lower() if a.grade else 'unsound'}"
115
+ f" fragment={a.fragment.execution} {a.note}"
116
+ for a in rejected
117
+ )
118
+ return "\n".join(lines) + "\n"