@jacklika/dsh-tool-memory-graph 0.1.0-alpha.2 → 0.1.0-alpha.4

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`. 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, backlinks }], edges: [{ from, to }] }`.
55
55
 
56
56
  -----
57
57
 
@@ -62,7 +62,7 @@ The package is a single Cordis function plugin that registers `wiki_graph` on `c
62
62
 
63
63
  #### What the model sees
64
64
 
65
- The model sees the generated `wiki_graph` schema in the [tool catalog](../../../docs/tool-catalog.md#deepseek-aidsh-tool-memory-graph). The description states that the graph is read-only and derived from `[[link]]` references.
65
+ The model sees the generated `wiki_graph` schema. The description states that the graph is read-only and derived from `[[link]]` references.
66
66
 
67
67
  ##### Verbatim description for `wiki_graph`
68
68
 
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]]` 目标。先用广度优先遍历确定节点集合,再从所有已选节点收集边,因此边界节点的出边不会丢失。输出为 `{ 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, backlinks }], edges: [{ from, to }] }`。
55
55
 
56
56
  -----
57
57
 
@@ -62,7 +62,7 @@ kind: "package-reference"
62
62
 
63
63
  #### 模型可见内容
64
64
 
65
- 模型在[工具目录](../../../docs/tool-catalog.zh.md#deepseek-aidsh-tool-memory-graph)中看到生成的 `wiki_graph` schema。描述声明该图是只读的,由 `[[link]]` 引用推导。
65
+ 模型看到生成的 `wiki_graph` schema。描述声明该图是只读的,由 `[[link]]` 引用推导。
66
66
 
67
67
  ##### `wiki_graph` 的原文描述
68
68
 
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, 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. */
@@ -42,8 +42,11 @@ async function noteTitle(absolutePath, id) {
42
42
  async function buildGraph(root, resolved) {
43
43
  const paths = await listNotePaths(root, resolved.extensions, resolved.indexHiddenDirs);
44
44
  const entries = paths.map(path => [vaultRelativeId(root, path), path]);
45
+ const linkResolver = createLinkResolver(entries.map(([id]) => id));
45
46
  const truncated = entries.length > resolved.maxNodes;
46
- const capped = entries.slice(0, resolved.maxNodes);
47
+ // `listNotePaths` follows readdir order, so the node cap would otherwise keep
48
+ // a filesystem-dependent subset of the vault.
49
+ const capped = [...entries].sort((a, b) => codeUnitCompare(a[0], b[0])).slice(0, resolved.maxNodes);
47
50
  const nodes = [];
48
51
  for (const [id, path] of capped)
49
52
  nodes.push({ id, title: await noteTitle(path, id) });
@@ -52,7 +55,7 @@ async function buildGraph(root, resolved) {
52
55
  for (const [id, path] of capped) {
53
56
  const text = await readFile(path, 'utf8');
54
57
  for (const link of extractLinks(text)) {
55
- const targetPath = await resolveLinkTarget(root, resolved.extensions, link);
58
+ const targetPath = await resolveLinkTarget(root, resolved.extensions, link, linkResolver);
56
59
  if (targetPath === undefined)
57
60
  continue;
58
61
  const targetId = vaultRelativeId(root, targetPath);
@@ -73,19 +76,23 @@ async function buildGraph(root, resolved) {
73
76
  */
74
77
  async function buildSubgraph(root, resolved, startId, depth) {
75
78
  const startPath = await containedPathReal(root, startId);
79
+ const linkResolver = createLinkResolver((await listNotePaths(root, resolved.extensions, resolved.indexHiddenDirs))
80
+ .map(path => vaultRelativeId(root, path)));
76
81
  const visited = new Map([[vaultRelativeId(root, startPath), startPath]]);
82
+ const levels = new Map([[vaultRelativeId(root, startPath), 0]]);
77
83
  let frontier = [startPath];
78
84
  for (let level = 0; level < depth && frontier.length > 0; level += 1) {
79
85
  const next = [];
80
86
  for (const path of frontier) {
81
87
  const text = await readFile(path, 'utf8');
82
88
  for (const link of extractLinks(text)) {
83
- const targetPath = await resolveLinkTarget(root, resolved.extensions, link);
89
+ const targetPath = await resolveLinkTarget(root, resolved.extensions, link, linkResolver);
84
90
  if (targetPath === undefined)
85
91
  continue;
86
92
  const toId = vaultRelativeId(root, targetPath);
87
93
  if (!visited.has(toId)) {
88
94
  visited.set(toId, targetPath);
95
+ levels.set(toId, level + 1);
89
96
  next.push(targetPath);
90
97
  }
91
98
  }
@@ -93,7 +100,12 @@ async function buildSubgraph(root, resolved, startId, depth) {
93
100
  frontier = next;
94
101
  }
95
102
  const truncated = visited.size > resolved.maxNodes;
96
- const ids = [...visited.keys()].slice(0, resolved.maxNodes);
103
+ // Truncate by distance from the center, then by id: the neighborhood closest
104
+ // to the requested note survives the cap, and which notes those are does not
105
+ // depend on readdir or link order.
106
+ const ids = [...visited.keys()]
107
+ .sort((a, b) => (levels.get(a) ?? 0) - (levels.get(b) ?? 0) || codeUnitCompare(a, b))
108
+ .slice(0, resolved.maxNodes);
97
109
  const idSet = new Set(ids);
98
110
  const edges = [];
99
111
  const nodes = [];
@@ -102,7 +114,7 @@ async function buildSubgraph(root, resolved, startId, depth) {
102
114
  nodes.push({ id, title: await noteTitle(path, id) });
103
115
  const text = await readFile(path, 'utf8');
104
116
  for (const link of extractLinks(text)) {
105
- const targetPath = await resolveLinkTarget(root, resolved.extensions, link);
117
+ const targetPath = await resolveLinkTarget(root, resolved.extensions, link, linkResolver);
106
118
  if (targetPath === undefined)
107
119
  continue;
108
120
  const toId = vaultRelativeId(root, targetPath);
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.2",
4
+ "version": "0.1.0-alpha.4",
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.4"
35
+ "@jacklika/dsh-tool-memory-filesystem": "^0.1.3-alpha.8"
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
  }