noteconnection 1.1.2 → 1.3.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/README.md +81 -3
- package/dist/src/core/Graph.js +84 -0
- package/dist/src/core/PathBridge.js +49 -0
- package/dist/src/core/PathEngine.js +196 -0
- package/dist/src/core/PathEngine.test.js +86 -0
- package/dist/src/electron/main.js +14 -0
- package/dist/src/frontend/README.md +81 -3
- package/dist/src/frontend/app.js +39 -0
- package/dist/src/frontend/index.html +128 -2
- package/dist/src/frontend/libs/path_core.js +429 -0
- package/dist/src/frontend/locales/en.json +52 -29
- package/dist/src/frontend/locales/zh.json +30 -7
- package/dist/src/frontend/path.html +100 -0
- package/dist/src/frontend/path_app.js +685 -0
- package/dist/src/frontend/path_styles.css +240 -0
- package/dist/src/frontend/path_worker.js +176 -0
- package/dist/src/frontend/styles.css +1 -1
- package/package.json +7 -3
package/README.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# 2026-01-
|
|
1
|
+
# 2026-01-24 v1.3.0
|
|
2
2
|
|
|
3
3
|
# NoteConnection Knowledge Graph
|
|
4
4
|
|
|
@@ -45,7 +45,15 @@ Unlike traditional "network" views that show a messy web of links, NoteConnectio
|
|
|
45
45
|
|
|
46
46
|
<img width="3723" height="1992" alt="image" src="https://github.com/user-attachments/assets/9e56e567-1742-48cf-b720-cf65a47fd317" />
|
|
47
47
|
|
|
48
|
-
### 3.
|
|
48
|
+
### 3. Path Mode: Structured Learning (v1.2.0)
|
|
49
|
+
|
|
50
|
+
- **Curriculum Generation**: Instantly transforms your graph into linear learning paths.
|
|
51
|
+
- **Domain Learning**: Master an entire concept cluster (Topological Sort).
|
|
52
|
+
- **Diffusion Learning**: Find the most efficient path to a specific goal (Shortest Path + Prerequisites).
|
|
53
|
+
- **Hybrid Architecture**: Connects to a high-fidelity **Godot 4.3 Desktop Renderer** via WebSocket (`ws://localhost:9876`) for AAA-quality visualization, while maintaining full web compatibility.
|
|
54
|
+
- **Smart Strategies**: Choose "Foundational" (Base-first) or "Core" (Importance-first) sorting to suit your learning style.
|
|
55
|
+
|
|
56
|
+
### 4. Performance & Control
|
|
49
57
|
|
|
50
58
|
- **High-Capacity Parallel Processing**: Utilizes Node.js `worker_threads` (up to 12 cores) to distribute computationally intensive keyword matching.
|
|
51
59
|
- **Simulation Controls (v0.9.0)**: Fine-tune the physics with a **Speed/Damping Slider** or use the **Freeze Layout** switch to stop the simulation for stable manual arrangement.
|
|
@@ -108,6 +116,10 @@ NoteConnection is built on a modular architecture designed for performance and e
|
|
|
108
116
|
- **State Management**: `SettingsManager` persists user preferences (Physics, Visuals) to `localStorage`.
|
|
109
117
|
- **Layout Logic**: Custom algorithms for Sugiyama-style layering and Force-directed physics.
|
|
110
118
|
|
|
119
|
+
### Desktop Bridge (`src/core`)
|
|
120
|
+
|
|
121
|
+
- **PathBridge**: standard WebSocket server (Port 9876) that exposes the internal graph state to external applications (e.g., Godot Engine), enabling hybrid web/native visualization pipelines.
|
|
122
|
+
|
|
111
123
|
---
|
|
112
124
|
|
|
113
125
|
<a id="quick-start-en"></a>
|
|
@@ -266,6 +278,33 @@ For optimal performance with "GPU Optimised Rendering", especially on AMD RDNA c
|
|
|
266
278
|
|
|
267
279
|
## 📅 Changelog
|
|
268
280
|
|
|
281
|
+
### v1.3.0 - Path Mode Polish & UI Refinements (2026-01-24)
|
|
282
|
+
|
|
283
|
+
- **Reader Integration**:
|
|
284
|
+
- **Seamless Access**: Double-clicking the central node in "Orbital Layout" now instantly opens the `Reader`, displaying full node content.
|
|
285
|
+
- **Data Fetching**: Fixed a critical issue where the reader would open empty; now correctly retrieves full metadata from the global graph state.
|
|
286
|
+
- **Visual Polish**:
|
|
287
|
+
- **Orbital Layout**: Significantly improved node dispersion (Radius 350-950px) to reduce label overlap.
|
|
288
|
+
- **Edge Clarity**: In Orbital mode, strictly hides edges not connected to the central node, reducing visual clutter by 90%.
|
|
289
|
+
- **Label Visibility**: Peripheral nodes now always display labels, sized proportionally to their distance (max 16px).
|
|
290
|
+
- **Depth of Field**: Adjusted opacity falloff to ensure distant nodes remain visible (min 0.4 opacity).
|
|
291
|
+
- **UX Improvements**:
|
|
292
|
+
- **Target Selection**: Increased the "Target Node" search limit from 20 to 300, ensuring users can find any node in the graph.
|
|
293
|
+
- **Interactive Layers**: Fixed `z-index` layering issues where the Reader window was previously hidden behind the Path visualization.
|
|
294
|
+
|
|
295
|
+
### v1.2.0 - Path Mode & Desktop Renderer (2026-01-23)
|
|
296
|
+
|
|
297
|
+
- **Path Mode**: Introduced a major new feature set for converting graphs into linear learning paths.
|
|
298
|
+
- **Learning Modes**: 'Domain Learning' (Topological) and 'Diffusion Learning' (Goal-oriented).
|
|
299
|
+
- **Visualization**: New Radial and Tree layouts powered by D3/Canvas.
|
|
300
|
+
- **Strategies**: 'Foundational' and 'Core' sorting algorithms.
|
|
301
|
+
- **Hybrid Architecture**:
|
|
302
|
+
- **Godot Bridge**: Implemented `PathBridge.ts` to sync graph state with external renderers via WebSocket (Port 9876).
|
|
303
|
+
- **Native Rendering**: Added support for Godot 4.3 to render the graph with high-fidelity Vulkan graphics (Source in `path_mode/`).
|
|
304
|
+
- **DevOps**:
|
|
305
|
+
- **NPM Scripts**: Added `pathmode:dev` and `pathmode:test` workflows.
|
|
306
|
+
- **UI Stability**: Fixed critical bugs in Radial Layout visibility (`centerView`) and Exit Mode logic.
|
|
307
|
+
|
|
269
308
|
### v1.1.2 - Path Resolution & UI Stability (2026-01-23)
|
|
270
309
|
|
|
271
310
|
- **Backend Protocol Fix**:
|
|
@@ -726,7 +765,15 @@ For optimal performance with "GPU Optimised Rendering", especially on AMD RDNA c
|
|
|
726
765
|
|
|
727
766
|
<img width="3723" height="2007" alt="image" src="https://github.com/user-attachments/assets/10978984-3e2d-4ab6-8b44-342d4f3c3800" />
|
|
728
767
|
|
|
729
|
-
### 3.
|
|
768
|
+
### 3. Path Mode (路径模式): 结构化学习 (v1.2.0)
|
|
769
|
+
|
|
770
|
+
- **课程生成**: 将复杂的网状图瞬间转化为线性的学习路径。
|
|
771
|
+
- **领域学习 (Domain Learning)**: 掌握整个概念集群(拓扑排序)。
|
|
772
|
+
- **扩散学习 (Diffusion Learning)**: 寻找通往特定目标的最优路径(最短路径 + 前置依赖)。
|
|
773
|
+
- **混合架构**: 通过 WebSocket (`ws://localhost:9876`) 连接到高保真 **Godot 4.3 桌面渲染器**,实现 3A 级的可视化效果,同时保持完全的 Web 兼容性。
|
|
774
|
+
- **智能策略**: 支持 "基础优先" (Foundational) 或 "核心优先" (Core) 排序,适应不同的学习风格。
|
|
775
|
+
|
|
776
|
+
### 4. 性能与控制 (Performance & Control)
|
|
730
777
|
|
|
731
778
|
- **高容量并行处理**: 利用 Node.js `worker_threads` (最多 12 核) 分发计算密集的关键词匹配任务。
|
|
732
779
|
- **模拟控制 (v0.9.0)**: 通过 **速度/阻尼滑块** 微调物理效果,或使用 **冻结布局** 开关停止模拟以进行稳定的手动排列。
|
|
@@ -789,6 +836,10 @@ NoteConnection 基于模块化架构构建,旨在实现高性能和可扩展
|
|
|
789
836
|
- **状态管理**: `SettingsManager` 将用户偏好(物理、视觉)持久化到 `localStorage`。
|
|
790
837
|
- **布局逻辑**: 自定义的 Sugiyama 风格分层算法和力导向物理算法。
|
|
791
838
|
|
|
839
|
+
### 桌面桥接 (Desktop Bridge) (`src/core`)
|
|
840
|
+
|
|
841
|
+
- **PathBridge**: 标准 WebSocket 服务器 (端口 9876),将内部图谱状态暴露给外部应用程序(例如 Godot 引擎),实现混合 Web/原生可视化管线。
|
|
842
|
+
|
|
792
843
|
---
|
|
793
844
|
|
|
794
845
|
<a id="quick-start-zh"></a>
|
|
@@ -934,6 +985,33 @@ npm start -- --path "E:/Knowledge/ObsidianVault" --no-gpu
|
|
|
934
985
|
|
|
935
986
|
## 📅 更新日志 (Changelog)
|
|
936
987
|
|
|
988
|
+
### v1.3.0 - 路径模式打磨与 UI 优化 (Path Mode Polish & UI Refinements) (2026-01-24)
|
|
989
|
+
|
|
990
|
+
- **阅读器集成 (Reader Integration)**:
|
|
991
|
+
- **无缝访问**: 在“轨道布局”中双击中心节点现在会立即打开`阅读器`,显示完整的节点内容。
|
|
992
|
+
- **数据获取**: 修复了阅读器打开为空的关键问题;现在可以正确地从全局图状态检索完整的元数据。
|
|
993
|
+
- **视觉打磨 (Visual Polish)**:
|
|
994
|
+
- **轨道布局**: 显著改进了节点分散度(半径 350-950px),减少了标签重叠。
|
|
995
|
+
- **边缘清晰度**: 在轨道模式下,严格隐藏未连接到中心节点的边,将视觉混乱减少了 90%。
|
|
996
|
+
- **标签可见性**: 周围节点现在总是显示标签,并根据距离按比例缩放(最大 16px)。
|
|
997
|
+
- **景深 (DoF)**: 调整了不透明度衰减,以确保远处的节点保持可见(最小 0.4 不透明度)。
|
|
998
|
+
- **用户体验改进 (UX Improvements)**:
|
|
999
|
+
- **目标选择**: 将“目标节点”搜索限制从 20 增加到 300,确保用户可以找到图中的任何节点。
|
|
1000
|
+
- **交互层级**: 修复了 `z-index` 层级问题,之前的阅读器窗口被隐藏在路径可视化后面。
|
|
1001
|
+
|
|
1002
|
+
### v1.2.0 - 路径模式与桌面渲染器 (2026-01-23)
|
|
1003
|
+
|
|
1004
|
+
- **路径模式 (Path Mode)**: 引入了一套主要的新功能,用于将图谱转化为线性的学习路径。
|
|
1005
|
+
- **学习模式**: '领域学习' (拓扑排序) 和 '扩散学习' (目标导向)。
|
|
1006
|
+
- **可视化**: 由 D3/Canvas 驱动的全新径向和树状布局。
|
|
1007
|
+
- **策略**: '基础优先' 和 '核心优先' 排序算法。
|
|
1008
|
+
- **混合架构**:
|
|
1009
|
+
- **Godot 桥接**: 实现了 `PathBridge.ts`,通过 WebSocket (端口 9876) 与外部渲染器同步图谱状态。
|
|
1010
|
+
- **原生渲染**: 添加了对 Godot 4.3 的支持,以渲染高保真的 Vulkan 图形 (源码位于 `path_mode/`).
|
|
1011
|
+
- **运维 (DevOps)**:
|
|
1012
|
+
- **NPM 脚本**: 添加了 `pathmode:dev` 和 `pathmode:test` 工作流。
|
|
1013
|
+
- **UI 稳定性**: 修复了径向布局可见性 (`centerView`) 和退出模式逻辑中的关键 Bug。
|
|
1014
|
+
|
|
937
1015
|
### v1.1.2 - 路径解析与 UI 稳定性 (2026-01-23)
|
|
938
1016
|
|
|
939
1017
|
- **后端协议修复**:
|
package/dist/src/core/Graph.js
CHANGED
|
@@ -124,5 +124,89 @@ class Graph {
|
|
|
124
124
|
edges: Array.from(this.adjacencyList.values()).flat()
|
|
125
125
|
};
|
|
126
126
|
}
|
|
127
|
+
/**
|
|
128
|
+
* Gets all predecessor nodes (transitive closure of incoming edges).
|
|
129
|
+
* 获取所有前驱节点(入边的传递闭包)。
|
|
130
|
+
* @param id Target node ID
|
|
131
|
+
*/
|
|
132
|
+
getPredecessors(id) {
|
|
133
|
+
const predecessors = new Set();
|
|
134
|
+
const queue = [id];
|
|
135
|
+
const visited = new Set();
|
|
136
|
+
visited.add(id);
|
|
137
|
+
while (queue.length > 0) {
|
|
138
|
+
const current = queue.shift();
|
|
139
|
+
const incoming = this.getIncomingEdges(current);
|
|
140
|
+
for (const edge of incoming) {
|
|
141
|
+
if (!visited.has(edge.source)) {
|
|
142
|
+
visited.add(edge.source);
|
|
143
|
+
predecessors.add(edge.source);
|
|
144
|
+
queue.push(edge.source);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
return predecessors;
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* Gets all successor nodes (transitive closure of outgoing edges).
|
|
152
|
+
* 获取所有后继节点(出边的传递闭包)。
|
|
153
|
+
* @param id Source node ID
|
|
154
|
+
*/
|
|
155
|
+
getSuccessors(id) {
|
|
156
|
+
const successors = new Set();
|
|
157
|
+
const queue = [id];
|
|
158
|
+
const visited = new Set();
|
|
159
|
+
visited.add(id);
|
|
160
|
+
while (queue.length > 0) {
|
|
161
|
+
const current = queue.shift();
|
|
162
|
+
const outgoing = this.getOutgoingEdges(current);
|
|
163
|
+
for (const edge of outgoing) {
|
|
164
|
+
if (!visited.has(edge.target)) {
|
|
165
|
+
visited.add(edge.target);
|
|
166
|
+
successors.add(edge.target);
|
|
167
|
+
queue.push(edge.target);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
return successors;
|
|
172
|
+
}
|
|
173
|
+
/**
|
|
174
|
+
* Calculates the shortest path between two nodes (unweighted BFS).
|
|
175
|
+
* 计算两个节点之间的最短路径 (无权 BFS)。
|
|
176
|
+
* @param source Source node ID
|
|
177
|
+
* @param target Target node ID
|
|
178
|
+
* @returns Array of node IDs representing the path, or empty if no path found
|
|
179
|
+
*/
|
|
180
|
+
getShortestPath(source, target) {
|
|
181
|
+
if (source === target)
|
|
182
|
+
return [source];
|
|
183
|
+
const queue = [source];
|
|
184
|
+
const visited = new Set();
|
|
185
|
+
const parent = new Map();
|
|
186
|
+
visited.add(source);
|
|
187
|
+
while (queue.length > 0) {
|
|
188
|
+
const current = queue.shift();
|
|
189
|
+
if (current === target)
|
|
190
|
+
break;
|
|
191
|
+
const neighbors = this.getNeighbors(current);
|
|
192
|
+
for (const neighbor of neighbors) {
|
|
193
|
+
if (!visited.has(neighbor)) {
|
|
194
|
+
visited.add(neighbor);
|
|
195
|
+
parent.set(neighbor, current);
|
|
196
|
+
queue.push(neighbor);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
if (!visited.has(target))
|
|
201
|
+
return [];
|
|
202
|
+
// Reconstruct path
|
|
203
|
+
const path = [target];
|
|
204
|
+
let curr = target;
|
|
205
|
+
while (curr !== source) {
|
|
206
|
+
curr = parent.get(curr);
|
|
207
|
+
path.unshift(curr);
|
|
208
|
+
}
|
|
209
|
+
return path;
|
|
210
|
+
}
|
|
127
211
|
}
|
|
128
212
|
exports.Graph = Graph;
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.PathBridge = void 0;
|
|
4
|
+
const ws_1 = require("ws");
|
|
5
|
+
class PathBridge {
|
|
6
|
+
constructor(port = 9876) {
|
|
7
|
+
this.clients = new Set();
|
|
8
|
+
this.port = port;
|
|
9
|
+
this.wss = new ws_1.WebSocketServer({ port });
|
|
10
|
+
console.log(`[PathBridge] WebSocket Server started on port ${port}`);
|
|
11
|
+
this.wss.on('connection', (ws) => {
|
|
12
|
+
console.log('[PathBridge] Client connected');
|
|
13
|
+
this.clients.add(ws);
|
|
14
|
+
ws.on('message', (message) => {
|
|
15
|
+
try {
|
|
16
|
+
const data = JSON.parse(message.toString());
|
|
17
|
+
this.handleMessage(data, ws);
|
|
18
|
+
}
|
|
19
|
+
catch (e) {
|
|
20
|
+
console.error('[PathBridge] Message error:', e);
|
|
21
|
+
}
|
|
22
|
+
});
|
|
23
|
+
ws.on('close', () => {
|
|
24
|
+
console.log('[PathBridge] Client disconnected');
|
|
25
|
+
this.clients.delete(ws);
|
|
26
|
+
});
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
handleMessage(data, sender) {
|
|
30
|
+
// Handle messages from Godot (e.g., node clicks)
|
|
31
|
+
if (data.type === 'nodeClick') {
|
|
32
|
+
console.log(`[PathBridge] Godot clicked node: ${data.payload}`);
|
|
33
|
+
// Broadcast to all clients (including Frontend)
|
|
34
|
+
this.broadcast('nodeClick', data.payload);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
broadcast(type, payload) {
|
|
38
|
+
const msg = JSON.stringify({ type, payload });
|
|
39
|
+
this.clients.forEach(client => {
|
|
40
|
+
if (client.readyState === ws_1.WebSocket.OPEN) {
|
|
41
|
+
client.send(msg);
|
|
42
|
+
}
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
close() {
|
|
46
|
+
this.wss.close();
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
exports.PathBridge = PathBridge;
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.PathEngine = void 0;
|
|
4
|
+
class PathEngine {
|
|
5
|
+
constructor(graph) {
|
|
6
|
+
this.graph = graph;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Domain Learning: Extracts an efficient learning path for a set of nodes (or all nodes).
|
|
10
|
+
* 领域学习:为一组节点(或所有节点)提取高效的学习路径。
|
|
11
|
+
* @param nodeIds Specific nodes to learn (optional, defaults to all)
|
|
12
|
+
* @param strategy prioritization strategy
|
|
13
|
+
*/
|
|
14
|
+
domainLearning(nodeIds, strategy) {
|
|
15
|
+
const targetNodes = nodeIds ? new Set(nodeIds) : new Set(this.graph.getNodes().map(n => n.id));
|
|
16
|
+
// For Domain Learning, we want to learn *everything* in the set.
|
|
17
|
+
// We strictly respect dependencies within the graph.
|
|
18
|
+
// If a node in the set depends on a node OUTSIDE the set, we assume the outside node is already known
|
|
19
|
+
// OR we must add it. Requirement implies "user-defined domain", so usually we strictly stay inside or include prereqs.
|
|
20
|
+
// Let's assume we need to be strictly self-contained or include necessary prerequisites.
|
|
21
|
+
// Safe bet: Extract subgraph of targetNodes + all their ancestors to ensure validity.
|
|
22
|
+
const relevantNodes = this.expandToIncludePrerequisites(targetNodes);
|
|
23
|
+
return this.generateLearningPath(relevantNodes, strategy);
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Diffusion Learning: Extracts shortest learning path to a specific target node.
|
|
27
|
+
* 扩散学习:提取通往特定目标节点的最短学习路径。
|
|
28
|
+
* @param targetId Target node ID
|
|
29
|
+
* @param strategy prioritization strategy for tie-breaking
|
|
30
|
+
*/
|
|
31
|
+
diffusionLearning(targetId, strategy) {
|
|
32
|
+
if (!this.graph.hasNode(targetId)) {
|
|
33
|
+
throw new Error(`Node ${targetId} not found in graph`);
|
|
34
|
+
}
|
|
35
|
+
// 1. Identify all ancestors (prerequisites)
|
|
36
|
+
const ancestors = this.graph.getPredecessors(targetId);
|
|
37
|
+
ancestors.add(targetId);
|
|
38
|
+
// 2. Generate path for this subset
|
|
39
|
+
return this.generateLearningPath(ancestors, strategy);
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Core generation logic using Priority-Queue Topological Sort.
|
|
43
|
+
*/
|
|
44
|
+
generateLearningPath(nodesOfInterest, strategy) {
|
|
45
|
+
const nodes = Array.from(nodesOfInterest).map(id => this.graph.getNode(id));
|
|
46
|
+
const nodeMap = new Map(nodes.map(n => [n.id, n]));
|
|
47
|
+
// Build local in-degrees for the subgraph
|
|
48
|
+
const localInDegree = new Map();
|
|
49
|
+
const localAdjacency = new Map();
|
|
50
|
+
nodes.forEach(node => {
|
|
51
|
+
localInDegree.set(node.id, 0);
|
|
52
|
+
localAdjacency.set(node.id, []);
|
|
53
|
+
});
|
|
54
|
+
// Populate edges restricted to the subgraph
|
|
55
|
+
const relevantEdges = [];
|
|
56
|
+
nodes.forEach(node => {
|
|
57
|
+
const outgoing = this.graph.getOutgoingEdges(node.id);
|
|
58
|
+
outgoing.forEach(edge => {
|
|
59
|
+
if (nodesOfInterest.has(edge.target)) {
|
|
60
|
+
localAdjacency.get(node.id).push(edge.target);
|
|
61
|
+
localInDegree.set(edge.target, (localInDegree.get(edge.target) || 0) + 1);
|
|
62
|
+
relevantEdges.push(edge);
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
});
|
|
66
|
+
// Initialize queue with nodes having 0 in-degree (within subgraph)
|
|
67
|
+
let available = [];
|
|
68
|
+
nodes.forEach(node => {
|
|
69
|
+
if (localInDegree.get(node.id) === 0) {
|
|
70
|
+
available.push(node.id);
|
|
71
|
+
}
|
|
72
|
+
});
|
|
73
|
+
const learnedPath = [];
|
|
74
|
+
const visited = new Set();
|
|
75
|
+
let step = 1;
|
|
76
|
+
// Helper to process a node and unlock neighbors
|
|
77
|
+
const processNode = (currentId) => {
|
|
78
|
+
visited.add(currentId);
|
|
79
|
+
const currentNode = nodeMap.get(currentId);
|
|
80
|
+
// Add to path
|
|
81
|
+
learnedPath.push({
|
|
82
|
+
...currentNode,
|
|
83
|
+
stepOrder: step++,
|
|
84
|
+
isCompleted: false,
|
|
85
|
+
unlocks: localAdjacency.get(currentId)
|
|
86
|
+
});
|
|
87
|
+
// "Unlock" neighbors
|
|
88
|
+
const neighbors = localAdjacency.get(currentId);
|
|
89
|
+
neighbors.forEach(neighborId => {
|
|
90
|
+
// Only decrement if neighbor not visited (avoid double counting in cycles)
|
|
91
|
+
if (!visited.has(neighborId)) {
|
|
92
|
+
const newDegree = (localInDegree.get(neighborId) || 0) - 1;
|
|
93
|
+
localInDegree.set(neighborId, newDegree);
|
|
94
|
+
if (newDegree <= 0) { // Changed to <= 0 to be robust against negative logic errors
|
|
95
|
+
// Check if already in available to prevent duplicates
|
|
96
|
+
if (!available.includes(neighborId)) {
|
|
97
|
+
available.push(neighborId);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
});
|
|
102
|
+
};
|
|
103
|
+
while (learnedPath.length < nodes.length) {
|
|
104
|
+
if (available.length > 0) {
|
|
105
|
+
// Normal Topological Sort Step
|
|
106
|
+
available.sort((a, b) => this.compareNodes(a, b, strategy));
|
|
107
|
+
const currentId = available.shift();
|
|
108
|
+
if (!visited.has(currentId)) {
|
|
109
|
+
processNode(currentId);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
else {
|
|
113
|
+
// Cycle Detected or Disconnected Components with strict dependencies
|
|
114
|
+
// Strategy: Break cycle by picking the "best" remaining node
|
|
115
|
+
// Prioritize nodes with HIGHEST Out-Degree (unlocks the most)
|
|
116
|
+
// or lowest remaining in-degree?
|
|
117
|
+
// "Lowest In-Degree" is usually the best heuristic for Feedback Arc Set.
|
|
118
|
+
// Find remaining nodes
|
|
119
|
+
const remainingIds = [];
|
|
120
|
+
nodes.forEach(n => {
|
|
121
|
+
if (!visited.has(n.id))
|
|
122
|
+
remainingIds.push(n.id);
|
|
123
|
+
});
|
|
124
|
+
if (remainingIds.length === 0)
|
|
125
|
+
break; // Done
|
|
126
|
+
// Sort by In-Degree (Ascending) -> Strategy Score (Desc)
|
|
127
|
+
remainingIds.sort((a, b) => {
|
|
128
|
+
const degA = localInDegree.get(a) || 0;
|
|
129
|
+
const degB = localInDegree.get(b) || 0;
|
|
130
|
+
if (degA !== degB)
|
|
131
|
+
return degA - degB;
|
|
132
|
+
return this.compareNodes(a, b, strategy);
|
|
133
|
+
});
|
|
134
|
+
const forceId = remainingIds[0];
|
|
135
|
+
// Force process strict dependency validation
|
|
136
|
+
localInDegree.set(forceId, 0); // Pretend it's free
|
|
137
|
+
processNode(forceId);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
return {
|
|
141
|
+
nodes: learnedPath,
|
|
142
|
+
edges: relevantEdges,
|
|
143
|
+
strategy,
|
|
144
|
+
coverage: learnedPath.length / nodes.length
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* Helper to ensure valid learning set (closure of predecessors).
|
|
149
|
+
*/
|
|
150
|
+
expandToIncludePrerequisites(initialNodes) {
|
|
151
|
+
const result = new Set(initialNodes);
|
|
152
|
+
let changed = true;
|
|
153
|
+
// Iteratively add parents until stable
|
|
154
|
+
// Optimally we just merge getPredecessors for all nodes
|
|
155
|
+
// But getPredecessors returns full closure, so we only need to do it once per node.
|
|
156
|
+
for (const nodeId of initialNodes) {
|
|
157
|
+
const preds = this.graph.getPredecessors(nodeId);
|
|
158
|
+
preds.forEach(p => result.add(p));
|
|
159
|
+
}
|
|
160
|
+
return result;
|
|
161
|
+
}
|
|
162
|
+
/**
|
|
163
|
+
* Strategy Comparator
|
|
164
|
+
* Returns negative if A is better than B (for sorting A before B).
|
|
165
|
+
*/
|
|
166
|
+
compareNodes(idA, idB, strategy) {
|
|
167
|
+
const nodeA = this.graph.getNode(idA);
|
|
168
|
+
const nodeB = this.graph.getNode(idB);
|
|
169
|
+
// Primary Metric: Strategy Score
|
|
170
|
+
const scoreA = this.calculateScore(nodeA, strategy);
|
|
171
|
+
const scoreB = this.calculateScore(nodeB, strategy);
|
|
172
|
+
if (scoreA !== scoreB) {
|
|
173
|
+
return scoreB - scoreA; // Higher score first
|
|
174
|
+
}
|
|
175
|
+
// Tie-breaker: ID (stable sort)
|
|
176
|
+
return idA.localeCompare(idB);
|
|
177
|
+
}
|
|
178
|
+
calculateScore(node, strategy) {
|
|
179
|
+
// Avoid division by zero
|
|
180
|
+
const safeInDegree = node.inDegree + 1;
|
|
181
|
+
if (strategy === 'foundational') {
|
|
182
|
+
// Foundational: Low In-Degree (Global), High Out-Degree (Global)
|
|
183
|
+
// "Low in-degree yet highly correlated with other required nodes (out-degree)"
|
|
184
|
+
// Score = OutDegree / InDegree
|
|
185
|
+
return (node.outDegree + 0.1) / safeInDegree;
|
|
186
|
+
}
|
|
187
|
+
else {
|
|
188
|
+
// Core: High Centrality, Low In-Degree (in learning set context)
|
|
189
|
+
// Note: In-degree in context is already 0 (since they are in 'available' list).
|
|
190
|
+
// So we use Global Centrality as the main differentiator.
|
|
191
|
+
// "Highly correlated (Centrality) ... low in-degree (Global)"
|
|
192
|
+
return (node.centrality || 0) * 10 - node.inDegree;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
exports.PathEngine = PathEngine;
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const Graph_1 = require("./Graph");
|
|
4
|
+
const PathEngine_1 = require("./PathEngine");
|
|
5
|
+
describe('PathEngine', () => {
|
|
6
|
+
let graph;
|
|
7
|
+
let engine;
|
|
8
|
+
beforeEach(() => {
|
|
9
|
+
graph = new Graph_1.Graph();
|
|
10
|
+
engine = new PathEngine_1.PathEngine(graph);
|
|
11
|
+
});
|
|
12
|
+
const createNode = (id, inDegree = 0, outDegree = 0, centrality = 0) => ({
|
|
13
|
+
id,
|
|
14
|
+
label: id,
|
|
15
|
+
inDegree,
|
|
16
|
+
outDegree,
|
|
17
|
+
centrality
|
|
18
|
+
});
|
|
19
|
+
test('Domain Learning: Simple Linear Chain', () => {
|
|
20
|
+
// A -> B -> C
|
|
21
|
+
graph.addNode(createNode('A'));
|
|
22
|
+
graph.addNode(createNode('B'));
|
|
23
|
+
graph.addNode(createNode('C'));
|
|
24
|
+
graph.addEdge('A', 'B');
|
|
25
|
+
graph.addEdge('B', 'C');
|
|
26
|
+
const result = engine.domainLearning(null, 'foundational');
|
|
27
|
+
expect(result.nodes.map(n => n.id)).toEqual(['A', 'B', 'C']);
|
|
28
|
+
expect(result.coverage).toBe(1);
|
|
29
|
+
});
|
|
30
|
+
test('Domain Learning: Branching with Strategy', () => {
|
|
31
|
+
// A -> B
|
|
32
|
+
// A -> C
|
|
33
|
+
// B and C are available after A.
|
|
34
|
+
// Make B "Foundational" (high out-degree) and C "Core" (high centrality)
|
|
35
|
+
graph.addNode(createNode('A', 0, 2, 0.1));
|
|
36
|
+
graph.addNode(createNode('B', 1, 5, 0.2)); // High out-degree
|
|
37
|
+
graph.addNode(createNode('C', 1, 0, 0.9)); // High centrality
|
|
38
|
+
graph.addEdge('A', 'B');
|
|
39
|
+
graph.addEdge('A', 'C');
|
|
40
|
+
// Foundational should pick B first (High Out / Low In)
|
|
41
|
+
const resFoundational = engine.domainLearning(null, 'foundational');
|
|
42
|
+
expect(resFoundational.nodes.slice(1).map(n => n.id)).toEqual(['B', 'C']);
|
|
43
|
+
// Core should pick C first (High Centrality)
|
|
44
|
+
const resCore = engine.domainLearning(null, 'core');
|
|
45
|
+
expect(resCore.nodes.slice(1).map(n => n.id)).toEqual(['C', 'B']);
|
|
46
|
+
});
|
|
47
|
+
test('Diffusion Learning: Prerequisites Extraction', () => {
|
|
48
|
+
// A -> B -> Target
|
|
49
|
+
// X -> Y (unrelated)
|
|
50
|
+
graph.addNode(createNode('A'));
|
|
51
|
+
graph.addNode(createNode('B'));
|
|
52
|
+
graph.addNode(createNode('Target'));
|
|
53
|
+
graph.addNode(createNode('X'));
|
|
54
|
+
graph.addNode(createNode('Y'));
|
|
55
|
+
graph.addEdge('A', 'B');
|
|
56
|
+
graph.addEdge('B', 'Target');
|
|
57
|
+
graph.addEdge('X', 'Y');
|
|
58
|
+
const result = engine.diffusionLearning('Target', 'foundational');
|
|
59
|
+
expect(result.nodes.length).toBe(3);
|
|
60
|
+
expect(result.nodes.map(n => n.id)).toContain('A');
|
|
61
|
+
expect(result.nodes.map(n => n.id)).toContain('B');
|
|
62
|
+
expect(result.nodes.map(n => n.id)).toContain('Target');
|
|
63
|
+
expect(result.nodes.map(n => n.id)).not.toContain('X');
|
|
64
|
+
});
|
|
65
|
+
test('Complex Prerequisite Chain', () => {
|
|
66
|
+
// A
|
|
67
|
+
// / \
|
|
68
|
+
// B C
|
|
69
|
+
// \ /
|
|
70
|
+
// D
|
|
71
|
+
graph.addNode(createNode('A'));
|
|
72
|
+
graph.addNode(createNode('B'));
|
|
73
|
+
graph.addNode(createNode('C'));
|
|
74
|
+
graph.addNode(createNode('D'));
|
|
75
|
+
graph.addEdge('A', 'B');
|
|
76
|
+
graph.addEdge('A', 'C');
|
|
77
|
+
graph.addEdge('B', 'D');
|
|
78
|
+
graph.addEdge('C', 'D');
|
|
79
|
+
const result = engine.domainLearning(null, 'foundational');
|
|
80
|
+
const order = result.nodes.map(n => n.id);
|
|
81
|
+
expect(order[0]).toBe('A');
|
|
82
|
+
expect(order[3]).toBe('D');
|
|
83
|
+
expect(order.includes('B')).toBeTruthy();
|
|
84
|
+
expect(order.includes('C')).toBeTruthy();
|
|
85
|
+
});
|
|
86
|
+
});
|
|
@@ -37,7 +37,9 @@ const electron_1 = require("electron");
|
|
|
37
37
|
const path = __importStar(require("path"));
|
|
38
38
|
const url = __importStar(require("url"));
|
|
39
39
|
const controller_1 = require("../backend/controller");
|
|
40
|
+
const PathBridge_1 = require("../core/PathBridge");
|
|
40
41
|
let mainWindow = null;
|
|
42
|
+
let pathBridge = null;
|
|
41
43
|
// Knowledge Base Path Management
|
|
42
44
|
const DEFAULT_KB_PATH = path.join(process.cwd(), 'Knowledge_Base');
|
|
43
45
|
let currentKbRoot = DEFAULT_KB_PATH;
|
|
@@ -329,6 +331,18 @@ const createWindow = async () => {
|
|
|
329
331
|
};
|
|
330
332
|
electron_1.app.whenReady().then(async () => {
|
|
331
333
|
log('App Ready');
|
|
334
|
+
// Initialize Path Bridge (WebSocket)
|
|
335
|
+
try {
|
|
336
|
+
pathBridge = new PathBridge_1.PathBridge(9876);
|
|
337
|
+
log('PathBridge initialized on port 9876');
|
|
338
|
+
}
|
|
339
|
+
catch (e) {
|
|
340
|
+
log(`Failed to initialize PathBridge: ${e}`);
|
|
341
|
+
}
|
|
342
|
+
electron_1.app.on('before-quit', () => {
|
|
343
|
+
if (pathBridge)
|
|
344
|
+
pathBridge.close();
|
|
345
|
+
});
|
|
332
346
|
// Suppress security warnings in dev mode (unsafe-eval is required for GPU.js)
|
|
333
347
|
process.env['ELECTRON_DISABLE_SECURITY_WARNINGS'] = 'true';
|
|
334
348
|
// Load saved knowledge base path or show first-run setup
|