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,985 @@
1
+ """CPython symbol-plane collector on `sys.monitoring` (PEP 669, Python >= 3.12).
2
+
3
+ Complete-fidelity capture of in-scope logical calls, with stand-in calls attributed to the
4
+ in-scope function that made them. This module is allowed to know about CPython and
5
+ `unittest.mock`; what it emits is the language-neutral protocol only.
6
+
7
+ Scope: a code object is in scope when its file lies under a source root (`REPO`) or a
8
+ test root (`TEST`). Everything else (stdlib, site-packages, generated `<string>` code) is
9
+ out of scope and its events are disabled at the first hit, so it costs nothing afterwards.
10
+
11
+ Known limits (see docs/experiment-01.md): generators/coroutines are not modelled (a
12
+ stack repair is counted instead), `side_effect`/`wraps` bodies run inside the stand-in
13
+ call and are attributed to it. A stand-in call's arguments and outcome are observed by
14
+ wrapping `unittest.mock`'s private `_mock_call` while tracing (a runtime-adapter detail,
15
+ restored on stop).
16
+ """
17
+
18
+ from __future__ import annotations
19
+
20
+ import ast
21
+ import dataclasses
22
+ import hashlib
23
+ import io
24
+ import sys
25
+ import threading
26
+ from dataclasses import dataclass, field
27
+ from pathlib import Path
28
+ from types import CodeType, ModuleType
29
+ from typing import Any
30
+ from unittest import mock
31
+
32
+ from diffgenome.model import (
33
+ ArgShapes,
34
+ BranchObs,
35
+ CallNode,
36
+ Collector,
37
+ Fidelity,
38
+ Node,
39
+ Origin,
40
+ OsEventKind,
41
+ OsEventNode,
42
+ Plane,
43
+ SourceLocation,
44
+ StateFacts,
45
+ SubstitutionMechanism,
46
+ SubstitutionNode,
47
+ Symbol,
48
+ SymbolId,
49
+ )
50
+
51
+ COLLECTOR = Collector("py-sys-monitoring", Plane.SYMBOL, Fidelity.COMPLETE)
52
+ EGRESS_GUARD = Collector("py-egress-guard", Plane.OS, Fidelity.COMPLETE)
53
+ """Connect attempts seen from inside the runtime. Kernel-boundary facts (OS plane) observed
54
+ by a runtime hook rather than by the kernel; the collector name says so."""
55
+ _TOOL_ID = 4 # sys.monitoring tool slot; coverage.py uses 2/3 by convention
56
+ _MISSING = sys.monitoring.MISSING
57
+ _DISABLE = sys.monitoring.DISABLE
58
+ # Captured at import: targets patch builtins.open (mock_open) during tests, and site maps
59
+ # are built lazily on the capture path.
60
+ _READ_SOURCE = io.open
61
+ _MAX_ARGS = 8
62
+ # Never repository code even when located under a source root: installed dependencies and
63
+ # hidden tool directories (.venv, .tox, .git...). "What counts as the repository" is a
64
+ # known open problem; this is the minimal rule that keeps dependencies out.
65
+ _EXCLUDED_PARTS = {"site-packages", "dist-packages", "node_modules", "__pycache__"}
66
+
67
+
68
+ def summarize_value(value: Any) -> str:
69
+ """Bounded, deterministic shape of a value for seam matching. Never repr()s user data."""
70
+ if value is None or isinstance(value, bool | int | float | str | bytes):
71
+ return type(value).__name__
72
+ if isinstance(value, mock.NonCallableMock):
73
+ spec = getattr(value, "_spec_class", None)
74
+ return f"stand-in:{spec.__qualname__}" if spec else "stand-in"
75
+ if isinstance(value, list | tuple | set | frozenset | dict):
76
+ return f"{type(value).__name__}[{len(value)}]"
77
+ return type(value).__qualname__
78
+
79
+
80
+ _DIGEST_DEPTH = 3
81
+ _DIGEST_WIDTH = 16 # elements/fields considered per container level
82
+
83
+
84
+ def _canonical(value: Any, depth: int) -> str | None:
85
+ """Canonical string of a value to a bounded depth, or None when not summarizable.
86
+ Anything None at any level makes the whole digest unavailable: a partial digest would
87
+ let two different values look equal."""
88
+ if value is None or isinstance(value, bool | int | str | bytes):
89
+ return f"{type(value).__name__}:{value!r}"
90
+ if isinstance(value, float):
91
+ return f"float:{value.hex()}"
92
+ if isinstance(value, mock.NonCallableMock) or depth == 0:
93
+ return None
94
+ if isinstance(value, list | tuple | set | frozenset):
95
+ if len(value) > _DIGEST_WIDTH:
96
+ return None
97
+ parts: list[str] = []
98
+ for v in value:
99
+ c = _canonical(v, depth - 1)
100
+ if c is None:
101
+ return None
102
+ parts.append(c)
103
+ if isinstance(value, set | frozenset):
104
+ parts.sort()
105
+ return f"{type(value).__name__}[{','.join(parts)}]"
106
+ if isinstance(value, dict):
107
+ if len(value) > _DIGEST_WIDTH:
108
+ return None
109
+ items: list[str] = []
110
+ for k, v in value.items():
111
+ ck, cv = _canonical(k, depth - 1), _canonical(v, depth - 1)
112
+ if ck is None or cv is None:
113
+ return None
114
+ items.append(f"{ck}={cv}")
115
+ return f"dict{{{','.join(sorted(items))}}}"
116
+ if dataclasses.is_dataclass(value) and not isinstance(value, type):
117
+ fields: list[str] = []
118
+ for f in dataclasses.fields(value)[:_DIGEST_WIDTH]:
119
+ c = _canonical(getattr(value, f.name), depth - 1)
120
+ if c is None:
121
+ return None
122
+ fields.append(f"{f.name}={c}")
123
+ return f"{type(value).__qualname__}({','.join(fields)})"
124
+ return None
125
+
126
+
127
+ _STATE_WIDTH = 16
128
+
129
+
130
+ def bucket(value: Any) -> tuple[str, str] | None:
131
+ """Language-neutral low-cardinality bucket of a value, or None when the value is not
132
+ branch-relevant enough to summarize (free-form objects, callables). Returns
133
+ (bucket, digest) where digest is filled only for public identifiers (enum members,
134
+ type names). Scalars are never stored: only their bucket."""
135
+ import enum
136
+
137
+ if value is None:
138
+ return "none", ""
139
+ if isinstance(value, bool):
140
+ return f"bool:{'true' if value else 'false'}", ""
141
+ if isinstance(value, enum.Enum):
142
+ return f"enum:{value.name}", digest_value(value.name)
143
+ if isinstance(value, int | float):
144
+ return ("num:zero" if value == 0 else "num:pos" if value > 0 else "num:neg"), ""
145
+ if isinstance(value, str | bytes):
146
+ return ("str:empty" if not value else "str:nonempty"), ""
147
+ if isinstance(value, list | tuple | set | frozenset | dict):
148
+ n = len(value)
149
+ return ("coll:empty" if n == 0 else "coll:one" if n == 1 else "coll:many"), ""
150
+ if isinstance(value, mock.NonCallableMock):
151
+ spec = getattr(value, "_spec_class", None)
152
+ return f"obj:stand-in{':' + spec.__qualname__ if spec else ''}", ""
153
+ if callable(value) and not isinstance(value, type):
154
+ return None
155
+ name = type(value).__qualname__
156
+ return f"obj:{name}", digest_value(name)
157
+
158
+
159
+ def state_facts(
160
+ receiver: Any, code: CodeType | None, module_globals: dict[str, Any] | None
161
+ ) -> StateFacts:
162
+ """Receiver fields (first _STATE_WIDTH) and module globals the code reads, bucketed."""
163
+ facts: list[tuple[str, str, str]] = []
164
+ if receiver is not None and not isinstance(receiver, mock.NonCallableMock):
165
+ facts.append(
166
+ (
167
+ "self:type",
168
+ f"obj:{type(receiver).__qualname__}",
169
+ digest_value(type(receiver).__qualname__),
170
+ )
171
+ )
172
+ d = getattr(receiver, "__dict__", None)
173
+ if isinstance(d, dict):
174
+ for name, value in list(d.items())[:_STATE_WIDTH]:
175
+ b = bucket(value)
176
+ if b is not None:
177
+ facts.append((f"self.{name}", b[0], b[1]))
178
+ if code is not None and module_globals:
179
+ names = code.co_names[: _STATE_WIDTH * 2]
180
+ for name in names:
181
+ if name in module_globals and not name.startswith("__"):
182
+ value = module_globals[name]
183
+ if isinstance(value, ModuleType | type):
184
+ # state read through an imported module or a class attribute:
185
+ # settings.STRICT, ModelManager._instance. Own namespace only.
186
+ for attr in names:
187
+ if attr != name and attr in vars(value):
188
+ inner = vars(value)[attr]
189
+ if isinstance(
190
+ inner, type | ModuleType | property | classmethod | staticmethod
191
+ ) or callable(inner):
192
+ continue
193
+ b = bucket(inner)
194
+ if b is not None:
195
+ facts.append((f"global.{name}.{attr}", b[0], b[1]))
196
+ continue
197
+ if callable(value):
198
+ continue
199
+ own = getattr(value, "__dict__", None)
200
+ if isinstance(own, dict) and not isinstance(value, mock.NonCallableMock):
201
+ # a global object (settings, config): the attributes this code reads
202
+ # through it, from its own instance dict only (no descriptors run)
203
+ for attr in names:
204
+ if attr != name and attr in own:
205
+ inner = own[attr]
206
+ if isinstance(inner, type | ModuleType) or callable(inner):
207
+ continue
208
+ b = bucket(inner)
209
+ if b is not None:
210
+ facts.append((f"global.{name}.{attr}", b[0], b[1]))
211
+ b = bucket(value)
212
+ if b is not None:
213
+ facts.append((f"global.{name}", b[0], b[1]))
214
+ return tuple(facts[: _STATE_WIDTH * 2])
215
+
216
+
217
+ def digest_value(value: Any) -> str:
218
+ """Stable content digest of a value, or "" when unavailable. Uses sha256 (not hash())
219
+ so digests agree across processes and runs. Short scalars are trivially reversible by
220
+ brute force; the digest hides values from casual reading, not from an adversary."""
221
+ canonical = _canonical(value, _DIGEST_DEPTH)
222
+ return "" if canonical is None else hashlib.sha256(canonical.encode()).hexdigest()[:16]
223
+
224
+
225
+ @dataclass
226
+ class _Scope:
227
+ origin: Origin
228
+ symbol: SymbolId
229
+ location: SourceLocation
230
+
231
+
232
+ Pos = tuple[int, int] # (line, 1-based byte column)
233
+
234
+
235
+ @dataclass(frozen=True)
236
+ class _IfSite:
237
+ site: str
238
+ test: tuple[Pos, Pos] # [start, end) of the condition
239
+ body: tuple[Pos, Pos] # [start, end) of the then-block
240
+
241
+
242
+ def if_sites(source: str, rel_path: str) -> list[_IfSite]:
243
+ """Every `if` statement's condition and then-block span in original source."""
244
+ from diffgenome.sites import site_id
245
+
246
+ out: list[_IfSite] = []
247
+ for node in ast.walk(ast.parse(source)):
248
+ if not isinstance(node, ast.If) or node.test.end_lineno is None:
249
+ continue
250
+ t = node.test
251
+ span = (t.lineno, t.col_offset + 1, t.end_lineno or t.lineno, (t.end_col_offset or 0) + 1)
252
+ first, last = node.body[0], node.body[-1]
253
+ out.append(
254
+ _IfSite(
255
+ site_id(rel_path, span),
256
+ ((span[0], span[1]), (span[2], span[3])),
257
+ (
258
+ (first.lineno, first.col_offset + 1),
259
+ (last.end_lineno or last.lineno, (last.end_col_offset or 0) + 1),
260
+ ),
261
+ )
262
+ )
263
+ return out
264
+
265
+
266
+ def _inside(p: Pos, span: tuple[Pos, Pos]) -> bool:
267
+ return span[0] <= p < span[1]
268
+
269
+
270
+ @dataclass
271
+ class TraceResult:
272
+ symbols: tuple[Symbol, ...]
273
+ nodes: tuple[Node, ...]
274
+ stack_repairs: int = 0 # returns that did not match the shadow-stack top
275
+ attribution_disagreements: int = (
276
+ 0 # calls where a shadow stack would have chosen another parent
277
+ )
278
+ branches: tuple[BranchObs, ...] = ()
279
+
280
+
281
+ @dataclass
282
+ class Tracer:
283
+ """One tracer per process. `start()`/`stop()` bracket a single stimulus."""
284
+
285
+ repo_root: Path
286
+ source_roots: tuple[Path, ...]
287
+ test_roots: tuple[Path, ...]
288
+ _scopes: dict[CodeType, _Scope | None] = field(default_factory=dict, repr=False)
289
+ _symbols: dict[SymbolId, Symbol] = field(default_factory=dict, repr=False)
290
+ _nodes: list[Node] = field(default_factory=list, repr=False)
291
+ _stacks: dict[int, list[tuple[CodeType | None, int]]] = field(default_factory=dict, repr=False)
292
+ _threads: dict[int, int] = field(default_factory=dict, repr=False)
293
+ _frame_nodes: dict[int, int] = field(default_factory=dict, repr=False) # id(frame) -> node
294
+ _fake_wrappers: dict[int, int] = field(
295
+ default_factory=dict, repr=False
296
+ ) # call node -> sub node
297
+ _root_code: CodeType | None = None
298
+ _stack_repairs: int = 0
299
+ _attribution_disagreements: int = 0
300
+ _pending_mock_calls: dict[int, list[int]] = field(default_factory=dict, repr=False)
301
+ _orig_mock_call: Any = None
302
+ _patched: dict[int, tuple[Any, str, Any]] = field(default_factory=dict, repr=False)
303
+ _orig_patch_enter: Any = None
304
+ _orig_patch_exit: Any = None
305
+ _egress_guard_installed: bool = False
306
+ _branches: list[BranchObs] = field(default_factory=list, repr=False)
307
+ _file_sites: dict[str, list[_IfSite]] = field(default_factory=dict, repr=False)
308
+ _positions: dict[CodeType, list[Any]] = field(default_factory=dict, repr=False)
309
+
310
+ # ------------------------------------------------------------------ symbol naming
311
+
312
+ def __post_init__(self) -> None:
313
+ # Everything below runs on the capture path and must not touch os.path, pathlib or
314
+ # inspect: those read patchable globals, and a target that patches os.path.join
315
+ # renamed our symbols after its temp file (Kokoro-FastAPI, experiment 02).
316
+ self._repo_prefix = str(self.repo_root).rstrip("/") + "/"
317
+ self._root_prefixes = [(Origin.TEST, str(r).rstrip("/") + "/") for r in self.test_roots] + [
318
+ (Origin.REPO, str(r).rstrip("/") + "/") for r in self.source_roots
319
+ ]
320
+
321
+ def _module_for(self, filename: str) -> tuple[Origin, str] | None:
322
+ for origin, prefix in self._root_prefixes:
323
+ if not filename.startswith(prefix):
324
+ continue
325
+ rel = filename[len(prefix) :]
326
+ parts = rel.split("/")
327
+ if any(part in _EXCLUDED_PARTS or part.startswith(".") for part in parts):
328
+ return None
329
+ if parts[-1].endswith(".py"):
330
+ parts[-1] = parts[-1][:-3]
331
+ if parts and parts[-1] == "__init__":
332
+ parts.pop()
333
+ if origin is Origin.TEST:
334
+ parts = prefix[len(self._repo_prefix) :].rstrip("/").split("/") + parts
335
+ return origin, ".".join(p for p in parts if p)
336
+ return None
337
+
338
+ def _relative(self, filename: str) -> str:
339
+ return (
340
+ filename[len(self._repo_prefix) :]
341
+ if filename.startswith(self._repo_prefix)
342
+ else filename
343
+ )
344
+
345
+ def scope_for(self, code: CodeType) -> _Scope | None:
346
+ if code in self._scopes:
347
+ return self._scopes[code]
348
+ scope = None
349
+ filename = code.co_filename
350
+ if not filename.startswith("<"):
351
+ named = self._module_for(filename)
352
+ if named:
353
+ origin, module = named
354
+ qualname = code.co_qualname
355
+ if qualname.rsplit(".", 1)[-1].startswith("<"):
356
+ # Anonymous code (<lambda>, <genexpr>, <listcomp>...) has no name of its
357
+ # own: two lambdas in one function share a qualname. Its identity is
358
+ # its definition site. Found as an IdentityMismatch on Kokoro-FastAPI.
359
+ qualname = f"{qualname}@{code.co_firstlineno}"
360
+ scope = _Scope(
361
+ origin,
362
+ f"py:{module}.{qualname}",
363
+ SourceLocation(self._relative(filename), code.co_firstlineno),
364
+ )
365
+ self._scopes[code] = scope
366
+ return scope
367
+
368
+ def symbol_for_code(self, code: CodeType) -> SymbolId | None:
369
+ scope = self.scope_for(code)
370
+ return scope.symbol if scope else None
371
+
372
+ def symbol_for_object(self, obj: Any) -> Symbol:
373
+ """Identity of a class/function reached by *object*, not by frame, with the origin
374
+ and location the collector can observe for it. Uses the same file-based naming as
375
+ frames when the object has a code object or an in-scope module file; otherwise a
376
+ module-based name, which is a second identity scheme (see exp 01 findings).
377
+ Reads only attributes, never os.path/inspect (see __post_init__)."""
378
+ code = getattr(obj, "__code__", None)
379
+ if isinstance(code, CodeType) and (scope := self.scope_for(code)):
380
+ return Symbol(scope.symbol, scope.origin, scope.location)
381
+ if isinstance(obj, ModuleType):
382
+ # A patched module attribute (patch("pkg.mod.torch")) claims the module itself.
383
+ file = getattr(obj, "__file__", None)
384
+ named = self._module_for(file) if isinstance(file, str) else None
385
+ if named and isinstance(file, str):
386
+ return Symbol(f"py:{named[1]}", named[0], SourceLocation(self._relative(file), 0))
387
+ return Symbol(f"py:{obj.__name__}", Origin.EXTERNAL, None)
388
+ qualname = getattr(obj, "__qualname__", type(obj).__qualname__)
389
+ owner = getattr(obj, "__objclass__", None)
390
+ module_name = getattr(obj, "__module__", None) or (owner.__module__ if owner else None)
391
+ module = sys.modules.get(module_name) if module_name else None
392
+ file = getattr(module, "__file__", None)
393
+ if isinstance(file, str):
394
+ named = self._module_for(file)
395
+ if named:
396
+ line = code.co_firstlineno if isinstance(code, CodeType) else 0
397
+ loc = SourceLocation(self._relative(file), line)
398
+ return Symbol(f"py:{named[1]}.{qualname}", named[0], loc)
399
+ if module_name is None:
400
+ return Symbol(f"py:{type(obj).__module__}.{qualname}", Origin.UNKNOWN, None)
401
+ # A real object in this process whose module is absent or out of scope: external.
402
+ # C-implemented members carry no __module__; their defining class does (exp 01).
403
+ return Symbol(f"py:{module_name}.{qualname}", Origin.EXTERNAL, None)
404
+
405
+ def _intern(
406
+ self,
407
+ symbol: SymbolId,
408
+ origin: Origin,
409
+ location: SourceLocation | None,
410
+ kind: str = "callable",
411
+ ) -> None:
412
+ if symbol not in self._symbols or (
413
+ kind != "callable" and self._symbols[symbol].kind == "callable"
414
+ ):
415
+ self._symbols[symbol] = Symbol(symbol, origin, location, kind)
416
+
417
+ def _intern_symbol(self, symbol: Symbol) -> SymbolId:
418
+ self._intern(symbol.id, symbol.origin, symbol.location)
419
+ return symbol.id
420
+
421
+ # ------------------------------------------------------------------ stand-ins
422
+
423
+ def _describe_mock(
424
+ self, m: Any, frame: Any = None
425
+ ) -> tuple[SubstitutionMechanism, SymbolId | None, str, tuple[str, ...]]:
426
+ path: list[str] = []
427
+ root = m
428
+ while True:
429
+ name = root._mock_name or root._mock_new_name
430
+ if name:
431
+ path.append(name)
432
+ parent = root._mock_parent or root._mock_new_parent
433
+ if parent is None:
434
+ break
435
+ root = parent
436
+ path.reverse()
437
+ patched = self._patched.get(id(root))
438
+ if patched is not None:
439
+ # Installed by unittest.mock.patch: the patcher saved the original, which is
440
+ # exactly what the stand-in replaces. path[0] is the patched attribute.
441
+ _, _, original = patched
442
+ claim = None
443
+ if original is not mock.DEFAULT:
444
+ original = getattr(original, "__func__", getattr(original, "fget", original))
445
+ if len(path) <= 1 and isinstance(original, type):
446
+ # Calling a patched *class* constructs it; the claim is its __init__
447
+ # when that is in-repo Python (same rule as spec'd class stand-ins).
448
+ init = _static_attr(original, "__init__")
449
+ if getattr(init, "__code__", None) is not None:
450
+ original = init
451
+ else:
452
+ sym = self.symbol_for_object(original)
453
+ self._intern(sym.id, sym.origin, sym.location, "declaration")
454
+ return (
455
+ SubstitutionMechanism.INTERPOSITION,
456
+ sym.id,
457
+ "patch-target",
458
+ tuple(path),
459
+ )
460
+ claim = self._intern_symbol(self.symbol_for_object(original))
461
+ return SubstitutionMechanism.INTERPOSITION, claim, "patch-target", tuple(path)
462
+ spec = getattr(root, "_spec_class", None)
463
+ if spec is None:
464
+ bound = self._bound_attribute(root, frame)
465
+ if bound is not None:
466
+ # The root stand-in is stored as attribute `name` of a real object whose
467
+ # class defines `name`: it replaced that member. Identity, not naming.
468
+ _, name, bound_member = bound
469
+ bound_member = getattr(
470
+ bound_member, "__func__", getattr(bound_member, "fget", bound_member)
471
+ )
472
+ claim = self._intern_symbol(self.symbol_for_object(bound_member))
473
+ return (
474
+ SubstitutionMechanism.INTERPOSITION,
475
+ claim,
476
+ "instance-attribute",
477
+ (name, *path[1:]) if path and path[0] == name else (name, *path),
478
+ )
479
+ return SubstitutionMechanism.MOCK_OBJECT, None, "none", tuple(path)
480
+ member: Any = None
481
+ if path and path[0] != "()":
482
+ member = _static_attr(spec, path[0])
483
+ member = getattr(member, "__func__", getattr(member, "fget", member))
484
+ elif not path and isinstance(spec, type):
485
+ # Calling a stand-in for a class is constructing it: what really runs is the
486
+ # class's own __init__ (when it has one in Python), so that is the claim.
487
+ init = _static_attr(spec, "__init__")
488
+ if getattr(init, "__code__", None) is not None:
489
+ member = init
490
+ else:
491
+ # No in-repo constructor body: the claim is a declaration, not a gap.
492
+ sym = self.symbol_for_object(spec)
493
+ self._intern(sym.id, sym.origin, sym.location, "declaration")
494
+ return SubstitutionMechanism.MOCK_OBJECT, sym.id, "spec", tuple(path)
495
+ target = self._intern_symbol(self.symbol_for_object(member if member is not None else spec))
496
+ return SubstitutionMechanism.MOCK_OBJECT, target, "spec", tuple(path)
497
+
498
+ def _bound_attribute(self, root: Any, frame: Any) -> tuple[type, str, Any] | None:
499
+ """Find a real (non-mock) object among the caller's locals, `self` first, whose
500
+ instance dict holds `root` under a name its class also defines. Bounded scan; reads
501
+ only __dict__ so no descriptor or property runs."""
502
+ if frame is None:
503
+ return None
504
+ locals_ = frame.f_locals
505
+ candidates = [locals_[n] for n in ("self", "cls") if n in locals_]
506
+ candidates += [v for k, v in list(locals_.items())[:_MAX_ARGS] if k not in ("self", "cls")]
507
+ for obj in candidates:
508
+ if isinstance(obj, mock.NonCallableMock | type(None) | int | str | float | bool):
509
+ continue
510
+ d = getattr(obj, "__dict__", None)
511
+ if not isinstance(d, dict):
512
+ continue
513
+ for name, value in d.items():
514
+ if value is root:
515
+ cls = obj if isinstance(obj, type) else type(obj)
516
+ member = _static_attr(cls, name)
517
+ if member is not None:
518
+ return cls, name, member
519
+ return None
520
+
521
+ def _seam_state(self, callable_: Any, frame: Any, relation: str) -> StateFacts:
522
+ """State the real target would see at this seam, when the seed exposes it."""
523
+ if frame is None:
524
+ return ()
525
+ root = callable_
526
+ while isinstance(root, mock.NonCallableMock) and (
527
+ root._mock_parent or root._mock_new_parent
528
+ ):
529
+ root = root._mock_parent or root._mock_new_parent
530
+ if relation == "instance-attribute":
531
+ bound = self._bound_attribute(root, frame)
532
+ if bound is not None:
533
+ obj = None
534
+ for cand in list(frame.f_locals.values())[: _MAX_ARGS + 2]:
535
+ d = getattr(cand, "__dict__", None)
536
+ if isinstance(d, dict) and any(v is root for v in d.values()):
537
+ obj = cand
538
+ break
539
+ member = bound[2]
540
+ func = getattr(member, "__func__", getattr(member, "fget", member))
541
+ code = getattr(func, "__code__", None)
542
+ return state_facts(obj, code, getattr(func, "__globals__", None))
543
+ if relation == "patch-target":
544
+ patched = self._patched.get(id(root))
545
+ if patched is not None:
546
+ target_obj, _, original = patched
547
+ if (
548
+ isinstance(target_obj, ModuleType)
549
+ and getattr(original, "__code__", None) is not None
550
+ ):
551
+ # the original's own globals: a function patched where it was imported
552
+ # still reads state from the module that defined it
553
+ return state_facts(
554
+ None, original.__code__, getattr(original, "__globals__", vars(target_obj))
555
+ )
556
+ if not isinstance(target_obj, type | ModuleType):
557
+ func = getattr(original, "__func__", getattr(original, "fget", original))
558
+ return state_facts(
559
+ target_obj,
560
+ getattr(func, "__code__", None),
561
+ getattr(func, "__globals__", None),
562
+ )
563
+ return ()
564
+
565
+ def _record_substitution(
566
+ self,
567
+ parent: int,
568
+ mechanism: SubstitutionMechanism,
569
+ callable_: Any,
570
+ args: ArgShapes,
571
+ frame: Any = None,
572
+ ) -> int:
573
+ state: StateFacts = ()
574
+ if isinstance(callable_, mock.NonCallableMock):
575
+ substitute = f"py:{type(callable_).__module__}.{type(callable_).__qualname__}"
576
+ mechanism, claimed, relation, path = self._describe_mock(callable_, frame)
577
+ state = self._seam_state(callable_, frame, relation)
578
+ else:
579
+ substitute = self._intern_symbol(self.symbol_for_object(callable_))
580
+ claimed, relation, path = None, "none", ()
581
+ node = SubstitutionNode(
582
+ id=len(self._nodes),
583
+ parent=parent,
584
+ collector=0,
585
+ mechanism=mechanism,
586
+ substitute=substitute,
587
+ claimed_target=claimed,
588
+ relation=relation,
589
+ path=path,
590
+ args=args,
591
+ state=state,
592
+ )
593
+ self._nodes.append(node)
594
+ return node.id
595
+
596
+ # ------------------------------------------------------------------ attribution
597
+
598
+ def _thread(self) -> int:
599
+ ident = threading.get_ident()
600
+ if ident not in self._threads:
601
+ self._threads[ident] = len(self._threads)
602
+ self._stacks[ident] = []
603
+ return ident
604
+
605
+ def _ancestor_node(self, frame: Any) -> int:
606
+ """Node of the nearest in-scope frame on the real call stack above `frame`, or the
607
+ root. Correct under generators and coroutines, where a shadow stack is not: a
608
+ suspended frame is not an ancestor of what runs while it is suspended."""
609
+ f = frame.f_back
610
+ while f is not None:
611
+ node = self._frame_nodes.get(id(f))
612
+ if node is not None:
613
+ return node
614
+ f = f.f_back
615
+ return 0
616
+
617
+ def _shadow_parent(self, ident: int) -> int:
618
+ stack = self._stacks[ident]
619
+ return stack[-1][1] if stack else 0
620
+
621
+ # ------------------------------------------------------------------ callbacks
622
+
623
+ def _on_start(self, code: CodeType, _offset: int) -> Any:
624
+ scope = self.scope_for(code)
625
+ if scope is None:
626
+ return _DISABLE
627
+ ident = self._thread()
628
+ frame = sys._getframe(1)
629
+ stack = self._stacks[ident]
630
+ if (
631
+ code is self._root_code
632
+ and self._threads[ident] == 0
633
+ and 0 not in self._frame_nodes.values()
634
+ ):
635
+ self._frame_nodes[id(frame)] = 0
636
+ stack.append((code, 0))
637
+ return None
638
+ names = code.co_varnames[: code.co_argcount + code.co_kwonlyargcount][:_MAX_ARGS]
639
+ args: ArgShapes = tuple(
640
+ (n, summarize_value(frame.f_locals[n]), digest_value(frame.f_locals[n]))
641
+ for n in names
642
+ if n not in ("self", "cls") and n in frame.f_locals
643
+ )
644
+ parent = self._ancestor_node(frame)
645
+ if parent != self._shadow_parent(ident):
646
+ self._attribution_disagreements += 1
647
+ parent_symbol = getattr(self._nodes[parent], "symbol", None)
648
+ parent_origin = self._symbols[parent_symbol].origin if parent_symbol else Origin.UNKNOWN
649
+ sub_id = None
650
+ if scope.origin is Origin.TEST and parent_origin is Origin.REPO:
651
+ # Production code called into test-defined code: a fake/stub that executes.
652
+ mechanism = (
653
+ SubstitutionMechanism.FAKE
654
+ if "." in code.co_qualname
655
+ else SubstitutionMechanism.STUB
656
+ )
657
+ sub_id = self._record_substitution(parent, mechanism, _CodeHolder(code), args)
658
+ recv = (
659
+ frame.f_locals.get("self")
660
+ if code.co_argcount and code.co_varnames[0] == "self"
661
+ else None
662
+ )
663
+ if recv is not None:
664
+ sub = self._nodes[sub_id]
665
+ assert isinstance(sub, SubstitutionNode)
666
+ self._nodes[sub_id] = dataclasses.replace(sub, state=state_facts(recv, None, None))
667
+ parent = sub_id
668
+ self._intern(scope.symbol, scope.origin, scope.location)
669
+ receiver = (
670
+ frame.f_locals.get("self")
671
+ if code.co_argcount and code.co_varnames[0] == "self"
672
+ else None
673
+ )
674
+ node = CallNode(
675
+ id=len(self._nodes),
676
+ parent=parent,
677
+ symbol=scope.symbol,
678
+ collector=0,
679
+ args=args,
680
+ thread=self._threads[ident],
681
+ state=state_facts(receiver, code, frame.f_globals),
682
+ )
683
+ self._nodes.append(node)
684
+ self._frame_nodes[id(frame)] = node.id
685
+ if sub_id is not None:
686
+ self._fake_wrappers[node.id] = sub_id
687
+ stack.append((code, node.id))
688
+ return None
689
+
690
+ def _set_outcome(self, node_id: int, outcome: str, result: str = "") -> None:
691
+ node = self._nodes[node_id]
692
+ assert not isinstance(node, OsEventNode)
693
+ self._nodes[node_id] = dataclasses.replace(node, outcome=outcome, result=result)
694
+
695
+ def _finish(self, code: CodeType, outcome: str, result: str = "") -> None:
696
+ frame = sys._getframe(2)
697
+ node_id = self._frame_nodes.pop(id(frame), None)
698
+ if node_id:
699
+ self._set_outcome(node_id, outcome, result)
700
+ node = self._nodes[node_id]
701
+ if isinstance(node, CallNode):
702
+ receiver = (
703
+ frame.f_locals.get("self")
704
+ if code.co_argcount and code.co_varnames[0] == "self"
705
+ else None
706
+ )
707
+ # the same bounded, value-free view as on entry: an observed delta, not a
708
+ # complete write set (nested mutations can be invisible to it)
709
+ self._nodes[node_id] = dataclasses.replace(
710
+ node, state_after=state_facts(receiver, code, frame.f_globals)
711
+ )
712
+ wrapper = self._fake_wrappers.pop(node_id, None)
713
+ if wrapper is not None:
714
+ self._set_outcome(wrapper, outcome, result)
715
+ # Shadow stack, kept only to measure how often it would have misattributed.
716
+ stack = self._stacks.get(threading.get_ident())
717
+ if not stack:
718
+ return
719
+ if stack[-1][0] is not code:
720
+ self._stack_repairs += 1
721
+ while stack and stack[-1][0] is not code:
722
+ stack.pop()
723
+ if stack:
724
+ stack.pop()
725
+
726
+ # PY_RETURN/PY_UNWIND cannot be disabled per location (CPython raises), so
727
+ # out-of-scope returns are simply ignored; PY_START's DISABLE keeps the cost down.
728
+ def _on_return(self, code: CodeType, _offset: int, value: object) -> Any:
729
+ if self.scope_for(code) is not None:
730
+ self._finish(code, "returned", digest_value(value))
731
+ return None
732
+
733
+ def _on_unwind(self, code: CodeType, _offset: int, exc: BaseException) -> Any:
734
+ if self.scope_for(code) is not None:
735
+ self._finish(code, "raised:" + self._intern_symbol(self.symbol_for_object(type(exc))))
736
+ return None
737
+
738
+ def _mock_call(self, m: Any, /, *args: Any, **kwargs: Any) -> Any:
739
+ """Wraps unittest.mock's call path while tracing, so a stand-in call's arguments and
740
+ outcome are observed rather than inferred. The CALL event already recorded the node."""
741
+ pending = self._pending_mock_calls.get(threading.get_ident())
742
+ node_id = pending.pop() if pending else None
743
+ if node_id is not None:
744
+ shapes: ArgShapes = tuple(
745
+ [
746
+ (f"arg{i}", summarize_value(a), digest_value(a))
747
+ for i, a in enumerate(args[:_MAX_ARGS])
748
+ ]
749
+ + [
750
+ (k, summarize_value(v), digest_value(v))
751
+ for k, v in list(kwargs.items())[:_MAX_ARGS]
752
+ ]
753
+ )
754
+ node = self._nodes[node_id]
755
+ assert isinstance(node, SubstitutionNode)
756
+ self._nodes[node_id] = dataclasses.replace(node, args=shapes)
757
+ try:
758
+ result = self._orig_mock_call(m, *args, **kwargs)
759
+ except BaseException as exc:
760
+ if node_id is not None:
761
+ self._set_outcome(
762
+ node_id, "raised:" + self._intern_symbol(self.symbol_for_object(type(exc)))
763
+ )
764
+ raise
765
+ if node_id is not None:
766
+ self._set_outcome(node_id, "returned", digest_value(result))
767
+ return result
768
+
769
+ def _sites_for(self, filename: str) -> list[_IfSite]:
770
+ sites = self._file_sites.get(filename)
771
+ if sites is None:
772
+ try:
773
+ with _READ_SOURCE(filename, encoding="utf-8") as fh:
774
+ sites = if_sites(fh.read(), self._relative(filename))
775
+ except (OSError, SyntaxError, ValueError):
776
+ sites = []
777
+ self._file_sites[filename] = sites
778
+ return sites
779
+
780
+ def _on_branch(self, code: CodeType, offset: int, destination: int) -> Any:
781
+ """A conditional jump in production code. Mapped to the `if` whose condition
782
+ contains the jump instruction; the outcome is whether control goes into that
783
+ if's then-block. Jumps that stay inside the condition (short-circuit operands)
784
+ are not decisions and are ignored; jumps of anything that is not an `if`
785
+ condition are disabled at that location."""
786
+ scope = self.scope_for(code)
787
+ if scope is None or scope.origin is not Origin.REPO:
788
+ return _DISABLE
789
+ positions = self._positions.get(code)
790
+ if positions is None:
791
+ positions = self._positions[code] = list(code.co_positions())
792
+ try:
793
+ line, _eline, col, _ecol = positions[offset // 2]
794
+ dline, _deline, dcol, _decol = positions[destination // 2]
795
+ except IndexError:
796
+ return None
797
+ if line is None or col is None:
798
+ return None
799
+ here = (line, col + 1)
800
+ match = None
801
+ for site in self._sites_for(code.co_filename):
802
+ if _inside(here, site.test) and (
803
+ match is None or site.test[0] >= match.test[0] # innermost
804
+ ):
805
+ match = site
806
+ if match is None:
807
+ return _DISABLE
808
+ if dline is None or dcol is None:
809
+ return None
810
+ there = (dline, dcol + 1)
811
+ if _inside(there, match.test):
812
+ return None # short-circuit inside the condition
813
+ outcome = _inside(there, match.body)
814
+ node = self._frame_nodes.get(id(sys._getframe(1)), 0)
815
+ self._branches.append(BranchObs(match.site, outcome, node, len(self._nodes)))
816
+ return None
817
+
818
+ def _on_call(self, code: CodeType, _offset: int, callable_: object, arg0: object) -> Any:
819
+ if self.scope_for(code) is None:
820
+ return _DISABLE
821
+ # create_autospec(function) returns a plain function wrapper whose `.mock` is the
822
+ # mock; without this it was invisible to the tracer (found by ground truth, exp 05).
823
+ if not isinstance(callable_, mock.NonCallableMock) and isinstance(
824
+ getattr(callable_, "mock", None), mock.NonCallableMock
825
+ ):
826
+ inner = callable_.mock # type: ignore[attr-defined]
827
+ if id(callable_) in self._patched and id(inner) not in self._patched:
828
+ self._patched[id(inner)] = self._patched[id(callable_)]
829
+ callable_ = inner
830
+ if isinstance(callable_, mock.NonCallableMock):
831
+ args: ArgShapes = (
832
+ () if arg0 is _MISSING else (("arg0", summarize_value(arg0), digest_value(arg0)),)
833
+ )
834
+ ident = self._thread()
835
+ frame = sys._getframe(1)
836
+ parent = self._frame_nodes.get(id(frame), self._ancestor_node(frame))
837
+ node_id = self._record_substitution(
838
+ parent, SubstitutionMechanism.MOCK_OBJECT, callable_, args, frame
839
+ )
840
+ self._pending_mock_calls.setdefault(ident, []).append(node_id)
841
+ return None
842
+
843
+ # ------------------------------------------------------------------ egress guard
844
+
845
+ def install_egress_guard(self) -> None:
846
+ """Refuse outbound connections and record each attempt as an OS-plane event. The
847
+ sandbox is the enforcement layer; this makes attempts *evidence* attributed to the
848
+ in-scope caller. Installed for the whole session."""
849
+ import socket
850
+
851
+ tracer = self
852
+
853
+ def connect(sock: Any, address: Any) -> Any:
854
+ target = address if isinstance(address, str) else ":".join(str(a) for a in address[:2])
855
+ if tracer._orig_mock_call is not None: # tracing a stimulus
856
+ frame = sys._getframe(1)
857
+ parent = tracer._frame_nodes.get(id(frame), tracer._ancestor_node(frame))
858
+ tracer._nodes.append(
859
+ OsEventNode(
860
+ id=len(tracer._nodes),
861
+ parent=parent,
862
+ collector=1,
863
+ kind=OsEventKind.CONNECT,
864
+ target=target,
865
+ outcome="refused:egress-guard",
866
+ )
867
+ )
868
+ raise PermissionError(f"diffgenome egress guard: connect to {target} refused")
869
+
870
+ socket.socket.connect = connect # type: ignore[method-assign]
871
+ self._egress_guard_installed = True
872
+
873
+ # ------------------------------------------------------------------ patch registry
874
+
875
+ def install_patch_hook(self) -> None:
876
+ """Record what `unittest.mock.patch` installs, for the whole session: fixtures may
877
+ enter patches before a test's call phase begins. A runtime-adapter detail."""
878
+ if self._orig_patch_enter is not None:
879
+ return
880
+ patch_cls: Any = mock._patch
881
+ tracer = self
882
+ self._orig_patch_enter, self._orig_patch_exit = patch_cls.__enter__, patch_cls.__exit__
883
+
884
+ def enter(patcher: Any) -> Any:
885
+ new = tracer._orig_patch_enter(patcher)
886
+ original = getattr(patcher, "temp_original", mock.DEFAULT)
887
+ tracer._patched[id(new)] = (patcher.target, patcher.attribute, original)
888
+ return new
889
+
890
+ def exit_(patcher: Any, *exc_info: Any) -> Any:
891
+ try:
892
+ current = getattr(patcher.target, patcher.attribute, None)
893
+ tracer._patched.pop(id(current), None)
894
+ except Exception:
895
+ pass
896
+ return tracer._orig_patch_exit(patcher, *exc_info)
897
+
898
+ patch_cls.__enter__, patch_cls.__exit__ = enter, exit_
899
+
900
+ # ------------------------------------------------------------------ lifecycle
901
+
902
+ def start(self, root_symbol: SymbolId, root_code: CodeType | None) -> None:
903
+ m = sys.monitoring
904
+ self._nodes.clear()
905
+ self._symbols.clear()
906
+ self._stacks.clear()
907
+ self._threads.clear()
908
+ self._frame_nodes.clear()
909
+ self._fake_wrappers.clear()
910
+ self._stack_repairs = 0
911
+ self._attribution_disagreements = 0
912
+ self._pending_mock_calls.clear()
913
+ self._root_code = root_code
914
+ origin = Origin.TEST
915
+ location = None
916
+ if root_code is not None and (scope := self.scope_for(root_code)):
917
+ origin, location = scope.origin, scope.location
918
+ self._intern(root_symbol, origin, location)
919
+ self._nodes.append(CallNode(0, None, root_symbol, 0, (), 0))
920
+ self._thread()
921
+ m.use_tool_id(_TOOL_ID, "diffgenome")
922
+ m.register_callback(_TOOL_ID, m.events.PY_START, self._on_start)
923
+ m.register_callback(_TOOL_ID, m.events.PY_RETURN, self._on_return)
924
+ m.register_callback(_TOOL_ID, m.events.PY_UNWIND, self._on_unwind)
925
+ m.register_callback(_TOOL_ID, m.events.CALL, self._on_call)
926
+ m.register_callback(_TOOL_ID, m.events.BRANCH, self._on_branch)
927
+ self._branches.clear()
928
+ mixin: Any = mock.CallableMixin # private attribute, absent from typeshed
929
+ self._orig_mock_call = mixin._mock_call
930
+ tracer = self
931
+
932
+ def _traced_mock_call(mock_self: Any, /, *args: Any, **kwargs: Any) -> Any:
933
+ return tracer._mock_call(mock_self, *args, **kwargs)
934
+
935
+ mixin._mock_call = _traced_mock_call
936
+ m.restart_events()
937
+ m.set_events(
938
+ _TOOL_ID,
939
+ m.events.PY_START
940
+ | m.events.PY_RETURN
941
+ | m.events.PY_UNWIND
942
+ | m.events.CALL
943
+ | m.events.BRANCH,
944
+ )
945
+
946
+ def stop(self) -> TraceResult:
947
+ m = sys.monitoring
948
+ m.set_events(_TOOL_ID, 0)
949
+ for ev in (
950
+ m.events.PY_START,
951
+ m.events.PY_RETURN,
952
+ m.events.PY_UNWIND,
953
+ m.events.CALL,
954
+ m.events.BRANCH,
955
+ ):
956
+ m.register_callback(_TOOL_ID, ev, None)
957
+ m.free_tool_id(_TOOL_ID)
958
+ if self._orig_mock_call is not None:
959
+ mixin: Any = mock.CallableMixin
960
+ mixin._mock_call = self._orig_mock_call
961
+ self._orig_mock_call = None
962
+ return TraceResult(
963
+ symbols=tuple(self._symbols[k] for k in sorted(self._symbols)),
964
+ nodes=tuple(self._nodes),
965
+ stack_repairs=self._stack_repairs,
966
+ attribution_disagreements=self._attribution_disagreements,
967
+ branches=tuple(self._branches),
968
+ )
969
+
970
+
971
+ def _static_attr(cls: Any, name: str) -> Any:
972
+ """Attribute lookup through the MRO without invoking descriptors or inspect."""
973
+ for klass in getattr(cls, "__mro__", (cls,)):
974
+ d = getattr(klass, "__dict__", {})
975
+ if name in d:
976
+ return d[name]
977
+ return None
978
+
979
+
980
+ class _CodeHolder:
981
+ """Lets `symbol_for_object` name a fake by its code object without a function object."""
982
+
983
+ def __init__(self, code: CodeType) -> None:
984
+ self.__code__ = code
985
+ self.__qualname__ = code.co_qualname