autosnippet 3.2.4 → 3.2.6

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.
Files changed (64) hide show
  1. package/README.md +2 -4
  2. package/bin/cli.js +164 -145
  3. package/config/constitution.yaml +2 -0
  4. package/dashboard/dist/assets/{index-DNOHYBhy.css → index-BaGY7kJI.css} +1 -1
  5. package/dashboard/dist/assets/{index-6itPuGFl.js → index-DfHY_3ln.js} +25 -25
  6. package/dashboard/dist/index.html +2 -2
  7. package/lib/cli/CliLogger.js +78 -0
  8. package/lib/cli/SetupService.js +9 -718
  9. package/lib/cli/UpgradeService.js +23 -398
  10. package/lib/cli/deploy/FileDeployer.js +562 -0
  11. package/lib/cli/deploy/FileManifest.js +272 -0
  12. package/lib/external/mcp/McpServer.js +22 -26
  13. package/lib/external/mcp/autoApproveInjector.js +1 -0
  14. package/lib/external/mcp/handlers/bootstrap/BootstrapSession.js +5 -5
  15. package/lib/external/mcp/handlers/bootstrap/pipeline/EpisodicMemory.js +25 -3
  16. package/lib/external/mcp/handlers/bootstrap/pipeline/IncrementalBootstrap.js +6 -6
  17. package/lib/external/mcp/handlers/bootstrap/pipeline/ToolResultCache.js +4 -0
  18. package/lib/external/mcp/handlers/bootstrap/pipeline/dimension-configs.js +5 -5
  19. package/lib/external/mcp/handlers/bootstrap/pipeline/orchestrator.js +89 -44
  20. package/lib/external/mcp/handlers/consolidated.js +8 -9
  21. package/lib/external/mcp/handlers/dimension-complete-external.js +4 -4
  22. package/lib/external/mcp/handlers/guard.js +283 -5
  23. package/lib/external/mcp/handlers/task.js +183 -9
  24. package/lib/external/mcp/tools.js +32 -81
  25. package/lib/http/routes/task.js +55 -0
  26. package/lib/service/chat/AnalystAgent.js +12 -12
  27. package/lib/service/chat/ChatAgent.js +227 -545
  28. package/lib/service/chat/ChatAgentPrompts.js +9 -11
  29. package/lib/service/chat/ContextWindow.js +2 -296
  30. package/lib/service/chat/EpisodicConsolidator.js +15 -15
  31. package/lib/service/chat/ExplorationTracker.js +1262 -0
  32. package/lib/service/chat/HandoffProtocol.js +8 -9
  33. package/lib/service/chat/Memory.js +4 -0
  34. package/lib/service/chat/ProducerAgent.js +9 -6
  35. package/lib/service/chat/ProjectSemanticMemory.js +4 -0
  36. package/lib/service/chat/ReasoningTrace.js +182 -0
  37. package/lib/service/chat/WorkingMemory.js +4 -0
  38. package/lib/service/chat/memory/ActiveContext.js +910 -0
  39. package/lib/service/chat/memory/MemoryCoordinator.js +662 -0
  40. package/lib/service/chat/memory/PersistentMemory.js +450 -0
  41. package/lib/service/chat/memory/SessionStore.js +896 -0
  42. package/lib/service/chat/memory/index.js +13 -0
  43. package/lib/service/chat/tools/ast-graph.js +17 -16
  44. package/lib/service/cursor/AgentInstructionsGenerator.js +76 -47
  45. package/lib/service/cursor/FileProtection.js +4 -1
  46. package/lib/service/guard/GuardCheckEngine.js +10 -3
  47. package/lib/service/task/TaskGraphService.js +3 -3
  48. package/lib/shared/LanguageService.js +2 -1
  49. package/package.json +1 -1
  50. package/skills/autosnippet-intent/SKILL.md +1 -3
  51. package/skills/autosnippet-recipes/SKILL.md +1 -3
  52. package/templates/claude-code/commands/prime.md +19 -0
  53. package/templates/claude-code/hooks/autosnippet-session.sh +63 -0
  54. package/templates/claude-code/settings.json +21 -0
  55. package/templates/copilot-instructions.md +64 -177
  56. package/templates/cursor-hooks/commands/prime.md +12 -0
  57. package/templates/cursor-hooks/hooks/session-start.sh +10 -0
  58. package/templates/cursor-hooks/hooks.json +11 -0
  59. package/templates/cursor-rules/autosnippet-conventions.mdc +52 -3
  60. package/templates/cursor-rules/autosnippet-workflow.mdc +51 -27
  61. package/lib/external/mcp/handlers/decide.js +0 -109
  62. package/lib/external/mcp/handlers/ready.js +0 -42
  63. package/lib/service/chat/ReasoningLayer.js +0 -888
  64. package/templates/claude-hooks.yaml +0 -19
