@jacklika/dsh-tool-memory-graph 0.1.0-alpha.3 → 0.1.0-alpha.5

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 CHANGED
@@ -51,7 +51,7 @@ It resolves the same vault root per call as `tool-memory-filesystem`: an explici
51
51
  <a id="understand-the-implementation"></a>
52
52
  ## Understand the implementation
53
53
 
54
- The package is a single Cordis function plugin that registers `wiki_graph` on `ctx.tools`. Per call it resolves the vault root through the shared `resolveMemoryVaultRoot` helper, lists notes through the shared `listNotePaths` walker, and extracts `[[link]]` targets through `extractLinks`/`resolveLinkTarget`. Targets resolve with the same Obsidian-style tiers as `wiki_read` — exact vault-relative path, then path suffix, then basename anywhere in the vault, with deterministic ambiguity handling. A breadth-first walk selects the node set first; edges are then collected from every selected node so boundary-node outgoing edges are not dropped. Output is `{ nodes: [{ id, title, backlinks }], edges: [{ from, to }] }`.
54
+ The package is a single Cordis function plugin that registers `wiki_graph` on `ctx.tools`. Per call it resolves the vault root through the shared `resolveMemoryVaultRoot` helper, lists notes through the shared `listNotePaths` walker, and extracts `[[link]]` targets through `extractLinks`/`resolveLinkTarget`. Targets resolve with the same Obsidian-style tiers as `wiki_read` — exact vault-relative path, then path suffix, then basename anywhere in the vault, with deterministic ambiguity handling. A breadth-first walk selects the node set first; edges are then collected from every selected node so boundary-node outgoing edges are not dropped. When the node set exceeds `maxNodes`, a whole-vault graph keeps the lowest ids in code-unit order (the ordering `tool-memory-filesystem` tie-breaks with) and a subgraph keeps the notes nearest the center — BFS level first, then id — so which notes survive the cap never depends on directory-listing order and a subgraph never drops its own center. Output is `{ nodes: [{ id, title }], edges: [{ from, to }], truncated: boolean }`.
55
55
 
56
56
  -----
57
57
 
@@ -83,7 +83,7 @@ Independent. The plugin only supplies tool results; it does not change the reque
83
83
  <a id="known-limitations-and-deferred-work"></a>
84
84
 
85
85
  - **No visualization** — the tool returns JSON nodes and edges; rendering (Obsidian graph view, Mermaid) is left to consumers.
86
- - **Backlinks reflect the whole vault** — node `backlinks` counts are computed over all notes, not just the selected subgraph.
86
+ - **Edges are scoped to selected nodes** — only links whose source and target both survive the `maxNodes` cap are returned, so boundary links outside the selected set are omitted.
87
87
 
88
88
  <a id="dev-note"></a>
89
89
  ### Dev Note
package/README.zh.md CHANGED
@@ -51,7 +51,7 @@ kind: "package-reference"
51
51
  <a id="understand-the-implementation"></a>
52
52
  ## 实现说明
53
53
 
54
- 本包是单个 Cordis 函数插件,在 `ctx.tools` 上注册 `wiki_graph`。每次调用通过共享的 `resolveMemoryVaultRoot` 解析仓库根,用 `listNotePaths` 遍历笔记,用 `extractLinks`/`resolveLinkTarget` 提取 `[[link]]` 目标。目标按与 `wiki_read` 相同的 Obsidian 语义解析——先精确仓库相对路径,再路径后缀,最后全仓库 basename 匹配,多重命中按确定性规则选择。先用广度优先遍历确定节点集合,再从所有已选节点收集边,因此边界节点的出边不会丢失。输出为 `{ nodes: [{ id, title, backlinks }], edges: [{ from, to }] }`。
54
+ 本包是单个 Cordis 函数插件,在 `ctx.tools` 上注册 `wiki_graph`。每次调用通过共享的 `resolveMemoryVaultRoot` 解析仓库根,用 `listNotePaths` 遍历笔记,用 `extractLinks`/`resolveLinkTarget` 提取 `[[link]]` 目标。目标按与 `wiki_read` 相同的 Obsidian 语义解析——先精确仓库相对路径,再路径后缀,最后全仓库 basename 匹配,多重命中按确定性规则选择。先用广度优先遍历确定节点集合,再从所有已选节点收集边,因此边界节点的出边不会丢失。节点集合超过 `maxNodes` 时,全仓库图保留码点序最小的若干 id(与 `tool-memory-filesystem` 的并列排序规则一致),子图则保留离中心最近的笔记——先按 BFS 层级、再按 id——因此保留哪些笔记不依赖目录列举顺序,子图也不会丢掉自己的中心节点。输出为 `{ nodes: [{ id, title }], edges: [{ from, to }], truncated: boolean }`。
55
55
 
56
56
  -----
57
57
 
@@ -83,7 +83,7 @@ Return the Obsidian-style [[link]] graph of the wiki vault: note ids, titles, an
83
83
  <a id="known-limitations-and-deferred-work"></a>
84
84
 
85
85
  - **无可视化** —— 工具返回 JSON 节点与边;渲染(Obsidian 图谱、Mermaid)由消费方负责。
86
- - **backlinks 基于全仓库统计** —— 节点的 `backlinks` 计数覆盖全部笔记,而非仅选中子图。
86
+ - **边限于已选节点** —— 只有两端都在 `maxNodes` 截断后保留下来的节点之间的链接才会出现,子图边界外的出边被省略。
87
87
 
88
88
  <a id="dev-note"></a>
89
89
  ### 开发备注
package/lib/index.js CHANGED
@@ -8,7 +8,7 @@
8
8
  import { readFile } from 'node:fs/promises';
