diffgenome 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (53) hide show
  1. diffgenome/__init__.py +7 -0
  2. diffgenome/__main__.py +240 -0
  3. diffgenome/_collectors/go/dg/dg.go +623 -0
  4. diffgenome/_collectors/go/go.mod +3 -0
  5. diffgenome/_collectors/go/instrument/facts.go +346 -0
  6. diffgenome/_collectors/go/instrument/main.go +484 -0
  7. diffgenome/_collectors/node/instrument.js +289 -0
  8. diffgenome/_collectors/node/jest-setup.js +40 -0
  9. diffgenome/_collectors/node/package-lock.json +35 -0
  10. diffgenome/_collectors/node/package.json +11 -0
  11. diffgenome/_collectors/node/runtime.js +426 -0
  12. diffgenome/ambiguity.py +122 -0
  13. diffgenome/api.py +67 -0
  14. diffgenome/change.py +86 -0
  15. diffgenome/change_artifact.py +310 -0
  16. diffgenome/collect/__init__.py +2 -0
  17. diffgenome/collect/go_test.py +271 -0
  18. diffgenome/collect/node_jest.py +319 -0
  19. diffgenome/collect/py_monitoring.py +985 -0
  20. diffgenome/collect/py_runtime.py +116 -0
  21. diffgenome/collect/py_symbols.py +238 -0
  22. diffgenome/collect/pytest_plugin.py +130 -0
  23. diffgenome/compose.py +469 -0
  24. diffgenome/dependence.py +264 -0
  25. diffgenome/evaluate.py +669 -0
  26. diffgenome/frontends/__init__.py +0 -0
  27. diffgenome/frontends/python_ir.py +335 -0
  28. diffgenome/genome.py +1016 -0
  29. diffgenome/genome_pipeline.py +674 -0
  30. diffgenome/genome_prompt.py +33 -0
  31. diffgenome/genome_state.py +2118 -0
  32. diffgenome/graph.py +426 -0
  33. diffgenome/llm.py +189 -0
  34. diffgenome/model.py +364 -0
  35. diffgenome/mvp.py +398 -0
  36. diffgenome/probe.py +509 -0
  37. diffgenome/projection.py +308 -0
  38. diffgenome/py.typed +0 -0
  39. diffgenome/render.py +118 -0
  40. diffgenome/report.py +363 -0
  41. diffgenome/resolve.py +37 -0
  42. diffgenome/runtime.py +74 -0
  43. diffgenome/runtime_evidence.py +261 -0
  44. diffgenome/sandbox.py +166 -0
  45. diffgenome/serialize.py +96 -0
  46. diffgenome/sites.py +19 -0
  47. diffgenome/static_types.py +69 -0
  48. diffgenome/structure.py +462 -0
  49. diffgenome-0.1.0.dist-info/METADATA +139 -0
  50. diffgenome-0.1.0.dist-info/RECORD +53 -0
  51. diffgenome-0.1.0.dist-info/WHEEL +4 -0
  52. diffgenome-0.1.0.dist-info/entry_points.txt +2 -0
  53. diffgenome-0.1.0.dist-info/licenses/LICENSE +202 -0
