@co-engram/core 0.1.2 → 0.1.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bootstrap/classify.d.ts +73 -0
- package/dist/bootstrap/classify.d.ts.map +1 -0
- package/dist/bootstrap/classify.js +134 -0
- package/dist/bootstrap/classify.js.map +1 -0
- package/dist/bootstrap/index.d.ts +89 -0
- package/dist/bootstrap/index.d.ts.map +1 -0
- package/dist/bootstrap/index.js +158 -0
- package/dist/bootstrap/index.js.map +1 -0
- package/dist/concepts/dictionary.d.ts +260 -0
- package/dist/concepts/dictionary.d.ts.map +1 -0
- package/dist/concepts/dictionary.js +253 -0
- package/dist/concepts/dictionary.js.map +1 -0
- package/dist/concepts/index.d.ts +11 -0
- package/dist/concepts/index.d.ts.map +1 -0
- package/dist/concepts/index.js +10 -0
- package/dist/concepts/index.js.map +1 -0
- package/dist/concepts/types.d.ts +65 -0
- package/dist/concepts/types.d.ts.map +1 -0
- package/dist/concepts/types.js +15 -0
- package/dist/concepts/types.js.map +1 -0
- package/dist/config/defaults.d.ts +32 -3
- package/dist/config/defaults.d.ts.map +1 -1
- package/dist/config/defaults.js +53 -3
- package/dist/config/defaults.js.map +1 -1
- package/dist/config/index.d.ts +18 -1
- package/dist/config/index.d.ts.map +1 -1
- package/dist/config/index.js +75 -3
- package/dist/config/index.js.map +1 -1
- package/dist/config/loader.d.ts +10 -0
- package/dist/config/loader.d.ts.map +1 -0
- package/dist/config/loader.js +10 -0
- package/dist/config/loader.js.map +1 -0
- package/dist/config/types.d.ts +97 -1
- package/dist/config/types.d.ts.map +1 -1
- package/dist/contradiction/auto-degrade.d.ts +11 -2
- package/dist/contradiction/auto-degrade.d.ts.map +1 -1
- package/dist/contradiction/auto-degrade.js +22 -0
- package/dist/contradiction/auto-degrade.js.map +1 -1
- package/dist/contradiction/resolver.d.ts.map +1 -1
- package/dist/contradiction/resolver.js +7 -1
- package/dist/contradiction/resolver.js.map +1 -1
- package/dist/dreaming/index.d.ts +1 -0
- package/dist/dreaming/index.d.ts.map +1 -1
- package/dist/dreaming/index.js +1 -0
- package/dist/dreaming/index.js.map +1 -1
- package/dist/dreaming/llm-pattern-abstraction.d.ts +31 -0
- package/dist/dreaming/llm-pattern-abstraction.d.ts.map +1 -0
- package/dist/dreaming/llm-pattern-abstraction.js +70 -0
- package/dist/dreaming/llm-pattern-abstraction.js.map +1 -0
- package/dist/dreaming/rem.d.ts.map +1 -1
- package/dist/dreaming/rem.js +1 -0
- package/dist/dreaming/rem.js.map +1 -1
- package/dist/dreaming/scheduler.d.ts +13 -0
- package/dist/dreaming/scheduler.d.ts.map +1 -1
- package/dist/dreaming/scheduler.js +14 -2
- package/dist/dreaming/scheduler.js.map +1 -1
- package/dist/evolution/triggered.d.ts.map +1 -1
- package/dist/evolution/triggered.js +1 -0
- package/dist/evolution/triggered.js.map +1 -1
- package/dist/generative/hypothesis.d.ts.map +1 -1
- package/dist/generative/hypothesis.js +1 -0
- package/dist/generative/hypothesis.js.map +1 -1
- package/dist/i18n/en.d.ts.map +1 -1
- package/dist/i18n/en.js +1278 -33
- package/dist/i18n/en.js.map +1 -1
- package/dist/i18n/index.d.ts +34 -1
- package/dist/i18n/index.d.ts.map +1 -1
- package/dist/i18n/index.js +36 -7
- package/dist/i18n/index.js.map +1 -1
- package/dist/i18n/zh.d.ts +709 -32
- package/dist/i18n/zh.d.ts.map +1 -1
- package/dist/i18n/zh.js +1282 -33
- package/dist/i18n/zh.js.map +1 -1
- package/dist/index/graph-builder.js +3 -3
- package/dist/index/graph-builder.js.map +1 -1
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -1
- package/dist/learning/loop.d.ts +9 -0
- package/dist/learning/loop.d.ts.map +1 -1
- package/dist/learning/loop.js +42 -1
- package/dist/learning/loop.js.map +1 -1
- package/dist/maintenance/types.d.ts +11 -0
- package/dist/maintenance/types.d.ts.map +1 -1
- package/dist/maintenance/types.js.map +1 -1
- package/dist/merge/auto-onboard.d.ts +7 -1
- package/dist/merge/auto-onboard.d.ts.map +1 -1
- package/dist/merge/auto-onboard.js +35 -13
- package/dist/merge/auto-onboard.js.map +1 -1
- package/dist/merge/post-merge-hook.d.ts.map +1 -1
- package/dist/merge/post-merge-hook.js +16 -2
- package/dist/merge/post-merge-hook.js.map +1 -1
- package/dist/merge/synapse-merger.js +6 -0
- package/dist/merge/synapse-merger.js.map +1 -1
- package/dist/merge-driver.cjs +64 -5
- package/dist/observability/audit-log.d.ts +7 -1
- package/dist/observability/audit-log.d.ts.map +1 -1
- package/dist/observability/audit-log.js.map +1 -1
- package/dist/observability/necessity-evaluator.d.ts +29 -0
- package/dist/observability/necessity-evaluator.d.ts.map +1 -1
- package/dist/observability/necessity-evaluator.js +240 -13
- package/dist/observability/necessity-evaluator.js.map +1 -1
- package/dist/observability/proposal-engine.d.ts +81 -4
- package/dist/observability/proposal-engine.d.ts.map +1 -1
- package/dist/observability/proposal-engine.js +207 -13
- package/dist/observability/proposal-engine.js.map +1 -1
- package/dist/observability/runtime-description-check.d.ts +55 -0
- package/dist/observability/runtime-description-check.d.ts.map +1 -0
- package/dist/observability/runtime-description-check.js +63 -0
- package/dist/observability/runtime-description-check.js.map +1 -0
- package/dist/prompt-signals/cache.d.ts +73 -0
- package/dist/prompt-signals/cache.d.ts.map +1 -1
- package/dist/prompt-signals/cache.js +102 -0
- package/dist/prompt-signals/cache.js.map +1 -1
- package/dist/prompt-signals/event-bus.d.ts +82 -0
- package/dist/prompt-signals/event-bus.d.ts.map +1 -0
- package/dist/prompt-signals/event-bus.js +105 -0
- package/dist/prompt-signals/event-bus.js.map +1 -0
- package/dist/prompt-signals/index.d.ts +2 -1
- package/dist/prompt-signals/index.d.ts.map +1 -1
- package/dist/prompt-signals/index.js +2 -1
- package/dist/prompt-signals/index.js.map +1 -1
- package/dist/reinforcement/ltp.d.ts +15 -1
- package/dist/reinforcement/ltp.d.ts.map +1 -1
- package/dist/reinforcement/ltp.js +24 -5
- package/dist/reinforcement/ltp.js.map +1 -1
- package/dist/reinforcement/related.d.ts +31 -2
- package/dist/reinforcement/related.d.ts.map +1 -1
- package/dist/reinforcement/related.js +39 -3
- package/dist/reinforcement/related.js.map +1 -1
- package/dist/retrieval/filter.d.ts.map +1 -1
- package/dist/retrieval/filter.js +7 -0
- package/dist/retrieval/filter.js.map +1 -1
- package/dist/retrieval/fts.d.ts +6 -5
- package/dist/retrieval/fts.d.ts.map +1 -1
- package/dist/retrieval/fts.js +74 -22
- package/dist/retrieval/fts.js.map +1 -1
- package/dist/status/index.d.ts +7 -0
- package/dist/status/index.d.ts.map +1 -0
- package/dist/status/index.js +7 -0
- package/dist/status/index.js.map +1 -0
- package/dist/status/status.d.ts +132 -0
- package/dist/status/status.d.ts.map +1 -0
- package/dist/status/status.js +437 -0
- package/dist/status/status.js.map +1 -0
- package/dist/storage/engram-store.d.ts.map +1 -1
- package/dist/storage/engram-store.js +17 -2
- package/dist/storage/engram-store.js.map +1 -1
- package/dist/storage/git.d.ts +168 -0
- package/dist/storage/git.d.ts.map +1 -1
- package/dist/storage/git.js +616 -33
- package/dist/storage/git.js.map +1 -1
- package/dist/storage/index.d.ts +1 -0
- package/dist/storage/index.d.ts.map +1 -1
- package/dist/storage/index.js +1 -0
- package/dist/storage/index.js.map +1 -1
- package/dist/storage/infra-doctor.d.ts +42 -0
- package/dist/storage/infra-doctor.d.ts.map +1 -0
- package/dist/storage/infra-doctor.js +92 -0
- package/dist/storage/infra-doctor.js.map +1 -0
- package/dist/storage/obsidian-links.d.ts +73 -0
- package/dist/storage/obsidian-links.d.ts.map +1 -0
- package/dist/storage/obsidian-links.js +177 -0
- package/dist/storage/obsidian-links.js.map +1 -0
- package/dist/storage/path.d.ts +24 -0
- package/dist/storage/path.d.ts.map +1 -1
- package/dist/storage/path.js +53 -0
- package/dist/storage/path.js.map +1 -1
- package/dist/storage/repository.d.ts +74 -5
- package/dist/storage/repository.d.ts.map +1 -1
- package/dist/storage/repository.js +337 -21
- package/dist/storage/repository.js.map +1 -1
- package/dist/storage/synapse-store.d.ts +7 -1
- package/dist/storage/synapse-store.d.ts.map +1 -1
- package/dist/storage/synapse-store.js +8 -0
- package/dist/storage/synapse-store.js.map +1 -1
- package/dist/tools/audit-query-tool.d.ts +53 -0
- package/dist/tools/audit-query-tool.d.ts.map +1 -0
- package/dist/tools/audit-query-tool.js +123 -0
- package/dist/tools/audit-query-tool.js.map +1 -0
- package/dist/tools/doctor-tools.d.ts +5 -0
- package/dist/tools/doctor-tools.d.ts.map +1 -1
- package/dist/tools/doctor-tools.js +11 -3
- package/dist/tools/doctor-tools.js.map +1 -1
- package/dist/tools/engram-tools.d.ts +13 -0
- package/dist/tools/engram-tools.d.ts.map +1 -1
- package/dist/tools/engram-tools.js +72 -8
- package/dist/tools/engram-tools.js.map +1 -1
- package/dist/tools/index.d.ts +3 -0
- package/dist/tools/index.d.ts.map +1 -1
- package/dist/tools/index.js +3 -0
- package/dist/tools/index.js.map +1 -1
- package/dist/tools/llm-descriptions.d.ts +28 -28
- package/dist/tools/llm-descriptions.d.ts.map +1 -1
- package/dist/tools/llm-descriptions.js +56 -489
- package/dist/tools/llm-descriptions.js.map +1 -1
- package/dist/tools/normalization.d.ts +43 -0
- package/dist/tools/normalization.d.ts.map +1 -0
- package/dist/tools/normalization.js +68 -0
- package/dist/tools/normalization.js.map +1 -0
- package/dist/tools/proposal-tools.d.ts +23 -1
- package/dist/tools/proposal-tools.d.ts.map +1 -1
- package/dist/tools/proposal-tools.js +58 -17
- package/dist/tools/proposal-tools.js.map +1 -1
- package/dist/tools/registry.d.ts.map +1 -1
- package/dist/tools/registry.js +6 -0
- package/dist/tools/registry.js.map +1 -1
- package/dist/tools/schemas.d.ts +96 -22
- package/dist/tools/schemas.d.ts.map +1 -1
- package/dist/tools/schemas.js +84 -11
- package/dist/tools/schemas.js.map +1 -1
- package/dist/tools/skill-tools.js +1 -1
- package/dist/tools/skill-tools.js.map +1 -1
- package/dist/tools/synapse-tools.d.ts.map +1 -1
- package/dist/tools/synapse-tools.js +1 -0
- package/dist/tools/synapse-tools.js.map +1 -1
- package/dist/tools/sync-tools.d.ts +102 -0
- package/dist/tools/sync-tools.d.ts.map +1 -0
- package/dist/tools/sync-tools.js +309 -0
- package/dist/tools/sync-tools.js.map +1 -0
- package/dist/tools/synthesize-tools.d.ts +79 -0
- package/dist/tools/synthesize-tools.d.ts.map +1 -0
- package/dist/tools/synthesize-tools.js +297 -0
- package/dist/tools/synthesize-tools.js.map +1 -0
- package/dist/tools/tool-profile.d.ts +68 -0
- package/dist/tools/tool-profile.d.ts.map +1 -0
- package/dist/tools/tool-profile.js +174 -0
- package/dist/tools/tool-profile.js.map +1 -0
- package/dist/tools/tool.d.ts +17 -0
- package/dist/tools/tool.d.ts.map +1 -1
- package/dist/tools/tool.js.map +1 -1
- package/dist/types/disclosure.d.ts +7 -0
- package/dist/types/disclosure.d.ts.map +1 -1
- package/dist/types/repository-types.d.ts +17 -1
- package/dist/types/repository-types.d.ts.map +1 -1
- package/dist/types/synapse.d.ts +19 -1
- package/dist/types/synapse.d.ts.map +1 -1
- package/package.json +9 -9
|
@@ -19,11 +19,14 @@ import { join, relative, sep } from "node:path";
|
|
|
19
19
|
import { ulid } from "ulid";
|
|
20
20
|
import { isStableEngramId } from "../types/repository-types.js";
|
|
21
21
|
import { slugify, inferDomainTagsFromPath } from "../types/slugify.js";
|
|
22
|
+
import { safeEmit } from "../prompt-signals/event-bus.js";
|
|
23
|
+
import { safeJoinWithinRoot, isPathWithinRoot, } from "./path.js";
|
|
22
24
|
import { computeContentHash, computeContentSize } from "./hash.js";
|
|
23
25
|
import { DEFAULT_LANGUAGE } from "../i18n/index.js";
|
|
24
26
|
import { readEngramFile, writeEngramFile, deleteEngramFile, renameEngramFile, parseEngramFile, detectEngramFileLanguage, } from "./engram-store.js";
|
|
25
27
|
import { SYNAPSES_DIR, collectAllSynapses, upsertSynapse, readSynapseByEndpoints, readSynapseById, listSynapsesForEngram, deleteSynapsesTouching, synapseRelativePath, deleteSynapseFile, writeSynapseFile, parseSynapseFile, } from "./synapse-store.js";
|
|
26
28
|
import { buildIndexEntryFromFrontmatter, engramIndexPath, readEngramIndex, rebuildEngramIndex, removeEngramIndexEntry, upsertEngramIndexEntry, writeEngramIndex, } from "./engram-index.js";
|
|
29
|
+
import { regenerateObsidianLinks, checkObsidianView, } from "./obsidian-links.js";
|
|
27
30
|
const DEFAULT_IMPORTANCE = 0.5;
|
|
28
31
|
const DEFAULT_CONFIDENCE_BY_SOURCE = {
|
|
29
32
|
firsthand: 0.85,
|
|
@@ -64,6 +67,21 @@ function deriveAutoSummary(content, title) {
|
|
|
64
67
|
return cleaned;
|
|
65
68
|
return cleaned.slice(0, 197) + "...";
|
|
66
69
|
}
|
|
70
|
+
/**
|
|
71
|
+
* Synapse visibility 继承规则:取两端 engram 的最严。
|
|
72
|
+
*
|
|
73
|
+
* 严格度排序:`private` > `restricted` > `team` > `public`。
|
|
74
|
+
* 任一端是 private,synapse 整条就按 private 处理(保守策略)。
|
|
75
|
+
*/
|
|
76
|
+
const VIS_STRICTNESS = {
|
|
77
|
+
public: 0,
|
|
78
|
+
team: 1,
|
|
79
|
+
restricted: 2,
|
|
80
|
+
private: 3,
|
|
81
|
+
};
|
|
82
|
+
function maxVisibility(a, b) {
|
|
83
|
+
return VIS_STRICTNESS[a] >= VIS_STRICTNESS[b] ? a : b;
|
|
84
|
+
}
|
|
67
85
|
/**
|
|
68
86
|
* EngramRepository — per-edge synapse + ULID stable id + 单文件 engram
|
|
69
87
|
*
|
|
@@ -90,6 +108,18 @@ export class EngramRepository {
|
|
|
90
108
|
* 会立即触发缓存失效,无需等下次 getIndex 的 mtime 兜底检查。
|
|
91
109
|
*/
|
|
92
110
|
indexWatcher;
|
|
111
|
+
/**
|
|
112
|
+
* 递归 fs.watch 句柄,监听 dataRoot 下所有 .md 文件变化(可选)。
|
|
113
|
+
*
|
|
114
|
+
* 触发场景(关键):git pull / git checkout / 手动编辑 / rsync 等任何外部
|
|
115
|
+
* 写入 .md 的途径,startWatching() 单独监听 index.json 看不到这些变化。
|
|
116
|
+
* dataWatcher 触发后 debounce 调用 rebuildIndex,把磁盘 .md 真相重新
|
|
117
|
+
* 投影到 engram-index.json,避免"git pull 拉到新文件但 engram_search
|
|
118
|
+
* 找不到"的 fail-silent(架构缺陷 index-no-truth 的具体表现)。
|
|
119
|
+
*/
|
|
120
|
+
dataWatcher;
|
|
121
|
+
/** dataWatcher debounce 定时器。git pull 一次性触发大量事件,合并为一次重建。 */
|
|
122
|
+
dataRebuildTimer;
|
|
93
123
|
language;
|
|
94
124
|
constructor(config) {
|
|
95
125
|
this.config = config;
|
|
@@ -180,17 +210,43 @@ export class EngramRepository {
|
|
|
180
210
|
removeEngramIndexEntry(index, id);
|
|
181
211
|
this.persistIndex(index);
|
|
182
212
|
}
|
|
213
|
+
/**
|
|
214
|
+
* 清理索引中所有指向 relativePath 的孤儿 entry,返回清理数量。
|
|
215
|
+
*
|
|
216
|
+
* 触发场景:外部(用户 rm / git 操作 / 进程异常)删除 engram 文件后,
|
|
217
|
+
* engram-index.json 仍保留旧 ULID 的 entry。下一次 createEngram 写入
|
|
218
|
+
* 同 path 的新 ULID 会留下永不消失的孤儿,导致 listEngrams / viewer
|
|
219
|
+
* 显示"重影"(同一文件被两个 ULID 引用)。
|
|
220
|
+
*
|
|
221
|
+
* 同 path 出现多条 entry 本身就是不变量破坏 —— 此处发现即清。多数调用
|
|
222
|
+
* 不会命中,N=0 时是 O(|index|) 扫一次,可接受。
|
|
223
|
+
*/
|
|
224
|
+
purgeStaleIndexEntriesForPath(relativePath) {
|
|
225
|
+
const index = this.getIndex();
|
|
226
|
+
const stale = [];
|
|
227
|
+
for (const [id, entry] of index.entries) {
|
|
228
|
+
if (entry.path === relativePath)
|
|
229
|
+
stale.push(id);
|
|
230
|
+
}
|
|
231
|
+
for (const id of stale) {
|
|
232
|
+
this.deleteIndexEntry(id);
|
|
233
|
+
}
|
|
234
|
+
return stale.length;
|
|
235
|
+
}
|
|
183
236
|
// ─── Cross-process watcher ─────────────────────────────────────────────
|
|
184
237
|
/**
|
|
185
|
-
* 启动对 engram-index.json 的 fs.watch
|
|
238
|
+
* 启动对 engram-index.json 的 fs.watch 监听 + dataRoot .md 递归监听。
|
|
186
239
|
*
|
|
187
|
-
*
|
|
188
|
-
*
|
|
240
|
+
* 启动后:
|
|
241
|
+
* - index.json watcher:外部进程修改 index(创建/更新/删除 engram)→ 失效 cache
|
|
242
|
+
* - dataRoot .md watcher:任何途径(git pull / checkout / 手动编辑)写入 .md
|
|
243
|
+
* → debounce 后调 rebuildIndex,把磁盘真相重新投影到 index.json
|
|
189
244
|
*
|
|
190
245
|
* 幂等:多次调用安全,只创建一个 watcher。
|
|
191
246
|
*
|
|
192
247
|
* 适用场景:多个 host adapter(MCP server / OpenClaw plugin)共享同一
|
|
193
|
-
* dataRoot
|
|
248
|
+
* dataRoot 时,确保各进程的缓存相互一致;同时覆盖 git pull 后索引漏更新
|
|
249
|
+
* 的 fail-silent 场景。
|
|
194
250
|
*
|
|
195
251
|
* 不需要显式停止 — 进程退出时 OS 自动回收 fd。stopWatching() 仅用于
|
|
196
252
|
* 测试 / 显式资源管理场景。
|
|
@@ -216,6 +272,76 @@ export class EngramRepository {
|
|
|
216
272
|
// persistIndex 写盘后会 lazy 重试 startWatching,覆盖首次空 dataRoot 场景。
|
|
217
273
|
this.indexWatcher = undefined;
|
|
218
274
|
}
|
|
275
|
+
this.startDataRootWatcher();
|
|
276
|
+
}
|
|
277
|
+
/**
|
|
278
|
+
* 启动 dataRoot 下 .md 文件的递归 fs.watch。
|
|
279
|
+
*
|
|
280
|
+
* Node 22+ 在 Linux/macOS/Windows 都支持 `recursive: true`(Linux 通过 inotify)。
|
|
281
|
+
* 不支持的平台(NFS / 老 Node)→ catch 后 noop,startWatching 的 index.json
|
|
282
|
+
* watcher + getIndex 的 mtime 兜底仍然有效,只是 .md 变化需要等下次主动 getIndex。
|
|
283
|
+
*
|
|
284
|
+
* 触发后通过 scheduleDataRebuild debounce 合并,避免 git pull 一次性大量事件
|
|
285
|
+
* 触发多次 rebuildIndex。
|
|
286
|
+
*/
|
|
287
|
+
startDataRootWatcher() {
|
|
288
|
+
if (this.dataWatcher)
|
|
289
|
+
return;
|
|
290
|
+
try {
|
|
291
|
+
this.dataWatcher = watch(this.config.rootPath, { recursive: true, persistent: false }, (_eventType, filename) => {
|
|
292
|
+
if (process.env["CO_ENGRAM_WATCHER_DEBUG"]) {
|
|
293
|
+
process.stderr.write(`[co-engram-watcher] event=${_eventType} filename=${filename ?? "null"}\n`);
|
|
294
|
+
}
|
|
295
|
+
// 只关心 .md 变化(.yaml / .json / .co-engram/ 内部状态由 index.json
|
|
296
|
+
// watcher 或 persistIndex 路径覆盖)。filename 跨平台可能为 null,
|
|
297
|
+
// 不可靠时宁可多触发一次 rebuildIndex 也不要漏事件。
|
|
298
|
+
if (typeof filename === "string" && !filename.endsWith(".md"))
|
|
299
|
+
return;
|
|
300
|
+
this.scheduleDataRebuild();
|
|
301
|
+
});
|
|
302
|
+
this.dataWatcher.on("error", () => {
|
|
303
|
+
this.dataWatcher = undefined;
|
|
304
|
+
});
|
|
305
|
+
}
|
|
306
|
+
catch {
|
|
307
|
+
// 平台不支持 recursive fs.watch → 降级,功能不阻塞
|
|
308
|
+
this.dataWatcher = undefined;
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
/**
|
|
312
|
+
* Debounce 调用 rebuildIndex,合并短时间内的多次 .md 变化事件。
|
|
313
|
+
*
|
|
314
|
+
* git pull / rsync 等批量操作会一次性产生几十~几百个事件,逐个触发
|
|
315
|
+
* rebuildIndex(全量扫盘)会卡死。1000ms debounce 把它们合并成一次重建。
|
|
316
|
+
*/
|
|
317
|
+
scheduleDataRebuild() {
|
|
318
|
+
if (this.dataRebuildTimer)
|
|
319
|
+
clearTimeout(this.dataRebuildTimer);
|
|
320
|
+
this.dataRebuildTimer = setTimeout(() => {
|
|
321
|
+
this.dataRebuildTimer = undefined;
|
|
322
|
+
try {
|
|
323
|
+
if (process.env["CO_ENGRAM_WATCHER_DEBUG"]) {
|
|
324
|
+
process.stderr.write(`[co-engram-watcher] rebuildIndex triggered\n`);
|
|
325
|
+
}
|
|
326
|
+
// rebuildIndex 全量扫盘重读所有 .md,更新 engram-index.json。
|
|
327
|
+
// 之后 persistIndex 写 index.json → 触发 indexWatcher → invalidateIndexCache
|
|
328
|
+
// → searchOrchestrator 重建 ftsIndex,完成全链路同步。
|
|
329
|
+
const result = this.rebuildIndex();
|
|
330
|
+
this.invalidateIndexCache();
|
|
331
|
+
if (process.env["CO_ENGRAM_WATCHER_DEBUG"]) {
|
|
332
|
+
const count = result.entries
|
|
333
|
+
? Object.keys(result.entries).length
|
|
334
|
+
: "?";
|
|
335
|
+
process.stderr.write(`[co-engram-watcher] rebuildIndex done, entries=${count}\n`);
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
catch (e) {
|
|
339
|
+
if (process.env["CO_ENGRAM_WATCHER_DEBUG"]) {
|
|
340
|
+
process.stderr.write(`[co-engram-watcher] rebuildIndex failed: ${e instanceof Error ? e.message : String(e)}\n`);
|
|
341
|
+
}
|
|
342
|
+
// 重建失败不能阻塞 watcher 后续触发,静默吞掉(下次事件再次尝试)
|
|
343
|
+
}
|
|
344
|
+
}, 1000);
|
|
219
345
|
}
|
|
220
346
|
/** 停止 watcher(主要用于测试隔离) */
|
|
221
347
|
stopWatching() {
|
|
@@ -228,6 +354,19 @@ export class EngramRepository {
|
|
|
228
354
|
}
|
|
229
355
|
this.indexWatcher = undefined;
|
|
230
356
|
}
|
|
357
|
+
if (this.dataWatcher) {
|
|
358
|
+
try {
|
|
359
|
+
this.dataWatcher.close();
|
|
360
|
+
}
|
|
361
|
+
catch {
|
|
362
|
+
// ignore
|
|
363
|
+
}
|
|
364
|
+
this.dataWatcher = undefined;
|
|
365
|
+
}
|
|
366
|
+
if (this.dataRebuildTimer) {
|
|
367
|
+
clearTimeout(this.dataRebuildTimer);
|
|
368
|
+
this.dataRebuildTimer = undefined;
|
|
369
|
+
}
|
|
231
370
|
}
|
|
232
371
|
/** 失效缓存 — 下次 getIndex 从磁盘重读。同时通知 invalidate listeners。 */
|
|
233
372
|
invalidateIndexCache() {
|
|
@@ -262,17 +401,26 @@ export class EngramRepository {
|
|
|
262
401
|
/** 解析 stableId → 相对路径 */
|
|
263
402
|
resolvePath(stableId) {
|
|
264
403
|
if (!isStableEngramId(stableId)) {
|
|
265
|
-
// 兼容:可能是相对路径,直接当 path
|
|
404
|
+
// 兼容:可能是相对路径,直接当 path 用。
|
|
405
|
+
// 但必须先校验路径在 root 内(防 `..` 逃逸,path traversal 防御)
|
|
406
|
+
if (!isPathWithinRoot(this.config.rootPath, stableId))
|
|
407
|
+
return undefined;
|
|
266
408
|
if (this.existsAtPath(stableId))
|
|
267
409
|
return stableId;
|
|
268
410
|
return undefined;
|
|
269
411
|
}
|
|
270
412
|
const entry = this.getIndex().entries.get(stableId);
|
|
413
|
+
// 防御:索引中的 path 也校验(理论上是 trusted,但 doctor 自愈后可能含异常)
|
|
414
|
+
if (entry?.path && !isPathWithinRoot(this.config.rootPath, entry.path)) {
|
|
415
|
+
return undefined;
|
|
416
|
+
}
|
|
271
417
|
return entry?.path;
|
|
272
418
|
}
|
|
273
419
|
/** 检查相对路径是否存在 engram 文件 */
|
|
274
420
|
existsAtPath(relativePath) {
|
|
275
|
-
|
|
421
|
+
if (!isPathWithinRoot(this.config.rootPath, relativePath))
|
|
422
|
+
return false;
|
|
423
|
+
return existsSync(safeJoinWithinRoot(this.config.rootPath, relativePath));
|
|
276
424
|
}
|
|
277
425
|
// ─── Engram CRUD ───────────────────────────────────────────────────────
|
|
278
426
|
/**
|
|
@@ -290,11 +438,18 @@ export class EngramRepository {
|
|
|
290
438
|
const sourceType = input.sourceType ?? "firsthand";
|
|
291
439
|
const contentHash = computeContentHash(input.content);
|
|
292
440
|
const contentSize = computeContentSize(input.content);
|
|
441
|
+
// pathHint 优先;否则用 deriveDefaultPath(slugify title + raw domainTags + .md)
|
|
442
|
+
// safeJoinWithinRoot 拦截 `..` 逃逸与绝对路径(path traversal 防御)
|
|
293
443
|
const relativePath = input.pathHint ?? this.deriveDefaultPath(input);
|
|
294
|
-
const absolutePath =
|
|
444
|
+
const absolutePath = safeJoinWithinRoot(this.config.rootPath, relativePath);
|
|
295
445
|
if (existsSync(absolutePath)) {
|
|
296
446
|
throw new Error(`Engram already exists at ${relativePath}`);
|
|
297
447
|
}
|
|
448
|
+
// 自愈:外部 rm / git 操作可能让 engram-index.json 残留指向同 path
|
|
449
|
+
// 但磁盘已无文件的孤儿 entry。新 ULID 写入后会留下永不消失的"重影"
|
|
450
|
+
// (listEngrams / viewer 显示重复节点)。在写入前清理。engram_doctor
|
|
451
|
+
// 是手动巡检版本,此处是写入路径的自动防线。
|
|
452
|
+
this.purgeStaleIndexEntriesForPath(relativePath);
|
|
298
453
|
const hasExplicitDomainTags = input.domainTags.length > 0;
|
|
299
454
|
const frontmatter = {
|
|
300
455
|
id: stableId,
|
|
@@ -344,14 +499,29 @@ export class EngramRepository {
|
|
|
344
499
|
contentHash,
|
|
345
500
|
});
|
|
346
501
|
this.updateIndexEntry(entry);
|
|
502
|
+
// Task 3.4 Phase B:engram 创建后 emit,让 prompt-signals cache 失效并 debounced rebuild
|
|
503
|
+
safeEmit({
|
|
504
|
+
type: "engram_created",
|
|
505
|
+
engramId: stableId,
|
|
506
|
+
at: new Date().toISOString(),
|
|
507
|
+
});
|
|
347
508
|
return this.readEngram(stableId);
|
|
348
509
|
}
|
|
349
|
-
/**
|
|
510
|
+
/**
|
|
511
|
+
* 默认路径:{domainTags.join('/')/}{slug}.md
|
|
512
|
+
*
|
|
513
|
+
* private engram 自动落 `private/` 子目录(被 .gitignore 隔离出团队仓库)。
|
|
514
|
+
* 注意:本函数只用于「无 pathHint」场景;调用方传 pathHint 时直接尊重用户路径,
|
|
515
|
+
* 不会自动加 private 前缀(避免 `private/private/...` 双前缀)。
|
|
516
|
+
*/
|
|
350
517
|
deriveDefaultPath(input) {
|
|
351
518
|
const slug = slugify(input.title);
|
|
352
519
|
const domains = input.domainTags.length > 0 ? input.domainTags : [];
|
|
353
520
|
const parts = [...domains, `${slug}.md`];
|
|
354
|
-
|
|
521
|
+
const basePath = parts.join("/");
|
|
522
|
+
return input.visibility === "private"
|
|
523
|
+
? `private/${basePath}`
|
|
524
|
+
: basePath;
|
|
355
525
|
}
|
|
356
526
|
/**
|
|
357
527
|
* 读取完整 Engram(单文件 + 统计)
|
|
@@ -361,7 +531,8 @@ export class EngramRepository {
|
|
|
361
531
|
if (!relativePath) {
|
|
362
532
|
throw new Error(`Engram not found: ${stableId}`);
|
|
363
533
|
}
|
|
364
|
-
|
|
534
|
+
// resolvePath 已校验路径在 root 内,这里再防御一次
|
|
535
|
+
const absolutePath = safeJoinWithinRoot(this.config.rootPath, relativePath);
|
|
365
536
|
const file = readEngramFile(absolutePath);
|
|
366
537
|
return this.assembleEngram(file, relativePath);
|
|
367
538
|
}
|
|
@@ -371,7 +542,8 @@ export class EngramRepository {
|
|
|
371
542
|
exists(stableId) {
|
|
372
543
|
const relativePath = this.resolvePath(stableId);
|
|
373
544
|
return (relativePath !== undefined &&
|
|
374
|
-
|
|
545
|
+
isPathWithinRoot(this.config.rootPath, relativePath) &&
|
|
546
|
+
existsSync(safeJoinWithinRoot(this.config.rootPath, relativePath)));
|
|
375
547
|
}
|
|
376
548
|
/**
|
|
377
549
|
* 更新 Engram(content/frontmatter 双写)
|
|
@@ -454,9 +626,18 @@ export class EngramRepository {
|
|
|
454
626
|
const newSlugUnlocked = oldFrontmatter.slug === undefined
|
|
455
627
|
? slugify(newTitle)
|
|
456
628
|
: oldFrontmatter.slug;
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
629
|
+
// 处理 visibility 变化:public/team/restricted ↔ private → 路径前缀调整
|
|
630
|
+
// (private engram 落 `private/` 子目录,变更 visibility 时同步迁移路径)
|
|
631
|
+
const oldVisibility = oldFrontmatter.visibility ?? "public";
|
|
632
|
+
const visibilityChanged = newVisibility !== oldVisibility;
|
|
633
|
+
// slug + visibility 都可能触发 rename,正交串联应用
|
|
634
|
+
let newPath = relativePath;
|
|
635
|
+
if (newSlugUnlocked !== oldSlug) {
|
|
636
|
+
newPath = this.rebuildPath(newPath, newSlugUnlocked);
|
|
637
|
+
}
|
|
638
|
+
if (visibilityChanged) {
|
|
639
|
+
newPath = this.rebuildPathForVisibility(newPath, newVisibility);
|
|
640
|
+
}
|
|
460
641
|
const newFile = {
|
|
461
642
|
frontmatter: newFrontmatter,
|
|
462
643
|
content: newContent,
|
|
@@ -464,10 +645,15 @@ export class EngramRepository {
|
|
|
464
645
|
if (newPath !== relativePath) {
|
|
465
646
|
const newAbsolutePath = join(this.config.rootPath, newPath);
|
|
466
647
|
if (existsSync(newAbsolutePath)) {
|
|
467
|
-
|
|
648
|
+
// 原子性保证:目标已存在就报错,不动旧文件
|
|
649
|
+
throw new Error(`Rename conflict: ${newPath} already exists`);
|
|
468
650
|
}
|
|
469
651
|
writeEngramFile(newAbsolutePath, newFile, this.language);
|
|
470
652
|
rmSync(absolutePath);
|
|
653
|
+
// 旧路径孤儿 index entry 清理(原 updateEngram 漏了这一步):
|
|
654
|
+
// rename 后旧 path 的反向 entry(path → stableId)若不清理,会留下
|
|
655
|
+
// 「磁盘无文件但 index 有记录」的孤儿,listEngrams / viewer 会显示重影。
|
|
656
|
+
this.purgeStaleIndexEntriesForPath(relativePath);
|
|
471
657
|
}
|
|
472
658
|
else {
|
|
473
659
|
writeEngramFile(absolutePath, newFile, this.language);
|
|
@@ -488,8 +674,28 @@ export class EngramRepository {
|
|
|
488
674
|
parts[parts.length - 1] = `${newSlug}.md`;
|
|
489
675
|
return parts.join("/");
|
|
490
676
|
}
|
|
677
|
+
/**
|
|
678
|
+
* 调整路径的 visibility 前缀(不改动 basename):
|
|
679
|
+
* - `'private'` → 加 `private/` 前缀(若已含则幂等返回原值)
|
|
680
|
+
* - 其他(`'public'`/`'team'`/`'restricted'`) → 移除 `private/` 前缀(若不含则幂等返回)
|
|
681
|
+
*
|
|
682
|
+
* 用于 updateEngram 中 visibility 变更时的路径迁移。
|
|
683
|
+
* 注意:仅处理前缀,basename 由 rebuildPath 负责,两者正交可串联应用。
|
|
684
|
+
*/
|
|
685
|
+
rebuildPathForVisibility(path, visibility) {
|
|
686
|
+
const PRIVATE_PREFIX = "private/";
|
|
687
|
+
if (visibility === "private") {
|
|
688
|
+
return path.startsWith(PRIVATE_PREFIX) ? path : `${PRIVATE_PREFIX}${path}`;
|
|
689
|
+
}
|
|
690
|
+
return path.startsWith(PRIVATE_PREFIX)
|
|
691
|
+
? path.slice(PRIVATE_PREFIX.length)
|
|
692
|
+
: path;
|
|
693
|
+
}
|
|
491
694
|
/**
|
|
492
695
|
* 删除 Engram + 级联删除触及的 synapses + 清理 index
|
|
696
|
+
*
|
|
697
|
+
* 走 `this.deleteSynapsesTouching` 方法版而非模块函数,以触发
|
|
698
|
+
* 邻居派生段( Obsidian wikilinks)的 cascade refresh。
|
|
493
699
|
*/
|
|
494
700
|
deleteEngram(stableId) {
|
|
495
701
|
const relativePath = this.resolvePath(stableId);
|
|
@@ -497,7 +703,7 @@ export class EngramRepository {
|
|
|
497
703
|
return;
|
|
498
704
|
const absolutePath = join(this.config.rootPath, relativePath);
|
|
499
705
|
deleteEngramFile(absolutePath);
|
|
500
|
-
deleteSynapsesTouching(
|
|
706
|
+
this.deleteSynapsesTouching(stableId);
|
|
501
707
|
if (isStableEngramId(stableId)) {
|
|
502
708
|
this.deleteIndexEntry(stableId);
|
|
503
709
|
}
|
|
@@ -613,9 +819,40 @@ export class EngramRepository {
|
|
|
613
819
|
readSynapseById(synapseId) {
|
|
614
820
|
return readSynapseById(this.config.rootPath, synapseId);
|
|
615
821
|
}
|
|
822
|
+
/**
|
|
823
|
+
* 触发 Obsidian 派生段重建(多条 engram 一次性刷新,去重)。
|
|
824
|
+
*
|
|
825
|
+
* 永不抛(regenerateObsidianLinks 内部已吞错)。调用方无需 try/catch。
|
|
826
|
+
*/
|
|
827
|
+
refreshObsidianLinks(...ids) {
|
|
828
|
+
const seen = new Set();
|
|
829
|
+
for (const id of ids) {
|
|
830
|
+
if (!id || seen.has(id))
|
|
831
|
+
continue;
|
|
832
|
+
seen.add(id);
|
|
833
|
+
regenerateObsidianLinks(this.config.rootPath, id, this.language);
|
|
834
|
+
}
|
|
835
|
+
}
|
|
616
836
|
/** 创建 synapse(idempotent) */
|
|
617
837
|
createSynapse(input) {
|
|
618
|
-
|
|
838
|
+
// 继承最严 visibility:取 from/to 两端 engram 的 max(private > restricted > team > public)
|
|
839
|
+
// 端点查不到(已删/无效 id)时降级 'public',不阻塞 synapse 创建
|
|
840
|
+
let fromVis = "public";
|
|
841
|
+
let toVis = "public";
|
|
842
|
+
try {
|
|
843
|
+
fromVis = this.readEngram(input.from).visibility ?? "public";
|
|
844
|
+
}
|
|
845
|
+
catch {
|
|
846
|
+
// from engram 不存在或不可读,降级 public
|
|
847
|
+
}
|
|
848
|
+
try {
|
|
849
|
+
toVis = this.readEngram(input.to).visibility ?? "public";
|
|
850
|
+
}
|
|
851
|
+
catch {
|
|
852
|
+
// to engram 不存在或不可读,降级 public
|
|
853
|
+
}
|
|
854
|
+
const inheritedVisibility = maxVisibility(fromVis, toVis);
|
|
855
|
+
const result = upsertSynapse(this.config.rootPath, {
|
|
619
856
|
from: input.from,
|
|
620
857
|
to: input.to,
|
|
621
858
|
kind: input.kind,
|
|
@@ -625,8 +862,17 @@ export class EngramRepository {
|
|
|
625
862
|
createdBy: input.createdBy,
|
|
626
863
|
sourceSemantic: input.sourceSemantic,
|
|
627
864
|
targetSemantic: input.targetSemantic,
|
|
865
|
+
visibility: inheritedVisibility,
|
|
628
866
|
language: this.language,
|
|
629
867
|
});
|
|
868
|
+
this.refreshObsidianLinks(input.from, input.to);
|
|
869
|
+
// Task 3.4 Phase B:synapse 创建影响 graph 结构,触发 prompt-signals rebuild
|
|
870
|
+
safeEmit({
|
|
871
|
+
type: "synapse_created",
|
|
872
|
+
engramId: input.from,
|
|
873
|
+
at: new Date().toISOString(),
|
|
874
|
+
});
|
|
875
|
+
return result;
|
|
630
876
|
}
|
|
631
877
|
/**
|
|
632
878
|
* 更新 synapse(走 upsert,合并 evidence)。
|
|
@@ -648,7 +894,7 @@ export class EngramRepository {
|
|
|
648
894
|
const oldPath = join(this.config.rootPath, synapseRelativePath(target.id, target.kind));
|
|
649
895
|
deleteSynapseFile(oldPath);
|
|
650
896
|
}
|
|
651
|
-
|
|
897
|
+
const result = upsertSynapse(this.config.rootPath, {
|
|
652
898
|
from: target.from,
|
|
653
899
|
to: target.to,
|
|
654
900
|
kind: nextKind,
|
|
@@ -659,8 +905,13 @@ export class EngramRepository {
|
|
|
659
905
|
sourceSemantic: target.sourceSemantic,
|
|
660
906
|
targetSemantic: target.targetSemantic,
|
|
661
907
|
resolutionState: target.resolutionState,
|
|
908
|
+
// 保守策略:不因端点 visibility 提升而自动调整 synapse visibility,
|
|
909
|
+
// 保留原值。Phase 1.5 可加 recomputeSynapseVisibility(engramId)。
|
|
910
|
+
visibility: target.visibility ?? "public",
|
|
662
911
|
language: this.language,
|
|
663
912
|
});
|
|
913
|
+
this.refreshObsidianLinks(target.from, target.to);
|
|
914
|
+
return result;
|
|
664
915
|
}
|
|
665
916
|
/** 删除某条 synapse */
|
|
666
917
|
deleteSynapse(synapseId) {
|
|
@@ -669,10 +920,26 @@ export class EngramRepository {
|
|
|
669
920
|
return;
|
|
670
921
|
const path = join(this.config.rootPath, synapseRelativePath(syn.id, syn.kind));
|
|
671
922
|
deleteSynapseFile(path);
|
|
923
|
+
this.refreshObsidianLinks(syn.from, syn.to);
|
|
672
924
|
}
|
|
673
925
|
/** 级联删除触及 engram 的所有 synapse */
|
|
674
926
|
deleteSynapsesTouching(engramId) {
|
|
675
|
-
|
|
927
|
+
// 先抓所有邻居 endpoint — 删除后这些 synapse 就找不到了,
|
|
928
|
+
// 邻居 engram.md 的派生段还引用着 engramId,需要重建。
|
|
929
|
+
const touching = listSynapsesForEngram(this.config.rootPath, engramId);
|
|
930
|
+
const neighbors = new Set();
|
|
931
|
+
for (const s of touching.outgoing) {
|
|
932
|
+
if (s.to !== engramId)
|
|
933
|
+
neighbors.add(s.to);
|
|
934
|
+
}
|
|
935
|
+
for (const s of touching.incoming) {
|
|
936
|
+
if (s.from !== engramId)
|
|
937
|
+
neighbors.add(s.from);
|
|
938
|
+
}
|
|
939
|
+
const count = deleteSynapsesTouching(this.config.rootPath, engramId);
|
|
940
|
+
if (neighbors.size > 0)
|
|
941
|
+
this.refreshObsidianLinks(...neighbors);
|
|
942
|
+
return count;
|
|
676
943
|
}
|
|
677
944
|
/**
|
|
678
945
|
* 添加 outgoing synapse。
|
|
@@ -683,7 +950,7 @@ export class EngramRepository {
|
|
|
683
950
|
* @returns 实际落盘的 synapse(其 id 是计算值)
|
|
684
951
|
*/
|
|
685
952
|
addOutgoingSynapse(fromId, synapse) {
|
|
686
|
-
|
|
953
|
+
const result = upsertSynapse(this.config.rootPath, {
|
|
687
954
|
from: fromId,
|
|
688
955
|
to: synapse.to,
|
|
689
956
|
kind: synapse.kind,
|
|
@@ -701,6 +968,8 @@ export class EngramRepository {
|
|
|
701
968
|
resolutionState: synapse.resolutionState,
|
|
702
969
|
language: this.language,
|
|
703
970
|
});
|
|
971
|
+
this.refreshObsidianLinks(fromId, synapse.to);
|
|
972
|
+
return result;
|
|
704
973
|
}
|
|
705
974
|
/**
|
|
706
975
|
* 删除 outgoing synapse。
|
|
@@ -714,6 +983,7 @@ export class EngramRepository {
|
|
|
714
983
|
return;
|
|
715
984
|
const path = join(this.config.rootPath, synapseRelativePath(target.id, target.kind));
|
|
716
985
|
deleteSynapseFile(path);
|
|
986
|
+
this.refreshObsidianLinks(target.from, target.to);
|
|
717
987
|
}
|
|
718
988
|
/**
|
|
719
989
|
* 更新 synapse 的 resolutionState(contradicts 专用)。
|
|
@@ -822,6 +1092,12 @@ export class EngramRepository {
|
|
|
822
1092
|
*/
|
|
823
1093
|
updateVerificationStatus(id, status) {
|
|
824
1094
|
this.mutateFrontmatter(id, (fm) => ({ ...fm, verificationStatus: status }));
|
|
1095
|
+
// Task 3.4 Phase B:验证状态变化影响 prompt 的 lowConfidenceTopics
|
|
1096
|
+
safeEmit({
|
|
1097
|
+
type: "engram_verified",
|
|
1098
|
+
engramId: id,
|
|
1099
|
+
at: new Date().toISOString(),
|
|
1100
|
+
});
|
|
825
1101
|
}
|
|
826
1102
|
/**
|
|
827
1103
|
* 低层 frontmatter 修改:不触发 version++,不改 slug,不改 path。
|
|
@@ -876,6 +1152,11 @@ export class EngramRepository {
|
|
|
876
1152
|
path: orphanPath,
|
|
877
1153
|
message: `Markdown file without frontmatter: ${orphanPath} (either delete it or add frontmatter with a stable id)`,
|
|
878
1154
|
autoFixed: false,
|
|
1155
|
+
nextAction: {
|
|
1156
|
+
tool: "engram_create",
|
|
1157
|
+
argsHint: `{ title, content, kind, domainTags, createdBy } // 直接读取这个 markdown 的内容作为 engram body`,
|
|
1158
|
+
explanation: "Markdown 文件没有 frontmatter,所以不在 engram 索引里。如果是新记忆,用 engram_create 注册(把现有正文粘到 content);如果是废弃草稿,直接 rm 即可。",
|
|
1159
|
+
},
|
|
879
1160
|
});
|
|
880
1161
|
pendingManualReview.push(issues[issues.length - 1]);
|
|
881
1162
|
});
|
|
@@ -913,11 +1194,17 @@ export class EngramRepository {
|
|
|
913
1194
|
// 标记相关 synapse dangling
|
|
914
1195
|
const touching = listSynapsesForEngram(this.config.rootPath, oldId);
|
|
915
1196
|
if (touching.outgoing.length + touching.incoming.length > 0) {
|
|
1197
|
+
const danglingCount = touching.outgoing.length + touching.incoming.length;
|
|
916
1198
|
pendingManualReview.push({
|
|
917
1199
|
kind: "dangling_synapse",
|
|
918
1200
|
stableId: oldId,
|
|
919
|
-
message: `Engram ${oldId} was deleted but ${
|
|
1201
|
+
message: `Engram ${oldId} was deleted but ${danglingCount} synapse(s) still reference it (clean up manually or restore the engram)`,
|
|
920
1202
|
autoFixed: false,
|
|
1203
|
+
nextAction: {
|
|
1204
|
+
tool: "synapse_delete",
|
|
1205
|
+
argsHint: `{ id: "<synapseId>" } // synapseId 在每个 synapse 的 yaml id 字段,逐条删`,
|
|
1206
|
+
explanation: `被删 engram 还有 ${danglingCount} 条 synapse 指向它,这些 synapse 现在是悬空的。去 synapses/ 目录找出涉及该 engram 的 synapse,用 synapse_delete 逐条清理;或者用 engram_create 重建该 engram(让 synapse 重新有目标)。`,
|
|
1207
|
+
},
|
|
921
1208
|
});
|
|
922
1209
|
}
|
|
923
1210
|
}
|
|
@@ -988,6 +1275,35 @@ export class EngramRepository {
|
|
|
988
1275
|
});
|
|
989
1276
|
}
|
|
990
1277
|
}
|
|
1278
|
+
// 5. Obsidian 视图一致性(派生段 wikilinks)
|
|
1279
|
+
// 对每条 engram:checkObsidianView 检测派生段与权威源(synapse yaml)不一致,
|
|
1280
|
+
// 不一致 → regenerateObsidianLinks 重写派生段(wikilink target=文件名)。
|
|
1281
|
+
for (const [id, entry] of freshIndex.entries) {
|
|
1282
|
+
const absPath = join(this.config.rootPath, entry.path);
|
|
1283
|
+
if (!existsSync(absPath))
|
|
1284
|
+
continue;
|
|
1285
|
+
let file;
|
|
1286
|
+
try {
|
|
1287
|
+
file = readEngramFile(absPath);
|
|
1288
|
+
}
|
|
1289
|
+
catch {
|
|
1290
|
+
continue; // parse 错误由别处报告(或phan_markdown 路径)
|
|
1291
|
+
}
|
|
1292
|
+
const touching = listSynapsesForEngram(this.config.rootPath, id);
|
|
1293
|
+
const status = checkObsidianView(file, touching, freshIndex);
|
|
1294
|
+
if (!status.stale)
|
|
1295
|
+
continue;
|
|
1296
|
+
regenerateObsidianLinks(this.config.rootPath, id, this.language);
|
|
1297
|
+
fixes.push({
|
|
1298
|
+
kind: "obsidian_view_stale",
|
|
1299
|
+
stableId: id,
|
|
1300
|
+
path: entry.path,
|
|
1301
|
+
message: `Obsidian derived wikilinks regenerated (target=filename, display=title·kind)`,
|
|
1302
|
+
autoFixed: true,
|
|
1303
|
+
});
|
|
1304
|
+
}
|
|
1305
|
+
// Task 3.4 Phase B:doctor 完成后 emit(doctor 可能 sweep/forget,engram 集合变化)
|
|
1306
|
+
safeEmit({ type: "doctor_completed", at: new Date().toISOString() });
|
|
991
1307
|
return {
|
|
992
1308
|
startedAt,
|
|
993
1309
|
finishedAt: now(),
|