autosnippet 2.13.0 → 2.15.0

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.
@@ -0,0 +1,163 @@
1
+ /**
2
+ * Recipes API 路由
3
+ * 提供 Recipe 知识图谱关系发现等操作
4
+ *
5
+ * 说明: Recipe 的 CRUD 已由 knowledge.js 统一提供,
6
+ * 此路由仅处理 Recipe 特有的批量 AI 操作。
7
+ */
8
+
9
+ import express from 'express';
10
+ import { asyncHandler } from '../middleware/errorHandler.js';
11
+ import { getServiceContainer } from '../../injection/ServiceContainer.js';
12
+ import Logger from '../../infrastructure/logging/Logger.js';
13
+
14
+ const router = express.Router();
15
+ const logger = Logger.getInstance();
16
+
17
+ /* ═══ 进程内任务状态(单实例足够) ═══════════════════════ */
18
+
19
+ let discoverTask = {
20
+ status: 'idle', // idle | running | done | error
21
+ startedAt: null,
22
+ finishedAt: null,
23
+ discovered: 0,
24
+ totalPairs: 0,
25
+ batchErrors: 0,
26
+ error: null,
27
+ elapsed: 0,
28
+ message: null,
29
+ };
30
+
31
+ function resetTask() {
32
+ discoverTask = {
33
+ status: 'idle',
34
+ startedAt: null,
35
+ finishedAt: null,
36
+ discovered: 0,
37
+ totalPairs: 0,
38
+ batchErrors: 0,
39
+ error: null,
40
+ elapsed: 0,
41
+ message: null,
42
+ };
43
+ }
44
+
45
+ /* ═══ POST /api/v1/recipes/discover-relations ═══════════ */
46
+
47
+ /**
48
+ * 异步启动 AI 批量发现 Recipe 知识图谱关系
49
+ * Body: { batchSize?: number }
50
+ *
51
+ * 立即返回 { status: 'started' },后台执行。
52
+ * Dashboard 通过 GET /discover-relations/status 轮询进度。
53
+ */
54
+ router.post('/discover-relations', asyncHandler(async (req, res) => {
55
+ const { batchSize = 20 } = req.body;
56
+
57
+ // 如果已有任务在运行,返回当前状态
58
+ if (discoverTask.status === 'running') {
59
+ const elapsed = Math.round((Date.now() - new Date(discoverTask.startedAt).getTime()) / 1000);
60
+ return res.json({
61
+ success: true,
62
+ data: {
63
+ status: 'running',
64
+ startedAt: discoverTask.startedAt,
65
+ elapsed,
66
+ message: 'AI 分析仍在进行中',
67
+ },
68
+ });
69
+ }
70
+
71
+ // 检查 ChatAgent 是否可用
72
+ const container = getServiceContainer();
73
+ let chatAgent;
74
+ try {
75
+ chatAgent = container.get('chatAgent');
76
+ } catch {
77
+ return res.json({
78
+ success: true,
79
+ data: { status: 'error', error: 'ChatAgent 不可用,请检查 AI Provider 配置' },
80
+ });
81
+ }
82
+
83
+ // 快速检查:至少需要 2 条活跃 Recipe
84
+ try {
85
+ const knowledgeService = container.get('knowledgeService');
86
+ const { items = [], data = [] } = await knowledgeService.list(
87
+ { lifecycle: 'active' }, { page: 1, pageSize: 5 },
88
+ );
89
+ const count = items.length || data.length;
90
+ if (count < 2) {
91
+ return res.json({
92
+ success: true,
93
+ data: {
94
+ status: 'empty',
95
+ message: `只有 ${count} 条活跃 Recipe,至少需要 2 条才能分析关系`,
96
+ },
97
+ });
98
+ }
99
+ } catch {
100
+ // 如果 list 失败,继续尝试(让 runTask 给出具体错误)
101
+ }
102
+
103
+ // 重置并启动后台任务
104
+ resetTask();
105
+ discoverTask.status = 'running';
106
+ discoverTask.startedAt = new Date().toISOString();
107
+
108
+ // 异步执行,不 await
109
+ (async () => {
110
+ try {
111
+ const result = await chatAgent.runTask('discover_all_relations', { batchSize });
112
+ discoverTask.status = 'done';
113
+ discoverTask.finishedAt = new Date().toISOString();
114
+ discoverTask.discovered = result.discovered || 0;
115
+ discoverTask.totalPairs = result.totalPairs || 0;
116
+ discoverTask.batchErrors = result.batchErrors || 0;
117
+ discoverTask.elapsed = Math.round(
118
+ (new Date(discoverTask.finishedAt).getTime() - new Date(discoverTask.startedAt).getTime()) / 1000,
119
+ );
120
+ logger.info('Discover relations completed', {
121
+ discovered: discoverTask.discovered,
122
+ totalPairs: discoverTask.totalPairs,
123
+ batchErrors: discoverTask.batchErrors,
124
+ elapsed: discoverTask.elapsed,
125
+ });
126
+ } catch (err) {
127
+ discoverTask.status = 'error';
128
+ discoverTask.finishedAt = new Date().toISOString();
129
+ discoverTask.error = err.message;
130
+ discoverTask.elapsed = Math.round(
131
+ (new Date(discoverTask.finishedAt).getTime() - new Date(discoverTask.startedAt).getTime()) / 1000,
132
+ );
133
+ logger.error('Discover relations failed', { error: err.message });
134
+ }
135
+ })();
136
+
137
+ res.json({
138
+ success: true,
139
+ data: {
140
+ status: 'started',
141
+ startedAt: discoverTask.startedAt,
142
+ message: 'AI 分析已启动,正在后台运行',
143
+ },
144
+ });
145
+ }));
146
+
147
+ /* ═══ GET /api/v1/recipes/discover-relations/status ═════ */
148
+
149
+ /**
150
+ * 查询关系发现任务状态
151
+ */
152
+ router.get('/discover-relations/status', asyncHandler(async (req, res) => {
153
+ const data = { ...discoverTask };
154
+
155
+ // 计算实时 elapsed
156
+ if (data.status === 'running' && data.startedAt) {
157
+ data.elapsed = Math.round((Date.now() - new Date(data.startedAt).getTime()) / 1000);
158
+ }
159
+
160
+ res.json({ success: true, data });
161
+ }));
162
+
163
+ export default router;
@@ -1,4 +1,5 @@
1
1
  import Logger from '../../infrastructure/logging/Logger.js';