diffgenome/model.py ADDED
@@ -0,0 +1,364 @@
1
+ """Core, language-agnostic data model: the diffgenome event protocol and evidence types.
2
+
3
+ Two layers, deliberately kept apart:
4
+
5
+ 1. **Observation** (`Execution` and its nodes): raw facts emitted by a capture adapter.
6
+ Adapters differ completely per runtime (sys.monitoring, JVMTI, uprobes, eBPF, perf,
7
+ V8 hooks...) but all emit exactly this. An adapter records *what happened* and never
8
+ interprets it. Two capture **planes** feed it:
9
+
10
+ - the *OS plane* (syscalls, sockets, files, processes): universal, gives physical
11
+ boundaries and native call structure;
12
+ - the *symbol plane* (logical calls inside the runtime): per-runtime, gives the
13
+ program's own call structure.
14
+
15
+ 2. **Interpretation** (`BoundaryResolution`, `Evidence`, `Edge`): conclusions drawn from
16
+ observations. Every conclusion carries the rule and the observation(s) it came from,
17
+ so provenance is never lost and a composed edge can never pass as an observed one.
18
+
19
+ Nothing in this module may mention a language, a test framework or a mocking library.
20
+ See docs/architecture.md for the reasoning behind each type.
21
+ """
22
+
23
+ from __future__ import annotations
24
+
25
+ from dataclasses import dataclass
26
+ from enum import Enum
27
+
28
+ SymbolId = str
29
+ """Symbol identity: ``<language>:<qualified name>``,
30
+ e.g. ``py:shop.pricing_service.PricingService.quote`` or ``go:db.Store.TransferTx``.
31
+ This is the key that lets fragments from different executions be joined, so both sides of
32
+ a seam must produce it identically.
33
+
34
+ **Provisional.** Sufficient for experiment 01; not assumed sufficient long term. Known
35
+ identity problems: source path vs module, defining type vs receiver type, overloads and
36
+ signatures, interfaces vs implementations, closures, generated functions, generics,
37
+ monorepos, dynamic runtimes. Whenever two components disagree about an identity, raise
38
+ ``IdentityMismatch``: compositions must never be lost silently."""
39
+
40
+
41
+ class IdentityMismatch(Exception):
42
+ """Two components produced incompatible identities for what should be one symbol."""
43
+
44
+
45
+ class Origin(Enum):
46
+ REPO = "repo" # defined in the repository's source roots
47
+ TEST = "test" # defined in the repository's test code (tests, fakes, helpers)
48
+ EXTERNAL = "external" # stdlib, runtime, third-party, or otherwise outside the repository
49
+ UNKNOWN = "unknown"
50
+
51
+
52
+ @dataclass(frozen=True)
53
+ class SourceLocation:
54
+ path: str # repository-relative for REPO/TEST symbols
55
+ line: int
56
+
57
+
58
+ @dataclass(frozen=True)
59
+ class Symbol:
60
+ id: SymbolId
61
+ origin: Origin
62
+ location: SourceLocation | None = None
63
+ kind: str = "callable"
64
+ """``callable`` (has an executable body) or ``declaration`` (a class/type/interface whose
65
+ construction or reference runs no in-repo code of its own). A stand-in that claims a
66
+ declaration is not a gap in behavior: there is no application-owned behavior there."""
67
+
68
+
69
+ # --------------------------------------------------------------------------- observation
70
+
71
+
72
+ class Plane(Enum):
73
+ OS = "os" # kernel-visible: syscalls, sockets, files, processes, native symbols
74
+ SYMBOL = "symbol" # runtime-visible: the program's logical functions and calls
75
+
76
+
77
+ class Fidelity(Enum):
78
+ COMPLETE = "complete" # every event of this kind was recorded, in order
79
+ SAMPLED = "sampled" # periodic snapshots; presence is evidence, order and count are not
80
+
81
+
82
+ class Stimulus(Enum):
83
+ """How execution was caused. It is only ever a way to *cause* execution, never how
84
+ execution is *observed*."""
85
+
86
+ EXISTING_TEST = "existing_test"
87
+ GENERATED_PROBE = "generated_probe" # disposable isolated unit probe (later experiment)
88
+ DIRECT_CALL = "direct_call" # harness invoked an entrypoint directly
89
+
90
+
91
+ @dataclass(frozen=True)
92
+ class Collector:
93
+ """The capture adapter that produced a set of nodes, and what its output means."""
94
+
95
+ name: str # e.g. "py-sys-monitoring", "linux-ptrace-syscalls", "perf-sample"
96
+ plane: Plane
97
+ fidelity: Fidelity
98
+
99
+
100
+ class SubstitutionMechanism(Enum):
101
+ """How the real implementation was kept out of the execution. Language-neutral: a mock
102
+ object, a hand-written fake, a DI binding, a monkey-patched module attribute, an
103
+ LD_PRELOAD interposer and a stubbed function pointer are all the same thing here."""
104
+
105
+ MOCK_OBJECT = "mock_object" # framework-generated stand-in (unittest.mock, gomock, jest.fn)
106
+ FAKE = "fake" # hand-written in-test implementation
107
+ DI_BINDING = "di_binding" # injector configured with a test implementation
108
+ INTERPOSITION = "interposition" # attribute/symbol replaced (patch, monkeypatch, LD_PRELOAD)
109
+ STUB = "stub" # function pointer/callback replaced
110
+ UNKNOWN = "unknown"
111
+
112
+
113
+ ArgShapes = tuple[tuple[str, str, str], ...]
114
+ """Bounded argument summary for seam matching: ``((name, shape, digest), ...)`` in
115
+ positional order, receiver excluded. ``shape`` is ``<type>`` or ``<type>[<size>]``.
116
+ ``digest`` is a stable content digest of the value to a bounded depth (never the value
117
+ itself), or ``""`` when the collector could not summarize it. A stand-in call that only
118
+ exposes positional arguments names them ``arg0``, ``arg1``... Composition compares by
119
+ position: ``ARG_SHAPE`` compares types only; ``VALUE`` requires equal, non-empty digests
120
+ at every observed position. Sizes are value-level and never affect ``ARG_SHAPE``."""
121
+
122
+ Outcome = str
123
+ """How a call ended, as ``<category>`` or ``<category>:<identity>``. Categories are
124
+ language-neutral exit kinds; adapters normalize their runtime's notions into them:
125
+
126
+ - ``returned`` normal completion (Python return, Node resolve, Go nil error)
127
+ - ``returned-error`` completion that signals failure by value (Go non-nil ``error``)
128
+ - ``raised`` exception / throw / rejection, identity = the type's symbol
129
+ - ``panic`` runtime abort (Go panic), identity = the panic value's type
130
+ - ``cancelled`` cooperative cancellation
131
+ - ``unknown`` not observed
132
+
133
+ Exit compatibility (see compose) compares category first, then identity when both are
134
+ known: a returning seam joined to a raising fragment, or an error-returning seam joined
135
+ to a panicking fragment, is a conflict, and the join is unsound."""
136
+
137
+ StateFacts = tuple[tuple[str, str, str], ...]
138
+ """Bounded execution-state summary at a call or a seam: ``((fact, bucket, digest), ...)``.
139
+ Facts are *branch-relevant, low-cardinality* observations, never raw values:
140
+ ``self:type`` (concrete receiver type), ``self.<field>`` for receiver fields, and
141
+ ``global.<name>`` for module/package state the function reads. Buckets are language-
142
+ neutral: ``bool:true|false``, ``none``, ``enum:<member>``, ``num:zero|pos|neg``,
143
+ ``str:empty|nonempty``, ``coll:empty|one|many``, ``obj:<type>`` (dependency identity).
144
+ ``digest`` is filled only for enum members and type names (already public identifiers);
145
+ scalars are stored as buckets only, so persisted state exposes no values. The ``STATE``
146
+ join compares facts by name: any differing bucket is a state conflict; all equal (and at
147
+ least one compared) is a ``STATE`` match; no facts in common leaves the seam at ``VALUE``."""
148
+
149
+
150
+ @dataclass(frozen=True)
151
+ class CallNode:
152
+ """One logical call observed on the symbol plane. Nodes form a tree via ``parent``."""
153
+
154
+ id: int # unique within its execution
155
+ parent: int | None # None only for the execution's root (the stimulus itself)
156
+ symbol: SymbolId
157
+ collector: int # index into Execution.collectors
158
+ args: ArgShapes = ()
159
+ thread: int = 0 # 0 is the stimulus thread; others numbered in order of first appearance
160
+ outcome: Outcome = "unknown"
161
+ result: str = "" # digest of the returned value (same scheme as ArgShapes), "" if unavailable
162
+ state: StateFacts = () # receiver/global state at entry; the fragment side of STATE
163
+ state_after: StateFacts = () # the same bounded view on exit (observed state deltas)
164
+
165
+
166
+ @dataclass(frozen=True)
167
+ class SubstitutionNode:
168
+ """Control left the production symbol space into a stand-in.
169
+
170
+ Three separate facts are kept apart: what actually executed (``substitute``), what
171
+ production component it stands in for (``claimed_target``), and how the two are related
172
+ (``relation``). A stand-in is **not necessarily a leaf**: a framework mock usually is,
173
+ but a hand-written fake, an in-memory repository, a DI test implementation or a proxy
174
+ executes code of its own, and that code appears as children of this node."""
175
+
176
+ id: int
177
+ parent: int # the real symbol-plane call that invoked the stand-in
178
+ collector: int
179
+ mechanism: SubstitutionMechanism
180
+ substitute: SymbolId | None
181
+ """What actually executed: the fake's class, the stub function, the mock's runtime type.
182
+ None when the substitute has no meaningful symbol of its own."""
183
+ claimed_target: SymbolId | None
184
+ """What the stand-in *says* it replaces, read off the artefact itself (a spec class, a
185
+ patch target string, a generated-mock interface). None when it says nothing. This is
186
+ a fact about the artefact, not a resolution: interpretation happens downstream."""
187
+ relation: str
188
+ """The observable relationship between substitute and claimed target, e.g. "spec",
189
+ "patch-target", "implements", "generated-from", "none". Lets the resolver weight the
190
+ claim and lets a reader audit it."""
191
+ path: tuple[str, ...] = ()
192
+ """Member path invoked on the stand-in, e.g. ``("execute", "()", "fetchone")`` where
193
+ ``"()"`` marks a call's return value."""
194
+ args: ArgShapes = ()
195
+ outcome: Outcome = "unknown"
196
+ result: str = ""
197
+ """Digest of what the stand-in returned. The seed's continuation after this node is
198
+ conditioned on this value; a composed fragment that returned something else leaves
199
+ that continuation unsupported by any execution."""
200
+ state: StateFacts = ()
201
+ """State the real target would have seen at this seam, when the seed can observe it:
202
+ the receiver whose member was replaced (instance-attribute interposition, subclass
203
+ fakes) or the globals a patched module function reads. Empty when the stand-in
204
+ replaced the whole object, which is the honest common case."""
205
+
206
+
207
+ class OsEventKind(Enum):
208
+ CONNECT = "connect" # outbound socket connection attempt
209
+ LISTEN = "listen"
210
+ DNS = "dns"
211
+ OPEN = "open" # file open (path recorded; may be a local config file, see docs)
212
+ EXEC = "exec" # process image replaced
213
+ SPAWN = "spawn" # child process created
214
+ OTHER = "other"
215
+
216
+
217
+ @dataclass(frozen=True)
218
+ class OsEventNode:
219
+ """A kernel-visible event, attributed to the innermost symbol-plane call active at the
220
+ time when attribution is possible, otherwise to the execution root."""
221
+
222
+ id: int
223
+ parent: int
224
+ collector: int
225
+ kind: OsEventKind
226
+ target: str # endpoint, path, or command as the kernel saw it
227
+ outcome: str # "ok", or the errno/reason it failed, e.g. "ENETUNREACH" inside the sandbox
228
+ native_symbol: str | None = None # calling native symbol when the OS plane could resolve it
229
+
230
+
231
+ Node = CallNode | SubstitutionNode | OsEventNode
232
+
233
+
234
+ @dataclass(frozen=True)
235
+ class BranchObs:
236
+ """One observed decision: the condition at `site` (see `diffgenome.sites`) evaluated to
237
+ `outcome` inside call node `node`. `seq` is the number of nodes that existed when the
238
+ branch was taken, which orders it against the calls around it."""
239
+
240
+ site: str
241
+ outcome: bool
242
+ node: int
243
+ seq: int
244
+
245
+
246
+ @dataclass(frozen=True)
247
+ class Execution:
248
+ """One stimulus applied to one process, and everything observed about it."""
249
+
250
+ id: str # stable, e.g. "<stimulus_ref>@<revision>"
251
+ stimulus: Stimulus
252
+ stimulus_ref: str # test id, probe path, or entrypoint symbol
253
+ outcome: str # "passed" | "failed" | ...; only passing executions are composition sources
254
+ revision: str | None # VCS revision the trace was taken at, if known
255
+ collectors: tuple[Collector, ...]
256
+ symbols: tuple[Symbol, ...] # every SymbolId referenced by nodes, with origin and location
257
+ nodes: tuple[Node, ...]
258
+ diagnostics: tuple[tuple[str, str], ...] = ()
259
+ """Collector self-reports as (key, value), e.g. ``("stack_repairs", "3")``. Facts about
260
+ the instrument, not about the target; a reader uses them to judge the trace."""
261
+ branches: tuple[BranchObs, ...] = () # observed decision outcomes, in order
262
+
263
+
264
+ # ------------------------------------------------------------------------ interpretation
265
+
266
+
267
+ class BoundaryClass(Enum):
268
+ INTERNAL = "internal" # stands in for code in this repository: a continuation point
269
+ EXTERNAL = "external" # stands in for something outside the repository: terminal
270
+ UNRESOLVED = "unresolved" # not enough evidence to decide: terminal, reported, never guessed
271
+
272
+
273
+ @dataclass(frozen=True)
274
+ class BoundaryResolution:
275
+ classification: BoundaryClass
276
+ target: SymbolId | None # the real symbol the stand-in represents, when known
277
+ rule: str # which deterministic rule decided this
278
+
279
+
280
+ class JoinStrength(Enum):
281
+ """How a composed seam's ENTRY was matched. Each level implies the ones before it.
282
+ Join validity = entry compatibility x exit compatibility (see `Evidence.exit`)."""
283
+
284
+ SYMBOL = 1 # same SymbolId on both sides
285
+ ARG_SHAPE = 2 # and argument arity/types compatible
286
+ VALUE = 3 # and argument values at the seam compatible
287
+ STATE = 4 # and branch-relevant receiver/global state at the seam compatible
288
+
289
+
290
+ class EvidenceKind(Enum):
291
+ OBSERVED = "observed" # complete trace: caller called callee, same execution
292
+ OBSERVED_SAMPLED = "observed_sampled" # sampled: callee seen under caller; order/count unknown
293
+ COMPOSED = "composed" # call hit an internal stand-in; continuation is another execution
294
+ STATIC = "static" # statically possible, never executed (reserved; not produced yet)
295
+ INTERNAL_GAP = "internal_gap" # internal stand-in, no execution of the real target yet
296
+ EXTERNAL_BOUNDARY = "external_boundary" # semantic boundary: stand-in for outside code
297
+ OS_BOUNDARY = "os_boundary" # physical boundary: the process tried to leave via the kernel
298
+ UNRESOLVED_BOUNDARY = "unresolved_boundary" # stand-in we could not classify
299
+
300
+
301
+ _RULE_REQUIRED = {
302
+ EvidenceKind.COMPOSED,
303
+ EvidenceKind.STATIC,
304
+ EvidenceKind.INTERNAL_GAP,
305
+ EvidenceKind.EXTERNAL_BOUNDARY,
306
+ EvidenceKind.UNRESOLVED_BOUNDARY,
307
+ }
308
+ _RULE_FORBIDDEN = {EvidenceKind.OBSERVED, EvidenceKind.OBSERVED_SAMPLED, EvidenceKind.OS_BOUNDARY}
309
+
310
+
311
+ @dataclass(frozen=True)
312
+ class NodeRef:
313
+ execution: str
314
+ node: int
315
+
316
+
317
+ @dataclass(frozen=True)
318
+ class Evidence:
319
+ """Why we believe an edge exists. Invariants are enforced at construction so that
320
+ evidence can never be silently upgraded."""
321
+
322
+ kind: EvidenceKind
323
+ site: NodeRef
324
+ """The observed node that grounds this evidence. For boundary and composed kinds this is
325
+ the substitution or OS event itself: the *call into it* was observed even when the
326
+ continuation was not."""
327
+ rule: str | None = None
328
+ """Resolution or analysis rule. Required for anything derived (composed, static,
329
+ boundaries); forbidden for direct observations, which have no rule to cite."""
330
+ fragment: NodeRef | None = None
331
+ """COMPOSED only: the root of the borrowed continuation, a real call to the target in
332
+ some (usually different) execution."""
333
+ join: JoinStrength | None = None
334
+ """COMPOSED only: how strongly the seam was matched."""
335
+ alternates: tuple[NodeRef, ...] = ()
336
+ """COMPOSED only: other fragments with the same behavior shape as ``fragment`` that
337
+ support this same edge. Merged for expansion, never dropped."""
338
+ exit: str | None = None
339
+ """COMPOSED only: exit compatibility of seam and fragment: ``same`` (category and
340
+ identity agree), ``kind`` (category agrees; an identity is unknown), or ``unknown``
341
+ (an outcome was not observed). Conflicts never become evidence."""
342
+ probe_derived: bool = False
343
+ """True if any execution this evidence cites was a generated probe rather than an
344
+ existing test. Set by whoever builds the evidence and knows the executions."""
345
+
346
+ def __post_init__(self) -> None:
347
+ if self.kind in _RULE_REQUIRED and self.rule is None:
348
+ raise ValueError(f"{self.kind.value} evidence must name its rule")
349
+ if self.kind in _RULE_FORBIDDEN and self.rule is not None:
350
+ raise ValueError(f"{self.kind.value} evidence is a direct observation and has no rule")
351
+ composed = self.kind is EvidenceKind.COMPOSED
352
+ if composed != (self.fragment is not None):
353
+ raise ValueError("a fragment is required for, and only for, composed evidence")
354
+ if composed != (self.join is not None):
355
+ raise ValueError("a join strength is required for, and only for, composed evidence")
356
+ if self.alternates and not composed:
357
+ raise ValueError("alternates only apply to composed evidence")
358
+
359
+
360
+ @dataclass(frozen=True)
361
+ class Edge:
362
+ caller: SymbolId
363
+ callee: SymbolId
364
+ evidence: Evidence