@dreamtree-org/graphify 1.0.0 → 1.1.1
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/README.md +106 -35
- package/dist/chunk-6JLEILYF.js +207 -0
- package/dist/chunk-6JLEILYF.js.map +1 -0
- package/dist/{chunk-DG5FECXV.js → chunk-EW23BLJC.js} +557 -27
- package/dist/chunk-EW23BLJC.js.map +1 -0
- package/dist/chunk-YT7B6DOD.js +554 -0
- package/dist/chunk-YT7B6DOD.js.map +1 -0
- package/dist/chunk-ZPB37LLQ.js +155 -0
- package/dist/chunk-ZPB37LLQ.js.map +1 -0
- package/dist/cli/index.cjs +1939 -143
- package/dist/cli/index.cjs.map +1 -1
- package/dist/cli/index.js +683 -40
- package/dist/cli/index.js.map +1 -1
- package/dist/index.cjs +1131 -45
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +333 -5
- package/dist/index.d.ts +333 -5
- package/dist/index.js +50 -5
- package/dist/mcp/server.cjs +444 -56
- package/dist/mcp/server.cjs.map +1 -1
- package/dist/mcp/server.js +65 -4
- package/dist/mcp/server.js.map +1 -1
- package/dist/mysql-EJ6XOWR4.js +8 -0
- package/dist/mysql-EJ6XOWR4.js.map +1 -0
- package/package.json +2 -1
- package/src/skill/SKILL.md +45 -10
- package/dist/chunk-5ANIDX3G.js +0 -364
- package/dist/chunk-5ANIDX3G.js.map +0 -1
- package/dist/chunk-DG5FECXV.js.map +0 -1
|
@@ -1,8 +1,12 @@
|
|
|
1
|
+
import {
|
|
2
|
+
affectedBy,
|
|
3
|
+
testsForNode
|
|
4
|
+
} from "./chunk-YT7B6DOD.js";
|
|
1
5
|
import {
|
|
2
6
|
escapeHtml,
|
|
3
7
|
sanitizeLabel,
|
|
4
8
|
validateGraphPath
|
|
5
|
-
} from "./chunk-
|
|
9
|
+
} from "./chunk-6JLEILYF.js";
|
|
6
10
|
|
|
7
11
|
// src/detect.ts
|
|
8
12
|
import * as fs from "fs";
|
|
@@ -242,7 +246,7 @@ async function createParser(grammarName) {
|
|
|
242
246
|
}
|
|
243
247
|
|
|
244
248
|
// src/extractors/csharp.ts
|
|
245
|
-
async function extractCSharp(filePath) {
|
|
249
|
+
async function extractCSharp(filePath, displayPath) {
|
|
246
250
|
const parser = await createParser("c_sharp");
|
|
247
251
|
const raw = await fs2.readFile(filePath);
|
|
248
252
|
const content = raw.toString("utf-8");
|
|
@@ -251,7 +255,7 @@ async function extractCSharp(filePath) {
|
|
|
251
255
|
throw new Error(`extractCSharp: parser produced no tree for ${filePath}`);
|
|
252
256
|
}
|
|
253
257
|
const root = tree.rootNode;
|
|
254
|
-
const sourceFile = toPosix(filePath);
|
|
258
|
+
const sourceFile = toPosix(displayPath ?? filePath);
|
|
255
259
|
const builder = new ExtractionBuilder();
|
|
256
260
|
const fileId = sourceFile;
|
|
257
261
|
builder.addNode({ id: fileId, label: sourceFile, sourceFile, sourceLocation: "L1" });
|
|
@@ -455,7 +459,7 @@ function defaultPackageQualifier(importPath) {
|
|
|
455
459
|
const segments = importPath.split("/");
|
|
456
460
|
return segments[segments.length - 1] ?? importPath;
|
|
457
461
|
}
|
|
458
|
-
async function extractGo(filePath) {
|
|
462
|
+
async function extractGo(filePath, displayPath) {
|
|
459
463
|
const parser = await createParser("go");
|
|
460
464
|
const raw = await fs3.readFile(filePath);
|
|
461
465
|
const content = raw.toString("utf-8");
|
|
@@ -464,7 +468,7 @@ async function extractGo(filePath) {
|
|
|
464
468
|
throw new Error(`extractGo: parser produced no tree for ${filePath}`);
|
|
465
469
|
}
|
|
466
470
|
const root = tree.rootNode;
|
|
467
|
-
const sourceFile = toPosix(filePath);
|
|
471
|
+
const sourceFile = toPosix(displayPath ?? filePath);
|
|
468
472
|
const builder = new ExtractionBuilder();
|
|
469
473
|
const fileId = sourceFile;
|
|
470
474
|
builder.addNode({ id: fileId, label: sourceFile, sourceFile, sourceLocation: "L1" });
|
|
@@ -662,7 +666,7 @@ async function extractGo(filePath) {
|
|
|
662
666
|
|
|
663
667
|
// src/extractors/java.ts
|
|
664
668
|
import * as fs4 from "fs/promises";
|
|
665
|
-
async function extractJava(filePath) {
|
|
669
|
+
async function extractJava(filePath, displayPath) {
|
|
666
670
|
const parser = await createParser("java");
|
|
667
671
|
const raw = await fs4.readFile(filePath);
|
|
668
672
|
const content = raw.toString("utf-8");
|
|
@@ -671,7 +675,7 @@ async function extractJava(filePath) {
|
|
|
671
675
|
throw new Error(`extractJava: parser produced no tree for ${filePath}`);
|
|
672
676
|
}
|
|
673
677
|
const root = tree.rootNode;
|
|
674
|
-
const sourceFile = toPosix(filePath);
|
|
678
|
+
const sourceFile = toPosix(displayPath ?? filePath);
|
|
675
679
|
const builder = new ExtractionBuilder();
|
|
676
680
|
const fileId = sourceFile;
|
|
677
681
|
builder.addNode({ id: fileId, label: sourceFile, sourceFile, sourceLocation: "L1" });
|
|
@@ -863,7 +867,7 @@ async function extractJava(filePath) {
|
|
|
863
867
|
|
|
864
868
|
// src/extractors/python.ts
|
|
865
869
|
import * as fs5 from "fs/promises";
|
|
866
|
-
async function extractPython(filePath) {
|
|
870
|
+
async function extractPython(filePath, displayPath) {
|
|
867
871
|
const parser = await createParser("python");
|
|
868
872
|
const raw = await fs5.readFile(filePath);
|
|
869
873
|
const content = raw.toString("utf-8");
|
|
@@ -872,7 +876,7 @@ async function extractPython(filePath) {
|
|
|
872
876
|
throw new Error(`extractPython: parser produced no tree for ${filePath}`);
|
|
873
877
|
}
|
|
874
878
|
const root = tree.rootNode;
|
|
875
|
-
const sourceFile = toPosix(filePath);
|
|
879
|
+
const sourceFile = toPosix(displayPath ?? filePath);
|
|
876
880
|
const builder = new ExtractionBuilder();
|
|
877
881
|
const fileId = sourceFile;
|
|
878
882
|
builder.addNode({ id: fileId, label: sourceFile, sourceFile, sourceLocation: "L1" });
|
|
@@ -1107,7 +1111,7 @@ function stringLiteralContent(node) {
|
|
|
1107
1111
|
const content = namedChildren(node).find((c) => c.type === "string_content");
|
|
1108
1112
|
return content ? content.text : "";
|
|
1109
1113
|
}
|
|
1110
|
-
async function extractRuby(filePath) {
|
|
1114
|
+
async function extractRuby(filePath, displayPath) {
|
|
1111
1115
|
const parser = await createParser("ruby");
|
|
1112
1116
|
const raw = await fs6.readFile(filePath);
|
|
1113
1117
|
const content = raw.toString("utf-8");
|
|
@@ -1116,7 +1120,7 @@ async function extractRuby(filePath) {
|
|
|
1116
1120
|
throw new Error(`extractRuby: parser produced no tree for ${filePath}`);
|
|
1117
1121
|
}
|
|
1118
1122
|
const root = tree.rootNode;
|
|
1119
|
-
const sourceFile = toPosix(filePath);
|
|
1123
|
+
const sourceFile = toPosix(displayPath ?? filePath);
|
|
1120
1124
|
const builder = new ExtractionBuilder();
|
|
1121
1125
|
const fileId = sourceFile;
|
|
1122
1126
|
builder.addNode({ id: fileId, label: sourceFile, sourceFile, sourceLocation: "L1" });
|
|
@@ -1309,7 +1313,7 @@ function convertUsePath(segments) {
|
|
|
1309
1313
|
}
|
|
1310
1314
|
return segments.join("::");
|
|
1311
1315
|
}
|
|
1312
|
-
async function extractRust(filePath) {
|
|
1316
|
+
async function extractRust(filePath, displayPath) {
|
|
1313
1317
|
const parser = await createParser("rust");
|
|
1314
1318
|
const raw = await fs7.readFile(filePath);
|
|
1315
1319
|
const content = raw.toString("utf-8");
|
|
@@ -1318,7 +1322,7 @@ async function extractRust(filePath) {
|
|
|
1318
1322
|
throw new Error(`extractRust: parser produced no tree for ${filePath}`);
|
|
1319
1323
|
}
|
|
1320
1324
|
const root = tree.rootNode;
|
|
1321
|
-
const sourceFile = toPosix(filePath);
|
|
1325
|
+
const sourceFile = toPosix(displayPath ?? filePath);
|
|
1322
1326
|
const builder = new ExtractionBuilder();
|
|
1323
1327
|
const fileId = sourceFile;
|
|
1324
1328
|
builder.addNode({ id: fileId, label: sourceFile, sourceFile, sourceLocation: "L1" });
|
|
@@ -1531,10 +1535,10 @@ async function extractRust(filePath) {
|
|
|
1531
1535
|
}
|
|
1532
1536
|
}
|
|
1533
1537
|
} else if (fn.type === "scoped_identifier") {
|
|
1534
|
-
const
|
|
1538
|
+
const path10 = fn.childForFieldName("path")?.text;
|
|
1535
1539
|
const name = fn.childForFieldName("name")?.text;
|
|
1536
|
-
if (
|
|
1537
|
-
const viaImport = importIndex.get(
|
|
1540
|
+
if (path10 && name) {
|
|
1541
|
+
const viaImport = importIndex.get(path10);
|
|
1538
1542
|
if (viaImport) {
|
|
1539
1543
|
targetId = viaImport.id;
|
|
1540
1544
|
confidence = "INFERRED";
|
|
@@ -1575,7 +1579,7 @@ function firstStringArgument(call) {
|
|
|
1575
1579
|
if (!first || first.type !== "string") return null;
|
|
1576
1580
|
return stripQuotes2(first.text);
|
|
1577
1581
|
}
|
|
1578
|
-
async function extractTypeScript(filePath) {
|
|
1582
|
+
async function extractTypeScript(filePath, displayPath) {
|
|
1579
1583
|
const ext = path3.extname(filePath);
|
|
1580
1584
|
const grammar = grammarForExtension(ext);
|
|
1581
1585
|
const parser = await createParser(grammar);
|
|
@@ -1586,7 +1590,7 @@ async function extractTypeScript(filePath) {
|
|
|
1586
1590
|
throw new Error(`extractTypeScript: parser produced no tree for ${filePath}`);
|
|
1587
1591
|
}
|
|
1588
1592
|
const root = tree.rootNode;
|
|
1589
|
-
const sourceFile = toPosix(filePath);
|
|
1593
|
+
const sourceFile = toPosix(displayPath ?? filePath);
|
|
1590
1594
|
const builder = new ExtractionBuilder();
|
|
1591
1595
|
const fileId = sourceFile;
|
|
1592
1596
|
builder.addNode({ id: fileId, label: sourceFile, sourceFile, sourceLocation: "L1" });
|
|
@@ -1903,13 +1907,13 @@ var EXTRACTOR_REGISTRY = {
|
|
|
1903
1907
|
".cs": extractCSharp,
|
|
1904
1908
|
".rb": extractRuby
|
|
1905
1909
|
};
|
|
1906
|
-
async function extract(filePath) {
|
|
1910
|
+
async function extract(filePath, displayPath) {
|
|
1907
1911
|
const ext = path4.extname(filePath);
|
|
1908
1912
|
const extractor = EXTRACTOR_REGISTRY[ext];
|
|
1909
1913
|
if (!extractor) {
|
|
1910
1914
|
return { nodes: [], edges: [] };
|
|
1911
1915
|
}
|
|
1912
|
-
return extractor(filePath);
|
|
1916
|
+
return extractor(filePath, displayPath);
|
|
1913
1917
|
}
|
|
1914
1918
|
|
|
1915
1919
|
// src/schema.ts
|
|
@@ -2012,6 +2016,107 @@ function buildGraph(extractions) {
|
|
|
2012
2016
|
return graph;
|
|
2013
2017
|
}
|
|
2014
2018
|
|
|
2019
|
+
// src/resolve.ts
|
|
2020
|
+
var CODE_EXTENSIONS2 = [
|
|
2021
|
+
".ts",
|
|
2022
|
+
".tsx",
|
|
2023
|
+
".mts",
|
|
2024
|
+
".cts",
|
|
2025
|
+
".js",
|
|
2026
|
+
".jsx",
|
|
2027
|
+
".mjs",
|
|
2028
|
+
".cjs",
|
|
2029
|
+
".py",
|
|
2030
|
+
".go",
|
|
2031
|
+
".rs",
|
|
2032
|
+
".java",
|
|
2033
|
+
".cs",
|
|
2034
|
+
".rb"
|
|
2035
|
+
];
|
|
2036
|
+
function isOpaque(id) {
|
|
2037
|
+
return id.startsWith("module:") || id.startsWith("external:") || id.startsWith("db:");
|
|
2038
|
+
}
|
|
2039
|
+
function stripKnownExtension(p) {
|
|
2040
|
+
for (const ext of CODE_EXTENSIONS2) {
|
|
2041
|
+
if (p.endsWith(ext)) return p.slice(0, -ext.length);
|
|
2042
|
+
}
|
|
2043
|
+
return null;
|
|
2044
|
+
}
|
|
2045
|
+
function candidatePaths(guessed) {
|
|
2046
|
+
const stem = stripKnownExtension(guessed) ?? guessed;
|
|
2047
|
+
const candidates = [];
|
|
2048
|
+
for (const ext of CODE_EXTENSIONS2) candidates.push(`${stem}${ext}`);
|
|
2049
|
+
for (const ext of CODE_EXTENSIONS2) candidates.push(`${guessed}/index${ext}`);
|
|
2050
|
+
if (stem !== guessed) {
|
|
2051
|
+
for (const ext of CODE_EXTENSIONS2) candidates.push(`${stem}/index${ext}`);
|
|
2052
|
+
}
|
|
2053
|
+
return candidates.filter((c) => c !== guessed);
|
|
2054
|
+
}
|
|
2055
|
+
function uniqueMatch(guessed, candidates, realIds) {
|
|
2056
|
+
const matches = /* @__PURE__ */ new Set();
|
|
2057
|
+
for (const candidate of candidates) {
|
|
2058
|
+
if (realIds.has(candidate)) matches.add(candidate);
|
|
2059
|
+
}
|
|
2060
|
+
if (matches.size !== 1) return null;
|
|
2061
|
+
return [...matches][0];
|
|
2062
|
+
}
|
|
2063
|
+
function resolveCrossFileReferences(extractions) {
|
|
2064
|
+
const realIds = /* @__PURE__ */ new Set();
|
|
2065
|
+
const realFiles = /* @__PURE__ */ new Set();
|
|
2066
|
+
for (const extraction of extractions) {
|
|
2067
|
+
for (const node of extraction.nodes) realIds.add(node.id);
|
|
2068
|
+
for (const node of extraction.nodes) {
|
|
2069
|
+
const sep2 = node.id.indexOf("::");
|
|
2070
|
+
if (sep2 > 0) realFiles.add(node.id.slice(0, sep2));
|
|
2071
|
+
}
|
|
2072
|
+
for (const edge of extraction.edges) {
|
|
2073
|
+
if (edge.relation === "contains") realFiles.add(edge.source);
|
|
2074
|
+
}
|
|
2075
|
+
}
|
|
2076
|
+
const remap = /* @__PURE__ */ new Map();
|
|
2077
|
+
const resolveId = (id) => {
|
|
2078
|
+
if (isOpaque(id)) return null;
|
|
2079
|
+
const cached = remap.get(id);
|
|
2080
|
+
if (cached !== void 0) return cached;
|
|
2081
|
+
const sep2 = id.indexOf("::");
|
|
2082
|
+
let resolved;
|
|
2083
|
+
if (sep2 > 0) {
|
|
2084
|
+
if (realIds.has(id)) return null;
|
|
2085
|
+
const guessedPath = id.slice(0, sep2);
|
|
2086
|
+
const name = id.slice(sep2);
|
|
2087
|
+
const candidates = candidatePaths(guessedPath).map((p) => `${p}${name}`);
|
|
2088
|
+
resolved = uniqueMatch(id, candidates, realIds);
|
|
2089
|
+
} else {
|
|
2090
|
+
if (realFiles.has(id)) return null;
|
|
2091
|
+
resolved = uniqueMatch(id, candidatePaths(id), realFiles);
|
|
2092
|
+
}
|
|
2093
|
+
if (resolved !== null) remap.set(id, resolved);
|
|
2094
|
+
return resolved;
|
|
2095
|
+
};
|
|
2096
|
+
let resolvedEndpoints = 0;
|
|
2097
|
+
for (const extraction of extractions) {
|
|
2098
|
+
for (const edge of extraction.edges) {
|
|
2099
|
+
const source = resolveId(edge.source);
|
|
2100
|
+
if (source !== null) {
|
|
2101
|
+
edge.source = source;
|
|
2102
|
+
resolvedEndpoints++;
|
|
2103
|
+
}
|
|
2104
|
+
const target = resolveId(edge.target);
|
|
2105
|
+
if (target !== null) {
|
|
2106
|
+
edge.target = target;
|
|
2107
|
+
resolvedEndpoints++;
|
|
2108
|
+
}
|
|
2109
|
+
}
|
|
2110
|
+
}
|
|
2111
|
+
let droppedPlaceholders = 0;
|
|
2112
|
+
for (const extraction of extractions) {
|
|
2113
|
+
const before = extraction.nodes.length;
|
|
2114
|
+
extraction.nodes = extraction.nodes.filter((node) => !remap.has(node.id));
|
|
2115
|
+
droppedPlaceholders += before - extraction.nodes.length;
|
|
2116
|
+
}
|
|
2117
|
+
return { resolvedEndpoints, droppedPlaceholders };
|
|
2118
|
+
}
|
|
2119
|
+
|
|
2015
2120
|
// src/cluster.ts
|
|
2016
2121
|
import { createHash } from "crypto";
|
|
2017
2122
|
import Graph2 from "graphology";
|
|
@@ -2408,29 +2513,81 @@ async function exportGraph(graph, options, report) {
|
|
|
2408
2513
|
}
|
|
2409
2514
|
}
|
|
2410
2515
|
|
|
2411
|
-
// src/
|
|
2516
|
+
// src/extractionCache.ts
|
|
2517
|
+
import { createHash as createHash2 } from "crypto";
|
|
2518
|
+
import * as fs10 from "fs/promises";
|
|
2412
2519
|
import * as path6 from "path";
|
|
2520
|
+
var ExtractionCache = class {
|
|
2521
|
+
dir;
|
|
2522
|
+
constructor(outDir) {
|
|
2523
|
+
this.dir = path6.join(outDir, "cache");
|
|
2524
|
+
}
|
|
2525
|
+
key(relFile, content) {
|
|
2526
|
+
return createHash2("sha256").update(relFile).update("\0").update(content).digest("hex");
|
|
2527
|
+
}
|
|
2528
|
+
async get(key) {
|
|
2529
|
+
try {
|
|
2530
|
+
const raw = await fs10.readFile(path6.join(this.dir, `${key}.json`), "utf-8");
|
|
2531
|
+
const parsed = JSON.parse(raw);
|
|
2532
|
+
if (!Array.isArray(parsed.nodes) || !Array.isArray(parsed.edges)) return null;
|
|
2533
|
+
return parsed;
|
|
2534
|
+
} catch {
|
|
2535
|
+
return null;
|
|
2536
|
+
}
|
|
2537
|
+
}
|
|
2538
|
+
async put(key, extraction) {
|
|
2539
|
+
await fs10.mkdir(this.dir, { recursive: true });
|
|
2540
|
+
await fs10.writeFile(path6.join(this.dir, `${key}.json`), JSON.stringify(extraction), "utf-8");
|
|
2541
|
+
}
|
|
2542
|
+
};
|
|
2543
|
+
|
|
2544
|
+
// src/pipeline.ts
|
|
2545
|
+
import * as fs11 from "fs/promises";
|
|
2546
|
+
import * as path7 from "path";
|
|
2413
2547
|
async function runPipeline(root, options = {}) {
|
|
2414
2548
|
const progress = options.onProgress ?? (() => {
|
|
2415
2549
|
});
|
|
2416
|
-
const resolvedRoot =
|
|
2550
|
+
const resolvedRoot = path7.resolve(root);
|
|
2417
2551
|
progress(`Scanning ${resolvedRoot} ...`);
|
|
2418
2552
|
const manifest = collectFiles(resolvedRoot);
|
|
2419
2553
|
progress(
|
|
2420
2554
|
`Found ${manifest.totalFiles} file(s) (${manifest.files.code.length} code, ${manifest.skippedSensitive.length} skipped as sensitive).`
|
|
2421
2555
|
);
|
|
2556
|
+
const outDir = options.outDir ?? path7.join(resolvedRoot, "graphify-out");
|
|
2557
|
+
const cache = new ExtractionCache(outDir);
|
|
2422
2558
|
progress("Extracting...");
|
|
2423
2559
|
const extractions = [];
|
|
2560
|
+
let cacheHits = 0;
|
|
2424
2561
|
for (const relFile of manifest.files.code.sort((a, b) => a.localeCompare(b))) {
|
|
2425
|
-
const filePath =
|
|
2562
|
+
const filePath = path7.relative(process.cwd(), path7.join(resolvedRoot, relFile)) || relFile;
|
|
2426
2563
|
try {
|
|
2427
|
-
const
|
|
2564
|
+
const key = cache.key(relFile, await fs11.readFile(path7.join(resolvedRoot, relFile)));
|
|
2565
|
+
let extraction = options.update ? await cache.get(key) : null;
|
|
2566
|
+
if (extraction) {
|
|
2567
|
+
cacheHits++;
|
|
2568
|
+
} else {
|
|
2569
|
+
extraction = await extract(filePath, relFile);
|
|
2570
|
+
await cache.put(key, extraction);
|
|
2571
|
+
}
|
|
2428
2572
|
extractions.push(extraction);
|
|
2429
2573
|
} catch (error) {
|
|
2430
2574
|
progress(` extraction failed for ${relFile}: ${error.message}`);
|
|
2431
2575
|
throw error;
|
|
2432
2576
|
}
|
|
2433
2577
|
}
|
|
2578
|
+
if (options.update) {
|
|
2579
|
+
progress(` cache: ${cacheHits} hit(s), ${extractions.length - cacheHits} re-extracted.`);
|
|
2580
|
+
}
|
|
2581
|
+
if (options.extraExtractions && options.extraExtractions.length > 0) {
|
|
2582
|
+
extractions.push(...options.extraExtractions);
|
|
2583
|
+
}
|
|
2584
|
+
progress("Resolving cross-file references...");
|
|
2585
|
+
const resolveStats = resolveCrossFileReferences(extractions);
|
|
2586
|
+
if (resolveStats.resolvedEndpoints > 0) {
|
|
2587
|
+
progress(
|
|
2588
|
+
` re-pointed ${resolveStats.resolvedEndpoints} guessed reference(s) at real nodes (${resolveStats.droppedPlaceholders} placeholder node(s) dropped).`
|
|
2589
|
+
);
|
|
2590
|
+
}
|
|
2434
2591
|
progress("Building graph...");
|
|
2435
2592
|
const graph = buildGraph(extractions);
|
|
2436
2593
|
progress(`Clustering (${options.algorithm ?? "louvain"})...`);
|
|
@@ -2439,7 +2596,6 @@ async function runPipeline(root, options = {}) {
|
|
|
2439
2596
|
const analysis = analyze(graph);
|
|
2440
2597
|
progress("Rendering report...");
|
|
2441
2598
|
const report = renderReport(graph, analysis);
|
|
2442
|
-
const outDir = options.outDir ?? path6.join(resolvedRoot, "graphify-out");
|
|
2443
2599
|
progress(`Exporting to ${outDir} ...`);
|
|
2444
2600
|
await exportGraph(
|
|
2445
2601
|
graph,
|
|
@@ -2457,6 +2613,367 @@ async function runPipeline(root, options = {}) {
|
|
|
2457
2613
|
return { manifest, graph, analysis, report };
|
|
2458
2614
|
}
|
|
2459
2615
|
|
|
2616
|
+
// src/merge.ts
|
|
2617
|
+
import Graph3 from "graphology";
|
|
2618
|
+
var CONFIDENCE_RANK2 = { EXTRACTED: 3, INFERRED: 2, AMBIGUOUS: 1 };
|
|
2619
|
+
function strongerConfidence2(a, b) {
|
|
2620
|
+
return CONFIDENCE_RANK2[a] >= CONFIDENCE_RANK2[b] ? a : b;
|
|
2621
|
+
}
|
|
2622
|
+
function isShared(id) {
|
|
2623
|
+
return id.startsWith("external:") || id.startsWith("module:");
|
|
2624
|
+
}
|
|
2625
|
+
function namespaced(project, id) {
|
|
2626
|
+
return isShared(id) ? id : `${project}/${id}`;
|
|
2627
|
+
}
|
|
2628
|
+
function mergeGraphs(entries) {
|
|
2629
|
+
const names = entries.map((e) => e.name);
|
|
2630
|
+
const duplicate = names.find((name, i) => names.indexOf(name) !== i);
|
|
2631
|
+
if (duplicate !== void 0) {
|
|
2632
|
+
throw new Error(`Duplicate project name in merge: "${duplicate}" \u2014 every entry needs a unique name.`);
|
|
2633
|
+
}
|
|
2634
|
+
const merged = new Graph3({ type: "directed", multi: true, allowSelfLoops: true });
|
|
2635
|
+
const sorted = [...entries].sort((a, b) => a.name.localeCompare(b.name));
|
|
2636
|
+
for (const { name, graph } of sorted) {
|
|
2637
|
+
const nodeIds = [...graph.nodes()].sort((a, b) => a.localeCompare(b));
|
|
2638
|
+
for (const id of nodeIds) {
|
|
2639
|
+
const attrs = graph.getNodeAttributes(id);
|
|
2640
|
+
const newId = namespaced(name, id);
|
|
2641
|
+
if (merged.hasNode(newId)) {
|
|
2642
|
+
merged.setNodeAttribute(newId, "project", "(shared)");
|
|
2643
|
+
continue;
|
|
2644
|
+
}
|
|
2645
|
+
const sourceFile = attrs.sourceFile ?? "";
|
|
2646
|
+
merged.addNode(newId, {
|
|
2647
|
+
label: attrs.label ?? id,
|
|
2648
|
+
sourceFile: isShared(id) || sourceFile === "" ? sourceFile : `${name}/${sourceFile}`,
|
|
2649
|
+
sourceLocation: attrs.sourceLocation ?? "",
|
|
2650
|
+
project: name
|
|
2651
|
+
});
|
|
2652
|
+
}
|
|
2653
|
+
const edgeKeys = [...graph.edges()].sort((a, b) => a.localeCompare(b));
|
|
2654
|
+
for (const edgeKey of edgeKeys) {
|
|
2655
|
+
const attrs = graph.getEdgeAttributes(edgeKey);
|
|
2656
|
+
const source = namespaced(name, graph.source(edgeKey));
|
|
2657
|
+
const target = namespaced(name, graph.target(edgeKey));
|
|
2658
|
+
const relation = String(attrs.relation);
|
|
2659
|
+
const confidence = attrs.confidence;
|
|
2660
|
+
const key = `${source}|${relation}|${target}`;
|
|
2661
|
+
if (merged.hasEdge(key)) {
|
|
2662
|
+
const existing = merged.getEdgeAttribute(key, "confidence");
|
|
2663
|
+
merged.setEdgeAttribute(key, "confidence", strongerConfidence2(existing, confidence));
|
|
2664
|
+
continue;
|
|
2665
|
+
}
|
|
2666
|
+
merged.addEdgeWithKey(key, source, target, { relation, confidence });
|
|
2667
|
+
}
|
|
2668
|
+
}
|
|
2669
|
+
return merged;
|
|
2670
|
+
}
|
|
2671
|
+
|
|
2672
|
+
// src/memory.ts
|
|
2673
|
+
import { createHash as createHash3 } from "crypto";
|
|
2674
|
+
import * as fs12 from "fs/promises";
|
|
2675
|
+
import * as path8 from "path";
|
|
2676
|
+
var OUTCOMES = /* @__PURE__ */ new Set(["useful", "dead_end", "corrected"]);
|
|
2677
|
+
var TYPES = /* @__PURE__ */ new Set(["query", "path", "explain", "affected"]);
|
|
2678
|
+
function validateResult(value) {
|
|
2679
|
+
if (!value.question) throw new Error("save-result requires --question");
|
|
2680
|
+
if (!value.answer) throw new Error("save-result requires --answer");
|
|
2681
|
+
if (!OUTCOMES.has(value.outcome)) {
|
|
2682
|
+
throw new Error(`--outcome must be one of: useful, dead_end, corrected (got "${value.outcome}")`);
|
|
2683
|
+
}
|
|
2684
|
+
if (!TYPES.has(value.type)) {
|
|
2685
|
+
throw new Error(`--type must be one of: query, path, explain, affected (got "${value.type}")`);
|
|
2686
|
+
}
|
|
2687
|
+
if (value.outcome === "corrected" && !value.correction) {
|
|
2688
|
+
throw new Error("--outcome corrected requires --correction with what the right answer was");
|
|
2689
|
+
}
|
|
2690
|
+
}
|
|
2691
|
+
async function saveResult(entry, memoryDir, now = /* @__PURE__ */ new Date()) {
|
|
2692
|
+
validateResult(entry);
|
|
2693
|
+
await fs12.mkdir(memoryDir, { recursive: true });
|
|
2694
|
+
const saved = { ...entry, savedAt: now.toISOString() };
|
|
2695
|
+
const hash = createHash3("sha256").update(JSON.stringify(saved)).digest("hex").slice(0, 8);
|
|
2696
|
+
const stamp = saved.savedAt.replace(/[:.]/g, "-");
|
|
2697
|
+
const filePath = path8.join(memoryDir, `result-${stamp}-${hash}.json`);
|
|
2698
|
+
await fs12.writeFile(filePath, JSON.stringify(saved, null, 2), "utf-8");
|
|
2699
|
+
return filePath;
|
|
2700
|
+
}
|
|
2701
|
+
async function loadResults(memoryDir) {
|
|
2702
|
+
let files;
|
|
2703
|
+
try {
|
|
2704
|
+
files = (await fs12.readdir(memoryDir)).filter((f) => f.startsWith("result-") && f.endsWith(".json"));
|
|
2705
|
+
} catch {
|
|
2706
|
+
return [];
|
|
2707
|
+
}
|
|
2708
|
+
const results = [];
|
|
2709
|
+
for (const file of files.sort((a, b) => a.localeCompare(b))) {
|
|
2710
|
+
try {
|
|
2711
|
+
const parsed = JSON.parse(await fs12.readFile(path8.join(memoryDir, file), "utf-8"));
|
|
2712
|
+
if (parsed.question && parsed.savedAt) results.push(parsed);
|
|
2713
|
+
} catch {
|
|
2714
|
+
}
|
|
2715
|
+
}
|
|
2716
|
+
return results;
|
|
2717
|
+
}
|
|
2718
|
+
function renderLessons(results, options = {}) {
|
|
2719
|
+
const halfLife = options.halfLifeDays ?? 30;
|
|
2720
|
+
const now = options.now ?? /* @__PURE__ */ new Date();
|
|
2721
|
+
const minCorroboration = options.minCorroboration ?? 2;
|
|
2722
|
+
const weightOf = (savedAt) => {
|
|
2723
|
+
const ageDays = Math.max(0, (now.getTime() - new Date(savedAt).getTime()) / 864e5);
|
|
2724
|
+
return Math.pow(0.5, ageDays / halfLife);
|
|
2725
|
+
};
|
|
2726
|
+
const signals = /* @__PURE__ */ new Map();
|
|
2727
|
+
const corrections = [];
|
|
2728
|
+
for (const result of results) {
|
|
2729
|
+
const weight = weightOf(result.savedAt);
|
|
2730
|
+
for (const node of result.nodes ?? []) {
|
|
2731
|
+
const signal = signals.get(node) ?? { useful: 0, deadEnd: 0, usefulCount: 0, deadEndCount: 0 };
|
|
2732
|
+
if (result.outcome === "useful") {
|
|
2733
|
+
signal.useful += weight;
|
|
2734
|
+
signal.usefulCount++;
|
|
2735
|
+
} else if (result.outcome === "dead_end") {
|
|
2736
|
+
signal.deadEnd += weight;
|
|
2737
|
+
signal.deadEndCount++;
|
|
2738
|
+
}
|
|
2739
|
+
signals.set(node, signal);
|
|
2740
|
+
}
|
|
2741
|
+
if (result.outcome === "corrected" && result.correction) {
|
|
2742
|
+
corrections.push({ question: result.question, correction: result.correction, savedAt: result.savedAt });
|
|
2743
|
+
}
|
|
2744
|
+
}
|
|
2745
|
+
const byScore = (kind) => [...signals.entries()].filter(([, s]) => kind === "useful" ? s.usefulCount >= minCorroboration : s.deadEndCount > 0).sort((a, b) => b[1][kind] - a[1][kind] || a[0].localeCompare(b[0])).slice(0, 10);
|
|
2746
|
+
const lines = [
|
|
2747
|
+
"# Graph lessons",
|
|
2748
|
+
"",
|
|
2749
|
+
`_Aggregated from ${results.length} saved result(s); signal half-life ${halfLife} day(s)._`,
|
|
2750
|
+
"",
|
|
2751
|
+
"## Nodes that keep answering",
|
|
2752
|
+
""
|
|
2753
|
+
];
|
|
2754
|
+
const useful = byScore("useful");
|
|
2755
|
+
if (useful.length === 0) {
|
|
2756
|
+
lines.push(`(none yet with >= ${minCorroboration} corroborating results \u2014 keep saving results)`);
|
|
2757
|
+
} else {
|
|
2758
|
+
for (const [node, s] of useful) {
|
|
2759
|
+
lines.push(`- **${node}** \u2014 score ${s.useful.toFixed(2)} across ${s.usefulCount} useful result(s)`);
|
|
2760
|
+
}
|
|
2761
|
+
}
|
|
2762
|
+
lines.push("", "## Dead ends", "");
|
|
2763
|
+
const deadEnds = byScore("deadEnd");
|
|
2764
|
+
if (deadEnds.length === 0) {
|
|
2765
|
+
lines.push("(none recorded)");
|
|
2766
|
+
} else {
|
|
2767
|
+
for (const [node, s] of deadEnds) {
|
|
2768
|
+
lines.push(`- **${node}** \u2014 score ${s.deadEnd.toFixed(2)} across ${s.deadEndCount} dead-end result(s)`);
|
|
2769
|
+
}
|
|
2770
|
+
}
|
|
2771
|
+
lines.push("", "## Corrections", "");
|
|
2772
|
+
if (corrections.length === 0) {
|
|
2773
|
+
lines.push("(none recorded)");
|
|
2774
|
+
} else {
|
|
2775
|
+
corrections.sort((a, b) => b.savedAt.localeCompare(a.savedAt));
|
|
2776
|
+
for (const c of corrections.slice(0, 20)) {
|
|
2777
|
+
lines.push(`- (${c.savedAt.slice(0, 10)}) Q: ${c.question}`);
|
|
2778
|
+
lines.push(` Right answer: ${c.correction}`);
|
|
2779
|
+
}
|
|
2780
|
+
}
|
|
2781
|
+
lines.push("");
|
|
2782
|
+
return lines.join("\n");
|
|
2783
|
+
}
|
|
2784
|
+
|
|
2785
|
+
// src/diff.ts
|
|
2786
|
+
import { execFile } from "child_process";
|
|
2787
|
+
import * as fs13 from "fs/promises";
|
|
2788
|
+
import * as os from "os";
|
|
2789
|
+
import * as path9 from "path";
|
|
2790
|
+
import { promisify } from "util";
|
|
2791
|
+
var execFileAsync = promisify(execFile);
|
|
2792
|
+
async function graphForDirectory(root) {
|
|
2793
|
+
const manifest = collectFiles(root);
|
|
2794
|
+
const extractions = [];
|
|
2795
|
+
for (const relFile of manifest.files.code.sort((a, b) => a.localeCompare(b))) {
|
|
2796
|
+
extractions.push(await extract(path9.join(root, relFile), relFile));
|
|
2797
|
+
}
|
|
2798
|
+
resolveCrossFileReferences(extractions);
|
|
2799
|
+
return buildGraph(extractions);
|
|
2800
|
+
}
|
|
2801
|
+
async function checkoutRev(repoRoot, rev) {
|
|
2802
|
+
const dest = await fs13.mkdtemp(path9.join(os.tmpdir(), `graphify-review-${rev.replace(/[^a-zA-Z0-9]/g, "_")}-`));
|
|
2803
|
+
const { stdout } = await execFileAsync(
|
|
2804
|
+
"git",
|
|
2805
|
+
["-C", repoRoot, "archive", "--format=tar", rev],
|
|
2806
|
+
{ encoding: "buffer", maxBuffer: 512 * 1024 * 1024 }
|
|
2807
|
+
);
|
|
2808
|
+
await new Promise((resolve4, reject) => {
|
|
2809
|
+
const tar = execFile("tar", ["-x", "-C", dest], (error) => error ? reject(error) : resolve4());
|
|
2810
|
+
tar.stdin?.end(stdout);
|
|
2811
|
+
});
|
|
2812
|
+
return dest;
|
|
2813
|
+
}
|
|
2814
|
+
function edgeSignature(graph, id) {
|
|
2815
|
+
const signature = /* @__PURE__ */ new Set();
|
|
2816
|
+
graph.forEachOutEdge(id, (_e, attrs, _s, target) => signature.add(`-> ${String(attrs.relation)} ${target}`));
|
|
2817
|
+
graph.forEachInEdge(id, (_e, attrs, source) => signature.add(`<- ${String(attrs.relation)} ${source}`));
|
|
2818
|
+
return signature;
|
|
2819
|
+
}
|
|
2820
|
+
function describeSymbol(graph, id) {
|
|
2821
|
+
const attrs = graph.getNodeAttributes(id);
|
|
2822
|
+
const impact = affectedBy(graph, id, { maxDepth: 3 });
|
|
2823
|
+
const tests = testsForNode(graph, id);
|
|
2824
|
+
return {
|
|
2825
|
+
id,
|
|
2826
|
+
label: attrs.label ?? id,
|
|
2827
|
+
sourceFile: attrs.sourceFile ?? "",
|
|
2828
|
+
blastRadius: impact?.affected.length ?? 0,
|
|
2829
|
+
tests: tests?.testFiles.map((t) => t.file) ?? []
|
|
2830
|
+
};
|
|
2831
|
+
}
|
|
2832
|
+
function entityIds(graph) {
|
|
2833
|
+
const ids = /* @__PURE__ */ new Set();
|
|
2834
|
+
graph.forEachNode((id) => {
|
|
2835
|
+
if (id.includes("::")) ids.add(id);
|
|
2836
|
+
});
|
|
2837
|
+
return ids;
|
|
2838
|
+
}
|
|
2839
|
+
function diffGraphs(baseGraph, headGraph, base, head) {
|
|
2840
|
+
const baseIds = entityIds(baseGraph);
|
|
2841
|
+
const headIds = entityIds(headGraph);
|
|
2842
|
+
const added = [];
|
|
2843
|
+
const removed = [];
|
|
2844
|
+
const rewired = [];
|
|
2845
|
+
for (const id of [...headIds].sort((a, b) => a.localeCompare(b))) {
|
|
2846
|
+
if (!baseIds.has(id)) {
|
|
2847
|
+
added.push(describeSymbol(headGraph, id));
|
|
2848
|
+
continue;
|
|
2849
|
+
}
|
|
2850
|
+
const before = edgeSignature(baseGraph, id);
|
|
2851
|
+
const after = edgeSignature(headGraph, id);
|
|
2852
|
+
const gained = [...after].filter((e) => !before.has(e)).sort((a, b) => a.localeCompare(b));
|
|
2853
|
+
const lost = [...before].filter((e) => !after.has(e)).sort((a, b) => a.localeCompare(b));
|
|
2854
|
+
if (gained.length > 0 || lost.length > 0) {
|
|
2855
|
+
rewired.push({ ...describeSymbol(headGraph, id), gainedEdges: gained, lostEdges: lost });
|
|
2856
|
+
}
|
|
2857
|
+
}
|
|
2858
|
+
for (const id of [...baseIds].sort((a, b) => a.localeCompare(b))) {
|
|
2859
|
+
if (!headIds.has(id)) removed.push(describeSymbol(baseGraph, id));
|
|
2860
|
+
}
|
|
2861
|
+
return { base, head, added, removed, rewired };
|
|
2862
|
+
}
|
|
2863
|
+
async function reviewRevisions(repoRoot, base, head = "HEAD") {
|
|
2864
|
+
const [baseDir, headDir] = await Promise.all([checkoutRev(repoRoot, base), checkoutRev(repoRoot, head)]);
|
|
2865
|
+
try {
|
|
2866
|
+
const [baseGraph, headGraph] = [await graphForDirectory(baseDir), await graphForDirectory(headDir)];
|
|
2867
|
+
return diffGraphs(baseGraph, headGraph, base, head);
|
|
2868
|
+
} finally {
|
|
2869
|
+
await Promise.all([
|
|
2870
|
+
fs13.rm(baseDir, { recursive: true, force: true }),
|
|
2871
|
+
fs13.rm(headDir, { recursive: true, force: true })
|
|
2872
|
+
]);
|
|
2873
|
+
}
|
|
2874
|
+
}
|
|
2875
|
+
function renderReview(diff) {
|
|
2876
|
+
const lines = [
|
|
2877
|
+
`# Structural review: ${diff.base}..${diff.head}`,
|
|
2878
|
+
"",
|
|
2879
|
+
`${diff.added.length} symbol(s) added, ${diff.removed.length} removed, ${diff.rewired.length} rewired.`,
|
|
2880
|
+
""
|
|
2881
|
+
];
|
|
2882
|
+
const section = (title, symbols) => {
|
|
2883
|
+
lines.push(`## ${title} (${symbols.length})`, "");
|
|
2884
|
+
if (symbols.length === 0) {
|
|
2885
|
+
lines.push("(none)", "");
|
|
2886
|
+
return;
|
|
2887
|
+
}
|
|
2888
|
+
for (const s of symbols) {
|
|
2889
|
+
const tests = s.tests.length > 0 ? ` | tests: ${s.tests.join(", ")}` : " | tests: none found";
|
|
2890
|
+
lines.push(`- **${s.label}** (${s.sourceFile}) \u2014 blast radius ${s.blastRadius}${tests}`);
|
|
2891
|
+
const r = s;
|
|
2892
|
+
if (r.gainedEdges) {
|
|
2893
|
+
for (const e of r.gainedEdges.slice(0, 5)) lines.push(` - gained: ${e}`);
|
|
2894
|
+
for (const e of r.lostEdges.slice(0, 5)) lines.push(` - lost: ${e}`);
|
|
2895
|
+
}
|
|
2896
|
+
}
|
|
2897
|
+
lines.push("");
|
|
2898
|
+
};
|
|
2899
|
+
section("Removed \u2014 check every caller", diff.removed);
|
|
2900
|
+
section("Rewired \u2014 dependencies changed", diff.rewired);
|
|
2901
|
+
section("Added", diff.added);
|
|
2902
|
+
const allTests = /* @__PURE__ */ new Set();
|
|
2903
|
+
for (const s of [...diff.removed, ...diff.rewired, ...diff.added]) {
|
|
2904
|
+
for (const t of s.tests) allTests.add(t);
|
|
2905
|
+
}
|
|
2906
|
+
lines.push(`## Suggested test run (${allTests.size} file(s))`, "");
|
|
2907
|
+
for (const t of [...allTests].sort((a, b) => a.localeCompare(b))) lines.push(`- ${t}`);
|
|
2908
|
+
lines.push("");
|
|
2909
|
+
return lines.join("\n");
|
|
2910
|
+
}
|
|
2911
|
+
|
|
2912
|
+
// src/check.ts
|
|
2913
|
+
var DEPENDENCY_RELATIONS = /* @__PURE__ */ new Set([
|
|
2914
|
+
"calls",
|
|
2915
|
+
"imports",
|
|
2916
|
+
"imports_from",
|
|
2917
|
+
"inherits",
|
|
2918
|
+
"implements",
|
|
2919
|
+
"mixes_in",
|
|
2920
|
+
"embeds",
|
|
2921
|
+
"re_exports"
|
|
2922
|
+
]);
|
|
2923
|
+
function globToRegExp(glob) {
|
|
2924
|
+
const escaped = glob.replace(/[.+^${}()|[\]\\]/g, "\\$&");
|
|
2925
|
+
const pattern = escaped.replace(
|
|
2926
|
+
/\*\*|\*|\?/g,
|
|
2927
|
+
(token) => token === "**" ? ".*" : token === "*" ? "[^/]*" : "[^/]"
|
|
2928
|
+
);
|
|
2929
|
+
return new RegExp(`^${pattern}$`);
|
|
2930
|
+
}
|
|
2931
|
+
function validateRules(value) {
|
|
2932
|
+
const config = value;
|
|
2933
|
+
if (!config || !Array.isArray(config.rules)) {
|
|
2934
|
+
throw new Error('Rules file must be JSON of shape { "rules": [{ "name", "from", "disallow": [...] }] }');
|
|
2935
|
+
}
|
|
2936
|
+
for (const rule of config.rules) {
|
|
2937
|
+
if (!rule.name || typeof rule.from !== "string" || !Array.isArray(rule.disallow)) {
|
|
2938
|
+
throw new Error(`Malformed rule "${rule.name ?? "(unnamed)"}" \u2014 need name, from, and disallow[].`);
|
|
2939
|
+
}
|
|
2940
|
+
}
|
|
2941
|
+
return config;
|
|
2942
|
+
}
|
|
2943
|
+
function checkRules(graph, config) {
|
|
2944
|
+
const compiled = config.rules.map((rule) => ({
|
|
2945
|
+
rule,
|
|
2946
|
+
from: globToRegExp(rule.from),
|
|
2947
|
+
disallow: rule.disallow.map(globToRegExp)
|
|
2948
|
+
}));
|
|
2949
|
+
const violations = [];
|
|
2950
|
+
graph.forEachEdge((_edge, attrs, source, target) => {
|
|
2951
|
+
const relation = String(attrs.relation);
|
|
2952
|
+
if (!DEPENDENCY_RELATIONS.has(relation)) return;
|
|
2953
|
+
const fromFile = graph.getNodeAttribute(source, "sourceFile") ?? "";
|
|
2954
|
+
const toFile = graph.getNodeAttribute(target, "sourceFile") ?? "";
|
|
2955
|
+
if (!fromFile || !toFile || fromFile === toFile) return;
|
|
2956
|
+
for (const { rule, from, disallow } of compiled) {
|
|
2957
|
+
if (!from.test(fromFile)) continue;
|
|
2958
|
+
if (disallow.some((d) => d.test(toFile))) {
|
|
2959
|
+
violations.push({
|
|
2960
|
+
rule: rule.name,
|
|
2961
|
+
reason: rule.reason,
|
|
2962
|
+
fromFile,
|
|
2963
|
+
fromNode: source,
|
|
2964
|
+
toFile,
|
|
2965
|
+
toNode: target,
|
|
2966
|
+
relation
|
|
2967
|
+
});
|
|
2968
|
+
}
|
|
2969
|
+
}
|
|
2970
|
+
});
|
|
2971
|
+
violations.sort(
|
|
2972
|
+
(a, b) => a.rule.localeCompare(b.rule) || a.fromFile.localeCompare(b.fromFile) || a.toFile.localeCompare(b.toFile) || a.fromNode.localeCompare(b.fromNode)
|
|
2973
|
+
);
|
|
2974
|
+
return violations;
|
|
2975
|
+
}
|
|
2976
|
+
|
|
2460
2977
|
export {
|
|
2461
2978
|
collectFiles,
|
|
2462
2979
|
EXTRACTOR_REGISTRY,
|
|
@@ -2465,10 +2982,23 @@ export {
|
|
|
2465
2982
|
ExtractionValidationError,
|
|
2466
2983
|
validateExtraction,
|
|
2467
2984
|
buildGraph,
|
|
2985
|
+
resolveCrossFileReferences,
|
|
2468
2986
|
cluster,
|
|
2469
2987
|
analyze,
|
|
2470
2988
|
renderReport,
|
|
2471
2989
|
exportGraph,
|
|
2472
|
-
|
|
2990
|
+
ExtractionCache,
|
|
2991
|
+
runPipeline,
|
|
2992
|
+
mergeGraphs,
|
|
2993
|
+
saveResult,
|
|
2994
|
+
loadResults,
|
|
2995
|
+
renderLessons,
|
|
2996
|
+
graphForDirectory,
|
|
2997
|
+
diffGraphs,
|
|
2998
|
+
reviewRevisions,
|
|
2999
|
+
renderReview,
|
|
3000
|
+
globToRegExp,
|
|
3001
|
+
validateRules,
|
|
3002
|
+
checkRules
|
|
2473
3003
|
};
|
|
2474
|
-
//# sourceMappingURL=chunk-
|
|
3004
|
+
//# sourceMappingURL=chunk-EW23BLJC.js.map
|