noteconnection 0.9.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.
Files changed (40) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +198 -0
  3. package/dist/backend/CommunityDetection.js +58 -0
  4. package/dist/backend/FileLoader.js +110 -0
  5. package/dist/backend/GraphBuilder.js +347 -0
  6. package/dist/backend/GraphMetrics.js +70 -0
  7. package/dist/backend/algorithms/CycleDetection.js +63 -0
  8. package/dist/backend/algorithms/HybridEngine.js +70 -0
  9. package/dist/backend/algorithms/StatisticalAnalyzer.js +123 -0
  10. package/dist/backend/algorithms/TopologicalSort.js +69 -0
  11. package/dist/backend/algorithms/VectorSpace.js +87 -0
  12. package/dist/backend/build_dag.js +164 -0
  13. package/dist/backend/config.js +17 -0
  14. package/dist/backend/graph.js +108 -0
  15. package/dist/backend/main.js +67 -0
  16. package/dist/backend/parser.js +94 -0
  17. package/dist/backend/test_robustness/test_hybrid.js +60 -0
  18. package/dist/backend/test_robustness/test_statistics.js +58 -0
  19. package/dist/backend/test_robustness/test_vector.js +54 -0
  20. package/dist/backend/test_robustness.js +113 -0
  21. package/dist/backend/types.js +3 -0
  22. package/dist/backend/utils/frontmatterParser.js +121 -0
  23. package/dist/backend/utils/stringUtils.js +66 -0
  24. package/dist/backend/workers/keywordMatchWorker.js +22 -0
  25. package/dist/core/Graph.js +121 -0
  26. package/dist/core/Graph.test.js +37 -0
  27. package/dist/core/types.js +2 -0
  28. package/dist/frontend/analysis.js +356 -0
  29. package/dist/frontend/app.js +1447 -0
  30. package/dist/frontend/data.js +8356 -0
  31. package/dist/frontend/graph_data.json +8356 -0
  32. package/dist/frontend/index.html +279 -0
  33. package/dist/frontend/reader.js +177 -0
  34. package/dist/frontend/settings.js +84 -0
  35. package/dist/frontend/source_manager.js +61 -0
  36. package/dist/frontend/styles.css +577 -0
  37. package/dist/frontend/styles_analysis.css +145 -0
  38. package/dist/index.js +121 -0
  39. package/dist/server.js +149 -0
  40. package/package.json +39 -0