2
+ import { unixNow } from '../../shared/utils/common.js';
2
3
 
3
4
  /** Only allow safe SQL identifier characters: letters, digits, underscore */
4
5
  const SAFE_IDENTIFIER_RE = /^[a-zA-Z_][a-zA-Z0-9_]*$/;
@@ -180,7 +181,7 @@ export class BaseRepository {
180
181
  `;
181
182
 
182
183
  const stmt = this.db.prepare(query);
183
- stmt.run(...updateValues, Math.floor(Date.now() / 1000), id);
184
+ stmt.run(...updateValues, unixNow(), id);
184
185
 
185
186
  return this.findById(id);
186
187
  } catch (error) {
@@ -1,6 +1,7 @@
1
1
  import { BaseRepository } from '../base/BaseRepository.js';
2
2
  import { KnowledgeEntry, Lifecycle, inferKind } from '../../domain/knowledge/index.js';
3
3
  import Logger from '../../infrastructure/logging/Logger.js';
4
+ import { safeJsonParse, safeJsonStringify, unixNow } from '../../shared/utils/common.js';
4
5
 
5
6
  /**
6
7
  * KnowledgeRepositoryImpl — 统一知识实体仓储实现
@@ -57,7 +58,7 @@ export class KnowledgeRepositoryImpl extends BaseRepository {
57
58
  const row = this._entityToRow(updates);
58
59
  delete row.id;
59
60
  delete row.createdAt;
60
- row.updatedAt = Math.floor(Date.now() / 1000);
61
+ row.updatedAt = unixNow();
61
62
 
62
63
  const setClauses = Object.keys(row).map(k => `${k} = ?`).join(', ');
63
64
  this.db.prepare(`UPDATE knowledge_entries SET ${setClauses} WHERE id = ?`)
@@ -69,7 +70,7 @@ export class KnowledgeRepositoryImpl extends BaseRepository {
69
70
  const merged = KnowledgeEntry.fromJSON({
70
71
  ...existing.toJSON(),
71
72
  ...updates,
72
- updatedAt: Math.floor(Date.now() / 1000),
73
+ updatedAt: unixNow(),
73
74
  });
74
75
  const row = this._entityToRow(merged);
75
76
  delete row.id;
@@ -248,17 +249,17 @@ export class KnowledgeRepositoryImpl extends BaseRepository {
248
249
  return new KnowledgeEntry({
249
250
  ...row,
250
251
  // JSON 列需要 parse
251
- lifecycleHistory: this._parseJson(row.lifecycleHistory, []),
252
- tags: this._parseJson(row.tags, []),
253
- content: this._parseJson(row.content, {}),
254
- relations: this._parseJson(row.relations, {}),
255
- constraints: this._parseJson(row.constraints, {}),
256
- reasoning: this._parseJson(row.reasoning, {}),
257
- quality: this._parseJson(row.quality, {}),
258
- stats: this._parseJson(row.stats, {}),
259
- headers: this._parseJson(row.headers, []),
260
- headerPaths: this._parseJson(row.headerPaths, []),
261
- agentNotes: this._parseJson(row.agentNotes, null),
252
+ lifecycleHistory: safeJsonParse(row.lifecycleHistory, []),
253
+ tags: safeJsonParse(row.tags, []),
254
+ content: safeJsonParse(row.content, {}),
255
+ relations: safeJsonParse(row.relations, {}),
256
+ constraints: safeJsonParse(row.constraints, {}),
257
+ reasoning: safeJsonParse(row.reasoning, {}),
258
+ quality: safeJsonParse(row.quality, {}),
259
+ stats: safeJsonParse(row.stats, {}),
260
+ headers: safeJsonParse(row.headers, []),
261
+ headerPaths: safeJsonParse(row.headerPaths, []),
262
+ agentNotes: safeJsonParse(row.agentNotes, null),
262
263
  // SQLite INTEGER → boolean
263
264
  autoApprovable: !!row.autoApprovable,
264
265
  includeHeaders: !!row.includeHeaders,
@@ -271,13 +272,13 @@ export class KnowledgeRepositoryImpl extends BaseRepository {
271
272
  * @returns {Object}
272
273
  */
273
274
  _entityToRow(e) {
274
- const now = Math.floor(Date.now() / 1000);
275
+ const now = unixNow();
275
276
  return {
276
277
  id: e.id,
277
278
  title: e.title,
278
279
  description: e.description || '',
279
280
  lifecycle: e.lifecycle,
280
- lifecycleHistory: JSON.stringify(e.lifecycleHistory || []),
281
+ lifecycleHistory: safeJsonStringify(e.lifecycleHistory || [], '[]'),
281
282
  autoApprovable: e.autoApprovable ? 1 : 0,
282
283
  language: e.language,
283
284
  category: e.category,
@@ -286,24 +287,24 @@ export class KnowledgeRepositoryImpl extends BaseRepository {
286
287
  complexity: e.complexity || 'intermediate',
287
288
  scope: e.scope || null,
288
289
  difficulty: e.difficulty || null,
289
- tags: JSON.stringify(e.tags || []),
290
+ tags: safeJsonStringify(e.tags || [], '[]'),
290
291
  trigger: e.trigger || '',
291
292
  topicHint: e.topicHint || '',
292
293
  whenClause: e.whenClause || '',
293
294
  doClause: e.doClause || '',
294
295
  dontClause: e.dontClause || '',
295
296
  coreCode: e.coreCode || '',
296
- content: JSON.stringify(typeof e.content?.toJSON === 'function' ? e.content.toJSON() : (e.content || {})),
297
- relations: JSON.stringify(typeof e.relations?.toJSON === 'function' ? e.relations.toJSON() : (e.relations || {})),
298
- constraints: JSON.stringify(typeof e.constraints?.toJSON === 'function' ? e.constraints.toJSON() : (e.constraints || {})),
299
- reasoning: JSON.stringify(typeof e.reasoning?.toJSON === 'function' ? e.reasoning.toJSON() : (e.reasoning || {})),
300
- quality: JSON.stringify(typeof e.quality?.toJSON === 'function' ? e.quality.toJSON() : (e.quality || {})),
301
- stats: JSON.stringify(typeof e.stats?.toJSON === 'function' ? e.stats.toJSON() : (e.stats || {})),
302
- headers: JSON.stringify(e.headers || []),
303
- headerPaths: JSON.stringify(e.headerPaths || []),
297
+ content: safeJsonStringify(e.content || {}),
298
+ relations: safeJsonStringify(e.relations || {}),
299
+ constraints: safeJsonStringify(e.constraints || {}),
300
+ reasoning: safeJsonStringify(e.reasoning || {}),
301
+ quality: safeJsonStringify(e.quality || {}),
302
+ stats: safeJsonStringify(e.stats || {}),
303
+ headers: safeJsonStringify(e.headers || [], '[]'),
304
+ headerPaths: safeJsonStringify(e.headerPaths || [], '[]'),
304
305
  moduleName: e.moduleName || null,
305
306
  includeHeaders: e.includeHeaders ? 1 : 0,
306
- agentNotes: e.agentNotes ? JSON.stringify(e.agentNotes) : null,
307
+ agentNotes: e.agentNotes ? safeJsonStringify(e.agentNotes) : null,
307
308
  aiInsight: e.aiInsight || null,
308
309
  reviewedBy: e.reviewedBy || null,
309
310
  reviewedAt: e.reviewedAt || null,
@@ -326,13 +327,6 @@ export class KnowledgeRepositoryImpl extends BaseRepository {
326
327
  _mapRowToEntity(row) {
327
328
  return this._rowToEntity(row);
328
329
  }
329
-
330
- /** @private 安全解析 JSON */
331
- _parseJson(value, fallback) {
332
- if (!value || value === 'null') return fallback;
333
- if (typeof value === 'object') return value;
334
- try { return JSON.parse(value); } catch { return fallback; }
335
- }
336
330
  }
337
331
 
338
332
  export default KnowledgeRepositoryImpl;
@@ -14,6 +14,7 @@ import { readFileSync, accessSync, statSync } from 'node:fs';
14
14
  import { basename, join, normalize } from 'node:path';
15
15
  import { detectTriggers, REGEX } from './DirectiveDetector.js';
16
16
  import { saveEventFilter } from './SaveEventFilter.js';
17
+ import { FILE_WATCHER } from '../../shared/constants.js';
17
18
 
18
19
  /* ── Handler imports ── */
19
20
  import { handleCreate } from './handlers/CreateHandler.js';
@@ -31,7 +32,7 @@ const IGNORED = [
31
32
  '**/xcuserdata/**', '**/.build/**', '**/*.swp', '**/*.tmp', '**/*~.m', '**/*~.h',
32
33
  '**/DerivedData/**', '**/Pods/**', '**/Carthage/**',
33
34
  ];
34
- const DEBOUNCE_DELAY = 300;
35
+ const DEBOUNCE_DELAY = FILE_WATCHER.DEBOUNCE_DELAY_MS;
35
36
 
36
37
  /* ────────── FileWatcher ────────── */
37
38
 
@@ -76,10 +77,10 @@ export class FileWatcher {
76
77
  ignored: IGNORED,
77
78
  ignoreInitial: true,
78
79
  persistent: true,
79
- awaitWriteFinish: { stabilityThreshold: 500, pollInterval: 100 },
80
+ awaitWriteFinish: { stabilityThreshold: FILE_WATCHER.STABILITY_THRESHOLD_MS, pollInterval: FILE_WATCHER.POLL_INTERVAL_MS },
80
81
  usePolling: process.env.ASD_WATCH_POLLING === 'true',
81
- interval: 100,
82
- binaryInterval: 300,
82
+ interval: FILE_WATCHER.POLL_INTERVAL_MS,
83
+ binaryInterval: FILE_WATCHER.BINARY_INTERVAL_MS,
83
84
  });
84
85
 
85
86
  const handleEvent = (relativePath) => {
@@ -115,17 +116,29 @@ export class FileWatcher {
115
116
  }
116
117
 
117
118
  /**
118
- * 停止监听
119
+ * 停止监听,释放所有资源
119
120
  */
120
121
  async stop() {
121
122
  if (this._watcher) {
123
+ // 移除所有事件监听器,避免泄漏
124
+ this._watcher.removeAllListeners();
122
125
  await this._watcher.close();
123
126
  this._watcher = null;
124
127
  }
128
+ // 清理所有防抖定时器
125
129
  for (const timer of this._debounceTimers.values()) {
126
130
  clearTimeout(timer);
127
131
  }
128
132
  this._debounceTimers.clear();
133
+ // 清理 handler 级别的延时定时器
134
+ if (this._timeoutLink) {
135
+ clearTimeout(this._timeoutLink);
136
+ this._timeoutLink = null;
137
+ }
138
+ if (this._timeoutHead) {
139
+ clearTimeout(this._timeoutHead);
140
+ this._timeoutHead = null;
141
+ }
129
142
  }
130
143
 
131
144
  /* ────────── 内部:文件处理(分派到 handlers) ────────── */
@@ -134,7 +147,7 @@ export class FileWatcher {
134
147
  try {
135
148
  accessSync(fullPath);
136
149
  const stat = statSync(fullPath);
137
- if (stat.isDirectory() || stat.size > 1024 * 1024) return;
150
+ if (stat.isDirectory() || stat.size > FILE_WATCHER.MAX_FILE_SIZE_BYTES) return;
138
151
  } catch {
139
152
  return;
140
153
  }
@@ -284,6 +284,17 @@ export class WorkingMemory {
284
284
  .sort((a, b) => b.importance - a.importance);
285
285
  }
286
286
 
287
+ /**
288
+ * 清空 WorkingMemory — 释放内存
289
+ * 在 Agent execute 结束后调用,避免残留引用导致内存泄漏
290
+ */
291
+ clear() {
292
+ this.#recentObservations.length = 0;
293
+ this.#compressedObservations.length = 0;
294
+ this.#scratchpad.length = 0;
295
+ this.#totalObservations = 0;
296
+ }
297
+
287
298
  // ─── 内部 ─────────────────────────────────────────────
288
299
 
289
300
  /**
@@ -1844,6 +1844,68 @@ const submitCandidate = {
1844
1844
  },
1845
1845
  };
1846
1846
 
1847
+ // ────────────────────────────────────────────────────────────
1848
+ // 16b. save_document — 保存开发文档到知识库
1849
+ // ────────────────────────────────────────────────────────────
1850
+ const saveDocument = {
1851
+ name: 'save_document',
1852
+ description: '保存开发文档到知识库(架构设计、排查报告、决策记录、调研笔记等)。仅需 title + markdown,无需 Cursor Delivery 字段。文档自动发布,可通过 autosnippet_search 检索。',
1853
+ parameters: {
1854
+ type: 'object',
1855
+ properties: {
1856
+ title: { type: 'string', description: '文档标题' },
1857
+ markdown: { type: 'string', description: '文档 Markdown 全文' },
1858
+ description: { type: 'string', description: '一句话摘要(可选)' },
1859
+ tags: { type: 'array', items: { type: 'string' }, description: '标签: adr, debug-report, design-doc, research, performance 等' },
1860
+ scope: { type: 'string', enum: ['universal', 'project-specific'], description: '适用范围(默认 project-specific)' },
1861
+ },
1862
+ required: ['title', 'markdown'],
1863
+ },
1864
+ handler: async (params, ctx) => {
1865
+ const knowledgeService = ctx.container.get('knowledgeService');
1866
+
1867
+ const data = {
1868
+ title: params.title.trim(),
1869
+ description: params.description || '',
1870
+ knowledgeType: 'dev-document',
1871
+ kind: 'fact',
1872
+ source: 'agent',
1873
+ scope: params.scope || 'project-specific',
1874
+ tags: params.tags || [],
1875
+ content: {
1876
+ markdown: params.markdown,
1877
+ pattern: '',
1878
+ },
1879
+ trigger: '',
1880
+ doClause: '',
1881
+ dontClause: '',
1882
+ whenClause: '',
1883
+ topicHint: '',
1884
+ coreCode: '',
1885
+ reasoning: {
1886
+ whyStandard: 'Agent development document',
1887
+ sources: ['agent'],
1888
+ confidence: 0.8,
1889
+ },
1890
+ };
1891
+
1892
+ const saved = await knowledgeService.create(data, { userId: 'agent' });
1893
+
1894
+ // 自动发布(文档不需要人工审核)
1895
+ try {
1896
+ await knowledgeService.publish(saved.id, { userId: 'agent' });
1897
+ } catch { /* best effort */ }
1898
+
1899
+ return {
1900
+ id: saved.id,
1901
+ title: saved.title,
1902
+ lifecycle: 'active',
1903
+ knowledgeType: 'dev-document',
1904
+ message: `文档「${saved.title}」已保存到知识库`,
1905
+ };
1906
+ },
1907
+ };
1908
+
1847
1909
  // ────────────────────────────────────────────────────────────
1848
1910
  // 17. approve_candidate
1849
1911
  // ────────────────────────────────────────────────────────────
@@ -3273,6 +3335,7 @@ export const ALL_TOOLS = [
3273
3335
  generateGuardRule,
3274
3336
  // 生命周期操作类 (7)
3275
3337
  submitCandidate,
3338
+ saveDocument,
3276
3339
  approveCandidate,
3277
3340
  rejectCandidate,
3278
3341
  publishRecipe,
@@ -15,7 +15,9 @@ import { TopicClassifier } from './TopicClassifier.js';
15
15
  import { RulesGenerator } from './RulesGenerator.js';
16
16
  import { SkillsSyncer } from './SkillsSyncer.js';
17
17
  import { estimateTokens, BUDGET } from './TokenBudget.js';
18
+ import { KNOWLEDGE_CONFIDENCE, DELIVERY_RANK } from '../../shared/constants.js';
18
19
  import path from 'node:path';
20
+ import fs from 'node:fs';
19
21
 
20
22
  export class CursorDeliveryPipeline {
21
23
  /**
@@ -48,6 +50,7 @@ export class CursorDeliveryPipeline {
48
50
  channelA: { rulesCount: 0, tokensUsed: 0 },
49
51
  channelB: { topicCount: 0, patternsCount: 0, totalTokens: 0 },
50
52
  channelC: { synced: 0, skipped: 0, errors: 0 },
53
+ channelD: { documentsCount: 0, filesWritten: 0 },
51
54
  totalTokensUsed: 0,
52
55
  duration: 0,
53
56
  };
@@ -57,9 +60,9 @@ export class CursorDeliveryPipeline {
57
60
  const entries = await this._loadEntries();
58
61
  this.logger.info?.(`[CursorDelivery] Loaded ${entries.length} knowledge entries`);
59
62
 
60
- // 2. 分类:rules vs patterns vs facts
61
- const { rules, patterns } = this._classify(entries);
62
- this.logger.info?.(`[CursorDelivery] Classified: ${rules.length} rules, ${patterns.length} patterns`);
63
+ // 2. 分类:rules vs patterns vs facts vs documents
64
+ const { rules, patterns, documents } = this._classify(entries);
65
+ this.logger.info?.(`[CursorDelivery] Classified: ${rules.length} rules, ${patterns.length} patterns, ${documents.length} documents`);
63
66
 
64
67
  // 3. 清理旧的动态生成文件
65
68
  this.rulesGenerator.cleanDynamicFiles();
@@ -76,6 +79,10 @@ export class CursorDeliveryPipeline {
76
79
  const channelC = await this._generateChannelC();
77
80
  stats.channelC = channelC;
78
81
 
82
+ // ── Channel D: Dev Documents → references ──
83
+ const channelD = this._generateChannelD(documents);
84
+ stats.channelD = channelD;
85
+
79
86
  // 统计
80
87
  stats.totalTokensUsed = channelA.tokensUsed + channelB.totalTokens;
81
88
  stats.duration = Date.now() - startTime;
@@ -83,9 +90,10 @@ export class CursorDeliveryPipeline {
83
90
  this.logger.info?.(`[CursorDelivery] Done in ${stats.duration}ms — ` +
84
91
  `A: ${channelA.rulesCount} rules (${channelA.tokensUsed} tokens), ` +
85
92
  `B: ${channelB.topicCount} topics (${channelB.totalTokens} tokens), ` +
86
- `C: ${channelC.synced} skills synced`);
93
+ `C: ${channelC.synced} skills synced, ` +
94
+ `D: ${channelD.documentsCount} documents`);
87
95
 
88
- return { channelA, channelB, channelC, stats };
96
+ return { channelA, channelB, channelC, channelD, stats };
89
97
  } catch (error) {
90
98
  this.logger.error?.(`[CursorDelivery] Error: ${error.message}`);
91
99
  throw error;
@@ -120,10 +128,10 @@ export class CursorDeliveryPipeline {
120
128
  { page: 1, pageSize: 200 }
121
129
  );
122
130
  const pendingItems = this._extractItems(pending);
123
- // 过滤高置信度 pending(quality.confidence >= 0.7 或无 quality 字段)
131
+ // 过滤高置信度 pending(quality.confidence >= PENDING_MIN 或无 quality 字段)
124
132
  const highConfPending = pendingItems.filter(e => {
125
133
  const conf = e.quality?.confidence;
126
- return conf === undefined || conf === null || conf >= 0.7;
134
+ return conf === undefined || conf === null || conf >= KNOWLEDGE_CONFIDENCE.PENDING_MIN;
127
135
  });
128
136
  allEntries.push(...highConfPending);
129
137
  } catch (e) {
@@ -146,16 +154,23 @@ export class CursorDeliveryPipeline {
146
154
 
147
155
  /**
148
156
  * 按 kind 分类知识条目
157
+ * dev-document 类型单独分流,不进入 Channel A/B 压缩
149
158
  * @private
150
159
  */
151
160
  _classify(entries) {
152
- const rules = [], patterns = [], facts = [];
161
+ const rules = [], patterns = [], facts = [], documents = [];
153
162
  for (const entry of entries) {
154
- if (entry.kind === 'rule') rules.push(entry);
155
- else if (entry.kind === 'fact') facts.push(entry);
156
- else patterns.push(entry); // 无 kind kind='pattern' → pattern
163
+ if (entry.knowledgeType === 'dev-document') {
164
+ documents.push(entry);
165
+ } else if (entry.kind === 'rule') {
166
+ rules.push(entry);
167
+ } else if (entry.kind === 'fact') {
168
+ facts.push(entry);
169
+ } else {
170
+ patterns.push(entry); // 无 kind 或 kind='pattern' → pattern
171
+ }
157
172
  }
158
- return { rules, patterns, facts };
173
+ return { rules, patterns, facts, documents };
159
174
  }
160
175
 
161
176
  /**
@@ -176,10 +191,10 @@ export class CursorDeliveryPipeline {
176
191
  */
177
192
  _rankScore(entry) {
178
193
  let score = 0;
179
- score += (entry.quality?.confidence || 0.5) * 50;
180
- score += (entry.quality?.authorityScore || 0) * 30;
181
- score += Math.min(entry.stats?.useCount || 0, 10) * 2;
182
- if (entry.lifecycle === 'active') score += 10;
194
+ score += (entry.quality?.confidence || KNOWLEDGE_CONFIDENCE.RANK_DEFAULT) * DELIVERY_RANK.CONFIDENCE_WEIGHT;
195
+ score += (entry.quality?.authorityScore || 0) * DELIVERY_RANK.AUTHORITY_WEIGHT;
196
+ score += Math.min(entry.stats?.useCount || 0, DELIVERY_RANK.USE_COUNT_MAX) * DELIVERY_RANK.USE_COUNT_WEIGHT;
197
+ if (entry.lifecycle === 'active') score += DELIVERY_RANK.ACTIVE_BONUS;
183
198
  return score;
184
199
  }
185
200
 
@@ -267,6 +282,98 @@ export class CursorDeliveryPipeline {
267
282
  }
268
283
  }
269
284
 
285
+ /**
286
+ * Channel D — Dev Documents 生成
287
+ * 将 knowledgeType='dev-document' 的条目以原始 MD 写入
288
+ * .cursor/skills/autosnippet-devdocs/references/ 目录
289
+ * @private
290
+ */
291
+ _generateChannelD(documents) {
292
+ const result = { documentsCount: 0, filesWritten: 0, filePaths: [] };
293
+ if (!documents || documents.length === 0) {
294
+ return result;
295
+ }
296
+
297
+ const devdocsDir = path.join(this.projectRoot, '.cursor', 'skills', 'autosnippet-devdocs');
298
+ const refsDir = path.join(devdocsDir, 'references');
299
+ fs.mkdirSync(refsDir, { recursive: true });
300
+
301
+ // 生成 SKILL.md(索引页)
302
+ const skillLines = [
303
+ '---',
304
+ 'name: autosnippet-devdocs',
305
+ `description: "Development documents and knowledge artifacts for ${this.projectName}. Use when looking up architecture decisions, debug reports, design docs, or analysis notes."`,
306
+ '---',
307
+ '',
308
+ `# Dev Documents — ${this.projectName}`,
309
+ '',
310
+ 'Use this skill when:',
311
+ '- Looking up architecture decisions or design rationale',
312
+ '- Reviewing debug reports or performance analysis',
313
+ '- Finding previous research or investigation notes',
314
+ '- Understanding project-specific decisions and trade-offs',
315
+ '',
316
+ '## Document Index',
317
+ '',
318
+ '| Title | Tags | Updated |',
319
+ '|-------|------|---------|',
320
+ ];
321
+
322
+ for (const doc of documents) {
323
+ const tags = (doc.tags || []).join(', ') || '-';
324
+ const updated = doc.updatedAt
325
+ ? new Date(doc.updatedAt * 1000).toISOString().split('T')[0]
326
+ : '-';
327
+ const slug = this._slugify(doc.title || doc.id);
328
+ skillLines.push(`| [${doc.title}](references/${slug}.md) | ${tags} | ${updated} |`);
329
+
330
+ // 写入单个文档 MD
331
+ const markdown = doc.content?.markdown || doc.description || '';
332
+ const docContent = [
333
+ `# ${doc.title || 'Untitled'}`,
334
+ '',
335
+ doc.description ? `> ${doc.description}` : '',
336
+ '',
337
+ `**Tags:** ${tags} `,
338
+ `**Scope:** ${doc.scope || 'universal'} `,
339
+ `**Created:** ${doc.createdAt ? new Date(doc.createdAt * 1000).toISOString().split('T')[0] : '-'}`,
340
+ '',
341
+ '---',
342
+ '',
343
+ markdown,
344
+ ].filter(Boolean).join('\n');
345
+
346
+ const docPath = path.join(refsDir, `${slug}.md`);
347
+ fs.writeFileSync(docPath, docContent, 'utf8');
348
+ result.filePaths.push(docPath);
349
+ result.filesWritten++;
350
+ }
351
+
352
+ skillLines.push('');
353
+ skillLines.push('## Deeper Knowledge');
354
+ skillLines.push('');
355
+ skillLines.push('For full-text search across all documents:');
356
+ skillLines.push('- `autosnippet_search("your query")`');
357
+
358
+ fs.writeFileSync(path.join(devdocsDir, 'SKILL.md'), skillLines.join('\n') + '\n', 'utf8');
359
+ result.documentsCount = documents.length;
360
+
361
+ this.logger.info?.(`[CursorDelivery] Channel D: ${result.documentsCount} documents → ${refsDir}`);
362
+ return result;
363
+ }
364
+
365
+ /**
366
+ * 文件名安全 slug 化
367
+ * @private
368
+ */
369
+ _slugify(text) {
370
+ return text
371
+ .toLowerCase()
372
+ .replace(/[^a-z0-9\u4e00-\u9fff]+/g, '-')
373
+ .replace(/^-+|-+$/g, '')
374
+ .substring(0, 80) || 'untitled';
375
+ }
376
+
270
377
  /**
271
378
  * 从项目路径推断项目名称
272
379
  * @private