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.
- package/LICENSE +21 -0
- package/README.md +198 -0
- package/dist/backend/CommunityDetection.js +58 -0
- package/dist/backend/FileLoader.js +110 -0
- package/dist/backend/GraphBuilder.js +347 -0
- package/dist/backend/GraphMetrics.js +70 -0
- package/dist/backend/algorithms/CycleDetection.js +63 -0
- package/dist/backend/algorithms/HybridEngine.js +70 -0
- package/dist/backend/algorithms/StatisticalAnalyzer.js +123 -0
- package/dist/backend/algorithms/TopologicalSort.js +69 -0
- package/dist/backend/algorithms/VectorSpace.js +87 -0
- package/dist/backend/build_dag.js +164 -0
- package/dist/backend/config.js +17 -0
- package/dist/backend/graph.js +108 -0
- package/dist/backend/main.js +67 -0
- package/dist/backend/parser.js +94 -0
- package/dist/backend/test_robustness/test_hybrid.js +60 -0
- package/dist/backend/test_robustness/test_statistics.js +58 -0
- package/dist/backend/test_robustness/test_vector.js +54 -0
- package/dist/backend/test_robustness.js +113 -0
- package/dist/backend/types.js +3 -0
- package/dist/backend/utils/frontmatterParser.js +121 -0
- package/dist/backend/utils/stringUtils.js +66 -0
- package/dist/backend/workers/keywordMatchWorker.js +22 -0
- package/dist/core/Graph.js +121 -0
- package/dist/core/Graph.test.js +37 -0
- package/dist/core/types.js +2 -0
- package/dist/frontend/analysis.js +356 -0
- package/dist/frontend/app.js +1447 -0
- package/dist/frontend/data.js +8356 -0
- package/dist/frontend/graph_data.json +8356 -0
- package/dist/frontend/index.html +279 -0
- package/dist/frontend/reader.js +177 -0
- package/dist/frontend/settings.js +84 -0
- package/dist/frontend/source_manager.js +61 -0
- package/dist/frontend/styles.css +577 -0
- package/dist/frontend/styles_analysis.css +145 -0
- package/dist/index.js +121 -0
- package/dist/server.js +149 -0
- package/package.json +39 -0
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.TopologicalSort = void 0;
|
|
4
|
+
/**
|
|
5
|
+
* Service to perform Topological Sort and Rank assignment.
|
|
6
|
+
* 执行拓扑排序和等级分配的服务。
|
|
7
|
+
*/
|
|
8
|
+
class TopologicalSort {
|
|
9
|
+
/**
|
|
10
|
+
* Assigns a topological rank (level) to each node.
|
|
11
|
+
* 为每个节点分配拓扑等级(层级)。
|
|
12
|
+
* Rank 0 = Roots (No dependencies).
|
|
13
|
+
* Rank N = Dependencies have max rank N-1.
|
|
14
|
+
*
|
|
15
|
+
* @param graph The graph to process.
|
|
16
|
+
* @returns Map of NodeId -> Rank.
|
|
17
|
+
*/
|
|
18
|
+
static assignRanks(graph) {
|
|
19
|
+
const ranks = new Map();
|
|
20
|
+
const inDegrees = new Map();
|
|
21
|
+
const nodes = graph.getNodes();
|
|
22
|
+
// 1. Initialize In-Degrees
|
|
23
|
+
nodes.forEach(node => {
|
|
24
|
+
inDegrees.set(node.id, node.inDegree);
|
|
25
|
+
ranks.set(node.id, 0); // Default rank
|
|
26
|
+
});
|
|
27
|
+
// 2. Queue for nodes with in-degree 0
|
|
28
|
+
const queue = [];
|
|
29
|
+
nodes.forEach(node => {
|
|
30
|
+
if (node.inDegree === 0) {
|
|
31
|
+
queue.push(node.id);
|
|
32
|
+
}
|
|
33
|
+
});
|
|
34
|
+
// 3. Process Queue (Kahn's Algorithm variant for Longest Path)
|
|
35
|
+
// We want rank[v] = max(rank[v], rank[u] + 1)
|
|
36
|
+
// Standard Kahn's processes nodes when all dependencies are met.
|
|
37
|
+
// This implicitly ensures we processed all 'u' before 'v'.
|
|
38
|
+
let processedCount = 0;
|
|
39
|
+
while (queue.length > 0) {
|
|
40
|
+
const uId = queue.shift();
|
|
41
|
+
processedCount++;
|
|
42
|
+
const uRank = ranks.get(uId);
|
|
43
|
+
const neighbors = graph.getNeighbors(uId);
|
|
44
|
+
for (const vId of neighbors) {
|
|
45
|
+
// Update rank of v
|
|
46
|
+
const currentVRank = ranks.get(vId) || 0;
|
|
47
|
+
if (uRank + 1 > currentVRank) {
|
|
48
|
+
ranks.set(vId, uRank + 1);
|
|
49
|
+
}
|
|
50
|
+
// Decrement in-degree
|
|
51
|
+
const d = inDegrees.get(vId) - 1;
|
|
52
|
+
inDegrees.set(vId, d);
|
|
53
|
+
if (d === 0) {
|
|
54
|
+
queue.push(vId);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
// 4. Handle Cycles
|
|
59
|
+
if (processedCount < nodes.length) {
|
|
60
|
+
console.warn(`Graph contains cycles! Processed ${processedCount}/${nodes.length} nodes.`);
|
|
61
|
+
// Nodes involved in cycles (or reachable from them) were not processed.
|
|
62
|
+
// Their ranks might be incorrect (default 0 or partial updates).
|
|
63
|
+
// We could identify them and push them to the bottom, or just leave as is.
|
|
64
|
+
// For now, we leave them. The CycleDetector should be used to resolve this separately.
|
|
65
|
+
}
|
|
66
|
+
return ranks;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
exports.TopologicalSort = TopologicalSort;
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.VectorSpace = void 0;
|
|
4
|
+
class VectorSpace {
|
|
5
|
+
constructor(files) {
|
|
6
|
+
this.vocab = new Map();
|
|
7
|
+
this.idf = new Map();
|
|
8
|
+
this.vectors = new Map();
|
|
9
|
+
this.build(files);
|
|
10
|
+
}
|
|
11
|
+
build(files) {
|
|
12
|
+
const docCount = files.length;
|
|
13
|
+
const docFreq = new Map();
|
|
14
|
+
const tokenizedDocs = new Map();
|
|
15
|
+
// 1. Tokenize & Build Vocabulary
|
|
16
|
+
files.forEach(file => {
|
|
17
|
+
const tokens = this.tokenize(file.content);
|
|
18
|
+
tokenizedDocs.set(file.filename, tokens);
|
|
19
|
+
const uniqueTokens = new Set(tokens);
|
|
20
|
+
uniqueTokens.forEach(token => {
|
|
21
|
+
docFreq.set(token, (docFreq.get(token) || 0) + 1);
|
|
22
|
+
});
|
|
23
|
+
});
|
|
24
|
+
// 2. Calculate IDF & Build Vocab Index
|
|
25
|
+
let index = 0;
|
|
26
|
+
docFreq.forEach((count, term) => {
|
|
27
|
+
if (count > 1) { // Ignore rare terms (min_doc_freq = 2)
|
|
28
|
+
this.vocab.set(term, index++);
|
|
29
|
+
this.idf.set(term, Math.log(docCount / (1 + count)));
|
|
30
|
+
}
|
|
31
|
+
});
|
|
32
|
+
// 3. Compute TF-IDF Vectors
|
|
33
|
+
tokenizedDocs.forEach((tokens, fileId) => {
|
|
34
|
+
const vector = new Array(this.vocab.size).fill(0);
|
|
35
|
+
const termCounts = new Map();
|
|
36
|
+
tokens.forEach(t => termCounts.set(t, (termCounts.get(t) || 0) + 1));
|
|
37
|
+
termCounts.forEach((count, term) => {
|
|
38
|
+
const idx = this.vocab.get(term);
|
|
39
|
+
if (idx !== undefined) {
|
|
40
|
+
const tf = count / tokens.length;
|
|
41
|
+
vector[idx] = tf * this.idf.get(term);
|
|
42
|
+
}
|
|
43
|
+
});
|
|
44
|
+
// L2 Normalize
|
|
45
|
+
const norm = Math.sqrt(vector.reduce((sum, val) => sum + val * val, 0));
|
|
46
|
+
if (norm > 0) {
|
|
47
|
+
for (let i = 0; i < vector.length; i++)
|
|
48
|
+
vector[i] /= norm;
|
|
49
|
+
}
|
|
50
|
+
this.vectors.set(fileId, vector);
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
tokenize(text) {
|
|
54
|
+
// Bilingual tokenizer: English words or Chinese characters
|
|
55
|
+
// 双语分词器:匹配英文单词或单个中文字符
|
|
56
|
+
// Matches sequence of Alphanumeric OR Single CJK character
|
|
57
|
+
const regex = /[a-zA-Z0-9]+|[\u4e00-\u9fa5]/g;
|
|
58
|
+
return (text.match(regex) || []).map(t => t.toLowerCase());
|
|
59
|
+
}
|
|
60
|
+
getVector(fileId) {
|
|
61
|
+
return this.vectors.get(fileId);
|
|
62
|
+
}
|
|
63
|
+
getSimilar(fileId, topK = 5) {
|
|
64
|
+
const sourceVec = this.vectors.get(fileId);
|
|
65
|
+
if (!sourceVec)
|
|
66
|
+
return [];
|
|
67
|
+
const results = [];
|
|
68
|
+
this.vectors.forEach((targetVec, targetId) => {
|
|
69
|
+
if (fileId !== targetId) {
|
|
70
|
+
const score = this.cosineSimilarity(sourceVec, targetVec);
|
|
71
|
+
if (score > 0) {
|
|
72
|
+
results.push({ id: targetId, score });
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
return results.sort((a, b) => b.score - a.score).slice(0, topK);
|
|
77
|
+
}
|
|
78
|
+
cosineSimilarity(vecA, vecB) {
|
|
79
|
+
let dot = 0;
|
|
80
|
+
// Since vectors are L2 normalized, cosine sim is just dot product
|
|
81
|
+
for (let i = 0; i < vecA.length; i++) {
|
|
82
|
+
dot += vecA[i] * vecB[i];
|
|
83
|
+
}
|
|
84
|
+
return dot;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
exports.VectorSpace = VectorSpace;
|
|
@@ -0,0 +1,164 @@
|
|
|
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 fs = __importStar(require("fs"));
|
|
37
|
+
const path = __importStar(require("path"));
|
|
38
|
+
// --- Implementations ---
|
|
39
|
+
class LocalFileLoader {
|
|
40
|
+
/**
|
|
41
|
+
* Loads all .md files from a directory.
|
|
42
|
+
* 从目录加载所有 .md 文件。
|
|
43
|
+
*/
|
|
44
|
+
static load(dirPath) {
|
|
45
|
+
if (!fs.existsSync(dirPath)) {
|
|
46
|
+
console.error(`Directory not found: ${dirPath}`);
|
|
47
|
+
return [];
|
|
48
|
+
}
|
|
49
|
+
const files = fs.readdirSync(dirPath);
|
|
50
|
+
const notes = [];
|
|
51
|
+
for (const file of files) {
|
|
52
|
+
if (path.extname(file).toLowerCase() === '.md') {
|
|
53
|
+
const fullPath = path.join(dirPath, file);
|
|
54
|
+
const content = fs.readFileSync(fullPath, 'utf-8');
|
|
55
|
+
const id = path.parse(file).name; // Filename as ID
|
|
56
|
+
notes.push({ id, content });
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
console.log(`Loaded ${notes.length} notes.`);
|
|
60
|
+
return notes;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
class KeywordMatcher {
|
|
64
|
+
/**
|
|
65
|
+
* Escapes RegExp special characters.
|
|
66
|
+
* 转义正则特殊字符。
|
|
67
|
+
*/
|
|
68
|
+
static escapeRegExp(string) {
|
|
69
|
+
return string.replace(/[.*+?^${}()|[\\]/g, '\\$&');
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Finds edges where one note mentions another's title.
|
|
73
|
+
* 查找一个笔记提及另一个笔记标题的边。
|
|
74
|
+
*/
|
|
75
|
+
static findMatches(notes) {
|
|
76
|
+
const edges = [];
|
|
77
|
+
const ids = notes.map(n => n.id);
|
|
78
|
+
// Pre-compile regex for each ID to improve performance
|
|
79
|
+
// 为每个 ID 预编译正则以提高性能
|
|
80
|
+
const idRegexes = ids.map(id => ({
|
|
81
|
+
id: id,
|
|
82
|
+
regex: new RegExp(`\\b${this.escapeRegExp(id)}\\b`, 'i') // Case insensitive, whole word
|
|
83
|
+
}));
|
|
84
|
+
for (const currentNote of notes) {
|
|
85
|
+
for (const other of idRegexes) {
|
|
86
|
+
if (currentNote.id === other.id)
|
|
87
|
+
continue; // No self-loops
|
|
88
|
+
// If current note contains other note's ID
|
|
89
|
+
// 如果当前笔记包含其他笔记的 ID
|
|
90
|
+
if (other.regex.test(currentNote.content)) {
|
|
91
|
+
// Logic: Mentioned (other) -> Mentioner (current)
|
|
92
|
+
// Basic -> Advanced
|
|
93
|
+
edges.push({
|
|
94
|
+
source: other.id,
|
|
95
|
+
target: currentNote.id,
|
|
96
|
+
weight: 1 // Could count matches for higher weight
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
console.log(`Discovered ${edges.length} edges.`);
|
|
102
|
+
return edges;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
class StructureGenerator {
|
|
106
|
+
/**
|
|
107
|
+
* Converts to D3 JSON format with Degree calculation.
|
|
108
|
+
* 转换为 D3 JSON 格式并计算度数。
|
|
109
|
+
*/
|
|
110
|
+
static generateJSON(notes, edges) {
|
|
111
|
+
// Calculate degrees
|
|
112
|
+
const inDegreeMap = new Map();
|
|
113
|
+
const outDegreeMap = new Map();
|
|
114
|
+
// Initialize maps
|
|
115
|
+
notes.forEach(n => {
|
|
116
|
+
inDegreeMap.set(n.id, 0);
|
|
117
|
+
outDegreeMap.set(n.id, 0);
|
|
118
|
+
});
|
|
119
|
+
// Sum degrees
|
|
120
|
+
edges.forEach(e => {
|
|
121
|
+
const currentIn = inDegreeMap.get(e.target) || 0;
|
|
122
|
+
inDegreeMap.set(e.target, currentIn + 1);
|
|
123
|
+
const currentOut = outDegreeMap.get(e.source) || 0;
|
|
124
|
+
outDegreeMap.set(e.source, currentOut + 1);
|
|
125
|
+
});
|
|
126
|
+
const d3Nodes = notes.map(n => ({
|
|
127
|
+
id: n.id,
|
|
128
|
+
group: 1,
|
|
129
|
+
inDegree: inDegreeMap.get(n.id) || 0,
|
|
130
|
+
outDegree: outDegreeMap.get(n.id) || 0
|
|
131
|
+
}));
|
|
132
|
+
const d3Links = edges.map(e => ({
|
|
133
|
+
source: e.source,
|
|
134
|
+
target: e.target,
|
|
135
|
+
value: e.weight
|
|
136
|
+
}));
|
|
137
|
+
return {
|
|
138
|
+
nodes: d3Nodes,
|
|
139
|
+
links: d3Links
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
// --- Main Execution ---
|
|
144
|
+
const CONFIG = {
|
|
145
|
+
inputDir: path.join(__dirname, '../../testconcept'),
|
|
146
|
+
outputFile: path.join(__dirname, '../../src/frontend/graph_data.json')
|
|
147
|
+
};
|
|
148
|
+
function main() {
|
|
149
|
+
console.log("Starting DAG Build Process...");
|
|
150
|
+
// 1. Load Files
|
|
151
|
+
const notes = LocalFileLoader.load(CONFIG.inputDir);
|
|
152
|
+
// 2. Find Edges
|
|
153
|
+
const edges = KeywordMatcher.findMatches(notes);
|
|
154
|
+
// 3. Generate Graph Data
|
|
155
|
+
const graphData = StructureGenerator.generateJSON(notes, edges);
|
|
156
|
+
// 4. Write Output
|
|
157
|
+
const outputDir = path.dirname(CONFIG.outputFile);
|
|
158
|
+
if (!fs.existsSync(outputDir)) {
|
|
159
|
+
fs.mkdirSync(outputDir, { recursive: true });
|
|
160
|
+
}
|
|
161
|
+
fs.writeFileSync(CONFIG.outputFile, JSON.stringify(graphData, null, 2));
|
|
162
|
+
console.log(`Graph data written to: ${CONFIG.outputFile}`);
|
|
163
|
+
}
|
|
164
|
+
main();
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.config = void 0;
|
|
4
|
+
exports.config = {
|
|
5
|
+
matchingStrategy: 'exact-phrase',
|
|
6
|
+
clusteringStrategy: 'label-propagation', // Default to current behavior
|
|
7
|
+
fuzzyThreshold: 2,
|
|
8
|
+
enableTags: true,
|
|
9
|
+
enableStatisticalInference: true, // Default on
|
|
10
|
+
enableVectorSimilarity: true, // Default on
|
|
11
|
+
enableHybridInference: true, // Default on
|
|
12
|
+
exclusionList: [
|
|
13
|
+
// Add common words or concepts here that cause too much noise
|
|
14
|
+
// e.g., "Introduction", "Summary", etc.
|
|
15
|
+
// For now, we leave it empty as a template
|
|
16
|
+
]
|
|
17
|
+
};
|
|
@@ -0,0 +1,108 @@
|
|
|
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
|
+
exports.Exporter = exports.GraphBuilder = void 0;
|
|
37
|
+
const fs = __importStar(require("fs/promises"));
|
|
38
|
+
class GraphBuilder {
|
|
39
|
+
static buildGraph(concepts) {
|
|
40
|
+
const nodes = concepts.map(c => ({
|
|
41
|
+
id: c.id,
|
|
42
|
+
label: c.title
|
|
43
|
+
}));
|
|
44
|
+
const edges = [];
|
|
45
|
+
const titleMap = new Map(); // lowercase title -> real id
|
|
46
|
+
// Pre-process titles for case-insensitive lookup
|
|
47
|
+
// 预处理标题以进行不区分大小写的查找
|
|
48
|
+
concepts.forEach(c => {
|
|
49
|
+
titleMap.set(c.title.toLowerCase(), c.id);
|
|
50
|
+
});
|
|
51
|
+
// 1. Explicit Dependencies (Placeholder for now)
|
|
52
|
+
// 1. 显式依赖 (目前占位)
|
|
53
|
+
// 2. Keyword Matching Strategy
|
|
54
|
+
// 2. 关键词匹配策略
|
|
55
|
+
concepts.forEach(sourceConcept => {
|
|
56
|
+
const contentLower = sourceConcept.content.toLowerCase();
|
|
57
|
+
concepts.forEach(targetConcept => {
|
|
58
|
+
if (sourceConcept.id === targetConcept.id)
|
|
59
|
+
return; // Self-loop check
|
|
60
|
+
const targetTitle = targetConcept.title.toLowerCase();
|
|
61
|
+
// Filter out very short words to reduce noise (e.g. "Ice", "mW")
|
|
62
|
+
// 过滤掉非常短的单词以减少噪声 (例如 "Ice", "mW")
|
|
63
|
+
// Heuristic: Length >= 3 or specific whitelist
|
|
64
|
+
// 启发式:长度 >= 3 或特定白名单
|
|
65
|
+
if (targetTitle.length < 3)
|
|
66
|
+
return;
|
|
67
|
+
// Check strict inclusion (maybe utilize regex boundary \b in future)
|
|
68
|
+
// 检查严格包含 (未来可能利用正则边界 \b)
|
|
69
|
+
if (contentLower.includes(targetTitle)) {
|
|
70
|
+
// Heuristic: If A mentions B, B -> A (B is concept used in A)
|
|
71
|
+
// 启发式:如果 A 提到 B,则 B -> A (B 是 A 中使用的概念)
|
|
72
|
+
edges.push({
|
|
73
|
+
source: targetConcept.id,
|
|
74
|
+
target: sourceConcept.id,
|
|
75
|
+
type: 'keyword',
|
|
76
|
+
weight: 1.0
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
// Deduplicate edges
|
|
82
|
+
// 去重边
|
|
83
|
+
const uniqueEdges = this.deduplicateEdges(edges);
|
|
84
|
+
return {
|
|
85
|
+
nodes,
|
|
86
|
+
edges: uniqueEdges
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
static deduplicateEdges(edges) {
|
|
90
|
+
const seen = new Set();
|
|
91
|
+
return edges.filter(e => {
|
|
92
|
+
const key = `${e.source}->${e.target}`;
|
|
93
|
+
if (seen.has(key))
|
|
94
|
+
return false;
|
|
95
|
+
seen.add(key);
|
|
96
|
+
return true;
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
exports.GraphBuilder = GraphBuilder;
|
|
101
|
+
class Exporter {
|
|
102
|
+
static async exportToJSON(graph, outputPath) {
|
|
103
|
+
const json = JSON.stringify(graph, null, 2);
|
|
104
|
+
await fs.writeFile(outputPath, json, 'utf-8');
|
|
105
|
+
console.log(`Graph exported to ${outputPath}`);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
exports.Exporter = Exporter;
|
|
@@ -0,0 +1,67 @@
|
|
|
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/main.ts
|
|
37
|
+
const path = __importStar(require("path"));
|
|
38
|
+
const parser_1 = require("./parser");
|
|
39
|
+
const graph_1 = require("./graph");
|
|
40
|
+
async function main() {
|
|
41
|
+
const inputDir = path.resolve(__dirname, '../../testconcept');
|
|
42
|
+
const outputDir = path.resolve(__dirname, '../frontend');
|
|
43
|
+
const outputFile = path.join(outputDir, 'graph_data.json');
|
|
44
|
+
console.log(`Starting Build Process...`);
|
|
45
|
+
console.log(`Input Directory: ${inputDir}`);
|
|
46
|
+
// 1. Load Files
|
|
47
|
+
// 1. 加载文件
|
|
48
|
+
const rawFiles = await parser_1.FileLoader.loadFiles(inputDir);
|
|
49
|
+
console.log(`Loaded ${rawFiles.length} files.`);
|
|
50
|
+
if (rawFiles.length === 0) {
|
|
51
|
+
console.warn('No markdown files found. Exiting.');
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
// 2. Parse Concepts
|
|
55
|
+
// 2. 解析概念
|
|
56
|
+
const concepts = parser_1.NoteParser.parse(rawFiles);
|
|
57
|
+
console.log(`Parsed ${concepts.length} concepts.`);
|
|
58
|
+
// 3. Build Graph
|
|
59
|
+
// 3. 构建图
|
|
60
|
+
const graph = graph_1.GraphBuilder.buildGraph(concepts);
|
|
61
|
+
console.log(`Graph Built: ${graph.nodes.length} Nodes, ${graph.edges.length} Edges.`);
|
|
62
|
+
// 4. Export
|
|
63
|
+
// 4. 导出
|
|
64
|
+
await graph_1.Exporter.exportToJSON(graph, outputFile);
|
|
65
|
+
console.log('Build Complete.');
|
|
66
|
+
}
|
|
67
|
+
main().catch(err => console.error(err));
|
|
@@ -0,0 +1,94 @@
|
|
|
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
|
+
exports.NoteParser = exports.FileLoader = void 0;
|
|
37
|
+
// src/backend/parser.ts
|
|
38
|
+
const fs_1 = require("fs");
|
|
39
|
+
const path = __importStar(require("path"));
|
|
40
|
+
// File Loader Implementation
|
|
41
|
+
// 文件加载器实现
|
|
42
|
+
class FileLoader {
|
|
43
|
+
static async loadFiles(directory) {
|
|
44
|
+
try {
|
|
45
|
+
const filenames = await fs_1.promises.readdir(directory);
|
|
46
|
+
const rawFiles = [];
|
|
47
|
+
for (const file of filenames) {
|
|
48
|
+
if (path.extname(file).toLowerCase() !== '.md')
|
|
49
|
+
continue;
|
|
50
|
+
const fullPath = path.join(directory, file);
|
|
51
|
+
const stat = await fs_1.promises.stat(fullPath);
|
|
52
|
+
const content = await fs_1.promises.readFile(fullPath, 'utf-8');
|
|
53
|
+
rawFiles.push({
|
|
54
|
+
filepath: fullPath,
|
|
55
|
+
filename: file,
|
|
56
|
+
content: content,
|
|
57
|
+
modifiedTime: stat.mtime
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
return rawFiles;
|
|
61
|
+
}
|
|
62
|
+
catch (error) {
|
|
63
|
+
console.error(`Error loading files from ${directory}:`, error);
|
|
64
|
+
return [];
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
exports.FileLoader = FileLoader;
|
|
69
|
+
// Note Parser Implementation
|
|
70
|
+
// 笔记解析器实现
|
|
71
|
+
class NoteParser {
|
|
72
|
+
static parse(files) {
|
|
73
|
+
return files.map(file => {
|
|
74
|
+
const id = path.parse(file.filename).name;
|
|
75
|
+
// Default title is ID, but can be extracted from first H1 if needed
|
|
76
|
+
// 默认标题为ID,但如果需要可以从第一个H1提取
|
|
77
|
+
const title = id;
|
|
78
|
+
// Simple parsing for now (Future: Extract YAML)
|
|
79
|
+
// 目前为简单解析(未来:提取 YAML)
|
|
80
|
+
// Removing special characters for cleaner keyword matching later
|
|
81
|
+
// 移除特殊字符以便后续更干净的关键词匹配
|
|
82
|
+
return {
|
|
83
|
+
id: id,
|
|
84
|
+
title: title,
|
|
85
|
+
content: file.content,
|
|
86
|
+
metadata: {
|
|
87
|
+
tags: [],
|
|
88
|
+
prerequisites: [] // Placeholder for v0.2.0 explicit parsing
|
|
89
|
+
}
|
|
90
|
+
};
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
exports.NoteParser = NoteParser;
|
|
@@ -0,0 +1,60 @@
|
|
|
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
|
+
const VectorSpace_1 = require("../algorithms/VectorSpace");
|
|
40
|
+
const HybridEngine_1 = require("../algorithms/HybridEngine");
|
|
41
|
+
async function run() {
|
|
42
|
+
const root = path.resolve(__dirname, '../../../testconcept');
|
|
43
|
+
console.log(`Loading files from: ${root}`);
|
|
44
|
+
const files = await FileLoader_1.FileLoader.loadFiles(root);
|
|
45
|
+
console.log(`Loaded ${files.length} files.`);
|
|
46
|
+
const terms = files.map(f => f.filename);
|
|
47
|
+
console.log('Building Statistical Matrix...');
|
|
48
|
+
const matrix = StatisticalAnalyzer_1.StatisticalAnalyzer.analyze(files, terms);
|
|
49
|
+
console.log('Building Vector Space...');
|
|
50
|
+
const vectorSpace = new VectorSpace_1.VectorSpace(files);
|
|
51
|
+
console.log('Running Hybrid Inference...');
|
|
52
|
+
const edges = HybridEngine_1.HybridEngine.infer(matrix, vectorSpace, 0.2, 0.1);
|
|
53
|
+
console.log(`Inferred ${edges.length} hybrid edges.`);
|
|
54
|
+
console.log('Top 20 Suggestions:');
|
|
55
|
+
edges.slice(0, 20).forEach(d => {
|
|
56
|
+
console.log(`[${d.source}] -> [${d.target}] (Conf: ${d.confidence.toFixed(2)}, Weight: ${d.weight.toFixed(2)})`);
|
|
57
|
+
console.log(` Reason: ${d.reason}`);
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
run().catch(e => console.error(e));
|