@@ -0,0 +1,58 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ const path = __importStar(require("path"));
37
+ const FileLoader_1 = require("../FileLoader");
38
+ const StatisticalAnalyzer_1 = require("../algorithms/StatisticalAnalyzer");
39
+ async function run() {
40
+ const root = path.resolve(__dirname, '../../../testconcept');
41
+ console.log(`Loading files from: ${root}`);
42
+ const files = await FileLoader_1.FileLoader.loadFiles(root);
43
+ console.log(`Loaded ${files.length} files.`);
44
+ const terms = files.map(f => f.filename);
45
+ console.log(`Analyzing ${terms.length} terms...`);
46
+ const matrix = StatisticalAnalyzer_1.StatisticalAnalyzer.analyze(files, terms);
47
+ // Low thresholds for demonstration on small dataset
48
+ const minSupport = 0.05;
49
+ const asymmetry = 0.1;
50
+ console.log(`Inferring dependencies (Min Support: ${minSupport}, Asymmetry: ${asymmetry})...`);
51
+ const deps = StatisticalAnalyzer_1.StatisticalAnalyzer.inferDependencies(matrix, minSupport, asymmetry);
52
+ console.log(`Inferred ${deps.length} dependencies.`);
53
+ console.log('Top 20 Suggestions:');
54
+ deps.slice(0, 20).forEach(d => {
55
+ console.log(`[${d.source}] -> [${d.target}] (Conf: ${d.confidence.toFixed(2)}, Jaccard: ${d.weight.toFixed(2)})`);
56
+ });
57
+ }
58
+ run().catch(e => console.error(e));
@@ -0,0 +1,54 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ const path = __importStar(require("path"));
37
+ const FileLoader_1 = require("../FileLoader");
38
+ const VectorSpace_1 = require("../algorithms/VectorSpace");
39
+ async function run() {
40
+ const root = path.resolve(__dirname, '../../../testconcept');
41
+ console.log(`Loading files from: ${root}`);
42
+ const files = await FileLoader_1.FileLoader.loadFiles(root);
43
+ console.log(`Loaded ${files.length} files.`);
44
+ const vectorSpace = new VectorSpace_1.VectorSpace(files);
45
+ console.log('Vector Space built.');
46
+ // Test a specific file
47
+ const testFile = files[0].filename; // e.g. "Absorption"
48
+ console.log(`Finding similarities for: [${testFile}]`);
49
+ const similar = vectorSpace.getSimilar(testFile, 5);
50
+ similar.forEach(res => {
51
+ console.log(` -> [${res.id}] Score: ${res.score.toFixed(4)}`);
52
+ });
53
+ }
54
+ run().catch(e => console.error(e));
@@ -0,0 +1,113 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ // src/backend/test_robustness.ts
37
+ const path = __importStar(require("path"));
38
+ const parser_1 = require("./parser");
39
+ const graph_1 = require("./graph");
40
+ async function test() {
41
+ const inputDir = path.resolve(__dirname, '../../test_robustness');
42
+ const outputDir = path.resolve(__dirname, '../../test_robustness');
43
+ const outputFile = path.join(outputDir, 'test_graph_result.json');
44
+ console.log(`Starting Robustness Test...`);
45
+ // 1. Load Files
46
+ // 1. 加载文件
47
+ const rawFiles = await parser_1.FileLoader.loadFiles(inputDir);
48
+ console.log(`Loaded ${rawFiles.length} files.`);
49
+ // 2. Parse Concepts
50
+ // 2. 解析概念
51
+ const concepts = parser_1.NoteParser.parse(rawFiles);
52
+ // 3. Build Graph
53
+ // 3. 构建图
54
+ const graph = graph_1.GraphBuilder.buildGraph(concepts);
55
+ // 4. Export
56
+ // 4. 导出
57
+ await graph_1.Exporter.exportToJSON(graph, outputFile);
58
+ // 5. Validation Logic
59
+ // 5. 验证逻辑
60
+ console.log('--- Validation Report ---');
61
+ // Check Node Count
62
+ // 检查节点数量
63
+ if (graph.nodes.length === 6) {
64
+ console.log('[PASS] Node Count is 6');
65
+ }
66
+ else {
67
+ console.error(`[FAIL] Node Count is ${graph.nodes.length}, expected 6`);
68
+ }
69
+ // Check Cycle A->B->C->A
70
+ // 检查循环 A->B->C->A
71
+ // Logic: Concept A mentions B -> Edge B->A (B is prerequisite for A)
72
+ // 逻辑:概念 A 提到 B -> 边 B->A (B 是 A 的先决条件)
73
+ // Concept B mentions C -> Edge C->B
74
+ // Concept C mentions A -> Edge A->C
75
+ // Cycle: A->C->B->A
76
+ const hasEdge = (src, tgt) => graph.edges.some(e => e.source === src && e.target === tgt);
77
+ if (hasEdge('Concept B', 'Concept A'))
78
+ console.log('[PASS] Edge B->A exists');
79
+ else
80
+ console.error('[FAIL] Edge B->A missing');
81
+ if (hasEdge('Concept C', 'Concept B'))
82
+ console.log('[PASS] Edge C->B exists');
83
+ else
84
+ console.error('[FAIL] Edge C->B missing');
85
+ if (hasEdge('Concept A', 'Concept C'))
86
+ console.log('[PASS] Edge A->C exists');
87
+ else
88
+ console.error('[FAIL] Edge A->C missing');
89
+ // Check Special Char
90
+ // 检查特殊字符
91
+ // Special mentions A -> A->Special
92
+ if (hasEdge('Concept A', 'Special (Char) Concept'))
93
+ console.log('[PASS] Edge A->Special exists');
94
+ else
95
+ console.error('[FAIL] Edge A->Special missing');
96
+ // Check Case Insensitivity
97
+ // 检查不区分大小写
98
+ // D mentions "concept a" -> A->D
99
+ if (hasEdge('Concept A', 'Concept D'))
100
+ console.log('[PASS] Edge A->D (Case Insensitive) exists');
101
+ else
102
+ console.error('[FAIL] Edge A->D missing');
103
+ // Check Empty Concept
104
+ // 检查空概念
105
+ // Should be a node, but no edges connected to/from it (unless its name is common)
106
+ // 应该是一个节点,但没有边连接到它/从它连接(除非它的名字很普通)
107
+ const emptyNode = graph.nodes.find(n => n.id === 'Empty Concept');
108
+ if (emptyNode)
109
+ console.log('[PASS] Empty Concept node exists');
110
+ else
111
+ console.error('[FAIL] Empty Concept node missing');
112
+ }
113
+ test().catch(err => console.error(err));
@@ -0,0 +1,3 @@
1
+ "use strict";
2
+ // src/backend/types.ts
3
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -0,0 +1,121 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.FrontmatterParser = void 0;
4
+ class FrontmatterParser {
5
+ /**
6
+ * Parses YAML frontmatter to extract structured metadata.
7
+ * 解析 YAML frontmatter 以提取结构化元数据。
8
+ */
9
+ static parse(content) {
10
+ const metadata = {
11
+ tags: [],
12
+ prerequisites: [],
13
+ next: []
14
+ };
15
+ const match = content.match(/^---\r?\n([\s\S]*?)\r?\n---/);
16
+ if (!match)
17
+ return metadata;
18
+ const yaml = match[1];
19
+ // Helper to extract list or inline array
20
+ const extractField = (fieldName) => {
21
+ const results = [];
22
+ // 1. Check for List format (starts with newline or empty value)
23
+ // field:
24
+ // - item
25
+ const listBlockRegex = new RegExp(`${fieldName}:\\s*\\r?\\n((?:\\s*-\\s*.*\\r?\\n?)*)`, 'i');
26
+ const listMatch = yaml.match(listBlockRegex);
27
+ if (listMatch) {
28
+ const lines = listMatch[1].split(/\r?\n/);
29
+ lines.forEach(line => {
30
+ const itemMatch = line.match(/\s*-\s*(.*)/);
31
+ if (itemMatch) {
32
+ const clean = this.cleanLink(itemMatch[1]);
33
+ if (clean)
34
+ results.push(clean);
35
+ }
36
+ });
37
+ // If we found a list, return it. Prioritize list over inline if both exist (rare but possible in bad yaml)
38
+ if (results.length > 0)
39
+ return results;
40
+ }
41
+ // 2. Check for Inline value (Array or Single)
42
+ const inlineRegex = new RegExp(`${fieldName}:\\s*(.*)`, 'i');
43
+ const inlineMatch = yaml.match(inlineRegex);
44
+ if (inlineMatch) {
45
+ let val = inlineMatch[1].trim();
46
+ if (!val)
47
+ return results; // handled by list or empty
48
+ // Check if it's a comment or invalid
49
+ if (val.startsWith('#'))
50
+ return results;
51
+ // Case A: WikiLink "[[Link]]" -> Single Item
52
+ if (val.startsWith('[[')) {
53
+ const clean = this.cleanLink(val);
54
+ if (clean)
55
+ results.push(clean);
56
+ return results;
57
+ }
58
+ // Case B: Inline Array "[a, b]"
59
+ if (val.startsWith('[')) {
60
+ // Remove outer [ ]
61
+ const inner = val.replace(/^\[|\]$/g, '');
62
+ inner.split(',').forEach(item => {
63
+ const clean = this.cleanLink(item);
64
+ if (clean)
65
+ results.push(clean);
66
+ });
67
+ return results;
68
+ }
69
+ // Case C: Plain Single Value "Item"
70
+ // Ensure it's not a list start (dash)
71
+ if (!val.startsWith('-')) {
72
+ const clean = this.cleanLink(val);
73
+ if (clean)
74
+ results.push(clean);
75
+ }
76
+ }
77
+ return results;
78
+ };
79
+ metadata.tags = extractField('tags');
80
+ metadata.prerequisites = extractField('prerequisites');
81
+ metadata.next = extractField('next');
82
+ return metadata;
83
+ }
84
+ /**
85
+ * Cleans a string from WikiLink brackets [[ ]] and whitespace.
86
+ * 清除 WikiLink 括号 [[ ]] 和空格。
87
+ */
88
+ static cleanLink(raw) {
89
+ let text = raw.trim();
90
+ // Remove [[ ]]
91
+ const linkMatch = text.match(/^\[\[(.*?)(?:\|.*?)?\]\]$/);
92
+ if (linkMatch) {
93
+ return linkMatch[1].trim();
94
+ }
95
+ // Remove quotes if present
96
+ text = text.replace(/^["']|["']$/g, '');
97
+ return text;
98
+ }
99
+ /**
100
+ * Extracts tags from YAML frontmatter and WikiLinks [[tag]].
101
+ * 从 YAML frontmatter 和 WikiLinks [[tag]] 中提取标签。
102
+ * @deprecated Use parse() for metadata. This method mixes concepts.
103
+ */
104
+ static extractTags(content) {
105
+ const tags = new Set();
106
+ // Use new parser for YAML tags
107
+ const parsed = this.parse(content);
108
+ parsed.tags.forEach(t => tags.add(t));
109
+ // 2. Scan for [[WikiLinks]] in the entire content
110
+ // Regex to capture [[Tag]] or [[Tag|Label]]
111
+ const wikiLinkRegex = /\[\[(.*?)(?:\|.*?)?\]\]/g;
112
+ let wikiMatch;
113
+ while ((wikiMatch = wikiLinkRegex.exec(content)) !== null) {
114
+ if (wikiMatch[1]) {
115
+ tags.add(wikiMatch[1].trim());
116
+ }
117
+ }
118
+ return Array.from(tags);
119
+ }
120
+ }
121
+ exports.FrontmatterParser = FrontmatterParser;
@@ -0,0 +1,66 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.levenshtein = levenshtein;
4
+ exports.isSimilar = isSimilar;
5
+ exports.checkMatch = checkMatch;
6
+ /**
7
+ * Calculates the Levenshtein distance between two strings.
8
+ * 计算两个字符串之间的 Levenshtein 距离。
9
+ */
10
+ function levenshtein(a, b) {
11
+ const matrix = [];
12
+ // Increment along the first column of each row
13
+ for (let i = 0; i <= b.length; i++) {
14
+ matrix[i] = [i];
15
+ }
16
+ // Increment each column in the first row
17
+ for (let j = 0; j <= a.length; j++) {
18
+ matrix[0][j] = j;
19
+ }
20
+ // Fill in the rest of the matrix
21
+ for (let i = 1; i <= b.length; i++) {
22
+ for (let j = 1; j <= a.length; j++) {
23
+ if (b.charAt(i - 1) == a.charAt(j - 1)) {
24
+ matrix[i][j] = matrix[i - 1][j - 1];
25
+ }
26
+ else {
27
+ matrix[i][j] = Math.min(matrix[i - 1][j - 1] + 1, // substitution
28
+ Math.min(matrix[i][j - 1] + 1, // insertion
29
+ matrix[i - 1][j] + 1 // deletion
30
+ ));
31
+ }
32
+ }
33
+ }
34
+ return matrix[b.length][a.length];
35
+ }
36
+ /**
37
+ * Checks if two strings are similar based on a threshold.
38
+ * 基于阈值检查两个字符串是否相似。
39
+ * @param a String A
40
+ * @param b String B
41
+ * @param threshold Max distance allowed (default 2)
42
+ */
43
+ function isSimilar(a, b, threshold = 2) {
44
+ if (Math.abs(a.length - b.length) > threshold)
45
+ return false;
46
+ return levenshtein(a.toLowerCase(), b.toLowerCase()) <= threshold;
47
+ }
48
+ /**
49
+ * Checks if the content contains the term based on the provided strategy.
50
+ * 根据提供的策略检查内容是否包含术语。
51
+ * @param content The text content to search within
52
+ * @param term The term to search for
53
+ * @param strategy 'exact-phrase' or 'fuzzy'
54
+ */
55
+ function checkMatch(content, term, strategy = 'exact-phrase') {
56
+ if (strategy === 'exact-phrase') {
57
+ const escapedTerm = term.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
58
+ const regex = new RegExp(`\\b${escapedTerm}\\b`, 'i');
59
+ return regex.test(content);
60
+ }
61
+ else if (strategy === 'fuzzy') {
62
+ // Simple inclusion as per original logic
63
+ return content.toLowerCase().includes(term.toLowerCase());
64
+ }
65
+ return false;
66
+ }
@@ -0,0 +1,22 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const worker_threads_1 = require("worker_threads");
4
+ const stringUtils_1 = require("../utils/stringUtils");
5
+ const { filesChunk, targetIds, strategy, exclusionList } = worker_threads_1.workerData;
6
+ const results = [];
7
+ filesChunk.forEach(sourceFile => {
8
+ const sourceId = sourceFile.filename;
9
+ const content = sourceFile.content;
10
+ targetIds.forEach(targetId => {
11
+ if (sourceId === targetId)
12
+ return;
13
+ if (exclusionList.includes(targetId))
14
+ return;
15
+ if ((0, stringUtils_1.checkMatch)(content, targetId, strategy)) {
16
+ results.push({ source: sourceId, target: targetId });
17
+ }
18
+ });
19
+ });
20
+ if (worker_threads_1.parentPort) {
21
+ worker_threads_1.parentPort.postMessage(results);
22
+ }
@@ -0,0 +1,121 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.Graph = void 0;
4
+ /**
5
+ * Directed Graph implementation for managing notes and dependencies.
6
+ * 用于管理笔记和依赖关系的有向图实现。
7
+ */
8
+ class Graph {
9
+ constructor() {
10
+ this.nodes = new Map();
11
+ this.adjacencyList = new Map();
12
+ this.reverseAdjacencyList = new Map();
13
+ }
14
+ /**
15
+ * Adds a node to the graph.
16
+ * 向图中添加一个节点。
17
+ * @param node The node to add | 要添加的节点
18
+ */
19
+ addNode(node) {
20
+ if (!this.nodes.has(node.id)) {
21
+ this.nodes.set(node.id, { ...node, inDegree: 0, outDegree: 0 });
22
+ this.adjacencyList.set(node.id, []);
23
+ this.reverseAdjacencyList.set(node.id, []);
24
+ }
25
+ }
26
+ /**
27
+ * Retrieves a node by its ID.
28
+ * 通过 ID 获取节点。
29
+ * @param id The node ID | 节点 ID
30
+ * @returns The node or undefined if not found | 节点,如果未找到则返回 undefined
31
+ */
32
+ getNode(id) {
33
+ return this.nodes.get(id);
34
+ }
35
+ /**
36
+ * Checks if a node exists in the graph.
37
+ * 检查图中是否存在该节点。
38
+ * @param id The node ID | 节点 ID
39
+ */
40
+ hasNode(id) {
41
+ return this.nodes.has(id);
42
+ }
43
+ /**
44
+ * Adds a directed edge between two nodes.
45
+ * 在两个节点之间添加有向边。
46
+ * @param source Source node ID | 源节点 ID
47
+ * @param target Target node ID | 目标节点 ID
48
+ * @param type Relationship type | 关系类型
49
+ * @param weight Edge weight (confidence) | 边权重 (置信度)
50
+ */
51
+ addEdge(source, target, type = 'dependency', weight = 1) {
52
+ if (!this.nodes.has(source)) {
53
+ this.addNode({ id: source, label: source, inDegree: 0, outDegree: 0 });
54
+ }
55
+ if (!this.nodes.has(target)) {
56
+ this.addNode({ id: target, label: target, inDegree: 0, outDegree: 0 });
57
+ }
58
+ const edge = { source, target, type, weight };
59
+ // Add to adjacency list (outgoing)
60
+ const outgoing = this.adjacencyList.get(source) || [];
61
+ // Prevent duplicate edges
62
+ if (!outgoing.some(e => e.target === target && e.type === type)) {
63
+ outgoing.push(edge);
64
+ this.adjacencyList.set(source, outgoing);
65
+ // Update out-degree
66
+ const sourceNode = this.nodes.get(source);
67
+ sourceNode.outDegree++;
68
+ }
69
+ // Add to reverse adjacency list (incoming)
70
+ const incoming = this.reverseAdjacencyList.get(target) || [];
71
+ if (!incoming.some(e => e.source === source && e.type === type)) {
72
+ incoming.push(edge);
73
+ this.reverseAdjacencyList.set(target, incoming);
74
+ // Update in-degree
75
+ const targetNode = this.nodes.get(target);
76
+ targetNode.inDegree++;
77
+ }
78
+ }
79
+ /**
80
+ * Gets all outgoing edges from a node.
81
+ * 获取节点的所有出边。
82
+ * @param id Node ID | 节点 ID
83
+ */
84
+ getOutgoingEdges(id) {
85
+ return this.adjacencyList.get(id) || [];
86
+ }
87
+ /**
88
+ * Gets all outgoing neighbor IDs for a node.
89
+ * 获取节点的所有出度邻居 ID。
90
+ * @param id Node ID | 节点 ID
91
+ */
92
+ getNeighbors(id) {
93
+ return (this.adjacencyList.get(id) || []).map(edge => edge.target);
94
+ }
95
+ /**
96
+ * Gets all nodes in the graph.
97
+ * 获取图中的所有节点。
98
+ */
99
+ getNodes() {
100
+ return Array.from(this.nodes.values());
101
+ }
102
+ /**
103
+ * Gets all incoming edges to a node.
104
+ * 获取节点的所有入边。
105
+ * @param id Node ID | 节点 ID
106
+ */
107
+ getIncomingEdges(id) {
108
+ return this.reverseAdjacencyList.get(id) || [];
109
+ }
110
+ /**
111
+ * Returns the graph data in a serializable format.
112
+ * 以可序列化的格式返回图数据。
113
+ */
114
+ toJSON() {
115
+ return {
116
+ nodes: Array.from(this.nodes.values()),
117
+ edges: Array.from(this.adjacencyList.values()).flat()
118
+ };
119
+ }
120
+ }
121
+ exports.Graph = Graph;
@@ -0,0 +1,37 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const Graph_1 = require("./Graph");
4
+ describe('Graph Core', () => {
5
+ let graph;
6
+ beforeEach(() => {
7
+ graph = new Graph_1.Graph();
8
+ });
9
+ test('should add a node correctly', () => {
10
+ const node = { id: 'A', label: 'Note A', inDegree: 0, outDegree: 0 };
11
+ graph.addNode(node);
12
+ expect(graph.hasNode('A')).toBe(true);
13
+ expect(graph.getNode('A')).toEqual(expect.objectContaining({ id: 'A' }));
14
+ });
15
+ test('should add an edge and update degrees', () => {
16
+ graph.addEdge('A', 'B');
17
+ expect(graph.hasNode('A')).toBe(true);
18
+ expect(graph.hasNode('B')).toBe(true);
19
+ const nodeA = graph.getNode('A');
20
+ const nodeB = graph.getNode('B');
21
+ expect(nodeA?.outDegree).toBe(1);
22
+ expect(nodeB?.inDegree).toBe(1);
23
+ const outgoing = graph.getOutgoingEdges('A');
24
+ expect(outgoing).toHaveLength(1);
25
+ expect(outgoing[0].target).toBe('B');
26
+ const incoming = graph.getIncomingEdges('B');
27
+ expect(incoming).toHaveLength(1);
28
+ expect(incoming[0].source).toBe('A');
29
+ });
30
+ test('should not add duplicate edges', () => {
31
+ graph.addEdge('A', 'B');
32
+ graph.addEdge('A', 'B'); // Duplicate
33
+ const nodeA = graph.getNode('A');
34
+ expect(nodeA?.outDegree).toBe(1);
35
+ expect(graph.getOutgoingEdges('A')).toHaveLength(1);
36
+ });
37
+ });
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });