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/projection.py
ADDED
|
@@ -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"
|