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
|
@@ -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 };
|
diffgenome/ambiguity.py
ADDED
|
@@ -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
|