@dreamtree-org/graphify 1.4.0 → 1.5.0
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.
- package/dist/{chunk-QEB7A5KB.js → chunk-EHMBINRV.js} +114 -101
- package/dist/chunk-EHMBINRV.js.map +1 -0
- package/dist/cli/index.cjs +19 -7
- package/dist/cli/index.cjs.map +1 -1
- package/dist/cli/index.js +1 -1
- package/dist/index.cjs +427 -108
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +162 -17
- package/dist/index.d.ts +162 -17
- package/dist/index.js +304 -1
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/dist/chunk-QEB7A5KB.js.map +0 -1
package/dist/cli/index.js
CHANGED
package/dist/index.cjs
CHANGED
|
@@ -51,9 +51,11 @@ __export(index_exports, {
|
|
|
51
51
|
extractMysql: () => extractMysql,
|
|
52
52
|
globToRegExp: () => globToRegExp,
|
|
53
53
|
graphForDirectory: () => graphForDirectory,
|
|
54
|
+
ingestDocument: () => ingestDocument,
|
|
54
55
|
isTestFile: () => isTestFile,
|
|
55
56
|
loadGraph: () => loadGraph,
|
|
56
57
|
loadResults: () => loadResults,
|
|
58
|
+
mergeExtractionsInto: () => mergeExtractionsInto,
|
|
57
59
|
mergeGraphs: () => mergeGraphs,
|
|
58
60
|
queryGraph: () => queryGraph,
|
|
59
61
|
rankForTask: () => rankForTask,
|
|
@@ -71,6 +73,7 @@ __export(index_exports, {
|
|
|
71
73
|
shortestPath: () => shortestPath,
|
|
72
74
|
testsForChangedFiles: () => testsForChangedFiles,
|
|
73
75
|
testsForNode: () => testsForNode,
|
|
76
|
+
updateCorpusGraph: () => updateCorpusGraph,
|
|
74
77
|
validateExtraction: () => validateExtraction,
|
|
75
78
|
validateRules: () => validateRules
|
|
76
79
|
});
|
|
@@ -2029,13 +2032,15 @@ var RelationSchema = import_zod.z.enum([
|
|
|
2029
2032
|
"references",
|
|
2030
2033
|
"contains",
|
|
2031
2034
|
"method",
|
|
2032
|
-
"re_exports"
|
|
2035
|
+
"re_exports",
|
|
2036
|
+
"follows"
|
|
2033
2037
|
]);
|
|
2034
2038
|
var GraphNodeSchema = import_zod.z.object({
|
|
2035
2039
|
id: import_zod.z.string().min(1, "node id must be non-empty"),
|
|
2036
2040
|
label: import_zod.z.string(),
|
|
2037
2041
|
sourceFile: import_zod.z.string(),
|
|
2038
|
-
sourceLocation: import_zod.z.string()
|
|
2042
|
+
sourceLocation: import_zod.z.string(),
|
|
2043
|
+
kind: import_zod.z.string().optional()
|
|
2039
2044
|
});
|
|
2040
2045
|
var GraphEdgeSchema = import_zod.z.object({
|
|
2041
2046
|
source: import_zod.z.string().min(1, "edge source must be non-empty"),
|
|
@@ -2083,16 +2088,27 @@ function buildGraph(extractions) {
|
|
|
2083
2088
|
const validated = extractions.map(
|
|
2084
2089
|
(extraction, index) => validateExtraction(extraction, `extraction #${index}`)
|
|
2085
2090
|
);
|
|
2091
|
+
mergeExtractionsInto(graph, validated);
|
|
2092
|
+
return graph;
|
|
2093
|
+
}
|
|
2094
|
+
function mergeExtractionsInto(graph, validated) {
|
|
2086
2095
|
const allNodes = validated.flatMap((extraction) => extraction.nodes);
|
|
2087
2096
|
const allEdges = validated.flatMap((extraction) => extraction.edges);
|
|
2088
2097
|
const sortedNodes = [...allNodes].sort((a, b) => a.id.localeCompare(b.id));
|
|
2089
2098
|
for (const node of sortedNodes) {
|
|
2090
|
-
|
|
2091
|
-
graph.addNode(node.id, {
|
|
2099
|
+
const attributes = {
|
|
2092
2100
|
label: node.label,
|
|
2093
2101
|
sourceFile: node.sourceFile,
|
|
2094
|
-
sourceLocation: node.sourceLocation
|
|
2095
|
-
|
|
2102
|
+
sourceLocation: node.sourceLocation,
|
|
2103
|
+
...node.kind !== void 0 ? { kind: node.kind } : {}
|
|
2104
|
+
};
|
|
2105
|
+
if (graph.hasNode(node.id)) {
|
|
2106
|
+
if (graph.getNodeAttribute(node.id, "sourceFile") === "<unknown>") {
|
|
2107
|
+
graph.mergeNodeAttributes(node.id, attributes);
|
|
2108
|
+
}
|
|
2109
|
+
continue;
|
|
2110
|
+
}
|
|
2111
|
+
graph.addNode(node.id, attributes);
|
|
2096
2112
|
}
|
|
2097
2113
|
const sortedEdges = [...allEdges].sort(
|
|
2098
2114
|
(a, b) => a.source.localeCompare(b.source) || a.target.localeCompare(b.target) || a.relation.localeCompare(b.relation)
|
|
@@ -2111,9 +2127,402 @@ function buildGraph(extractions) {
|
|
|
2111
2127
|
confidence: edge.confidence
|
|
2112
2128
|
});
|
|
2113
2129
|
}
|
|
2130
|
+
}
|
|
2131
|
+
|
|
2132
|
+
// src/ingest/document.ts
|
|
2133
|
+
var import_node_crypto = require("crypto");
|
|
2134
|
+
var posix2 = __toESM(require("path/posix"), 1);
|
|
2135
|
+
var DEFAULT_TARGET_TOKENS = 400;
|
|
2136
|
+
var DEFAULT_MAX_TOKENS = 512;
|
|
2137
|
+
var CHUNK_LABEL_LEN = 60;
|
|
2138
|
+
function estimateTokens(text) {
|
|
2139
|
+
return Math.ceil(text.length / 4);
|
|
2140
|
+
}
|
|
2141
|
+
function contentHash(content) {
|
|
2142
|
+
const normalized = content.replace(/\r\n/g, "\n").trim();
|
|
2143
|
+
return (0, import_node_crypto.createHash)("sha256").update(normalized, "utf8").digest("hex");
|
|
2144
|
+
}
|
|
2145
|
+
function chunkLabel(content) {
|
|
2146
|
+
const collapsed = content.replace(/\s+/g, " ").trim();
|
|
2147
|
+
return collapsed.length > CHUNK_LABEL_LEN ? `${collapsed.slice(0, CHUNK_LABEL_LEN - 1)}\u2026` : collapsed;
|
|
2148
|
+
}
|
|
2149
|
+
function parseBlocks(text, honorHeadings) {
|
|
2150
|
+
const lines = text.replace(/\r\n/g, "\n").split("\n");
|
|
2151
|
+
const blocks = [];
|
|
2152
|
+
let paragraph = [];
|
|
2153
|
+
let paragraphLine = 0;
|
|
2154
|
+
let inFence = false;
|
|
2155
|
+
const flush = () => {
|
|
2156
|
+
if (paragraph.length === 0) return;
|
|
2157
|
+
blocks.push({ type: "paragraph", text: paragraph.join("\n"), depth: 0, line: paragraphLine });
|
|
2158
|
+
paragraph = [];
|
|
2159
|
+
};
|
|
2160
|
+
for (let i = 0; i < lines.length; i++) {
|
|
2161
|
+
const line = lines[i];
|
|
2162
|
+
if (/^(```|~~~)/.test(line.trim())) {
|
|
2163
|
+
if (paragraph.length === 0) paragraphLine = i + 1;
|
|
2164
|
+
paragraph.push(line);
|
|
2165
|
+
inFence = !inFence;
|
|
2166
|
+
continue;
|
|
2167
|
+
}
|
|
2168
|
+
if (inFence) {
|
|
2169
|
+
paragraph.push(line);
|
|
2170
|
+
continue;
|
|
2171
|
+
}
|
|
2172
|
+
const heading = honorHeadings ? /^(#{1,6})\s+(.+?)\s*#*\s*$/.exec(line) : null;
|
|
2173
|
+
if (heading) {
|
|
2174
|
+
flush();
|
|
2175
|
+
blocks.push({
|
|
2176
|
+
type: "heading",
|
|
2177
|
+
text: heading[2].trim(),
|
|
2178
|
+
depth: heading[1].length,
|
|
2179
|
+
line: i + 1
|
|
2180
|
+
});
|
|
2181
|
+
continue;
|
|
2182
|
+
}
|
|
2183
|
+
if (line.trim() === "") {
|
|
2184
|
+
flush();
|
|
2185
|
+
continue;
|
|
2186
|
+
}
|
|
2187
|
+
if (paragraph.length === 0) paragraphLine = i + 1;
|
|
2188
|
+
paragraph.push(line);
|
|
2189
|
+
}
|
|
2190
|
+
flush();
|
|
2191
|
+
return blocks;
|
|
2192
|
+
}
|
|
2193
|
+
function splitLongParagraph(text, maxTokens) {
|
|
2194
|
+
const maxChars = maxTokens * 4;
|
|
2195
|
+
const pieces = [];
|
|
2196
|
+
let rest = text;
|
|
2197
|
+
while (rest.length > maxChars) {
|
|
2198
|
+
let cut = rest.lastIndexOf(" ", maxChars);
|
|
2199
|
+
const newlineCut = rest.lastIndexOf("\n", maxChars);
|
|
2200
|
+
cut = Math.max(cut, newlineCut);
|
|
2201
|
+
if (cut <= 0) cut = maxChars;
|
|
2202
|
+
pieces.push(rest.slice(0, cut));
|
|
2203
|
+
rest = rest.slice(cut).trimStart();
|
|
2204
|
+
}
|
|
2205
|
+
if (rest.length > 0) pieces.push(rest);
|
|
2206
|
+
return pieces;
|
|
2207
|
+
}
|
|
2208
|
+
function extractLinkTargets(content, sourceRef) {
|
|
2209
|
+
const targets = /* @__PURE__ */ new Set();
|
|
2210
|
+
const linkPattern = /\[[^\]]*\]\(([^)\s]+)(?:\s+"[^"]*")?\)/g;
|
|
2211
|
+
const dir = posix2.dirname(sourceRef);
|
|
2212
|
+
for (const match of content.matchAll(linkPattern)) {
|
|
2213
|
+
const raw = match[1].split("#")[0];
|
|
2214
|
+
if (raw === "" || /^[a-z][a-z0-9+.-]*:/i.test(raw)) continue;
|
|
2215
|
+
targets.add(posix2.normalize(posix2.join(dir === "." ? "" : dir, raw)));
|
|
2216
|
+
}
|
|
2217
|
+
return [...targets].sort((a, b) => a.localeCompare(b));
|
|
2218
|
+
}
|
|
2219
|
+
async function ingestDocument(input, options = {}) {
|
|
2220
|
+
if (input.sourceRef.trim() === "") throw new Error("ingestDocument: sourceRef must be non-empty");
|
|
2221
|
+
const targetTokens = options.chunking?.targetTokens ?? DEFAULT_TARGET_TOKENS;
|
|
2222
|
+
const maxTokens = Math.max(options.chunking?.maxTokens ?? DEFAULT_MAX_TOKENS, targetTokens);
|
|
2223
|
+
const overlapTokens = options.chunking?.overlapTokens ?? 0;
|
|
2224
|
+
const metadata = input.metadata ?? {};
|
|
2225
|
+
const sourceRef = input.sourceRef;
|
|
2226
|
+
const honorHeadings = !/^text\/plain\b/i.test(input.mimeType ?? "");
|
|
2227
|
+
const blocks = parseBlocks(input.text, honorHeadings);
|
|
2228
|
+
const nodes = [];
|
|
2229
|
+
const edges = [];
|
|
2230
|
+
const documentId = sourceRef;
|
|
2231
|
+
nodes.push({
|
|
2232
|
+
id: documentId,
|
|
2233
|
+
label: input.title ?? posix2.basename(sourceRef),
|
|
2234
|
+
sourceFile: sourceRef,
|
|
2235
|
+
sourceLocation: "L1",
|
|
2236
|
+
kind: "document"
|
|
2237
|
+
});
|
|
2238
|
+
const sectionStack = [];
|
|
2239
|
+
const sectionIdCounts = /* @__PURE__ */ new Map();
|
|
2240
|
+
let sectionCount = 0;
|
|
2241
|
+
const chunks = [];
|
|
2242
|
+
let buffer = [];
|
|
2243
|
+
let bufferLine = 0;
|
|
2244
|
+
let bufferSection = null;
|
|
2245
|
+
let previousChunkId = null;
|
|
2246
|
+
const flushChunk = () => {
|
|
2247
|
+
const content = buffer.join("\n\n");
|
|
2248
|
+
buffer = [];
|
|
2249
|
+
if (content.trim() === "") return;
|
|
2250
|
+
const index = chunks.length;
|
|
2251
|
+
const nodeId = `${sourceRef}::chunk:${String(index).padStart(4, "0")}`;
|
|
2252
|
+
chunks.push({
|
|
2253
|
+
nodeId,
|
|
2254
|
+
content,
|
|
2255
|
+
tokenEstimate: estimateTokens(content),
|
|
2256
|
+
index,
|
|
2257
|
+
sectionPath: bufferSection?.path ?? [],
|
|
2258
|
+
contentHash: contentHash(content),
|
|
2259
|
+
metadata: { ...metadata }
|
|
2260
|
+
});
|
|
2261
|
+
nodes.push({
|
|
2262
|
+
id: nodeId,
|
|
2263
|
+
label: chunkLabel(content),
|
|
2264
|
+
sourceFile: sourceRef,
|
|
2265
|
+
sourceLocation: `L${bufferLine}`,
|
|
2266
|
+
kind: "chunk"
|
|
2267
|
+
});
|
|
2268
|
+
edges.push({
|
|
2269
|
+
source: bufferSection?.id ?? documentId,
|
|
2270
|
+
target: nodeId,
|
|
2271
|
+
relation: "contains",
|
|
2272
|
+
confidence: "EXTRACTED"
|
|
2273
|
+
});
|
|
2274
|
+
if (previousChunkId !== null) {
|
|
2275
|
+
edges.push({ source: previousChunkId, target: nodeId, relation: "follows", confidence: "EXTRACTED" });
|
|
2276
|
+
}
|
|
2277
|
+
previousChunkId = nodeId;
|
|
2278
|
+
for (const target of extractLinkTargets(content, sourceRef)) {
|
|
2279
|
+
if (target === sourceRef) continue;
|
|
2280
|
+
edges.push({ source: nodeId, target, relation: "references", confidence: "INFERRED" });
|
|
2281
|
+
}
|
|
2282
|
+
if (overlapTokens > 0) {
|
|
2283
|
+
const tailChars = overlapTokens * 4;
|
|
2284
|
+
const tail = content.length > tailChars ? content.slice(content.indexOf(" ", content.length - tailChars) + 1) : "";
|
|
2285
|
+
if (tail.trim() !== "") {
|
|
2286
|
+
buffer.push(tail);
|
|
2287
|
+
}
|
|
2288
|
+
}
|
|
2289
|
+
};
|
|
2290
|
+
for (const block of blocks) {
|
|
2291
|
+
if (block.type === "heading") {
|
|
2292
|
+
flushChunk();
|
|
2293
|
+
buffer = [];
|
|
2294
|
+
while (sectionStack.length > 0 && sectionStack[sectionStack.length - 1].depth >= block.depth) {
|
|
2295
|
+
sectionStack.pop();
|
|
2296
|
+
}
|
|
2297
|
+
const parent = sectionStack[sectionStack.length - 1];
|
|
2298
|
+
const path12 = [...parent?.path ?? [], block.text];
|
|
2299
|
+
const baseId = `${sourceRef}::sec:${path12.join("/")}`;
|
|
2300
|
+
const seen = sectionIdCounts.get(baseId) ?? 0;
|
|
2301
|
+
sectionIdCounts.set(baseId, seen + 1);
|
|
2302
|
+
const id = seen === 0 ? baseId : `${baseId}#${seen + 1}`;
|
|
2303
|
+
nodes.push({
|
|
2304
|
+
id,
|
|
2305
|
+
label: block.text,
|
|
2306
|
+
sourceFile: sourceRef,
|
|
2307
|
+
sourceLocation: `L${block.line}`,
|
|
2308
|
+
kind: "section"
|
|
2309
|
+
});
|
|
2310
|
+
edges.push({ source: parent?.id ?? documentId, target: id, relation: "contains", confidence: "EXTRACTED" });
|
|
2311
|
+
sectionStack.push({ id, depth: block.depth, path: path12 });
|
|
2312
|
+
sectionCount += 1;
|
|
2313
|
+
bufferSection = sectionStack[sectionStack.length - 1];
|
|
2314
|
+
bufferLine = block.line + 1;
|
|
2315
|
+
continue;
|
|
2316
|
+
}
|
|
2317
|
+
const pieces = estimateTokens(block.text) > maxTokens ? splitLongParagraph(block.text, maxTokens) : [block.text];
|
|
2318
|
+
for (const piece of pieces) {
|
|
2319
|
+
const bufferTokens = estimateTokens(buffer.join("\n\n"));
|
|
2320
|
+
if (buffer.length > 0 && bufferTokens + estimateTokens(piece) > targetTokens) {
|
|
2321
|
+
flushChunk();
|
|
2322
|
+
}
|
|
2323
|
+
if (buffer.length === 0) bufferLine = block.line;
|
|
2324
|
+
buffer.push(piece);
|
|
2325
|
+
}
|
|
2326
|
+
}
|
|
2327
|
+
flushChunk();
|
|
2328
|
+
let entities = 0;
|
|
2329
|
+
if (options.semanticExtractor) {
|
|
2330
|
+
const semantic = validateExtraction(
|
|
2331
|
+
await options.semanticExtractor.extractSemantic(sourceRef, input.text),
|
|
2332
|
+
`semantic pass for ${sourceRef}`
|
|
2333
|
+
);
|
|
2334
|
+
entities = semantic.nodes.length;
|
|
2335
|
+
nodes.push(...semantic.nodes);
|
|
2336
|
+
edges.push(...semantic.edges);
|
|
2337
|
+
}
|
|
2338
|
+
const extraction = validateExtraction({ nodes, edges }, `ingestDocument(${sourceRef})`);
|
|
2339
|
+
return {
|
|
2340
|
+
chunks,
|
|
2341
|
+
extraction,
|
|
2342
|
+
stats: {
|
|
2343
|
+
sections: sectionCount,
|
|
2344
|
+
entities,
|
|
2345
|
+
tokenTotal: chunks.reduce((sum, chunk) => sum + chunk.tokenEstimate, 0)
|
|
2346
|
+
}
|
|
2347
|
+
};
|
|
2348
|
+
}
|
|
2349
|
+
|
|
2350
|
+
// src/ingest/corpus.ts
|
|
2351
|
+
var import_graphology3 = __toESM(require("graphology"), 1);
|
|
2352
|
+
|
|
2353
|
+
// src/cluster.ts
|
|
2354
|
+
var import_node_crypto2 = require("crypto");
|
|
2355
|
+
var import_graphology2 = __toESM(require("graphology"), 1);
|
|
2356
|
+
var import_graphology_communities_leiden = __toESM(require("@aflsolutions/graphology-communities-leiden"), 1);
|
|
2357
|
+
var import_graphology_communities_louvain = __toESM(require("graphology-communities-louvain"), 1);
|
|
2358
|
+
var FIXED_SEED = 1592594996;
|
|
2359
|
+
function mulberry32(seed) {
|
|
2360
|
+
let a = seed >>> 0;
|
|
2361
|
+
return function next() {
|
|
2362
|
+
a = a + 1831565813 | 0;
|
|
2363
|
+
let t = Math.imul(a ^ a >>> 15, 1 | a);
|
|
2364
|
+
t = t + Math.imul(t ^ t >>> 7, 61 | t) ^ t;
|
|
2365
|
+
return ((t ^ t >>> 14) >>> 0) / 4294967296;
|
|
2366
|
+
};
|
|
2367
|
+
}
|
|
2368
|
+
function sortedWorkingCopy(graph) {
|
|
2369
|
+
const copy = new import_graphology2.default({ type: graph.type, multi: graph.multi, allowSelfLoops: graph.allowSelfLoops });
|
|
2370
|
+
const nodeIds = graph.nodes().sort((a, b) => a.localeCompare(b));
|
|
2371
|
+
for (const id of nodeIds) {
|
|
2372
|
+
copy.addNode(id, { ...graph.getNodeAttributes(id) });
|
|
2373
|
+
}
|
|
2374
|
+
const edgeEntries = [];
|
|
2375
|
+
graph.forEachEdge((edge, attributes, source, target) => {
|
|
2376
|
+
edgeEntries.push({ key: edge, source, target, attributes });
|
|
2377
|
+
});
|
|
2378
|
+
edgeEntries.sort(
|
|
2379
|
+
(a, b) => a.source.localeCompare(b.source) || a.target.localeCompare(b.target) || String(a.attributes.relation).localeCompare(String(b.attributes.relation))
|
|
2380
|
+
);
|
|
2381
|
+
for (const entry of edgeEntries) {
|
|
2382
|
+
copy.addEdgeWithKey(entry.key, entry.source, entry.target, { ...entry.attributes });
|
|
2383
|
+
}
|
|
2384
|
+
return copy;
|
|
2385
|
+
}
|
|
2386
|
+
function toUndirectedCopy(graph) {
|
|
2387
|
+
const copy = new import_graphology2.default({ type: "undirected", multi: true, allowSelfLoops: graph.allowSelfLoops });
|
|
2388
|
+
graph.forEachNode((nodeId, attributes) => copy.addNode(nodeId, { ...attributes }));
|
|
2389
|
+
graph.forEachEdge((edgeKey, attributes, source, target) => {
|
|
2390
|
+
copy.addEdgeWithKey(edgeKey, source, target, { ...attributes });
|
|
2391
|
+
});
|
|
2392
|
+
return copy;
|
|
2393
|
+
}
|
|
2394
|
+
function runAlgorithm(working, options) {
|
|
2395
|
+
const rng = mulberry32(FIXED_SEED);
|
|
2396
|
+
if (options.algorithm === "leiden") {
|
|
2397
|
+
const undirected = toUndirectedCopy(working);
|
|
2398
|
+
import_graphology_communities_leiden.default.assign(undirected, { rng, maxIterations: options.maxIterations ?? 0 });
|
|
2399
|
+
undirected.forEachNode((nodeId, attributes) => {
|
|
2400
|
+
working.setNodeAttribute(nodeId, "community", attributes.community);
|
|
2401
|
+
});
|
|
2402
|
+
return;
|
|
2403
|
+
}
|
|
2404
|
+
import_graphology_communities_louvain.default.assign(working, { rng });
|
|
2405
|
+
}
|
|
2406
|
+
function cluster(graph, options = {}) {
|
|
2407
|
+
if (graph.order === 0) return graph;
|
|
2408
|
+
const working = sortedWorkingCopy(graph);
|
|
2409
|
+
runAlgorithm(working, options);
|
|
2410
|
+
const hubs = /* @__PURE__ */ new Map();
|
|
2411
|
+
working.forEachNode((nodeId) => {
|
|
2412
|
+
const community = working.getNodeAttribute(nodeId, "community");
|
|
2413
|
+
const degree = working.degree(nodeId);
|
|
2414
|
+
const current = hubs.get(community);
|
|
2415
|
+
if (!current || degree > current.degree) {
|
|
2416
|
+
hubs.set(community, { id: nodeId, degree });
|
|
2417
|
+
}
|
|
2418
|
+
});
|
|
2419
|
+
const membersByCommunity = /* @__PURE__ */ new Map();
|
|
2420
|
+
working.forEachNode((nodeId) => {
|
|
2421
|
+
const community = working.getNodeAttribute(nodeId, "community");
|
|
2422
|
+
const list = membersByCommunity.get(community);
|
|
2423
|
+
if (list) list.push(nodeId);
|
|
2424
|
+
else membersByCommunity.set(community, [nodeId]);
|
|
2425
|
+
});
|
|
2426
|
+
const labelByCommunity = /* @__PURE__ */ new Map();
|
|
2427
|
+
const hashByCommunity = /* @__PURE__ */ new Map();
|
|
2428
|
+
for (const [community, members] of membersByCommunity) {
|
|
2429
|
+
members.sort((a, b) => a.localeCompare(b));
|
|
2430
|
+
hashByCommunity.set(community, (0, import_node_crypto2.createHash)("sha256").update(members.join("\n")).digest("hex"));
|
|
2431
|
+
const hub = hubs.get(community);
|
|
2432
|
+
const hubLabel = hub ? working.getNodeAttribute(hub.id, "label") : void 0;
|
|
2433
|
+
labelByCommunity.set(community, hubLabel && hubLabel.length > 0 ? hubLabel : `community-${community}`);
|
|
2434
|
+
}
|
|
2435
|
+
working.forEachNode((nodeId) => {
|
|
2436
|
+
const community = working.getNodeAttribute(nodeId, "community");
|
|
2437
|
+
graph.mergeNodeAttributes(nodeId, {
|
|
2438
|
+
community,
|
|
2439
|
+
communityLabel: labelByCommunity.get(community),
|
|
2440
|
+
communityHash: hashByCommunity.get(community)
|
|
2441
|
+
});
|
|
2442
|
+
});
|
|
2114
2443
|
return graph;
|
|
2115
2444
|
}
|
|
2116
2445
|
|
|
2446
|
+
// src/ingest/corpus.ts
|
|
2447
|
+
var CLUSTER_STALE_ATTR = "graphifyClusterStale";
|
|
2448
|
+
var DEFAULT_RECLUSTER_RATIO = 0.1;
|
|
2449
|
+
function assignLocalCommunities(graph) {
|
|
2450
|
+
const communityMeta = /* @__PURE__ */ new Map();
|
|
2451
|
+
let maxCommunity = -1;
|
|
2452
|
+
graph.forEachNode((_id, attrs) => {
|
|
2453
|
+
const community = attrs.community;
|
|
2454
|
+
if (typeof community !== "number") return;
|
|
2455
|
+
if (community > maxCommunity) maxCommunity = community;
|
|
2456
|
+
if (!communityMeta.has(community)) {
|
|
2457
|
+
communityMeta.set(community, {
|
|
2458
|
+
label: attrs.communityLabel,
|
|
2459
|
+
hash: attrs.communityHash
|
|
2460
|
+
});
|
|
2461
|
+
}
|
|
2462
|
+
});
|
|
2463
|
+
const unassigned = graph.filterNodes((_id, attrs) => attrs.community === void 0).sort((a, b) => a.localeCompare(b));
|
|
2464
|
+
for (const id of unassigned) {
|
|
2465
|
+
const votes = /* @__PURE__ */ new Map();
|
|
2466
|
+
graph.forEachNeighbor(id, (_neighbor, attrs) => {
|
|
2467
|
+
const community = attrs.community;
|
|
2468
|
+
if (typeof community === "number") votes.set(community, (votes.get(community) ?? 0) + 1);
|
|
2469
|
+
});
|
|
2470
|
+
let adopted = null;
|
|
2471
|
+
let bestVotes = 0;
|
|
2472
|
+
for (const [community, count] of votes) {
|
|
2473
|
+
if (count > bestVotes || count === bestVotes && adopted !== null && community < adopted) {
|
|
2474
|
+
adopted = community;
|
|
2475
|
+
bestVotes = count;
|
|
2476
|
+
}
|
|
2477
|
+
}
|
|
2478
|
+
if (adopted === null) {
|
|
2479
|
+
adopted = ++maxCommunity;
|
|
2480
|
+
communityMeta.set(adopted, {
|
|
2481
|
+
label: graph.getNodeAttribute(id, "label") ?? id
|
|
2482
|
+
});
|
|
2483
|
+
}
|
|
2484
|
+
const meta = communityMeta.get(adopted);
|
|
2485
|
+
graph.mergeNodeAttributes(id, {
|
|
2486
|
+
community: adopted,
|
|
2487
|
+
communityLabel: meta?.label ?? `community-${adopted}`,
|
|
2488
|
+
...meta?.hash !== void 0 ? { communityHash: meta.hash } : {}
|
|
2489
|
+
});
|
|
2490
|
+
}
|
|
2491
|
+
}
|
|
2492
|
+
function updateCorpusGraph(graph, extraction, options = {}) {
|
|
2493
|
+
const validated = validateExtraction(extraction, "updateCorpusGraph");
|
|
2494
|
+
const target = graph ?? new import_graphology3.default({ type: "directed", multi: true, allowSelfLoops: true });
|
|
2495
|
+
const sourceFiles = new Set(validated.nodes.map((node) => node.sourceFile));
|
|
2496
|
+
const stale = target.filterNodes((_id, attrs) => sourceFiles.has(attrs.sourceFile));
|
|
2497
|
+
for (const id of stale) target.dropNode(id);
|
|
2498
|
+
const extractionIds = new Set(validated.nodes.map((node) => node.id));
|
|
2499
|
+
const addedReal = validated.nodes.filter((node) => !target.hasNode(node.id)).length;
|
|
2500
|
+
const newPlaceholders = /* @__PURE__ */ new Set();
|
|
2501
|
+
for (const edge of validated.edges) {
|
|
2502
|
+
for (const endpoint of [edge.source, edge.target]) {
|
|
2503
|
+
if (!extractionIds.has(endpoint) && !target.hasNode(endpoint)) newPlaceholders.add(endpoint);
|
|
2504
|
+
}
|
|
2505
|
+
}
|
|
2506
|
+
mergeExtractionsInto(target, [validated]);
|
|
2507
|
+
const orphaned = target.filterNodes(
|
|
2508
|
+
(id, attrs) => attrs.sourceFile === "<unknown>" && target.degree(id) === 0
|
|
2509
|
+
);
|
|
2510
|
+
for (const id of orphaned) target.dropNode(id);
|
|
2511
|
+
const churn = stale.length + addedReal + newPlaceholders.size + orphaned.length;
|
|
2512
|
+
const staleCount = (Number(target.getAttribute(CLUSTER_STALE_ATTR)) || 0) + churn;
|
|
2513
|
+
const mode = options.recluster ?? "auto";
|
|
2514
|
+
const ratio = options.reclusterRatio ?? DEFAULT_RECLUSTER_RATIO;
|
|
2515
|
+
const neverClustered = target.order > 0 && target.findNode((_id, attrs) => attrs.community !== void 0) === void 0;
|
|
2516
|
+
if (mode === true || mode === "auto" && (neverClustered || staleCount > target.order * ratio)) {
|
|
2517
|
+
cluster(target, { algorithm: options.algorithm });
|
|
2518
|
+
target.setAttribute(CLUSTER_STALE_ATTR, 0);
|
|
2519
|
+
} else {
|
|
2520
|
+
if (mode === "auto") assignLocalCommunities(target);
|
|
2521
|
+
target.setAttribute(CLUSTER_STALE_ATTR, staleCount);
|
|
2522
|
+
}
|
|
2523
|
+
return target;
|
|
2524
|
+
}
|
|
2525
|
+
|
|
2117
2526
|
// src/resolve.ts
|
|
2118
2527
|
var fs9 = __toESM(require("fs/promises"), 1);
|
|
2119
2528
|
var path5 = __toESM(require("path"), 1);
|
|
@@ -2308,99 +2717,6 @@ function resolveCrossFileReferences(extractions, options = {}) {
|
|
|
2308
2717
|
return { resolvedEndpoints, droppedPlaceholders };
|
|
2309
2718
|
}
|
|
2310
2719
|
|
|
2311
|
-
// src/cluster.ts
|
|
2312
|
-
var import_node_crypto = require("crypto");
|
|
2313
|
-
var import_graphology2 = __toESM(require("graphology"), 1);
|
|
2314
|
-
var import_graphology_communities_leiden = __toESM(require("@aflsolutions/graphology-communities-leiden"), 1);
|
|
2315
|
-
var import_graphology_communities_louvain = __toESM(require("graphology-communities-louvain"), 1);
|
|
2316
|
-
var FIXED_SEED = 1592594996;
|
|
2317
|
-
function mulberry32(seed) {
|
|
2318
|
-
let a = seed >>> 0;
|
|
2319
|
-
return function next() {
|
|
2320
|
-
a = a + 1831565813 | 0;
|
|
2321
|
-
let t = Math.imul(a ^ a >>> 15, 1 | a);
|
|
2322
|
-
t = t + Math.imul(t ^ t >>> 7, 61 | t) ^ t;
|
|
2323
|
-
return ((t ^ t >>> 14) >>> 0) / 4294967296;
|
|
2324
|
-
};
|
|
2325
|
-
}
|
|
2326
|
-
function sortedWorkingCopy(graph) {
|
|
2327
|
-
const copy = new import_graphology2.default({ type: graph.type, multi: graph.multi, allowSelfLoops: graph.allowSelfLoops });
|
|
2328
|
-
const nodeIds = graph.nodes().sort((a, b) => a.localeCompare(b));
|
|
2329
|
-
for (const id of nodeIds) {
|
|
2330
|
-
copy.addNode(id, { ...graph.getNodeAttributes(id) });
|
|
2331
|
-
}
|
|
2332
|
-
const edgeEntries = [];
|
|
2333
|
-
graph.forEachEdge((edge, attributes, source, target) => {
|
|
2334
|
-
edgeEntries.push({ key: edge, source, target, attributes });
|
|
2335
|
-
});
|
|
2336
|
-
edgeEntries.sort(
|
|
2337
|
-
(a, b) => a.source.localeCompare(b.source) || a.target.localeCompare(b.target) || String(a.attributes.relation).localeCompare(String(b.attributes.relation))
|
|
2338
|
-
);
|
|
2339
|
-
for (const entry of edgeEntries) {
|
|
2340
|
-
copy.addEdgeWithKey(entry.key, entry.source, entry.target, { ...entry.attributes });
|
|
2341
|
-
}
|
|
2342
|
-
return copy;
|
|
2343
|
-
}
|
|
2344
|
-
function toUndirectedCopy(graph) {
|
|
2345
|
-
const copy = new import_graphology2.default({ type: "undirected", multi: true, allowSelfLoops: graph.allowSelfLoops });
|
|
2346
|
-
graph.forEachNode((nodeId, attributes) => copy.addNode(nodeId, { ...attributes }));
|
|
2347
|
-
graph.forEachEdge((edgeKey, attributes, source, target) => {
|
|
2348
|
-
copy.addEdgeWithKey(edgeKey, source, target, { ...attributes });
|
|
2349
|
-
});
|
|
2350
|
-
return copy;
|
|
2351
|
-
}
|
|
2352
|
-
function runAlgorithm(working, options) {
|
|
2353
|
-
const rng = mulberry32(FIXED_SEED);
|
|
2354
|
-
if (options.algorithm === "leiden") {
|
|
2355
|
-
const undirected = toUndirectedCopy(working);
|
|
2356
|
-
import_graphology_communities_leiden.default.assign(undirected, { rng, maxIterations: options.maxIterations ?? 0 });
|
|
2357
|
-
undirected.forEachNode((nodeId, attributes) => {
|
|
2358
|
-
working.setNodeAttribute(nodeId, "community", attributes.community);
|
|
2359
|
-
});
|
|
2360
|
-
return;
|
|
2361
|
-
}
|
|
2362
|
-
import_graphology_communities_louvain.default.assign(working, { rng });
|
|
2363
|
-
}
|
|
2364
|
-
function cluster(graph, options = {}) {
|
|
2365
|
-
if (graph.order === 0) return graph;
|
|
2366
|
-
const working = sortedWorkingCopy(graph);
|
|
2367
|
-
runAlgorithm(working, options);
|
|
2368
|
-
const hubs = /* @__PURE__ */ new Map();
|
|
2369
|
-
working.forEachNode((nodeId) => {
|
|
2370
|
-
const community = working.getNodeAttribute(nodeId, "community");
|
|
2371
|
-
const degree = working.degree(nodeId);
|
|
2372
|
-
const current = hubs.get(community);
|
|
2373
|
-
if (!current || degree > current.degree) {
|
|
2374
|
-
hubs.set(community, { id: nodeId, degree });
|
|
2375
|
-
}
|
|
2376
|
-
});
|
|
2377
|
-
const membersByCommunity = /* @__PURE__ */ new Map();
|
|
2378
|
-
working.forEachNode((nodeId) => {
|
|
2379
|
-
const community = working.getNodeAttribute(nodeId, "community");
|
|
2380
|
-
const list = membersByCommunity.get(community);
|
|
2381
|
-
if (list) list.push(nodeId);
|
|
2382
|
-
else membersByCommunity.set(community, [nodeId]);
|
|
2383
|
-
});
|
|
2384
|
-
const labelByCommunity = /* @__PURE__ */ new Map();
|
|
2385
|
-
const hashByCommunity = /* @__PURE__ */ new Map();
|
|
2386
|
-
for (const [community, members] of membersByCommunity) {
|
|
2387
|
-
members.sort((a, b) => a.localeCompare(b));
|
|
2388
|
-
hashByCommunity.set(community, (0, import_node_crypto.createHash)("sha256").update(members.join("\n")).digest("hex"));
|
|
2389
|
-
const hub = hubs.get(community);
|
|
2390
|
-
const hubLabel = hub ? working.getNodeAttribute(hub.id, "label") : void 0;
|
|
2391
|
-
labelByCommunity.set(community, hubLabel && hubLabel.length > 0 ? hubLabel : `community-${community}`);
|
|
2392
|
-
}
|
|
2393
|
-
working.forEachNode((nodeId) => {
|
|
2394
|
-
const community = working.getNodeAttribute(nodeId, "community");
|
|
2395
|
-
graph.mergeNodeAttributes(nodeId, {
|
|
2396
|
-
community,
|
|
2397
|
-
communityLabel: labelByCommunity.get(community),
|
|
2398
|
-
communityHash: hashByCommunity.get(community)
|
|
2399
|
-
});
|
|
2400
|
-
});
|
|
2401
|
-
return graph;
|
|
2402
|
-
}
|
|
2403
|
-
|
|
2404
2720
|
// src/analyze.ts
|
|
2405
2721
|
var ABSOLUTE_DEGREE_FLOOR = 10;
|
|
2406
2722
|
var MEAN_DEGREE_MULTIPLIER = 3;
|
|
@@ -2578,7 +2894,7 @@ var fs12 = __toESM(require("fs/promises"), 1);
|
|
|
2578
2894
|
var path6 = __toESM(require("path"), 1);
|
|
2579
2895
|
|
|
2580
2896
|
// src/security.ts
|
|
2581
|
-
var
|
|
2897
|
+
var import_node_crypto3 = require("crypto");
|
|
2582
2898
|
var dns = __toESM(require("dns"), 1);
|
|
2583
2899
|
var http = __toESM(require("http"), 1);
|
|
2584
2900
|
var https = __toESM(require("https"), 1);
|
|
@@ -2653,12 +2969,12 @@ function escapeHtml(text) {
|
|
|
2653
2969
|
var fs11 = __toESM(require("fs/promises"), 1);
|
|
2654
2970
|
|
|
2655
2971
|
// src/store/serialize.ts
|
|
2656
|
-
var
|
|
2972
|
+
var import_graphology4 = __toESM(require("graphology"), 1);
|
|
2657
2973
|
function serializeGraph(graph) {
|
|
2658
2974
|
return graph.export();
|
|
2659
2975
|
}
|
|
2660
2976
|
function deserializeGraph(data) {
|
|
2661
|
-
return
|
|
2977
|
+
return import_graphology4.default.from(data);
|
|
2662
2978
|
}
|
|
2663
2979
|
|
|
2664
2980
|
// src/store/localFile.ts
|
|
@@ -2820,7 +3136,7 @@ var fs14 = __toESM(require("fs/promises"), 1);
|
|
|
2820
3136
|
var path8 = __toESM(require("path"), 1);
|
|
2821
3137
|
|
|
2822
3138
|
// src/extractionCache.ts
|
|
2823
|
-
var
|
|
3139
|
+
var import_node_crypto4 = require("crypto");
|
|
2824
3140
|
var fs13 = __toESM(require("fs/promises"), 1);
|
|
2825
3141
|
var path7 = __toESM(require("path"), 1);
|
|
2826
3142
|
var ExtractionCache = class {
|
|
@@ -2829,7 +3145,7 @@ var ExtractionCache = class {
|
|
|
2829
3145
|
this.dir = path7.join(outDir, "cache");
|
|
2830
3146
|
}
|
|
2831
3147
|
key(relFile, content) {
|
|
2832
|
-
return (0,
|
|
3148
|
+
return (0, import_node_crypto4.createHash)("sha256").update(relFile).update("\0").update(content).digest("hex");
|
|
2833
3149
|
}
|
|
2834
3150
|
async get(key) {
|
|
2835
3151
|
try {
|
|
@@ -3401,7 +3717,7 @@ function affectedBy(graph, nodeQuery, options = {}) {
|
|
|
3401
3717
|
}
|
|
3402
3718
|
|
|
3403
3719
|
// src/merge.ts
|
|
3404
|
-
var
|
|
3720
|
+
var import_graphology5 = __toESM(require("graphology"), 1);
|
|
3405
3721
|
var CONFIDENCE_RANK3 = { EXTRACTED: 3, INFERRED: 2, AMBIGUOUS: 1 };
|
|
3406
3722
|
function strongerConfidence2(a, b) {
|
|
3407
3723
|
return CONFIDENCE_RANK3[a] >= CONFIDENCE_RANK3[b] ? a : b;
|
|
@@ -3418,7 +3734,7 @@ function mergeGraphs(entries) {
|
|
|
3418
3734
|
if (duplicate !== void 0) {
|
|
3419
3735
|
throw new Error(`Duplicate project name in merge: "${duplicate}" \u2014 every entry needs a unique name.`);
|
|
3420
3736
|
}
|
|
3421
|
-
const merged = new
|
|
3737
|
+
const merged = new import_graphology5.default({ type: "directed", multi: true, allowSelfLoops: true });
|
|
3422
3738
|
const sorted = [...entries].sort((a, b) => a.name.localeCompare(b.name));
|
|
3423
3739
|
for (const { name, graph } of sorted) {
|
|
3424
3740
|
const nodeIds = [...graph.nodes()].sort((a, b) => a.localeCompare(b));
|
|
@@ -3604,7 +3920,7 @@ async function addViewEdges(query, schema, viewNames, tableNames, edges) {
|
|
|
3604
3920
|
}
|
|
3605
3921
|
|
|
3606
3922
|
// src/memory.ts
|
|
3607
|
-
var
|
|
3923
|
+
var import_node_crypto5 = require("crypto");
|
|
3608
3924
|
var fs15 = __toESM(require("fs/promises"), 1);
|
|
3609
3925
|
var path10 = __toESM(require("path"), 1);
|
|
3610
3926
|
var OUTCOMES = /* @__PURE__ */ new Set(["useful", "dead_end", "corrected"]);
|
|
@@ -3626,7 +3942,7 @@ async function saveResult(entry, memoryDir, now = /* @__PURE__ */ new Date()) {
|
|
|
3626
3942
|
validateResult(entry);
|
|
3627
3943
|
await fs15.mkdir(memoryDir, { recursive: true });
|
|
3628
3944
|
const saved = { ...entry, savedAt: now.toISOString() };
|
|
3629
|
-
const hash = (0,
|
|
3945
|
+
const hash = (0, import_node_crypto5.createHash)("sha256").update(JSON.stringify(saved)).digest("hex").slice(0, 8);
|
|
3630
3946
|
const stamp = saved.savedAt.replace(/[:.]/g, "-");
|
|
3631
3947
|
const filePath = path10.join(memoryDir, `result-${stamp}-${hash}.json`);
|
|
3632
3948
|
await fs15.writeFile(filePath, JSON.stringify(saved, null, 2), "utf-8");
|
|
@@ -4148,9 +4464,11 @@ function checkRules(graph, config) {
|
|
|
4148
4464
|
extractMysql,
|
|
4149
4465
|
globToRegExp,
|
|
4150
4466
|
graphForDirectory,
|
|
4467
|
+
ingestDocument,
|
|
4151
4468
|
isTestFile,
|
|
4152
4469
|
loadGraph,
|
|
4153
4470
|
loadResults,
|
|
4471
|
+
mergeExtractionsInto,
|
|
4154
4472
|
mergeGraphs,
|
|
4155
4473
|
queryGraph,
|
|
4156
4474
|
rankForTask,
|
|
@@ -4168,6 +4486,7 @@ function checkRules(graph, config) {
|
|
|
4168
4486
|
shortestPath,
|
|
4169
4487
|
testsForChangedFiles,
|
|
4170
4488
|
testsForNode,
|
|
4489
|
+
updateCorpusGraph,
|
|
4171
4490
|
validateExtraction,
|
|
4172
4491
|
validateRules
|
|
4173
4492
|
});
|