@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/cli/index.js CHANGED
@@ -13,7 +13,7 @@ import {
13
13
  runPipeline,
14
14
  saveResult,
15
15
  validateRules
16
- } from "../chunk-QEB7A5KB.js";
16
+ } from "../chunk-EHMBINRV.js";
17
17
  import {
18
18
  affectedBy,
19
19
  buildContextPack,
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
- if (graph.hasNode(node.id)) continue;
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 import_node_crypto2 = require("crypto");
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 import_graphology3 = __toESM(require("graphology"), 1);
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 import_graphology3.default.from(data);
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 import_node_crypto3 = require("crypto");
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, 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");
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 import_graphology4 = __toESM(require("graphology"), 1);
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 import_graphology4.default({ type: "directed", multi: true, allowSelfLoops: true });
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 import_node_crypto4 = require("crypto");
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, 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);
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
  });