@jacklika/dsh-tool-memory-graph 0.1.0-alpha.7 → 0.1.0-alpha.8

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
@@ -40,19 +40,20 @@ It resolves the same vault root per call as `tool-memory-filesystem`: an explici
40
40
  | `extensions` | `string[]` | `['.md']` | File extensions treated as notes. |
41
41
  | `indexHiddenDirs` | `boolean` | `false` | Index directories whose names start with `.` (`.git` and `node_modules` stay excluded). Enable when `vaultRoot` points at the workspace root so `.dsh/memory/` notes are included. |
42
42
  | `maxDepth` | `number` | `1` | Maximum BFS depth when a center id is given. |
43
+ | `maxDepthLimit` | `number` | `20` | Hard upper bound for the effective `depth`. A call asking for more is clamped to this value, and the subgraph reports the depth actually traversed. Must be `>= maxDepth`. |
43
44
  | `maxNodes` | `number` | `200` | Maximum nodes returned in one graph. |
44
45
  | `maxEdges` | `number` | `1000` | Maximum edges returned in one graph. |
45
46
 
46
47
  ### Tools
47
48
 
48
- - `wiki_graph(id?, depth?)` — without `id`, return every note and directed edge in the vault. With `id`, return the reachable subgraph within `depth` hops; edges from boundary nodes are included when both endpoints are selected. Edges carry `type` (`link` or `keyword`), `keywords` (for `keyword` edges), and `weight` (combined term frequency). Output is `{ nodes, edges, truncated, edgesTruncated }`: `truncated` reports the `maxNodes` cut and `edgesTruncated` the `maxEdges` cut, kept separate so a complete node set is not misread as a complete graph.
49
+ - `wiki_graph(id?, depth?)` — without `id`, return every note and directed edge in the vault. With `id`, return the reachable subgraph within `depth` hops (clamped to `maxDepthLimit`); edges from boundary nodes are included when both endpoints are selected. Edges carry `type` (`link` or `keyword`), `keywords` (for `keyword` edges), and `weight` (combined term frequency). Output is `{ nodes, edges, truncated, edgesTruncated, depth? }`: `truncated` reports the `maxNodes` cut and `edgesTruncated` the `maxEdges` cut, kept separate so a complete node set is not misread as a complete graph, and a subgraph result also reports the effective `depth` after clamping.
49
50
 
50
51
  -----
51
52
 
52
53
  <a id="understand-the-implementation"></a>
53
54
  ## Understand the implementation
54
55
 
55
- 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. In addition to explicit links, the tool extracts significant terms from each note (frontmatter `tags` ×3, frontmatter `title` ×2, body terms ×1) and creates `keyword` edges between any two notes that share one or more terms. A breadth-first walk with a visited set (so cyclical link graphs terminate) follows both `link` and `keyword` edges to select 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. Because `wiki_graph` recomputes from disk on every call, rewriting a note removes stale `keyword` edges and creates new ones for the terms that survive or appear in the new text. Edges are capped at `maxEdges` after node selection: explicit `link` edges first, then `keyword` edges by descending weight, with a deterministic id tie-break, so the surviving set never depends on directory-listing order. Requesting a center id that is not an indexed note fails with `note not found`, and an unreadable note is skipped (logged as a warning) instead of failing the call. Output is `{ nodes: [{ id, title }], edges: [{ from, to, type, keywords?, weight? }], truncated: boolean, edgesTruncated: boolean }`, where `truncated` covers the node cap and `edgesTruncated` the edge cap.
56
+ 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. In addition to explicit links, the tool extracts significant terms from each note (frontmatter `tags` ×3, frontmatter `title` ×2, body terms ×1) and creates `keyword` edges between any two notes that share one or more terms. A breadth-first walk with a visited set (so cyclical link graphs terminate) follows both `link` and `keyword` edges to select 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. Because `wiki_graph` recomputes from disk on every call, rewriting a note removes stale `keyword` edges and creates new ones for the terms that survive or appear in the new text. Edges are capped at `maxEdges` after node selection: explicit `link` edges first, then `keyword` edges by descending weight, with a deterministic id tie-break, so the surviving set never depends on directory-listing order. The requested `depth` is clamped to `maxDepthLimit` before the walk — a configuration-time bound that must be at least `maxDepth` — and a subgraph result reports the effective `depth` it traversed, so a caller cannot force an unbounded walk and can still tell how far the returned result reaches. Requesting a center id that is not an indexed note fails with `note not found`, and an unreadable note is skipped (logged as a warning) instead of failing the call. Output is `{ nodes: [{ id, title }], edges: [{ from, to, type, keywords?, weight? }], truncated: boolean, edgesTruncated: boolean, depth?: number }`, where `truncated` covers the node cap, `edgesTruncated` the edge cap, and `depth` is present on subgraph results.
56
57
 
