@sanqianx/project-knowledge 5.0.9-rc.9 → 5.0.10-rc.1
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/.claude-plugin/marketplace.json +1 -1
- package/CHANGELOG.md +140 -0
- package/README.md +11 -4
- package/_modules/knowledge-engine/README.md +9 -1
- package/_modules/knowledge-engine/package.json +1 -1
- package/_modules/knowledge-engine/src/coordinator.js +11 -2
- package/_modules/knowledge-engine/src/headless-analyzer.js +2 -1
- package/_modules/knowledge-engine/src/memories.js +133 -24
- package/_modules/knowledge-engine/src/retrieval.js +19 -4
- package/_modules/knowledge-engine/src/runtime/contracts.js +1 -0
- package/_modules/knowledge-engine/src/runtime/knowledge-promotion.js +6 -2
- package/_modules/knowledge-engine/src/runtime/markdown-knowledge-indexer.js +7 -2
- package/_modules/knowledge-engine/src/server.js +1 -1
- package/_modules/knowledge-engine/src/store.js +9 -6
- package/_site/_test/bin-cli-test.js +15 -2
- package/_site/_test/mcp-server-test.js +1 -1
- package/_site/_test/module-agent-start-proxy-test.js +65 -0
- package/_site/_test/module-bridge-eventbridge-test.js +1 -1
- package/_site/_test/module-wiring-unbind-race-test.js +59 -0
- package/_site/_test/path-consistency-test.js +1 -1
- package/_site/_test/product-diagnostics-ui-test.js +5 -1
- package/_site/_test/product-progress-status-test.js +70 -0
- package/_site/_test/shared-scope-root-binding-test.js +67 -0
- package/_site/_test/test-server-cleanup-test.js +5 -1
- package/_site/_test/vector-storage-root-test.js +16 -0
- package/_site/lib/engine-connection.js +4 -1
- package/_site/lib/knowledge-tool-runtime.js +1 -1
- package/_site/lib/module-bridge.js +65 -8
- package/_site/lib/server-app.js +4 -2
- package/_site/lib/settings-store.js +20 -0
- package/module-runtime-manifest.json +41 -41
- package/package.json +3 -3
- package/plugins/project-knowledge/.claude-plugin/plugin.json +1 -1
- package/plugins/project-knowledge/.codex-plugin/plugin.json +1 -1
- package/plugins/project-knowledge/.mcp.json +1 -1
- package/ui/index.html +2 -2
- package/ui/product.css +1 -0
- package/ui/product.js +62 -10
- package/vendor-manifest.json +12 -12
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,145 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [5.0.10-rc.1] - 2026-10-12
|
|
4
|
+
|
|
5
|
+
- Add global/domain/project memory applicability and a single review form for
|
|
6
|
+
editing content, selecting any existing target and confirming atomically.
|
|
7
|
+
- Retain original evidence, scope-aware duplicate suppression, existing v5
|
|
8
|
+
global memories and all fixed code/knowledge directory bindings.
|
|
9
|
+
- Filter exclusive memories before retrieval ranking and file reads; clear old
|
|
10
|
+
projections after scope changes and report/retry pending file synchronization.
|
|
11
|
+
- Keep the accepted Workbench, vector-hub and event-bridge versions unchanged;
|
|
12
|
+
candidate package acceptance precedes stable latest promotion.
|
|
13
|
+
|
|
14
|
+
## [5.0.9] - 2026-10-12
|
|
15
|
+
|
|
16
|
+
- Freeze the accepted rc.19 web runtime and exact module versions; no new
|
|
17
|
+
product features, old-version migration or existing v5 data reset.
|
|
18
|
+
- Include index failure diagnostics, index-only recovery, knowledge-root
|
|
19
|
+
index storage, safe client discovery/notify chaining, maintenance isolation
|
|
20
|
+
and the embedded Workbench startup/streaming fixes from the candidate series.
|
|
21
|
+
- Publish initially to next. Official npm installation, Node 22/24 regression,
|
|
22
|
+
real MiniMax knowledge/embedding loops and existing-user upgrade acceptance
|
|
23
|
+
must pass before stable latest promotion.
|
|
24
|
+
|
|
25
|
+
## [5.0.9-rc.19] - 2026-10-12
|
|
26
|
+
|
|
27
|
+
- Pin Workbench 0.13.2-rc.7. Startup shows a loading state and keeps controls
|
|
28
|
+
inert until projects and event handlers are ready; the existing streaming
|
|
29
|
+
and session recovery paths are unchanged.
|
|
30
|
+
- Preserve the real rc.18 premature-click failure and require its actual
|
|
31
|
+
usable-page ready receipt in browser acceptance. Add a deterministic
|
|
32
|
+
startup gate regression and Node 22/24 retests.
|
|
33
|
+
- Include Windows test-server cleanup hardening and corrected storage-root
|
|
34
|
+
assertions. Retain existing v5 data, third-party configuration and the
|
|
35
|
+
official-package acceptance gate before stable latest promotion.
|
|
36
|
+
|
|
37
|
+
## [5.0.9-rc.18] - 2026-10-12
|
|
38
|
+
|
|
39
|
+
- Project resolution for MCP and CLI now reports the vector-hub storage root
|
|
40
|
+
selected in settings, not the retired shell database path. A running tool
|
|
41
|
+
rereads root changes; resolution does not move files or rebuild indexes.
|
|
42
|
+
- Preserve the reproduced rc.17 diagnostic mismatch and add Node 22/24
|
|
43
|
+
coverage for configured, changed and unset roots and MCP output.
|
|
44
|
+
- Retain existing v5 data and all accepted module pins. Publish to next;
|
|
45
|
+
stable latest remains behind official-package and actual-user acceptance.
|
|
46
|
+
|
|
47
|
+
## [5.0.9-rc.17] - 2026-10-11
|
|
48
|
+
|
|
49
|
+
- Pin Workbench 0.13.2-rc.6. Repeated embedded notifications for the current
|
|
50
|
+
project no longer abandon a newly created session; real project changes and
|
|
51
|
+
late project lookups still preserve isolation and per-project drafts.
|
|
52
|
+
- Keep a healthy progress stream labelled as connected on redundant online
|
|
53
|
+
events. Verify actual offline cleanup, snapshot recovery and exactly one
|
|
54
|
+
restored SSE connection with a real browser and HTTP proxy.
|
|
55
|
+
- Preserve the rc.16 real Workbench failure and require official-package
|
|
56
|
+
streaming/citation acceptance before upgrading the actual user instance.
|
|
57
|
+
- No data reset or third-party configuration overwrite. Stable latest remains
|
|
58
|
+
behind the full candidate and installed-product acceptance gates.
|
|
59
|
+
|
|
60
|
+
## [5.0.9-rc.16] - 2026-10-11
|
|
61
|
+
|
|
62
|
+
- Pin Workbench 0.13.2-rc.5. Bound OpenCode's health request at both
|
|
63
|
+
response headers and JSON body so a stalled local server cannot exceed
|
|
64
|
+
the startup handshake budget before the existing CLI fallback.
|
|
65
|
+
- Reproduce the old behavior using two live HTTP connections, retain the
|
|
66
|
+
failed rc.15 client acceptance and require a new official-package retest.
|
|
67
|
+
- No project, knowledge, history, provider or third-party hook reset.
|
|
68
|
+
Candidate is published to next; stable latest still requires acceptance.
|
|
69
|
+
|
|
70
|
+
## [5.0.9-rc.15] - 2026-10-11
|
|
71
|
+
|
|
72
|
+
- Serialize registration, module removal and registry deletion for each project.
|
|
73
|
+
Recheck membership inside the wiring lease so delayed supervisor registration
|
|
74
|
+
cannot resurrect a project after keep-data unbind. Unrelated projects remain
|
|
75
|
+
independent; failed deletion can still be repaired and retried.
|
|
76
|
+
- Reproduce the race with a delayed real HTTP registration and preserve the
|
|
77
|
+
downloaded rc.14 Node 22 UI failure. Include the regression in the package.
|
|
78
|
+
- Use non-binding CLI readiness probes in tests; the old bind probe could take
|
|
79
|
+
the exact port while its own foreground child was starting.
|
|
80
|
+
- Real MiniMax acceptance covers overlapping analyses, another commit and a
|
|
81
|
+
dirty developer draft during analysis, streaming, secondary development,
|
|
82
|
+
four installed clients and evidenced preference candidates. New downloaded
|
|
83
|
+
candidate acceptance and actual-user cutover still precede stable latest.
|
|
84
|
+
|
|
85
|
+
## [5.0.9-rc.14] - 2026-10-11
|
|
86
|
+
|
|
87
|
+
- Vendor engine 0.2.3-rc.7: isolated maintenance consumes frozen evidence even
|
|
88
|
+
while the user edits or commits in the source repository. Do not mistake live
|
|
89
|
+
development for analyzer writes; retain live-source protection for custom
|
|
90
|
+
analyzers without the isolation capability.
|
|
91
|
+
- Add a paused child-worker regression covering another commit, uncommitted
|
|
92
|
+
drafts, frozen attribution, same-project FIFO and unchanged path/tool guards.
|
|
93
|
+
- Existing-user npm acceptance verified real MiniMax streaming, retained v5
|
|
94
|
+
history/configuration, D-root indexes and index-only e9f6560 recovery. Preserve
|
|
95
|
+
the actual rc.13 PROJECT_BUSY failure and require a real mid-analysis commit
|
|
96
|
+
retest from downloaded rc.14 before stable promotion.
|
|
97
|
+
|
|
98
|
+
## [5.0.9-rc.13] - 2026-10-11
|
|
99
|
+
|
|
100
|
+
- Give native Agent session creation and input setup a bounded 90-second proxy
|
|
101
|
+
budget. Ordinary API reads/abort/permissions retain 10 seconds; SSE streaming
|
|
102
|
+
and folder-picker contracts are unchanged.
|
|
103
|
+
- Record safe proxy failure categories and deadlines without raw errors or URL
|
|
104
|
+
queries. Preserve MODULE_UNREACHABLE compatibility and add optional details.
|
|
105
|
+
- Reproduce the installed OpenCode cold-start failure with a real delayed HTTP
|
|
106
|
+
server; verify the targeted budget, unrelated routes and credential redaction.
|
|
107
|
+
- Retain rc.12 real acceptance and failed OpenCode results separately; require
|
|
108
|
+
a newly downloaded candidate and cold-client retest before user upgrade.
|
|
109
|
+
|
|
110
|
+
## [5.0.9-rc.12] - 2026-10-11
|
|
111
|
+
|
|
112
|
+
- Vendor engine 0.2.3-rc.6 to address the Windows transient state-file rename
|
|
113
|
+
failure exposed by installed rc.11 fault acceptance. Use exclusive random
|
|
114
|
+
temporary files and bounded sharing retries, retain durable state on failure,
|
|
115
|
+
and never unlink live state to force replacement.
|
|
116
|
+
- Add deterministic sharing-violation, permanent-lock, foreign-temp and
|
|
117
|
+
disk-full regressions; all 39 engine tests pass on Node 22 and Node 24.
|
|
118
|
+
- Preserve the rc.11 failure and require new installed acceptance before any
|
|
119
|
+
existing-user upgrade or stable latest promotion.
|
|
120
|
+
|
|
121
|
+
## [5.0.9-rc.11] - 2026-10-11
|
|
122
|
+
|
|
123
|
+
- Pin vector-hub 1.0.8-rc.3 and vendor engine 0.2.3-rc.5. Raw commit
|
|
124
|
+
conversations and internal backups are analysis evidence, not knowledge.
|
|
125
|
+
Exclude them from managed indexes, search and get; retain source evidence.
|
|
126
|
+
- Prune stale derived records during sync and rebuild once on source-policy
|
|
127
|
+
upgrade, without repeating paid whole-index work on ordinary restarts.
|
|
128
|
+
- Keep standalone vector-hub general document support unchanged.
|
|
129
|
+
- Record both original failing regressions and successful Node 22/24 reruns;
|
|
130
|
+
installed, real model and existing-v5 gates still precede latest promotion.
|
|
131
|
+
|
|
132
|
+
## [5.0.9-rc.10] - 2026-10-11
|
|
133
|
+
|
|
134
|
+
- Preserve acknowledged brain/domain knowledge-directory bindings when changing
|
|
135
|
+
the knowledge root. Only derived indexes and future imports/shared scopes use
|
|
136
|
+
the new root; existing knowledge and confirmed memories stay in place.
|
|
137
|
+
- Persist bindings in shell metadata, reject implicit rebinding, and report
|
|
138
|
+
shared-scope connection failures in UI and safe structured logs.
|
|
139
|
+
- Add real-engine regression for confirmed memory, domain retrieval, restart,
|
|
140
|
+
new domain registration, offline retry and protected bindings on Node 22/24.
|
|
141
|
+
- Candidate acceptance still precedes the existing-v5 upgrade and stable latest.
|
|
142
|
+
|
|
3
143
|
## [5.0.9-rc.9] - 2026-10-11
|
|
4
144
|
|
|
5
145
|
- Vendor engine 0.2.3-rc.4. Freeze existing target paths and hashes before
|
package/README.md
CHANGED
|
@@ -194,13 +194,18 @@ project-knowledge
|
|
|
194
194
|
```
|
|
195
195
|
|
|
196
196
|
The user selects a global knowledge root before importing projects. That root
|
|
197
|
-
|
|
197
|
+
chooses directories for future imports, new shared scopes, and derived vector
|
|
198
|
+
indexes (`<knowledge root>/.indexes`). Each imported project stores an
|
|
198
199
|
absolute, fixed `knowledgePath`; changing the global root does not move existing
|
|
199
200
|
projects.
|
|
200
201
|
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
202
|
+
Existing project and shared-knowledge directories do not move when the root
|
|
203
|
+
changes. Brain/domain directory bindings are retained in the shell settings
|
|
204
|
+
after the engine acknowledges them. Existing engine bindings are reused when
|
|
205
|
+
first recording those fields. A failed registration is shown as pending, not
|
|
206
|
+
healthy. Old index directories are retained; indexes rebuild at the new root.
|
|
207
|
+
Settings, registration metadata, caches, runtime state, logs, and recovery
|
|
208
|
+
assets remain in the internal data directory; vector payloads do not:
|
|
204
209
|
|
|
205
210
|
```text
|
|
206
211
|
~/.project-knowledge-v5/
|
|
@@ -220,6 +225,8 @@ remain in the internal data directory:
|
|
|
220
225
|
└── recovery/ # migrations, promotion/index backups
|
|
221
226
|
|
|
222
227
|
<selected knowledge root>/
|
|
228
|
+
├── .indexes/ # rebuildable vector payloads
|
|
229
|
+
├── _shared/<brain or domain ID>/ # fixed shared-knowledge bindings
|
|
223
230
|
└── <project storage name>/
|
|
224
231
|
├── README.md # when produced by verified knowledge
|
|
225
232
|
├── GOAL.md
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# knowledge-engine
|
|
2
2
|
|
|
3
|
+
正式知识检索(包括 `get`)排除 `dev-conversations`、内部会话、备份与隐藏目录。提交会话只作为分析证据,不直接充当已晋升知识;原始证据仍由事件模块保留。
|
|
4
|
+
|
|
3
5
|
> 开发前读 [AGENTS.md](AGENTS.md)。该指南中的脚手架状态是历史背景;当前运行契约及实现状态以本 README 为准。
|
|
4
6
|
|
|
5
7
|
`project-knowledge` 模块家族的第四块:**知识引擎**。owns 两个职责面,shell 只负责连接与投影("The shell owns no features"):
|
|
@@ -39,6 +41,8 @@ node src/bin.js serve [--port 5790] [--host 127.0.0.1] [--data DIR]
|
|
|
39
41
|
|
|
40
42
|
本模块零新增 npm 依赖、CommonJS、Node ≥18;无头 Agent SDK 由宿主已安装的 Workbench/SDK 提供,缺失时明确失败,不退回假分析。完整产品的 Node 门槛为 ≥22。
|
|
41
43
|
|
|
44
|
+
隔离无头分析只读冻结证据、只写 staging,不以开发工作树的前后变化判定失败。用户可以在分析中继续编辑或提交;当前任务仍归属冻结提交,后续提交按项目 FIFO 执行。未声明隔离冻结证据能力的自定义分析器仍保留源码变更检查;目录越界、工具权限和晋升校验不变。
|
|
45
|
+
|
|
42
46
|
每次真实分析在只读证据区冻结已有知识的目标路径与哈希清单,覆盖未被检索选中的文件;重试复用该清单。Agent 不得把“未检索到”当作“文件不存在”,也不得用部分片段覆盖完整旧文档;无法保留原文时只追加当前提交的变更记录。已有目标仍由晋升校验拒绝非法 create,不自动改为覆盖。清单仅保存元数据,不把整个知识树复制到每次运行中。
|
|
43
47
|
|
|
44
48
|
## API
|
|
@@ -58,7 +62,7 @@ node src/bin.js serve [--port 5790] [--host 127.0.0.1] [--data DIR]
|
|
|
58
62
|
| GET | `/api/snapshot`、`/api/progress` | 持久化快照 / 快照先行的 SSE 更新 |
|
|
59
63
|
| GET / POST | `/api/events` | 真实事件查询 / 仅调试模式注入事件 |
|
|
60
64
|
| POST | `/api/bridge/notify` | 唤醒 journal 消费;通知丢失仍由轮询恢复 |
|
|
61
|
-
| GET / PATCH | `/api/memories`、`/api/memories/:id` |
|
|
65
|
+
| GET / PATCH | `/api/memories`、`/api/memories/:id` | 按 status/scopeType/scopeId 筛选;一次编辑内容、适用范围和确认状态;来源证据只读 |
|
|
62
66
|
| POST | `/api/scopes` | 外壳投影智脑、领域共享目录;受运行期鉴权保护 |
|
|
63
67
|
| POST | `/api/runtime-config` | 运行期模型配置,仅保留在内存;受鉴权保护 |
|
|
64
68
|
| GET | `/api/runtime-auth` | 本机服务确认运行期鉴权;受鉴权保护,不对浏览器反代 |
|
|
@@ -81,7 +85,11 @@ node src/bin.js serve [--port 5790] [--host 127.0.0.1] [--data DIR]
|
|
|
81
85
|
- 项目、领域、智脑检索使用包含范围。未分配领域不是共享领域:领域范围只包含当前项目与智脑共享知识;显式加入同一领域后才包含其他项目。整个智脑范围仍包含全部同智脑项目。向量只提供排序,显示内容必须来自当前真实文件;索引不可用时降级为文件检索。
|
|
82
86
|
- 已晋升提交的 SHA 或短 SHA 在自然语言查询中仍作为标识符匹配,只返回当前文件的晋升依据;仅在原始会话中提到相同 SHA 不构成知识晋升引用。多个提交标识符可以一起查询。
|
|
83
87
|
- 候选必须有已晋升文件中的引用依据,未确认候选不能进入任务。确认记录原子持久化后写共享记忆文件,派生索引异步更新,索引失败不会撤销有效记忆。
|
|
88
|
+
- 记忆的适用范围为 `global/domain/project`,目标用 `scopeId` 指定;来源项目/领域不等于适用范围。新候选默认来源项目,分析器只有明确证据时才建议领域/全局;用户审核时可以编辑内容并选择当前智脑中的其他已有目标。原始总结与证据保持不变,同一范围内容去重,忽略和编辑后的原候选不会重复生成。
|
|
89
|
+
- 长期记忆统一保存在已绑定的智脑共享目录:`memories/confirmed.md`(全局)、`memories/domains/<id>.md`、`memories/projects/<id>.md`。不创建第二份项目知识目录。现有 v5 缺少范围字段的记忆仍按全局解释;不读取 pre-v5 状态、不重扫历史。
|
|
90
|
+
- 任务检索只允许当前项目、当前所属领域与全局的已确认记忆,即使检索整个智脑也不放宽专属记忆范围。过滤在排名之前完成,`get` 同样校验;向量旧内容不能绕过当前状态和文件一致性校验。已确认记录修改范围会同步清理旧投影;写入失败显示待同步并自动重试,未同步文件不能进入上下文。
|
|
84
91
|
- 旧格式不读取、不自动迁移。损坏的正式状态不覆盖、不清空。
|
|
92
|
+
- 引擎状态复用受控原子写入:独占随机临时文件、文件同步、Windows 短暂共享冲突有界重试;永久失败保留原状态并清理本次临时文件,绝不先删正式状态。
|
|
85
93
|
|
|
86
94
|
## 验证与尚未验收项
|
|
87
95
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sanqianx/ai-coding-knowledge-engine",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.4-rc.1",
|
|
4
4
|
"description": "Knowledge engine module for project-knowledge: commit-boundary triggered knowledge analysis/generation (trigger plane) and retrieval for development assistants (retrieval plane).",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"main": "src/server.js",
|
|
@@ -39,7 +39,15 @@ class EngineCoordinator extends EventEmitter {
|
|
|
39
39
|
this.sharedIndexJobs = new Map();
|
|
40
40
|
this.sharedIndexDirty = new Set();
|
|
41
41
|
this.sharedIndexQueue = Promise.resolve();
|
|
42
|
-
this.memories = new MemoryService(store, {
|
|
42
|
+
this.memories = new MemoryService(store, {
|
|
43
|
+
getIndexStatus: scope => scope && this.sharedIndexJobs.has(scope.id) ? 'pending' : null,
|
|
44
|
+
onMaterialized: scope => { this.indexSharedScope(scope).catch(() => {}); },
|
|
45
|
+
onSyncChange: (scope, code) => {
|
|
46
|
+
this.emit('progress', store.addEvent({ type: 'memory.sync', scopeId: scope.id, summary: code ? `记忆文件待恢复(${code})` : '记忆文件已恢复同步' }));
|
|
47
|
+
const log = code ? 'error' : 'info';
|
|
48
|
+
Promise.resolve(this.diagnostics[log]('knowledge.memory.' + (code ? 'write_failed' : 'write_recovered'), code ? '记忆状态已保存,文件待恢复。' : '记忆文件已恢复同步。', { phase: 'memory-sync', context: { scopeId: scope.id, code: code || null } })).catch(() => {});
|
|
49
|
+
},
|
|
50
|
+
});
|
|
43
51
|
this.projectStore = {
|
|
44
52
|
readConfig: id => { const p = store.getProject(id); if (!p) throw Object.assign(new Error('项目未登记。'), { code: 'PROJECT_NOT_FOUND' }); return { ...p, displayName: p.name, automation: { enabled: true } }; },
|
|
45
53
|
readState: id => store.readRuntime(id),
|
|
@@ -50,7 +58,7 @@ class EngineCoordinator extends EventEmitter {
|
|
|
50
58
|
this.retrieval = new EngineRetrieval({ layout: this.layout, projectStore: this.projectStore, registryStore, store, memories: this.memories, vectorUrl: this.vectorUrl });
|
|
51
59
|
this.conversations = new ConversationStore({ layout: this.layout, projectStore: this.projectStore });
|
|
52
60
|
this.analyzer = options.analyzer || new HeadlessAnalyzer({ resolveProfile: id => this.profiles.get(id) || this.profiles.get('default'), timeoutMs: options.timeoutMs });
|
|
53
|
-
this.promotion = new KnowledgePromotionService({ layout: this.layout, logger: this.diagnostics, projectStore: this.projectStore, analyzer: { runClaim: async input => {
|
|
61
|
+
this.promotion = new KnowledgePromotionService({ layout: this.layout, logger: this.diagnostics, projectStore: this.projectStore, analyzer: { sourceReadMode: this.analyzer.sourceReadMode, runClaim: async input => {
|
|
54
62
|
const run = this.currentRun(input.projectId);
|
|
55
63
|
store.updateRun(run.runId, { analysisRunId: input.claim.runId, phase: 'analyzing' });
|
|
56
64
|
this.notify(run, '正在分析冻结的提交证据');
|
|
@@ -291,6 +299,7 @@ class EngineCoordinator extends EventEmitter {
|
|
|
291
299
|
return run;
|
|
292
300
|
}
|
|
293
301
|
async close() {
|
|
302
|
+
await this.memories.close();
|
|
294
303
|
this.closed = true; clearInterval(this.poll);
|
|
295
304
|
this.bridge.close?.();
|
|
296
305
|
await this.analyzer.close?.();
|
|
@@ -6,6 +6,7 @@ const { finalizeAnalysisOutput } = require('./analysis-output');
|
|
|
6
6
|
const { freezeKnowledgeTargets } = require('./knowledge-targets');
|
|
7
7
|
|
|
8
8
|
class HeadlessAnalyzer {
|
|
9
|
+
get sourceReadMode() { return 'isolated-frozen-evidence'; }
|
|
9
10
|
constructor({ resolveProfile, timeoutMs = 15 * 60 * 1000, workerPath = path.join(__dirname, 'analysis-worker.js') } = {}) { this.resolveProfile = resolveProfile; this.timeoutMs = timeoutMs; this.workerPath = workerPath; this.children = new Map(); this.closed = false; }
|
|
10
11
|
async runClaim(input) {
|
|
11
12
|
if (this.closed) throw Object.assign(new Error('分析服务正在关闭。'), { code: 'WORKER_STOPPED', retryable: true });
|
|
@@ -50,7 +51,7 @@ class HeadlessAnalyzer {
|
|
|
50
51
|
if (!requested) outcome = Object.assign(new Error('后台分析进程意外退出。'), { code: 'WORKER_EXITED', retryable: true });
|
|
51
52
|
if (outcome) reject(outcome); else resolve();
|
|
52
53
|
});
|
|
53
|
-
const memoryContract = '\nYou may optionally include memoryCandidates in manifest.json: [{text, entryId, quote}]. Only suggest durable user preferences explicitly evidenced in a promoted document; quote must be an exact excerpt (at least 8 characters) from that document. Never infer preferences from code style alone. These stay unconfirmed until the user accepts them.';
|
|
54
|
+
const memoryContract = '\nYou may optionally include memoryCandidates in manifest.json: [{text, entryId, quote, scopeType}]. Only suggest durable user preferences explicitly evidenced in a promoted document; quote must be an exact excerpt (at least 8 characters) from that document. Never infer preferences from code style alone. scopeType is project (default), domain or global. Recommend domain/global only when the quoted owner requirement explicitly applies across that domain/all projects; ambiguous or local requirements stay project-scoped. Do not invent target IDs. These stay unconfirmed until the user edits their text/scope and accepts them.';
|
|
54
55
|
const languageContract = input.config?.knowledgeLanguage === 'en-US' ? '\nOUTPUT LANGUAGE: English. Preserve identifiers and verbatim evidence quotations.' : '\n输出语言:简体中文。知识正文、operations[].reason 与候选记忆用简体中文;代码标识、路径、哈希和原文证据引用保持不变。';
|
|
55
56
|
child.send({ profile, projectId: input.projectId, runId: input.claim.runId, commitSha: input.claim.commitSha, prompt: `${frameFrozenEvidence(input.prompt)}${contract}${pathContract}${targetContract}${memoryContract}${languageContract}`, manifestPath: input.manifestPath, stagingPath: input.stagingPath, safetyPolicy: input.safetyPolicy }, error => { if (error) stop(Object.assign(new Error('无法发送分析任务。'), { code: 'WORKER_START_FAILED', retryable: true })); });
|
|
56
57
|
});
|
|
@@ -6,8 +6,69 @@ const AtomicFile = require('./runtime/atomic-file');
|
|
|
6
6
|
const { RuntimeLayout } = require('./runtime/layout');
|
|
7
7
|
const { validateProjectId, DomainError } = require('./runtime/contracts');
|
|
8
8
|
|
|
9
|
+
const TYPES = ['global', 'domain', 'project'];
|
|
10
|
+
const normalized = text => String(text).toLowerCase().replace(/\s+/g, ' ').trim();
|
|
11
|
+
const fingerprint = (brainId, type, id, text) => crypto.createHash('sha256').update(`${brainId}:${type}:${id}:${normalized(text)}`).digest('hex');
|
|
12
|
+
|
|
9
13
|
class MemoryService {
|
|
10
|
-
constructor(store, options = {}) {
|
|
14
|
+
constructor(store, options = {}) {
|
|
15
|
+
this.store = store; this.layout = new RuntimeLayout(store.dataDir); this.onMaterialized = options.onMaterialized;
|
|
16
|
+
this.syncErrors = new Map(); this.closed = false; this.onSyncChange = options.onSyncChange; this.writes = new Set();
|
|
17
|
+
this.getIndexStatus = options.getIndexStatus;
|
|
18
|
+
}
|
|
19
|
+
lock() { return this.layout.getRuntimePath('locks', 'memories.lock'); }
|
|
20
|
+
scopeOf(memory) { return { type: memory.scopeType || 'global', id: memory.scopeId || memory.brainId || 'brain-default' }; }
|
|
21
|
+
brainScope(brainId) { return this.store.state.sharedScopes.find(s => s.kind === 'brain' && s.brainId === brainId); }
|
|
22
|
+
target(memory) {
|
|
23
|
+
const scope = this.scopeOf(memory), brainId = memory.brainId || 'brain-default';
|
|
24
|
+
const target = scope.type === 'global' ? this.brainScope(brainId) : scope.type === 'domain'
|
|
25
|
+
? this.store.state.sharedScopes.find(s => s.kind === 'domain' && s.id === scope.id && s.brainId === brainId)
|
|
26
|
+
: this.store.listProjects().find(p => p.projectId === scope.id && p.brainId === brainId && p.enabled !== false);
|
|
27
|
+
return { name: target?.name || scope.id, available: Boolean(target) };
|
|
28
|
+
}
|
|
29
|
+
validateScope(type, id, brainId) {
|
|
30
|
+
if (!TYPES.includes(type)) throw new DomainError('INVALID_ARGUMENT', '适用范围无效。');
|
|
31
|
+
validateProjectId(id);
|
|
32
|
+
if (type === 'global' && id !== brainId || type !== 'global' && !this.target({ brainId, scopeType: type, scopeId: id }).available) throw new DomainError('INVALID_ARGUMENT', '所选记忆目标不存在或不属于当前智脑。');
|
|
33
|
+
}
|
|
34
|
+
entry(memory) {
|
|
35
|
+
const scope = this.scopeOf(memory);
|
|
36
|
+
if (scope.type === 'global') return 'memories/confirmed.md';
|
|
37
|
+
validateProjectId(scope.id);
|
|
38
|
+
return `memories/${scope.type === 'domain' ? 'domains' : 'projects'}/${scope.id}.md`;
|
|
39
|
+
}
|
|
40
|
+
descriptor(entry) {
|
|
41
|
+
if (entry === 'memories/confirmed.md') return { type: 'global' };
|
|
42
|
+
const match = /^memories\/(domains|projects)\/([A-Za-z0-9][A-Za-z0-9._-]*)\.md$/.exec(entry);
|
|
43
|
+
if (!match) return null;
|
|
44
|
+
try { validateProjectId(match[2]); } catch { return null; }
|
|
45
|
+
return { type: match[1] === 'domains' ? 'domain' : 'project', id: match[2] };
|
|
46
|
+
}
|
|
47
|
+
file(scope, entry = 'memories/confirmed.md') { return path.join(scope.knowledgePath, ...entry.split('/')); }
|
|
48
|
+
safeFile(scope, entry) {
|
|
49
|
+
const file = this.file(scope, entry);
|
|
50
|
+
if (!this.descriptor(entry) || !this.layout.isPathInside(scope.knowledgePath, file, { realpath: true }) || fs.existsSync(file) && fs.lstatSync(file).isSymbolicLink()) throw new DomainError('PATH_OUTSIDE_ROOT', '记忆文件目录越界。', { status: 403 });
|
|
51
|
+
return file;
|
|
52
|
+
}
|
|
53
|
+
confirmed(brainId, entry) { return this.store.state.memories.filter(m => m.status === 'confirmed' && (m.brainId || 'brain-default') === brainId && this.entry(m) === entry); }
|
|
54
|
+
applicable(memory, project) {
|
|
55
|
+
if (!project || memory.status !== 'confirmed' || (memory.brainId || 'brain-default') !== project.brainId || !this.target(memory).available) return false;
|
|
56
|
+
const scope = this.scopeOf(memory);
|
|
57
|
+
return scope.type === 'global' || scope.type === 'domain' && scope.id === project.domainId || scope.type === 'project' && scope.id === project.projectId;
|
|
58
|
+
}
|
|
59
|
+
readableEntry(scope, entry, project) {
|
|
60
|
+
return scope.kind === 'brain' && !this.syncErrors.has(scope.id) && this.validFile(scope, entry) && this.confirmed(scope.brainId, entry).some(m => this.applicable(m, project));
|
|
61
|
+
}
|
|
62
|
+
memoryForChunk(scope, entry, heading) {
|
|
63
|
+
return this.confirmed(scope.brainId, entry).find(m => (Array.isArray(heading) ? heading : String(heading).split(' > ')).includes(m.id));
|
|
64
|
+
}
|
|
65
|
+
project(memory) {
|
|
66
|
+
const scope = this.scopeOf(memory), target = this.target(memory), brain = this.brainScope(memory.brainId || 'brain-default');
|
|
67
|
+
return { ...memory, scopeType: scope.type, scopeId: scope.id, targetName: target.name, targetAvailable: target.available,
|
|
68
|
+
syncStatus: memory.status !== 'confirmed' ? 'not-required' : brain && !this.syncErrors.has(brain.id) && this.validFile(brain, this.entry(memory)) ? 'ready' : 'pending',
|
|
69
|
+
indexStatus: memory.status === 'confirmed' ? this.getIndexStatus?.(brain) || (brain?.index?.error ? 'failed' : brain?.index?.status || 'pending') : 'not-required',
|
|
70
|
+
...(brain && this.syncErrors.has(brain.id) ? { syncError: this.syncErrors.get(brain.id) } : {}) };
|
|
71
|
+
}
|
|
11
72
|
async configure(scopes) {
|
|
12
73
|
if (!Array.isArray(scopes)) throw new DomainError('INVALID_ARGUMENT', '共享范围必须是数组。');
|
|
13
74
|
const ids = new Set();
|
|
@@ -31,7 +92,10 @@ class MemoryService {
|
|
|
31
92
|
await this.recover();
|
|
32
93
|
return projected;
|
|
33
94
|
}
|
|
34
|
-
list({ brainId, status } = {}) {
|
|
95
|
+
list({ brainId, status, scopeType, scopeId } = {}) {
|
|
96
|
+
if (scopeType && !TYPES.includes(scopeType)) throw new DomainError('INVALID_ARGUMENT', '记忆筛选范围无效。');
|
|
97
|
+
return this.store.state.memories.filter(m => (!brainId || m.brainId === brainId) && (!status || m.status === status) && (!scopeType || this.scopeOf(m).type === scopeType) && (!scopeId || this.scopeOf(m).id === scopeId)).map(m => this.project(m));
|
|
98
|
+
}
|
|
35
99
|
capture(run) {
|
|
36
100
|
const project = this.store.getProject(run.projectId);
|
|
37
101
|
if (!project || !run.promotedAt) return;
|
|
@@ -43,43 +107,88 @@ class MemoryService {
|
|
|
43
107
|
const entryId = String(raw.entryId || '');
|
|
44
108
|
const file = path.resolve(project.knowledgePath, entryId);
|
|
45
109
|
if (!text || text.length > 2000 || quote.length < 8 || !run.staging.operations?.some(op => op.operation !== 'delete' && op.path === entryId) || !this.layout.isPathInside(project.knowledgePath, file, { realpath: true }) || !fs.existsSync(file) || !fs.readFileSync(file, 'utf8').includes(quote)) continue;
|
|
46
|
-
const
|
|
47
|
-
|
|
48
|
-
|
|
110
|
+
const brainId = project.brainId || 'brain-default';
|
|
111
|
+
const scopeType = raw.scopeType === 'global' ? 'global' : raw.scopeType === 'domain' && project.domainId ? 'domain' : 'project';
|
|
112
|
+
const scopeId = scopeType === 'global' ? brainId : scopeType === 'domain' ? project.domainId : project.projectId;
|
|
113
|
+
const key = fingerprint(brainId, scopeType, scopeId, text);
|
|
114
|
+
if (this.store.state.memories.some(m => m.captureFingerprint === key || fingerprint(m.brainId || 'brain-default', this.scopeOf(m).type, this.scopeOf(m).id, m.text) === key || !m.scopeType && m.evidence.projectId === project.projectId && normalized(m.originalText || m.text) === normalized(text))) continue;
|
|
115
|
+
this.store.state.memories.push({ id: `memory-${crypto.randomUUID()}`, fingerprint: key, captureFingerprint: key, brainId, domainId: project.domainId || null, scopeType, scopeId, text, originalText: text, status: 'candidate', evidence: { projectId: project.projectId, commitSha: run.commitSha, runId: run.runId, entryId, quote }, createdAt: new Date().toISOString(), updatedAt: new Date().toISOString() });
|
|
49
116
|
}
|
|
50
117
|
this.store.save();
|
|
51
118
|
}
|
|
52
119
|
async update(id, input) {
|
|
53
|
-
|
|
120
|
+
if (this.closed) throw new DomainError('PROJECT_BUSY', '引擎正在关闭,请稍后重试。', { status: 409 });
|
|
121
|
+
if (Object.keys(input).some(key => !['text', 'status', 'scopeType', 'scopeId'].includes(key))) throw new DomainError('INVALID_ARGUMENT', '来源证据和文件路径不能修改。');
|
|
122
|
+
const job = AtomicFile.withFileLock(this.lock(), async () => {
|
|
54
123
|
const memory = this.store.state.memories.find(m => m.id === id);
|
|
55
124
|
if (!memory) throw new DomainError('PROJECT_NOT_FOUND', '记忆不存在。', { status: 404 });
|
|
56
125
|
if (input.text !== undefined && (typeof input.text !== 'string' || !input.text.trim() || input.text.length > 2000)) throw new DomainError('INVALID_ARGUMENT', '记忆内容必须为 1–2000 个字符。');
|
|
57
126
|
if (input.status !== undefined && !['confirmed', 'ignored'].includes(input.status)) throw new DomainError('INVALID_ARGUMENT', '记忆状态无效。');
|
|
58
127
|
if (memory.status === 'ignored') throw new DomainError('INVALID_ARGUMENT', '该候选已忽略。', { status: 409 });
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
this.
|
|
64
|
-
|
|
65
|
-
|
|
128
|
+
const brainId = memory.brainId || 'brain-default', old = this.scopeOf(memory);
|
|
129
|
+
const type = input.scopeType === undefined ? old.type : input.scopeType;
|
|
130
|
+
const targetId = input.scopeId === undefined ? (type === 'global' ? brainId : type === old.type ? old.id : '') : input.scopeId;
|
|
131
|
+
if (input.status !== 'ignored' || input.scopeType !== undefined || input.scopeId !== undefined) this.validateScope(type, targetId, brainId);
|
|
132
|
+
if ((input.status || memory.status) === 'confirmed' && !this.brainScope(brainId)) throw new DomainError('INVALID_ARGUMENT', '请先绑定智脑共享目录。', { status: 409 });
|
|
133
|
+
const text = input.text?.trim() || memory.text, key = fingerprint(brainId, type, targetId, text);
|
|
134
|
+
if (this.store.state.memories.some(m => m.id !== id && fingerprint(m.brainId || 'brain-default', this.scopeOf(m).type, this.scopeOf(m).id, m.text) === key)) throw new DomainError('MEMORY_DUPLICATE', '该范围内已存在相同记忆。', { status: 409 });
|
|
135
|
+
const next = { ...memory, originalText: memory.originalText || memory.text, captureFingerprint: memory.captureFingerprint || fingerprint(brainId, old.type, old.id, memory.originalText || memory.text), fingerprint: key, scopeType: type, scopeId: targetId, text, status: input.status || memory.status, updatedAt: new Date().toISOString() };
|
|
136
|
+
const before = this.store.state.memories;
|
|
137
|
+
this.store.state.memories = before.map(m => m.id === id ? next : m);
|
|
138
|
+
try { this.store.save(); } catch (error) { this.store.state.memories = before; throw error; }
|
|
139
|
+
await this.materialize(); return this.project(next);
|
|
66
140
|
});
|
|
141
|
+
this.writes.add(job);
|
|
142
|
+
try { return await job; } finally { this.writes.delete(job); }
|
|
67
143
|
}
|
|
68
|
-
content(brainId) {
|
|
69
|
-
return '# 已确认的长期记忆\n\n' + this.
|
|
144
|
+
content(brainId, entry = 'memories/confirmed.md') {
|
|
145
|
+
return '# 已确认的长期记忆\n\n' + this.confirmed(brainId, entry).map(m => {
|
|
146
|
+
const scope = this.scopeOf(m);
|
|
147
|
+
// Preserve legacy global bytes until the user actually edits that record.
|
|
148
|
+
const scopeText = m.scopeType ? `适用范围:${{ global: '全局', domain: '领域', project: '项目' }[scope.type]} · ${scope.id}\n\n` : '';
|
|
149
|
+
const text = m.scopeType ? m.text.replace(/^\s*(#{1,6})\s/gm, '\\$1 ') : m.text;
|
|
150
|
+
return `## ${m.id}\n\n${scopeText}${text}\n\n来源:${m.evidence.projectId} · ${m.evidence.commitSha} · ${m.evidence.entryId}\n`;
|
|
151
|
+
}).join('\n');
|
|
70
152
|
}
|
|
71
|
-
|
|
72
|
-
|
|
153
|
+
existingEntries(scope) {
|
|
154
|
+
const entries = new Set(['memories/confirmed.md']);
|
|
155
|
+
for (const group of ['domains', 'projects']) {
|
|
156
|
+
const directory = path.join(scope.knowledgePath, 'memories', group);
|
|
157
|
+
if (!fs.existsSync(directory)) continue;
|
|
158
|
+
if (fs.lstatSync(directory).isSymbolicLink() || !this.layout.isPathInside(scope.knowledgePath, directory, { realpath: true })) throw new DomainError('PATH_OUTSIDE_ROOT', '记忆目录越界。', { status: 403 });
|
|
159
|
+
for (const name of fs.readdirSync(directory)) if (this.descriptor(`memories/${group}/${name}`)) entries.add(`memories/${group}/${name}`);
|
|
160
|
+
}
|
|
161
|
+
for (const memory of this.store.state.memories.filter(m => m.status === 'confirmed' && (m.brainId || 'brain-default') === scope.brainId)) entries.add(this.entry(memory));
|
|
162
|
+
return entries;
|
|
163
|
+
}
|
|
164
|
+
async materialize() {
|
|
165
|
+
clearTimeout(this.retryTimer);
|
|
73
166
|
for (const scope of this.store.state.sharedScopes.filter(s => s.kind === 'brain')) {
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
167
|
+
try {
|
|
168
|
+
let changed = false;
|
|
169
|
+
for (const entry of this.existingEntries(scope)) {
|
|
170
|
+
const file = this.safeFile(scope, entry), content = this.content(scope.brainId, entry);
|
|
171
|
+
if (!fs.existsSync(file) || fs.readFileSync(file, 'utf8') !== content) { AtomicFile.writeFileAtomic(file, content); changed = true; }
|
|
172
|
+
}
|
|
173
|
+
const recovered = this.syncErrors.delete(scope.id);
|
|
174
|
+
if (recovered) this.onSyncChange?.(scope, null);
|
|
175
|
+
if (changed) await this.onMaterialized?.(scope);
|
|
176
|
+
} catch (error) {
|
|
177
|
+
const code = error.code || 'MEMORY_WRITE_FAILED', prior = this.syncErrors.get(scope.id);
|
|
178
|
+
this.syncErrors.set(scope.id, code);
|
|
179
|
+
if (prior !== code) this.onSyncChange?.(scope, code);
|
|
80
180
|
}
|
|
81
181
|
}
|
|
182
|
+
if (this.syncErrors.size && !this.closed) { this.retryTimer = setTimeout(() => this.recover().catch(() => {}), 5000); this.retryTimer.unref(); }
|
|
183
|
+
}
|
|
184
|
+
async recover() {
|
|
185
|
+
if (this.recovery) return this.recovery;
|
|
186
|
+
this.recovery = AtomicFile.withFileLock(this.lock(), () => this.materialize()).finally(() => { this.recovery = null; });
|
|
187
|
+
return this.recovery;
|
|
188
|
+
}
|
|
189
|
+
validFile(scope, entry = 'memories/confirmed.md') {
|
|
190
|
+
try { const file = this.safeFile(scope, entry); return fs.existsSync(file) && fs.readFileSync(file, 'utf8') === this.content(scope.brainId, entry); } catch { return false; }
|
|
82
191
|
}
|
|
83
|
-
|
|
192
|
+
async close() { this.closed = true; clearTimeout(this.retryTimer); await Promise.allSettled([...this.writes, this.recovery].filter(Boolean)); }
|
|
84
193
|
}
|
|
85
194
|
module.exports = { MemoryService };
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
const fs = require('node:fs');
|
|
3
3
|
const path = require('node:path');
|
|
4
4
|
const { KnowledgeRetrievalService } = require('./runtime/knowledge-retrieval-service');
|
|
5
|
-
const { listMarkdownFiles } = require('./runtime/markdown-knowledge-indexer');
|
|
5
|
+
const { listMarkdownFiles, isExcludedKnowledgePath } = require('./runtime/markdown-knowledge-indexer');
|
|
6
6
|
const { DomainError } = require('./runtime/contracts');
|
|
7
7
|
|
|
8
8
|
class EngineRetrieval extends KnowledgeRetrievalService {
|
|
@@ -45,15 +45,22 @@ class EngineRetrieval extends KnowledgeRetrievalService {
|
|
|
45
45
|
return this.store.state.sharedScopes.filter(s => s.brainId === registration.brainId && (s.kind === 'brain' || range === 'brain' || registration.domainId && s.domainId === registration.domainId)).map(s => ({ projectId: s.id, spaceId: `${s.kind}:${s.id}`, knowledgePath: s.knowledgePath, weight: s.kind === 'brain' ? 0.6 : 0.75, reason: s.kind }));
|
|
46
46
|
}
|
|
47
47
|
scanCurrent(scopes) {
|
|
48
|
+
const registration = this.store.getProject(scopes.find(s => s.reason === 'primary')?.projectId);
|
|
48
49
|
const ordinary = scopes.filter(s => this.store.getProject(s.projectId));
|
|
49
50
|
const shared = scopes.filter(s => !this.store.getProject(s.projectId));
|
|
50
51
|
const result = super.scanCurrent(ordinary);
|
|
52
|
+
for (const [id, candidate] of result.candidates) if (candidate.entryId.startsWith('memories/')) result.candidates.delete(id);
|
|
51
53
|
if (!shared.length) return result;
|
|
52
54
|
const scanner = new KnowledgeRetrievalService({ layout: this.layout, projectStore: { readState: () => ({ index: {} }) } });
|
|
53
55
|
const extra = scanner.scanCurrent(shared);
|
|
54
56
|
for (const [id, candidate] of extra.candidates) {
|
|
55
57
|
const scope = this.store.state.sharedScopes.find(s => s.id === candidate.projectId);
|
|
56
|
-
if (candidate.entryId.startsWith('memories/')
|
|
58
|
+
if (candidate.entryId.startsWith('memories/')) {
|
|
59
|
+
if (!this.memories?.readableEntry(scope, candidate.entryId, registration) || !this.memories.memoryForChunk(scope, candidate.entryId, candidate.headingPath)) continue;
|
|
60
|
+
const memory = this.memories.memoryForChunk(scope, candidate.entryId, candidate.headingPath), type = this.memories.scopeOf(memory).type;
|
|
61
|
+
candidate.scopeWeight = { project: 1, domain: 0.75, global: 0.6 }[type];
|
|
62
|
+
candidate.title = `${{ project: '项目记忆', domain: '领域记忆', global: '全局记忆' }[type]} · ${this.memories.target(memory).name}`;
|
|
63
|
+
} else if (scopes.find(s => s.projectId === candidate.projectId)?.memoryOnly) continue;
|
|
57
64
|
result.candidates.set(id, candidate);
|
|
58
65
|
}
|
|
59
66
|
result.scopeHealth.push(...extra.scopeHealth);
|
|
@@ -97,12 +104,19 @@ class EngineRetrieval extends KnowledgeRetrievalService {
|
|
|
97
104
|
&& (input.scope === 'brain' || p.projectId === registration.projectId || registration.domainId && p.domainId === registration.domainId)).map(p => ({ projectId: p.projectId, spaceId: `project:${p.projectId}`, knowledgePath: p.knowledgePath, weight: p.projectId === registration.projectId ? 1 : 0.88, reason: p.projectId === registration.projectId ? 'primary' : input.scope }));
|
|
98
105
|
scopes.push(...this.sharedScopes(registration, input.scope));
|
|
99
106
|
}
|
|
107
|
+
if (!scopes.some(s => s.spaceId === `brain:${registration.brainId}`)) {
|
|
108
|
+
scopes.push(...this.sharedScopes(registration, 'domain').filter(s => s.reason === 'brain').map(s => ({ ...s, memoryOnly: true })));
|
|
109
|
+
}
|
|
100
110
|
const result = await super.search({ ...input, project, scopes });
|
|
101
111
|
result.results = result.results.map(row => {
|
|
102
112
|
const run = this.store.listRuns(row.scope_project_id).find(r => r.promotedAt && r.staging?.operations?.some(op => op.path === row.entry_id));
|
|
103
113
|
const source = this.store.getProject(row.scope_project_id) || this.store.state.sharedScopes.find(s => s.id === row.scope_project_id);
|
|
104
|
-
|
|
114
|
+
const memory = source?.kind === 'brain' && row.entry_id.startsWith('memories/') ? this.memories.memoryForChunk(source, row.entry_id, row.heading_path) : null;
|
|
115
|
+
return { ...row, projectName: source?.name || row.scope_project_id, path: row.entry_id, snippet: row.chunk_text, score: row.relevance_score, projectId: row.scope_project_id, commitSha: memory?.evidence.commitSha || run?.commitSha || null, ...(memory ? { memoryScopeType: this.memories.scopeOf(memory).type, memoryScopeId: this.memories.scopeOf(memory).id, memoryId: memory.id } : {}) };
|
|
105
116
|
});
|
|
117
|
+
const slots = result.results.map((r, i) => r.memoryId ? i : -1).filter(i => i >= 0);
|
|
118
|
+
const ordered = slots.map(i => result.results[i]).sort((a, b) => ({ project: 0, domain: 1, global: 2 }[a.memoryScopeType] - { project: 0, domain: 1, global: 2 }[b.memoryScopeType]) || b.score - a.score);
|
|
119
|
+
slots.forEach((slot, i) => { result.results[slot] = ordered[i]; });
|
|
106
120
|
return result;
|
|
107
121
|
}
|
|
108
122
|
async ask(input) {
|
|
@@ -116,10 +130,11 @@ class EngineRetrieval extends KnowledgeRetrievalService {
|
|
|
116
130
|
if (input.spaceId && !scope && input.spaceId !== `project:${registration.projectId}`) throw new DomainError('PROJECT_NOT_FOUND', '知识范围与项目归属不匹配。', { status: 404 });
|
|
117
131
|
const project = scope ? { projectId: scope.id, knowledgePath: scope.knowledgePath } : registration;
|
|
118
132
|
const entryId = String(input.entryId || input.entry || '').replace(/\\/g, '/');
|
|
133
|
+
if (isExcludedKnowledgePath(entryId)) throw new DomainError('PATH_OUTSIDE_ROOT', '会话证据与内部数据不能作为正式知识读取。', { status: 403 });
|
|
119
134
|
const target = path.resolve(project.knowledgePath, entryId);
|
|
120
135
|
if (!entryId || !/\.md$/i.test(target) || !this.layout.isPathInside(project.knowledgePath, target, { realpath: true })) throw new DomainError('PATH_OUTSIDE_ROOT', '知识文件越界。', { status: 403 });
|
|
121
136
|
if (!fs.existsSync(target) || !fs.statSync(target).isFile() || fs.lstatSync(target).isSymbolicLink()) throw new DomainError('PROJECT_NOT_FOUND', '知识文件不存在。', { status: 404 });
|
|
122
|
-
if (
|
|
137
|
+
if (entryId.startsWith('memories/') && (!scope || !this.memories?.readableEntry(scope, entryId, registration))) throw new DomainError('PROJECT_NOT_FOUND', '记忆尚未同步或不适用于当前项目。', { status: 404 });
|
|
123
138
|
return { ok: true, projectId: project.projectId, projectSlug: project.projectId, entryId, source: 'markdown', chunks: [{ chunk_text: fs.readFileSync(target, 'utf8'), entry_id: entryId, space_id: scope ? `${scope.kind}:${scope.id}` : `project:${project.projectId}` }] };
|
|
124
139
|
}
|
|
125
140
|
async history(input) {
|
|
@@ -312,7 +312,11 @@ class KnowledgePromotionService {
|
|
|
312
312
|
if (!claim || claim.projectId !== projectId || !claim.runId || !claim.commitSha) throw new DomainError('INVALID_ARGUMENT', 'Active claim is required for knowledge promotion.');
|
|
313
313
|
const operationId = input.operationId || claim.operationId || createId('op');
|
|
314
314
|
const stagingPath = this.prepareStaging(projectId, claim.runId);
|
|
315
|
-
|
|
315
|
+
// The isolated worker cannot read or write the live repository. A user may
|
|
316
|
+
// keep developing while it consumes frozen evidence; live status is neither
|
|
317
|
+
// its input nor proof of worker writes. Keep the guard for other analyzers.
|
|
318
|
+
const verifyLiveSource = this.analyzer?.sourceReadMode !== 'isolated-frozen-evidence';
|
|
319
|
+
const beforeSource = verifyLiveSource ? await gitStatus(config.repoPath) : null;
|
|
316
320
|
try {
|
|
317
321
|
if (!this.analyzer || typeof this.analyzer.runClaim !== 'function') throw new DomainError('INVALID_ARGUMENT', 'Knowledge analyzer is unavailable.', { status: 503, retryable: true });
|
|
318
322
|
const analyzerResult = await this.analyzer.runClaim({
|
|
@@ -329,7 +333,7 @@ class KnowledgePromotionService {
|
|
|
329
333
|
}),
|
|
330
334
|
});
|
|
331
335
|
if (analyzerResult && analyzerResult.ok === false) throw new DomainError('INVALID_ARGUMENT', analyzerResult.error || 'Knowledge analyzer failed.', { status: 500, retryable: true });
|
|
332
|
-
const afterSource = await gitStatus(config.repoPath);
|
|
336
|
+
const afterSource = verifyLiveSource ? await gitStatus(config.repoPath) : null;
|
|
333
337
|
if (beforeSource != null && afterSource != null && beforeSource !== afterSource) {
|
|
334
338
|
throw new DomainError('PROJECT_BUSY', 'Source repository changed during knowledge analysis.', { status: 409, retryable: true });
|
|
335
339
|
}
|