@dreamtree-org/graphify 1.3.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/index.cjs CHANGED
@@ -44,15 +44,18 @@ __export(index_exports, {
44
44
  collectFiles: () => collectFiles,
45
45
  deserializeGraph: () => deserializeGraph,
46
46
  diffGraphs: () => diffGraphs,
47
+ expandRetrieval: () => expandRetrieval,
47
48
  explainNode: () => explainNode,
48
49
  exportGraph: () => exportGraph,
49
50
  extract: () => extract,
50
51
  extractMysql: () => extractMysql,
51
52
  globToRegExp: () => globToRegExp,
52
53
  graphForDirectory: () => graphForDirectory,
54
+ ingestDocument: () => ingestDocument,
53
55
  isTestFile: () => isTestFile,
54
56
  loadGraph: () => loadGraph,
55
57
  loadResults: () => loadResults,
58
+ mergeExtractionsInto: () => mergeExtractionsInto,
56
59
  mergeGraphs: () => mergeGraphs,
57
60
  queryGraph: () => queryGraph,
58
61
  rankForTask: () => rankForTask,
@@ -70,6 +73,7 @@ __export(index_exports, {
70
73
  shortestPath: () => shortestPath,
71
74
  testsForChangedFiles: () => testsForChangedFiles,
72
75
  testsForNode: () => testsForNode,
76
+ updateCorpusGraph: () => updateCorpusGraph,
73
77
  validateExtraction: () => validateExtraction,
74
78
  validateRules: () => validateRules
75
79
  });
@@ -2028,13 +2032,15 @@ var RelationSchema = import_zod.z.enum([
2028
2032
  "references",
2029
2033
  "contains",
2030
2034
  "method",
2031
- "re_exports"
2035
+ "re_exports",
2036
+ "follows"
2032
2037
  ]);
2033
2038
  var GraphNodeSchema = import_zod.z.object({
2034
2039
  id: import_zod.z.string().min(1, "node id must be non-empty"),
2035
2040
  label: import_zod.z.string(),
2036
2041
  sourceFile: import_zod.z.string(),
2037
- sourceLocation: import_zod.z.string()
2042
+ sourceLocation: import_zod.z.string(),
2043
+ kind: import_zod.z.string().optional()
2038
2044
  });
2039
2045
  var GraphEdgeSchema = import_zod.z.object({
2040
2046
  source: import_zod.z.string().min(1, "edge source must be non-empty"),
@@ -2082,16 +2088,27 @@ function buildGraph(extractions) {
2082
2088
  const validated = extractions.map(
2083
2089
  (extraction, index) => validateExtraction(extraction, `extraction #${index}`)
2084
2090
  );
2091
+ mergeExtractionsInto(graph, validated);
2092
+ return graph;
2093
+ }
2094
+ function mergeExtractionsInto(graph, validated) {
2085
2095
  const allNodes = validated.flatMap((extraction) => extraction.nodes);
2086
2096
  const allEdges = validated.flatMap((extraction) => extraction.edges);
2087
2097
  const sortedNodes = [...allNodes].sort((a, b) => a.id.localeCompare(b.id));
2088
2098
  for (const node of sortedNodes) {
2089
- if (graph.hasNode(node.id)) continue;
2090
- graph.addNode(node.id, {
2099
+ const attributes = {
2091
2100
  label: node.label,
2092
2101
  sourceFile: node.sourceFile,
2093
- sourceLocation: node.sourceLocation
2094
- });
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);
2095
2112
  }
2096
2113
  const sortedEdges = [...allEdges].sort(
2097
2114
  (a, b) => a.source.localeCompare(b.source) || a.target.localeCompare(b.target) || a.relation.localeCompare(b.relation)
@@ -2110,9 +2127,402 @@ function buildGraph(extractions) {
2110
2127
  confidence: edge.confidence
2111
2128
  });
2112
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
+ });
2113
2443
  return graph;
2114
2444
  }
2115
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
+
2116
2526
  // src/resolve.ts
2117
2527
  var fs9 = __toESM(require("fs/promises"), 1);
2118
2528
  var path5 = __toESM(require("path"), 1);
@@ -2307,99 +2717,6 @@ function resolveCrossFileReferences(extractions, options = {}) {
2307
2717
  return { resolvedEndpoints, droppedPlaceholders };
2308
2718
  }
2309
2719
 
2310
- // src/cluster.ts
2311
- var import_node_crypto = require("crypto");
2312
- var import_graphology2 = __toESM(require("graphology"), 1);
2313
- var import_graphology_communities_leiden = __toESM(require("@aflsolutions/graphology-communities-leiden"), 1);
2314
- var import_graphology_communities_louvain = __toESM(require("graphology-communities-louvain"), 1);
2315
- var FIXED_SEED = 1592594996;
2316
- function mulberry32(seed) {
2317
- let a = seed >>> 0;
2318
- return function next() {
2319
- a = a + 1831565813 | 0;
2320
- let t = Math.imul(a ^ a >>> 15, 1 | a);
2321
- t = t + Math.imul(t ^ t >>> 7, 61 | t) ^ t;
2322
- return ((t ^ t >>> 14) >>> 0) / 4294967296;
2323
- };
2324
- }
2325
- function sortedWorkingCopy(graph) {
2326
- const copy = new import_graphology2.default({ type: graph.type, multi: graph.multi, allowSelfLoops: graph.allowSelfLoops });
2327
- const nodeIds = graph.nodes().sort((a, b) => a.localeCompare(b));
2328
- for (const id of nodeIds) {
2329
- copy.addNode(id, { ...graph.getNodeAttributes(id) });
2330
- }
2331
- const edgeEntries = [];
2332
- graph.forEachEdge((edge, attributes, source, target) => {
2333
- edgeEntries.push({ key: edge, source, target, attributes });
2334
- });
2335
- edgeEntries.sort(
2336
- (a, b) => a.source.localeCompare(b.source) || a.target.localeCompare(b.target) || String(a.attributes.relation).localeCompare(String(b.attributes.relation))
2337
- );
2338
- for (const entry of edgeEntries) {
2339
- copy.addEdgeWithKey(entry.key, entry.source, entry.target, { ...entry.attributes });
2340
- }
2341
- return copy;
2342
- }
2343
- function toUndirectedCopy(graph) {
2344
- const copy = new import_graphology2.default({ type: "undirected", multi: true, allowSelfLoops: graph.allowSelfLoops });
2345
- graph.forEachNode((nodeId, attributes) => copy.addNode(nodeId, { ...attributes }));
2346
- graph.forEachEdge((edgeKey, attributes, source, target) => {
2347
- copy.addEdgeWithKey(edgeKey, source, target, { ...attributes });
2348
- });
2349
- return copy;
2350
- }
2351
- function runAlgorithm(working, options) {
2352
- const rng = mulberry32(FIXED_SEED);
2353
- if (options.algorithm === "leiden") {
2354
- const undirected = toUndirectedCopy(working);
2355
- import_graphology_communities_leiden.default.assign(undirected, { rng, maxIterations: options.maxIterations ?? 0 });
2356
- undirected.forEachNode((nodeId, attributes) => {
2357
- working.setNodeAttribute(nodeId, "community", attributes.community);
2358
- });
2359
- return;
2360
- }
2361
- import_graphology_communities_louvain.default.assign(working, { rng });
2362
- }
2363
- function cluster(graph, options = {}) {
2364
- if (graph.order === 0) return graph;
2365
- const working = sortedWorkingCopy(graph);
2366
- runAlgorithm(working, options);
2367
- const hubs = /* @__PURE__ */ new Map();
2368
- working.forEachNode((nodeId) => {
2369
- const community = working.getNodeAttribute(nodeId, "community");
2370
- const degree = working.degree(nodeId);
2371
- const current = hubs.get(community);
2372
- if (!current || degree > current.degree) {
2373
- hubs.set(community, { id: nodeId, degree });
2374
- }
2375
- });
2376
- const membersByCommunity = /* @__PURE__ */ new Map();
2377
- working.forEachNode((nodeId) => {
2378
- const community = working.getNodeAttribute(nodeId, "community");
2379
- const list = membersByCommunity.get(community);
2380
- if (list) list.push(nodeId);
2381
- else membersByCommunity.set(community, [nodeId]);
2382
- });
2383
- const labelByCommunity = /* @__PURE__ */ new Map();
2384
- const hashByCommunity = /* @__PURE__ */ new Map();
2385
- for (const [community, members] of membersByCommunity) {
2386
- members.sort((a, b) => a.localeCompare(b));
2387
- hashByCommunity.set(community, (0, import_node_crypto.createHash)("sha256").update(members.join("\n")).digest("hex"));
2388
- const hub = hubs.get(community);
2389
- const hubLabel = hub ? working.getNodeAttribute(hub.id, "label") : void 0;
2390
- labelByCommunity.set(community, hubLabel && hubLabel.length > 0 ? hubLabel : `community-${community}`);
2391
- }
2392
- working.forEachNode((nodeId) => {
2393
- const community = working.getNodeAttribute(nodeId, "community");
2394
- graph.mergeNodeAttributes(nodeId, {
2395
- community,
2396
- communityLabel: labelByCommunity.get(community),
2397
- communityHash: hashByCommunity.get(community)
2398
- });
2399
- });
2400
- return graph;
2401
- }
2402
-
2403
2720
  // src/analyze.ts
2404
2721
  var ABSOLUTE_DEGREE_FLOOR = 10;
2405
2722
  var MEAN_DEGREE_MULTIPLIER = 3;
@@ -2577,7 +2894,7 @@ var fs12 = __toESM(require("fs/promises"), 1);
2577
2894
  var path6 = __toESM(require("path"), 1);
2578
2895
 
2579
2896
  // src/security.ts
2580
- var import_node_crypto2 = require("crypto");
2897
+ var import_node_crypto3 = require("crypto");
2581
2898
  var dns = __toESM(require("dns"), 1);
2582
2899
  var http = __toESM(require("http"), 1);
2583
2900
  var https = __toESM(require("https"), 1);
@@ -2652,12 +2969,12 @@ function escapeHtml(text) {
2652
2969
  var fs11 = __toESM(require("fs/promises"), 1);
2653
2970
 
2654
2971
  // src/store/serialize.ts
2655
- var import_graphology3 = __toESM(require("graphology"), 1);
2972
+ var import_graphology4 = __toESM(require("graphology"), 1);
2656
2973
  function serializeGraph(graph) {
2657
2974
  return graph.export();
2658
2975
  }
2659
2976
  function deserializeGraph(data) {
2660
- return import_graphology3.default.from(data);
2977
+ return import_graphology4.default.from(data);
2661
2978
  }
2662
2979
 
2663
2980
  // src/store/localFile.ts
@@ -2819,7 +3136,7 @@ var fs14 = __toESM(require("fs/promises"), 1);
2819
3136
  var path8 = __toESM(require("path"), 1);
2820
3137
 
2821
3138
  // src/extractionCache.ts
2822
- var import_node_crypto3 = require("crypto");
3139
+ var import_node_crypto4 = require("crypto");
2823
3140
  var fs13 = __toESM(require("fs/promises"), 1);
2824
3141
  var path7 = __toESM(require("path"), 1);
2825
3142
  var ExtractionCache = class {
@@ -2828,7 +3145,7 @@ var ExtractionCache = class {
2828
3145
  this.dir = path7.join(outDir, "cache");
2829
3146
  }
2830
3147
  key(relFile, content) {
2831
- return (0, import_node_crypto3.createHash)("sha256").update(relFile).update("\0").update(content).digest("hex");
3148
+ return (0, import_node_crypto4.createHash)("sha256").update(relFile).update("\0").update(content).digest("hex");
2832
3149
  }
2833
3150
  async get(key) {
2834
3151
  try {
@@ -3200,6 +3517,141 @@ function explainNode(graph, query) {
3200
3517
  };
3201
3518
  }
3202
3519
 
3520
+ // src/retrieval.ts
3521
+ var DEFAULT_TOKEN_BUDGET2 = 2e3;
3522
+ var DEFAULT_MAX_HOPS = 2;
3523
+ function resolveSeeds(graph, hits) {
3524
+ const seeds = /* @__PURE__ */ new Map();
3525
+ for (const hit of hits) {
3526
+ let id = null;
3527
+ if (hit.nodeId !== void 0 && graph.hasNode(hit.nodeId)) {
3528
+ id = hit.nodeId;
3529
+ } else if (hit.text !== void 0) {
3530
+ id = resolveNode(graph, hit.text)?.id ?? null;
3531
+ }
3532
+ const key = id ?? hit.nodeId ?? hit.text ?? "";
3533
+ if (key === "") continue;
3534
+ const existing = seeds.get(key);
3535
+ if (existing) {
3536
+ if (hit.score !== void 0 && (existing.score === void 0 || hit.score > existing.score)) {
3537
+ existing.score = hit.score;
3538
+ }
3539
+ continue;
3540
+ }
3541
+ seeds.set(key, { id: key, score: hit.score, inGraph: id !== null, text: hit.text });
3542
+ }
3543
+ return [...seeds.values()];
3544
+ }
3545
+ function expandRetrieval(graph, hits, options = {}) {
3546
+ const tokenBudget = options.tokenBudget ?? DEFAULT_TOKEN_BUDGET2;
3547
+ const maxHops = options.maxHops ?? DEFAULT_MAX_HOPS;
3548
+ const relations = options.relations ? new Set(options.relations) : null;
3549
+ const seeds = resolveSeeds(graph, hits);
3550
+ const graphSeeds = seeds.filter((s) => s.inGraph);
3551
+ const seedScore = new Map(graphSeeds.map((s) => [s.id, s.score]));
3552
+ const hops = /* @__PURE__ */ new Map();
3553
+ const via = /* @__PURE__ */ new Map();
3554
+ const frontier = [];
3555
+ let spentTokens = 0;
3556
+ for (const seed of graphSeeds) {
3557
+ hops.set(seed.id, 0);
3558
+ frontier.push(seed.id);
3559
+ spentTokens += nodeTokenCost(graph, seed.id);
3560
+ }
3561
+ let truncated = false;
3562
+ while (frontier.length > 0) {
3563
+ const current = frontier.shift();
3564
+ const currentHops = hops.get(current);
3565
+ if (currentHops >= maxHops) continue;
3566
+ const candidates = [];
3567
+ graph.forEachEdge(current, (_key, attrs, source, target) => {
3568
+ const relation = String(attrs.relation);
3569
+ if (relations && !relations.has(relation)) return;
3570
+ const neighbor = source === current ? target : source;
3571
+ candidates.push({
3572
+ neighbor,
3573
+ edge: { source, target, relation, confidence: String(attrs.confidence) }
3574
+ });
3575
+ });
3576
+ candidates.sort((a, b) => a.neighbor.localeCompare(b.neighbor));
3577
+ for (const { neighbor, edge } of candidates) {
3578
+ if (hops.has(neighbor)) continue;
3579
+ const cost = nodeTokenCost(graph, neighbor);
3580
+ if (spentTokens + cost > tokenBudget) {
3581
+ truncated = true;
3582
+ continue;
3583
+ }
3584
+ spentTokens += cost;
3585
+ hops.set(neighbor, currentHops + 1);
3586
+ via.set(neighbor, edge);
3587
+ frontier.push(neighbor);
3588
+ }
3589
+ }
3590
+ const degree = (id) => graph.degree(id);
3591
+ const nodes = [...hops.entries()].map(([id, hopCount]) => {
3592
+ const attrs = graph.getNodeAttributes(id);
3593
+ return {
3594
+ id,
3595
+ label: attrs.label ?? id,
3596
+ ...attrs.kind !== void 0 ? { kind: String(attrs.kind) } : {},
3597
+ sourceFile: attrs.sourceFile ?? "",
3598
+ sourceLocation: attrs.sourceLocation ?? "",
3599
+ hops: hopCount,
3600
+ ...via.has(id) ? { via: via.get(id) } : {},
3601
+ ...hopCount === 0 && seedScore.get(id) !== void 0 ? { seedScore: seedScore.get(id) } : {}
3602
+ };
3603
+ });
3604
+ for (const seed of seeds) {
3605
+ if (seed.inGraph) continue;
3606
+ nodes.push({
3607
+ id: seed.id,
3608
+ label: seed.text ?? seed.id,
3609
+ sourceFile: "",
3610
+ sourceLocation: "",
3611
+ hops: 0,
3612
+ ...seed.score !== void 0 ? { seedScore: seed.score } : {}
3613
+ });
3614
+ }
3615
+ nodes.sort((a, b) => {
3616
+ if (a.hops !== b.hops) return a.hops - b.hops;
3617
+ if (a.hops === 0) {
3618
+ return (b.seedScore ?? -Infinity) - (a.seedScore ?? -Infinity) || a.id.localeCompare(b.id);
3619
+ }
3620
+ const degreeDiff = (graph.hasNode(b.id) ? degree(b.id) : 0) - (graph.hasNode(a.id) ? degree(a.id) : 0);
3621
+ return degreeDiff || a.id.localeCompare(b.id);
3622
+ });
3623
+ const included = /* @__PURE__ */ new Set([...hops.keys()]);
3624
+ const edges = [];
3625
+ graph.forEachEdge((_key, attrs, source, target) => {
3626
+ if (!included.has(source) || !included.has(target)) return;
3627
+ const relation = String(attrs.relation);
3628
+ if (relations && !relations.has(relation)) return;
3629
+ edges.push({ source, target, relation, confidence: String(attrs.confidence) });
3630
+ });
3631
+ edges.sort(
3632
+ (a, b) => a.source.localeCompare(b.source) || a.target.localeCompare(b.target) || a.relation.localeCompare(b.relation)
3633
+ );
3634
+ const communities = /* @__PURE__ */ new Map();
3635
+ for (const id of included) {
3636
+ const attrs = graph.getNodeAttributes(id);
3637
+ const community = attrs.community;
3638
+ if (community === void 0) continue;
3639
+ const entry = communities.get(community) ?? {
3640
+ id: community,
3641
+ label: attrs.communityLabel ?? String(community),
3642
+ seedCount: 0
3643
+ };
3644
+ if (hops.get(id) === 0) entry.seedCount += 1;
3645
+ communities.set(community, entry);
3646
+ }
3647
+ return {
3648
+ nodes,
3649
+ edges,
3650
+ communities: [...communities.values()].sort((a, b) => b.seedCount - a.seedCount || a.id - b.id),
3651
+ truncated
3652
+ };
3653
+ }
3654
+
3203
3655
  // src/impact.ts
3204
3656
  var DEPENDENCY_RELATIONS = /* @__PURE__ */ new Set([
3205
3657
  "calls",
@@ -3265,7 +3717,7 @@ function affectedBy(graph, nodeQuery, options = {}) {
3265
3717
  }
3266
3718
 
3267
3719
  // src/merge.ts
3268
- var import_graphology4 = __toESM(require("graphology"), 1);
3720
+ var import_graphology5 = __toESM(require("graphology"), 1);
3269
3721
  var CONFIDENCE_RANK3 = { EXTRACTED: 3, INFERRED: 2, AMBIGUOUS: 1 };
3270
3722
  function strongerConfidence2(a, b) {
3271
3723
  return CONFIDENCE_RANK3[a] >= CONFIDENCE_RANK3[b] ? a : b;
@@ -3282,7 +3734,7 @@ function mergeGraphs(entries) {
3282
3734
  if (duplicate !== void 0) {
3283
3735
  throw new Error(`Duplicate project name in merge: "${duplicate}" \u2014 every entry needs a unique name.`);
3284
3736
  }
3285
- const merged = new import_graphology4.default({ type: "directed", multi: true, allowSelfLoops: true });
3737
+ const merged = new import_graphology5.default({ type: "directed", multi: true, allowSelfLoops: true });
3286
3738
  const sorted = [...entries].sort((a, b) => a.name.localeCompare(b.name));
3287
3739
  for (const { name, graph } of sorted) {
3288
3740
  const nodeIds = [...graph.nodes()].sort((a, b) => a.localeCompare(b));
@@ -3468,7 +3920,7 @@ async function addViewEdges(query, schema, viewNames, tableNames, edges) {
3468
3920
  }
3469
3921
 
3470
3922
  // src/memory.ts
3471
- var import_node_crypto4 = require("crypto");
3923
+ var import_node_crypto5 = require("crypto");
3472
3924
  var fs15 = __toESM(require("fs/promises"), 1);
3473
3925
  var path10 = __toESM(require("path"), 1);
3474
3926
  var OUTCOMES = /* @__PURE__ */ new Set(["useful", "dead_end", "corrected"]);
@@ -3490,7 +3942,7 @@ async function saveResult(entry, memoryDir, now = /* @__PURE__ */ new Date()) {
3490
3942
  validateResult(entry);
3491
3943
  await fs15.mkdir(memoryDir, { recursive: true });
3492
3944
  const saved = { ...entry, savedAt: now.toISOString() };
3493
- const hash = (0, import_node_crypto4.createHash)("sha256").update(JSON.stringify(saved)).digest("hex").slice(0, 8);
3945
+ const hash = (0, import_node_crypto5.createHash)("sha256").update(JSON.stringify(saved)).digest("hex").slice(0, 8);
3494
3946
  const stamp = saved.savedAt.replace(/[:.]/g, "-");
3495
3947
  const filePath = path10.join(memoryDir, `result-${stamp}-${hash}.json`);
3496
3948
  await fs15.writeFile(filePath, JSON.stringify(saved, null, 2), "utf-8");
@@ -4005,15 +4457,18 @@ function checkRules(graph, config) {
4005
4457
  collectFiles,
4006
4458
  deserializeGraph,
4007
4459
  diffGraphs,
4460
+ expandRetrieval,
4008
4461
  explainNode,
4009
4462
  exportGraph,
4010
4463
  extract,
4011
4464
  extractMysql,
4012
4465
  globToRegExp,
4013
4466
  graphForDirectory,
4467
+ ingestDocument,
4014
4468
  isTestFile,
4015
4469
  loadGraph,
4016
4470
  loadResults,
4471
+ mergeExtractionsInto,
4017
4472
  mergeGraphs,
4018
4473
  queryGraph,
4019
4474
  rankForTask,
@@ -4031,6 +4486,7 @@ function checkRules(graph, config) {
4031
4486
  shortestPath,
4032
4487
  testsForChangedFiles,
4033
4488
  testsForNode,
4489
+ updateCorpusGraph,
4034
4490
  validateExtraction,
4035
4491
  validateRules
4036
4492
  });