57
58
  -----
58
59
 
@@ -68,12 +69,12 @@ The model sees the generated `wiki_graph` schema. The description states that th
68
69
  ##### Verbatim description for `wiki_graph`
69
70
 
70
71
  ```markdown
71
- Return the link and keyword graph of the wiki vault: note ids, titles, and directed edges. Explicit `[[link]]` edges are type `link`; edges derived from shared significant terms are type `keyword` with `keywords` and `weight`. Without an id it returns the whole vault graph (node-capped); with an id it returns the subgraph reachable from that note within the given depth. Nodes are capped at maxNodes (`truncated`) and edges at maxEdges (`edgesTruncated`).
72
+ Return the link and keyword graph of the wiki vault: note ids, titles, and directed edges. Explicit `[[link]]` edges are type `link`; edges derived from shared significant terms are type `keyword` with `keywords` and `weight`. Without an id it returns the whole vault graph (node-capped); with an id it returns the subgraph reachable from that note within the given depth. Nodes are capped at maxNodes (`truncated`) and edges at maxEdges (`edgesTruncated`); a subgraph result also reports the effective `depth` after clamping the request to maxDepthLimit.
72
73
  ```
73
74
 
74
75
  #### Token effect
75
76
 
76
- A whole-vault call returns every note and edge; large vaults can add many tokens. Center-id subgraph calls bound output by `depth`.
77
+ A whole-vault call returns every note and edge; large vaults can add many tokens. Center-id subgraph calls bound output by `depth`, and `maxDepthLimit` caps how large that bound may grow.
77
78
 
78
79
  #### KV Cache effect
79
80
 
package/README.zh.md CHANGED
@@ -40,19 +40,20 @@ kind: "package-reference"
40
40
  | `extensions` | `string[]` | `['.md']` | 视为笔记的文件扩展名。 |
41
41
  | `indexHiddenDirs` | `boolean` | `false` | 是否索引以 `.` 开头的目录(`.git` 与 `node_modules` 始终排除)。当 `vaultRoot` 指向工作区根时开启,以纳入 `.dsh/memory/`。 |
42
42
  | `maxDepth` | `number` | `1` | 指定中心笔记时 BFS 的最大深度。 |
43
+ | `maxDepthLimit` | `number` | `20` | 生效 `depth` 的硬上限。请求更大值时会被钳制到该值,子图报告实际遍历的深度。必须 `>= maxDepth`。 |
43
44
  | `maxNodes` | `number` | `200` | 单次图查询返回的最大节点数。 |
44
45
  | `maxEdges` | `number` | `1000` | 单次图查询返回的最大边数。 |
45
46
 
46
47
  ### 工具
47
48
 
