@yolk_vat-y/dsh-project-memory 0.5.9 → 0.5.11
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/CHANGELOG.md +117 -0
- package/README.md +16 -16
- package/README.zh-CN.md +16 -16
- package/cordis.patch.yml +1 -1
- package/package.json +2 -2
- package/scripts/bench-synthetic.mjs +2 -4
- package/src/enhancer.js +13 -42
- package/src/index-pipeline.js +5 -4
- package/src/index.js +1 -1
- package/src/link.js +104 -38
- package/src/store.js +172 -32
- package/src/symbols.js +3 -1
- package/src/tools/index-doc.js +0 -2
- package/src/tools/index-repo.js +3 -4
- package/src/tools/query-memory.js +7 -10
- package/src/util/fs.js +59 -20
- package/src/util/search.js +4 -9
- package/src/util/text.js +16 -0
- package/src/watch.js +40 -12
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,122 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.5.11 (2026-09-25)
|
|
4
|
+
|
|
5
|
+
### 修复:doc↔symbol 链接不再物化进 entry(真实大仓库的内存与索引开销)
|
|
6
|
+
|
|
7
|
+
`linkedSymbols` 是**跨实体派生关系**(一个 doc chunk 链接到哪些符号,取决于符号表的当前状态),
|
|
8
|
+
旧实现却在索引时把它算好、写进 `entry` 并落盘。实测本仓库自己的 store(11698 文件 / 70119 条目):
|
|
9
|
+
|
|
10
|
+
- 17174 个 chunk 共 **3,948,420** 个链接槽位,只对应 15,456 个符号,单 chunk 最多 2231 个;
|
|
11
|
+
- 唯一的消费者 `query_memory` 只读前 5 个——存了消费量的 **46 倍**;
|
|
12
|
+
- 加载这个 store 的堆占用 **443MB**,链接槽位是其中最大的一块;
|
|
13
|
+
- 每个索引提交点还要对整库做 O(chunks × symbols) 重扫,并靠 `markFile` 标脏追失效,
|
|
14
|
+
否则"文档先索引、符号后到"会永久丢链接。
|
|
15
|
+
|
|
16
|
+
现在链接在**读取期**用每 store 的符号索引解算(`src/link.js` 的 `resolveLinkedSymbols`):
|
|
17
|
+
纯 latin 符号名走倒排表按整词查,CJK/混合名保留原有边界语义的正则回退;成本 O(本 chunk 词数),
|
|
18
|
+
与符号表规模无关。排序改为命中次数 → 名字长度 → id(旧实现交给消费者的前 5 个是符号表插入序,
|
|
19
|
+
即"任意 5 个",这是本次一并修掉的行为)。
|
|
20
|
+
|
|
21
|
+
- **内存**:同一 store 的加载堆占用 **443MB → 129–196MB**,RSS **581MB → ~300MB**;
|
|
22
|
+
- `linkedSymbols` 在加载时从旧 shard 剥离、写入时不再产生;磁盘上的存量 shard 会在该文件
|
|
23
|
+
下次重新索引时自然压实(不主动重写 11698 个 shard);
|
|
24
|
+
- 删除 `linkEntries` 导出,以及 `commitFileUpdates` 的 `link` 参数(它只为"中间批次跳过、
|
|
25
|
+
最后一批统一重建链接"而存在);`enhancer` / `index-doc` 里的链接重建调用一并移除;
|
|
26
|
+
- `query_memory` 的 `references` 输出格式不变,`test/run-test.mjs` 的链接用例改为断言
|
|
27
|
+
解析器行为,并新增"文档先索引、符号后到也能解出"与"limit / 排序"回归。
|
|
28
|
+
|
|
29
|
+
### 修复:其余派生/中间字段与两处无界缓存(体积、内存、轮询)
|
|
30
|
+
|
|
31
|
+
第一轮去掉了链接,这一轮把剩下的放大源和常驻开销一起收掉。同一个 store(11698 文件 /
|
|
32
|
+
70119 条目 / 索引源码 108.5MB)实测:**落盘 361MB → 93MB**(老 store 由下面的自动压实
|
|
33
|
+
收敛;再叠加 type-cache 自愈清理后是 **73MB**),加载 **1017ms → 279ms**,堆占用
|
|
34
|
+
**129–196MB → 101MB**,`recallItems` p50 **53ms → 47ms**。
|
|
35
|
+
|
|
36
|
+
- **`searchText` 不再落盘**(省 22.7MB)。它是 `weightedFieldText` 的纯派生结果,改成
|
|
37
|
+
`allEntries()` 在内存里按需物化;写入时与 `linkedSymbols` 一起剥离(`PERSISTED_DERIVED`)。
|
|
38
|
+
检索语义与输出不变;磁盘与加载解析变少,堆占用基本持平(物化改到首次查询时做)。
|
|
39
|
+
- **符号声明限长成一行**(`oneLineDeclaration`,≤200 字符)。TypeScript enricher 之前把
|
|
40
|
+
interface 的**全部成员**拼进 `typeSig`、再整体落进 `text`/`typeSig`(实测符号条目平均
|
|
41
|
+
1.25KB,其中 `text` 648B),而这两个字段没有任何读取方。现在 interface 只留前 6 个成员 +
|
|
42
|
+
`… +N more`,`typeSig` 不再落盘。代码层 **31% → 19% 源码**,README 声称的"一行声明"
|
|
43
|
+
由此第一次成立。
|
|
44
|
+
- **storeCache 按字节预算 + LRU**。原来只按个数(32),而单个大仓库 store 实测驻留
|
|
45
|
+
130–200MB;现在同时限制条数与估算驻留量(256MB,约 2.5KB/entry),命中会把条目挪到
|
|
46
|
+
队尾,热的不会先被逐出。
|
|
47
|
+
- **删除 type-cache(TS 增强结果缓存)**。它按内容哈希缓存增强结果,但三个增强入口
|
|
48
|
+
(lazy 的 `fs/observed`、watch 轮询、`index_repo`)**都只在"文件已变更并重新索引"之后**
|
|
49
|
+
才触发,此时内容哈希必然是新值——这个缓存永远命中不了。实测本仓库残留 9827 个文件
|
|
50
|
+
(`du` 41MB,内容其实 9.2MB,约 31MB 是 4KB 块开销)。现在 `load()` 会自愈删除该目录;
|
|
51
|
+
文件变更照常触发增强,进程内仍由 `enhanceQueue` 按 (relPath, 内容哈希) 去重。
|
|
52
|
+
- **watch 轮询空闲退避**。轮询是 O(树) 的 walkDir + 逐文件 stat(本仓库实测 58–87ms),
|
|
53
|
+
原来固定一轮、不管有没有改动都在磨 I/O。现在改成递归 `setTimeout`:无变化时翻倍
|
|
54
|
+
退避到最长 2 分钟,任何变化立即回到 `watchInterval`。基准间隔同时 **15s → 30s**
|
|
55
|
+
(仍可配置)。
|
|
56
|
+
- **删除死代码** `rankEntries` / `rankEntriesMerged` / `store.searchEntries`:它们每次调用
|
|
57
|
+
都 `buildBm25` 全库分词(70k 条目实测 p50 1.4s),生产路径没有调用方;相关测试改用线上
|
|
58
|
+
真正跑的 `rankEntriesStreaming` / `rankEntriesMergedScored`。
|
|
59
|
+
|
|
60
|
+
**存量 store 的压实是自动且有界的**:加载时把带派生字段的老分片放进待压实队列,此后任意一次
|
|
61
|
+
`save()`(watch 轮询、索引、写入都会触发)最多补写 `COMPACT_BATCH = 200` 个分片,直到队列
|
|
62
|
+
清空。升级后不需要重新索引,也不会在首次启动时一次性重写整库;想让它立刻跑完,随便索引一次
|
|
63
|
+
即可。为了让压实不产生副作用,IDF 缓存的失效键也从"任何脏写"改成 entries 的变更计数
|
|
64
|
+
(`_entriesVersion`)——IDF 只依赖 entries,只写经验/insight 或只做压实都不该重建它。
|
|
65
|
+
|
|
66
|
+
### 文档
|
|
67
|
+
|
|
68
|
+
- README 的 `npm test` 断言与实测对齐:360 → **476**(核心 184 → 205、host-contract 9 → 10;
|
|
69
|
+
补上此前漏记的 task-view 6 / root-guards 79 / store-gitignore 9);
|
|
70
|
+
- 两份 README 的"交叉链接"机制描述由"索引后挂载到条目"改为"读取期按当前符号表解算";
|
|
71
|
+
- `watchInterval` 文档补充空闲退避语义;"紧凑性"一节改用实测区间(符号稀疏项目 ~0.5%,
|
|
72
|
+
符号密集的 TS monorepo ~19%),不再把 0.5% 当普遍值。
|
|
73
|
+
|
|
74
|
+
## 0.5.10 (2026-09-24)
|
|
75
|
+
|
|
76
|
+
### 修复:共享临时目录仍然是可用的记忆根(issue #5 现场复核的补充发现)
|
|
77
|
+
|
|
78
|
+
@jinchaofeiyang 在 issue #5 的补充材料里指出 `isUnsafeRoot('/tmp')` 在 macOS 上返回 `false`:
|
|
79
|
+
`os.tmpdir()` 在 macOS 上是 `/var/folders/…/T` 这个**每用户私有**目录,共享的 `/tmp`
|
|
80
|
+
(→ `/private/tmp`)是**另一个路径**,0.5.9 之前不在名单里——于是 `cd /tmp && dsh` 仍会把整个
|
|
81
|
+
/tmp 当项目根全量扫描,与家目录是同一类问题。Windows 同理:`C:\Windows` 被拒,但
|
|
82
|
+
`C:\Windows\Temp` 是精确匹配之外的漏网路径。
|
|
83
|
+
|
|
84
|
+
- 每用户临时目录与**共享/系统**临时目录合并成"临时目录"一组:POSIX `/tmp`、`/var/tmp`、
|
|
85
|
+
`/private/tmp`、`/private/var/tmp`、`/var/folders`、`/dev/shm`;Windows `%TEMP%`、`%TMP%`、
|
|
86
|
+
`%SystemRoot%\Temp`、`%windir%\Temp`。仍是精确匹配——`/tmp/myproj` 这类子目录照常可用
|
|
87
|
+
(插件自己的测试夹具就依赖这一点)。
|
|
88
|
+
- 拒绝文案据此区分"shared temp directory"与笼统的"system directory"。
|
|
89
|
+
|
|
90
|
+
### 新增:反直觉输入的回归测试(现场复核要求的第 1 点)
|
|
91
|
+
|
|
92
|
+
`/opt/homebrew` 是被 `VCS_MARKERS` 的 `.git` **主动选中**的(ARM Mac 的 Homebrew 是 git clone),
|
|
93
|
+
不是兜底误判。所以"拒绝前缀"必须能压过"合法项目标记"。`findProjectRoot` 现在支持注入
|
|
94
|
+
`isUnsafe` 判定,于是这条输入可以在任意平台上用一个临时目录复现:
|
|
95
|
+
|
|
96
|
+
- 控制组证明:只有 `.git` 时该目录确实会当选(否则这条用例什么都没测);
|
|
97
|
+
- 加前缀拒绝后必须为 `null`,且上溯在边界处停止;
|
|
98
|
+
- 边界之外的兄弟项目不受影响。
|
|
99
|
+
- 另外用 `path.win32` / `path.posix` 模拟两个平台,把各自的临时目录与系统前缀断言在
|
|
100
|
+
**任意 runner** 上(macOS 的 `/tmp` 缺口正是靠这个才可能在 ubuntu CI 上被发现)。
|
|
101
|
+
|
|
102
|
+
### 新增:store 目录自我忽略(不再需要用户改 `.gitignore`)
|
|
103
|
+
|
|
104
|
+
让用户手动把 `.dsh-project-memory/` 写进自己的 `.gitignore` 是个设计缺陷:忘了就是一次误提交,
|
|
105
|
+
而插件也不该去改用户的文件。现在 store 在自己目录里写一条 `*` 规则(`<store>/.gitignore`)——
|
|
106
|
+
git 会读取工作区里任意目录下的 `.gitignore`,`*` 连这个文件自己一起命中,于是 `git status` /
|
|
107
|
+
`git add -A` 里整棵树都不出现,用户零配置。老版本留下的 store 在第一次 `load()` 时自动补上。
|
|
108
|
+
|
|
109
|
+
- 顺带:被忽略的文件不会被 `git clean -fd` 删除(未跟踪且未忽略的会被删)。
|
|
110
|
+
- 想跟着仓库提交记忆:`git add -f .dsh-project-memory`(已跟踪的文件不受忽略规则影响)。
|
|
111
|
+
- 回归用例 `test/store-gitignore.test.mjs` 用真 git 仓库验证:store 不进 `git status`、用户的
|
|
112
|
+
`.gitignore` 逐字不变、`git clean -fd` 后 store 仍在、重复保存不重写该文件、历史 store 首次
|
|
113
|
+
`load()` 即补上。
|
|
114
|
+
|
|
115
|
+
### 文档(现场复核要求的第 2 点)
|
|
116
|
+
|
|
117
|
+
- README 把根策略写成参考文档口径:根解析顺序、排除名单、扫描上限(20000 文件 / 12 层;
|
|
118
|
+
截断会在结果里说明,且不删除没扫到的条目),以及 store 的自我忽略。
|
|
119
|
+
|
|
3
120
|
## 0.5.9 (2026-09-24)
|
|
4
121
|
|
|
5
122
|
### 修复:issue #5 —— 在家目录 / `/opt/homebrew` 自动建索引把 DSH 撑到 OOM
|
package/README.md
CHANGED
|
@@ -64,8 +64,8 @@ The tools below are **invoked by the agent**, not typed by the user. In the chat
|
|
|
64
64
|
| Tool | Purpose |
|
|
65
65
|
|---|---|
|
|
66
66
|
| `index_doc file_path` | Index one document (PDF/MD/txt): chunk → deterministic `summary` + whole-chunk `terms` → store with `path:line`. Unchanged files are skipped. |
|
|
67
|
-
| `index_repo root` | Index a whole project: docs get deterministic summaries + whole-chunk terms, code files get a zero-token symbol table. Incremental, cleans up deleted files, cross-links docs to symbols. A root that does not exist — including a Windows-style path resolved on Linux/macOS —
|
|
68
|
-
| `watch_repo root` | Enable automatic refresh: a background poll detects new/changed files (mtime + content hash) and re-indexes only those. Watched roots persist across plugin restarts; a non-existent
|
|
67
|
+
| `index_repo root` | Index a whole project: docs get deterministic summaries + whole-chunk terms, code files get a zero-token symbol table. Incremental, cleans up deleted files, cross-links docs to symbols. A root that does not exist — including a Windows-style path resolved on Linux/macOS — or one on the excluded list is rejected before anything is written. |
|
|
68
|
+
| `watch_repo root` | Enable automatic refresh: a background poll detects new/changed files (mtime + content hash) and re-indexes only those. Watched roots persist across plugin restarts; a non-existent or excluded root is refused, roots that disappear are dropped instead of being re-created, and entries that are no longer valid roots are dropped on startup. |
|
|
69
69
|
| `memory_stats root` | Show what the store contains: totals (files / entries / experience notes), last index time, and the per-file list sorted by recency. |
|
|
70
70
|
| `query_memory query` | BM25 search over docs + symbols + experience + insights (lessons / decisions / procedures), optionally query-expanded by the LLM. `type` selects a layer (`all` / `doc` / `symbol` / `experience` / `insight` / `task`). Returns ranked hits with relative scores, sources or insight ids, and doc→symbol references. |
|
|
71
71
|
| `list_tasks` | List task records for the project (archived marked). Call first in a new session before continuing work. |
|
|
@@ -91,7 +91,7 @@ The design follows four principles:
|
|
|
91
91
|
|
|
92
92
|
- **Volatility** — context is ephemeral; it is lost when a session is compacted.
|
|
93
93
|
- **Persistence** — the **memory** is stored on disk and survives compaction and new sessions.
|
|
94
|
-
- **Compactness** — the code layer stores one declaration line per symbol
|
|
94
|
+
- **Compactness** — the code layer stores one bounded declaration line per symbol (≤200 chars) and the document layer keeps a ≤300-char `summary` plus a bounded `terms` set per chunk. **Derived data is never stored**: doc→symbol links and the BM25 `searchText` are computed at read time. How small the index ends up depends on symbol density, so treat "0.5%" as the sparse end of the range, not a guarantee: a Java/Vue project measured **~0.5% of source** (8.8 MB → 49 KB), while a symbol-dense TypeScript monorepo (11.7k files / 108 MB indexed) measured **~19%** for the code layer and **~106%** for the document layer. On a docs-only corpus (179 chunks / 225 KB of Markdown) `terms` ≈ **27.5%** of source and the on-disk store ≈ **166%** of source — on doc-heavy projects budget for roughly the docs themselves.
|
|
95
95
|
- **Verifiability** — **recalls** carry a `path:line` citation where applicable, so the agent can confirm details against the source.
|
|
96
96
|
|
|
97
97
|
Building the **memory** does not require an upfront scan: files are memorized as the model reads them, so the **memory** grows to cover exactly what has been worked with. Re-reading a file that has not changed is a no-op (content hash), so the **memory** stays fresh with minimal ongoing overhead.
|
|
@@ -117,7 +117,7 @@ The store is per-project and follows the codebase: changed files are re-extracte
|
|
|
117
117
|
Stores created before v0.2.0 (single `entries.json` / `index.json`) migrate automatically and idempotently on first load. Within one dsh process, all tool calls share a single in-memory store per project, so hot-path indexing writes only the shard that changed.
|
|
118
118
|
|
|
119
119
|
- **Incremental** — content hash per file; only changed files are re-extracted.
|
|
120
|
-
- **Cross-linking** —
|
|
120
|
+
- **Cross-linking** — when `query_memory` returns a doc chunk, it resolves the symbols that chunk mentions against the **current** symbol table and appends them as `references`. Links are computed at read time, so they cannot go stale and are not stored in the index (a doc indexed before its symbols still links correctly).
|
|
121
121
|
- **Query expansion** — when `llmQueryExpansion` is on, `query_memory` asks `ctx.llm` to rewrite the query into several variants (synonyms, EN/CN, identifier guesses) and merges BM25 scores across variants; when off, queries never touch the LLM. Indexing itself is model-free: keywords are rule-derived (title-weighted top terms), and doc↔symbol links surface English symbol names from Chinese hits.
|
|
122
122
|
- **Consistency** — the fact layer follows the codebase (hash re-extract / remove-on-delete); the experience layer is retrieval-only with supersede and `forget`. Store writes are serialized per memory directory; the lock is in-process, so avoid running multiple dsh instances against the same project store concurrently.
|
|
123
123
|
|
|
@@ -152,10 +152,10 @@ The workflow panel is collapsible, automatically adapts to dsh and theme plugin
|
|
|
152
152
|
| `lazyIndexing` | true | index files the moment the model reads them (`fs/observed`) |
|
|
153
153
|
| `autoIndexOnFirstUse` | false | full scan of the current working directory on plugin load (opt-in) |
|
|
154
154
|
| `watch` | true | enable the background refresh |
|
|
155
|
-
| `watchInterval` |
|
|
156
|
-
| `maxScanFiles` | 20000 | hard cap on files per scan pass; a truncated
|
|
155
|
+
| `watchInterval` | 30 | base poll interval (seconds); idle polls back off up to 2 minutes and reset to this value on any change |
|
|
156
|
+
| `maxScanFiles` | 20000 | hard cap on files per scan pass; a truncated pass is reported and does not remove the entries it did not reach. Set `0` to disable the cap |
|
|
157
157
|
| `maxScanDepth` | 12 | hard cap on directory depth per scan pass. Set `0` to disable |
|
|
158
|
-
| `allowUnsafeRoots` | false | allow **explicit** tool calls (`index_repo`/`watch_repo`/`remember` with a `root`) to target a
|
|
158
|
+
| `allowUnsafeRoots` | false | allow **explicit** tool calls (`index_repo`/`watch_repo`/`remember` with a `root`) to target a directory on the excluded list. Automatic paths (lazy indexing, session audit, TaskBridge, `autoIndexOnFirstUse`) stay inert in these directories regardless |
|
|
159
159
|
| `tsPath` | (auto) | optional absolute path to a specific `typescript` install; if omitted, resolves from project cwd → plugin node_modules |
|
|
160
160
|
| `enableTypeScript` | true | set `false` to disable L2 TS enhancement entirely (L1 regex only) |
|
|
161
161
|
|
|
@@ -192,15 +192,15 @@ Automatic injection used to be a *retrieval* problem ("which entry is most relat
|
|
|
192
192
|
|
|
193
193
|
The two most relevant switches are `lazyIndexing` (index a file the moment the model reads it; default on) and `autoIndexOnFirstUse` (full scan of the current working directory on plugin load; default off). Lazily indexed project roots are automatically registered with the watcher, so changed files stay fresh without an explicit `watch_repo`.
|
|
194
194
|
|
|
195
|
-
**
|
|
195
|
+
**Root resolution.** In order: an explicit `root` argument; a registered root (`watch_repo`); the nearest ancestor with a VCS marker (`.git`/`.hg`/`.svn`) or a build/manifest marker (`package.json`, `go.mod`, `Cargo.toml`, `pyproject.toml`, …); the session working directory, unless it is on the excluded list. A file that matches none of these is not indexed.
|
|
196
196
|
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
197
|
+
When the root comes from the working directory, the model gets one notice per session naming it and how to change it; mute with `autoContext.rootNotice: false`. Starting dsh in a container directory such as `~/workspace` therefore makes that directory the root, and memory spans everything beneath it up to the scan limit — start dsh inside the project for one store per project.
|
|
198
|
+
|
|
199
|
+
**Excluded directories.** Not used as a root, matched exactly (subdirectories are unaffected): the filesystem root, the home directory, temp directories — `os.tmpdir()` and the shared ones (`/tmp`, `/var/tmp`, `%TEMP%`, `%SystemRoot%\Temp`) — and system / package-manager prefixes (`/opt/homebrew` on POSIX; `%SystemRoot%`, `%ProgramFiles%`, `%ProgramData%` on Windows). A session in one of these runs without memory, with one line on stderr.
|
|
200
|
+
|
|
201
|
+
**Scan limits.** One pass covers at most `maxScanFiles` files (20000) and `maxScanDepth` directory levels (12). A truncated pass is reported in the `index_repo` result and logged once per root by the watcher, and it does not remove entries it did not reach. Raise both for a larger tree.
|
|
202
202
|
|
|
203
|
-
|
|
203
|
+
The store lives in the tree it indexes and **ignores itself**: it writes a `*` rule into its own `<store>/.gitignore`, which git honours for any directory, so nothing shows up in `git status` or `git add -A` and you have nothing to add to your own `.gitignore`. (It also means `git clean -fd` leaves the store alone.) To commit project memory deliberately, `git add -f .dsh-project-memory` — tracked files are not affected by ignore rules.
|
|
204
204
|
|
|
205
205
|
Settings live in the plugin's config object. To change them, add an override entry to your profile's `cordis.patch.yml` — for the web profile that is `~/.dsh/profiles/web/cordis.patch.yml`:
|
|
206
206
|
|
|
@@ -211,7 +211,7 @@ Settings live in the plugin's config object. To change them, add an override ent
|
|
|
211
211
|
autoIndexOnFirstUse: false # off: no upfront full scan (default)
|
|
212
212
|
llmQueryExpansion: false # off: do not spend tokens on LLM query expansion (default)
|
|
213
213
|
watch: true # on: background refresh for watched roots (default)
|
|
214
|
-
watchInterval:
|
|
214
|
+
watchInterval: 30 # base poll interval; idle polls back off to at most 2 min
|
|
215
215
|
maxScanFiles: 20000 # per-scan file cap (truncation is reported, never deletes)
|
|
216
216
|
maxScanDepth: 12 # per-scan directory-depth cap
|
|
217
217
|
enableTypeScript: true # on: L2 TS enhancement when TS is installed (default)
|
|
@@ -297,7 +297,7 @@ These commands are for **maintaining the plugin code** — regular users do not
|
|
|
297
297
|
|
|
298
298
|
```bash
|
|
299
299
|
npm install
|
|
300
|
-
npm test #
|
|
300
|
+
npm test # 476 tests (205 core + 16 TaskBridge + 12 insight-store + 9 insight-actions + 8 doc-index + 7 auto-inject + 10 host-contract + 5 reflection + 4 llm-route + 2 client-hints + 8 recall + 14 readiness + 7 insight-derive + 7 readiness-eval + 6 ops + 8 injection-audit + 5 injection-budget + 6 injection-scenarios + 18 bugfix-0.5.7 + 3 client-icons + 10 client-slash + 5 workflow-command + 7 client-session-id + 6 task-view + 79 root-guards + 9 store-gitignore)
|
|
301
301
|
npm run eval:injection # scenario P/R on the synthetic pool: 14/14 hits, 0 false positives, control group clean
|
|
302
302
|
npm run eval:injection -- --store .dsh-project-memory/insights.json # replay on YOUR store; control group is a hard gate
|
|
303
303
|
npm run selfcheck:triggers # which entries can still push, which declarations are dead (reads your local store)
|
package/README.zh-CN.md
CHANGED
|
@@ -63,8 +63,8 @@ dsh plugin --profile web add /path/to/dsh-project-memory.tgz
|
|
|
63
63
|
| 工具 | 用途 |
|
|
64
64
|
|---|---|
|
|
65
65
|
| `index_doc file_path` | 索引单个文档(PDF/MD/txt):分块 → 确定性 `summary` + 整 chunk `terms` → 带 `路径:行号` 入库。未变更文件自动跳过。 |
|
|
66
|
-
| `index_repo root` | 索引整个项目:文档生成确定性摘要 + 整 chunk 词项,代码文件生成零 token 符号表。增量更新、清理已删除文件、文档与符号交叉链接。根目录不存在(含在 Linux/macOS 上被解析成相对路径的 Windows
|
|
67
|
-
| `watch_repo root` | 启用自动刷新:后台轮询检测新增/变更文件(mtime +
|
|
66
|
+
| `index_repo root` | 索引整个项目:文档生成确定性摘要 + 整 chunk 词项,代码文件生成零 token 符号表。增量更新、清理已删除文件、文档与符号交叉链接。根目录不存在(含在 Linux/macOS 上被解析成相对路径的 Windows 风格路径)或在排除名单上时,会在写入任何内容前拒绝。 |
|
|
67
|
+
| `watch_repo root` | 启用自动刷新:后台轮询检测新增/变更文件(mtime + 内容哈希),仅重抽这些文件。监听的项目在插件重启后自动恢复;不存在或在排除名单上的根目录会被拒绝,已消失的根目录会被丢弃而不是被重新创建,不再有效的条目会在启动时清掉。 |
|
|
68
68
|
| `memory_stats root` | 查看记忆库内容:总量(文件 / 条目 / 经验笔记)、最近索引时间,以及按时间排序的逐文件清单。 |
|
|
69
69
|
| `query_memory query` | 对文档、符号、经验与 insight(教训/决策/流程)执行 BM25 检索,可选 LLM 查询扩展。`type` 选择层(`all` / `doc` / `symbol` / `experience` / `insight` / `task`)。返回带相对分数(0-100)、引用或 insight id、以及文档→符号链接的排序结果。 |
|
|
70
70
|
| `list_tasks` | 列出本项目任务记录(含归档,带标记)。新会话/续接前先调用。 |
|
|
@@ -88,7 +88,7 @@ dsh plugin --profile web add /path/to/dsh-project-memory.tgz
|
|
|
88
88
|
|
|
89
89
|
- **易失性** — 上下文是临时的,会话压缩即丢失。
|
|
90
90
|
- **持久性** — **记忆**存于磁盘,跨压缩与会话保留。
|
|
91
|
-
- **紧凑性** —
|
|
91
|
+
- **紧凑性** — 代码层每个符号只存一行声明(≤200 字符),文档层每个 chunk 保留 ≤300 字符的 `summary` 与有界的 `terms`。**派生数据一律不落盘**:doc→symbol 链接与 BM25 的 `searchText` 都在读取期计算。最终体积取决于符号密度,所以「0.5%」是区间里稀疏的那一端、不是承诺:Java/Vue 项目实测约 **0.5% 源码**(8.8 MB → 49 KB),而符号密集的 TypeScript monorepo(11.7k 文件 / 108 MB 索引)实测代码层约 **19%**、文档层约 **106%**。纯文档语料实测(179 chunk / 225 KB Markdown)`terms` ≈ 源码 **27.5%**、整库落盘 ≈ 源码 **166%**——文档占比高的项目请按「约等于文档本身大小」估。
|
|
92
92
|
- **可核验性** — **召回**在适用时携带 `路径:行号` 引用,agent 可对照源文件核实。
|
|
93
93
|
|
|
94
94
|
构建**记忆**无需预先全量扫描:文件在模型读取时被记忆,**记忆**恰好覆盖实际处理过的内容。未变更的文件重读是空操作(内容哈希),因此**记忆**的持续维护开销很低。
|
|
@@ -114,7 +114,7 @@ dsh plugin --profile web add /path/to/dsh-project-memory.tgz
|
|
|
114
114
|
v0.2.0 之前创建的库(单文件 `entries.json` / `index.json`)在首次加载时自动幂等迁移。同一个 dsh 进程内,所有工具调用共享每个项目的单一内存 store 实例,热路径索引只写发生变化的那一个分片。
|
|
115
115
|
|
|
116
116
|
- **增量** — 按文件内容哈希,仅重新抽取变更文件。
|
|
117
|
-
- **交叉链接** —
|
|
117
|
+
- **交叉链接** — `query_memory` 返回文档 chunk 时,按**当前**符号表解算它提到的符号,以 `references` 带出。链接在读取期解算、不落盘,因此不会过期(文档先索引、符号后到也能链上),也不占存储。
|
|
118
118
|
- **查询扩展** — `llmQueryExpansion` 开启时,`query_memory` 让 `ctx.llm` 将查询改写为多个变体(同义词、中英、符号名猜测),再跨变体合并 BM25 分数;关闭时查询完全不碰 LLM。索引本身不调用模型:keywords 由规则推导(标题加权词项),doc↔symbol 链接也会从中文命中带出英文符号名。
|
|
119
119
|
- **一致性** — 事实层跟随代码库(哈希重抽 / 删除即移除);经验层仅检索,配合覆盖与 `forget` 机制。每个记忆目录的写入走同步事务 `store.commit(fn)`:fn 内完成校验与变更、成功后才原子落盘,单进程内天然串行;请避免多个 dsh 实例同时写同一项目存储。
|
|
120
120
|
|
|
@@ -149,10 +149,10 @@ TaskPanel (Container)
|
|
|
149
149
|
| `lazyIndexing` | true | 模型读取文件的瞬间即索引(`fs/observed`) |
|
|
150
150
|
| `autoIndexOnFirstUse` | false | 插件加载时对当前工作目录做全量扫描(可选) |
|
|
151
151
|
| `watch` | true | 启用后台刷新 |
|
|
152
|
-
| `watchInterval` |
|
|
153
|
-
| `maxScanFiles` | 20000 |
|
|
152
|
+
| `watchInterval` | 30 | 基础轮询间隔(秒);空闲时逐步退避到最长 2 分钟,一有变化立即回到该值 |
|
|
153
|
+
| `maxScanFiles` | 20000 | 单次扫描的文件数硬上限;被截断时会在报告里说明,且不会删除没扫到的条目。设 `0` 取消上限 |
|
|
154
154
|
| `maxScanDepth` | 12 | 单次扫描的目录深度硬上限。设 `0` 取消 |
|
|
155
|
-
| `allowUnsafeRoots` | false | 允许**显式**工具调用(带 `root` 的 `index_repo`/`watch_repo`/`remember
|
|
155
|
+
| `allowUnsafeRoots` | false | 允许**显式**工具调用(带 `root` 的 `index_repo`/`watch_repo`/`remember`)指向排除名单上的目录。自动路径(懒索引、会话审计、TaskBridge、`autoIndexOnFirstUse`)无论此项如何都不会越权 |
|
|
156
156
|
| `tsPath` | (自动) | 可选:强制指定特定 `typescript` 安装路径;省略时按项目 cwd → 插件 node_modules 向上解析 |
|
|
157
157
|
| `enableTypeScript` | true | 设为 `false` 彻底禁用 L2 TS 增强(仅保留 L1 正则) |
|
|
158
158
|
|
|
@@ -189,15 +189,15 @@ TaskPanel (Container)
|
|
|
189
189
|
|
|
190
190
|
两个最常用的开关是 `lazyIndexing`(模型读取文件的瞬间即索引;默认开启)和 `autoIndexOnFirstUse`(插件加载时对当前工作目录做全量扫描;默认关闭)。懒加载建立的索引根会自动注册到 watcher,文件变更无需手动 `watch_repo` 也能保持新鲜。
|
|
191
191
|
|
|
192
|
-
|
|
192
|
+
**项目根怎么定。** 按顺序:显式 `root` 参数 → 登记的根(`watch_repo`)→ 最近的 VCS 标记(`.git`/`.hg`/`.svn`)或构建/清单标记(`package.json`、`go.mod`、`Cargo.toml`、`pyproject.toml` 等)所在祖先 → 会话工作目录(前提是它不在排除名单里)。都不命中则不索引该文件。
|
|
193
193
|
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
194
|
+
根来自工作目录时,模型每个会话收到一条通告,说明根的位置与改法;`autoContext.rootNotice: false` 关闭。因此在 `~/workspace` 这类容器目录里启动 dsh,该目录就是根,记忆覆盖其下所有项目直到扫描上限——想一个项目一个 store,就在项目目录里启动。
|
|
195
|
+
|
|
196
|
+
**排除名单。** 以下目录不会作为根,精确匹配(子目录不受影响):文件系统根、家目录、临时目录(`os.tmpdir()` 与共享的 `/tmp`、`/var/tmp`、`%TEMP%`、`%SystemRoot%\Temp`),以及系统/包管理器前缀(POSIX 上的 `/opt/homebrew`,Windows 上的 `%SystemRoot%`、`%ProgramFiles%`、`%ProgramData%`)。这些目录下的会话不启用记忆,stderr 输出一行说明。
|
|
197
|
+
|
|
198
|
+
**扫描上限。** 单次扫描最多 `maxScanFiles` 个文件(20000)、`maxScanDepth` 层目录(12)。被截断时,`index_repo` 的结果里会说明,watcher 每个根记一行,且不会删除没扫到的条目。目录树更大就调高这两个值。
|
|
199
199
|
|
|
200
|
-
|
|
200
|
+
store 建在被索引的目录树里,并且**自我忽略**:它在自己目录内写入一条 `*` 规则(`<store>/.gitignore`)。git 会读取任意目录下的 `.gitignore`,所以 `git status` / `git add -A` 里都看不到它,你自己的 `.gitignore` 一个字都不用加(`git clean -fd` 也因此不会删它)。确实想把记忆跟着仓库提交:`git add -f .dsh-project-memory`——已跟踪的文件不受忽略规则影响。
|
|
201
201
|
|
|
202
202
|
配置存放在插件的 config 对象中。修改方式:在 profile 的 `cordis.patch.yml` 里加一条覆盖项——web profile 对应 `~/.dsh/profiles/web/cordis.patch.yml`:
|
|
203
203
|
|
|
@@ -208,7 +208,7 @@ or restart dsh inside the project directory.
|
|
|
208
208
|
autoIndexOnFirstUse: false # 关闭:不做加载时的全量扫描(默认)
|
|
209
209
|
llmQueryExpansion: false # 关闭:不用 LLM 扩展查询,节省 token(默认)
|
|
210
210
|
watch: true # 开启:被监听根目录后台保持新鲜(默认)
|
|
211
|
-
watchInterval:
|
|
211
|
+
watchInterval: 30 # 基础轮询间隔;空闲时退避到最长 2 分钟
|
|
212
212
|
maxScanFiles: 20000 # 单次扫描文件上限(截断会报告,且不会误删旧条目)
|
|
213
213
|
maxScanDepth: 12 # 单次扫描目录深度上限
|
|
214
214
|
enableTypeScript: true # 开启:装了 TS 时启用 L2 语义增强(默认)
|
|
@@ -294,7 +294,7 @@ node scripts/bench.mjs /你的/项目路径 [--json] [--samples 100] [--no-pdf]
|
|
|
294
294
|
|
|
295
295
|
```bash
|
|
296
296
|
npm install
|
|
297
|
-
npm test #
|
|
297
|
+
npm test # 476 项测试(核心 205 + TaskBridge 16 + insight-store 12 + insight-actions 9 + doc-index 8 + auto-inject 7 + host-contract 10 + reflection 5 + llm-route 4 + client-hints 2 + recall 8 + readiness 14 + insight-derive 7 + readiness-eval 7 + ops 6 + injection-audit 8 + injection-budget 5 + injection-scenarios 6 + bugfix-0.5.7 18 + client-icons 3 + client-slash 10 + workflow-command 5 + client-session-id 7 + task-view 6 + root-guards 79 + store-gitignore 9)
|
|
298
298
|
npm run eval:injection # 合成池上的场景 P/R:命中 14/14、假阳性 0、对照组零注入
|
|
299
299
|
npm run eval:injection -- --store .dsh-project-memory/insights.json # 用你自己的 store 重放;对照组是硬闸门
|
|
300
300
|
npm run selfcheck:triggers # 哪些条目还推得动、哪些声明是死的(读你本地的 store)
|
package/cordis.patch.yml
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@yolk_vat-y/dsh-project-memory",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.11",
|
|
4
4
|
"description": "Persistent project memory for dsh agents: index docs (PDF/Markdown/text) and code symbols into a searchable per-workspace store, recall them with cited sources, and keep experience entries (problems -> solutions) searchable on demand.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/index.js",
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
"url": "https://github.com/00080000/dsh-project-memory.git"
|
|
19
19
|
},
|
|
20
20
|
"scripts": {
|
|
21
|
-
"test": "node test/run-test.mjs && node test/taskbridge.test.mjs && node test/insight-store.test.mjs && node test/reflection-pipeline.test.mjs && node test/auto-inject.test.mjs && node test/insight-actions.test.mjs && node test/host-contract.test.mjs && node test/llm-route.test.mjs && node test/doc-index.test.mjs && node test/client-hints.test.mjs && node test/recall.test.mjs && node test/readiness.test.mjs && node test/insight-derive.test.mjs && node test/readiness-eval.test.mjs && node test/ops.test.mjs && node test/injection-audit.test.mjs && node test/injection-budget.test.mjs && node test/injection-scenarios.test.mjs && node test/bugfix-0.5.7.test.mjs && node test/client-icons.test.mjs && node test/client-slash.test.mjs && node test/workflow-command.test.mjs && node test/client-session-id.test.mjs && node test/task-view.test.mjs && node test/root-guards.test.mjs",
|
|
21
|
+
"test": "node test/run-test.mjs && node test/taskbridge.test.mjs && node test/insight-store.test.mjs && node test/reflection-pipeline.test.mjs && node test/auto-inject.test.mjs && node test/insight-actions.test.mjs && node test/host-contract.test.mjs && node test/llm-route.test.mjs && node test/doc-index.test.mjs && node test/client-hints.test.mjs && node test/recall.test.mjs && node test/readiness.test.mjs && node test/insight-derive.test.mjs && node test/readiness-eval.test.mjs && node test/ops.test.mjs && node test/injection-audit.test.mjs && node test/injection-budget.test.mjs && node test/injection-scenarios.test.mjs && node test/bugfix-0.5.7.test.mjs && node test/client-icons.test.mjs && node test/client-slash.test.mjs && node test/workflow-command.test.mjs && node test/client-session-id.test.mjs && node test/task-view.test.mjs && node test/root-guards.test.mjs && node test/store-gitignore.test.mjs",
|
|
22
22
|
"eval:injection": "node test/injection-scenarios.test.mjs",
|
|
23
23
|
"typecheck": "tsc -p tsconfig.json && tsc -p tsconfig.client.json",
|
|
24
24
|
"selfcheck:triggers": "node test/injection-scenarios.test.mjs --selfcheck",
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* - store.js : load / commit / addExperience(findSupersede) / removeExperience
|
|
6
6
|
* - util/search.js : buildBm25 / rankEntriesStreaming / rankExperienceScored
|
|
7
7
|
* - symbols.js : scanSymbols(零 token 符号抽取,无 LLM)
|
|
8
|
-
* - link.js :
|
|
8
|
+
* - link.js : resolveLinkedSymbols(doc↔symbol 交叉链接,读取期解算)
|
|
9
9
|
* - insight-store.js: GlobalStore read/write + saveInsight(归一化去重)
|
|
10
10
|
* - similarity.js : normalizedTokenOverlap(去重阈值判定)
|
|
11
11
|
*
|
|
@@ -21,7 +21,6 @@ import { performance } from 'node:perf_hooks'
|
|
|
21
21
|
import { ProjectMemoryStore } from '../src/store.js'
|
|
22
22
|
import { walkDir, readFileForIndex, relativePath, storeKey, isSupportedCode } from '../src/util/fs.js'
|
|
23
23
|
import { scanSymbols } from '../src/symbols.js'
|
|
24
|
-
import { linkEntries } from '../src/link.js'
|
|
25
24
|
import { rankEntriesStreaming, rankExperienceScored, makeSearchText } from '../src/util/search.js'
|
|
26
25
|
import { GlobalStore, saveInsight, normalizeInsight } from '../src/insight-store.js'
|
|
27
26
|
import { normalizedTokenOverlap } from '../src/similarity.js'
|
|
@@ -108,7 +107,6 @@ async function coldIndex(root, config) {
|
|
|
108
107
|
const report = store.commit((s) => {
|
|
109
108
|
for (const u of fileUpdates) s.applyFileUpdate(u.rel, u)
|
|
110
109
|
for (const rel of Object.keys(s.files)) if (!seen.has(rel)) s.removeFile(rel)
|
|
111
|
-
linkEntries(s)
|
|
112
110
|
return s.stats()
|
|
113
111
|
})
|
|
114
112
|
return { report, store }
|
|
@@ -146,7 +144,7 @@ async function main() {
|
|
|
146
144
|
console.log(`\n[语料生成] ${FILES} 文件写盘耗时 ${genMs.toFixed(0)} ms`)
|
|
147
145
|
|
|
148
146
|
// ---------- 1. 冷索引(完整内核管线,无 LLM)----------
|
|
149
|
-
console.log('\n--- 1. 冷索引 index_repo 内核管线(walk + sha256 + scanSymbols +
|
|
147
|
+
console.log('\n--- 1. 冷索引 index_repo 内核管线(walk + sha256 + scanSymbols + commit)---')
|
|
150
148
|
const idxTimes = []
|
|
151
149
|
let entriesCount = 0
|
|
152
150
|
for (let r = 0; r < 3; r++) {
|
package/src/enhancer.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { createHash } from 'node:crypto'
|
|
2
|
-
import { readFileSync
|
|
2
|
+
import { readFileSync } from 'node:fs'
|
|
3
3
|
import { join } from 'node:path'
|
|
4
4
|
import { createRequire } from 'node:module'
|
|
5
|
-
import {
|
|
5
|
+
import { oneLineDeclaration } from './util/text.js'
|
|
6
6
|
|
|
7
7
|
const require = createRequire(import.meta.url)
|
|
8
8
|
|
|
@@ -98,34 +98,11 @@ const PRIORITY = {
|
|
|
98
98
|
const enhanceQueue = []
|
|
99
99
|
let processing = false
|
|
100
100
|
|
|
101
|
-
function getCacheDirForRoot(root, config) {
|
|
102
|
-
return join(root, config.memoryDir || '.dsh-project-memory', 'type-cache')
|
|
103
|
-
}
|
|
104
|
-
|
|
105
101
|
function getCacheKey(content) {
|
|
106
102
|
const hash = createHash('sha256').update(content).digest('hex').slice(0, 16)
|
|
107
103
|
return hash
|
|
108
104
|
}
|
|
109
105
|
|
|
110
|
-
async function loadTypeCache(cacheDir, key) {
|
|
111
|
-
const file = join(cacheDir, `${key}.json`)
|
|
112
|
-
if (!existsSync(file)) return null
|
|
113
|
-
try {
|
|
114
|
-
const data = JSON.parse(readFileSync(file, 'utf8'))
|
|
115
|
-
return data
|
|
116
|
-
} catch {
|
|
117
|
-
return null
|
|
118
|
-
}
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
async function saveTypeCache(cacheDir, key, data) {
|
|
122
|
-
try {
|
|
123
|
-
if (!existsSync(cacheDir)) mkdirSync(cacheDir, { recursive: true })
|
|
124
|
-
const file = join(cacheDir, `${key}.json`)
|
|
125
|
-
writeFileSync(file, JSON.stringify(data))
|
|
126
|
-
} catch {}
|
|
127
|
-
}
|
|
128
|
-
|
|
129
106
|
export function isTypeScriptFile(filePath) {
|
|
130
107
|
const ext = filePath.slice(filePath.lastIndexOf('.')).toLowerCase()
|
|
131
108
|
return ext === '.ts' || ext === '.tsx' || ext === '.js' || ext === '.jsx' ||
|
|
@@ -264,11 +241,14 @@ export function deepParseWithTS(filePath, content) {
|
|
|
264
241
|
if (!m.name) return null
|
|
265
242
|
const type = m.type ? getTypeStr(checker.getTypeAtLocation(m.type)) : 'any'
|
|
266
243
|
return `${m.name.getText()}: ${type}`
|
|
267
|
-
}).filter(Boolean)
|
|
244
|
+
}).filter(Boolean)
|
|
245
|
+
// 成员列表必须有界:一个大 interface 的全部成员拼起来能到几 KB,而它整条只作为
|
|
246
|
+
// "一行声明"存在。只留前 6 个,其余记数量。
|
|
247
|
+
const shown = members.slice(0, 6)
|
|
268
248
|
symbols.push({
|
|
269
249
|
name: node.name.getText(),
|
|
270
250
|
kind: 'interface',
|
|
271
|
-
typeSig: `{ ${members} }`,
|
|
251
|
+
typeSig: `{ ${shown.join('; ')}${members.length > shown.length ? `; … +${members.length - shown.length} more` : ''} }`,
|
|
272
252
|
line: getLine(node)
|
|
273
253
|
})
|
|
274
254
|
} else if (ts.isTypeAliasDeclaration(node)) {
|
|
@@ -326,18 +306,8 @@ export function enqueueEnhance(store, relPath, filePath, priority = PRIORITY.BAT
|
|
|
326
306
|
const p = (async () => {
|
|
327
307
|
try {
|
|
328
308
|
const content = readFileSync(filePath, 'utf8')
|
|
329
|
-
const cacheKey = getCacheKey(content)
|
|
330
|
-
const cacheDir = getCacheDirForRoot(root || process.cwd(), config)
|
|
331
|
-
const cached = await loadTypeCache(cacheDir, cacheKey)
|
|
332
|
-
if (cached) {
|
|
333
|
-
// Cache hit: persist via store.commit
|
|
334
|
-
await store.commit(fn => applyEnhancedSymbols(fn, relPath, cached.symbols))
|
|
335
|
-
return
|
|
336
|
-
}
|
|
337
|
-
|
|
338
309
|
const enhanced = deepParseWithTS(filePath, content)
|
|
339
310
|
if (enhanced?.length) {
|
|
340
|
-
await saveTypeCache(cacheDir, cacheKey, { symbols: enhanced })
|
|
341
311
|
await store.commit(fn => applyEnhancedSymbols(fn, relPath, enhanced))
|
|
342
312
|
}
|
|
343
313
|
} catch (err) {
|
|
@@ -396,8 +366,10 @@ function applyEnhancedSymbols(fn, relPath, enhanced) {
|
|
|
396
366
|
if (!enh || nameOf(e) !== enh.name) return e
|
|
397
367
|
return {
|
|
398
368
|
...e,
|
|
399
|
-
|
|
400
|
-
typeSig
|
|
369
|
+
// 一行声明(限长)。typeSig 只是构建期的中间量,不落进 entry:它没有读取方,
|
|
370
|
+
// 且 interface 的 typeSig 就是整个类型体,是符号条目变胖的主因。
|
|
371
|
+
text: oneLineDeclaration(`${enh.name}${enh.typeSig} -- ${relPath}:${enh.line}`),
|
|
372
|
+
typeSig: undefined,
|
|
401
373
|
enhanced: true
|
|
402
374
|
}
|
|
403
375
|
})
|
|
@@ -420,7 +392,7 @@ function applyEnhancedSymbols(fn, relPath, enhanced) {
|
|
|
420
392
|
type: 'symbol',
|
|
421
393
|
title: `${s.name} (${s.kind})`,
|
|
422
394
|
keywords: [s.name, s.kind],
|
|
423
|
-
text: `${s.name}${s.typeSig} -- ${relPath}:${s.line}
|
|
395
|
+
text: oneLineDeclaration(`${s.name}${s.typeSig} -- ${relPath}:${s.line}`),
|
|
424
396
|
enhanced: true
|
|
425
397
|
})
|
|
426
398
|
}
|
|
@@ -428,8 +400,7 @@ function applyEnhancedSymbols(fn, relPath, enhanced) {
|
|
|
428
400
|
// Write to store.entries via setEntries so the shard is marked dirty and persisted
|
|
429
401
|
fn.setEntries(relPath, [...mergedEntries, ...newEntries])
|
|
430
402
|
|
|
431
|
-
//
|
|
432
|
-
if (newEntries.length) linkEntries(fn)
|
|
403
|
+
// doc<->symbol 链接不在这里维护:它是读取期解算的派生关系(见 src/link.js)
|
|
433
404
|
|
|
434
405
|
// Also update fn.files metadata
|
|
435
406
|
if (fn.files[relPath]) {
|
package/src/index-pipeline.js
CHANGED
|
@@ -10,7 +10,6 @@ import { isSupportedCode, isSupportedDoc, readFileForIndex } from './util/fs.js'
|
|
|
10
10
|
import { buildDocEntries } from './doc-pipeline.js'
|
|
11
11
|
import { docEntriesNeedBackfill } from './doc-index.js'
|
|
12
12
|
import { scanSymbols } from './symbols.js'
|
|
13
|
-
import { linkEntries } from './link.js'
|
|
14
13
|
|
|
15
14
|
/** 不索引的后缀。 */
|
|
16
15
|
export const UNSUPPORTED = 'unsupported'
|
|
@@ -88,13 +87,16 @@ export function toFileUpdate(rel, plan, record) {
|
|
|
88
87
|
}
|
|
89
88
|
|
|
90
89
|
/**
|
|
91
|
-
* 一批更新一次性落盘:写入 / 移除 →
|
|
90
|
+
* 一批更新一次性落盘:写入 / 移除 → 清掉本轮未见到的旧条目。
|
|
92
91
|
* 单事务的好处是 store 只 save 一次,watch 每轮不会反复重写。
|
|
93
92
|
*
|
|
93
|
+
* 不再重建 doc↔symbol 链接:链接是读取期解算的派生关系(见 src/link.js),
|
|
94
|
+
* 所以这里也没有了那个「中间批次跳过链接、最后一批统一做」的 `link` 参数。
|
|
95
|
+
*
|
|
94
96
|
* @returns {{stale: string[], removed: number}} stale 是 CAS 失败(并发改动)的 rel,
|
|
95
97
|
* 调用方应让它们保持「未落快照」状态,下一轮重试。
|
|
96
98
|
*/
|
|
97
|
-
export function commitFileUpdates(store, { updates, unseen = null
|
|
99
|
+
export function commitFileUpdates(store, { updates, unseen = null }) {
|
|
98
100
|
const stale = []
|
|
99
101
|
let removed = 0
|
|
100
102
|
store.commit((s) => {
|
|
@@ -113,7 +115,6 @@ export function commitFileUpdates(store, { updates, unseen = null, link = true }
|
|
|
113
115
|
}
|
|
114
116
|
}
|
|
115
117
|
}
|
|
116
|
-
if (link) linkEntries(s)
|
|
117
118
|
})
|
|
118
119
|
return { stale, removed }
|
|
119
120
|
}
|
package/src/index.js
CHANGED
|
@@ -40,7 +40,7 @@ export const Config = Schema.object({
|
|
|
40
40
|
lazyIndexing: Schema.boolean().default(true),
|
|
41
41
|
autoIndexOnFirstUse: Schema.boolean().default(false),
|
|
42
42
|
watch: Schema.boolean().default(true),
|
|
43
|
-
watchInterval: Schema.number().default(
|
|
43
|
+
watchInterval: Schema.number().default(30),
|
|
44
44
|
// 危险根护栏(issue #5):家目录 / 文件系统根 / 系统目录 / 包管理器前缀(/opt/homebrew …)
|
|
45
45
|
// 整体扫描会吃满内存,默认一律拒绝。**自动路径永不越权**——懒索引、会话审计、任务桥在
|
|
46
46
|
// 这类目录里始终零副作用;这个开关只放开**显式**工具调用(index_repo / watch_repo /
|