@@ -0,0 +1,896 @@
1
+ /**
2
+ * SessionStore — Bootstrap 会话级存储 (合并 EpisodicMemory + ToolResultCache)
3
+ *
4
+ * 设计来源: docs/copilot/memory-system-redesign.md §4.4, §6.3
5
+ *
6
+ * 内部子系统:
7
+ * 1. DimensionReports — 跨维度分析报告 + 结构化证据 + 交叉引用 (from EpisodicMemory)
8
+ * 2. ReadOnlyCache — 只读工具结果缓存 (from ToolResultCache, 排除副作用工具 B3 fix)
9
+ *
10
+ * 替代关系:
11
+ * EpisodicMemory.js → 全部维度报告/证据/反思逻辑
12
+ * ToolResultCache.js → LRU 缓存逻辑 (仅只读工具)
13
+ *
14
+ * 新增能力 (vs 原模块):
15
+ * - getDistilledForProducer(dimId): Producer 专用蒸馏上下文 (B2 fix)
16
+ * - NON_CACHEABLE 内置: 副作用工具自动排除 (B3 fix)
17
+ * - buildContextForDimension 增强: 消费 workingMemoryDistilled (B1 fix, 已在 EpisodicMemory 修复)
18
+ * - 统一的 getStats(): 合并维度 + 缓存统计
19
+ *
20
+ * 生命周期: 与 Bootstrap 会话一致。
21
+ * 持久化: 通过 saveCheckpoint / loadCheckpoint 实现断点续传。
22
+ *
23
+ * @module SessionStore
24
+ */
25
+
26
+ import fs from 'node:fs';
27
+ import path from 'node:path';
28
+ import Logger from '../../../infrastructure/logging/Logger.js';
29
+ import { CACHE } from '../../../shared/constants.js';
30
+
31
+ // ── 常量 ──
32
+
33
+ /** 副作用工具 — 不缓存结果 (B3 fix) */
34
+ const NON_CACHEABLE = new Set([
35
+ 'submit_knowledge',
36
+ 'submit_with_check',
37
+ 'note_finding',
38
+ 'get_previous_analysis',
39
+ 'get_previous_evidence',
40
+ ]);
41
+
42
+ /** 缓存上限 */
43
+ const MAX_FILE_CACHE = CACHE.MAX_FILE_ENTRIES;
44
+ const MAX_SEARCH_CACHE = CACHE.MAX_SEARCH_ENTRIES;
45
+ const DEFAULT_TTL_MS = CACHE.DEFAULT_TTL_MS;
46
+
47
+ // ── 类型定义 ──
48
+
49
+ /**
50
+ * @typedef {object} DimensionReport
51
+ * @property {string} dimId
52
+ * @property {number} completedAt
53
+ * @property {string} analysisText
54
+ * @property {Array<Finding>} findings
55
+ * @property {string[]} referencedFiles
56
+ * @property {Array<CandidateSummary>} candidatesSummary
57
+ * @property {object|null} workingMemoryDistilled
58
+ * @property {object|null} digest
59
+ */
60
+
61
+ /**
62
+ * @typedef {object} Finding
63
+ * @property {string} finding
64
+ * @property {string} [evidence]
65
+ * @property {number} importance
66
+ */
67
+
68
+ /**
69
+ * @typedef {object} CandidateSummary
70
+ * @property {string} dimId
71
+ * @property {string} title
72
+ * @property {string} subTopic
73
+ * @property {string} summary
74
+ */
75
+
76
+ /**
77
+ * @typedef {object} CrossReference
78
+ * @property {string} from
79
+ * @property {string} to
80
+ * @property {string} relation
81
+ * @property {string} detail
82
+ */
83
+
84
+ /**
85
+ * @typedef {object} TierReflection
86
+ * @property {number} tierIndex
87
+ * @property {string[]} completedDimensions
88
+ * @property {Array<Finding>} topFindings
89
+ * @property {string[]} crossDimensionPatterns
90
+ * @property {string[]} suggestionsForNextTier
91
+ */
92
+
93
+ // ═══════════════════════════════════════════════════════════
94
+ export class SessionStore {
95
+ // ── 子系统 1: DimensionReports (from EpisodicMemory) ──
96
+ /** @type {Map<string, DimensionReport>} */
97
+ #dimensionReports = new Map();
98
+ /** @type {Map<string, Finding[]>} filePath → Evidence[] */
99
+ #evidenceStore = new Map();
100
+ /** @type {CrossReference[]} */
101
+ #crossReferences = [];
102
+ /** @type {TierReflection[]} */
103
+ #tierReflections = [];
104
+ /** @type {Map<string, CandidateSummary[]>} dimId → candidates */
105
+ #submittedCandidates = new Map();
106
+ /** @type {object} */
107
+ #projectContext;
108
+
109
+ // ── 子系统 2: ReadOnlyCache (from ToolResultCache) ──
110
+ /** @type {Map<string, {result: any, cachedAt: number, hitCount: number}>} */
111
+ #searchCache = new Map();
112
+ /** @type {Map<string, {content: string, cachedAt: number, hitCount: number}>} */
113
+ #fileCache = new Map();
114
+ /** @type {{hits: number, misses: number, evictions: number}} */
115
+ #cacheStats = { hits: 0, misses: 0, evictions: 0 };
116
+ /** @type {number} */
117
+ #ttlMs;
118
+ /** @type {ReturnType<typeof setInterval>|null} */
119
+ #cleanupTimer = null;
120
+
121
+ /** @type {import('../../../infrastructure/logging/Logger.js').default} */
122
+ #logger;
123
+
124
+ /**
125
+ * @param {object} [config]
126
+ * @param {object} [config.projectContext] - 项目基础信息
127
+ * @param {number} [config.ttlMs] - 缓存 TTL(毫秒)
128
+ * @param {number} [config.cleanupIntervalMs] - 清理周期
129
+ */
130
+ constructor(config = {}) {
131
+ this.#projectContext = config.projectContext || {};
132
+ this.#ttlMs = config.ttlMs ?? DEFAULT_TTL_MS;
133
+ this.#logger = Logger.getInstance();
134
+
135
+ // 定期清理过期缓存条目
136
+ const cleanupInterval = config.cleanupIntervalMs ?? 5 * 60 * 1000;
137
+ if (this.#ttlMs > 0 && cleanupInterval > 0) {
138
+ this.#cleanupTimer = setInterval(() => this.#evictExpired(), cleanupInterval);
139
+ if (this.#cleanupTimer.unref) {
140
+ this.#cleanupTimer.unref();
141
+ }
142
+ }
143
+ }
144
+
145
+ // ═══════════════════════════════════════════════════════
146
+ // §1: 维度报告 (from EpisodicMemory)
147
+ // ═══════════════════════════════════════════════════════
148
+
149
+ /**
150
+ * 维度完成后存储完整报告
151
+ * @param {string} dimId
152
+ * @param {object} report
153
+ */
154
+ storeDimensionReport(dimId, report) {
155
+ // findings 统一形状: { finding: string, evidence: string, importance: number }
156
+ // 源头 buildAnalysisArtifact() 和 ActiveContext.distill() 已保证一致
157
+ const findings = (report.findings || []).map((f) => ({
158
+ finding: f.finding || '',
159
+ evidence: f.evidence || '',
160
+ importance: f.importance || 5,
161
+ }));
162
+
163
+ this.#dimensionReports.set(dimId, {
164
+ dimId,
165
+ completedAt: Date.now(),
166
+ analysisText: report.analysisText || '',
167
+ findings,
168
+ referencedFiles: report.referencedFiles || [],
169
+ candidatesSummary: report.candidatesSummary || [],
170
+ workingMemoryDistilled: report.workingMemoryDistilled || null,
171
+ digest: report.digest || null,
172
+ });
173
+
174
+ // 自动提取文件级 Evidence
175
+ for (const f of findings) {
176
+ if (f.evidence) {
177
+ const filePath = f.evidence.split(':')[0];
178
+ this.addEvidence(filePath, {
179
+ dimId,
180
+ finding: f.finding,
181
+ importance: f.importance,
182
+ });
183
+ }
184
+ }
185
+
186
+ // 从 digest 中提取 crossRefs
187
+ if (report.digest?.crossRefs) {
188
+ for (const [targetDim, detail] of Object.entries(report.digest.crossRefs)) {
189
+ if (detail) {
190
+ this.#crossReferences.push({
191
+ from: dimId,
192
+ to: targetDim,
193
+ relation: 'suggests',
194
+ detail: String(detail),
195
+ });
196
+ }
197
+ }
198
+ }
199
+
200
+ this.#logger.info(
201
+ `[SessionStore] Stored report for "${dimId}": ` +
202
+ `${report.findings?.length || 0} findings, ` +
203
+ `${report.referencedFiles?.length || 0} files`
204
+ );
205
+ }
206
+
207
+ /**
208
+ * @param {string} dimId
209
+ * @returns {DimensionReport|undefined}
210
+ */
211
+ getDimensionReport(dimId) {
212
+ return this.#dimensionReports.get(dimId);
213
+ }
214
+
215
+ /**
216
+ * @returns {string[]}
217
+ */
218
+ getCompletedDimensions() {
219
+ return [...this.#dimensionReports.keys()];
220
+ }
221
+
222
+ // ═══════════════════════════════════════════════════════
223
+ // §2: Evidence Store
224
+ // ═══════════════════════════════════════════════════════
225
+
226
+ /**
227
+ * @param {string} filePath
228
+ * @param {object} evidence
229
+ */
230
+ addEvidence(filePath, evidence) {
231
+ if (!this.#evidenceStore.has(filePath)) {
232
+ this.#evidenceStore.set(filePath, []);
233
+ }
234
+ this.#evidenceStore.get(filePath).push({
235
+ ...evidence,
236
+ timestamp: Date.now(),
237
+ });
238
+ }
239
+
240
+ /**
241
+ * @param {string} filePath
242
+ * @returns {Finding[]}
243
+ */
244
+ getEvidenceForFile(filePath) {
245
+ return this.#evidenceStore.get(filePath) || [];
246
+ }
247
+
248
+ /**
249
+ * @param {string} query
250
+ * @param {string} [dimId]
251
+ * @returns {Array<{filePath: string, evidence: object}>}
252
+ */
253
+ searchEvidence(query, dimId) {
254
+ const results = [];
255
+ const lowerQuery = query.toLowerCase();
256
+ for (const [filePath, evidences] of this.#evidenceStore) {
257
+ for (const ev of evidences) {
258
+ if (dimId && ev.dimId !== dimId) continue;
259
+ const matchesFile = filePath.toLowerCase().includes(lowerQuery);
260
+ const matchesFinding = (ev.finding || '').toLowerCase().includes(lowerQuery);
261
+ if (matchesFile || matchesFinding) {
262
+ results.push({ filePath, evidence: ev });
263
+ }
264
+ }
265
+ }
266
+ return results.sort((a, b) => (b.evidence.importance || 5) - (a.evidence.importance || 5));
267
+ }
268
+
269
+ // ═══════════════════════════════════════════════════════
270
+ // §3: 已提交候选
271
+ // ═══════════════════════════════════════════════════════
272
+
273
+ /**
274
+ * @param {string} dimId
275
+ * @param {CandidateSummary} candidate
276
+ */
277
+ addSubmittedCandidate(dimId, candidate) {
278
+ if (!this.#submittedCandidates.has(dimId)) {
279
+ this.#submittedCandidates.set(dimId, []);
280
+ }
281
+ this.#submittedCandidates.get(dimId).push({
282
+ dimId,
283
+ title: candidate.title || '',
284
+ subTopic: candidate.subTopic || '',
285
+ summary: candidate.summary || '',
286
+ });
287
+ }
288
+
289
+ // ═══════════════════════════════════════════════════════
290
+ // §4: DimensionDigest 兼容层
291
+ // ═══════════════════════════════════════════════════════
292
+
293
+ /**
294
+ * @param {string} dimId
295
+ * @param {object} digest
296
+ */
297
+ addDimensionDigest(dimId, digest) {
298
+ const existing = this.#dimensionReports.get(dimId);
299
+ if (existing) {
300
+ existing.digest = digest;
301
+ } else {
302
+ this.#dimensionReports.set(dimId, {
303
+ dimId,
304
+ completedAt: Date.now(),
305
+ analysisText: digest.summary || '',
306
+ findings: (digest.keyFindings || []).map((f) => ({
307
+ finding: typeof f === 'string' ? f : f.finding || '',
308
+ evidence: '',
309
+ importance: 5,
310
+ })),
311
+ referencedFiles: [],
312
+ candidatesSummary: [],
313
+ workingMemoryDistilled: null,
314
+ digest,
315
+ });
316
+ }
317
+ // 提取 crossRefs
318
+ if (digest.crossRefs) {
319
+ for (const [targetDim, detail] of Object.entries(digest.crossRefs)) {
320
+ if (detail) {
321
+ const exists = this.#crossReferences.some(
322
+ (cr) => cr.from === dimId && cr.to === targetDim
323
+ );
324
+ if (!exists) {
325
+ this.#crossReferences.push({
326
+ from: dimId,
327
+ to: targetDim,
328
+ relation: 'suggests',
329
+ detail: String(detail),
330
+ });
331
+ }
332
+ }
333
+ }
334
+ }
335
+ }
336
+
337
+ // ═══════════════════════════════════════════════════════
338
+ // §5: Tier Reflection
339
+ // ═══════════════════════════════════════════════════════
340
+
341
+ /**
342
+ * @param {number} tierIndex
343
+ * @param {TierReflection} reflection
344
+ */
345
+ addTierReflection(tierIndex, reflection) {
346
+ this.#tierReflections.push(reflection);
347
+ this.#logger.info(
348
+ `[SessionStore] Tier ${tierIndex + 1} reflection: ` +
349
+ `${reflection.topFindings?.length || 0} top findings, ` +
350
+ `${reflection.crossDimensionPatterns?.length || 0} patterns`
351
+ );
352
+ }
353
+
354
+ /**
355
+ * 获取所有 TierReflection (F17: EpisodicConsolidator 需要)
356
+ * @returns {TierReflection[]}
357
+ */
358
+ getTierReflections() {
359
+ return [...this.#tierReflections];
360
+ }
361
+
362
+ /**
363
+ * @param {string} currentDimId
364
+ * @returns {string|null}
365
+ */
366
+ getRelevantReflections(currentDimId) {
367
+ if (this.#tierReflections.length === 0) return null;
368
+ const parts = [];
369
+ for (const ref of this.#tierReflections) {
370
+ parts.push(`### Tier ${ref.tierIndex + 1} 综合洞察`);
371
+ if (ref.topFindings?.length > 0) {
372
+ parts.push('**核心发现**:');
373
+ for (const f of ref.topFindings.slice(0, 5)) {
374
+ parts.push(`- [${f.importance || 5}/10] ${f.finding}`);
375
+ }
376
+ }
377
+ if (ref.crossDimensionPatterns?.length > 0) {
378
+ parts.push('**跨维度模式**:');
379
+ for (const p of ref.crossDimensionPatterns) parts.push(`- ${p}`);
380
+ }
381
+ if (ref.suggestionsForNextTier?.length > 0) {
382
+ parts.push('**对后续维度的建议**:');
383
+ for (const s of ref.suggestionsForNextTier) parts.push(`- ${s}`);
384
+ }
385
+ }
386
+ return parts.length > 0 ? parts.join('\n') : null;
387
+ }
388
+
389
+ // ═══════════════════════════════════════════════════════
390
+ // §6: 上下文构建 (核心: 替代 DimensionContext)
391
+ // ═══════════════════════════════════════════════════════
392
+
393
+ /**
394
+ * 构建给 Analyst 的跨维度上下文
395
+ *
396
+ * @param {string} currentDimId
397
+ * @param {string[]|object} [focusKeywordsOrOpts] — 关键词数组或 options 对象
398
+ * @returns {string}
399
+ */
400
+ buildContextForDimension(currentDimId, focusKeywordsOrOpts = []) {
401
+ // 兼容两种调用方式: (dimId, keywords[]) 或 (dimId, { focusKeywords, tokenBudget })
402
+ let focusKeywords = [];
403
+ let tokenBudget = Infinity;
404
+ if (Array.isArray(focusKeywordsOrOpts)) {
405
+ focusKeywords = focusKeywordsOrOpts;
406
+ } else if (typeof focusKeywordsOrOpts === 'object') {
407
+ focusKeywords = focusKeywordsOrOpts.focusKeywords || [];
408
+ tokenBudget = focusKeywordsOrOpts.tokenBudget || Infinity;
409
+ }
410
+
411
+ const parts = [];
412
+ const completedDims = [...this.#dimensionReports.entries()].filter(
413
+ ([id]) => id !== currentDimId
414
+ );
415
+
416
+ if (completedDims.length === 0 && this.#tierReflections.length === 0) return '';
417
+
418
+ parts.push('## 前序维度分析成果(避免重复探索)');
419
+
420
+ // §1: 前序维度的关键发现
421
+ for (const [dimId, report] of completedDims) {
422
+ parts.push(`### ${dimId}`);
423
+
424
+ if (report.digest?.summary) {
425
+ parts.push(report.digest.summary);
426
+ } else if (report.analysisText) {
427
+ parts.push(`${report.analysisText.substring(0, 300)}…`);
428
+ }
429
+
430
+ // B1 fix: 优先 findings,为空时从 workingMemoryDistilled 补充
431
+ let findings = report.findings;
432
+ if ((!findings || findings.length === 0) && report.workingMemoryDistilled?.keyFindings) {
433
+ findings = report.workingMemoryDistilled.keyFindings.map((f) => ({
434
+ finding: f.finding || '',
435
+ evidence: f.evidence || '',
436
+ importance: f.importance || 5,
437
+ }));
438
+ }
439
+
440
+ const relevantFindings = this.#selectRelevantFindings(findings, focusKeywords, 5);
441
+ if (relevantFindings.length > 0) {
442
+ parts.push('**具体发现**:');
443
+ for (const f of relevantFindings) {
444
+ let line = `- [${f.importance}/10] ${f.finding}`;
445
+ if (f.evidence) line += ` _(${f.evidence})_`;
446
+ parts.push(line);
447
+ }
448
+ }
449
+
450
+ const candidates = this.#submittedCandidates.get(dimId) || [];
451
+ if (candidates.length > 0) {
452
+ parts.push(
453
+ `已提交 ${candidates.length} 个候选: ${candidates.map((c) => c.title).join(', ')}`
454
+ );
455
+ }
456
+ }
457
+
458
+ // §2: 已读文件汇总
459
+ const allReadFiles = this.getAllReferencedFiles();
460
+ if (allReadFiles.size > 0) {
461
+ parts.push(`### 前序维度已扫描的文件 (${allReadFiles.size} 个)`);
462
+ const fileList = [...allReadFiles].slice(0, 30).join(', ');
463
+ parts.push(fileList);
464
+ if (allReadFiles.size > 30) {
465
+ parts.push(`…还有 ${allReadFiles.size - 30} 个文件`);
466
+ }
467
+ }
468
+
469
+ // §3: 跨维度引用建议
470
+ const relevantCrossRefs = this.#crossReferences.filter((cr) => cr.to === currentDimId);
471
+ if (relevantCrossRefs.length > 0) {
472
+ parts.push(`### 其他维度对 ${currentDimId} 的建议`);
473
+ for (const cr of relevantCrossRefs) {
474
+ parts.push(`- [来自 ${cr.from}] ${cr.detail}`);
475
+ }
476
+ }
477
+
478
+ // §4: Tier Reflection
479
+ const reflections = this.getRelevantReflections(currentDimId);
480
+ if (reflections) parts.push(reflections);
481
+
482
+ // Token 预算裁剪
483
+ let result = parts.join('\n');
484
+ if (tokenBudget < Infinity) {
485
+ const estimatedTokens = Math.ceil(result.length / 4);
486
+ if (estimatedTokens > tokenBudget) {
487
+ // 粗略裁剪
488
+ const maxChars = tokenBudget * 4;
489
+ result = result.substring(0, maxChars) + '\n…(truncated due to budget)';
490
+ }
491
+ }
492
+
493
+ return result;
494
+ }
495
+
496
+ /**
497
+ * 兼容 DimensionContext.buildContextForDimension 返回格式
498
+ * @param {string} currentDimId
499
+ * @returns {object}
500
+ */
501
+ buildContextSnapshot(currentDimId) {
502
+ const previousDimensions = {};
503
+ for (const [dimId, report] of this.#dimensionReports) {
504
+ if (dimId === currentDimId) continue;
505
+ previousDimensions[dimId] = report.digest || {
506
+ summary: report.analysisText?.substring(0, 300) || '',
507
+ candidateCount: report.candidatesSummary?.length || 0,
508
+ keyFindings: report.findings?.map((f) => f.finding) || [],
509
+ crossRefs: {},
510
+ gaps: [],
511
+ };
512
+ }
513
+ const submittedCandidates = [];
514
+ for (const [, candidates] of this.#submittedCandidates) {
515
+ submittedCandidates.push(...candidates);
516
+ }
517
+ return { previousDimensions, submittedCandidates };
518
+ }
519
+
520
+ // ═══════════════════════════════════════════════════════
521
+ // §7: 蒸馏上下文 (新: for ProducerAgent, B2 fix)
522
+ // ═══════════════════════════════════════════════════════
523
+
524
+ /**
525
+ * 获取维度的蒸馏上下文 (供 Producer 使用)
526
+ * @param {string} dimId
527
+ * @returns {{ keyFindings: Array, toolCallSummary: Array, referencedFiles: string[] }|null}
528
+ */
529
+ getDistilledForProducer(dimId) {
530
+ const report = this.#dimensionReports.get(dimId);
531
+ if (!report) return null;
532
+
533
+ return {
534
+ keyFindings: report.workingMemoryDistilled?.keyFindings || [],
535
+ toolCallSummary: report.workingMemoryDistilled?.toolCallSummary || [],
536
+ referencedFiles: report.referencedFiles || [],
537
+ };
538
+ }
539
+
540
+ // ═══════════════════════════════════════════════════════
541
+ // §8: 只读缓存 (from ToolResultCache, B3 fix)
542
+ // ═══════════════════════════════════════════════════════
543
+
544
+ /**
545
+ * 获取缓存的工具结果
546
+ * @param {string} toolName
547
+ * @param {object} args
548
+ * @returns {*|null}
549
+ */
550
+ getCachedResult(toolName, args) {
551
+ if (NON_CACHEABLE.has(toolName)) return null;
552
+
553
+ if (toolName === 'search_project_code') {
554
+ const pattern = args?.pattern || '';
555
+ if (pattern) {
556
+ const entry = this.#searchCache.get(pattern);
557
+ if (entry) {
558
+ if (this.#ttlMs > 0 && Date.now() - entry.cachedAt > this.#ttlMs) {
559
+ this.#searchCache.delete(pattern);
560
+ this.#cacheStats.evictions++;
561
+ this.#cacheStats.misses++;
562
+ return null;
563
+ }
564
+ entry.hitCount++;
565
+ this.#cacheStats.hits++;
566
+ return entry.result;
567
+ }
568
+ }
569
+ }
570
+ if (toolName === 'read_project_file') {
571
+ const filePath = args?.filePath || '';
572
+ if (filePath) {
573
+ const entry = this.#fileCache.get(filePath);
574
+ if (entry) {
575
+ if (this.#ttlMs > 0 && Date.now() - entry.cachedAt > this.#ttlMs) {
576
+ this.#fileCache.delete(filePath);
577
+ this.#cacheStats.evictions++;
578
+ this.#cacheStats.misses++;
579
+ return null;
580
+ }
581
+ entry.hitCount++;
582
+ this.#cacheStats.hits++;
583
+ return { content: entry.content, path: filePath, cached: true };
584
+ }
585
+ }
586
+ }
587
+ this.#cacheStats.misses++;
588
+ return null;
589
+ }
590
+
591
+ /**
592
+ * 缓存工具结果 (自动排除副作用工具)
593
+ * @param {string} toolName
594
+ * @param {object} args
595
+ * @param {*} result
596
+ */
597
+ cacheToolResult(toolName, args, result) {
598
+ if (NON_CACHEABLE.has(toolName)) return;
599
+
600
+ if (toolName === 'search_project_code') {
601
+ const pattern = args?.pattern || '';
602
+ if (pattern) {
603
+ if (this.#searchCache.size >= MAX_SEARCH_CACHE) {
604
+ const oldestKey = this.#searchCache.keys().next().value;
605
+ this.#searchCache.delete(oldestKey);
606
+ }
607
+ this.#searchCache.set(pattern, { result, cachedAt: Date.now(), hitCount: 0 });
608
+ }
609
+ }
610
+ if (toolName === 'read_project_file') {
611
+ const filePath = args?.filePath || '';
612
+ const content = typeof result === 'object' ? result.content : String(result);
613
+ if (filePath && content) {
614
+ if (this.#fileCache.size >= MAX_FILE_CACHE) {
615
+ const oldestKey = this.#fileCache.keys().next().value;
616
+ this.#fileCache.delete(oldestKey);
617
+ }
618
+ this.#fileCache.set(filePath, { content, cachedAt: Date.now(), hitCount: 0 });
619
+ }
620
+ }
621
+ }
622
+
623
+ /**
624
+ * 兼容 ToolResultCache.get()
625
+ * @param {string} toolName
626
+ * @param {object} args
627
+ * @returns {*|null}
628
+ */
629
+ get(toolName, args) {
630
+ return this.getCachedResult(toolName, args);
631
+ }
632
+
633
+ /**
634
+ * 兼容 ToolResultCache.set()
635
+ * @param {string} toolName
636
+ * @param {object} args
637
+ * @param {*} result
638
+ */
639
+ set(toolName, args, result) {
640
+ this.cacheToolResult(toolName, args, result);
641
+ }
642
+
643
+ // ═══════════════════════════════════════════════════════
644
+ // §9: 持久化 (断点续传)
645
+ // ═══════════════════════════════════════════════════════
646
+
647
+ /**
648
+ * @param {string} projectRoot
649
+ */
650
+ async saveCheckpoint(projectRoot) {
651
+ const checkpointDir = path.join(projectRoot, '.autosnippet', 'bootstrap-checkpoint');
652
+ try {
653
+ fs.mkdirSync(checkpointDir, { recursive: true });
654
+ const data = {
655
+ version: 2,
656
+ savedAt: Date.now(),
657
+ dimensionReports: Object.fromEntries(
658
+ [...this.#dimensionReports].map(([k, v]) => [
659
+ k,
660
+ {
661
+ ...v,
662
+ analysisText: v.analysisText?.substring(0, 500) || '',
663
+ },
664
+ ])
665
+ ),
666
+ crossReferences: this.#crossReferences,
667
+ tierReflections: this.#tierReflections,
668
+ submittedCandidates: Object.fromEntries(this.#submittedCandidates),
669
+ evidenceIndex: [...this.#evidenceStore.keys()],
670
+ };
671
+ fs.writeFileSync(
672
+ path.join(checkpointDir, 'session-store.json'),
673
+ JSON.stringify(data, null, 2),
674
+ 'utf-8'
675
+ );
676
+ this.#logger.info(
677
+ `[SessionStore] Checkpoint saved: ${this.#dimensionReports.size} reports`
678
+ );
679
+ } catch (err) {
680
+ this.#logger.warn(`[SessionStore] Failed to save checkpoint: ${err.message}`);
681
+ }
682
+ }
683
+
684
+ /**
685
+ * @param {string} projectRoot
686
+ * @returns {boolean}
687
+ */
688
+ async loadCheckpoint(projectRoot) {
689
+ // Try new format first, then legacy
690
+ const newPath = path.join(projectRoot, '.autosnippet', 'bootstrap-checkpoint', 'session-store.json');
691
+ const legacyPath = path.join(projectRoot, '.autosnippet', 'bootstrap-checkpoint', 'episodic-memory.json');
692
+ const checkpointPath = fs.existsSync(newPath) ? newPath : legacyPath;
693
+
694
+ try {
695
+ if (!fs.existsSync(checkpointPath)) return false;
696
+
697
+ const raw = fs.readFileSync(checkpointPath, 'utf-8');
698
+ const data = JSON.parse(raw);
699
+
700
+ if (data.version !== 1 && data.version !== 2) {
701
+ this.#logger.warn(`[SessionStore] Unsupported checkpoint version: ${data.version}`);
702
+ return false;
703
+ }
704
+ if (Date.now() - data.savedAt > 3600_000) {
705
+ this.#logger.info(`[SessionStore] Checkpoint expired (>1h), ignoring`);
706
+ return false;
707
+ }
708
+
709
+ if (data.dimensionReports) {
710
+ for (const [dimId, report] of Object.entries(data.dimensionReports)) {
711
+ this.#dimensionReports.set(dimId, report);
712
+ }
713
+ }
714
+ if (data.crossReferences) this.#crossReferences = data.crossReferences;
715
+ if (data.tierReflections) this.#tierReflections = data.tierReflections;
716
+ if (data.submittedCandidates) {
717
+ for (const [dimId, candidates] of Object.entries(data.submittedCandidates)) {
718
+ this.#submittedCandidates.set(dimId, candidates);
719
+ }
720
+ }
721
+
722
+ this.#logger.info(
723
+ `[SessionStore] Checkpoint loaded: ${this.#dimensionReports.size} reports`
724
+ );
725
+ return true;
726
+ } catch (err) {
727
+ this.#logger.warn(`[SessionStore] Failed to load checkpoint: ${err.message}`);
728
+ return false;
729
+ }
730
+ }
731
+
732
+ // ═══════════════════════════════════════════════════════
733
+ // §10: 序列化
734
+ // ═══════════════════════════════════════════════════════
735
+
736
+ toJSON() {
737
+ return {
738
+ dimensionReports: Object.fromEntries(this.#dimensionReports),
739
+ crossReferences: this.#crossReferences,
740
+ tierReflections: this.#tierReflections,
741
+ submittedCandidates: Object.fromEntries(this.#submittedCandidates),
742
+ projectContext: this.#projectContext,
743
+ };
744
+ }
745
+
746
+ static fromJSON(json) {
747
+ const store = new SessionStore({ projectContext: json.projectContext || {} });
748
+ if (json.dimensionReports) {
749
+ for (const [k, v] of Object.entries(json.dimensionReports)) {
750
+ store.#dimensionReports.set(k, v);
751
+ }
752
+ }
753
+ if (json.crossReferences) store.#crossReferences = json.crossReferences;
754
+ if (json.tierReflections) store.#tierReflections = json.tierReflections;
755
+ if (json.submittedCandidates) {
756
+ for (const [k, v] of Object.entries(json.submittedCandidates)) {
757
+ store.#submittedCandidates.set(k, v);
758
+ }
759
+ }
760
+ return store;
761
+ }
762
+
763
+ // ═══════════════════════════════════════════════════════
764
+ // §11: 统计 + 查询
765
+ // ═══════════════════════════════════════════════════════
766
+
767
+ /**
768
+ * 获取所有已引用文件 (去重, F10)
769
+ * @returns {Set<string>}
770
+ */
771
+ getAllReferencedFiles() {
772
+ const files = new Set();
773
+ for (const report of this.#dimensionReports.values()) {
774
+ for (const f of report.referencedFiles) files.add(f);
775
+ }
776
+ return files;
777
+ }
778
+
779
+ /**
780
+ * 获取统计数据 (合并维度 + 缓存统计, F12)
781
+ * @returns {object}
782
+ */
783
+ getStats() {
784
+ const totalFindings = [...this.#dimensionReports.values()].reduce(
785
+ (sum, r) => sum + r.findings.length, 0
786
+ );
787
+ const totalEvidence = [...this.#evidenceStore.values()].reduce(
788
+ (sum, arr) => sum + arr.length, 0
789
+ );
790
+ const totalCandidates = [...this.#submittedCandidates.values()].reduce(
791
+ (sum, arr) => sum + arr.length, 0
792
+ );
793
+ const { hits, misses } = this.#cacheStats;
794
+ return {
795
+ completedDimensions: this.#dimensionReports.size,
796
+ totalFindings,
797
+ totalEvidence,
798
+ totalCandidates,
799
+ crossReferences: this.#crossReferences.length,
800
+ tierReflections: this.#tierReflections.length,
801
+ referencedFiles: this.getAllReferencedFiles().size,
802
+ cache: {
803
+ ...this.#cacheStats,
804
+ hitRate: hits + misses > 0
805
+ ? `${((hits / (hits + misses)) * 100).toFixed(1)}%`
806
+ : '0%',
807
+ searchCacheSize: this.#searchCache.size,
808
+ fileCacheSize: this.#fileCache.size,
809
+ },
810
+ };
811
+ }
812
+
813
+ // ═══════════════════════════════════════════════════════
814
+ // §12: 清理
815
+ // ═══════════════════════════════════════════════════════
816
+
817
+ /**
818
+ * 清空所有缓存
819
+ */
820
+ clearCache() {
821
+ this.#searchCache.clear();
822
+ this.#fileCache.clear();
823
+ this.#cacheStats = { hits: 0, misses: 0, evictions: 0 };
824
+ }
825
+
826
+ /**
827
+ * 销毁实例,释放定时器
828
+ */
829
+ dispose() {
830
+ this.clearCache();
831
+ this.#dimensionReports.clear();
832
+ this.#evidenceStore.clear();
833
+ this.#crossReferences.length = 0;
834
+ this.#tierReflections.length = 0;
835
+ this.#submittedCandidates.clear();
836
+ if (this.#cleanupTimer) {
837
+ clearInterval(this.#cleanupTimer);
838
+ this.#cleanupTimer = null;
839
+ }
840
+ }
841
+
842
+ // ═══════════════════════════════════════════════════════
843
+ // 私有方法
844
+ // ═══════════════════════════════════════════════════════
845
+
846
+ /**
847
+ * 从 findings 中选择与当前焦点最相关的
848
+ */
849
+ #selectRelevantFindings(findings, focusKeywords, limit) {
850
+ if (!findings || findings.length === 0) return [];
851
+
852
+ if (!focusKeywords || focusKeywords.length === 0) {
853
+ return [...findings]
854
+ .sort((a, b) => (b.importance || 5) - (a.importance || 5))
855
+ .slice(0, limit);
856
+ }
857
+
858
+ return [...findings]
859
+ .map((f) => {
860
+ const relevance = focusKeywords.some((kw) =>
861
+ (f.finding || '').toLowerCase().includes(kw.toLowerCase())
862
+ ) ? 1 : 0;
863
+ return { ...f, _score: relevance * 10 + (f.importance || 5) };
864
+ })
865
+ .sort((a, b) => b._score - a._score)
866
+ .slice(0, limit)
867
+ .map(({ _score, ...rest }) => rest);
868
+ }
869
+
870
+ /**
871
+ * 清理过期缓存条目 (F13)
872
+ */
873
+ #evictExpired() {
874
+ if (this.#ttlMs <= 0) return;
875
+ const now = Date.now();
876
+ let evicted = 0;
877
+ for (const [key, entry] of this.#searchCache) {
878
+ if (now - entry.cachedAt > this.#ttlMs) {
879
+ this.#searchCache.delete(key);
880
+ evicted++;
881
+ }
882
+ }
883
+ for (const [key, entry] of this.#fileCache) {
884
+ if (now - entry.cachedAt > this.#ttlMs) {
885
+ this.#fileCache.delete(key);
886
+ evicted++;
887
+ }
888
+ }
889
+ if (evicted > 0) {
890
+ this.#cacheStats.evictions += evicted;
891
+ this.#logger.debug(`[SessionStore] evicted ${evicted} expired cache entries`);
892
+ }
893
+ }
894
+ }
895
+
896
+ export default SessionStore;