48
- - `wiki_graph(id?, depth?)` —— 不传 `id` 时返回全仓库的节点与有向边;传 `id` 时返回 `depth` 跳内的可达子图,两个端点都被选中时边界节点的出边也会包含。边携带 `type`(`link` 或 `keyword`)、`keywords`(仅 `keyword` 边)和 `weight`(合并词频)。输出为 `{ nodes, edges, truncated, edgesTruncated }`:`truncated` 对应 `maxNodes` 截断,`edgesTruncated` 对应 `maxEdges` 截断,两者分开报告,节点完整不会被误读为整图完整。
49
+ - `wiki_graph(id?, depth?)` —— 不传 `id` 时返回全仓库的节点与有向边;传 `id` 时返回 `depth` 跳内的可达子图(`depth` 会被钳制到 `maxDepthLimit`),两个端点都被选中时边界节点的出边也会包含。边携带 `type`(`link` 或 `keyword`)、`keywords`(仅 `keyword` 边)和 `weight`(合并词频)。输出为 `{ nodes, edges, truncated, edgesTruncated, depth? }`:`truncated` 对应 `maxNodes` 截断,`edgesTruncated` 对应 `maxEdges` 截断,两者分开报告,节点完整不会被误读为整图完整;子图结果还会报告钳制后实际生效的 `depth`。
49
50
 
50
51
  -----
51
52
 
52
53
  <a id="understand-the-implementation"></a>
53
54
  ## 实现说明
54
55
 
55
- 本包是单个 Cordis 函数插件,在 `ctx.tools` 上注册 `wiki_graph`。每次调用通过共享的 `resolveMemoryVaultRoot` 解析仓库根,用 `listNotePaths` 遍历笔记,用 `extractLinks`/`resolveLinkTarget` 提取 `[[link]]` 目标。目标按与 `wiki_read` 相同的 Obsidian 语义解析——先精确仓库相对路径,再路径后缀,最后全仓库 basename 匹配,多重命中按确定性规则选择。除显式链接外,工具还会从每条笔记提取显著词(frontmatter `tags` ×3、frontmatter `title` ×2、正文词 ×1),并为任意两篇共享一个或多个词的笔记创建 `keyword` 边。广度优先遍历带 visited 集合(循环链接图因此可终止),同时跟随 `link` 边与 `keyword` 边以确定节点集合,再从所有已选节点收集边,因此边界节点的出边不会丢失。节点集合超过 `maxNodes` 时,全仓库图保留码点序最小的若干 id(与 `tool-memory-filesystem` 的并列排序规则一致),子图则保留离中心最近的笔记——先按 BFS 层级、再按 id——因此保留哪些笔记不依赖目录列举顺序,子图也不会丢掉自己的中心节点。由于 `wiki_graph` 每次调用都会从磁盘重新计算,重写一条笔记会移除旧的 `keyword` 边,并为新正文中保留下来的或新出现的词建立新边。边在节点集合确定后按 `maxEdges` 截断:显式 `link` 边优先,其后是 `keyword` 边按 `weight` 降序,并以 id 作确定性并列裁决——保留哪些边同样不依赖目录列举顺序。以非已索引笔记作为中心 id 会以 `note not found` 失败;无法读取的笔记会被跳过(记入 logger 警告),不会让整次调用失败。输出为 `{ nodes: [{ id, title }], edges: [{ from, to, type, keywords?, weight? }], truncated: boolean, edgesTruncated: boolean }`,其中 `truncated` 对应节点上限、`edgesTruncated` 对应边上限。
56
+ 本包是单个 Cordis 函数插件,在 `ctx.tools` 上注册 `wiki_graph`。每次调用通过共享的 `resolveMemoryVaultRoot` 解析仓库根,用 `listNotePaths` 遍历笔记,用 `extractLinks`/`resolveLinkTarget` 提取 `[[link]]` 目标。目标按与 `wiki_read` 相同的 Obsidian 语义解析——先精确仓库相对路径,再路径后缀,最后全仓库 basename 匹配,多重命中按确定性规则选择。除显式链接外,工具还会从每条笔记提取显著词(frontmatter `tags` ×3、frontmatter `title` ×2、正文词 ×1),并为任意两篇共享一个或多个词的笔记创建 `keyword` 边。广度优先遍历带 visited 集合(循环链接图因此可终止),同时跟随 `link` 边与 `keyword` 边以确定节点集合,再从所有已选节点收集边,因此边界节点的出边不会丢失。节点集合超过 `maxNodes` 时,全仓库图保留码点序最小的若干 id(与 `tool-memory-filesystem` 的并列排序规则一致),子图则保留离中心最近的笔记——先按 BFS 层级、再按 id——因此保留哪些笔记不依赖目录列举顺序,子图也不会丢掉自己的中心节点。由于 `wiki_graph` 每次调用都会从磁盘重新计算,重写一条笔记会移除旧的 `keyword` 边,并为新正文中保留下来的或新出现的词建立新边。边在节点集合确定后按 `maxEdges` 截断:显式 `link` 边优先,其后是 `keyword` 边按 `weight` 降序,并以 id 作确定性并列裁决——保留哪些边同样不依赖目录列举顺序。请求的 `depth` 在遍历前会被钳制到 `maxDepthLimit`——一个配置期上限,必须不小于 `maxDepth`——子图结果会报告实际遍历生效的 `depth`,调用方因此无法触发无界遍历,同时仍能看出结果延伸了多远。以非已索引笔记作为中心 id 会以 `note not found` 失败;无法读取的笔记会被跳过(记入 logger 警告),不会让整次调用失败。输出为 `{ nodes: [{ id, title }], edges: [{ from, to, type, keywords?, weight? }], truncated: boolean, edgesTruncated: boolean, depth?: number }`,其中 `truncated` 对应节点上限、`edgesTruncated` 对应边上限,`depth` 仅在子图结果中出现。
56
57
 
