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,426 @@
1
+ "use strict";
2
+ /*
3
+ * diffgenome Node runtime: receives events from source-instrumented code and emits one
4
+ * Execution (diffgenome event protocol, JSON) per stimulus. No dependencies.
5
+ *
6
+ * Attribution uses AsyncLocalStorage: `run` (async functions, generators) scopes the
7
+ * store to the body so continuations after `await` keep their parent and the caller's
8
+ * store is restored on the synchronous return; `enter`/`exit` (sync functions) set and
9
+ * restore the store explicitly. Observer integrity: everything this module needs is bound
10
+ * at load time (fs, crypto, path, the AsyncLocalStorage class), so a target that patches
11
+ * globals or modules later cannot redirect it.
12
+ */
13
+ const fs = require("fs");
14
+ const path = require("path");
15
+ const crypto = require("crypto");
16
+ const { AsyncLocalStorage } = require("async_hooks");
17
+ const net = require("net");
18
+
19
+ const createHash = crypto.createHash;
20
+ const writeFileSync = fs.writeFileSync;
21
+ const mkdirSync = fs.mkdirSync;
22
+ const joinPath = path.join;
23
+ const ObjectKeys = Object.keys;
24
+ const ObjectGetPrototypeOf = Object.getPrototypeOf;
25
+ const ArrayIsArray = Array.isArray;
26
+ const JSONStringify = JSON.stringify;
27
+
28
+ const als = new AsyncLocalStorage();
29
+ const MAX_ARGS = 8;
30
+ const DIGEST_DEPTH = 3;
31
+ const DIGEST_WIDTH = 16;
32
+
33
+ const state = {
34
+ active: false,
35
+ stimulus: process.env.DIFFGENOME_STIMULUS || "existing_test",
36
+ outDir: process.env.DIFFGENOME_OUT || null,
37
+ revision: process.env.DIFFGENOME_REVISION || null,
38
+ ref: null,
39
+ nodes: [],
40
+ symbols: new Map(),
41
+ diagnostics: [],
42
+ egress: 0,
43
+ };
44
+
45
+ // ----------------------------------------------------------------------------- summaries
46
+
47
+ function shape(v) {
48
+ if (v === null) return "null";
49
+ const t = typeof v;
50
+ if (t === "undefined" || t === "boolean" || t === "number" || t === "bigint" || t === "string" || t === "symbol") return t;
51
+ if (t === "function") return v._isMockFunction ? "stand-in" : "function";
52
+ if (ArrayIsArray(v)) return `array[${v.length}]`;
53
+ if (v instanceof Promise) return "Promise";
54
+ const proto = ObjectGetPrototypeOf(v);
55
+ if (proto === null || proto === Object.prototype) return `object[${ObjectKeys(v).length}]`;
56
+ const name = proto.constructor && proto.constructor.name;
57
+ return name || "object";
58
+ }
59
+
60
+ function canonical(v, depth) {
61
+ if (v === null) return "null";
62
+ const t = typeof v;
63
+ if (t === "undefined") return "undefined";
64
+ if (t === "boolean" || t === "number" || t === "bigint") return `${t}:${String(v)}`;
65
+ if (t === "string") return `string:${JSONStringify(v)}`;
66
+ if (t === "symbol" || t === "function") return null;
67
+ if (depth === 0) return null;
68
+ if (ArrayIsArray(v)) {
69
+ if (v.length > DIGEST_WIDTH) return null;
70
+ const parts = [];
71
+ for (const x of v) {
72
+ const c = canonical(x, depth - 1);
73
+ if (c === null) return null;
74
+ parts.push(c);
75
+ }
76
+ return `array[${parts.join(",")}]`;
77
+ }
78
+ if (v instanceof Promise || v instanceof Date || v instanceof RegExp || v instanceof Map || v instanceof Set) return null;
79
+ const proto = ObjectGetPrototypeOf(v);
80
+ const plain = proto === null || proto === Object.prototype;
81
+ const keys = ObjectKeys(v);
82
+ if (keys.length > DIGEST_WIDTH) return null;
83
+ const items = [];
84
+ for (const k of keys.sort()) {
85
+ const c = canonical(v[k], depth - 1);
86
+ if (c === null) return null;
87
+ items.push(`${k}=${c}`);
88
+ }
89
+ const name = plain ? "object" : (proto.constructor && proto.constructor.name) || "object";
90
+ return `${name}{${items.join(",")}}`;
91
+ }
92
+
93
+ function digest(v) {
94
+ const c = canonical(v, DIGEST_DEPTH);
95
+ if (c === null) return "";
96
+ return createHash("sha256").update(c).digest("hex").slice(0, 16);
97
+ }
98
+
99
+ function argShapes(names, values) {
100
+ const out = [];
101
+ const n = Math.min(values.length, MAX_ARGS);
102
+ for (let i = 0; i < n; i++) {
103
+ const name = names && names[i] ? names[i] : `arg${i}`;
104
+ out.push([name, shape(values[i]), digest(values[i])]);
105
+ }
106
+ return out;
107
+ }
108
+
109
+ // ----------------------------------------------------------------------------- state facts
110
+
111
+ const STATE_WIDTH = 16;
112
+
113
+ function bucket(v) {
114
+ if (v === null || v === undefined) return ["none", ""];
115
+ const t = typeof v;
116
+ if (t === "boolean") return [`bool:${v}`, ""];
117
+ if (t === "number" || t === "bigint") return [v == 0 ? "num:zero" : v > 0 ? "num:pos" : "num:neg", ""];
118
+ if (t === "string") return [v.length ? "str:nonempty" : "str:empty", ""];
119
+ if (t === "function") return v._isMockFunction ? ["obj:stand-in", ""] : null;
120
+ if (ArrayIsArray(v)) return [v.length === 0 ? "coll:empty" : v.length === 1 ? "coll:one" : "coll:many", ""];
121
+ if (v instanceof Map || v instanceof Set) return [v.size === 0 ? "coll:empty" : v.size === 1 ? "coll:one" : "coll:many", ""];
122
+ if (v && typeof v === "object" && v._isMockObject) return ["obj:stand-in", ""];
123
+ const proto = ObjectGetPrototypeOf(v);
124
+ if (proto === null || proto === Object.prototype) return [`coll:${ObjectKeys(v).length === 0 ? "empty" : ObjectKeys(v).length === 1 ? "one" : "many"}`, ""];
125
+ const name = (proto.constructor && proto.constructor.name) || "object";
126
+ return [`obj:${name}`, digest(name)];
127
+ }
128
+
129
+ function stateFacts(receiver) {
130
+ if (receiver === undefined || receiver === null || typeof receiver !== "object") return [];
131
+ const facts = [];
132
+ const proto = ObjectGetPrototypeOf(receiver);
133
+ const name = (proto && proto.constructor && proto.constructor.name) || "object";
134
+ facts.push(["self:type", `obj:${name}`, digest(name)]);
135
+ for (const k of ObjectKeys(receiver).slice(0, STATE_WIDTH)) {
136
+ const b = bucket(receiver[k]);
137
+ if (b) facts.push([`self.${k}`, b[0], b[1]]);
138
+ }
139
+ return facts;
140
+ }
141
+
142
+ // ----------------------------------------------------------------------------- nodes
143
+
144
+ function currentNode() {
145
+ const store = als.getStore();
146
+ return store ? store.node : null;
147
+ }
148
+
149
+ function intern(symbol, origin, file, line) {
150
+ if (!state.symbols.has(symbol)) {
151
+ state.symbols.set(symbol, { id: symbol, origin, location: file ? { path: file, line } : null });
152
+ }
153
+ }
154
+
155
+ function nodeOrigin(id) {
156
+ const n = state.nodes[id];
157
+ if (!n || n.type !== "call") return "unknown";
158
+ const s = state.symbols.get(n.symbol);
159
+ return s ? s.origin : "unknown";
160
+ }
161
+
162
+ function newCall(symbol, args, parent, facts) {
163
+ const id = state.nodes.length;
164
+ state.nodes.push({
165
+ type: "call", id, parent, symbol, collector: 0, args, thread: 0, outcome: "unknown", result: "",
166
+ state: facts || [],
167
+ });
168
+ return id;
169
+ }
170
+
171
+ function newSubstitution(parent, mechanism, substitute, claimed, relation, pathParts, args, facts) {
172
+ const id = state.nodes.length;
173
+ state.nodes.push({
174
+ type: "substitution", id, parent, collector: 0, mechanism, substitute, claimed_target: claimed,
175
+ relation, path: pathParts, args, outcome: "unknown", result: "", state: facts || [],
176
+ });
177
+ return id;
178
+ }
179
+
180
+ function settle(id, outcome, result) {
181
+ const n = state.nodes[id];
182
+ if (!n || n.outcome !== "unknown") return;
183
+ n.outcome = outcome;
184
+ n.result = result || "";
185
+ }
186
+
187
+ function errorSymbol(e) {
188
+ const name = e && e.constructor && e.constructor.name ? e.constructor.name : "Error";
189
+ const sym = `js:${name}`;
190
+ intern(sym, "external", null, 0);
191
+ return sym;
192
+ }
193
+
194
+ const classSymbols = new Map(); // constructor -> {sym, origin}
195
+
196
+ /** Registered by instrumented modules after each named top-level class declaration. */
197
+ function registerClass(ctor, sym, origin) {
198
+ if (typeof ctor === "function") classSymbols.set(ctor, { sym, origin });
199
+ }
200
+
201
+ /** A test-origin method overriding a repo class's member: the fake's claim is that member,
202
+ * found by walking `this`'s prototype chain to the nearest registered repo class that
203
+ * defines the same name. Identity through the prototype chain, not a name match. */
204
+ function overriddenMember(meta, thisArg) {
205
+ if (thisArg === undefined || thisArg === null) return null;
206
+ const member = meta.sym.slice(meta.sym.lastIndexOf(".") + 1);
207
+ if (!member || member.startsWith("<")) return null;
208
+ let proto = ObjectGetPrototypeOf(thisArg);
209
+ let hops = 0;
210
+ while (proto && proto !== Object.prototype && hops < 20) {
211
+ const reg = classSymbols.get(proto.constructor);
212
+ if (reg && reg.origin === "repo" && Object.prototype.hasOwnProperty.call(proto, member)) {
213
+ const sym = `${reg.sym}.${member}`;
214
+ intern(sym, "repo", null, 0);
215
+ return sym;
216
+ }
217
+ proto = ObjectGetPrototypeOf(proto);
218
+ hops += 1;
219
+ }
220
+ return null;
221
+ }
222
+
223
+ /** Called on function entry. `meta`: {sym, origin, file, line, params}. Returns a context. */
224
+ function enter(meta, values, thisArg) {
225
+ if (!state.active) return null;
226
+ intern(meta.sym, meta.origin, meta.file, meta.line);
227
+ const args = argShapes(meta.params, values);
228
+ let parent = currentNode();
229
+ if (parent === null) parent = 0; // the stimulus root created by begin()
230
+ let sub = null;
231
+ if (meta.origin === "test" && nodeOrigin(parent) === "repo") {
232
+ // Production code called into test-defined code: a fake/stub that executes.
233
+ const mechanism = meta.sym.includes(".") && !/\.<anon>@\d+$/.test(meta.sym) ? "fake" : "stub";
234
+ let claimed = null;
235
+ let relation = "none";
236
+ const overridden = overriddenMember(meta, thisArg);
237
+ if (overridden) { claimed = overridden; relation = "overrides"; }
238
+ // A subclass fake's inherited fields are the real receiver's state.
239
+ sub = newSubstitution(parent, mechanism, meta.sym, claimed, relation, [], args, claimed ? stateFacts(thisArg) : []);
240
+ parent = sub;
241
+ }
242
+ const id = newCall(meta.sym, args, parent, stateFacts(thisArg));
243
+ const prev = als.getStore();
244
+ als.enterWith({ node: id });
245
+ return { id, prev, sub };
246
+ }
247
+
248
+ function exit(ctx) {
249
+ if (!ctx) return;
250
+ settle(ctx.id, "returned", "");
251
+ if (ctx.sub !== null) settle(ctx.sub, state.nodes[ctx.id].outcome, state.nodes[ctx.id].result);
252
+ als.enterWith(ctx.prev || { node: null });
253
+ }
254
+
255
+ function ret(ctx, value) {
256
+ if (!ctx) return value;
257
+ if (value && typeof value.then === "function") {
258
+ value.then(
259
+ (v) => { settle(ctx.id, "returned", digest(v)); if (ctx.sub !== null) settle(ctx.sub, "returned", digest(v)); },
260
+ (e) => { const s = "raised:" + errorSymbol(e); settle(ctx.id, s, ""); if (ctx.sub !== null) settle(ctx.sub, s, ""); },
261
+ );
262
+ return value;
263
+ }
264
+ settle(ctx.id, "returned", digest(value));
265
+ if (ctx.sub !== null) settle(ctx.sub, "returned", digest(value));
266
+ return value;
267
+ }
268
+
269
+ function throwed(ctx, e) {
270
+ if (!ctx) return;
271
+ const s = "raised:" + errorSymbol(e);
272
+ settle(ctx.id, s, "");
273
+ if (ctx.sub !== null) settle(ctx.sub, s, "");
274
+ }
275
+
276
+ /** Async functions and generators: scope the store to the body via als.run. */
277
+ function run(meta, values, thisArg, fn) {
278
+ if (!state.active) return fn.call(thisArg, null);
279
+ const ctx = enter(meta, values, thisArg);
280
+ // enter() used enterWith; undo that and use run() so the caller's store is restored on
281
+ // the synchronous return and continuations inside keep the body's store.
282
+ als.enterWith(ctx.prev || { node: null });
283
+ let result;
284
+ try {
285
+ result = als.run({ node: ctx.id }, () => fn.call(thisArg, ctx));
286
+ } catch (e) {
287
+ throwed(ctx, e);
288
+ throw e;
289
+ }
290
+ return ret(ctx, result);
291
+ }
292
+
293
+ // ----------------------------------------------------------------------------- call sites
294
+
295
+ function describeMock(f) {
296
+ const name = typeof f.getMockName === "function" ? f.getMockName() : "";
297
+ const pathParts = name && name !== "jest.fn()" && name !== "mockConstructor" ? [name] : [];
298
+ return { substitute: "js:jest.fn", claimed: null, relation: "none", pathParts };
299
+ }
300
+
301
+ function recordStandIn(f, values) {
302
+ if (!state.active) return;
303
+ const parent = currentNode();
304
+ const d = describeMock(f);
305
+ const id = newSubstitution(parent === null ? 0 : parent, "mock_object", d.substitute, d.claimed, d.relation, d.pathParts, argShapes(null, values));
306
+ return id;
307
+ }
308
+
309
+ function finishStandIn(id, value) {
310
+ if (id === undefined) return value;
311
+ if (value && typeof value.then === "function") {
312
+ value.then((v) => settle(id, "returned", digest(v)), (e) => settle(id, "raised:" + errorSymbol(e), ""));
313
+ return value;
314
+ }
315
+ settle(id, "returned", digest(value));
316
+ return value;
317
+ }
318
+
319
+ /** obj.key(...args) */
320
+ function callm(obj, key, args) {
321
+ const f = obj == null ? undefined : obj[key];
322
+ if (typeof f !== "function") {
323
+ throw new TypeError(`${obj == null ? "undefined" : shape(obj)}.${String(key)} is not a function`);
324
+ }
325
+ if (f._isMockFunction) {
326
+ const id = recordStandIn(f, args);
327
+ try {
328
+ return finishStandIn(id, f.apply(obj, args));
329
+ } catch (e) {
330
+ if (id !== undefined) settle(id, "raised:" + errorSymbol(e), "");
331
+ throw e;
332
+ }
333
+ }
334
+ return f.apply(obj, args);
335
+ }
336
+
337
+ /** f(...args) */
338
+ function callf(f, args) {
339
+ if (typeof f !== "function") throw new TypeError(`${String(f)} is not a function`);
340
+ if (f._isMockFunction) {
341
+ const id = recordStandIn(f, args);
342
+ try {
343
+ return finishStandIn(id, f.apply(undefined, args));
344
+ } catch (e) {
345
+ if (id !== undefined) settle(id, "raised:" + errorSymbol(e), "");
346
+ throw e;
347
+ }
348
+ }
349
+ return f.apply(undefined, args);
350
+ }
351
+
352
+ // ----------------------------------------------------------------------------- egress guard
353
+
354
+ function isLoopback(host) {
355
+ return host === undefined || host === null || host === "" || host === "localhost" || host === "127.0.0.1" || host === "::1" || host === "0.0.0.0" || host === "::";
356
+ }
357
+
358
+ function installEgressGuard() {
359
+ const original = net.Socket.prototype.connect;
360
+ net.Socket.prototype.connect = function (...args) {
361
+ let host = "";
362
+ let port = "";
363
+ const a = args[0];
364
+ if (a && typeof a === "object" && !ArrayIsArray(a)) { host = a.host || a.path || ""; port = a.port || ""; }
365
+ else if (typeof a === "number") { port = a; host = typeof args[1] === "string" ? args[1] : ""; }
366
+ else if (typeof a === "string") { host = a; }
367
+ const target = host ? `${host}:${port}` : `:${port}`;
368
+ const allowed = isLoopback(host);
369
+ if (state.active) {
370
+ const parent = currentNode();
371
+ state.nodes.push({
372
+ type: "os_event", id: state.nodes.length, parent: parent === null ? 0 : parent, collector: 1,
373
+ kind: "connect", target, outcome: allowed ? "allowed:loopback" : "refused:egress-guard", native_symbol: null,
374
+ });
375
+ }
376
+ if (!allowed) {
377
+ state.egress += 1;
378
+ throw new Error(`diffgenome egress guard: connect to ${target} refused`);
379
+ }
380
+ return original.apply(this, args);
381
+ };
382
+ }
383
+
384
+ // ----------------------------------------------------------------------------- stimulus
385
+
386
+ function begin(ref) {
387
+ state.active = true;
388
+ state.ref = ref;
389
+ state.nodes = [];
390
+ state.symbols = new Map();
391
+ state.diagnostics = [];
392
+ state.egress = 0;
393
+ // Node 0 is the stimulus itself; every in-scope call with no in-scope caller hangs off it.
394
+ const sym = `js:${ref}`;
395
+ intern(sym, "test", null, 0);
396
+ newCall(sym, [], null);
397
+ als.enterWith({ node: null });
398
+ }
399
+
400
+ function end(outcome) {
401
+ state.active = false;
402
+ if (!state.outDir || state.ref === null) return null;
403
+ const execution = {
404
+ id: `${state.ref}@${state.revision || "unknown"}`,
405
+ stimulus: state.stimulus,
406
+ stimulus_ref: state.ref,
407
+ outcome,
408
+ revision: state.revision,
409
+ collectors: [
410
+ { name: "node-source-instrumentation", plane: "symbol", fidelity: "complete" },
411
+ { name: "node-egress-guard", plane: "os", fidelity: "complete" },
412
+ ],
413
+ symbols: [...state.symbols.values()].sort((a, b) => (a.id < b.id ? -1 : a.id > b.id ? 1 : 0)),
414
+ nodes: state.nodes,
415
+ diagnostics: [["egress_attempts", String(state.egress)]],
416
+ };
417
+ mkdirSync(state.outDir, { recursive: true });
418
+ const h = createHash("sha256").update(state.ref).digest("hex").slice(0, 12);
419
+ const name = state.ref.replace(/[^A-Za-z0-9_.-]+/g, "_").slice(0, 100);
420
+ writeFileSync(joinPath(state.outDir, `${name}-${h}.json`), JSONStringify(execution, null, 1) + "\n");
421
+ return execution;
422
+ }
423
+
424
+ installEgressGuard();
425
+
426
+ module.exports = { enter, exit, ret, throwed, run, callm, callf, begin, end, registerClass, _state: state };
@@ -0,0 +1,122 @@
1
+ """Ambiguity report: seams with more than one materially different continuation, how the
2
+ join lattice ranked the candidates, and which candidates were rejected and why. Derived
3
+ from the evidence graph's composed edges and its full list of join attempts."""
4
+
5
+ from __future__ import annotations
6
+
7
+ from collections import defaultdict
8
+ from typing import Any
9
+
10
+ from diffgenome.graph import BehavioralGraph
11
+ from diffgenome.model import EvidenceKind, JoinStrength, NodeRef
12
+
13
+
14
+ def _short(s: str) -> str:
15
+ return s.split(":", 1)[-1]
16
+
17
+
18
+ def ambiguous_seams(graph: BehavioralGraph) -> list[dict[str, Any]]:
19
+ """One entry per (site, target) seam whose accepted continuations span more than one
20
+ fragment shape, or that has rejected/unsound candidates. Candidates carry grade,
21
+ note, acceptance and outcome compatibility from the join attempts."""
22
+ attempts_by: dict[tuple[NodeRef, str], list[Any]] = defaultdict(list)
23
+ for a in graph.attempts:
24
+ attempts_by[(a.site, a.target)].append(a)
25
+ shapes_by: dict[tuple[NodeRef, str], int] = defaultdict(int)
26
+ alternates_by: dict[tuple[NodeRef, str], int] = defaultdict(int)
27
+ callers: dict[tuple[NodeRef, str], str] = {}
28
+ for e in graph.edges.values():
29
+ if e.kind is not EvidenceKind.COMPOSED:
30
+ continue
31
+ for ev in e.evidence:
32
+ key = (ev.site, e.callee)
33
+ shapes_by[key] += 1
34
+ alternates_by[key] += len(ev.alternates)
35
+ callers[key] = e.caller
36
+ out: list[dict[str, Any]] = []
37
+ for key, atts in attempts_by.items():
38
+ accepted = [a for a in atts if a.accepted]
39
+ rejected = [a for a in atts if not a.accepted]
40
+ distinct_shapes = shapes_by.get(key, 0)
41
+ if distinct_shapes <= 1 and not rejected:
42
+ continue
43
+ grades: dict[str, int] = defaultdict(int)
44
+ for a in accepted:
45
+ grades[a.grade.name if a.grade else "unsound"] += 1
46
+ out.append(
47
+ {
48
+ "caller": callers.get(key, "?"),
49
+ "site": {"execution": key[0].execution, "node": key[0].node},
50
+ "target": key[1],
51
+ "distinct_shapes_accepted": distinct_shapes,
52
+ "same_shape_alternates": alternates_by.get(key, 0),
53
+ "accepted_by_grade": dict(grades),
54
+ "best_grade": _best_grade([a.grade for a in accepted if a.grade]),
55
+ "rejected": [
56
+ {
57
+ "fragment": a.fragment.execution.split("@")[0],
58
+ "grade": a.grade.name if a.grade else "unsound",
59
+ "note": a.note,
60
+ "result_compatible": a.result_compatible,
61
+ }
62
+ for a in rejected
63
+ ],
64
+ "accepted": [
65
+ {
66
+ "fragment": a.fragment.execution.split("@")[0],
67
+ "grade": a.grade.name if a.grade else "unsound",
68
+ "note": a.note,
69
+ "result_compatible": a.result_compatible,
70
+ }
71
+ for a in accepted
72
+ ],
73
+ }
74
+ )
75
+ out.sort(key=lambda x: (-x["distinct_shapes_accepted"], x["target"]))
76
+ return out
77
+
78
+
79
+ def _best_grade(grades: list[JoinStrength]) -> str | None:
80
+ return max(grades, key=lambda g: g.value).name if grades else None
81
+
82
+
83
+ def render_ambiguity(graph: BehavioralGraph, limit: int = 40) -> str:
84
+ seams = ambiguous_seams(graph)
85
+ lines = [
86
+ "# Ambiguous and rejected compositions",
87
+ f"seams with >1 accepted continuation shape or with rejected candidates: {len(seams)}",
88
+ "",
89
+ ]
90
+ for s in seams[:limit]:
91
+ lines.append(
92
+ f"## {_short(s['caller'])} ⇢ {_short(s['target'])} "
93
+ f"[{s['distinct_shapes_accepted']} shapes accepted, "
94
+ f"+{s['same_shape_alternates']} same-shape; best={s['best_grade']}; "
95
+ f"by grade={s['accepted_by_grade']}]"
96
+ )
97
+ for a in s["accepted"][:6]:
98
+ rc = (
99
+ ""
100
+ if a["result_compatible"] is None
101
+ else f" result_compatible={a['result_compatible']}"
102
+ )
103
+ lines.append(f" accepted {a['grade']:9s} {a['fragment']} {a['note']}{rc}")
104
+ if len(s["accepted"]) > 6:
105
+ lines.append(f" … {len(s['accepted']) - 6} more accepted")
106
+ for a in s["rejected"][:6]:
107
+ lines.append(f" REJECTED {a['grade']:9s} {a['fragment']} {a['note']}")
108
+ if len(s["rejected"]) > 6:
109
+ lines.append(f" … {len(s['rejected']) - 6} more rejected")
110
+ lines.append("")
111
+ grade_counts: dict[str, int] = defaultdict(int)
112
+ for a in graph.attempts:
113
+ grade_counts[
114
+ (a.grade.name if a.grade else "unsound") + ("" if a.accepted else " rejected")
115
+ ] += 1
116
+ lines.append("## all join attempts by grade")
117
+ for k in sorted(grade_counts, key=lambda k: (-grade_counts[k], k)):
118
+ lines.append(f" {k}: {grade_counts[k]}")
119
+ return "\n".join(lines) + "\n"
120
+
121
+
122
+ _ = JoinStrength # re-exported for callers that key by grade
diffgenome/api.py ADDED
@@ -0,0 +1,67 @@
1
+ """Library surface for integrators (Sydes and others).
2
+
3
+ `analyze_change` runs the pipeline for one change and returns the ``diffgenome-change/1``
4
+ artifact as a dict; `load_change_artifact` reads one back. Everything an integrator needs is
5
+ in the artifact; nothing here exposes collectors, corpora or graphs.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import json
11
+ from pathlib import Path
12
+ from typing import Any
13
+
14
+ from diffgenome.change_artifact import FORMAT
15
+
16
+ ARTIFACT_FILENAME = "diffgenome-change.json"
17
+ MAP_FILENAME = "behavioral-map.md"
18
+
19
+
20
+ class ChangeAnalysisError(RuntimeError):
21
+ """The pipeline could not produce an artifact (no executions, unsupported runtime, ...)."""
22
+
23
+
24
+ def analyze_change(
25
+ repo: str | Path,
26
+ *,
27
+ runtime: str,
28
+ diff: str | None = None,
29
+ symbols: list[str] | None = None,
30
+ rev: str | None = None,
31
+ out: str | Path,
32
+ runtime_args: list[str] | None = None,
33
+ probes: int = 0,
34
+ attempts: int = 1,
35
+ writer: str = "none",
36
+ up: int = 3,
37
+ down: int = 4,
38
+ ) -> dict[str, Any]:
39
+ """Run DiffGenome on one change and return the artifact. `runtime_args` are the
40
+ runtime adapter's own options (`--test-root`, `--tests`, `--mock-dir`, `--python`,
41
+ `--pytest-arg`, `--source-root`); the integrator forwards them opaquely. `probes` is the
42
+ budget: 0 means existing tests only, no LLM call."""
43
+ from diffgenome.mvp import main as mvp_main
44
+
45
+ argv = [
46
+ "--runtime", runtime, "--repo", str(repo), "--out", str(out), "--writer", writer,
47
+ "--probes", str(probes), "--attempts", str(attempts), "--up", str(up), "--down", str(down),
48
+ ] # fmt: skip
49
+ if diff:
50
+ argv += ["--diff", diff]
51
+ for s in symbols or []:
52
+ argv += ["--symbol", s]
53
+ if rev:
54
+ argv += ["--rev", rev]
55
+ argv += list(runtime_args or [])
56
+ code = mvp_main(argv)
57
+ path = Path(out) / ARTIFACT_FILENAME
58
+ if code != 0 or not path.is_file():
59
+ raise ChangeAnalysisError(f"diffgenome exited {code}; no {ARTIFACT_FILENAME} in {out}")
60
+ return load_change_artifact(path)
61
+
62
+
63
+ def load_change_artifact(path: str | Path) -> dict[str, Any]:
64
+ doc: dict[str, Any] = json.loads(Path(path).read_text())
65
+ if doc.get("format") != FORMAT:
66
+ raise ChangeAnalysisError(f"{path}: not a {FORMAT} artifact (format={doc.get('format')!r})")
67
+ return doc