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/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
|