57
58
  -----
58
59
 
@@ -68,12 +69,12 @@ kind: "package-reference"
68
69
  ##### `wiki_graph` 的原文描述
69
70
 
70
71
  ```markdown
71
- Return the link and keyword graph of the wiki vault: note ids, titles, and directed edges. Explicit `[[link]]` edges are type `link`; edges derived from shared significant terms are type `keyword` with `keywords` and `weight`. Without an id it returns the whole vault graph (node-capped); with an id it returns the subgraph reachable from that note within the given depth. Nodes are capped at maxNodes (`truncated`) and edges at maxEdges (`edgesTruncated`).
72
+ Return the link and keyword graph of the wiki vault: note ids, titles, and directed edges. Explicit `[[link]]` edges are type `link`; edges derived from shared significant terms are type `keyword` with `keywords` and `weight`. Without an id it returns the whole vault graph (node-capped); with an id it returns the subgraph reachable from that note within the given depth. Nodes are capped at maxNodes (`truncated`) and edges at maxEdges (`edgesTruncated`); a subgraph result also reports the effective `depth` after clamping the request to maxDepthLimit.
72
73
  ```
73
74
 
74
75
  #### Token 影响
75
76
 
76
- 全仓库调用返回所有节点与边,大仓库可能产生较多 token。指定中心 id 的子图调用通过 `depth` 限制输出规模。
77
+ 全仓库调用返回所有节点与边,大仓库可能产生较多 token。指定中心 id 的子图调用通过 `depth` 限制输出规模,`maxDepthLimit` 则封顶该规模的上限。
77
78
 
78
79
  #### KV Cache 影响
79
80
 