9
9
  import z from '@deepseek-ai/schemastery';
10
10
  import { defineTool } from '@deepseek-ai/dsh-tools';
11
- import { containedPathReal, createLinkResolver, extractLinks, listNotePaths, resolveLinkTarget, resolveMemoryVaultRoot, splitFrontmatter, vaultRelativeId, } from '@jacklika/dsh-tool-memory-filesystem';
11
+ import { codeUnitCompare, containedPathReal, createLinkResolver, extractLinks, listNotePaths, resolveLinkTarget, resolveMemoryVaultRoot, splitFrontmatter, vaultRelativeId, } from '@jacklika/dsh-tool-memory-filesystem';
12
12
  /** Cordis plugin name used by loader diagnostics. */
13
13
  export const name = 'tool-memory-graph';
14
14
  /** Services required by the wiki graph tool. */
@@ -28,7 +28,10 @@ function assertPositiveInteger(name, value) {
28
28
  }
29
29
  async function noteTitle(absolutePath, id) {
30
30
  const text = await readFile(absolutePath, 'utf8');
31
- const { body } = splitFrontmatter(text);
31
+ const { frontmatter, body } = splitFrontmatter(text);
32
+ const explicitTitle = typeof frontmatter.title === 'string' ? frontmatter.title.trim() : '';
33
+ if (explicitTitle !== '')
34
+ return explicitTitle;
32
35
  const heading = /^#\s+(.+)$/m.exec(body);
33
36
  return heading?.[1]?.trim() ?? id.replace(/\.[^.]+$/, '');
34
37
  }
@@ -44,7 +47,9 @@ async function buildGraph(root, resolved) {
44
47
  const entries = paths.map(path => [vaultRelativeId(root, path), path]);
45
48
  const linkResolver = createLinkResolver(entries.map(([id]) => id));
46
49
  const truncated = entries.length > resolved.maxNodes;
47
- const capped = entries.slice(0, resolved.maxNodes);
50
+ // `listNotePaths` follows readdir order, so the node cap would otherwise keep
51
+ // a filesystem-dependent subset of the vault.
52
+ const capped = [...entries].sort((a, b) => codeUnitCompare(a[0], b[0])).slice(0, resolved.maxNodes);
48
53
  const nodes = [];
49
54
  for (const [id, path] of capped)
50
55
  nodes.push({ id, title: await noteTitle(path, id) });
@@ -77,6 +82,7 @@ async function buildSubgraph(root, resolved, startId, depth) {
77
82
  const linkResolver = createLinkResolver((await listNotePaths(root, resolved.extensions, resolved.indexHiddenDirs))
78
83
  .map(path => vaultRelativeId(root, path)));
79
84
  const visited = new Map([[vaultRelativeId(root, startPath), startPath]]);
85
+ const levels = new Map([[vaultRelativeId(root, startPath), 0]]);
80
86
  let frontier = [startPath];
81
87
  for (let level = 0; level < depth && frontier.length > 0; level += 1) {
82
88
  const next = [];
@@ -89,6 +95,7 @@ async function buildSubgraph(root, resolved, startId, depth) {
89
95
  const toId = vaultRelativeId(root, targetPath);
90
96
  if (!visited.has(toId)) {
91
97
  visited.set(toId, targetPath);
98
+ levels.set(toId, level + 1);
92
99
  next.push(targetPath);
93
100
  }
94
101
  }
@@ -96,7 +103,12 @@ async function buildSubgraph(root, resolved, startId, depth) {
96
103
  frontier = next;
97
104
  }
98
105
  const truncated = visited.size > resolved.maxNodes;
99
- const ids = [...visited.keys()].slice(0, resolved.maxNodes);
106
+ // Truncate by distance from the center, then by id: the neighborhood closest
107
+ // to the requested note survives the cap, and which notes those are does not
108
+ // depend on readdir or link order.
109
+ const ids = [...visited.keys()]
110
+ .sort((a, b) => (levels.get(a) ?? 0) - (levels.get(b) ?? 0) || codeUnitCompare(a, b))
111
+ .slice(0, resolved.maxNodes);
100
112
  const idSet = new Set(ids);
101
113
  const edges = [];
102
114
  const nodes = [];
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@jacklika/dsh-tool-memory-graph",
3
3
  "description": "Model-facing link-graph tool over a Markdown memory vault: returns note nodes and [[link]] edges, or the subgraph reachable from one note.",
4
- "version": "0.1.0-alpha.3",
4
+ "version": "0.1.0-alpha.5",
5
5
  "publishConfig": {
6
6
  "access": "public"
7
7
  },
@@ -32,11 +32,11 @@
32
32
  "license": "MIT",
33
33
  "dependencies": {
34
34
  "@deepseek-ai/schemastery": "3.18.4",
35
- "@jacklika/dsh-tool-memory-filesystem": "^0.1.3-alpha.5"
35
+ "@jacklika/dsh-tool-memory-filesystem": "^0.1.3-alpha.9"
36
36
  },
37
37
  "peerDependencies": {
38
38
  "@deepseek-ai/cordis": "4.0.4",
39
- "@deepseek-ai/dsh-tools": ">=0.1.7-rc.1 <0.3.0-0",
40
- "@deepseek-ai/dsh-util-values": ">=0.1.7-rc.1 <0.3.0-0"
39
+ "@deepseek-ai/dsh-tools": ">=0.1.7-rc.1 <0.3.0-0 || >=0.2.0-rc.1 <0.3.0-0",
40
+ "@deepseek-ai/dsh-util-values": ">=0.1.7-rc.1 <0.3.0-0 || >=0.2.0-rc.1 <0.3.0-0"
41
41
  }
42
42
  }