@yolk_vat-y/dsh-project-memory 0.3.1 → 0.3.2
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 +32 -0
- package/README.md +20 -14
- package/README.zh-CN.md +19 -13
- package/package.json +11 -6
- package/src/doc-pipeline.js +7 -0
- package/src/enhancer.js +444 -0
- package/src/index.js +8 -0
- package/src/lazy.js +3 -1
- package/src/llm.js +12 -5
- package/src/symbols.js +228 -19
- package/src/tools/index-repo.js +14 -1
- package/src/util/fs.js +1 -1
- package/src/watch.js +8 -4
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,37 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.3.2 (2026-08-31)
|
|
4
|
+
|
|
5
|
+
### TS Compiler API 增强器 (Phase 2 L2/L3)
|
|
6
|
+
- **L2 语义增强层**:用户项目安装 `typescript`(`npm i -D typescript`)后,插件自动激活,利用 TS Compiler API 推导返回类型、实例化泛型、提取接口与类型别名、丰富箭头函数签名
|
|
7
|
+
- **L3 磁盘缓存层**:增强结果按文件内容 SHA256 哈希缓存至 `.dsh-project-memory/type-cache/<hash>.json`,冷启动毫秒级复用,跨会话持久化
|
|
8
|
+
- **三级优先级队列**:P0 ACTIVE(`fs/observed` 读文件瞬间)> P1 RECENT(`watch` 变更后)> P2 BATCH(`index_repo` 批量)> P3 BACKLOG(启动补全历史),`setImmediate` 每任务后让出事件循环
|
|
9
|
+
- **零配置、零感知**:装 TS 重启 dsh 即可;无 TS 或 `enableTypeScript: false` 时优雅回退 L1 正则;TS 6.x/7.x 检测到警告并回退 L1
|
|
10
|
+
- **解析策略**:单文件 `createProgram`(快 10x),`createRequire(import.meta.url)` 兼容 ESM,解析优先级:配置 `tsPath` → 项目 cwd 向上 `node_modules/typescript` → 插件自身 `node_modules`
|
|
11
|
+
- **新增配置**:`tsPath`(可选指定 TS 路径)、`enableTypeScript`(默认 true,设 false 彻底禁用)
|
|
12
|
+
- **扩展名支持**:新增 `.mts` `.cts`
|
|
13
|
+
- **文档同步**:README.md / README.zh-CN.md 新增功能介绍与配置表
|
|
14
|
+
- **依赖升级**:cordis 4.0.2、schemastery 3.18.2、dsh-tools/llm 0.1.2-alpha.2
|
|
15
|
+
|
|
16
|
+
## Unreleased
|
|
17
|
+
|
|
18
|
+
### 符号层:只存身份牌,不存行为
|
|
19
|
+
- `src/symbols.js` `buildSymbol`:删除 `summary` 废话字段;`text` 不再截断,保存完整声明行(含签名);返回一行身份牌 `fn(a: A, b: B): R — file.ts:42`;删除冗余 `sig` 字段
|
|
20
|
+
|
|
21
|
+
### 文档层:答案级摘要 + 自报盲区
|
|
22
|
+
- `src/llm.js` `extractDocEntry`:新增 `blindSpots` 字段(自报盲区,如 `// 未覆盖:部署细节、性能基准、v0.2 前 API`);Prompt 要求 LLM 返回 `blindSpots`;摘要通过 `summarizeText()` 截断至 300 字符;`blindSpots` 追加在摘要末尾 `// 未覆盖:...`
|
|
23
|
+
- `src/doc-pipeline.js` `buildDocEntries`:新增 `hash` 字段(SHA256 内容哈希,用于更新检测);新增 `blindSpots` 字段存入分片
|
|
24
|
+
|
|
25
|
+
## 0.3.1 (2026-08-30)
|
|
26
|
+
|
|
27
|
+
### 存储:相对路径存储
|
|
28
|
+
- Entry 存相对路径(如 `src/main.js`),查询时按项目根解析绝对路径;项目搬家仅在根目录变更时需重新索引,兼容旧绝对路径 entry
|
|
29
|
+
- 所有写入路径(`index_doc` / `index_repo` / `watch` / `lazy`)统一传相对路径给构建函数
|
|
30
|
+
- `scanSymbols` / `buildDocEntries` 兼容旧签名(绝对路径 entry 仍可读)
|
|
31
|
+
|
|
32
|
+
### 设计取舍同步
|
|
33
|
+
- 移除「绝对路径引用」项,该限制已由相对路径存储方案解决
|
|
34
|
+
|
|
3
35
|
## 0.3.0 (2026-08-29)
|
|
4
36
|
|
|
5
37
|
### 存储:无锁同步事务重构
|
package/README.md
CHANGED
|
@@ -4,19 +4,21 @@
|
|
|
4
4
|
|
|
5
5
|
[](https://github.com/00080000/dsh-project-memory/actions/workflows/ci.yml) [](LICENSE) [](https://www.npmjs.com/package/@yolk_vat-y/dsh-project-memory) [](https://dsh-plugin.org/plugins/00080000/dsh-project-memory) [](https://awesome-dsh-plugin.com)
|
|
6
6
|
|
|
7
|
-
Persistent project memory for [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness)(dsh) agents.
|
|
7
|
+
Persistent project memory for [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness)(dsh) agents. **Memorizes** documents (PDF / Markdown / txt) and code symbols into a per-workspace store, refreshes them automatically, and **recalls** them with source citations — documents are cross-linked to the code symbols they reference.
|
|
8
8
|
|
|
9
|
-
> The plugin keeps a compact project
|
|
9
|
+
> The plugin keeps a compact project **memory** on disk, with every entry pointing to a concrete file and line — the agent can reorient quickly instead of re-reading the whole project.
|
|
10
10
|
|
|
11
11
|
## Features
|
|
12
12
|
|
|
13
|
-
- **Document
|
|
14
|
-
- **Code symbol
|
|
15
|
-
- **
|
|
16
|
-
- **
|
|
13
|
+
- **Document memorization** — PDF, Markdown, and plain text files are chunked and summarized by the LLM; each entry carries a `path:line` citation back to the source.
|
|
14
|
+
- **Code symbol memory** — function, class, and method names with full type signatures (generics, parameters, return types, overloads) are extracted by a dependency-free source scanner (string/comment masking, multi-line signature joining, indentation-aware Python, class-method context), without LLM token usage.
|
|
15
|
+
- **Optional TypeScript semantic enhancement** — when `typescript` is installed in the user project (`npm i -D typescript`), the plugin automatically activates a second layer (L2) that uses the TS Compiler API to infer return types, resolve generics, extract interfaces and type aliases, and enrich arrow functions — all asynchronously in a priority queue (P0 on `fs/observed`, P1 on `watch`, P2 on `index_repo`). Results are cached on disk keyed by file content hash for instant cold-start reuse. Zero config: just install TS and restart dsh. Fully optional; if TS is absent or disabled via `enableTypeScript: false`, the plugin falls back to L1 regex-only extraction.
|
|
16
|
+
- **Automatic refresh** — a background poll (`watch_repo`) detects new or changed files by content hash and re-memorizes only those.
|
|
17
|
+
- **Read-time memorization** — files are memorized the moment the model actually reads them (`fs/observed`), so the memory is a byproduct of normal work, not a separate upfront scan. Files that are never read are never indexed. The project root is detected by markers (`.git`, `package.json`, …), a README plus source directories, or the file's own directory as a last resort.
|
|
17
18
|
- **Doc ↔ code cross-linking** — when a document mentions a symbol, the match is recorded as a `reference`; querying a symbol also surfaces the documents that describe it.
|
|
18
|
-
- **BM25
|
|
19
|
+
- **BM25 memory recall** — ranked search over documents, symbols, and experience notes, with optional LLM query expansion to handle vocabulary mismatch. **CJK-optimized**: precise phrase boost (3+ char phrases ×1.5 score on title/keywords match), synonym table (e.g. 数据库连接池 ↔ 连接池 ↔ DB pool), and CJK-aware word boundaries for doc↔symbol linking.
|
|
19
20
|
- **Experience notes** — problems → solutions; similar problems supersede instead of duplicating, and notes are returned only when a search matches. The note store is bounded: capacity scales with project size (clamped to 100–2000), and the oldest notes are pruned when the limit is exceeded. **Supersede tightened to bidirectional 0.7 overlap** (was 0.6); **experience `problem` field now participates in CJK phrase boost** for long-tail query recall.
|
|
21
|
+
- **Lock-free sync transactions** — all writes (index / watch / remember / forget / watch_repo) go through synchronous transactions `store.commit(fn)`; fn succeeds then atomic write; JS single-threaded event loop guarantees no interleaving; `remember`/`forget` never blocked by watch re-indexing.
|
|
20
22
|
- **Minimal dependencies** — pure JavaScript; the only runtime dependency is `pdfjs-dist` (PDF text extraction), no native builds required.
|
|
21
23
|
- **Negligible overhead** — pure in-process operation; cold start <100 ms (5k files), typical project query median 2–3 ms (p99 < 7 ms); bottleneck is LLM summarization and PDF parsing, not the plugin.
|
|
22
24
|
|
|
@@ -25,11 +27,11 @@ Persistent project memory for [DeepSeek Harness](https://github.com/deepseek-ai/
|
|
|
25
27
|
The design follows four principles:
|
|
26
28
|
|
|
27
29
|
- **Volatility** — context is ephemeral; it is lost when a session is compacted.
|
|
28
|
-
- **Persistence** — the
|
|
29
|
-
- **Compactness** — only summaries are stored; the
|
|
30
|
-
- **Verifiability** —
|
|
30
|
+
- **Persistence** — the **memory** is stored on disk and survives compaction and new sessions.
|
|
31
|
+
- **Compactness** — only summaries are stored; the **memory** runs around 0.5% the size of the source it covers (8.8 MB of source → 49 KB of index in the example project), so **recall** replaces re-reading the full file.
|
|
32
|
+
- **Verifiability** — **recalls** carry a `path:line` citation where applicable, so the agent can confirm details against the source.
|
|
31
33
|
|
|
32
|
-
Building the
|
|
34
|
+
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.
|
|
33
35
|
|
|
34
36
|
The store is per-project and follows the codebase: changed files are re-extracted by content hash, deleted files are removed. Experience notes are retrieval-only, so accumulation does not affect context.
|
|
35
37
|
|
|
@@ -52,7 +54,7 @@ dsh plugin --profile web add @yolk_vat-y/dsh-project-memory -w
|
|
|
52
54
|
A prebuilt tarball is published with each release, installable without a build step:
|
|
53
55
|
|
|
54
56
|
```bash
|
|
55
|
-
dsh plugin --profile web add /path/to/dsh-project-memory
|
|
57
|
+
dsh plugin --profile web add /path/to/dsh-project-memory.tgz
|
|
56
58
|
```
|
|
57
59
|
|
|
58
60
|
Each indexed project has its own store at `<root>/.dsh-project-memory/`. Add it to `.gitignore` if it should not be committed.
|
|
@@ -93,8 +95,8 @@ Stores created before v0.2.0 (single `entries.json` / `index.json`) migrate auto
|
|
|
93
95
|
|
|
94
96
|
These are deliberate scope choices.
|
|
95
97
|
|
|
96
|
-
- **
|
|
97
|
-
- **Watch
|
|
98
|
+
- **Lock-free sync transactions** — all writes (index / watch / remember / forget / watch_repo) go through synchronous transactions `store.commit(fn)`; fn succeeds then atomic write; JS single-threaded event loop guarantees no interleaving; `remember`/`forget` never blocked by watch re-indexing. Multi-instance concurrent writes to the same project store are naturally idempotent via CAS + atomic commits. We don't fear multi-process DSH because DSH is built on Cordis, and Cordis's single-process architecture is foundational — changing it would be a breaking change for the entire ecosystem.
|
|
99
|
+
- **Watch compute and commit separated** — watcher does heavy work (mtime/hash/symbol scan/LLM summary) outside transaction, then single `commit` applies atomically; `applyFileUpdate` uses CAS verification (unified null handling, delete skips hash compare) to prevent concurrent modification; failures rollback snapshot for next retry; indexing failures delete snapshot for auto-retry. Polling (mtime + content hash) instead of `fs.watch` events keeps behavior consistent across platforms.
|
|
98
100
|
- **Corrupt files are quarantined** — a store JSON that fails to parse falls back to empty for that file and is rebuilt on the next write; the broken file is renamed to `*.corrupt` with an error logged, but its data cannot be recovered. Auto-repairing partial writes would need a write-ahead journal or an embedded database — out of proportion when quarantining one bad file costs nothing.
|
|
99
101
|
- **`forget` by query is eager** — keyword deletion matches at ≥0.5 token overlap and may remove several notes at once; prefer deleting by id for precision.
|
|
100
102
|
- **Cross-language recall depends on index time** — with `llmQueryExpansion` off, a Chinese-only query reaches English content through bilingual keywords captured when docs are indexed, plus doc↔symbol links; queries stay LLM-free. Stores indexed before v0.1.1 gain bilingual keywords as files change, or immediately via `index_repo` with `reindex: true`.
|
|
@@ -116,6 +118,8 @@ These are deliberate scope choices.
|
|
|
116
118
|
| `autoIndexOnFirstUse` | false | full scan of the current working directory on plugin load (opt-in) |
|
|
117
119
|
| `watch` | true | enable the background refresh |
|
|
118
120
|
| `watchInterval` | 15 | poll interval (seconds) |
|
|
121
|
+
| `tsPath` | (auto) | optional absolute path to a specific `typescript` install; if omitted, resolves from project cwd → plugin node_modules |
|
|
122
|
+
| `enableTypeScript` | true | set `false` to disable L2 TS enhancement entirely (L1 regex only) |
|
|
119
123
|
|
|
120
124
|
### Toggling features
|
|
121
125
|
|
|
@@ -131,6 +135,8 @@ Settings live in the plugin's config object. To change them, add an override ent
|
|
|
131
135
|
llmQueryExpansion: false # off: do not spend tokens on LLM query expansion (default)
|
|
132
136
|
watch: true # on: background refresh for watched roots (default)
|
|
133
137
|
watchInterval: 15 # poll interval in seconds
|
|
138
|
+
enableTypeScript: true # on: L2 TS enhancement when TS is installed (default)
|
|
139
|
+
# tsPath: /custom/path/to/typescript # optional: force specific TS install
|
|
134
140
|
```
|
|
135
141
|
|
|
136
142
|
Only list the keys you want to change; the rest fall back to the plugin defaults. Verify the result with `dsh --profile web --dump-config`.
|
package/README.zh-CN.md
CHANGED
|
@@ -4,19 +4,21 @@
|
|
|
4
4
|
|
|
5
5
|
[](https://github.com/00080000/dsh-project-memory/actions/workflows/ci.yml) [](LICENSE) [](https://www.npmjs.com/package/@yolk_vat-y/dsh-project-memory) [](https://dsh-plugin.org/plugins/00080000/dsh-project-memory) [](https://awesome-dsh-plugin.com)
|
|
6
6
|
|
|
7
|
-
为 [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness)(dsh)agent
|
|
7
|
+
为 [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness)(dsh)agent 提供持久化的项目记忆。**记住**文档(PDF / Markdown / txt)与代码符号,写入每个工作区独立的存储库,自动维护更新,**回忆**时附源文件引用——文档自动交叉链接到其提及的代码符号。
|
|
8
8
|
|
|
9
|
-
>
|
|
9
|
+
> 插件在磁盘上维护一份精简的项目**记忆**,每条记录指向具体的文件与行号;agent 需要快速了解项目时先查**记忆**,无需重读整个项目。
|
|
10
10
|
|
|
11
11
|
## 特性
|
|
12
12
|
|
|
13
|
-
-
|
|
14
|
-
-
|
|
15
|
-
-
|
|
16
|
-
-
|
|
13
|
+
- **文档记忆** — PDF、Markdown、纯文本按块切分并由 LLM 生成摘要,每条记忆携带 `路径:行号` 引用回源文件。
|
|
14
|
+
- **代码符号记忆** — 通过零依赖的源码扫描器提取函数、类与方法名及完整类型签名(泛型、参数类型、返回类型、重载签名),包含字符串/注释掩码、多行签名续行、Python 缩进感知、类方法上下文,不使用 LLM token。
|
|
15
|
+
- **可选 TypeScript 语义增强** — 当用户项目安装了 `typescript`(`npm i -D typescript`),插件自动激活第二层(L2),利用 TS Compiler API 推导返回类型、实例化泛型、提取接口与类型别名、丰富箭头函数签名 —— 全部在优先级队列中异步后台处理(P0:`fs/observed` 读文件瞬间、P1:`watch` 变更后、P2:`index_repo` 批量索引)。结果按文件内容哈希缓存到磁盘,冷启动毫秒级复用。零配置:装 TS 再重启 dsh 即可。完全可选;若无 TS 或设置 `enableTypeScript: false`,回退至 L1 正则提取。
|
|
16
|
+
- **自动刷新** — `watch_repo` 后台轮询,按内容哈希识别新增或变更文件,仅重记这些文件。
|
|
17
|
+
- **读到即记忆** — 文件在模型**实际读取的瞬间**被记忆(监听 `fs/observed`),记忆是正常工作的副产品,而非额外的一次全量扫描。从未读过的文件不会被记忆。项目根通过标记(`.git`、`package.json` 等)、README 加源码目录、或兜底到文件所在目录逐级识别。
|
|
17
18
|
- **文档 ↔ 代码交叉链接** — 文档提及某符号时记录为 `reference`;查询符号时同时带出描述该符号的文档。
|
|
18
|
-
- **BM25
|
|
19
|
+
- **BM25 记忆召回** — 对文档、符号与经验笔记进行排序召回,可选 LLM 查询扩展以应对表述不一致。**CJK 增强**:精确短语乘法加分(3+ 字短语在标题/关键词命中 ×1.5)、同义词表(如 数据库连接池 ↔ 连接池 ↔ DB pool)、CJK 感知的文档↔符号链接边界。
|
|
19
20
|
- **经验笔记** — 记录问题 → 方案;相似问题覆盖而非重复;笔记仅在检索命中时返回。笔记数量有界:容量随项目规模伸缩(钳制在 100–2000),超限时淘汰最旧的笔记。**覆盖阈值收紧为双向 0.7 重叠**(原 0.6);**经验 `problem` 字段现参与 CJK 短语加分**,提升长尾问句召回。
|
|
21
|
+
- **无锁同步事务** — 不采用锁:所有写入(index / watch / remember / forget / watch_repo)统一走同步事务 `store.commit(fn)`,fn 成功后才一次落盘;JS 单线程事件循环保证事务间不交错,`remember`/`forget` 不会被 watch 重索引阻塞排队。多实例并发写入同一项目存储时,得益于 CAS 幂等更新与原子提交,自然具备幂等性,无数据损坏风险。
|
|
20
22
|
- **依赖极简** — 纯 JavaScript;唯一运行时依赖是 `pdfjs-dist`(PDF 文本提取),无需原生构建。
|
|
21
23
|
- **开销可忽略** — 纯进程内操作;冷启动 <100 ms(5k 文件),典型项目查询中位数 2–3 ms(p99 < 7 ms);瓶颈在 LLM 摘要与 PDF 解析,插件本身不阻塞。
|
|
22
24
|
|
|
@@ -25,11 +27,11 @@
|
|
|
25
27
|
设计遵循四个原则:
|
|
26
28
|
|
|
27
29
|
- **易失性** — 上下文是临时的,会话压缩即丢失。
|
|
28
|
-
- **持久性** —
|
|
29
|
-
- **紧凑性** —
|
|
30
|
-
- **可核验性** —
|
|
30
|
+
- **持久性** — **记忆**存于磁盘,跨压缩与会话保留。
|
|
31
|
+
- **紧凑性** — 仅存摘要;**记忆**规模约为其覆盖源码的 0.5%(示例项目中 8.8 MB 源码 → 49 KB 索引),**召回**替代了通读整个文件。
|
|
32
|
+
- **可核验性** — **召回**在适用时携带 `路径:行号` 引用,agent 可对照源文件核实。
|
|
31
33
|
|
|
32
|
-
|
|
34
|
+
构建**记忆**无需预先全量扫描:文件在模型读取时被记忆,**记忆**恰好覆盖实际处理过的内容。未变更的文件重读是空操作(内容哈希),因此**记忆**的持续维护开销很低。
|
|
33
35
|
|
|
34
36
|
存储按项目独立存放,并跟随代码库变化:文件变更按内容哈希重新抽取,文件删除则同步移除。经验层仅检索,累积不影响上下文。
|
|
35
37
|
|
|
@@ -52,7 +54,7 @@ dsh plugin --profile web add @yolk_vat-y/dsh-project-memory -w
|
|
|
52
54
|
每个版本会附带预构建 tarball,无需构建步骤即可安装:
|
|
53
55
|
|
|
54
56
|
```bash
|
|
55
|
-
dsh plugin --profile web add /path/to/dsh-project-memory
|
|
57
|
+
dsh plugin --profile web add /path/to/dsh-project-memory.tgz
|
|
56
58
|
```
|
|
57
59
|
|
|
58
60
|
每个被索引的项目在 `<root>/.dsh-project-memory/` 下有独立存储。如无需入库,可加入 `.gitignore`。
|
|
@@ -93,7 +95,7 @@ v0.2.0 之前创建的库(单文件 `entries.json` / `index.json`)在首次
|
|
|
93
95
|
|
|
94
96
|
以下是刻意的范围选择。
|
|
95
97
|
|
|
96
|
-
- **无锁同步事务** — 不采用锁:所有写入(index / watch / remember / forget / watch_repo)统一走同步事务 `store.commit(fn)`,fn 成功后才一次落盘;JS 单线程事件循环保证事务间不交错,`remember`/`forget` 不会被 watch
|
|
98
|
+
- **无锁同步事务** — 不采用锁:所有写入(index / watch / remember / forget / watch_repo)统一走同步事务 `store.commit(fn)`,fn 成功后才一次落盘;JS 单线程事件循环保证事务间不交错,`remember`/`forget` 不会被 watch 重索引阻塞排队。多实例并发写入同一项目存储时,得益于 CAS 幂等更新与原子提交,自然具备幂等性,无数据损坏风险。我们不担心 dsh 变多进程,因为 dsh 基于 Cordis,而 Cordis 的单进程架构是基础——改变它将是整个生态的破坏性变更。
|
|
97
99
|
- **watch 计算与提交分离** — watcher 在事务外完成 mtime/哈希/符号扫描/LLM 摘要等重活,再以单次 `commit` 原子应用全部变更;`applyFileUpdate` 用 CAS 校验(统一 null 处理、删除跳过对比)防并发修改,失败回滚 snapshot 下轮重试,索引失败删除 snapshot 自动重试。轮询(mtime + 内容哈希)而非 `fs.watch` 事件驱动,是为了跨平台行为一致。间隔可用 `watchInterval` 调整。
|
|
98
100
|
- **损坏隔离重建** — 存储 JSON 损坏时该文件回落为空并在下次写入时重建;坏文件会改名备份为 `*.corrupt` 并输出错误日志,但该文件内的数据无法恢复。自动修复半写文件需要预写日志或嵌入式数据库,代价与收益不成比例——而隔离一个坏文件的成本几乎为零。
|
|
99
101
|
- **`forget` 按关键词删除偏激进** — 关键词删除按 ≥0.5 token 重叠匹配,可能一次删掉多条;追求精确请用 id 删除。
|
|
@@ -116,6 +118,8 @@ v0.2.0 之前创建的库(单文件 `entries.json` / `index.json`)在首次
|
|
|
116
118
|
| `autoIndexOnFirstUse` | false | 插件加载时对当前工作目录做全量扫描(可选) |
|
|
117
119
|
| `watch` | true | 启用后台刷新 |
|
|
118
120
|
| `watchInterval` | 15 | 轮询间隔(秒) |
|
|
121
|
+
| `tsPath` | (自动) | 可选:强制指定特定 `typescript` 安装路径;省略时按项目 cwd → 插件 node_modules 向上解析 |
|
|
122
|
+
| `enableTypeScript` | true | 设为 `false` 彻底禁用 L2 TS 增强(仅保留 L1 正则) |
|
|
119
123
|
|
|
120
124
|
### 功能开关
|
|
121
125
|
|
|
@@ -131,6 +135,8 @@ v0.2.0 之前创建的库(单文件 `entries.json` / `index.json`)在首次
|
|
|
131
135
|
llmQueryExpansion: false # 关闭:不用 LLM 扩展查询,节省 token(默认)
|
|
132
136
|
watch: true # 开启:被监听根目录后台保持新鲜(默认)
|
|
133
137
|
watchInterval: 15 # 轮询间隔(秒)
|
|
138
|
+
enableTypeScript: true # 开启:装了 TS 时启用 L2 语义增强(默认)
|
|
139
|
+
# tsPath: /custom/path/to/typescript # 可选:强制指定 TS 安装路径
|
|
134
140
|
```
|
|
135
141
|
|
|
136
142
|
只需列出要改的键,其余键回落到插件默认值。用 `dsh --profile web --dump-config` 验证生效。
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@yolk_vat-y/dsh-project-memory",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.2",
|
|
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",
|
|
@@ -39,16 +39,18 @@
|
|
|
39
39
|
"pdfjs-dist": "^4.10.38"
|
|
40
40
|
},
|
|
41
41
|
"devDependencies": {
|
|
42
|
-
"@deepseek-ai/cordis": "4.0.
|
|
43
|
-
"@deepseek-ai/dsh-
|
|
44
|
-
"@deepseek-ai/
|
|
45
|
-
"@deepseek-ai/
|
|
42
|
+
"@deepseek-ai/cordis": "^4.0.2",
|
|
43
|
+
"@deepseek-ai/dsh-llm": "^0.1.2-alpha.2",
|
|
44
|
+
"@deepseek-ai/dsh-tools": "^0.1.2-alpha.2",
|
|
45
|
+
"@deepseek-ai/schemastery": "^3.18.2",
|
|
46
|
+
"typescript": "^5.6.3"
|
|
46
47
|
},
|
|
47
48
|
"peerDependencies": {
|
|
48
49
|
"@deepseek-ai/cordis": "^4.0.1",
|
|
50
|
+
"@deepseek-ai/dsh-llm": ">=0.0.1-rc.1 <0.1.0 || >=0.1.0-rc.1 <0.2.0-0 || >=0.1.1-rc.1 <0.2.0-0",
|
|
49
51
|
"@deepseek-ai/dsh-tools": ">=0.0.1-rc.1 <0.1.0 || >=0.1.0-rc.1 <0.2.0-0 || >=0.1.1-rc.1 <0.2.0-0",
|
|
50
52
|
"@deepseek-ai/schemastery": "^3.18.1",
|
|
51
|
-
"
|
|
53
|
+
"typescript": "^5.0.0"
|
|
52
54
|
},
|
|
53
55
|
"peerDependenciesMeta": {
|
|
54
56
|
"@deepseek-ai/cordis": {
|
|
@@ -62,6 +64,9 @@
|
|
|
62
64
|
},
|
|
63
65
|
"@deepseek-ai/dsh-llm": {
|
|
64
66
|
"optional": true
|
|
67
|
+
},
|
|
68
|
+
"typescript": {
|
|
69
|
+
"optional": true
|
|
65
70
|
}
|
|
66
71
|
},
|
|
67
72
|
"dsh": {
|
package/src/doc-pipeline.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto'
|
|
1
2
|
import path from 'node:path'
|
|
2
3
|
import { stat } from 'node:fs/promises'
|
|
3
4
|
import { looksLikeDump, readTextFile } from './util/fs.js'
|
|
@@ -27,6 +28,10 @@ export async function buildDocEntries(llm, a, b, c) {
|
|
|
27
28
|
const [relPath, filePath, opts] = c === undefined ? [a, a, b] : [a, b, c]
|
|
28
29
|
const text = await extractTextFromFile(filePath, opts)
|
|
29
30
|
if (looksLikeDump(text)) return null
|
|
31
|
+
|
|
32
|
+
// Compute content hash for update detection
|
|
33
|
+
const hash = createHash('sha256').update(text).digest('hex').slice(0, 16)
|
|
34
|
+
|
|
30
35
|
const chunks = chunkText(text, opts.chunkChars, opts.maxChunks)
|
|
31
36
|
const metas = new Array(chunks.length)
|
|
32
37
|
let cursor = 0
|
|
@@ -47,7 +52,9 @@ export async function buildDocEntries(llm, a, b, c) {
|
|
|
47
52
|
type: 'doc',
|
|
48
53
|
title: meta.title,
|
|
49
54
|
summary: meta.summary,
|
|
55
|
+
blindSpots: meta.blindSpots || '',
|
|
50
56
|
keywords: meta.keywords,
|
|
57
|
+
hash,
|
|
51
58
|
}))
|
|
52
59
|
}
|
|
53
60
|
|
package/src/enhancer.js
ADDED
|
@@ -0,0 +1,444 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto'
|
|
2
|
+
import { readFileSync, writeFileSync, mkdirSync, existsSync } from 'node:fs'
|
|
3
|
+
import { join } from 'node:path'
|
|
4
|
+
import { createRequire } from 'node:module'
|
|
5
|
+
import { linkEntries } from './link.js'
|
|
6
|
+
|
|
7
|
+
const require = createRequire(import.meta.url)
|
|
8
|
+
|
|
9
|
+
let ts = null
|
|
10
|
+
let tsPath = null
|
|
11
|
+
let tsVersion = null
|
|
12
|
+
|
|
13
|
+
function acceptTsVersion(mod) {
|
|
14
|
+
if (mod && mod.version && (mod.version.startsWith('6.') || mod.version.startsWith('7.'))) {
|
|
15
|
+
console.warn(`[dsh-project-memory] TypeScript ${mod.version} 不受支持,回退到 L1 正则。请使用 TS 5.x 获得增强类型。`)
|
|
16
|
+
return null
|
|
17
|
+
}
|
|
18
|
+
return mod
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function initTypeScript(config) {
|
|
22
|
+
if (config?.enableTypeScript === false) return null
|
|
23
|
+
if (ts) return ts
|
|
24
|
+
|
|
25
|
+
// 1. 配置指定路径
|
|
26
|
+
if (config?.tsPath) {
|
|
27
|
+
try {
|
|
28
|
+
const req = createRequire(config.tsPath)
|
|
29
|
+
const mod = acceptTsVersion(req(config.tsPath))
|
|
30
|
+
if (mod) {
|
|
31
|
+
ts = mod
|
|
32
|
+
tsPath = config.tsPath
|
|
33
|
+
tsVersion = mod.version
|
|
34
|
+
return ts
|
|
35
|
+
}
|
|
36
|
+
} catch (e) {
|
|
37
|
+
console.warn(`[dsh-project-memory] tsPath 无效: ${config.tsPath}`)
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// 2. 从用户项目 cwd 向上查找 node_modules/typescript
|
|
42
|
+
try {
|
|
43
|
+
const req = createRequire(process.cwd() + '/')
|
|
44
|
+
const mod = acceptTsVersion(req('typescript'))
|
|
45
|
+
if (mod) {
|
|
46
|
+
ts = mod
|
|
47
|
+
tsPath = req.resolve('typescript')
|
|
48
|
+
tsVersion = mod.version
|
|
49
|
+
return ts
|
|
50
|
+
}
|
|
51
|
+
} catch {}
|
|
52
|
+
|
|
53
|
+
// 3. 全局(从插件自身位置解析)
|
|
54
|
+
try {
|
|
55
|
+
const mod = acceptTsVersion(require('typescript'))
|
|
56
|
+
if (mod) {
|
|
57
|
+
ts = mod
|
|
58
|
+
tsPath = require.resolve('typescript')
|
|
59
|
+
tsVersion = mod.version
|
|
60
|
+
return ts
|
|
61
|
+
}
|
|
62
|
+
} catch {}
|
|
63
|
+
|
|
64
|
+
ts = null
|
|
65
|
+
return null
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function getTypeScript() {
|
|
69
|
+
return ts
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function hasTypeScript() {
|
|
73
|
+
return ts !== null
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function getTsPath() {
|
|
77
|
+
return tsPath
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function getTsVersion() {
|
|
81
|
+
return tsVersion
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const PRIORITY = {
|
|
85
|
+
ACTIVE: 0,
|
|
86
|
+
RECENT: 1,
|
|
87
|
+
BATCH: 2,
|
|
88
|
+
BACKLOG: 3
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const enhanceQueue = []
|
|
92
|
+
let processing = false
|
|
93
|
+
|
|
94
|
+
function getCacheDirForRoot(root, config) {
|
|
95
|
+
return join(root, config.memoryDir || '.dsh-project-memory', 'type-cache')
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function getCacheKey(content) {
|
|
99
|
+
const hash = createHash('sha256').update(content).digest('hex').slice(0, 16)
|
|
100
|
+
return hash
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
async function loadTypeCache(cacheDir, key) {
|
|
104
|
+
const file = join(cacheDir, `${key}.json`)
|
|
105
|
+
if (!existsSync(file)) return null
|
|
106
|
+
try {
|
|
107
|
+
const data = JSON.parse(readFileSync(file, 'utf8'))
|
|
108
|
+
return data
|
|
109
|
+
} catch {
|
|
110
|
+
return null
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
async function saveTypeCache(cacheDir, key, data) {
|
|
115
|
+
try {
|
|
116
|
+
if (!existsSync(cacheDir)) mkdirSync(cacheDir, { recursive: true })
|
|
117
|
+
const file = join(cacheDir, `${key}.json`)
|
|
118
|
+
writeFileSync(file, JSON.stringify(data))
|
|
119
|
+
} catch {}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export function isTypeScriptFile(filePath) {
|
|
123
|
+
const ext = filePath.slice(filePath.lastIndexOf('.')).toLowerCase()
|
|
124
|
+
return ext === '.ts' || ext === '.tsx' || ext === '.js' || ext === '.jsx' ||
|
|
125
|
+
ext === '.mjs' || ext === '.cjs' || ext === '.mts' || ext === '.cts'
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export function deepParseWithTS(filePath, content) {
|
|
129
|
+
if (!ts) return null
|
|
130
|
+
|
|
131
|
+
// Create a compiler host that provides the source file from memory
|
|
132
|
+
const host = {
|
|
133
|
+
getSourceFile: (fileName, languageVersion, onError) => {
|
|
134
|
+
if (fileName === filePath) {
|
|
135
|
+
return ts.createSourceFile(filePath, content, ts.ScriptTarget.Latest, true)
|
|
136
|
+
}
|
|
137
|
+
return undefined
|
|
138
|
+
},
|
|
139
|
+
getDefaultLibFileName: (options) => ts.getDefaultLibFileName(options),
|
|
140
|
+
getDefaultLibLocation: () => ts.getDefaultLibFilePath({}),
|
|
141
|
+
getCanonicalFileName: (fileName) => fileName,
|
|
142
|
+
getCurrentDirectory: () => process.cwd(),
|
|
143
|
+
getNewLine: () => '\n',
|
|
144
|
+
useCaseSensitiveFileNames: () => true,
|
|
145
|
+
fileExists: (fileName) => fileName === filePath,
|
|
146
|
+
readFile: (fileName) => fileName === filePath ? content : undefined,
|
|
147
|
+
directoryExists: () => true,
|
|
148
|
+
getDirectories: () => [],
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const program = ts.createProgram([filePath], {}, host)
|
|
152
|
+
const sourceFile = program.getSourceFile(filePath)
|
|
153
|
+
if (!sourceFile) return []
|
|
154
|
+
const checker = program.getTypeChecker()
|
|
155
|
+
|
|
156
|
+
const symbols = []
|
|
157
|
+
|
|
158
|
+
function getSignature(node) {
|
|
159
|
+
try {
|
|
160
|
+
return checker.getSignatureFromDeclaration(node)
|
|
161
|
+
} catch {
|
|
162
|
+
return undefined
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function getReturnType(signature) {
|
|
167
|
+
if (!signature) return null
|
|
168
|
+
try {
|
|
169
|
+
return checker.getReturnTypeOfSignature(signature)
|
|
170
|
+
} catch {
|
|
171
|
+
return null
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function getTypeStr(type) {
|
|
176
|
+
if (!type) return 'void'
|
|
177
|
+
try {
|
|
178
|
+
return checker.typeToString(type)
|
|
179
|
+
} catch {
|
|
180
|
+
return 'any'
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function getLine(node) {
|
|
185
|
+
const pos = node.getStart()
|
|
186
|
+
if (pos < 0) return 1
|
|
187
|
+
return sourceFile.getLineAndCharacterOfPosition(pos).line + 1
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function visit(node) {
|
|
191
|
+
if (ts.isFunctionDeclaration(node) && node.name) {
|
|
192
|
+
const signature = getSignature(node)
|
|
193
|
+
const returnType = getReturnType(signature)
|
|
194
|
+
const typeParams = node.typeParameters?.map(tp => tp.getText()) || []
|
|
195
|
+
const params = node.parameters.map(p => {
|
|
196
|
+
const type = p.type ? getTypeStr(checker.getTypeAtLocation(p.type)) : 'any'
|
|
197
|
+
return `${p.name.getText()}: ${type}`
|
|
198
|
+
})
|
|
199
|
+
const returnTypeStr = getTypeStr(returnType)
|
|
200
|
+
const generics = typeParams.length ? `<${typeParams.join(', ')}>` : ''
|
|
201
|
+
symbols.push({
|
|
202
|
+
name: node.name.getText(),
|
|
203
|
+
kind: 'function',
|
|
204
|
+
typeSig: `${generics}(${params.join(', ')}): ${returnTypeStr}`,
|
|
205
|
+
line: getLine(node)
|
|
206
|
+
})
|
|
207
|
+
} else if (ts.isMethodDeclaration(node) && node.name) {
|
|
208
|
+
const signature = getSignature(node)
|
|
209
|
+
const returnType = getReturnType(signature)
|
|
210
|
+
const typeParams = node.typeParameters?.map(tp => tp.getText()) || []
|
|
211
|
+
const params = node.parameters.map(p => {
|
|
212
|
+
const type = p.type ? getTypeStr(checker.getTypeAtLocation(p.type)) : 'any'
|
|
213
|
+
return `${p.name.getText()}: ${type}`
|
|
214
|
+
})
|
|
215
|
+
const returnTypeStr = getTypeStr(returnType)
|
|
216
|
+
const generics = typeParams.length ? `<${typeParams.join(', ')}>` : ''
|
|
217
|
+
symbols.push({
|
|
218
|
+
name: node.name.getText(),
|
|
219
|
+
kind: 'method',
|
|
220
|
+
typeSig: `${generics}(${params.join(', ')}): ${returnTypeStr}`,
|
|
221
|
+
line: getLine(node)
|
|
222
|
+
})
|
|
223
|
+
} else if (ts.isArrowFunction(node) || ts.isFunctionExpression(node)) {
|
|
224
|
+
const parent = node.parent
|
|
225
|
+
if (parent && (ts.isVariableDeclaration(parent) || ts.isPropertyAssignment(parent) || ts.isPropertyDeclaration(parent))) {
|
|
226
|
+
const signature = getSignature(node)
|
|
227
|
+
const returnType = getReturnType(signature)
|
|
228
|
+
const typeParams = node.typeParameters?.map(tp => tp.getText()) || []
|
|
229
|
+
const params = node.parameters.map(p => {
|
|
230
|
+
const type = p.type ? getTypeStr(checker.getTypeAtLocation(p.type)) : 'any'
|
|
231
|
+
return `${p.name.getText()}: ${type}`
|
|
232
|
+
})
|
|
233
|
+
const returnTypeStr = getTypeStr(returnType)
|
|
234
|
+
const generics = typeParams.length ? `<${typeParams.join(', ')}>` : ''
|
|
235
|
+
const name = parent.name?.getText() || '(anonymous)'
|
|
236
|
+
const kind = ts.isArrowFunction(node) ? 'arrow' : 'function'
|
|
237
|
+
symbols.push({
|
|
238
|
+
name,
|
|
239
|
+
kind,
|
|
240
|
+
typeSig: `${generics}(${params.join(', ')}): ${returnTypeStr}`,
|
|
241
|
+
line: getLine(node)
|
|
242
|
+
})
|
|
243
|
+
}
|
|
244
|
+
} else if (ts.isClassDeclaration(node) && node.name) {
|
|
245
|
+
const typeParams = node.typeParameters?.map(tp => tp.getText()) || []
|
|
246
|
+
const generics = typeParams.length ? `<${typeParams.join(', ')}>` : ''
|
|
247
|
+
symbols.push({
|
|
248
|
+
name: node.name.getText(),
|
|
249
|
+
kind: 'class',
|
|
250
|
+
typeSig: generics,
|
|
251
|
+
line: getLine(node)
|
|
252
|
+
})
|
|
253
|
+
ts.forEachChild(node, visit)
|
|
254
|
+
} else if (ts.isInterfaceDeclaration(node)) {
|
|
255
|
+
const members = node.members.map(m => {
|
|
256
|
+
// Skip index signatures (m.name is undefined for index signatures like [key: string]: T)
|
|
257
|
+
if (!m.name) return null
|
|
258
|
+
const type = m.type ? getTypeStr(checker.getTypeAtLocation(m.type)) : 'any'
|
|
259
|
+
return `${m.name.getText()}: ${type}`
|
|
260
|
+
}).filter(Boolean).join('; ')
|
|
261
|
+
symbols.push({
|
|
262
|
+
name: node.name.getText(),
|
|
263
|
+
kind: 'interface',
|
|
264
|
+
typeSig: `{ ${members} }`,
|
|
265
|
+
line: getLine(node)
|
|
266
|
+
})
|
|
267
|
+
} else if (ts.isTypeAliasDeclaration(node)) {
|
|
268
|
+
const typeStr = node.type ? getTypeStr(checker.getTypeAtLocation(node.type)) : 'any'
|
|
269
|
+
symbols.push({
|
|
270
|
+
name: node.name.getText(),
|
|
271
|
+
kind: 'type',
|
|
272
|
+
typeSig: `= ${typeStr}`,
|
|
273
|
+
line: getLine(node)
|
|
274
|
+
})
|
|
275
|
+
} else {
|
|
276
|
+
ts.forEachChild(node, visit)
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
const sf = program.getSourceFile(filePath)
|
|
281
|
+
if (sf) {
|
|
282
|
+
visit(sf)
|
|
283
|
+
}
|
|
284
|
+
return symbols
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
export function enqueueEnhance(store, relPath, filePath, priority = PRIORITY.BATCH, config, root) {
|
|
288
|
+
if (!ts) return Promise.resolve()
|
|
289
|
+
|
|
290
|
+
let content
|
|
291
|
+
try {
|
|
292
|
+
content = readFileSync(filePath, 'utf8')
|
|
293
|
+
} catch {
|
|
294
|
+
// File disappeared between detection and enqueue - skip silently
|
|
295
|
+
return Promise.resolve()
|
|
296
|
+
}
|
|
297
|
+
const cacheKey = getCacheKey(content)
|
|
298
|
+
|
|
299
|
+
// Dedupe by (relPath, contentHash) - if same content, skip; if different, replace
|
|
300
|
+
const existingIdx = enhanceQueue.findIndex(q => q.relPath === relPath)
|
|
301
|
+
if (existingIdx >= 0) {
|
|
302
|
+
const existing = enhanceQueue[existingIdx]
|
|
303
|
+
if (existing.cacheKey === cacheKey) {
|
|
304
|
+
// Same content, just update priority if higher
|
|
305
|
+
if (priority < existing.priority) {
|
|
306
|
+
enhanceQueue[existingIdx].priority = priority
|
|
307
|
+
enhanceQueue.sort((a, b) => a.priority - b.priority)
|
|
308
|
+
}
|
|
309
|
+
return existing.promise
|
|
310
|
+
} else {
|
|
311
|
+
// Content changed - replace the task
|
|
312
|
+
enhanceQueue[existingIdx] = { relPath, filePath, priority, cacheKey, promise: null }
|
|
313
|
+
// Will create new promise below
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
const p = (async () => {
|
|
318
|
+
try {
|
|
319
|
+
const content = readFileSync(filePath, 'utf8')
|
|
320
|
+
const cacheKey = getCacheKey(content)
|
|
321
|
+
const cacheDir = getCacheDirForRoot(root || process.cwd(), config)
|
|
322
|
+
const cached = await loadTypeCache(cacheDir, cacheKey)
|
|
323
|
+
if (cached) {
|
|
324
|
+
// Cache hit: persist via store.commit
|
|
325
|
+
await store.commit(fn => applyEnhancedSymbols(fn, relPath, cached.symbols))
|
|
326
|
+
return
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
const enhanced = deepParseWithTS(filePath, content)
|
|
330
|
+
if (enhanced?.length) {
|
|
331
|
+
await saveTypeCache(cacheDir, cacheKey, { symbols: enhanced })
|
|
332
|
+
await store.commit(fn => applyEnhancedSymbols(fn, relPath, enhanced))
|
|
333
|
+
}
|
|
334
|
+
} catch (err) {
|
|
335
|
+
console.warn(`[dsh-project-memory] enhance failed for ${relPath}: ${err.message}`)
|
|
336
|
+
} finally {
|
|
337
|
+
// Remove only this task's own queue entry (not a newer one for the same relPath)
|
|
338
|
+
const idx = enhanceQueue.findIndex(q => q.promise === p)
|
|
339
|
+
if (idx >= 0) enhanceQueue.splice(idx, 1)
|
|
340
|
+
}
|
|
341
|
+
})()
|
|
342
|
+
|
|
343
|
+
// Update or add to queue
|
|
344
|
+
const existingIdx2 = enhanceQueue.findIndex(q => q.relPath === relPath)
|
|
345
|
+
if (existingIdx2 >= 0) {
|
|
346
|
+
enhanceQueue[existingIdx2] = { relPath, filePath, priority, cacheKey, promise: p }
|
|
347
|
+
} else {
|
|
348
|
+
enhanceQueue.push({ relPath, filePath, priority, cacheKey, promise: p })
|
|
349
|
+
}
|
|
350
|
+
enhanceQueue.sort((a, b) => a.priority - b.priority)
|
|
351
|
+
|
|
352
|
+
scheduleProcess()
|
|
353
|
+
return p
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
function scheduleProcess() {
|
|
357
|
+
if (processing) return
|
|
358
|
+
processing = true
|
|
359
|
+
|
|
360
|
+
const processNext = async () => {
|
|
361
|
+
while (enhanceQueue.length > 0) {
|
|
362
|
+
const task = enhanceQueue.shift()
|
|
363
|
+
try {
|
|
364
|
+
await task.promise
|
|
365
|
+
} catch (err) {
|
|
366
|
+
console.warn(`[dsh-project-memory] enhance task failed: ${err.message}`)
|
|
367
|
+
}
|
|
368
|
+
await new Promise(r => setImmediate(r))
|
|
369
|
+
}
|
|
370
|
+
processing = false
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
processNext()
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
function applyEnhancedSymbols(fn, relPath, enhanced) {
|
|
377
|
+
// Store entries in fn.entries[relPath], not fn.files[relPath].entries
|
|
378
|
+
const existingEntries = fn.entries[relPath] || []
|
|
379
|
+
// L1 entries carry title "name (kind)" + sourceLine but no name/line fields
|
|
380
|
+
const nameOf = e => (e.title || '').replace(/\s*\([^)]*\)\s*$/, '')
|
|
381
|
+
const enhancedByLine = new Map(enhanced.map(s => [s.line, s]))
|
|
382
|
+
const existingKeys = new Set(existingEntries.map(e => `${nameOf(e)}#${e.sourceLine}`))
|
|
383
|
+
|
|
384
|
+
// Upgrade L1 entries in place only when TS found the same symbol on the same line
|
|
385
|
+
const mergedEntries = existingEntries.map(e => {
|
|
386
|
+
const enh = enhancedByLine.get(e.sourceLine)
|
|
387
|
+
if (!enh || nameOf(e) !== enh.name) return e
|
|
388
|
+
return {
|
|
389
|
+
...e,
|
|
390
|
+
text: `${enh.name}${enh.typeSig} -- ${relPath}:${enh.line}`,
|
|
391
|
+
typeSig: enh.typeSig,
|
|
392
|
+
enhanced: true
|
|
393
|
+
}
|
|
394
|
+
})
|
|
395
|
+
|
|
396
|
+
// Add enhanced symbols that L1 missed; keep ids unique even when several
|
|
397
|
+
// symbols share one line (e.g. one-line class + method)
|
|
398
|
+
const usedIds = new Set(existingEntries.map(e => e.id))
|
|
399
|
+
const newEntries = []
|
|
400
|
+
for (const s of enhanced) {
|
|
401
|
+
if (existingKeys.has(`${s.name}#${s.line}`)) continue
|
|
402
|
+
const base = `${relPath.replace(/[\/:\s]/g, '_')}#${s.line}`
|
|
403
|
+
let id = base
|
|
404
|
+
if (usedIds.has(id)) id = `${base}-${s.kind}`
|
|
405
|
+
for (let n = 2; usedIds.has(id); n++) id = `${base}-${s.kind}-${n}`
|
|
406
|
+
usedIds.add(id)
|
|
407
|
+
newEntries.push({
|
|
408
|
+
id,
|
|
409
|
+
sourcePath: relPath,
|
|
410
|
+
sourceLine: s.line,
|
|
411
|
+
type: 'symbol',
|
|
412
|
+
title: `${s.name} (${s.kind})`,
|
|
413
|
+
keywords: [s.name, s.kind],
|
|
414
|
+
text: `${s.name}${s.typeSig} -- ${relPath}:${s.line}`,
|
|
415
|
+
enhanced: true
|
|
416
|
+
})
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
// Write to store.entries via setEntries so the shard is marked dirty and persisted
|
|
420
|
+
fn.setEntries(relPath, [...mergedEntries, ...newEntries])
|
|
421
|
+
|
|
422
|
+
// Refresh doc<->symbol links for newly added symbols
|
|
423
|
+
if (newEntries.length) linkEntries(fn)
|
|
424
|
+
|
|
425
|
+
// Also update fn.files metadata
|
|
426
|
+
if (fn.files[relPath]) {
|
|
427
|
+
fn.files[relPath].enhanced = true
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
export function onFileObserved(store, relPath, filePath, config, root) {
|
|
432
|
+
if (!isTypeScriptFile(filePath)) return
|
|
433
|
+
enqueueEnhance(store, relPath, filePath, PRIORITY.ACTIVE, config, root)
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
export function onFileChanged(store, relPath, filePath, config, root) {
|
|
437
|
+
if (!isTypeScriptFile(filePath)) return
|
|
438
|
+
enqueueEnhance(store, relPath, filePath, PRIORITY.RECENT, config, root)
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
export function onFileIndexed(store, relPath, filePath, config, root) {
|
|
442
|
+
if (!isTypeScriptFile(filePath)) return
|
|
443
|
+
enqueueEnhance(store, relPath, filePath, PRIORITY.BATCH, config, root)
|
|
444
|
+
}
|
package/src/index.js
CHANGED
|
@@ -8,6 +8,7 @@ import { watchRepoTool } from './tools/watch-repo.js'
|
|
|
8
8
|
import { statsTool } from './tools/stats.js'
|
|
9
9
|
import { WatchManager } from './watch.js'
|
|
10
10
|
import { setupLazyIndexing } from './lazy.js'
|
|
11
|
+
import { initTypeScript } from './enhancer.js'
|
|
11
12
|
|
|
12
13
|
export const name = 'dsh-project-memory'
|
|
13
14
|
export const inject = ['llm', 'tools']
|
|
@@ -25,9 +26,16 @@ export const Config = Schema.object({
|
|
|
25
26
|
autoIndexOnFirstUse: Schema.boolean().default(false),
|
|
26
27
|
watch: Schema.boolean().default(true),
|
|
27
28
|
watchInterval: Schema.number().default(15),
|
|
29
|
+
tsPath: Schema.string(),
|
|
30
|
+
enableTypeScript: Schema.boolean().default(true),
|
|
28
31
|
})
|
|
29
32
|
|
|
30
33
|
export function apply(ctx, config) {
|
|
34
|
+
// Initialize TypeScript enhancer if enabled
|
|
35
|
+
if (config.enableTypeScript !== false) {
|
|
36
|
+
initTypeScript(config)
|
|
37
|
+
}
|
|
38
|
+
|
|
31
39
|
const watchManager = new WatchManager(ctx, config)
|
|
32
40
|
if (config.watch) {
|
|
33
41
|
watchManager.restorePersisted()
|
package/src/lazy.js
CHANGED
|
@@ -6,6 +6,7 @@ import { buildDocEntries } from './doc-pipeline.js'
|
|
|
6
6
|
import { scanSymbols } from './symbols.js'
|
|
7
7
|
import { linkEntries } from './link.js'
|
|
8
8
|
import { ProjectMemoryStore } from './store.js'
|
|
9
|
+
import { onFileObserved } from './enhancer.js'
|
|
9
10
|
|
|
10
11
|
const STRONG_MARKERS = ['.git', '.hg', '.svn']
|
|
11
12
|
|
|
@@ -87,6 +88,7 @@ export async function indexFile(ctx, config, filePath, watchManager = null) {
|
|
|
87
88
|
const existing = store.fileRecord(rel)
|
|
88
89
|
let hash
|
|
89
90
|
let size
|
|
91
|
+
let entries
|
|
90
92
|
try {
|
|
91
93
|
if (isSupportedCode(ext) && config.maxFileSizeMb && statSync(filePath).size > config.maxFileSizeMb * 1024 * 1024) {
|
|
92
94
|
return false
|
|
@@ -102,9 +104,9 @@ export async function indexFile(ctx, config, filePath, watchManager = null) {
|
|
|
102
104
|
store.addWatch(root)
|
|
103
105
|
}
|
|
104
106
|
|
|
105
|
-
let entries
|
|
106
107
|
if (isSupportedCode(ext)) {
|
|
107
108
|
entries = scanSymbols(rel, filePath, readFileSync(filePath, 'utf8'))
|
|
109
|
+
onFileObserved(store, rel, filePath, config, root)
|
|
108
110
|
return store.commit((s) => {
|
|
109
111
|
s.markFile(rel, { sha256: hash, size, type: 'code', indexedAt: new Date().toISOString() })
|
|
110
112
|
s.setEntries(rel, entries)
|
package/src/llm.js
CHANGED
|
@@ -96,10 +96,15 @@ export async function expandQuery(llm, query, count = 6) {
|
|
|
96
96
|
export async function extractDocEntry(llm, chunk, sourcePath) {
|
|
97
97
|
const system =
|
|
98
98
|
'You are a project-documentation indexer. Given a chunk of a project document, ' +
|
|
99
|
-
'return a STRICT JSON object with exactly
|
|
100
|
-
'"title" (short section title, string),
|
|
101
|
-
'
|
|
102
|
-
'
|
|
99
|
+
'return a STRICT JSON object with exactly four fields: ' +
|
|
100
|
+
'"title" (short section title, string), ' +
|
|
101
|
+
'"summary" (3-5 sentence dense summary of what this section covers, ' +
|
|
102
|
+
'mentioning concrete names, decisions, constraints, and key technical details), ' +
|
|
103
|
+
'"blindSpots" (string describing what this summary does NOT cover, ' +
|
|
104
|
+
'e.g. "未覆盖:部署细节、性能基准、v0.2 前 API", empty string if none), ' +
|
|
105
|
+
'"keywords" (array of 5-10 searchable strings: ' +
|
|
106
|
+
'cover the document\'s own language AND English equivalents, ' +
|
|
107
|
+
'so a query in either language can match). ' +
|
|
103
108
|
'Do not include markdown fences, do not add commentary, output only the JSON object.'
|
|
104
109
|
|
|
105
110
|
const user =
|
|
@@ -109,6 +114,7 @@ export async function extractDocEntry(llm, chunk, sourcePath) {
|
|
|
109
114
|
const fallback = () => ({
|
|
110
115
|
title: chunk.title || sourcePath,
|
|
111
116
|
summary: summarizeText(chunk.text),
|
|
117
|
+
blindSpots: '',
|
|
112
118
|
keywords: tokenize(chunk.title).slice(0, 5),
|
|
113
119
|
})
|
|
114
120
|
|
|
@@ -121,7 +127,8 @@ export async function extractDocEntry(llm, chunk, sourcePath) {
|
|
|
121
127
|
const kw = Array.isArray(parsed.keywords) ? parsed.keywords.map(String).filter((k) => k).slice(0, 8) : []
|
|
122
128
|
return {
|
|
123
129
|
title: typeof parsed.title === 'string' && parsed.title.trim() ? parsed.title.trim() : chunk.title || sourcePath,
|
|
124
|
-
summary: summarizeText(parsed.summary),
|
|
130
|
+
summary: summarizeText(parsed.summary.trim()),
|
|
131
|
+
blindSpots: typeof parsed.blindSpots === 'string' ? parsed.blindSpots.trim() : '',
|
|
125
132
|
keywords: kw.length ? kw : tokenize(chunk.title).slice(0, 5),
|
|
126
133
|
}
|
|
127
134
|
} catch {
|
package/src/symbols.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { readFileSync } from 'node:fs'
|
|
2
2
|
|
|
3
|
-
const JS_LIKE = new Set(['.js', '.mjs', '.cjs', '.ts', '.jsx', '.tsx'])
|
|
3
|
+
const JS_LIKE = new Set(['.js', '.mjs', '.cjs', '.ts', '.mts', '.cts', '.jsx', '.tsx'])
|
|
4
4
|
const PYTHON = new Set(['.py'])
|
|
5
5
|
const GO = new Set(['.go'])
|
|
6
6
|
const RUST = new Set(['.rs'])
|
|
@@ -96,19 +96,121 @@ function balanceDelta(text) {
|
|
|
96
96
|
return delta
|
|
97
97
|
}
|
|
98
98
|
|
|
99
|
+
function extractTypeSignature(line) {
|
|
100
|
+
let sig = ''
|
|
101
|
+
|
|
102
|
+
// 1. Generics: <T, U extends Base> - but NOT the return type generics like Promise<T>
|
|
103
|
+
// Match generics that appear BEFORE the first ( (function params) or => (arrow)
|
|
104
|
+
// For: function foo<T>(a: T): R
|
|
105
|
+
// For: const foo = <T>(a: T): R =>
|
|
106
|
+
// For: class Foo<T> { method() }
|
|
107
|
+
const beforeParams = line.split(/\(|=>/)[0]
|
|
108
|
+
const genericMatch = beforeParams.match(/<[^<>]*(?:<[^<>]*>[^<>]*)*>/)
|
|
109
|
+
if (genericMatch) sig += genericMatch[0]
|
|
110
|
+
|
|
111
|
+
// 2. Parameters: (a: string, b: number) - handle nested parentheses
|
|
112
|
+
let paramMatch = null
|
|
113
|
+
const parenIdx = line.indexOf('(')
|
|
114
|
+
if (parenIdx >= 0) {
|
|
115
|
+
let depth = 0
|
|
116
|
+
for (let i = parenIdx; i < line.length; i++) {
|
|
117
|
+
if (line[i] === '(') depth++
|
|
118
|
+
else if (line[i] === ')') {
|
|
119
|
+
depth--
|
|
120
|
+
if (depth === 0) {
|
|
121
|
+
paramMatch = line.slice(parenIdx, i + 1)
|
|
122
|
+
// 3. Return type: ONLY after the closing ) of params
|
|
123
|
+
const afterParams = line.slice(i + 1)
|
|
124
|
+
// Match return type including nested generics, stop at { ; =>
|
|
125
|
+
const retMatch = afterParams.match(/^\s*:\s*([^{;=]+)/)
|
|
126
|
+
if (retMatch) sig += paramMatch + `: ${retMatch[1].trim()}`
|
|
127
|
+
else sig += paramMatch
|
|
128
|
+
break
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// 3b. Arrow function without parens: foo = (x): R => or foo = x: R =>
|
|
135
|
+
if (!paramMatch) {
|
|
136
|
+
const arrowMatch = line.match(/=>/)
|
|
137
|
+
if (arrowMatch) {
|
|
138
|
+
const afterArrow = line.slice(arrowMatch.index + 2)
|
|
139
|
+
const retMatch = afterArrow.match(/^\s*([^{;=]+)/)
|
|
140
|
+
if (retMatch) sig += `: ${retMatch[1].trim()}`
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
return sig
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function extractInterfaceOrType(line) {
|
|
148
|
+
// interface User { name: string; age: number }
|
|
149
|
+
const ifaceMatch = line.match(/^interface\s+(\w+)\s*(?:extends\s+[^{]+)?\s*\{([^}]*)\}/)
|
|
150
|
+
if (ifaceMatch) {
|
|
151
|
+
return `{ ${ifaceMatch[2].trim()} }`
|
|
152
|
+
}
|
|
153
|
+
// type UserMap = Map<string, User>
|
|
154
|
+
const typeMatch = line.match(/^type\s+(\w+)\s*=\s*([^;{]+)/)
|
|
155
|
+
if (typeMatch) {
|
|
156
|
+
return `= ${typeMatch[2].trim()}`
|
|
157
|
+
}
|
|
158
|
+
return ''
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function extractOverloads(masked, startIdx) {
|
|
162
|
+
const overloads = []
|
|
163
|
+
for (let i = startIdx; i < masked.length; i++) {
|
|
164
|
+
const text = masked[i].trim()
|
|
165
|
+
if (!text) continue
|
|
166
|
+
|
|
167
|
+
// Skip comments
|
|
168
|
+
if (text.startsWith('//') || text.startsWith('/*')) continue
|
|
169
|
+
|
|
170
|
+
// Check for function declaration (with or without export/async/default)
|
|
171
|
+
const fnMatch = text.match(/^(?:export\s+)?(?:default\s+)?(?:async\s+)?function\s+\w+\s*/)
|
|
172
|
+
if (!fnMatch) break
|
|
173
|
+
|
|
174
|
+
// Extract ONLY the signature part (generics + params + return), NOT the function name
|
|
175
|
+
const sig = extractTypeSignature(text)
|
|
176
|
+
if (sig) overloads.push(sig)
|
|
177
|
+
|
|
178
|
+
// If it has a body {, it's the implementation - stop
|
|
179
|
+
if (text.includes('{')) break
|
|
180
|
+
}
|
|
181
|
+
return overloads
|
|
182
|
+
}
|
|
183
|
+
|
|
99
184
|
function matchJsLike(line) {
|
|
185
|
+
// export default async function foo<T>(a: T): Promise<T> { ... }
|
|
100
186
|
let m = line.match(/^export\s+(?:default\s+)?(?:async\s+)?(?:function\s+([A-Za-z_$][\w$]*)|class\s+([A-Za-z_$][\w$]*))/)
|
|
101
187
|
if (m) return { name: m[1] || m[2], kind: m[1] ? 'function' : 'class' }
|
|
102
|
-
|
|
188
|
+
|
|
189
|
+
// export async function foo<T>(a: T): Promise<T> { ... }
|
|
190
|
+
// Handle optional generics: function name<T>(...
|
|
191
|
+
m = line.match(/^(?:export\s+)?(?:async\s+)?function\s+([A-Za-z_$][\w$]*)\s*(?:<[^<>]*>)?\s*\(/)
|
|
103
192
|
if (m) return { name: m[1], kind: 'function' }
|
|
193
|
+
|
|
194
|
+
// export class Foo { ... }
|
|
104
195
|
m = line.match(/^(?:export\s+)?class\s+([A-Za-z_$][\w$]*)/)
|
|
105
196
|
if (m) return { name: m[1], kind: 'class' }
|
|
197
|
+
|
|
198
|
+
// export const foo = (a: T): Promise<T> => { ... }
|
|
106
199
|
m = line.match(/^(?:export\s+)?const\s+([A-Za-z_$][\w$]*)\s*=\s*(?:async\s*)?\(/)
|
|
107
200
|
if (m) return { name: m[1], kind: 'function' }
|
|
201
|
+
|
|
202
|
+
// export const foo = async (a: T): Promise<T> => { ... }
|
|
108
203
|
m = line.match(/^(?:export\s+)?const\s+([A-Za-z_$][\w$]*)\s*=\s*(?:async\s*)?(?:\([^)]*\)|[A-Za-z_$][\w$]*)\s*=>/)
|
|
109
204
|
if (m) return { name: m[1], kind: 'function' }
|
|
110
|
-
|
|
205
|
+
|
|
206
|
+
// export function (multi-line, name on next line)
|
|
207
|
+
m = line.match(/^(?:export\s+)?(?:async\s+)?function\s*$/)
|
|
208
|
+
if (m) return { name: '(pending)', kind: 'function' }
|
|
209
|
+
|
|
210
|
+
// anonymous function
|
|
211
|
+
m = line.match(/^(?:export\s+)?(?:async\s+)?function\s*\(/)
|
|
111
212
|
if (m) return { name: '(anonymous)', kind: 'function' }
|
|
213
|
+
|
|
112
214
|
return null
|
|
113
215
|
}
|
|
114
216
|
|
|
@@ -124,31 +226,77 @@ function scanJsLike(masked, relPath, rawLines) {
|
|
|
124
226
|
const symbols = []
|
|
125
227
|
let prevOpensBlock = false
|
|
126
228
|
for (let i = 0; i < masked.length; i++) {
|
|
127
|
-
const
|
|
128
|
-
|
|
229
|
+
const rawText = rawLines[i].trim()
|
|
230
|
+
const maskedText = masked[i].trim()
|
|
231
|
+
if (!rawText) continue
|
|
232
|
+
|
|
129
233
|
let matched = null
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
234
|
+
let joinedText = null
|
|
235
|
+
|
|
236
|
+
// Check for interface / type alias first (on raw line, not masked)
|
|
237
|
+
const ifaceSig = extractInterfaceOrType(rawText)
|
|
238
|
+
if (ifaceSig) {
|
|
239
|
+
const nameMatch = rawText.match(/^(?:export\s+)?(?:interface|type)\s+(\w+)/)
|
|
240
|
+
if (nameMatch) {
|
|
241
|
+
matched = { name: nameMatch[1], kind: 'interface', typeSig: ifaceSig }
|
|
242
|
+
}
|
|
133
243
|
}
|
|
134
|
-
|
|
135
|
-
if (!matched
|
|
136
|
-
|
|
244
|
+
|
|
245
|
+
if (!matched) {
|
|
246
|
+
// Check for method inside class (prev line ends with {)
|
|
247
|
+
if (prevOpensBlock) {
|
|
248
|
+
const method = maskedText.match(/^([A-Za-z_$][\w$]*)\s*(?:<[^<>]*>)?\s*\(([^()]*)\)\s*(?::\s*[^={]{1,80})?\{/)
|
|
249
|
+
if (method && !JS_NON_METHOD.has(method[1])) matched = { name: method[1], kind: 'method' }
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// Regular declarations - try initial match
|
|
253
|
+
if (!matched) matched = matchJsLike(maskedText)
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// Multi-line declaration joining (only when no initial match, or when match is pending)
|
|
257
|
+
if ((!matched || (matched && matched.name === '(pending)')) && JS_DECL_START.test(maskedText)) {
|
|
258
|
+
// No initial match, or pending match - do joining to find/resolve one
|
|
259
|
+
let joined = maskedText
|
|
137
260
|
let extra = 0
|
|
138
|
-
|
|
261
|
+
let declMatched = null
|
|
262
|
+
for (let j = i + 1; j < masked.length && extra < 5; j++) {
|
|
139
263
|
const tail = masked[j].trim()
|
|
140
264
|
if (!tail) continue
|
|
141
265
|
joined += ' ' + tail
|
|
142
266
|
extra++
|
|
143
|
-
|
|
144
|
-
if (
|
|
145
|
-
|
|
267
|
+
if (!declMatched) declMatched = matchJsLike(joined)
|
|
268
|
+
if (/[{};]/.test(tail)) {
|
|
269
|
+
if (declMatched && declMatched.name !== '(pending)') {
|
|
270
|
+
matched = declMatched
|
|
271
|
+
i = j
|
|
272
|
+
joinedText = joined
|
|
273
|
+
}
|
|
146
274
|
break
|
|
147
275
|
}
|
|
148
|
-
|
|
276
|
+
}
|
|
277
|
+
if (!matched && declMatched && declMatched.name !== '(pending)') {
|
|
278
|
+
matched = declMatched
|
|
279
|
+
joinedText = joined
|
|
149
280
|
}
|
|
150
281
|
}
|
|
151
|
-
|
|
282
|
+
|
|
283
|
+
if (matched) {
|
|
284
|
+
// Extract type signature: use joined text if multi-line, else raw line
|
|
285
|
+
if (matched.kind !== 'interface') {
|
|
286
|
+
const sourceForType = joinedText || rawText
|
|
287
|
+
const typeSig = extractTypeSignature(sourceForType)
|
|
288
|
+
if (typeSig) matched.typeSig = typeSig
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// Extract overloads for function declarations
|
|
292
|
+
if (matched.kind === 'function' && (joinedText || rawText).startsWith('function')) {
|
|
293
|
+
const overloads = extractOverloads(masked, i)
|
|
294
|
+
if (overloads.length > 1) matched.overloads = overloads
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
symbols.push(buildSymbol(matched, relPath, rawLines[i], i + 1))
|
|
298
|
+
}
|
|
299
|
+
|
|
152
300
|
prevOpensBlock = masked[i].trim().endsWith('{')
|
|
153
301
|
}
|
|
154
302
|
return symbols
|
|
@@ -208,16 +356,77 @@ function matchShell(line) {
|
|
|
208
356
|
return m ? { name: m[1], kind: 'function' } : null
|
|
209
357
|
}
|
|
210
358
|
|
|
359
|
+
function extractParamsAndReturn(line) {
|
|
360
|
+
const trimmed = line.trim()
|
|
361
|
+
|
|
362
|
+
// Try to extract params and return type from function declaration
|
|
363
|
+
// Matches: function name(params): returnType { or => returnType {
|
|
364
|
+
const funcMatch = line.match(/function\s+([A-Za-z_$][\w$]*)\s*(\([^)]*\))\s*(?::\s*([^{]+))?\s*\{/)
|
|
365
|
+
if (funcMatch) {
|
|
366
|
+
const params = funcMatch[2] || '()'
|
|
367
|
+
const returnType = funcMatch[3] ? `: ${funcMatch[3].trim()}` : ''
|
|
368
|
+
return `${funcMatch[1]}${params}${returnType}`
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
// Arrow function: const name = (params): returnType => {
|
|
372
|
+
const arrowMatch = line.match(/const\s+([A-Za-z_$][\w$]*)\s*=\s*(?:async\s+)?\(([^)]*)\)\s*:\s*([^=]+)\s*=>/)
|
|
373
|
+
if (arrowMatch) {
|
|
374
|
+
return `${arrowMatch[1]}(${arrowMatch[2]}): ${arrowMatch[3].trim()}`
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
// Arrow function without explicit return type: const name = (params) => {
|
|
378
|
+
const arrowSimple = line.match(/const\s+([A-Za-z_$][\w$]*)\s*=\s*(?:async\s+)?\(([^)]*)\)\s*=>/)
|
|
379
|
+
if (arrowMatch) {
|
|
380
|
+
return `${arrowMatch[1]}(${arrowMatch[2]})`
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
// Class method: methodName(params): returnType {
|
|
384
|
+
const methodMatch = line.match(/^\s*([A-Za-z_$][\w$]*)\s*(?:<[^<>]*>)?\s*\(([^()]*)\)\s*(?::\s*([^{]+))?\s*\{/)
|
|
385
|
+
if (methodMatch) {
|
|
386
|
+
const returnType = methodMatch[3] ? `: ${methodMatch[3].trim()}` : ''
|
|
387
|
+
return `${methodMatch[1]}(${methodMatch[2] || ''})${returnType}`
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
// Constructor
|
|
391
|
+
const constructorMatch = line.match(/constructor\s*\(([^)]*)\)/)
|
|
392
|
+
if (constructorMatch) {
|
|
393
|
+
return `constructor(${constructorMatch[1]})`
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
// Fallback: try to extract from the raw line
|
|
397
|
+
const declMatch = line.match(/(function|const|let|var)\s+([A-Za-z_$][\w$]*)/)
|
|
398
|
+
if (declMatch) {
|
|
399
|
+
return declMatch[0]
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
// Fallback: return the raw line trimmed
|
|
403
|
+
return ''
|
|
404
|
+
}
|
|
405
|
+
|
|
211
406
|
function buildSymbol(matched, relPath, rawLine, lineNo) {
|
|
407
|
+
// Use pre-extracted type signature if available, otherwise fall back to extractParamsAndReturn
|
|
408
|
+
let typeSig = matched.typeSig || extractParamsAndReturn(rawLine)
|
|
409
|
+
|
|
410
|
+
// Add overloads if present
|
|
411
|
+
const overloadPart = matched.overloads?.length
|
|
412
|
+
? ` | overloads: ${matched.overloads.join('; ')}`
|
|
413
|
+
: ''
|
|
414
|
+
|
|
415
|
+
// For interface/type, typeSig already includes the full signature without name
|
|
416
|
+
// For others, typeSig is just the generics+params+return
|
|
417
|
+
const prefix = matched.kind === 'interface' ? '' : matched.name
|
|
418
|
+
|
|
419
|
+
// Build one-line identity: name<Generics>(params): ReturnType — file.ts:lineNo
|
|
420
|
+
const identity = `${prefix}${typeSig}${overloadPart} — ${relPath}:${lineNo}`
|
|
421
|
+
|
|
212
422
|
return {
|
|
213
423
|
id: `${String(relPath).replace(/[\\/:\s]/g, '_')}#${lineNo}`,
|
|
214
424
|
sourcePath: relPath,
|
|
215
425
|
sourceLine: lineNo,
|
|
216
426
|
type: 'symbol',
|
|
217
427
|
title: `${matched.name} (${matched.kind})`,
|
|
218
|
-
summary: `${matched.kind} "${matched.name}" declared at ${relPath}:${lineNo}`,
|
|
219
428
|
keywords: [matched.name, matched.kind],
|
|
220
|
-
text:
|
|
429
|
+
text: identity,
|
|
221
430
|
}
|
|
222
431
|
}
|
|
223
432
|
|
package/src/tools/index-repo.js
CHANGED
|
@@ -6,6 +6,7 @@ import { buildDocEntries } from '../doc-pipeline.js'
|
|
|
6
6
|
import { scanSymbols } from '../symbols.js'
|
|
7
7
|
import { linkEntries } from '../link.js'
|
|
8
8
|
import { ProjectMemoryStore } from '../store.js'
|
|
9
|
+
import { onFileIndexed, isTypeScriptFile } from '../enhancer.js'
|
|
9
10
|
|
|
10
11
|
export async function indexRepository(ctx, config, root, { reindex = false } = {}) {
|
|
11
12
|
const memoryDir = memoryRootFor(root, config.memoryDir)
|
|
@@ -77,7 +78,7 @@ export async function indexRepository(ctx, config, root, { reindex = false } = {
|
|
|
77
78
|
}
|
|
78
79
|
|
|
79
80
|
// Second pass: single commit with all updates
|
|
80
|
-
|
|
81
|
+
const report = store.commit((s) => {
|
|
81
82
|
for (const update of fileUpdates) {
|
|
82
83
|
const result = s.applyFileUpdate(update.rel, update)
|
|
83
84
|
if (result.skipped) {
|
|
@@ -105,6 +106,18 @@ export async function indexRepository(ctx, config, root, { reindex = false } = {
|
|
|
105
106
|
}
|
|
106
107
|
return report
|
|
107
108
|
})
|
|
109
|
+
|
|
110
|
+
// Trigger TS enhancement for code files (TS/JS only)
|
|
111
|
+
for (const update of fileUpdates) {
|
|
112
|
+
if (update.type === 'code') {
|
|
113
|
+
const filePath = path.join(root, update.rel)
|
|
114
|
+
if (isTypeScriptFile(filePath)) {
|
|
115
|
+
onFileIndexed(store, update.rel, filePath, config, root)
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return report
|
|
108
121
|
}
|
|
109
122
|
|
|
110
123
|
export function indexRepoTool(ctx, config) {
|
package/src/util/fs.js
CHANGED
|
@@ -85,7 +85,7 @@ export function looksLikeDump(text, maxBytes = 4096) {
|
|
|
85
85
|
|
|
86
86
|
export function isSupportedCode(ext) {
|
|
87
87
|
return [
|
|
88
|
-
'.js', '.mjs', '.cjs', '.ts', '.jsx', '.tsx', '.py', '.java', '.go',
|
|
88
|
+
'.js', '.mjs', '.cjs', '.ts', '.mts', '.cts', '.jsx', '.tsx', '.py', '.java', '.go',
|
|
89
89
|
'.rs', '.c', '.cpp', '.cc', '.h', '.hpp', '.cs', '.php', '.rb',
|
|
90
90
|
'.swift', '.kt', '.kts', '.sh', '.zsh',
|
|
91
91
|
].includes(ext)
|
package/src/watch.js
CHANGED
|
@@ -5,6 +5,7 @@ import { buildDocEntries } from './doc-pipeline.js'
|
|
|
5
5
|
import { scanSymbols } from './symbols.js'
|
|
6
6
|
import { linkEntries } from './link.js'
|
|
7
7
|
import { ProjectMemoryStore } from './store.js'
|
|
8
|
+
import { onFileChanged, isTypeScriptFile } from './enhancer.js'
|
|
8
9
|
|
|
9
10
|
export class WatchManager {
|
|
10
11
|
constructor(ctx, config) {
|
|
@@ -144,12 +145,15 @@ export class WatchManager {
|
|
|
144
145
|
}
|
|
145
146
|
})
|
|
146
147
|
|
|
147
|
-
//
|
|
148
|
-
// first-pass signature: if the file changed during the compute window
|
|
149
|
-
// (hash / scanSymbols / LLM summary), the next poll's signature differs
|
|
150
|
-
// and the file is re-indexed — restoring the pre-refactor self-healing.
|
|
148
|
+
// Trigger TS enhancement for code files (TS/JS only)
|
|
151
149
|
for (const update of fileUpdates) {
|
|
152
150
|
if (update._skipSnapshot) continue
|
|
151
|
+
if (update.type === 'code') {
|
|
152
|
+
const filePath = path.join(root, update.rel)
|
|
153
|
+
if (isTypeScriptFile(filePath)) {
|
|
154
|
+
onFileChanged(state.store, update.rel, filePath, this.config, root)
|
|
155
|
+
}
|
|
156
|
+
}
|
|
153
157
|
state.snapshot[update.rel] = update._sig
|
|
154
158
|
}
|
|
155
159
|
}
|