package/lib/index.js CHANGED
@@ -24,6 +24,7 @@ export const Config = z.object({
24
24
  vaultRoot: z.string().default(''),
25
25
  extensions: z.array(z.string()).default(['.md']),
26
26
  maxDepth: z.number().default(1),
27
+ maxDepthLimit: z.number().default(20),
27
28
  maxNodes: z.number().default(200),
28
29
  maxEdges: z.number().default(1000),
29
30
  indexHiddenDirs: z.boolean().default(false),
@@ -337,7 +338,7 @@ async function buildSubgraph(root, resolved, startId, depth, onSkip) {
337
338
  const filteredKeywordEdges = keywordEdges.filter(e => idSet.has(e.from) && idSet.has(e.to));
338
339
  edges.push(...filteredKeywordEdges);
339
340
  const budgeted = capEdges(edges, resolved.maxEdges);
340
- return { nodes, edges: budgeted.edges, truncated, edgesTruncated: budgeted.truncated };
341
+ return { nodes, edges: budgeted.edges, truncated, edgesTruncated: budgeted.truncated, depth };
341
342
  }
342
343
  /**
343
344
  * Register the `wiki_graph` tool on `ctx.tools`.
@@ -352,12 +353,16 @@ export function apply(ctx, config) {
352
353
  }
353
354
  const resolved = config;
354
355
  assertPositiveInteger('maxDepth', resolved.maxDepth);
356
+ assertPositiveInteger('maxDepthLimit', resolved.maxDepthLimit);
357
+ if (resolved.maxDepthLimit < resolved.maxDepth) {
358
+ throw new Error('tool-memory-graph: maxDepthLimit must be >= maxDepth');
359
+ }
355
360
  assertPositiveInteger('maxNodes', resolved.maxNodes);
356
361
  assertPositiveInteger('maxEdges', resolved.maxEdges);
357
362
  assertExtensions(resolved.extensions);
358
363
  ctx.effect(() => ctx.tools.register(defineTool({
359
364
  name: 'wiki_graph',
360
- description: 'Return the link and keyword graph of the wiki vault: note ids, titles, and directed edges. Explicit `[[link]]` edges are type `link`; edges derived from shared significant terms are type `keyword` with `keywords` and `weight`. Without an id it returns the whole vault graph (node-capped); with an id it returns the subgraph reachable from that note within the given depth. Nodes are capped at maxNodes (`truncated`) and edges at maxEdges (`edgesTruncated`).',
365
+ description: 'Return the link and keyword graph of the wiki vault: note ids, titles, and directed edges. Explicit `[[link]]` edges are type `link`; edges derived from shared significant terms are type `keyword` with `keywords` and `weight`. Without an id it returns the whole vault graph (node-capped); with an id it returns the subgraph reachable from that note within the given depth. Nodes are capped at maxNodes (`truncated`) and edges at maxEdges (`edgesTruncated`); a subgraph result also reports the effective `depth` after clamping the request to maxDepthLimit.',
361
366
  parameters: {
362
367
  id: {
363
368
  type: 'string',
@@ -365,7 +370,7 @@ export function apply(ctx, config) {
365
370
  },
366
371
  depth: {
367
372
  type: 'integer',
368
- description: 'Maximum link or keyword hops from the center note; defaults to the configured maxDepth.',
373
+ description: 'Maximum link or keyword hops from the center note; defaults to the configured maxDepth and is capped at the configured maxDepthLimit.',
369
374
  },
370
375
  },
371
376
  output: {
@@ -374,8 +379,9 @@ export function apply(ctx, config) {
374
379
  },
375
380
  async execute(args, exec) {
376
381
  const vaultRoot = resolveMemoryVaultRoot(resolved.vaultRoot, exec);
377
- const depth = args.depth ?? resolved.maxDepth;
378
- assertPositiveInteger('depth', depth);
382
+ const requestedDepth = args.depth ?? resolved.maxDepth;
383
+ assertPositiveInteger('depth', requestedDepth);
384
+ const depth = Math.min(requestedDepth, resolved.maxDepthLimit);
379
385
  const onSkip = skipLogger(ctx, vaultRoot);
380
386
  const graph = args.id === undefined
381
387
  ? await buildGraph(vaultRoot, resolved, onSkip)
@@ -29,6 +29,12 @@ export interface Config {
29
29
  extensions?: string[];
30
30
  /** Default traversal depth when the call omits `depth`. */
31
31
  maxDepth?: number;
32
+ /**
33
+ * Hard upper bound for the effective depth. A call asking for more is
34
+ * clamped to this value, and the subgraph reports the depth actually
35
+ * traversed. Must be greater than or equal to `maxDepth`.
36
+ */
37
+ maxDepthLimit?: number;
32
38
  /** Maximum nodes returned in one graph. */
33
39
  maxNodes?: number;
34
40
  /** Maximum edges returned in one graph. */
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.7",
4
+ "version": "0.1.0-alpha.8",
5
5
  "publishConfig": {
6
6
  "access": "public"
7
7
  },
@@ -32,7 +32,7 @@
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.10"
35
+ "@jacklika/dsh-tool-memory-filesystem": "^0.1.3-alpha.12"
36
36
  },
37
37
  "peerDependencies": {
38
38
  "@deepseek-ai/cordis": "4.0.4",